@kaddo/mcp 3.25.0 → 3.26.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 +330 -11
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2702,11 +2702,23 @@ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
2702
2702
  const graphMult = GRAPH_MULT[graphQuality] ?? 0;
2703
2703
  const archH = round2(a.architecture_discovery_hours_saved_when_graph_good * graphMult);
2704
2704
  const guard = buildGuardHistory(dir);
2705
- const driftSavings = guard.available && guard.resolved > 0 ? {
2706
- available: true,
2707
- hours: round2(guard.resolved * a.rework_hours_avoided_per_resolved_drift),
2708
- formula: `${guard.resolved} resolved drift warnings \xD7 ${a.rework_hours_avoided_per_resolved_drift} h`
2709
- } : { available: false, hours: 0, reason: "Guard history is not persisted yet." };
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
+ }
2710
2722
  const totalHours = round2(ctxPrepH + reviewH + clarH + onboardH + archH + driftSavings.hours);
2711
2723
  const totalValue = Math.round(totalHours * a.hourly_cost);
2712
2724
  const reasons = [];
@@ -2721,15 +2733,27 @@ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
2721
2733
  level = "Medium";
2722
2734
  reasons.push("Strong knowledge and graph evidence.");
2723
2735
  }
2724
- if (!guard.available) reasons.push("Guard history is not persisted yet \u2014 avoided rework is not estimated.");
2725
- else reasons.push(`Guard history available (${guard.resolved} resolved drift warning(s)).`);
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
+ }
2726
2743
  reasons.push(source === "file" ? "Assumptions come from `.kaddo/savings.yml`." : "Savings assumptions are defaults and should be calibrated.");
2727
2744
  const actions = [];
2728
2745
  if (source === "default") actions.push("Run `kaddo savings init` to customize assumptions.");
2729
2746
  actions.push("Calibrate hourly cost and team assumptions with real team data.");
2730
2747
  actions.push("Run `kaddo impact` to verify the underlying evidence quality.");
2731
2748
  actions.push("Use this report as directional evidence, not accounting data.");
2732
- actions.push("Future: persist Guard history to estimate avoided rework.");
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
+ }
2733
2757
  return {
2734
2758
  generated_at: now.toISOString(),
2735
2759
  project: impact.project,
@@ -2755,7 +2779,9 @@ function buildSavingsReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
2755
2779
  graph_nodes: impact.graph_quality.available ? impact.graph_quality.nodes : null,
2756
2780
  graph_edges: impact.graph_quality.available ? impact.graph_quality.edges : null,
2757
2781
  context_readiness: readiness,
2758
- guard_history_available: guard.available
2782
+ guard_history_available: guard.available,
2783
+ guard_runs_recorded: guard.total_runs,
2784
+ resolved_drift_warnings: guard.resolved
2759
2785
  },
