@kaddo/mcp 3.114.0 → 3.115.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 +377 -56
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -386,7 +386,7 @@ function join(...parts) {
386
386
  }
387
387
 
388
388
  // ../cli/src/core/impact-report.ts
389
- import matter9 from "gray-matter";
389
+ import matter10 from "gray-matter";
390
390
 
391
391
  // ../cli/src/core/config.ts
392
392
  import { z } from "zod";
@@ -2590,8 +2590,117 @@ function discoverInitiatives(dir) {
2590
2590
  return out.sort((a2, b2) => a2.id.localeCompare(b2.id));
2591
2591
  }
2592
2592
 
2593
+ // ../cli/src/core/resources.ts
2594
+ import matter6 from "gray-matter";
2595
+ var RESOURCE_ARTIFACT_TYPE = "project-resource";
2596
+ function str(v) {
2597
+ return typeof v === "string" && v.trim() ? v.trim() : null;
2598
+ }
2599
+ function strArray(v) {
2600
+ if (Array.isArray(v)) return v.map((x2) => String(x2).trim()).filter(Boolean);
2601
+ if (typeof v === "string" && v.trim()) return [v.trim()];
2602
+ return [];
2603
+ }
2604
+ function parseInterfaces(v) {
2605
+ if (!Array.isArray(v)) return [];
2606
+ const out = [];
2607
+ for (const raw of v) {
2608
+ if (!raw || typeof raw !== "object") continue;
2609
+ const o2 = raw;
2610
+ const type = str(o2.type);
2611
+ if (!type) continue;
2612
+ out.push({
2613
+ type,
2614
+ tool: str(o2.tool),
2615
+ provider: str(o2.provider),
2616
+ purpose: str(o2.purpose),
2617
+ operations: strArray(o2.operations),
2618
+ environments: strArray(o2.environments),
2619
+ constraints: str(o2.constraints)
2620
+ });
2621
+ }
2622
+ return out;
2623
+ }
2624
+ function parseBoundaries(v) {
2625
+ if (!v || typeof v !== "object" || Array.isArray(v)) return {};
2626
+ const out = {};
2627
+ for (const [env, val] of Object.entries(v)) {
2628
+ const list3 = strArray(val);
2629
+ if (list3.length > 0) out[env] = list3;
2630
+ }
2631
+ return out;
2632
+ }
2633
+ function parseAuth(v) {
2634
+ if (!v || typeof v !== "object" || Array.isArray(v)) return { mode: null, refs: [] };
2635
+ const o2 = v;
2636
+ const refs = strArray(o2.refs).filter((r2) => !r2.includes("=") && /^[A-Za-z0-9_.-]+$/.test(r2));
2637
+ return { mode: str(o2.mode), refs };
2638
+ }
2639
+ function toDetail(filePath, relPath, raw) {
2640
+ const parsed = matter6(raw);
2641
+ const fm = parsed.data ?? {};
2642
+ const body = parsed.content.trim();
2643
+ const id = str(fm.id) ?? "";
2644
+ return {
2645
+ id,
2646
+ title: str(fm.title) ?? id,
2647
+ resourceType: str(fm.resource_type),
2648
+ provider: str(fm.provider),
2649
+ environments: strArray(fm.environments),
2650
+ path: relPath,
2651
+ purpose: str(fm.purpose),
2652
+ interfaces: parseInterfaces(fm.access_interfaces),
2653
+ boundaries: parseBoundaries(fm.access_boundaries),
2654
+ ...(() => {
2655
+ const a2 = parseAuth(fm.authentication);
2656
+ return { authRefs: a2.refs, authMode: a2.mode };
2657
+ })(),
2658
+ markdownBody: body
2659
+ };
2660
+ }
2661
+ function getResources(dir) {
2662
+ return discoverKnowledge(dir).filter((a2) => a2.type === RESOURCE_ARTIFACT_TYPE).map((a2) => {
2663
+ const fm = a2.rawFrontmatter;
2664
+ return {
2665
+ id: a2.id || str(fm.id) || a2.title,
2666
+ title: a2.title || str(fm.title) || a2.id,
2667
+ resourceType: str(fm.resource_type),
2668
+ provider: str(fm.provider),
2669
+ environments: strArray(fm.environments),
2670
+ path: a2.relPath
2671
+ };
2672
+ }).sort((x2, y) => x2.id.localeCompare(y.id));
2673
+ }
2674
+ function getResource(dir, id) {
2675
+ const match = discoverKnowledge(dir).find(
2676
+ (a2) => a2.type === RESOURCE_ARTIFACT_TYPE && (a2.id === id || a2.title === id)
2677
+ );
2678
+ if (!match) return null;
2679
+ const raw = readFile(match.filePath);
2680
+ return toDetail(match.filePath, match.relPath, raw);
2681
+ }
2682
+ function parseWorkItemResources(fm) {
2683
+ const v = fm.resources;
2684
+ if (!Array.isArray(v)) return [];
2685
+ const out = [];
2686
+ for (const raw of v) {
2687
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
2688
+ const o2 = raw;
2689
+ const id = str(o2.id);
2690
+ if (!id) continue;
2691
+ out.push({ id, role: str(o2.role) ?? "affected" });
2692
+ }
2693
+ return out;
2694
+ }
2695
+
2593
2696
  // ../cli/src/core/graph.ts
2594
2697
  var KNOWLEDGE3 = "knowledge";
2698
+ var RESOURCE_ROLE_EDGE = {
2699
+ affected: "affects",
2700
+ implementation: "uses",
2701
+ validation: "validates_with",
2702
+ delivery: "delivers_through"
2703
+ };
2595
2704
  var ACTIVE_STATUSES = ["draft", "ready", "in-progress", "blocked"];
2596
2705
  var ALL_STATUSES = ["draft", "ready", "in-progress", "blocked", "completed"];
2597
2706
  function scopeStatuses(scope) {
@@ -2670,6 +2779,16 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
2670
2779
  if (cand.materializedAs) addEdge(candId, `wi:${cand.materializedAs}`, "materialized_as");
2671
2780
  }
2672
2781
  }
