@kaddo/mcp 3.113.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 +403 -64
  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
  }
@@ -17319,8 +17583,16 @@ ${body}`);
17319
17583
  ${out.join("\n\n")}
17320
17584
  `.replace(/\n{3,}/g, "\n\n");
17321
17585
  }
17586
+ function ensureH1(body, title) {
17587
+ const trimmed = body.trim();
17588
+ const firstMeaningful = trimmed.split(/\r?\n/).find((l2) => l2.trim());
17589
+ if (firstMeaningful && /^#\s+/.test(firstMeaningful.trim())) return trimmed;
17590
+ return `# ${title}
17591
+
17592
+ ${trimmed}`;
17593
+ }
17322
17594
  function serialize(data, body) {
17323
- return matter82.stringify(`
17595
+ return matter92.stringify(`
17324
17596
  ${body.trim()}
17325
17597
  `, data);
17326
17598
  }
@@ -17354,6 +17626,7 @@ function createWorkItem(dir, opts) {
17354
17626
  const id = nextWorkItemId(dir);
17355
17627
  const title = intent.split(/\r?\n/)[0].trim().slice(0, 120);
17356
17628
  const today22 = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
17629
+ const summary = opts.summary?.trim() || intent;
17357
17630
  const source = opts.source ? { ...opts.source, inferred: false } : { type: "manual", inferred: false };
17358
17631
  const data = {
17359
17632
  type,
@@ -17365,15 +17638,16 @@ function createWorkItem(dir, opts) {
17365
17638
  source,
17366
17639
  generated_by: "kaddo-admin",
17367
17640
  affected_modules: [],
17368
- summary: intent
17641
+ summary
17369
17642
  };
17370
17643
  if (opts.originalSnapshot) data.original_snapshot = opts.originalSnapshot;
17371
17644
  const answers = opts.answers ?? {};
17372
17645
  const hasAnswers = Object.values(answers).some((v) => v?.trim());
17373
- const body = hasAnswers ? captureBody(title, type, answers) : freshBody({
17646
+ const providedBody = opts.body?.trim();
17647
+ const body = providedBody ? ensureH1(providedBody, title) : hasAnswers ? captureBody(title, type, answers) : freshBody({
17374
17648
  title,
17375
17649
  type,
17376
- summary: intent,
17650
+ summary,
17377
17651
  scopeUnknowns: [],
17378
17652
  affectedModules: [],
17379
17653
  moduleCoverage: [],
@@ -17712,6 +17986,7 @@ function buildImplementationHandoff(dir, workItemId) {
17712
17986
  const affectedModules = match.affectedModules;
17713
17987
  const domains = match.domains;
17714
17988
  const scopeConfidence = match.scopeConfidence;
17989
+ const wiResources = parseWorkItemResources2(match.rawFrontmatter);
17715
17990
  const lines = [
17716
17991
  `Implement Work Item ${wiId} \u2014 "${wiTitle}" \u2014 in project "${projectName}" using Kaddo.`,
17717
17992
  "",
@@ -17801,6 +18076,41 @@ function buildImplementationHandoff(dir, workItemId) {
17801
18076
  "Do not copy all legacy knowledge \u2014 assemble proportionally to scope."
17802
18077
  );
17803
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
+ }
17804
18114
  lines.push(
17805
18115
  "",
17806
18116
  "--- Implementation Plan ---",
@@ -17821,6 +18131,7 @@ function buildImplementationHandoff(dir, workItemId) {
17821
18131
  projectName,
17822
18132
  lifecycle,
17823
18133
  affectedModules,
18134
+ resources: wiResources,
17824
18135
  domains,
17825
18136
  scopeConfidence,
17826
18137
  hasUnmaterializedDecisions: unmaterialized,
@@ -18072,7 +18383,7 @@ function detectFormat(raw) {
18072
18383
  if (!trimmed) throw new ImportError("EMPTY_INPUT", "Import content is empty.");
18073
18384
  if (trimmed.startsWith("---")) {
18074
18385
  try {
18075
- const { data } = matter92(trimmed);
18386
+ const { data } = matter102(trimmed);
18076
18387
  if (data && typeof data === "object" && Object.keys(data).length > 0) {
18077
18388
  const d3 = data;
18078
18389
  if (d3.type === "work-item" || d3.id && typeof d3.id === "string" && /^WI-\d+/.test(d3.id)) {
@@ -18094,7 +18405,7 @@ function parseContent(raw) {
18094
18405
  let bodyText = trimmed;
18095
18406
  if (format === "kaddo-frontmatter" || format === "markdown-frontmatter") {
18096
18407
  try {
18097
- const parsed = matter92(trimmed);
18408
+ const parsed = matter102(trimmed);
18098
18409
  frontmatterFields = parsed.data ?? {};
18099
18410
  bodyText = parsed.content.trim();
18100
18411
  } catch {
@@ -18151,15 +18462,21 @@ function extractSections(body) {
18151
18462
  flush();
18152
18463
  return sections;
18153
18464
  }
18465
+ function deriveShortSummary(parsed) {
18466
+ const paragraph = parsed.body.split(/\n{2,}/).map((p) => p.trim()).find((p) => p && !/^#{1,6}\s/.test(p) && !/^>/.test(p) && p !== parsed.title);
18467
+ const base = paragraph ? paragraph.replace(/\s+/g, " ") : parsed.title;
18468
+ return base.length > 280 ? `${base.slice(0, 277).trimEnd()}\u2026` : base;
18469
+ }
18154
18470
  function normalizeImportFields(parsed, opts) {
18155
18471
  const discardedFields = [];
18156
18472
  for (const key of Object.keys(parsed.candidateFields)) {
18157
18473
  if (LIFECYCLE_FIELDS.has(key)) discardedFields.push(key);
18158
18474
  }
18159
18475
  const type = opts.userType ? normalizeType(opts.userType) ?? "feature" : parsed.type ?? "feature";
18160
- const intent = parsed.body && parsed.body !== parsed.title ? `${parsed.title}
18161
-
18162
- ${parsed.body}` : parsed.title;
18476
+ const hasRealBody = !!parsed.body && parsed.body !== parsed.title;
18477
+ const intent = parsed.title;
18478
+ const body = hasRealBody ? parsed.body : void 0;
18479
+ const summary = hasRealBody ? deriveShortSummary(parsed) : parsed.title;
18163
18480
  const source = {
18164
18481
  type: opts.importSource === "chat" ? "chat" : "external",
18165
18482
  imported_at: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
@@ -18167,14 +18484,14 @@ ${parsed.body}` : parsed.title;
18167
18484
  source.source_format = opts.sourceFormat;
18168
18485
  source.source_hash = opts.sourceHash;
18169
18486
  const snapshot = { title: parsed.title };
18170
- if (parsed.summary !== parsed.title) snapshot.description = parsed.summary;
18487
+ if (summary !== parsed.title) snapshot.description = summary;
18171
18488
  if (typeof parsed.candidateFields.type === "string") snapshot.type = parsed.candidateFields.type;
18172
18489
  if (typeof parsed.candidateFields.status === "string") snapshot.status = parsed.candidateFields.status;
18173
18490
  if (Array.isArray(parsed.candidateFields.labels)) {
18174
18491
  const labels = parsed.candidateFields.labels.filter((l2) => typeof l2 === "string");
18175
18492
  if (labels.length > 0) snapshot.labels = labels;
18176
18493
  }
18177
- return { type, intent, source, snapshot, discardedFields };
18494
+ return { type, intent, summary, body, source, snapshot, discardedFields };
18178
18495
  }
18179
18496
  function computeContentHash(raw) {
18180
18497
  return crypto3.createHash("sha256").update(raw.trim(), "utf-8").digest("hex");
@@ -18185,7 +18502,7 @@ function findDuplicateByHash(dir, hash) {
18185
18502
  const raw = readFile2(item.filePath);
18186
18503
  if (!raw) continue;
18187
18504
  try {
18188
- const { data } = matter92(raw);
18505
+ const { data } = matter102(raw);
18189
18506
  const source = data?.source;
18190
18507
  if (source && typeof source === "object" && !Array.isArray(source)) {
18191
18508
  const s = source;
@@ -18260,6 +18577,8 @@ function importWorkItem(dir, opts) {
18260
18577
  const result = createWorkItem(dir, {
18261
18578
  intent: normalized.intent,
18262
18579
  type: normalized.type,
18580
+ summary: normalized.summary,
18581
+ body: normalized.body,
18263
18582
  source: normalized.source,
18264
18583
  originalSnapshot: normalized.snapshot
18265
18584
  });
@@ -19078,7 +19397,7 @@ function buildInitiativeContent(id, input) {
19078
19397
  if (input.priority) data.priority = input.priority;
19079
19398
  if (input.source) data.source = input.source;
19080
19399
  if (input.sourceId) data.source_id = input.sourceId;
19081
- return matter112.stringify("\n" + (input.body ?? defaultBody(input.title.trim())), data);
19400
+ return matter122.stringify("\n" + (input.body ?? defaultBody(input.title.trim())), data);
19082
19401
  }
19083
19402
  function createInitiative(dir, input) {
19084
19403
  if (!input.title?.trim()) throw new InitiativeWriteError("Initiative title is required.");
@@ -19102,10 +19421,10 @@ function transitionInitiative(dir, id, to) {
19102
19421
  throw new InitiativeWriteError(`Invalid transition ${ini.status} \u2192 ${to}.`);
19103
19422
  }
19104
19423
  const raw = readFile2(ini.filePath);
19105
- const { data, content } = matter112(raw, {});
19424
+ const { data, content } = matter122(raw, {});
19106
19425
  data.status = to;
19107
19426
  if (to === "completed") data.completed_at = today2();
19108
- const newContent = matter112.stringify(content, data);
19427
+ const newContent = matter122.stringify(content, data);
19109
19428
  writeFile2(ini.filePath, newContent);
19110
19429
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19111
19430
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after transition.");
@@ -19115,7 +19434,7 @@ function addCandidate(dir, id, candidate) {
19115
19434
  const ini = getInitiative(dir, id);
19116
19435
  if (!ini) throw new InitiativeWriteError(`Initiative ${id} not found.`);
19117
19436
  const raw = readFile2(ini.filePath);
19118
- const { data, content } = matter112(raw, {});
19437
+ const { data, content } = matter122(raw, {});
19119
19438
  const existing = Array.isArray(data.candidates) ? data.candidates : [];
19120
19439
  const fallbackId = `WI-CANDIDATE-${String(existing.length + 1).padStart(3, "0")}`;
19121
19440
  const normalized = normalizeCandidate(candidate, fallbackId);
@@ -19123,7 +19442,7 @@ function addCandidate(dir, id, candidate) {
19123
19442
  throw new InitiativeWriteError(`Candidate ${normalized.id} already exists in ${id}.`);
19124
19443
  }
19125
19444
  data.candidates = [...existing, candidateToData(normalized)];
19126
- const newContent = matter112.stringify(content, data);
19445
+ const newContent = matter122.stringify(content, data);
19127
19446
  writeFile2(ini.filePath, newContent);
19128
19447
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19129
19448
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after adding a candidate.");
@@ -19133,7 +19452,7 @@ function updateInitiative(dir, id, patch) {
19133
19452
  const ini = getInitiative(dir, id);
19134
19453
  if (!ini) throw new InitiativeWriteError(`Initiative ${id} not found.`);
19135
19454
  const raw = readFile2(ini.filePath);
19136
- const { data, content } = matter112(raw, {});
19455
+ const { data, content } = matter122(raw, {});
19137
19456
  if (patch.title !== void 0) {
19138
19457
  if (!patch.title.trim()) throw new InitiativeWriteError("Initiative title cannot be empty.");
19139
19458
  data.title = patch.title.trim();
@@ -19143,7 +19462,7 @@ function updateInitiative(dir, id, patch) {
19143
19462
  if (patch.knowledgeLevel !== void 0) data.knowledge_level = patch.knowledgeLevel;
19144
19463
  if (patch.domains !== void 0) data.domains = patch.domains;
19145
19464
  if (patch.relatedCapabilities !== void 0) data.related_capabilities = patch.relatedCapabilities;
19146
- const newContent = matter112.stringify(content, data);
19465
+ const newContent = matter122.stringify(content, data);
19147
19466
  writeFile2(ini.filePath, newContent);
19148
19467
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19149
19468
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after update.");
@@ -19161,10 +19480,10 @@ function addExternalLink(dir, id, link) {
19161
19480
  throw new InitiativeWriteError(`External link ${link.integration}:${link.externalId} already exists on ${id}.`);
19162
19481
  }
19163
19482
  const raw = readFile2(ini.filePath);
19164
- const { data, content } = matter112(raw, {});
19483
+ const { data, content } = matter122(raw, {});
19165
19484
  const existing = Array.isArray(data.external_links) ? data.external_links : [];
19166
19485
  data.external_links = [...existing, externalLinkToData(link)];
19167
- const newContent = matter112.stringify(content, data);
19486
+ const newContent = matter122.stringify(content, data);
19168
19487
  writeFile2(ini.filePath, newContent);
19169
19488
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19170
19489
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after adding an external link.");
@@ -19233,7 +19552,7 @@ function buildWorkItemContent(wiId, iniId, candidate) {
19233
19552
  "_What did we learn? Update after completion._",
19234
19553
  ""
19235
19554
  ].join("\n");
19236
- return matter112.stringify("\n" + body, data);
19555
+ return matter122.stringify("\n" + body, data);
19237
19556
  }
19238
19557
  function materializeCandidate(dir, id, candidateId) {
19239
19558
  const ini = getInitiative(dir, id);
@@ -19250,13 +19569,13 @@ function materializeCandidate(dir, id, candidateId) {
19250
19569
  ensureDir2(join3(dir, DRAFT_DIR));
19251
19570
  writeFile2(join3(dir, DRAFT_DIR, fileName), buildWorkItemContent(wiId, ini.id, candidate));
19252
19571
  const raw = readFile2(ini.filePath);
19253
- const { data, content } = matter112(raw, {});
19572
+ const { data, content } = matter122(raw, {});
19254
19573
  const cands = Array.isArray(data.candidates) ? data.candidates : [];
19255
19574
  for (const c of cands) {
19256
19575
  if (c && typeof c === "object" && String(c.id) === candidateId) c.materialized_as = wiId;
19257
19576
  }
19258
19577
  data.candidates = cands;
19259
- const newContent = matter112.stringify(content, data);
19578
+ const newContent = matter122.stringify(content, data);
19260
19579
  writeFile2(ini.filePath, newContent);
19261
19580
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19262
19581
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after materialization.");
@@ -19462,12 +19781,12 @@ function markWorkItemReady(root, id, confirm2) {
19462
19781
  }
19463
19782
  const raw = readText(root, item.path);
19464
19783
  if (!raw) return fail(`Could not read Work Item file: ${item.path}`);
19465
- const parsed = matter12(raw);
19784
+ const parsed = matter13(raw);
19466
19785
  const data = { ...parsed.data };
19467
19786
  const today3 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
19468
19787
  data.status = "ready";
19469
19788
  data.ready_at = today3;
19470
- const newContent = matter12.stringify(parsed.content, data);
19789
+ const newContent = matter13.stringify(parsed.content, data);
19471
19790
  const posixPath = item.path.replace(/\\/g, "/");
19472
19791
  let newRelPath = item.path;
19473
19792
  if (posixPath.includes("/work-items/draft/")) {
@@ -19630,11 +19949,19 @@ function setTelemetryConsentTool(root, args) {
19630
19949
  persistConsent(root, consent);
19631
19950
  return ok({ applied: true, consent, message: `Telemetry ${consent}.` });
19632
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
+ }
19633
19960
 
19634
19961
  // src/multirepo.ts
19635
19962
  import fs5 from "fs";
19636
19963
  import path4 from "path";
19637
- import matter13 from "gray-matter";
19964
+ import matter14 from "gray-matter";
19638
19965
  import { parse as parseYaml11, stringify as stringifyYaml7 } from "yaml";
19639
19966
  var ok2 = (data) => ({ ok: true, data });
19640
19967
  var fail2 = (message) => ({ ok: false, message });
@@ -19786,7 +20113,7 @@ function validateWorkItemModulesTool(root, args) {
19786
20113
  if (!wi) return fail2(`Work Item "${args.workItemId}" not found.`);
19787
20114
  const raw = readText(root, wi.path);
19788
20115
  if (!raw) return fail2(`Could not read Work Item file.`);
19789
- const parsed = matter13(raw);
20116
+ const parsed = matter14(raw);
19790
20117
  const data = parsed.data;
19791
20118
  const affectedModules = Array.isArray(data.affected_modules) ? data.affected_modules.map(String) : [];
19792
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) : [] })) : [];
@@ -19839,7 +20166,7 @@ function getWorkItemContextTool(root, args) {
19839
20166
  if (!wi) return fail2(`Work Item "${args.workItemId}" not found.`);
19840
20167
  const raw = readText(root, wi.path);
19841
20168
  if (!raw) return fail2(`Could not read Work Item file.`);
19842
- const parsed = matter13(raw);
20169
+ const parsed = matter14(raw);
19843
20170
  const data = parsed.data;
19844
20171
  const affectedModules = Array.isArray(data.affected_modules) ? data.affected_modules.map(String) : [];
19845
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) : [] })) : [];
@@ -19925,7 +20252,7 @@ function suggestBranchStrategyTool(root, args) {
19925
20252
  if (!wi) return fail2(`Work Item "${args.workItemId}" not found.`);
19926
20253
  const raw = readText(root, wi.path);
19927
20254
  if (!raw) return fail2(`Could not read Work Item file.`);
19928
- const parsed = matter13(raw);
20255
+ const parsed = matter14(raw);
19929
20256
  const data = parsed.data;
19930
20257
  const affectedModules = Array.isArray(data.affected_modules) ? data.affected_modules.map(String) : [];
19931
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) : [] })) : [];
@@ -20685,14 +21012,14 @@ function systemImpactCandidatesTool(root, args) {
20685
21012
  }
20686
21013
 
20687
21014
  // ../cli/src/services/integrations.ts
20688
- import matter15 from "gray-matter";
21015
+ import matter16 from "gray-matter";
20689
21016
  import { parse as parseYaml13, stringify as stringifyYaml9 } from "yaml";
20690
21017
 
20691
21018
  // ../cli/src/core/work-item-write.ts
20692
21019
  import fs7 from "fs";
20693
21020
  import path6 from "path";
20694
21021
  import crypto4 from "crypto";
20695
- import matter14 from "gray-matter";
21022
+ import matter15 from "gray-matter";
20696
21023
  var SECTION_ORDER2 = [
20697
21024
  "Actor",
20698
21025
  "Outcome",
@@ -21740,7 +22067,7 @@ async function integrationsWorkItemTool(root, args) {
21740
22067
  }
21741
22068
 
21742
22069
  // ../cli/src/core/context-pack.ts
21743
- import matter16 from "gray-matter";
22070
+ import matter17 from "gray-matter";
21744
22071
  var CONTEXT_PACK_VERSION = "1";
21745
22072
  var ARCH_DIR4 = "knowledge";
21746
22073
  function readScanJson(dir) {
@@ -21753,7 +22080,7 @@ function readScanJson(dir) {
21753
22080
  }
21754
22081
  }
21755
22082
  function firstParagraph4(markdown) {
21756
- const body = matter16(markdown).content.trim();
22083
+ const body = matter17(markdown).content.trim();
21757
22084
  const para = body.split("\n\n").map((p) => p.trim()).find((p) => p && !p.startsWith("#"));
21758
22085
  return para ?? "";
21759
22086
  }
@@ -21807,6 +22134,8 @@ function toContextWorkItem(a2) {
21807
22134
  wi.validationStatus = a2.validationStatus || (isHistorical ? "not-assessed" : void 0);
21808
22135
  wi.releaseStatus = a2.releaseStatus || (isHistorical ? "not-assessed" : void 0);
21809
22136
  if (a2.affectedModules.length > 0) wi.affectedModules = a2.affectedModules;
22137
+ const wiResources = parseWorkItemResources(a2.rawFrontmatter);
22138
+ if (wiResources.length > 0) wi.resources = wiResources;
21810
22139
  if (a2.scopeConfidence) wi.scopeConfidence = a2.scopeConfidence;
21811
22140
  if (a2.moduleCoverage) wi.moduleCoverage = a2.moduleCoverage;
21812
22141
  if (a2.impactAnalysis) wi.impactAnalysis = a2.impactAnalysis;
@@ -23139,6 +23468,16 @@ function createServer(root) {
23139
23468
  },
23140
23469
  async (args) => toolText(guarded(root, () => markWorkItemReady(root, args.id, args.confirm)))
23141
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
+ );
23142
23481
  server.registerTool(
23143
23482
  "kaddo_list_capsules",
23144
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.113.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": {