2760
2786
  estimated_savings: {
2761
2787
  context_preparation: { hours: ctxPrepH, formula: `${completed} Work Items \xD7 ${a.context_preparation_minutes_saved_per_work_item} min` },
@@ -2805,7 +2831,13 @@ function renderSavingsMarkdown(r) {
2805
2831
  if (e.graph_nodes !== null) L.push(`- Graph nodes: ${e.graph_nodes}`);
2806
2832
  if (e.graph_edges !== null) L.push(`- Graph edges: ${e.graph_edges}`);
2807
2833
  L.push(`- Context readiness: ${e.context_readiness}`);
2808
- L.push("- Guard history: not available");
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
+ }
2809
2841
  L.push("");
2810
2842
  L.push("## Estimated Savings", "");
2811
2843
  const s = r.estimated_savings;
@@ -2822,7 +2854,9 @@ function renderSavingsMarkdown(r) {
2822
2854
  L.push("### Drift Prevention", "");
2823
2855
  if (s.drift_prevention.available) {
2824
2856
  L.push(`- Formula: ${s.drift_prevention.formula}`);
2825
- L.push(`- Estimated: ${s.drift_prevention.hours} h`, "");
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("");
2826
2860
  } else {
2827
2861
  L.push("- Not available yet.");
2828
2862
  L.push(`- Reason: ${s.drift_prevention.reason}`, "");
@@ -2956,6 +2990,235 @@ function serializeDriftJson(r) {
2956
2990
  return JSON.stringify(out, null, 2) + "\n";
2957
2991
  }
2958
2992
 
2993
+ // ../cli/src/core/open-questions.ts
2994
+ var SOURCES = [
2995
+ "knowledge/business/business.md",
2996
+ "knowledge/product/product.md",
2997
+ "knowledge/tech/codebase.md",
2998
+ "knowledge/delivery/roadmap.md"
2999
+ ];
3000
+ var BLOCKING = [
3001
+ "mvp",
3002
+ "stack",
3003
+ "arquitect",
3004
+ "architecture",
3005
+ "api",
3006
+ "web app",
3007
+ "webapp",
3008
+ "cli",
3009
+ "backend",
3010
+ "auth",
3011
+ "autenticaci",
3012
+ "persist",
3013
+ "base de datos",
3014
+ "database",
3015
+ "roadmap",
3016
+ "alcance",
3017
+ "scope",
3018
+ "primera versi\xF3n",
3019
+ "primera version",
3020
+ "first version",
3021
+ "fastify",
3022
+ "nestjs",
3023
+ "framework"
3024
+ ];
3025
+ var DEFERRED = [
3026
+ "integraci",
3027
+ "integration",
3028
+ "analytics",
3029
+ "notificaci",
3030
+ "notification",
3031
+ "google calendar",
3032
+ "eventbrite",
3033
+ "meetup",
3034
+ "dashboard",
3035
+ "multi-tenant",
3036
+ "multitenant",
3037
+ "mobile",
3038
+ "m\xF3vil",
3039
+ "pago",
3040
+ "payment",
3041
+ "ticketing",
3042
+ "multiple tracks",
3043
+ "m\xFAltiples tracks",
3044
+ "webhook"
3045
+ ];
3046
+ var IMPORTANT = [
3047
+ "validaci",
3048
+ "validation",
3049
+ "paginaci",
3050
+ "pagination",
3051
+ "soft delete",
3052
+ "sponsor",
3053
+ "agenda",
3054
+ "ubicaci",
3055
+ "location",
3056
+ "rol",
3057
+ "role",
3058
+ "permis",
3059
+ "permission"
3060
+ ];
3061
+ function matchesAny(text2, keywords) {
3062
+ return keywords.some((k) => text2.includes(k));
3063
+ }
3064
+ function classifyQuestion(question) {
3065
+ const t = question.toLowerCase();
3066
+ if (matchesAny(t, BLOCKING)) {
3067
+ return { classification: "blocking", reason: "Affects scope, architecture or the first Work Items." };
3068
+ }
3069
+ if (matchesAny(t, DEFERRED) && !matchesAny(t, IMPORTANT)) {
3070
+ return { classification: "deferred", reason: "Can be moved to a later phase." };
3071
+ }
3072
+ if (matchesAny(t, IMPORTANT)) {
3073
+ return { classification: "important", reason: "Relevant, but a temporary assumption can unblock progress." };
3074
+ }
3075
+ return { classification: "important", reason: "Relevant, but a temporary assumption can unblock progress." };
3076
+ }
3077
+ function extractFromMarkdown(md) {
3078
+ const lines = md.split(/\r?\n/);
3079
+ const start = lines.findIndex((l) => /^#{1,6}\s+open questions\s*$/i.test(l.trim()) || /^#{1,6}\s+preguntas abiertas\s*$/i.test(l.trim()));
3080
+ if (start < 0) return [];
3081
+ const out = [];
3082
+ for (let i = start + 1; i < lines.length; i++) {
3083
+ const line = lines[i];
3084
+ if (/^#{1,6}\s+/.test(line)) break;
3085
+ const m = line.match(/^\s*(?:[-*]|\d+\.)\s+(.+?)\s*$/);
3086
+ if (m) {
3087
+ const q = m[1].trim();
3088
+ if (q && !/^_.*_$/.test(q)) out.push(q);
3089
+ }
3090
+ }
3091
+ return out;
3092
+ }
3093
+ function suggestedAssumption(question) {
3094
+ const t = question.toLowerCase();
3095
+ if (matchesAny(t, ["api", "web app", "webapp", "cli", "backend"])) {
3096
+ return "Start as a backend API to keep the initial scope small.";
3097
+ }
3098
+ if (matchesAny(t, ["auth", "autenticaci"])) {
3099
+ return "No authentication in the MVP.";
3100
+ }
3101
+ if (matchesAny(t, ["fastify", "nestjs", "framework", "stack"])) {
3102
+ return "Choose the lighter stack option to minimize MVP scope.";
3103
+ }
3104
+ if (matchesAny(t, ["persist", "base de datos", "database"])) {
3105
+ return "Use a simple local datastore (e.g. SQLite) for the MVP.";
3106
+ }
3107
+ return "Proceed with the smallest reasonable MVP interpretation and revisit later.";
3108
+ }
3109
+ function buildOpenQuestionsReport(dir, now = /* @__PURE__ */ new Date()) {
3110
+ const project = loadConfig(dir)?.project.name ?? "unknown";
3111
+ const questions = [];
3112
+ let seq = 0;
3113
+ for (const source of SOURCES) {
3114
+ const p = join(dir, source);
3115
+ if (!exists(p)) continue;
3116
+ let md;
3117
+ try {
3118
+ md = readFile(p);
3119
+ } catch {
3120
+ continue;
3121
+ }
3122
+ for (const q of extractFromMarkdown(md)) {
3123
+ seq += 1;
3124
+ const { classification, reason } = classifyQuestion(q);
3125
+ questions.push({
3126
+ id: `OQ-${String(seq).padStart(3, "0")}`,
3127
+ source,
3128
+ section: "Open Questions",
3129
+ question: q,
3130
+ classification,
3131
+ reason,
3132
+ ...classification === "blocking" ? { suggested_assumption: suggestedAssumption(q) } : {}
3133
+ });
3134
+ }
3135
+ }
3136
+ const blocking = questions.filter((q) => q.classification === "blocking");
3137
+ const important = questions.filter((q) => q.classification === "important");
3138
+ const deferred = questions.filter((q) => q.classification === "deferred");
3139
+ let readiness;
3140
+ if (questions.length === 0) readiness = "unknown";
3141
+ else if (blocking.length > 0) readiness = "needs_decisions";
3142
+ else readiness = "ready";
3143
+ const suggested_assumptions = blocking.map((q) => q.suggested_assumption).filter(Boolean);
3144
+ const recommended_next_step = readiness === "needs_decisions" ? "Confirm suggested assumptions with the user (resolve, assume or defer blocking questions) before generating the roadmap." : readiness === "ready" ? "No blocking questions \u2014 you can proceed to the roadmap (review important questions when convenient)." : "No open questions found. Run `kaddo bootstrap` to capture early questions, or proceed.";
3145
+ return {
3146
+ generated_at: now.toISOString(),
3147
+ project,
3148
+ summary: {
3149
+ open_questions: questions.length,
3150
+ roadmap_readiness: readiness,
3151
+ blocking: blocking.length,
3152
+ important: important.length,
3153
+ deferred: deferred.length
3154
+ },
3155
+ questions,
3156
+ blocking_questions: blocking,
3157
+ important_questions: important,
3158
+ deferred_questions: deferred,
3159
+ suggested_assumptions,
3160
+ recommended_next_step
3161
+ };
3162
+ }
3163
+ function renderOpenQuestionsMarkdown(r) {
3164
+ const L = [];
3165
+ L.push("# Kaddo Open Questions Readiness Report", "");
3166
+ L.push(`Generated at: ${r.generated_at}`);
3167
+ L.push(`Project: ${r.project}`);
3168
+ L.push("");
3169
+ L.push("## Summary", "");
3170
+ L.push(`- Open questions found: ${r.summary.open_questions}`);
3171
+ L.push(`- Roadmap readiness: ${r.summary.roadmap_readiness}`);
3172
+ L.push(`- Blocking: ${r.summary.blocking}`);
3173
+ L.push(`- Important: ${r.summary.important}`);
3174
+ L.push(`- Deferred: ${r.summary.deferred}`);
3175
+ L.push("");
3176
+ if (r.summary.open_questions === 0) {
3177
+ L.push("No `## Open Questions` sections found in business / product / codebase / roadmap.", "");
3178
+ L.push("## Recommended Next Step", "", r.recommended_next_step, "");
3179
+ return L.join("\n");
3180
+ }
3181
+ L.push("## Blocking Questions", "");
3182
+ if (r.blocking_questions.length === 0) L.push("_None._", "");
3183
+ for (const q of r.blocking_questions) {
3184
+ L.push(`### ${q.id}`, "");
3185
+ L.push(`- Source: \`${q.source}\``);
3186
+ L.push(`- Question: ${q.question}`);
3187
+ L.push(`- Reason: ${q.reason}`);
3188
+ if (q.suggested_assumption) L.push(`- Suggested assumption: ${q.suggested_assumption}`);
3189
+ L.push("");
3190
+ }
3191
+ L.push("## Important Questions", "");
3192
+ if (r.important_questions.length === 0) L.push("_None._");
3193
+ for (const q of r.important_questions) L.push(`- ${q.question}`);
3194
+ L.push("");
3195
+ L.push("## Deferred Questions", "");
3196
+ if (r.deferred_questions.length === 0) L.push("_None._");
3197
+ for (const q of r.deferred_questions) L.push(`- ${q.question}`);
3198
+ L.push("");
3199
+ if (r.suggested_assumptions.length > 0) {
3200
+ L.push("## Suggested Assumptions", "");
3201
+ for (const a of r.suggested_assumptions) L.push(`- ${a}`);
3202
+ L.push("");
3203
+ }
3204
+ L.push("## Recommended Next Step", "", r.recommended_next_step, "");
3205
+ return L.join("\n");
3206
+ }
3207
+ function serializeOpenQuestionsJson(r) {
3208
+ return JSON.stringify(r, null, 2) + "\n";
3209
+ }
3210
+ function roadmapReadinessSummary(dir) {
3211
+ const r = buildOpenQuestionsReport(dir);
3212
+ return {
3213
+ roadmap_readiness: r.summary.roadmap_readiness,
3214
+ blocking_questions: r.summary.blocking,
3215
+ important_questions: r.summary.important,
3216
+ deferred_questions: r.summary.deferred,
3217
+ suggested_assumptions: r.suggested_assumptions,
3218
+ recommended_next_step: r.summary.roadmap_readiness === "needs_decisions" ? "Resolve, assume, or defer blocking questions before generating roadmap." : r.recommended_next_step
3219
+ };
3220
+ }
3221
+
2959
3222
  // src/workitems.ts
2960
3223
  import matter7 from "gray-matter";
2961
3224
  var ACTIVE_STATES2 = /* @__PURE__ */ new Set(["draft", "ready", "in-progress", "blocked"]);
@@ -3206,6 +3469,31 @@ var RESOURCES = [
3206
3469
  }
3207
3470
  return [{ uri: "kaddo://guard-history", text: runs, mimeType: "application/jsonl" }];
3208
3471
  }
3472
+ },
3473
+ {
3474
+ uri: "kaddo://open-questions",
3475
+ name: "Kaddo open questions",
3476
+ description: "Open questions from business/product/codebase/roadmap, classified for the readiness gate.",
3477
+ mimeType: "application/json",
3478
+ read: (root) => {
3479
+ if (!hasKnowledge(root)) {
3480
+ return text("kaddo://open-questions", "Knowledge repository not found. Run `kaddo bootstrap` first.", "text/plain");
3481
+ }
3482
+ const r = buildOpenQuestionsReport(root);
3483
+ return [{ uri: "kaddo://open-questions", text: JSON.stringify({ questions: r.questions }, null, 2), mimeType: "application/json" }];
3484
+ }
3485
+ },
3486
+ {
3487
+ uri: "kaddo://roadmap-readiness",
3488
+ name: "Kaddo roadmap readiness",
3489
+ description: "Decision-oriented roadmap readiness summary (blocking/important/deferred + suggested assumptions).",
3490
+ mimeType: "application/json",
3491
+ read: (root) => {
3492
+ if (!hasKnowledge(root)) {
3493
+ return text("kaddo://roadmap-readiness", "Knowledge repository not found. Run `kaddo bootstrap` first.", "text/plain");
3494
+ }
3495
+ return [{ uri: "kaddo://roadmap-readiness", text: JSON.stringify(roadmapReadinessSummary(root), null, 2), mimeType: "application/json" }];
3496
+ }
3209
3497
  }
3210
3498
  ];
3211
3499
 
@@ -4015,6 +4303,21 @@ function generateDriftReport(root, opts = {}) {
4015
4303
  next_suggested_resources: ["kaddo://drift-report"]
4016
4304
  };
4017
4305
  }
4306
+ function generateQuestionsReport(root, opts = {}) {
4307
+ requireConfig(root);
4308
+ const format = opts.format ?? "markdown";
4309
+ const report = buildOpenQuestionsReport(root);
4310
+ const content = format === "json" ? serializeOpenQuestionsJson(report) : renderOpenQuestionsMarkdown(report);
4311
+ const out = opts.output ?? `.kaddo/reports/questions-report.${format === "json" ? "json" : "md"}`;
4312
+ writeDerived(root, out, content);
4313
+ return {
4314
+ status: "ok",
4315
+ files_written: [out.replace(/\\/g, "/")],
4316
+ summary: `Open questions report generated (readiness: ${report.summary.roadmap_readiness}, ${report.summary.blocking} blocking).`,
4317
+ warnings: report.summary.blocking > 0 ? [`${report.summary.blocking} blocking question(s) before the roadmap.`] : [],
4318
+ next_suggested_resources: ["kaddo://open-questions", "kaddo://roadmap-readiness"]
4319
+ };
4320
+ }
4018
4321
  function generateCapsuleDraft(root) {
4019
4322
  const config = requireConfig(root);
4020
4323
  requireKnowledge(root);
@@ -4248,6 +4551,22 @@ function createServer(root) {
4248
4551
  () => generateDriftReport(root, { format: args.format, output: args.output })
4249
4552
  )
4250
4553
  );
4554
+ server.registerTool(
4555
+ "kaddo_generate_questions_report",
4556
+ {
4557
+ title: "Generate open-questions report",
4558
+ description: `Write the Open Questions readiness report under .kaddo/reports/ (classifies blocking/important/deferred). Does NOT resolve questions. ${DERIVED_NOTE}`,
4559
+ inputSchema: {
4560
+ format: z2.enum(["markdown", "json"]).optional(),
4561
+ output: z2.string().optional()
4562
+ }
4563
+ },
4564
+ async (args) => generated(
4565
+ root,
4566
+ "generate open-questions report",
4567
+ () => generateQuestionsReport(root, { format: args.format, output: args.output })
4568
+ )
4569
+ );
4251
4570
  server.registerTool(
4252
4571
  "kaddo_generate_capsule_draft",
4253
4572
  { 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.25.0",
3
+ "version": "3.26.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": {