2782
+ const resourceTitleById = /* @__PURE__ */ new Map();
2783
+ for (const r2 of getResources(dir)) {
2784
+ resourceTitleById.set(r2.id, r2.title);
2785
+ addNode({
2786
+ id: `resource:${r2.id}`,
2787
+ type: "project-resource",
2788
+ label: r2.title ? `${r2.id} ${r2.title}`.trim() : r2.id,
2789
+ path: r2.path
2790
+ });
2791
+ }
2673
2792
  for (const wi of selectedWIs) {
2674
2793
  const id = wi.id || wi.title;
2675
2794
  if (!id || !id.trim()) continue;
@@ -2694,6 +2813,12 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
2694
2813
  addNode({ id: capId, type: "capability", label: cap });
2695
2814
  addEdge(wiNodeId, capId, "implements");
2696
2815
  }
2816
+ for (const rel of parseWorkItemResources(wi.rawFrontmatter)) {
2817
+ const resId = `resource:${rel.id}`;
2818
+ const title = resourceTitleById.get(rel.id);
2819
+ addNode({ id: resId, type: "project-resource", label: title ? `${rel.id} ${title}`.trim() : rel.id });
2820
+ addEdge(wiNodeId, resId, RESOURCE_ROLE_EDGE[rel.role] ?? "uses");
2821
+ }
2697
2822
  for (const dec of wi.decisions) {
2698
2823
  if (!dec || !dec.trim()) continue;
2699
2824
  const adrId = `adr:${dec}`;
@@ -3072,7 +3197,7 @@ function loadGraphHints(dir) {
3072
3197
  }
3073
3198
 
3074
3199
  // ../cli/src/services/installed-skills.ts
3075
- import matter6 from "gray-matter";
3200
+ import matter7 from "gray-matter";
3076
3201
  var SKILLS_DIR = join("knowledge", "skills");
3077
3202
  function discoverInstalledSkills(dir) {
3078
3203
  const base = join(dir, SKILLS_DIR);
@@ -3082,7 +3207,7 @@ function discoverInstalledSkills(dir) {
3082
3207
  const skillFile = join(base, entry, "skill.md");
3083
3208
  if (!isFile(skillFile)) continue;
3084
3209
  try {
3085
- const { data } = matter6(readFile(skillFile));
3210
+ const { data } = matter7(readFile(skillFile));
3086
3211
  if (data.type && String(data.type) !== "skill") continue;
3087
3212
  const id = String(data.id ?? entry);
3088
3213
  out.push({
@@ -4671,7 +4796,7 @@ function deriveDeliveryCompletedStatus(dir) {
4671
4796
  }
4672
4797
 
4673
4798
  // ../cli/src/core/assets.ts
4674
- import matter7 from "gray-matter";
4799
+ import matter8 from "gray-matter";
4675
4800
 
4676
4801
  // ../cli/src/core/version.ts
4677
4802
  import { fileURLToPath } from "url";
@@ -4826,8 +4951,13 @@ dependencies.
4826
4951
  flags, content/copy, authentication/authorization, notifications, analytics, documentation,
4827
4952
  operations/release \u2014 as affected, reviewed-not-affected, unknown, or not-applicable.
4828
4953
  4. **Module review** \u2014 for multirepo, evaluate each mapped module with the same statuses.
4829
- 5. **Completeness review** \u2014 confirm: outcome covered, journey covered, modules assessed, unknowns
4830
- visible, acceptance criteria end-to-end, scope and out-of-scope coherent.
4954
+ 5. **Resource review** \u2014 ask: does this change need to observe, modify, validate or deliver against a
4955
+ known external resource of the project (a Project Resource under \`knowledge/tech/resources/\` \u2014
4956
+ database, cloud, API, queue, storage\u2026)? If so, declare it in the WI front matter \`resources:\` with
4957
+ a role (\`affected\`, \`implementation\`, \`validation\`, \`delivery\`). Resources are external systems,
4958
+ distinct from \`affected_modules\` (code). Not every WI needs resources (e.g. a CSS change does not).
4959
+ 6. **Completeness review** \u2014 confirm: outcome covered, journey covered, modules assessed, resources
4960
+ assessed, unknowns visible, acceptance criteria end-to-end, scope and out-of-scope coherent.
4831
4961
 
4832
4962
  ## Rules
4833
4963
 
@@ -4838,6 +4968,8 @@ dependencies.
4838
4968
  - Include at least one end-to-end acceptance criterion for user-facing changes.
4839
4969
  - Do not reduce a product intent to the first technical implementation found.
4840
4970
  - Evaluate surfaces and modules before proposing files.
4971
+ - Consider Project Resources when the scope justifies it; never invent resources not backed by
4972
+ project knowledge, and keep \`resources\` (external systems) separate from \`affected_modules\` (code).
4841
4973
  - This skill refines scope and acceptance criteria but does not approve implementation readiness.
4842
4974
  Readiness requires human confirmation through \`kaddo ready\` or the MCP \`mark_work_item_ready\` action.
4843
4975
 
@@ -7877,7 +8009,7 @@ function canonicalSkills() {
7877
8009
  }
7878
8010
  function versionOf(content) {
7879
8011
  try {
7880
- const v = matter7(content).data.version;
8012
+ const v = matter8(content).data.version;
7881
8013
  return v === void 0 || v === null ? null : String(v);
7882
8014
  } catch {
7883
8015
  return null;
@@ -8649,7 +8781,7 @@ function renderRouteCompact(route) {
8649
8781
  }
8650
8782
 
8651
8783
  // ../cli/src/core/metadata-health.ts
8652
- import matter8 from "gray-matter";
8784
+ import matter9 from "gray-matter";
8653
8785
  var KNOWLEDGE_FILES2 = [
8654
8786
  "knowledge/business/business.md",
8655
8787
  "knowledge/product/product.md",
@@ -8669,7 +8801,7 @@ function analyzeMetadataHealth(dir) {
8669
8801
  let data;
8670
8802
  try {
8671
8803
  const raw = readFile(p);
8672
- data = matter8(raw).data;
8804
+ data = matter9(raw).data;
8673
8805
  } catch {
8674
8806
  continue;
8675
8807
  }
@@ -8701,7 +8833,7 @@ function analyzeMetadataHealth(dir) {
8701
8833
  if (isFile(p) && entry.endsWith(".md")) {
8702
8834
  try {
8703
8835
  const raw = readFile(p);
8704
- const fm = matter8(raw).data;
8836
+ const fm = matter9(raw).data;
8705
8837
  if (fm.status === "done") {
8706
8838
  const rel = p.replace(dir + "/", "").replace(dir + "\\", "").replace(/\\/g, "/");
8707
8839
  findings.push({
@@ -9656,7 +9788,7 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
9656
9788
  }
9657
9789
  }
9658
9790
  try {
9659
- const body = matter9(readFile(wi.filePath)).content;
9791
+ const body = matter10(readFile(wi.filePath)).content;
9660
9792
  if (hasSection(body, /acceptance|criterios de aceptaci/i)) withAcceptance++;
9661
9793
  else gaps.missing_acceptance_criteria.push(gapItem(wi, "Add an `## Acceptance Criteria` section."));
9662
9794
  if (hasSection(body, /definition of done|^#{1,6}\s*dod\b|definici[oó]n de (terminado|hecho)/i)) withDoD++;
@@ -10369,7 +10501,7 @@ function serializeDriftJson(r2) {
10369
10501
  }
10370
10502
 
10371
10503
  // src/workitems.ts
10372
- import matter10 from "gray-matter";
10504
+ import matter11 from "gray-matter";
10373
10505
  var VALID_SOURCES2 = /* @__PURE__ */ new Set(["manual", "roadmap", "jira", "github", "notion", "xlsx", "csv", "api", "external", "unknown"]);
10374
10506
  function optStr3(v) {
10375
10507
  return typeof v === "string" && v.trim() ? v.trim() : void 0;
@@ -10392,7 +10524,7 @@ function parseSource(data) {
10392
10524
  return { type: "unknown", inferred: true };
10393
10525
  }
10394
10526
  var ACTIVE_STATES2 = /* @__PURE__ */ new Set(["draft", "ready", "in-progress", "blocked"]);
10395
- function strArray(v) {
10527
+ function strArray2(v) {
10396
10528
  return Array.isArray(v) ? v.map((x2) => String(x2)).filter(Boolean) : [];
10397
10529
  }
10398
10530
  function lifecycleOf(data, relPath) {
@@ -10460,15 +10592,15 @@ function listWorkItems(root) {
10460
10592
  if (raw === null) continue;
10461
10593
  let parsed;
10462
10594
  try {
10463
- parsed = matter10(raw);
10595
+ parsed = matter11(raw);
10464
10596
  } catch {
10465
10597
  continue;
10466
10598
  }
10467
10599
  const data = parsed.data;
10468
10600
  if (!data.type) continue;
10469
10601
  const status = lifecycleOf(data, rel);
10470
- const code = strArray(data.code);
10471
- const domains = strArray(data.domains);
10602
+ const code = strArray2(data.code);
10603
+ const domains = strArray2(data.domains);
10472
10604
  const source = parseSource(data);
10473
10605
  const readiness = status === "draft" ? assessReadiness(data, parsed.content, code, domains, source) : void 0;
10474
10606
  items.push({
@@ -10480,9 +10612,9 @@ function listWorkItems(root) {
10480
10612
  path: rel,
10481
10613
  summary: String(data.summary ?? "") || firstParagraph2(parsed.content),
10482
10614
  code,
10483
- capabilities: strArray(data.capabilities),
10484
- decisions: strArray(data.decisions),
10485
- capsules: strArray(data.capsules),
10615
+ capabilities: strArray2(data.capabilities),
10616
+ decisions: strArray2(data.decisions),
10617
+ capsules: strArray2(data.capsules),
10486
10618
  source,
10487
10619
  domains,
10488
10620
  readiness,
@@ -10928,14 +11060,14 @@ function getPrompt(root, name) {
10928
11060
  }
10929
11061
 
10930
11062
  // src/tools.ts
10931
- import matter12 from "gray-matter";
11063
+ import matter13 from "gray-matter";
10932
11064
 
10933
11065
  // ../cli/dist/core.js
10934
11066
  import fs3 from "fs";
10935
11067
  import path3 from "path";
10936
11068
  import { z as z3 } from "zod";
10937
11069
  import { parse as parseYaml10 } from "yaml";
10938
- import matter11 from "gray-matter";
11070
+ import matter12 from "gray-matter";
10939
11071
  import matter22 from "gray-matter";
10940
11072
  import { parse as parseYaml62, stringify as stringifyYaml32 } from "yaml";
10941
11073
  import { parse as parseYaml22 } from "yaml";
@@ -10944,25 +11076,26 @@ import { parse as parseYaml52, stringify as stringifyYaml22 } from "yaml";
10944
11076
  import { parse as parseYaml42, stringify as stringifyYaml4 } from "yaml";
10945
11077
  import matter32 from "gray-matter";
10946
11078
  import matter42 from "gray-matter";
10947
- import { parse as parseYaml72 } from "yaml";
10948
11079
  import matter52 from "gray-matter";
11080
+ import { parse as parseYaml72 } from "yaml";
11081
+ import matter62 from "gray-matter";
10949
11082
  import { fileURLToPath as fileURLToPath2 } from "url";
10950
11083
  import { dirname as dirname2, join as join22, parse as parse2 } from "path";
10951
11084
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
10952
- import matter62 from "gray-matter";
10953
11085
  import matter72 from "gray-matter";
11086
+ import matter82 from "gray-matter";
10954
11087
  import { parse as parseYaml82, stringify as stringifyYaml42 } from "yaml";
10955
11088
  import fs32 from "fs";
10956
11089
  import path32 from "path";
10957
11090
  import crypto2 from "crypto";
10958
- import matter82 from "gray-matter";
11091
+ import matter92 from "gray-matter";
10959
11092
  import crypto3 from "crypto";
10960
11093
  import fs4 from "fs";
10961
- import matter92 from "gray-matter";
10962
11094
  import matter102 from "gray-matter";
11095
+ import matter112 from "gray-matter";
10963
11096
  import { parse as parseYaml92, stringify as stringifyYaml5 } from "yaml";
10964
11097
  import { parse as parseYaml102, stringify as stringifyYaml6 } from "yaml";
10965
- import matter112 from "gray-matter";
11098
+ import matter122 from "gray-matter";
10966
11099
  function exists2(p) {
10967
11100
  return fs3.existsSync(p);
10968
11101
  }
@@ -11130,7 +11263,7 @@ function parseImpactAnalysis2(data) {
11130
11263
  }
11131
11264
  function parseArtifact2(filePath, raw) {
11132
11265
  try {
11133
- const { data } = matter11(raw);
11266
+ const { data } = matter12(raw);
11134
11267
  const globs = Array.isArray(data.code) ? data.code.filter(Boolean) : [];
11135
11268
  return {
11136
11269
  filePath,
@@ -12684,7 +12817,113 @@ function suggestInitiativesForExternalItem(dir, integration, externalId) {
12684
12817
  )
12685
12818
  );
12686
12819
  }
12820
+ var RESOURCE_ARTIFACT_TYPE2 = "project-resource";
12821
+ function str2(v) {
12822
+ return typeof v === "string" && v.trim() ? v.trim() : null;
12823
+ }
12824
+ function strArray3(v) {
12825
+ if (Array.isArray(v)) return v.map((x2) => String(x2).trim()).filter(Boolean);
12826
+ if (typeof v === "string" && v.trim()) return [v.trim()];
12827
+ return [];
12828
+ }
12829
+ function parseInterfaces2(v) {
12830
+ if (!Array.isArray(v)) return [];
12831
+ const out = [];
12832
+ for (const raw of v) {
12833
+ if (!raw || typeof raw !== "object") continue;
12834
+ const o2 = raw;
12835
+ const type = str2(o2.type);
12836
+ if (!type) continue;
12837
+ out.push({
12838
+ type,
12839
+ tool: str2(o2.tool),
12840
+ provider: str2(o2.provider),
12841
+ purpose: str2(o2.purpose),
12842
+ operations: strArray3(o2.operations),
12843
+ environments: strArray3(o2.environments),
12844
+ constraints: str2(o2.constraints)
12845
+ });
12846
+ }
12847
+ return out;
12848
+ }
12849
+ function parseBoundaries2(v) {
12850
+ if (!v || typeof v !== "object" || Array.isArray(v)) return {};
12851
+ const out = {};
12852
+ for (const [env, val] of Object.entries(v)) {
12853
+ const list22 = strArray3(val);
12854
+ if (list22.length > 0) out[env] = list22;
12855
+ }
12856
+ return out;
12857
+ }
12858
+ function parseAuth2(v) {
12859
+ if (!v || typeof v !== "object" || Array.isArray(v)) return { mode: null, refs: [] };
12860
+ const o2 = v;
12861
+ const refs = strArray3(o2.refs).filter((r2) => !r2.includes("=") && /^[A-Za-z0-9_.-]+$/.test(r2));
12862
+ return { mode: str2(o2.mode), refs };
12863
+ }
12864
+ function toDetail2(filePath, relPath, raw) {
12865
+ const parsed = matter42(raw);
12866
+ const fm = parsed.data ?? {};
12867
+ const body = parsed.content.trim();
12868
+ const id = str2(fm.id) ?? "";
12869
+ return {
12870
+ id,
12871
+ title: str2(fm.title) ?? id,
12872
+ resourceType: str2(fm.resource_type),
12873
+ provider: str2(fm.provider),
12874
+ environments: strArray3(fm.environments),
12875
+ path: relPath,
12876
+ purpose: str2(fm.purpose),
12877
+ interfaces: parseInterfaces2(fm.access_interfaces),
12878
+ boundaries: parseBoundaries2(fm.access_boundaries),
12879
+ ...(() => {
12880
+ const a2 = parseAuth2(fm.authentication);
12881
+ return { authRefs: a2.refs, authMode: a2.mode };
12882
+ })(),
12883
+ markdownBody: body
12884
+ };
12885
+ }
12886
+ function getResources2(dir) {
12887
+ return discoverKnowledge2(dir).filter((a2) => a2.type === RESOURCE_ARTIFACT_TYPE2).map((a2) => {
12888
+ const fm = a2.rawFrontmatter;
12889
+ return {
12890
+ id: a2.id || str2(fm.id) || a2.title,
12891
+ title: a2.title || str2(fm.title) || a2.id,
12892
+ resourceType: str2(fm.resource_type),
12893
+ provider: str2(fm.provider),
12894
+ environments: strArray3(fm.environments),
12895
+ path: a2.relPath
12896
+ };
12897
+ }).sort((x2, y) => x2.id.localeCompare(y.id));
12898
+ }
12899
+ function getResource2(dir, id) {
12900
+ const match = discoverKnowledge2(dir).find(
12901
+ (a2) => a2.type === RESOURCE_ARTIFACT_TYPE2 && (a2.id === id || a2.title === id)
12902
+ );
12903
+ if (!match) return null;
12904
+ const raw = readFile2(match.filePath);
12905
+ return toDetail2(match.filePath, match.relPath, raw);
12906
+ }
12907
+ function parseWorkItemResources2(fm) {
12908
+ const v = fm.resources;
12909
+ if (!Array.isArray(v)) return [];
12910
+ const out = [];
12911
+ for (const raw of v) {
12912
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
12913
+ const o2 = raw;
12914
+ const id = str2(o2.id);
12915
+ if (!id) continue;
12916
+ out.push({ id, role: str2(o2.role) ?? "affected" });
12917
+ }
12918
+ return out;
12919
+ }
12687
12920
  var KNOWLEDGE22 = "knowledge";
12921
+ var RESOURCE_ROLE_EDGE2 = {
12922
+ affected: "affects",
12923
+ implementation: "uses",
12924
+ validation: "validates_with",
12925
+ delivery: "delivers_through"
12926
+ };
12688
12927
  var ACTIVE_STATUSES2 = ["draft", "ready", "in-progress", "blocked"];
12689
12928
  var ALL_STATUSES2 = ["draft", "ready", "in-progress", "blocked", "completed"];
12690
12929
  function scopeStatuses2(scope) {
@@ -12763,6 +13002,16 @@ function buildGraph2(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
12763
13002
  if (cand.materializedAs) addEdge(candId, `wi:${cand.materializedAs}`, "materialized_as");
12764
13003
  }
12765
13004
  }
13005
+ const resourceTitleById = /* @__PURE__ */ new Map();
13006
+ for (const r2 of getResources2(dir)) {
13007
+ resourceTitleById.set(r2.id, r2.title);
13008
+ addNode({
13009
+ id: `resource:${r2.id}`,
13010
+ type: "project-resource",
13011
+ label: r2.title ? `${r2.id} ${r2.title}`.trim() : r2.id,
13012
+ path: r2.path
13013
+ });
13014
+ }
12766
13015
  for (const wi of selectedWIs) {
12767
13016
  const id = wi.id || wi.title;
12768
13017
  if (!id || !id.trim()) continue;
@@ -12787,6 +13036,12 @@ function buildGraph2(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
12787
13036
  addNode({ id: capId, type: "capability", label: cap });
12788
13037
  addEdge(wiNodeId, capId, "implements");
12789
13038
  }
13039
+ for (const rel of parseWorkItemResources2(wi.rawFrontmatter)) {
13040
+ const resId = `resource:${rel.id}`;
13041
+ const title = resourceTitleById.get(rel.id);
13042
+ addNode({ id: resId, type: "project-resource", label: title ? `${rel.id} ${title}`.trim() : rel.id });
13043
+ addEdge(wiNodeId, resId, RESOURCE_ROLE_EDGE2[rel.role] ?? "uses");
13044
+ }
12790
13045
  for (const dec of wi.decisions) {
12791
13046
  if (!dec || !dec.trim()) continue;
12792
13047
  const adrId = `adr:${dec}`;
@@ -13280,8 +13535,13 @@ dependencies.
13280
13535
  flags, content/copy, authentication/authorization, notifications, analytics, documentation,
13281
13536
  operations/release \u2014 as affected, reviewed-not-affected, unknown, or not-applicable.
13282
13537
  4. **Module review** \u2014 for multirepo, evaluate each mapped module with the same statuses.
13283
- 5. **Completeness review** \u2014 confirm: outcome covered, journey covered, modules assessed, unknowns
13284
- visible, acceptance criteria end-to-end, scope and out-of-scope coherent.
13538
+ 5. **Resource review** \u2014 ask: does this change need to observe, modify, validate or deliver against a
13539
+ known external resource of the project (a Project Resource under \`knowledge/tech/resources/\` \u2014
13540
+ database, cloud, API, queue, storage\u2026)? If so, declare it in the WI front matter \`resources:\` with
13541
+ a role (\`affected\`, \`implementation\`, \`validation\`, \`delivery\`). Resources are external systems,
13542
+ distinct from \`affected_modules\` (code). Not every WI needs resources (e.g. a CSS change does not).
13543
+ 6. **Completeness review** \u2014 confirm: outcome covered, journey covered, modules assessed, resources
13544
+ assessed, unknowns visible, acceptance criteria end-to-end, scope and out-of-scope coherent.
13285
13545
 
13286
13546
  ## Rules
13287
13547
 
@@ -13292,6 +13552,8 @@ dependencies.
13292
13552
  - Include at least one end-to-end acceptance criterion for user-facing changes.
13293
13553
  - Do not reduce a product intent to the first technical implementation found.
13294
13554
  - Evaluate surfaces and modules before proposing files.
13555
+ - Consider Project Resources when the scope justifies it; never invent resources not backed by
13556
+ project knowledge, and keep \`resources\` (external systems) separate from \`affected_modules\` (code).
13295
13557
  - This skill refines scope and acceptance criteria but does not approve implementation readiness.
13296
13558
  Readiness requires human confirmation through \`kaddo ready\` or the MCP \`mark_work_item_ready\` action.
13297
13559
 
@@ -16979,6 +17241,8 @@ function getWorkItem(dir, workItemId) {
16979
17241
  path: match.relPath,
16980
17242
  // Faithful source of truth: the artifact body already read above, not reconstructed (WI-025).
16981
17243
  markdownBody: body.trim(),
17244
+ // External systems this WI relates to (WI-030), separate from affectedModules (code).
17245
+ resources: parseWorkItemResources2(fm),
16982
17246
  ...parseSystemImpact(dir, fm)
16983
17247
  };
16984
17248
  return { ...detail, refinement: computeRefinementStatus(detail) };
@@ -17043,7 +17307,7 @@ function parseOriginalSnapshot(fm) {
17043
17307
  function readBody(filePath) {
17044
17308
  try {
17045
17309
  const raw = readFile2(filePath);
17046
- return matter72(raw).content;
17310
+ return matter82(raw).content;
17047
17311
  } catch {
17048
17312
  return "";
17049
17313
  }
@@ -17328,7 +17592,7 @@ function ensureH1(body, title) {
17328
17592
  ${trimmed}`;
17329
17593
  }
17330
17594
  function serialize(data, body) {
17331
- return matter82.stringify(`
17595
+ return matter92.stringify(`
17332
17596
  ${body.trim()}
17333
17597
  `, data);
17334
17598
  }
@@ -17722,6 +17986,7 @@ function buildImplementationHandoff(dir, workItemId) {
17722
17986
  const affectedModules = match.affectedModules;
17723
17987
  const domains = match.domains;
17724
17988
  const scopeConfidence = match.scopeConfidence;
17989
+ const wiResources = parseWorkItemResources2(match.rawFrontmatter);
17725
17990
  const lines = [
17726
17991
  `Implement Work Item ${wiId} \u2014 "${wiTitle}" \u2014 in project "${projectName}" using Kaddo.`,
17727
17992
  "",
@@ -17811,6 +18076,41 @@ function buildImplementationHandoff(dir, workItemId) {
17811
18076
  "Do not copy all legacy knowledge \u2014 assemble proportionally to scope."
17812
18077
  );
17813
18078
  }
18079
+ if (wiResources.length > 0) {
18080
+ lines.push(
18081
+ "",
18082
+ "--- Relevant Resources ---",
18083
+ "",
18084
+ "This Work Item relates to external project resources. The handoff INFORMS you about them \u2014 it",
18085
+ "never connects to them, runs any interface, or reveals credential values. Respect each boundary."
18086
+ );
18087
+ for (const rel of wiResources) {
18088
+ const res = getResource2(dir, rel.id);
18089
+ if (!res) {
18090
+ lines.push("", `- ${rel.id} (role: ${rel.role}) \u2014 definition not found under knowledge/tech/resources/.`);
18091
+ continue;
18092
+ }
18093
+ lines.push("", `- ${res.id} \u2014 ${res.title} (role: ${rel.role})`);
18094
+ if (res.resourceType || res.provider) {
18095
+ lines.push(` type: ${res.resourceType ?? "\u2014"}${res.provider ? ` \xB7 provider: ${res.provider}` : ""}`);
18096
+ }
18097
+ if (res.purpose) lines.push(` purpose: ${res.purpose}`);
18098
+ if (res.environments.length > 0) lines.push(` environments: ${res.environments.join(", ")}`);
18099
+ if (res.interfaces.length > 0) {
18100
+ const ifaces = res.interfaces.map((i) => i.tool || i.provider ? `${i.type} (${i.tool ?? i.provider})` : i.type);
18101
+ lines.push(` interfaces: ${ifaces.join(", ")}`);
18102
+ }
18103
+ for (const [env, ops] of Object.entries(res.boundaries)) {
18104
+ lines.push(` boundary (${env}): ${ops.join(", ")}`);
18105
+ }
18106
+ if (res.authRefs.length > 0) lines.push(` auth references (names only): ${res.authRefs.join(", ")}`);
18107
+ }
18108
+ lines.push(
18109
+ "",
18110
+ "Treat these as knowledge, not availability: an interface being declared does not mean it is",
18111
+ "installed or connected. Do not execute any interface or migration without explicit human confirmation."
18112
+ );
18113
+ }
17814
18114
  lines.push(
17815
18115
  "",
17816
18116
  "--- Implementation Plan ---",
@@ -17831,6 +18131,7 @@ function buildImplementationHandoff(dir, workItemId) {
17831
18131
  projectName,
17832
18132
  lifecycle,
17833
18133
  affectedModules,
18134
+ resources: wiResources,
17834
18135
  domains,
17835
18136
  scopeConfidence,
17836
18137
  hasUnmaterializedDecisions: unmaterialized,
@@ -18082,7 +18383,7 @@ function detectFormat(raw) {
18082
18383
  if (!trimmed) throw new ImportError("EMPTY_INPUT", "Import content is empty.");
18083
18384
  if (trimmed.startsWith("---")) {
18084
18385
  try {
18085
- const { data } = matter92(trimmed);
18386
+ const { data } = matter102(trimmed);
18086
18387
  if (data && typeof data === "object" && Object.keys(data).length > 0) {
18087
18388
  const d3 = data;
18088
18389
  if (d3.type === "work-item" || d3.id && typeof d3.id === "string" && /^WI-\d+/.test(d3.id)) {
@@ -18104,7 +18405,7 @@ function parseContent(raw) {
18104
18405
  let bodyText = trimmed;
18105
18406
  if (format === "kaddo-frontmatter" || format === "markdown-frontmatter") {
18106
18407
  try {
18107
- const parsed = matter92(trimmed);
18408
+ const parsed = matter102(trimmed);
18108
18409
  frontmatterFields = parsed.data ?? {};
18109
18410
  bodyText = parsed.content.trim();
18110
18411
  } catch {
@@ -18201,7 +18502,7 @@ function findDuplicateByHash(dir, hash) {
18201
18502
  const raw = readFile2(item.filePath);
18202
18503
  if (!raw) continue;
18203
18504
  try {
18204
- const { data } = matter92(raw);
18505
+ const { data } = matter102(raw);
18205
18506
  const source = data?.source;
18206
18507
  if (source && typeof source === "object" && !Array.isArray(source)) {
18207
18508
  const s = source;
@@ -19096,7 +19397,7 @@ function buildInitiativeContent(id, input) {
19096
19397
  if (input.priority) data.priority = input.priority;
19097
19398
  if (input.source) data.source = input.source;
19098
19399
  if (input.sourceId) data.source_id = input.sourceId;
19099
- return matter112.stringify("\n" + (input.body ?? defaultBody(input.title.trim())), data);
19400
+ return matter122.stringify("\n" + (input.body ?? defaultBody(input.title.trim())), data);
19100
19401
  }
19101
19402
  function createInitiative(dir, input) {
19102
19403
  if (!input.title?.trim()) throw new InitiativeWriteError("Initiative title is required.");
@@ -19120,10 +19421,10 @@ function transitionInitiative(dir, id, to) {
19120
19421
  throw new InitiativeWriteError(`Invalid transition ${ini.status} \u2192 ${to}.`);
19121
19422
  }
19122
19423
  const raw = readFile2(ini.filePath);
19123
- const { data, content } = matter112(raw, {});
19424
+ const { data, content } = matter122(raw, {});
19124
19425
  data.status = to;
19125
19426
  if (to === "completed") data.completed_at = today2();
19126
- const newContent = matter112.stringify(content, data);
19427
+ const newContent = matter122.stringify(content, data);
19127
19428
  writeFile2(ini.filePath, newContent);
19128
19429
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19129
19430
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after transition.");
@@ -19133,7 +19434,7 @@ function addCandidate(dir, id, candidate) {
19133
19434
  const ini = getInitiative(dir, id);
19134
19435
  if (!ini) throw new InitiativeWriteError(`Initiative ${id} not found.`);
19135
19436
  const raw = readFile2(ini.filePath);
19136
- const { data, content } = matter112(raw, {});
19437
+ const { data, content } = matter122(raw, {});
19137
19438
  const existing = Array.isArray(data.candidates) ? data.candidates : [];
19138
19439
  const fallbackId = `WI-CANDIDATE-${String(existing.length + 1).padStart(3, "0")}`;
19139
19440
  const normalized = normalizeCandidate(candidate, fallbackId);
@@ -19141,7 +19442,7 @@ function addCandidate(dir, id, candidate) {
19141
19442
  throw new InitiativeWriteError(`Candidate ${normalized.id} already exists in ${id}.`);
19142
19443
  }
19143
19444
  data.candidates = [...existing, candidateToData(normalized)];
19144
- const newContent = matter112.stringify(content, data);
19445
+ const newContent = matter122.stringify(content, data);
19145
19446
  writeFile2(ini.filePath, newContent);
19146
19447
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19147
19448
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after adding a candidate.");
@@ -19151,7 +19452,7 @@ function updateInitiative(dir, id, patch) {
19151
19452
  const ini = getInitiative(dir, id);
19152
19453
  if (!ini) throw new InitiativeWriteError(`Initiative ${id} not found.`);
19153
19454
  const raw = readFile2(ini.filePath);
19154
- const { data, content } = matter112(raw, {});
19455
+ const { data, content } = matter122(raw, {});
19155
19456
  if (patch.title !== void 0) {
19156
19457
  if (!patch.title.trim()) throw new InitiativeWriteError("Initiative title cannot be empty.");
19157
19458
  data.title = patch.title.trim();
@@ -19161,7 +19462,7 @@ function updateInitiative(dir, id, patch) {
19161
19462
  if (patch.knowledgeLevel !== void 0) data.knowledge_level = patch.knowledgeLevel;
19162
19463
  if (patch.domains !== void 0) data.domains = patch.domains;
19163
19464
  if (patch.relatedCapabilities !== void 0) data.related_capabilities = patch.relatedCapabilities;
19164
- const newContent = matter112.stringify(content, data);
19465
+ const newContent = matter122.stringify(content, data);
19165
19466
  writeFile2(ini.filePath, newContent);
19166
19467
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19167
19468
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after update.");
@@ -19179,10 +19480,10 @@ function addExternalLink(dir, id, link) {
19179
19480
  throw new InitiativeWriteError(`External link ${link.integration}:${link.externalId} already exists on ${id}.`);
19180
19481
  }
19181
19482
  const raw = readFile2(ini.filePath);
19182
- const { data, content } = matter112(raw, {});
19483
+ const { data, content } = matter122(raw, {});
19183
19484
  const existing = Array.isArray(data.external_links) ? data.external_links : [];
19184
19485
  data.external_links = [...existing, externalLinkToData(link)];
19185
- const newContent = matter112.stringify(content, data);
19486
+ const newContent = matter122.stringify(content, data);
19186
19487
  writeFile2(ini.filePath, newContent);
19187
19488
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19188
19489
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after adding an external link.");
@@ -19251,7 +19552,7 @@ function buildWorkItemContent(wiId, iniId, candidate) {
19251
19552
  "_What did we learn? Update after completion._",
19252
19553
  ""
19253
19554
  ].join("\n");
19254
- return matter112.stringify("\n" + body, data);
19555
+ return matter122.stringify("\n" + body, data);
19255
19556
  }
19256
19557
  function materializeCandidate(dir, id, candidateId) {
19257
19558
  const ini = getInitiative(dir, id);
@@ -19268,13 +19569,13 @@ function materializeCandidate(dir, id, candidateId) {
19268
19569
  ensureDir2(join3(dir, DRAFT_DIR));
19269
19570
  writeFile2(join3(dir, DRAFT_DIR, fileName), buildWorkItemContent(wiId, ini.id, candidate));
19270
19571
  const raw = readFile2(ini.filePath);
19271
- const { data, content } = matter112(raw, {});
19572
+ const { data, content } = matter122(raw, {});
19272
19573
  const cands = Array.isArray(data.candidates) ? data.candidates : [];
19273
19574
  for (const c of cands) {
19274
19575
  if (c && typeof c === "object" && String(c.id) === candidateId) c.materialized_as = wiId;
19275
19576
  }
19276
19577
  data.candidates = cands;
19277
- const newContent = matter112.stringify(content, data);
19578
+ const newContent = matter122.stringify(content, data);
19278
19579
  writeFile2(ini.filePath, newContent);
19279
19580
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19280
19581
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after materialization.");
@@ -19480,12 +19781,12 @@ function markWorkItemReady(root, id, confirm2) {
19480
19781
  }
19481
19782
  const raw = readText(root, item.path);
19482
19783
  if (!raw) return fail(`Could not read Work Item file: ${item.path}`);
19483
- const parsed = matter12(raw);
19784
+ const parsed = matter13(raw);
19484
19785
  const data = { ...parsed.data };
19485
19786
  const today3 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
19486
19787
  data.status = "ready";
19487
19788
  data.ready_at = today3;
19488
- const newContent = matter12.stringify(parsed.content, data);
19789
+ const newContent = matter13.stringify(parsed.content, data);
19489
19790
  const posixPath = item.path.replace(/\\/g, "/");
19490
19791
  let newRelPath = item.path;
19491
19792
  if (posixPath.includes("/work-items/draft/")) {
@@ -19648,11 +19949,19 @@ function setTelemetryConsentTool(root, args) {
19648
19949
  persistConsent(root, consent);
19649
19950
  return ok({ applied: true, consent, message: `Telemetry ${consent}.` });
19650
19951
  }
19952
+ function listResourcesTool(root) {
19953
+ return ok(getResources(root));
19954
+ }
19955
+ function getResourceTool(root, id) {
19956
+ const resource = getResource(root, id);
19957
+ if (!resource) return fail(`Project Resource "${id}" not found.`);
19958
+ return ok(resource);
19959
+ }
19651
19960
 
19652
19961
  // src/multirepo.ts
19653
19962
  import fs5 from "fs";
19654
19963
  import path4 from "path";
19655
- import matter13 from "gray-matter";
19964
+ import matter14 from "gray-matter";
19656
19965
  import { parse as parseYaml11, stringify as stringifyYaml7 } from "yaml";
19657
19966
  var ok2 = (data) => ({ ok: true, data });
19658
19967
  var fail2 = (message) => ({ ok: false, message });
@@ -19804,7 +20113,7 @@ function validateWorkItemModulesTool(root, args) {
19804
20113
  if (!wi) return fail2(`Work Item "${args.workItemId}" not found.`);
19805
20114
  const raw = readText(root, wi.path);
19806
20115
  if (!raw) return fail2(`Could not read Work Item file.`);
19807
- const parsed = matter13(raw);
20116
+ const parsed = matter14(raw);
19808
20117
  const data = parsed.data;
19809
20118
  const affectedModules = Array.isArray(data.affected_modules) ? data.affected_modules.map(String) : [];
19810
20119
  const codeEntries = Array.isArray(data.code) ? data.code.filter((c) => typeof c === "object" && c !== null).map((c) => ({ repo: typeof c.repo === "string" ? c.repo : void 0, paths: Array.isArray(c.paths) ? c.paths.map(String) : [] })) : [];
@@ -19857,7 +20166,7 @@ function getWorkItemContextTool(root, args) {
19857
20166
  if (!wi) return fail2(`Work Item "${args.workItemId}" not found.`);
19858
20167
  const raw = readText(root, wi.path);
19859
20168
  if (!raw) return fail2(`Could not read Work Item file.`);
19860
- const parsed = matter13(raw);
20169
+ const parsed = matter14(raw);
19861
20170
  const data = parsed.data;
19862
20171
  const affectedModules = Array.isArray(data.affected_modules) ? data.affected_modules.map(String) : [];
19863
20172
  const codeEntries = Array.isArray(data.code) ? data.code.filter((c) => typeof c === "object" && c !== null).map((c) => ({ repo: typeof c.repo === "string" ? c.repo : void 0, paths: Array.isArray(c.paths) ? c.paths.map(String) : [] })) : [];
@@ -19943,7 +20252,7 @@ function suggestBranchStrategyTool(root, args) {
19943
20252
  if (!wi) return fail2(`Work Item "${args.workItemId}" not found.`);
19944
20253
  const raw = readText(root, wi.path);
19945
20254
  if (!raw) return fail2(`Could not read Work Item file.`);
19946
- const parsed = matter13(raw);
20255
+ const parsed = matter14(raw);
19947
20256
  const data = parsed.data;
19948
20257
  const affectedModules = Array.isArray(data.affected_modules) ? data.affected_modules.map(String) : [];
19949
20258
  const codeEntries = Array.isArray(data.code) ? data.code.filter((c) => typeof c === "object" && c !== null).map((c) => ({ repo: typeof c.repo === "string" ? c.repo : void 0, paths: Array.isArray(c.paths) ? c.paths.map(String) : [] })) : [];
@@ -20703,14 +21012,14 @@ function systemImpactCandidatesTool(root, args) {
20703
21012
  }
20704
21013
 
20705
21014
  // ../cli/src/services/integrations.ts
20706
- import matter15 from "gray-matter";
21015
+ import matter16 from "gray-matter";
20707
21016
  import { parse as parseYaml13, stringify as stringifyYaml9 } from "yaml";
20708
21017
 
20709
21018
  // ../cli/src/core/work-item-write.ts
20710
21019
  import fs7 from "fs";
20711
21020
  import path6 from "path";
20712
21021
  import crypto4 from "crypto";
20713
- import matter14 from "gray-matter";
21022
+ import matter15 from "gray-matter";
20714
21023
  var SECTION_ORDER2 = [
20715
21024
  "Actor",
20716
21025
  "Outcome",
@@ -21758,7 +22067,7 @@ async function integrationsWorkItemTool(root, args) {
21758
22067
  }
21759
22068
 
21760
22069
  // ../cli/src/core/context-pack.ts
21761
- import matter16 from "gray-matter";
22070
+ import matter17 from "gray-matter";
21762
22071
  var CONTEXT_PACK_VERSION = "1";
21763
22072
  var ARCH_DIR4 = "knowledge";
21764
22073
  function readScanJson(dir) {
@@ -21771,7 +22080,7 @@ function readScanJson(dir) {
21771
22080
  }
21772
22081
  }
21773
22082
  function firstParagraph4(markdown) {
21774
- const body = matter16(markdown).content.trim();
22083
+ const body = matter17(markdown).content.trim();
21775
22084
  const para = body.split("\n\n").map((p) => p.trim()).find((p) => p && !p.startsWith("#"));
21776
22085
  return para ?? "";
21777
22086
  }
@@ -21825,6 +22134,8 @@ function toContextWorkItem(a2) {
21825
22134
  wi.validationStatus = a2.validationStatus || (isHistorical ? "not-assessed" : void 0);
21826
22135
  wi.releaseStatus = a2.releaseStatus || (isHistorical ? "not-assessed" : void 0);
21827
22136
  if (a2.affectedModules.length > 0) wi.affectedModules = a2.affectedModules;
22137
+ const wiResources = parseWorkItemResources(a2.rawFrontmatter);
22138
+ if (wiResources.length > 0) wi.resources = wiResources;
21828
22139
  if (a2.scopeConfidence) wi.scopeConfidence = a2.scopeConfidence;
21829
22140
  if (a2.moduleCoverage) wi.moduleCoverage = a2.moduleCoverage;
21830
22141
  if (a2.impactAnalysis) wi.impactAnalysis = a2.impactAnalysis;
@@ -23157,6 +23468,16 @@ function createServer(root) {
23157
23468
  },
23158
23469
  async (args) => toolText(guarded(root, () => markWorkItemReady(root, args.id, args.confirm)))
23159
23470
  );
23471
+ server.registerTool(
23472
+ "kaddo_list_resources",
23473
+ { title: "List Project Resources", description: "List external systems the project uses (databases, cloud, APIs\u2026), read-only, no secrets.", inputSchema: {} },
23474
+ async () => toolText(guarded(root, () => listResourcesTool(root)))
23475
+ );
23476
+ server.registerTool(
23477
+ "kaddo_get_resource",
23478
+ { title: "Get Project Resource", description: "Get one Project Resource by ID: purpose, environments, access interfaces, boundaries and auth references (never values).", inputSchema: { id: z4.string() } },
23479
+ async (args) => toolText(guarded(root, () => getResourceTool(root, args.id)))
23480
+ );
23160
23481
  server.registerTool(
23161
23482
  "kaddo_list_capsules",
23162
23483
  { 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.114.0",
3
+ "version": "3.115.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": {