@kaddo/mcp 3.56.0 → 3.58.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 +168 -11
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -111,6 +111,36 @@ function writeDerived(root, relPath, content) {
111
111
  fs.mkdirSync(path.dirname(abs), { recursive: true });
112
112
  fs.writeFileSync(abs, content, "utf-8");
113
113
  }
114
+ function writeWorkItemTransition(root, relPath, content) {
115
+ const p = toPosix(relPath).replace(/^\/+/, "");
116
+ if (path.isAbsolute(relPath) || p.split("/").some((seg) => seg === "..")) {
117
+ throw new KaddoMcpError("Blocked unsafe work-item write path.");
118
+ }
119
+ if (!p.startsWith("knowledge/delivery/work-items/")) {
120
+ throw new KaddoMcpError("Work Item transitions can only write under knowledge/delivery/work-items/.");
121
+ }
122
+ if (!p.endsWith(".md")) {
123
+ throw new KaddoMcpError("Work Item files must be markdown.");
124
+ }
125
+ const abs = path.resolve(root, p);
126
+ const base = toPosix(root).replace(/\/+$/, "");
127
+ if (toPosix(abs) !== base && !toPosix(abs).startsWith(base + "/")) {
128
+ throw new KaddoMcpError("Blocked unsafe work-item write path.");
129
+ }
130
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
131
+ fs.writeFileSync(abs, content, "utf-8");
132
+ }
133
+ function removeWorkItemFile(root, relPath) {
134
+ const p = toPosix(relPath).replace(/^\/+/, "");
135
+ if (path.isAbsolute(relPath) || p.split("/").some((seg) => seg === "..")) {
136
+ throw new KaddoMcpError("Blocked unsafe work-item remove path.");
137
+ }
138
+ if (!p.startsWith("knowledge/delivery/work-items/")) {
139
+ throw new KaddoMcpError("Work Item removal can only target knowledge/delivery/work-items/.");
140
+ }
141
+ const abs = path.resolve(root, p);
142
+ if (fs.existsSync(abs)) fs.unlinkSync(abs);
143
+ }
114
144
  function listFiles(root, relDir, filterExt) {
115
145
  let absDir;
116
146
  try {
@@ -382,7 +412,7 @@ function parseArtifact(filePath, raw) {
382
412
  status: String(data.status ?? ""),
383
413
  phase: String(data.phase ?? ""),
384
414
  initiative: String(data.initiative ?? data.source_initiative ?? ""),
385
- source: data.source ? String(data.source) : "",
415
+ source: data.source && typeof data.source === "object" ? String(data.source.type ?? "") : data.source ? String(data.source) : "",
386
416
  sourceId: String(data.source_id ?? ""),
387
417
  rawFrontmatter: data,
388
418
  decisions: Array.isArray(data.decisions) ? data.decisions.map(String).filter(Boolean) : [],
@@ -2419,6 +2449,7 @@ function buildDeliveryState(dir) {
2419
2449
  return {
2420
2450
  phase: "",
2421
2451
  draft_work_items: byState("draft"),
2452
+ refined_draft_work_items: wis.filter((w) => w.lifecycle === "draft" && w.rawFrontmatter.refined_by).length,
2422
2453
  ready_work_items: byState("ready"),
2423
2454
  in_progress_work_items: byState("in-progress"),
2424
2455
  blocked_work_items: byState("blocked"),
@@ -2546,6 +2577,17 @@ function resolveNextStep(dir, now = /* @__PURE__ */ new Date()) {
2546
2577
  const secondary = buildSecondaryRecommendations(st);
2547
2578
  const roadmap = roadmapSignal(dir);
2548
2579
  if (roadmap !== "has-candidates" && st.draft_work_items > 0) {
2580
+ if (st.refined_draft_work_items > 0) {
2581
+ return {
2582
+ id: "review-work-item",
2583
+ phase: "Active Delivery",
2584
+ label: "Review the refined draft Work Item and mark it ready with `kaddo ready`.",
2585
+ command: "kaddo ready",
2586
+ mcpAction: "kaddo_mark_work_item_ready",
2587
+ reason: `There ${st.refined_draft_work_items === 1 ? "is" : "are"} ${st.refined_draft_work_items} refined draft Work Item${st.refined_draft_work_items === 1 ? "" : "s"} awaiting human review.`,
2588
+ ...secondary.length ? { secondary } : {}
2589
+ };
2590
+ }
2549
2591
  return {
2550
2592
  id: "refine-work-item",
2551
2593
  phase: "Active Delivery",
@@ -2912,6 +2954,8 @@ validation (how to test it), definition of done, open questions and dependencies
2912
2954
  - Do not expand scope without explicit confirmation.
2913
2955
  - Do not create mega Work Items \u2014 split when it covers multiple outcomes.
2914
2956
  - Keep acceptance criteria testable.
2957
+ - This skill refines scope and acceptance criteria but does not approve implementation readiness.
2958
+ Readiness requires human confirmation through \`kaddo ready\` or the MCP \`mark_work_item_ready\` action.
2915
2959
 
2916
2960
  ## Quality checklist
2917
2961
 
@@ -3301,9 +3345,9 @@ var RESPONSIBILITY_MATRIX = {
3301
3345
  agent: "work-item-agent",
3302
3346
  responsibleFor: ["Work Item refinement"],
3303
3347
  produces: ["knowledge/delivery/work-items/"],
3304
- canSuggest: ["implementation-agent"],
3348
+ canSuggest: ["kaddo ready", "mark_work_item_ready", "implementation-agent"],
3305
3349
  cannotSuggest: ["branches", "commits", "pull requests", "code"],
3306
- next: ["implementation-agent"]
3350
+ next: ["kaddo ready (human review)", "implementation-agent"]
3307
3351
  },
3308
3352
  "implementation-agent": {
3309
3353
  agent: "implementation-agent",
@@ -4316,7 +4360,16 @@ and \`blocked\` as active work; \`completed\` and \`archived\` are historical kn
4316
4360
 
4317
4361
  ## Handoff
4318
4362
 
4319
- When the Work Item is refined and ready to build, **hand off to the implementation-agent**.
4363
+ After refining a Work Item, **do not mark it ready automatically**. If the Work Item appears
4364
+ complete (acceptance criteria, validation, code globs, domains all present, open questions
4365
+ resolved), recommend human review and the ready transition:
4366
+
4367
+ - **CLI:** \`kaddo ready <WI-ID>\`
4368
+ - **MCP:** \`mark_work_item_ready({ id: "<WI-ID>" })\`
4369
+
4370
+ The transition to \`ready\` requires explicit human confirmation. Only after a human marks the
4371
+ Work Item as ready should it be handed off to the implementation-agent.
4372
+
4320
4373
  You do **not** suggest branches, commits or pull requests \u2014 implementation (including any Git
4321
4374
  branch suggestion) is the implementation-agent's responsibility, and only by respecting the
4322
4375
  project Git strategy. Your job ends at a clear, traceable Work Item that **states how to test it**.
@@ -6052,7 +6105,8 @@ function mapNextStepId(id) {
6052
6105
  "refine-product": "define-product",
6053
6106
  "refine-capabilities": "discover-capabilities",
6054
6107
  "refine-current-state": "describe-architecture",
6055
- "refine-codebase": "describe-architecture"
6108
+ "refine-codebase": "describe-architecture",
6109
+ "review-work-item": "refine-work-item"
6056
6110
  };
6057
6111
  return MAP[id] ?? id;
6058
6112
  }
@@ -7680,7 +7734,14 @@ function optStr2(v) {
7680
7734
  return typeof v === "string" && v.trim() ? v.trim() : void 0;
7681
7735
  }
7682
7736
  function parseSource(data) {
7683
- const raw = data.source ? String(data.source) : "";
7737
+ const rawSource = data.source;
7738
+ if (rawSource != null && typeof rawSource === "object" && !Array.isArray(rawSource)) {
7739
+ const obj = rawSource;
7740
+ const t = optStr2(obj.type);
7741
+ const type = t && VALID_SOURCES2.has(t) ? t : "unknown";
7742
+ return { type, id: optStr2(obj.id) ?? optStr2(data.source_id), title: optStr2(obj.title) ?? optStr2(data.source_title), url: optStr2(obj.url) ?? optStr2(data.source_url), provider: optStr2(obj.provider) ?? optStr2(data.source_provider), inferred: obj.inferred === true || obj.inferred === "true" };
7743
+ }
7744
+ const raw = rawSource ? String(rawSource) : "";
7684
7745
  if (raw && VALID_SOURCES2.has(raw)) {
7685
7746
  return { type: raw, id: optStr2(data.source_id), title: optStr2(data.source_title), url: optStr2(data.source_url), provider: optStr2(data.source_provider), inferred: false };
7686
7747
  }
@@ -7721,23 +7782,53 @@ function listWorkItems(root) {
7721
7782
  }
7722
7783
  const data = parsed.data;
7723
7784
  if (!data.type) continue;
7785
+ const status = lifecycleOf(data, rel);
7786
+ const code = strArray(data.code);
7787
+ const domains = strArray(data.domains);
7788
+ const source = parseSource(data);
7789
+ const readiness = status === "draft" ? assessReadiness(data, parsed.content, code, domains, source) : void 0;
7724
7790
  items.push({
7725
7791
  id: String(data.id ?? "") || rel,
7726
7792
  title: String(data.title ?? ""),
7727
- status: lifecycleOf(data, rel),
7793
+ status,
7728
7794
  type: String(data.type ?? ""),
7729
7795
  knowledge_level: String(data.knowledge_level ?? ""),
7730
7796
  path: rel,
7731
7797
  summary: String(data.summary ?? "") || firstParagraph2(parsed.content),
7732
- code: strArray(data.code),
7798
+ code,
7733
7799
  capabilities: strArray(data.capabilities),
7734
7800
  decisions: strArray(data.decisions),
7735
7801
  capsules: strArray(data.capsules),
7736
- source: parseSource(data)
7802
+ source,
7803
+ domains,
7804
+ readiness
7737
7805
  });
7738
7806
  }
7739
7807
  return items.sort((a, b) => a.id.localeCompare(b.id));
7740
7808
  }
7809
+ function assessReadiness(data, content, code, domains, source) {
7810
+ const body = content.trim();
7811
+ const lower = body.toLowerCase();
7812
+ const warnings = [];
7813
+ if (domains.length === 0) warnings.push("No domains declared.");
7814
+ if (code.length === 0) warnings.push("No code ownership globs declared.");
7815
+ if (!/## acceptance criteria|## criterios de aceptación/i.test(body)) warnings.push("No acceptance criteria section found.");
7816
+ if (!/## validation|## validación/i.test(body)) warnings.push("No validation section found.");
7817
+ const oqMatch = body.match(/## (?:open questions|preguntas abiertas)\s*\n([\s\S]*?)(?=\n##|\n*$)/i);
7818
+ if (oqMatch) {
7819
+ const oqText = oqMatch[1].trim().toLowerCase();
7820
+ if (oqText && !/^(none|ninguna|resolved|resueltas|deferred|diferidas)\.?$/i.test(oqText)) {
7821
+ warnings.push("Open questions are still present.");
7822
+ }
7823
+ }
7824
+ const hasRefinedBy = Boolean(data.refined_by);
7825
+ const readyCandidate = hasRefinedBy && warnings.length === 0;
7826
+ return {
7827
+ readyCandidate,
7828
+ warnings,
7829
+ recommendedAction: readyCandidate ? "mark_work_item_ready" : void 0
7830
+ };
7831
+ }
7741
7832
 
7742
7833
  // src/resources.ts
7743
7834
  function text(uri, body, mimeType = "text/markdown") {
@@ -8092,6 +8183,7 @@ function getPrompt(root, name) {
8092
8183
  }
8093
8184
 
8094
8185
  // src/tools.ts
8186
+ import matter10 from "gray-matter";
8095
8187
  var ok = (data) => ({ ok: true, data });
8096
8188
  var fail = (message) => ({ ok: false, message });
8097
8189
  function projectStatus(root) {
@@ -8137,6 +8229,62 @@ function getWorkItem(root, id) {
8137
8229
  const content = readText(root, item.path);
8138
8230
  return ok({ ...item, content: content ?? "" });
8139
8231
  }
8232
+ function markWorkItemReady(root, id, confirm) {
8233
+ const item = listWorkItems(root).find((w) => w.id === id);
8234
+ if (!item) return fail(`Work Item "${id}" not found.`);
8235
+ if (item.status === "ready") {
8236
+ return ok({ status: "already_ready", id: item.id, message: `Work Item ${item.id} is already ready.` });
8237
+ }
8238
+ if (item.status !== "draft") {
8239
+ return fail(`Work Item ${item.id} is in state "${item.status}", not "draft". Only draft Work Items can be marked as ready.`);
8240
+ }
8241
+ const warnings = item.readiness?.warnings ?? [];
8242
+ if (!confirm) {
8243
+ return ok({
8244
+ status: "needs_confirmation",
8245
+ workItem: {
8246
+ id: item.id,
8247
+ title: item.title,
8248
+ type: item.type,
8249
+ status: item.status,
8250
+ domains: item.domains,
8251
+ source: item.source,
8252
+ code: item.code,
8253
+ readiness: item.readiness
8254
+ },
8255
+ command: `kaddo ready ${item.id}`,
8256
+ message: `Review this Work Item and call mark_work_item_ready with confirm=true to mark it ready.`
8257
+ });
8258
+ }
8259
+ const raw = readText(root, item.path);
8260
+ if (!raw) return fail(`Could not read Work Item file: ${item.path}`);
8261
+ const parsed = matter10(raw);
8262
+ const data = { ...parsed.data };
8263
+ const today2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8264
+ data.status = "ready";
8265
+ data.ready_at = today2;
8266
+ const newContent = matter10.stringify(parsed.content, data);
8267
+ const posixPath = item.path.replace(/\\/g, "/");
8268
+ let newRelPath = item.path;
8269
+ if (posixPath.includes("/work-items/draft/")) {
8270
+ newRelPath = item.path.replace(/[/\\]draft[/\\]/, "/ready/");
8271
+ }
8272
+ writeWorkItemTransition(root, newRelPath, newContent);
8273
+ if (newRelPath !== item.path) {
8274
+ removeWorkItemFile(root, item.path);
8275
+ }
8276
+ const status = warnings.length > 0 ? "ready_with_warnings" : "ready";
8277
+ return ok({
8278
+ status,
8279
+ id: item.id,
8280
+ from: "draft",
8281
+ to: "ready",
8282
+ ready_at: today2,
8283
+ moved: newRelPath !== item.path ? { from: item.path, to: newRelPath } : void 0,
8284
+ ...warnings.length > 0 ? { warnings } : {},
8285
+ next: ["Run kaddo context", "Run kaddo understand"]
8286
+ });
8287
+ }
8140
8288
  function listCapsulesTool(root) {
8141
8289
  return ok(listCapsules(root));
8142
8290
  }
@@ -8178,7 +8326,7 @@ function listGraphHints(root, filter = {}) {
8178
8326
  }
8179
8327
 
8180
8328
  // ../cli/src/core/context-pack.ts
8181
- import matter10 from "gray-matter";
8329
+ import matter11 from "gray-matter";
8182
8330
  var CONTEXT_PACK_VERSION = "1";
8183
8331
  var ARCH_DIR3 = "knowledge";
8184
8332
  function readScanJson(dir) {
@@ -8191,7 +8339,7 @@ function readScanJson(dir) {
8191
8339
  }
8192
8340
  }
8193
8341
  function firstParagraph3(markdown) {
8194
- const body = matter10(markdown).content.trim();
8342
+ const body = matter11(markdown).content.trim();
8195
8343
  const para = body.split("\n\n").map((p) => p.trim()).find((p) => p && !p.startsWith("#"));
8196
8344
  return para ?? "";
8197
8345
  }
@@ -9253,6 +9401,15 @@ function createServer(root) {
9253
9401
  { title: "Get Work Item", description: "Get a Work Item by ID (summary + full markdown).", inputSchema: { id: z2.string() } },
9254
9402
  async (args) => toolText(guarded(root, () => getWorkItem(root, args.id)))
9255
9403
  );
9404
+ server.registerTool(
9405
+ "kaddo_mark_work_item_ready",
9406
+ {
9407
+ title: "Mark Work Item ready",
9408
+ description: "Assess whether a draft Work Item is ready for implementation. Without confirm, returns a preview with readiness warnings. With confirm=true, transitions the Work Item from draft to ready.",
9409
+ inputSchema: { id: z2.string(), confirm: z2.boolean().optional() }
9410
+ },
9411
+ async (args) => toolText(guarded(root, () => markWorkItemReady(root, args.id, args.confirm)))
9412
+ );
9256
9413
  server.registerTool(
9257
9414
  "kaddo_list_capsules",
9258
9415
  { title: "List Knowledge Capsules", description: "List registered external Knowledge Capsules.", inputSchema: {} },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/mcp",
3
- "version": "3.56.0",
3
+ "version": "3.58.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": {