@kaddo/mcp 3.114.0 → 3.116.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 +808 -145
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -371,6 +371,9 @@ function exists(p) {
371
371
  function readFile(p) {
372
372
  return fs2.readFileSync(p, "utf-8");
373
373
  }
374
+ function ensureDir(dirPath) {
375
+ fs2.mkdirSync(dirPath, { recursive: true });
376
+ }
374
377
  function readDir(dirPath) {
375
378
  if (!exists(dirPath)) return [];
376
379
  return fs2.readdirSync(dirPath);
@@ -386,7 +389,7 @@ function join(...parts) {
386
389
  }
387
390
 
388
391
  // ../cli/src/core/impact-report.ts
389
- import matter9 from "gray-matter";
392
+ import matter10 from "gray-matter";
390
393
 
391
394
  // ../cli/src/core/config.ts
392
395
  import { z } from "zod";
@@ -485,8 +488,8 @@ function loadConfig(dir) {
485
488
  const parsed = configSchema.safeParse(raw ?? {});
486
489
  if (!parsed.success) {
487
490
  const issues = parsed.error.issues.map((i) => {
488
- const path7 = i.path.join(".");
489
- return path7 ? ` - ${path7}: ${i.message}` : ` - ${i.message}`;
491
+ const path9 = i.path.join(".");
492
+ return path9 ? ` - ${path9}: ${i.message}` : ` - ${i.message}`;
490
493
  }).join("\n");
491
494
  throw new ConfigError(`Invalid .kaddo/config.yml:
492
495
  ${issues}`);
@@ -927,11 +930,11 @@ function moduleArtifactCoverage(dir, id) {
927
930
  };
928
931
  }
929
932
  function loadMappedModules(dir) {
930
- const path7 = join(dir, DESCRIPTOR_PATH);
931
- if (!exists(path7)) return [];
933
+ const path9 = join(dir, DESCRIPTOR_PATH);
934
+ if (!exists(path9)) return [];
932
935
  let parsed;
933
936
  try {
934
- parsed = parseYaml4(readFile(path7));
937
+ parsed = parseYaml4(readFile(path9));
935
938
  } catch {
936
939
  return [];
937
940
  }
@@ -2438,7 +2441,7 @@ function sectionParagraph2(md, title) {
2438
2441
  }
2439
2442
  return "";
2440
2443
  }
2441
- function parseCapsule(id, path7, md) {
2444
+ function parseCapsule(id, path9, md) {
2442
2445
  const { data } = matter4(md);
2443
2446
  const updatedAt = data.updated_at ? String(data.updated_at) : void 0;
2444
2447
  let ageDays = null;
@@ -2448,7 +2451,7 @@ function parseCapsule(id, path7, md) {
2448
2451
  }
2449
2452
  return {
2450
2453
  id,
2451
- path: path7,
2454
+ path: path9,
2452
2455
  system: data.system ? String(data.system) : id,
2453
2456
  owner: data.owner ? String(data.owner) : void 0,
2454
2457
  updatedAt,
@@ -2590,8 +2593,145 @@ function discoverInitiatives(dir) {
2590
2593
  return out.sort((a2, b2) => a2.id.localeCompare(b2.id));
2591
2594
  }
2592
2595
 
2596
+ // ../cli/src/core/resources.ts
2597
+ import matter6 from "gray-matter";
2598
+ var RESOURCE_ARTIFACT_TYPE = "project-resource";
2599
+ var RESOURCE_TYPES = [
2600
+ "database",
2601
+ "cloud",
2602
+ "api",
2603
+ "queue",
2604
+ "storage",
2605
+ "repository",
2606
+ "platform",
2607
+ "service",
2608
+ "other"
2609
+ ];
2610
+ var INTERFACE_TYPES = ["cli", "mcp", "api", "sql", "sdk", "iac", "console", "other"];
2611
+ function str(v) {
2612
+ return typeof v === "string" && v.trim() ? v.trim() : null;
2613
+ }
2614
+ function strArray(v) {
2615
+ if (Array.isArray(v)) return v.map((x2) => String(x2).trim()).filter(Boolean);
2616
+ if (typeof v === "string" && v.trim()) return [v.trim()];
2617
+ return [];
2618
+ }
2619
+ function parseInterfaces(v) {
2620
+ if (!Array.isArray(v)) return [];
2621
+ const out = [];
2622
+ for (const raw of v) {
2623
+ if (!raw || typeof raw !== "object") continue;
2624
+ const o2 = raw;
2625
+ const type = str(o2.type);
2626
+ if (!type) continue;
2627
+ out.push({
2628
+ type,
2629
+ tool: str(o2.tool),
2630
+ provider: str(o2.provider),
2631
+ purpose: str(o2.purpose),
2632
+ operations: strArray(o2.operations),
2633
+ environments: strArray(o2.environments),
2634
+ constraints: str(o2.constraints)
2635
+ });
2636
+ }
2637
+ return out;
2638
+ }
2639
+ function parseBoundaries(v) {
2640
+ if (!v || typeof v !== "object" || Array.isArray(v)) return {};
2641
+ const out = {};
2642
+ for (const [env, val] of Object.entries(v)) {
2643
+ const list3 = strArray(val);
2644
+ if (list3.length > 0) out[env] = list3;
2645
+ }
2646
+ return out;
2647
+ }
2648
+ function parseAuth(v) {
2649
+ if (!v || typeof v !== "object" || Array.isArray(v)) return { mode: null, refs: [] };
2650
+ const o2 = v;
2651
+ const refs = strArray(o2.refs).filter((r2) => !r2.includes("=") && /^[A-Za-z0-9_.-]+$/.test(r2));
2652
+ return { mode: str(o2.mode), refs };
2653
+ }
2654
+ function parseScope(v) {
2655
+ if (!v || typeof v !== "object" || Array.isArray(v)) return null;
2656
+ const o2 = v;
2657
+ const type = str(o2.type);
2658
+ if (!type) return null;
2659
+ return { type, module: str(o2.module) };
2660
+ }
2661
+ function purposeFromBody(body) {
2662
+ const m2 = body.match(/^#{1,2}\s+Purpose\s*\n+([\s\S]*?)(?=\n#{1,2}\s|\s*$)/m);
2663
+ const text3 = m2 ? m2[1].trim() : "";
2664
+ return text3 || null;
2665
+ }
2666
+ function toDetail(filePath, relPath, raw) {
2667
+ const parsed = matter6(raw);
2668
+ const fm = parsed.data ?? {};
2669
+ const body = parsed.content.trim();
2670
+ const id = str(fm.id) ?? "";
2671
+ return {
2672
+ id,
2673
+ title: str(fm.title) ?? id,
2674
+ resourceType: str(fm.resource_type),
2675
+ provider: str(fm.provider),
2676
+ environments: strArray(fm.environments),
2677
+ scope: parseScope(fm.scope),
2678
+ modules: strArray(fm.modules),
2679
+ path: relPath,
2680
+ purpose: str(fm.purpose) ?? purposeFromBody(body),
2681
+ interfaces: parseInterfaces(fm.access_interfaces),
2682
+ boundaries: parseBoundaries(fm.access_boundaries),
2683
+ ...(() => {
2684
+ const a2 = parseAuth(fm.authentication);
2685
+ return { authRefs: a2.refs, authMode: a2.mode };
2686
+ })(),
2687
+ markdownBody: body
2688
+ };
2689
+ }
2690
+ function getResources(dir) {
2691
+ return discoverKnowledge(dir).filter((a2) => a2.type === RESOURCE_ARTIFACT_TYPE).map((a2) => {
2692
+ const fm = a2.rawFrontmatter;
2693
+ return {
2694
+ id: a2.id || str(fm.id) || a2.title,
2695
+ title: a2.title || str(fm.title) || a2.id,
2696
+ resourceType: str(fm.resource_type),
2697
+ provider: str(fm.provider),
2698
+ environments: strArray(fm.environments),
2699
+ scope: parseScope(fm.scope),
2700
+ modules: strArray(fm.modules),
2701
+ path: a2.relPath
2702
+ };
2703
+ }).sort((x2, y) => x2.id.localeCompare(y.id));
2704
+ }
2705
+ function getResource(dir, id) {
2706
+ const match = discoverKnowledge(dir).find(
2707
+ (a2) => a2.type === RESOURCE_ARTIFACT_TYPE && (a2.id === id || a2.title === id)
2708
+ );
2709
+ if (!match) return null;
2710
+ const raw = readFile(match.filePath);
2711
+ return toDetail(match.filePath, match.relPath, raw);
2712
+ }
2713
+ function parseWorkItemResources(fm) {
2714
+ const v = fm.resources;
2715
+ if (!Array.isArray(v)) return [];
2716
+ const out = [];
2717
+ for (const raw of v) {
2718
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
2719
+ const o2 = raw;
2720
+ const id = str(o2.id);
2721
+ if (!id) continue;
2722
+ out.push({ id, role: str(o2.role) ?? "affected" });
2723
+ }
2724
+ return out;
2725
+ }
2726
+
2593
2727
  // ../cli/src/core/graph.ts
2594
2728
  var KNOWLEDGE3 = "knowledge";
2729
+ var RESOURCE_ROLE_EDGE = {
2730
+ affected: "affects",
2731
+ implementation: "uses",
2732
+ validation: "validates_with",
2733
+ delivery: "delivers_through"
2734
+ };
2595
2735
  var ACTIVE_STATUSES = ["draft", "ready", "in-progress", "blocked"];
2596
2736
  var ALL_STATUSES = ["draft", "ready", "in-progress", "blocked", "completed"];
2597
2737
  function scopeStatuses(scope) {
@@ -2628,9 +2768,9 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
2628
2768
  ];
2629
2769
  const presentLayers = [];
2630
2770
  for (const layer2 of layerDocs) {
2631
- const path7 = layer2.files.map((f3) => `${KNOWLEDGE3}/${f3}`).find((rel) => exists(join(dir, rel)));
2632
- if (path7) {
2633
- addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path7 });
2771
+ const path9 = layer2.files.map((f3) => `${KNOWLEDGE3}/${f3}`).find((rel) => exists(join(dir, rel)));
2772
+ if (path9) {
2773
+ addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path9 });
2634
2774
  presentLayers.push(layer2.id);
2635
2775
  }
2636
2776
  }
@@ -2670,6 +2810,24 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
2670
2810
  if (cand.materializedAs) addEdge(candId, `wi:${cand.materializedAs}`, "materialized_as");
2671
2811
  }
2672
2812
  }
2813
+ const resourceTitleById = /* @__PURE__ */ new Map();
2814
+ for (const r2 of getResources(dir)) {
2815
+ resourceTitleById.set(r2.id, r2.title);
2816
+ const resId = `resource:${r2.id}`;
2817
+ addNode({
2818
+ id: resId,
2819
+ type: "project-resource",
2820
+ label: r2.title ? `${r2.id} ${r2.title}`.trim() : r2.id,
2821
+ path: r2.path
2822
+ });
2823
+ const depModules = new Set(r2.modules);
2824
+ if (r2.scope?.type === "module" && r2.scope.module) depModules.add(r2.scope.module);
2825
+ for (const mod of depModules) {
2826
+ const modId = `module:${mod}`;
2827
+ addNode({ id: modId, type: "module", label: mod });
2828
+ addEdge(modId, resId, "depends_on");
2829
+ }
2830
+ }
2673
2831
  for (const wi of selectedWIs) {
2674
2832
  const id = wi.id || wi.title;
2675
2833
  if (!id || !id.trim()) continue;
@@ -2694,6 +2852,12 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
2694
2852
  addNode({ id: capId, type: "capability", label: cap });
2695
2853
  addEdge(wiNodeId, capId, "implements");
2696
2854
  }
2855
+ for (const rel of parseWorkItemResources(wi.rawFrontmatter)) {
2856
+ const resId = `resource:${rel.id}`;
2857
+ const title = resourceTitleById.get(rel.id);
2858
+ addNode({ id: resId, type: "project-resource", label: title ? `${rel.id} ${title}`.trim() : rel.id });
2859
+ addEdge(wiNodeId, resId, RESOURCE_ROLE_EDGE[rel.role] ?? "uses");
2860
+ }
2697
2861
  for (const dec of wi.decisions) {
2698
2862
  if (!dec || !dec.trim()) continue;
2699
2863
  const adrId = `adr:${dec}`;
@@ -3072,7 +3236,7 @@ function loadGraphHints(dir) {
3072
3236
  }
3073
3237
 
3074
3238
  // ../cli/src/services/installed-skills.ts
3075
- import matter6 from "gray-matter";
3239
+ import matter7 from "gray-matter";
3076
3240
  var SKILLS_DIR = join("knowledge", "skills");
3077
3241
  function discoverInstalledSkills(dir) {
3078
3242
  const base = join(dir, SKILLS_DIR);
@@ -3082,7 +3246,7 @@ function discoverInstalledSkills(dir) {
3082
3246
  const skillFile = join(base, entry, "skill.md");
3083
3247
  if (!isFile(skillFile)) continue;
3084
3248
  try {
3085
- const { data } = matter6(readFile(skillFile));
3249
+ const { data } = matter7(readFile(skillFile));
3086
3250
  if (data.type && String(data.type) !== "skill") continue;
3087
3251
  const id = String(data.id ?? entry);
3088
3252
  out.push({
@@ -4238,10 +4402,10 @@ function resolveNextStep(dir, now = /* @__PURE__ */ new Date()) {
4238
4402
  const q3 = (rel) => analyzeKnowledgeArtifact(dir, rel);
4239
4403
  const resolveAgent = (agent) => {
4240
4404
  const file = agent.endsWith(".md") ? agent : `${agent}.md`;
4241
- const path7 = agentInstallPath(file);
4242
- const installed = isFile(join(dir, path7));
4405
+ const path9 = agentInstallPath(file);
4406
+ const installed = isFile(join(dir, path9));
4243
4407
  const group = agentGroupOf(file);
4244
- return { agentPath: path7, agentInstalled: installed, installCommand: installed ? void 0 : `kaddo add agents --group ${group}` };
4408
+ return { agentPath: path9, agentInstalled: installed, installCommand: installed ? void 0 : `kaddo add agents --group ${group}` };
4245
4409
  };
4246
4410
  const moduleRepo = isModule(config);
4247
4411
  const coreRepo = isCore(config);
@@ -4671,7 +4835,7 @@ function deriveDeliveryCompletedStatus(dir) {
4671
4835
  }
4672
4836
 
4673
4837
  // ../cli/src/core/assets.ts
4674
- import matter7 from "gray-matter";
4838
+ import matter8 from "gray-matter";
4675
4839
 
4676
4840
  // ../cli/src/core/version.ts
4677
4841
  import { fileURLToPath } from "url";
@@ -4826,8 +4990,13 @@ dependencies.
4826
4990
  flags, content/copy, authentication/authorization, notifications, analytics, documentation,
4827
4991
  operations/release \u2014 as affected, reviewed-not-affected, unknown, or not-applicable.
4828
4992
  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.
4993
+ 5. **Resource review** \u2014 ask: does this change need to observe, modify, validate or deliver against a
4994
+ known external resource of the project (a Project Resource under \`knowledge/tech/resources/\` \u2014
4995
+ database, cloud, API, queue, storage\u2026)? If so, declare it in the WI front matter \`resources:\` with
4996
+ a role (\`affected\`, \`implementation\`, \`validation\`, \`delivery\`). Resources are external systems,
4997
+ distinct from \`affected_modules\` (code). Not every WI needs resources (e.g. a CSS change does not).
4998
+ 6. **Completeness review** \u2014 confirm: outcome covered, journey covered, modules assessed, resources
4999
+ assessed, unknowns visible, acceptance criteria end-to-end, scope and out-of-scope coherent.
4831
5000
 
4832
5001
  ## Rules
4833
5002
 
@@ -4838,6 +5007,8 @@ dependencies.
4838
5007
  - Include at least one end-to-end acceptance criterion for user-facing changes.
4839
5008
  - Do not reduce a product intent to the first technical implementation found.
4840
5009
  - Evaluate surfaces and modules before proposing files.
5010
+ - Consider Project Resources when the scope justifies it; never invent resources not backed by
5011
+ project knowledge, and keep \`resources\` (external systems) separate from \`affected_modules\` (code).
4841
5012
  - This skill refines scope and acceptance criteria but does not approve implementation readiness.
4842
5013
  Readiness requires human confirmation through \`kaddo ready\` or the MCP \`mark_work_item_ready\` action.
4843
5014
 
@@ -5989,6 +6160,16 @@ supporting notes as \`knowledge/tech/discovery/architecture-notes.md\` and decis
5989
6160
  under \`knowledge/tech/decisions/\`. Kaddo still reads the legacy root locations for backward
5990
6161
  compatibility, but new output should use \`discovery/\`.
5991
6162
 
6163
+ ## Project Resource candidates
6164
+
6165
+ When the knowledge describes an external system the project depends on (a database, cloud account,
6166
+ API, queue, storage, service\u2026), you may propose it as a **candidate** Project Resource \u2014 never create
6167
+ it silently. Discovery \u2260 creation: present candidates for human review and materialize only through
6168
+ \`kaddo_create_resource\` / \`kaddo resources create\` with explicit confirmation. Ground every
6169
+ candidate in the knowledge (no guessing from ambiguous names). Model the stable system, not its access
6170
+ interface (CLI/MCP/SQL/API are interfaces); keep \`resources\` separate from \`affected_modules\`; and
6171
+ never include secret values \u2014 only reference names.
6172
+
5992
6173
  ## Quality Checklist
5993
6174
 
5994
6175
  - Every component is backed by evidence from the context pack.
@@ -7877,7 +8058,7 @@ function canonicalSkills() {
7877
8058
  }
7878
8059
  function versionOf(content) {
7879
8060
  try {
7880
- const v = matter7(content).data.version;
8061
+ const v = matter8(content).data.version;
7881
8062
  return v === void 0 || v === null ? null : String(v);
7882
8063
  } catch {
7883
8064
  return null;
@@ -8649,7 +8830,7 @@ function renderRouteCompact(route) {
8649
8830
  }
8650
8831
 
8651
8832
  // ../cli/src/core/metadata-health.ts
8652
- import matter8 from "gray-matter";
8833
+ import matter9 from "gray-matter";
8653
8834
  var KNOWLEDGE_FILES2 = [
8654
8835
  "knowledge/business/business.md",
8655
8836
  "knowledge/product/product.md",
@@ -8669,7 +8850,7 @@ function analyzeMetadataHealth(dir) {
8669
8850
  let data;
8670
8851
  try {
8671
8852
  const raw = readFile(p);
8672
- data = matter8(raw).data;
8853
+ data = matter9(raw).data;
8673
8854
  } catch {
8674
8855
  continue;
8675
8856
  }
@@ -8701,7 +8882,7 @@ function analyzeMetadataHealth(dir) {
8701
8882
  if (isFile(p) && entry.endsWith(".md")) {
8702
8883
  try {
8703
8884
  const raw = readFile(p);
8704
- const fm = matter8(raw).data;
8885
+ const fm = matter9(raw).data;
8705
8886
  if (fm.status === "done") {
8706
8887
  const rel = p.replace(dir + "/", "").replace(dir + "\\", "").replace(/\\/g, "/");
8707
8888
  findings.push({
@@ -9569,7 +9750,7 @@ function buildGuardHistory(dir) {
9569
9750
  const d3 = hotspotDir(t.code_path);
9570
9751
  hotspotMap.set(d3, (hotspotMap.get(d3) ?? 0) + 1);
9571
9752
  }
9572
- const hotspots = [...hotspotMap.entries()].map(([path7, warnings]) => ({ path: path7, warnings })).sort((a2, b2) => b2.warnings - a2.warnings);
9753
+ const hotspots = [...hotspotMap.entries()].map(([path9, warnings]) => ({ path: path9, warnings })).sort((a2, b2) => b2.warnings - a2.warnings);
9573
9754
  return {
9574
9755
  available: true,
9575
9756
  total_runs: sorted.length,
@@ -9656,7 +9837,7 @@ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
9656
9837
  }
9657
9838
  }
9658
9839
  try {
9659
- const body = matter9(readFile(wi.filePath)).content;
9840
+ const body = matter10(readFile(wi.filePath)).content;
9660
9841
  if (hasSection(body, /acceptance|criterios de aceptaci/i)) withAcceptance++;
9661
9842
  else gaps.missing_acceptance_criteria.push(gapItem(wi, "Add an `## Acceptance Criteria` section."));
9662
9843
  if (hasSection(body, /definition of done|^#{1,6}\s*dod\b|definici[oó]n de (terminado|hecho)/i)) withDoD++;
@@ -10369,7 +10550,7 @@ function serializeDriftJson(r2) {
10369
10550
  }
10370
10551
 
10371
10552
  // src/workitems.ts
10372
- import matter10 from "gray-matter";
10553
+ import matter11 from "gray-matter";
10373
10554
  var VALID_SOURCES2 = /* @__PURE__ */ new Set(["manual", "roadmap", "jira", "github", "notion", "xlsx", "csv", "api", "external", "unknown"]);
10374
10555
  function optStr3(v) {
10375
10556
  return typeof v === "string" && v.trim() ? v.trim() : void 0;
@@ -10392,7 +10573,7 @@ function parseSource(data) {
10392
10573
  return { type: "unknown", inferred: true };
10393
10574
  }
10394
10575
  var ACTIVE_STATES2 = /* @__PURE__ */ new Set(["draft", "ready", "in-progress", "blocked"]);
10395
- function strArray(v) {
10576
+ function strArray2(v) {
10396
10577
  return Array.isArray(v) ? v.map((x2) => String(x2)).filter(Boolean) : [];
10397
10578
  }
10398
10579
  function lifecycleOf(data, relPath) {
@@ -10460,15 +10641,15 @@ function listWorkItems(root) {
10460
10641
  if (raw === null) continue;
10461
10642
  let parsed;
10462
10643
  try {
10463
- parsed = matter10(raw);
10644
+ parsed = matter11(raw);
10464
10645
  } catch {
10465
10646
  continue;
10466
10647
  }
10467
10648
  const data = parsed.data;
10468
10649
  if (!data.type) continue;
10469
10650
  const status = lifecycleOf(data, rel);
10470
- const code = strArray(data.code);
10471
- const domains = strArray(data.domains);
10651
+ const code = strArray2(data.code);
10652
+ const domains = strArray2(data.domains);
10472
10653
  const source = parseSource(data);
10473
10654
  const readiness = status === "draft" ? assessReadiness(data, parsed.content, code, domains, source) : void 0;
10474
10655
  items.push({
@@ -10480,9 +10661,9 @@ function listWorkItems(root) {
10480
10661
  path: rel,
10481
10662
  summary: String(data.summary ?? "") || firstParagraph2(parsed.content),
10482
10663
  code,
10483
- capabilities: strArray(data.capabilities),
10484
- decisions: strArray(data.decisions),
10485
- capsules: strArray(data.capsules),
10664
+ capabilities: strArray2(data.capabilities),
10665
+ decisions: strArray2(data.decisions),
10666
+ capsules: strArray2(data.capsules),
10486
10667
  source,
10487
10668
  domains,
10488
10669
  readiness,
@@ -10928,14 +11109,14 @@ function getPrompt(root, name) {
10928
11109
  }
10929
11110
 
10930
11111
  // src/tools.ts
10931
- import matter12 from "gray-matter";
11112
+ import matter15 from "gray-matter";
10932
11113
 
10933
11114
  // ../cli/dist/core.js
10934
11115
  import fs3 from "fs";
10935
11116
  import path3 from "path";
10936
11117
  import { z as z3 } from "zod";
10937
11118
  import { parse as parseYaml10 } from "yaml";
10938
- import matter11 from "gray-matter";
11119
+ import matter12 from "gray-matter";
10939
11120
  import matter22 from "gray-matter";
10940
11121
  import { parse as parseYaml62, stringify as stringifyYaml32 } from "yaml";
10941
11122
  import { parse as parseYaml22 } from "yaml";
@@ -10944,25 +11125,27 @@ import { parse as parseYaml52, stringify as stringifyYaml22 } from "yaml";
10944
11125
  import { parse as parseYaml42, stringify as stringifyYaml4 } from "yaml";
10945
11126
  import matter32 from "gray-matter";
10946
11127
  import matter42 from "gray-matter";
10947
- import { parse as parseYaml72 } from "yaml";
10948
11128
  import matter52 from "gray-matter";
11129
+ import { parse as parseYaml72 } from "yaml";
11130
+ import matter62 from "gray-matter";
10949
11131
  import { fileURLToPath as fileURLToPath2 } from "url";
10950
11132
  import { dirname as dirname2, join as join22, parse as parse2 } from "path";
10951
11133
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
10952
- import matter62 from "gray-matter";
10953
11134
  import matter72 from "gray-matter";
10954
- import { parse as parseYaml82, stringify as stringifyYaml42 } from "yaml";
10955
- import fs32 from "fs";
10956
- import path32 from "path";
10957
- import crypto2 from "crypto";
10958
11135
  import matter82 from "gray-matter";
10959
- import crypto3 from "crypto";
10960
- import fs4 from "fs";
10961
11136
  import matter92 from "gray-matter";
11137
+ import { parse as parseYaml82, stringify as stringifyYaml42 } from "yaml";
11138
+ import fs4 from "fs";
11139
+ import path4 from "path";
11140
+ import crypto2 from "crypto";
10962
11141
  import matter102 from "gray-matter";
11142
+ import crypto3 from "crypto";
11143
+ import fs5 from "fs";
11144
+ import matter112 from "gray-matter";
11145
+ import matter122 from "gray-matter";
10963
11146
  import { parse as parseYaml92, stringify as stringifyYaml5 } from "yaml";
10964
11147
  import { parse as parseYaml102, stringify as stringifyYaml6 } from "yaml";
10965
- import matter112 from "gray-matter";
11148
+ import matter13 from "gray-matter";
10966
11149
  function exists2(p) {
10967
11150
  return fs3.existsSync(p);
10968
11151
  }
@@ -11073,8 +11256,8 @@ function loadConfig2(dir) {
11073
11256
  const parsed = configSchema2.safeParse(raw ?? {});
11074
11257
  if (!parsed.success) {
11075
11258
  const issues = parsed.error.issues.map((i) => {
11076
- const path42 = i.path.join(".");
11077
- return path42 ? ` - ${path42}: ${i.message}` : ` - ${i.message}`;
11259
+ const path52 = i.path.join(".");
11260
+ return path52 ? ` - ${path52}: ${i.message}` : ` - ${i.message}`;
11078
11261
  }).join("\n");
11079
11262
  throw new ConfigError2(`Invalid .kaddo/config.yml:
11080
11263
  ${issues}`);
@@ -11130,7 +11313,7 @@ function parseImpactAnalysis2(data) {
11130
11313
  }
11131
11314
  function parseArtifact2(filePath, raw) {
11132
11315
  try {
11133
- const { data } = matter11(raw);
11316
+ const { data } = matter12(raw);
11134
11317
  const globs = Array.isArray(data.code) ? data.code.filter(Boolean) : [];
11135
11318
  return {
11136
11319
  filePath,
@@ -11278,11 +11461,11 @@ function moduleArtifactCoverage2(dir, id) {
11278
11461
  };
11279
11462
  }
11280
11463
  function loadMappedModules2(dir) {
11281
- const path42 = join3(dir, DESCRIPTOR_PATH2);
11282
- if (!exists2(path42)) return [];
11464
+ const path52 = join3(dir, DESCRIPTOR_PATH2);
11465
+ if (!exists2(path52)) return [];
11283
11466
  let parsed;
11284
11467
  try {
11285
- parsed = parseYaml32(readFile2(path42));
11468
+ parsed = parseYaml32(readFile2(path52));
11286
11469
  } catch {
11287
11470
  return [];
11288
11471
  }
@@ -12684,7 +12867,129 @@ function suggestInitiativesForExternalItem(dir, integration, externalId) {
12684
12867
  )
12685
12868
  );
12686
12869
  }
12870
+ var RESOURCE_ARTIFACT_TYPE2 = "project-resource";
12871
+ function str2(v) {
12872
+ return typeof v === "string" && v.trim() ? v.trim() : null;
12873
+ }
12874
+ function strArray3(v) {
12875
+ if (Array.isArray(v)) return v.map((x2) => String(x2).trim()).filter(Boolean);
12876
+ if (typeof v === "string" && v.trim()) return [v.trim()];
12877
+ return [];
12878
+ }
12879
+ function parseInterfaces2(v) {
12880
+ if (!Array.isArray(v)) return [];
12881
+ const out = [];
12882
+ for (const raw of v) {
12883
+ if (!raw || typeof raw !== "object") continue;
12884
+ const o2 = raw;
12885
+ const type = str2(o2.type);
12886
+ if (!type) continue;
12887
+ out.push({
12888
+ type,
12889
+ tool: str2(o2.tool),
12890
+ provider: str2(o2.provider),
12891
+ purpose: str2(o2.purpose),
12892
+ operations: strArray3(o2.operations),
12893
+ environments: strArray3(o2.environments),
12894
+ constraints: str2(o2.constraints)
12895
+ });
12896
+ }
12897
+ return out;
12898
+ }
12899
+ function parseBoundaries2(v) {
12900
+ if (!v || typeof v !== "object" || Array.isArray(v)) return {};
12901
+ const out = {};
12902
+ for (const [env, val] of Object.entries(v)) {
12903
+ const list22 = strArray3(val);
12904
+ if (list22.length > 0) out[env] = list22;
12905
+ }
12906
+ return out;
12907
+ }
12908
+ function parseAuth2(v) {
12909
+ if (!v || typeof v !== "object" || Array.isArray(v)) return { mode: null, refs: [] };
12910
+ const o2 = v;
12911
+ const refs = strArray3(o2.refs).filter((r2) => !r2.includes("=") && /^[A-Za-z0-9_.-]+$/.test(r2));
12912
+ return { mode: str2(o2.mode), refs };
12913
+ }
12914
+ function parseScope2(v) {
12915
+ if (!v || typeof v !== "object" || Array.isArray(v)) return null;
12916
+ const o2 = v;
12917
+ const type = str2(o2.type);
12918
+ if (!type) return null;
12919
+ return { type, module: str2(o2.module) };
12920
+ }
12921
+ function purposeFromBody2(body) {
12922
+ const m2 = body.match(/^#{1,2}\s+Purpose\s*\n+([\s\S]*?)(?=\n#{1,2}\s|\s*$)/m);
12923
+ const text22 = m2 ? m2[1].trim() : "";
12924
+ return text22 || null;
12925
+ }
12926
+ function toDetail2(filePath, relPath, raw) {
12927
+ const parsed = matter42(raw);
12928
+ const fm = parsed.data ?? {};
12929
+ const body = parsed.content.trim();
12930
+ const id = str2(fm.id) ?? "";
12931
+ return {
12932
+ id,
12933
+ title: str2(fm.title) ?? id,
12934
+ resourceType: str2(fm.resource_type),
12935
+ provider: str2(fm.provider),
12936
+ environments: strArray3(fm.environments),
12937
+ scope: parseScope2(fm.scope),
12938
+ modules: strArray3(fm.modules),
12939
+ path: relPath,
12940
+ purpose: str2(fm.purpose) ?? purposeFromBody2(body),
12941
+ interfaces: parseInterfaces2(fm.access_interfaces),
12942
+ boundaries: parseBoundaries2(fm.access_boundaries),
12943
+ ...(() => {
12944
+ const a2 = parseAuth2(fm.authentication);
12945
+ return { authRefs: a2.refs, authMode: a2.mode };
12946
+ })(),
12947
+ markdownBody: body
12948
+ };
12949
+ }
12950
+ function getResources2(dir) {
12951
+ return discoverKnowledge2(dir).filter((a2) => a2.type === RESOURCE_ARTIFACT_TYPE2).map((a2) => {
12952
+ const fm = a2.rawFrontmatter;
12953
+ return {
12954
+ id: a2.id || str2(fm.id) || a2.title,
12955
+ title: a2.title || str2(fm.title) || a2.id,
12956
+ resourceType: str2(fm.resource_type),
12957
+ provider: str2(fm.provider),
12958
+ environments: strArray3(fm.environments),
12959
+ scope: parseScope2(fm.scope),
12960
+ modules: strArray3(fm.modules),
12961
+ path: a2.relPath
12962
+ };
12963
+ }).sort((x2, y) => x2.id.localeCompare(y.id));
12964
+ }
12965
+ function getResource2(dir, id) {
12966
+ const match = discoverKnowledge2(dir).find(
12967
+ (a2) => a2.type === RESOURCE_ARTIFACT_TYPE2 && (a2.id === id || a2.title === id)
12968
+ );
12969
+ if (!match) return null;
12970
+ const raw = readFile2(match.filePath);
12971
+ return toDetail2(match.filePath, match.relPath, raw);
12972
+ }
12973
+ function parseWorkItemResources2(fm) {
12974
+ const v = fm.resources;
12975
+ if (!Array.isArray(v)) return [];
12976
+ const out = [];
12977
+ for (const raw of v) {
12978
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
12979
+ const o2 = raw;
12980
+ const id = str2(o2.id);
12981
+ if (!id) continue;
12982
+ out.push({ id, role: str2(o2.role) ?? "affected" });
12983
+ }
12984
+ return out;
12985
+ }
12687
12986
  var KNOWLEDGE22 = "knowledge";
12987
+ var RESOURCE_ROLE_EDGE2 = {
12988
+ affected: "affects",
12989
+ implementation: "uses",
12990
+ validation: "validates_with",
12991
+ delivery: "delivers_through"
12992
+ };
12688
12993
  var ACTIVE_STATUSES2 = ["draft", "ready", "in-progress", "blocked"];
12689
12994
  var ALL_STATUSES2 = ["draft", "ready", "in-progress", "blocked", "completed"];
12690
12995
  function scopeStatuses2(scope) {
@@ -12721,9 +13026,9 @@ function buildGraph2(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
12721
13026
  ];
12722
13027
  const presentLayers = [];
12723
13028
  for (const layer2 of layerDocs) {
12724
- const path42 = layer2.files.map((f3) => `${KNOWLEDGE22}/${f3}`).find((rel) => exists2(join3(dir, rel)));
12725
- if (path42) {
12726
- addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path42 });
13029
+ const path52 = layer2.files.map((f3) => `${KNOWLEDGE22}/${f3}`).find((rel) => exists2(join3(dir, rel)));
13030
+ if (path52) {
13031
+ addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path52 });
12727
13032
  presentLayers.push(layer2.id);
12728
13033
  }
12729
13034
  }
@@ -12763,6 +13068,24 @@ function buildGraph2(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
12763
13068
  if (cand.materializedAs) addEdge(candId, `wi:${cand.materializedAs}`, "materialized_as");
12764
13069
  }
12765
13070
  }
13071
+ const resourceTitleById = /* @__PURE__ */ new Map();
13072
+ for (const r2 of getResources2(dir)) {
13073
+ resourceTitleById.set(r2.id, r2.title);
13074
+ const resId = `resource:${r2.id}`;
13075
+ addNode({
13076
+ id: resId,
13077
+ type: "project-resource",
13078
+ label: r2.title ? `${r2.id} ${r2.title}`.trim() : r2.id,
13079
+ path: r2.path
13080
+ });
13081
+ const depModules = new Set(r2.modules);
13082
+ if (r2.scope?.type === "module" && r2.scope.module) depModules.add(r2.scope.module);
13083
+ for (const mod of depModules) {
13084
+ const modId = `module:${mod}`;
13085
+ addNode({ id: modId, type: "module", label: mod });
13086
+ addEdge(modId, resId, "depends_on");
13087
+ }
13088
+ }
12766
13089
  for (const wi of selectedWIs) {
12767
13090
  const id = wi.id || wi.title;
12768
13091
  if (!id || !id.trim()) continue;
@@ -12787,6 +13110,12 @@ function buildGraph2(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
12787
13110
  addNode({ id: capId, type: "capability", label: cap });
12788
13111
  addEdge(wiNodeId, capId, "implements");
12789
13112
  }
13113
+ for (const rel of parseWorkItemResources2(wi.rawFrontmatter)) {
13114
+ const resId = `resource:${rel.id}`;
13115
+ const title = resourceTitleById.get(rel.id);
13116
+ addNode({ id: resId, type: "project-resource", label: title ? `${rel.id} ${title}`.trim() : rel.id });
13117
+ addEdge(wiNodeId, resId, RESOURCE_ROLE_EDGE2[rel.role] ?? "uses");
13118
+ }
12790
13119
  for (const dec of wi.decisions) {
12791
13120
  if (!dec || !dec.trim()) continue;
12792
13121
  const adrId = `adr:${dec}`;
@@ -13280,8 +13609,13 @@ dependencies.
13280
13609
  flags, content/copy, authentication/authorization, notifications, analytics, documentation,
13281
13610
  operations/release \u2014 as affected, reviewed-not-affected, unknown, or not-applicable.
13282
13611
  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.
13612
+ 5. **Resource review** \u2014 ask: does this change need to observe, modify, validate or deliver against a
13613
+ known external resource of the project (a Project Resource under \`knowledge/tech/resources/\` \u2014
13614
+ database, cloud, API, queue, storage\u2026)? If so, declare it in the WI front matter \`resources:\` with
13615
+ a role (\`affected\`, \`implementation\`, \`validation\`, \`delivery\`). Resources are external systems,
13616
+ distinct from \`affected_modules\` (code). Not every WI needs resources (e.g. a CSS change does not).
13617
+ 6. **Completeness review** \u2014 confirm: outcome covered, journey covered, modules assessed, resources
13618
+ assessed, unknowns visible, acceptance criteria end-to-end, scope and out-of-scope coherent.
13285
13619
 
13286
13620
  ## Rules
13287
13621
 
@@ -13292,6 +13626,8 @@ dependencies.
13292
13626
  - Include at least one end-to-end acceptance criterion for user-facing changes.
13293
13627
  - Do not reduce a product intent to the first technical implementation found.
13294
13628
  - Evaluate surfaces and modules before proposing files.
13629
+ - Consider Project Resources when the scope justifies it; never invent resources not backed by
13630
+ project knowledge, and keep \`resources\` (external systems) separate from \`affected_modules\` (code).
13295
13631
  - This skill refines scope and acceptance criteria but does not approve implementation readiness.
13296
13632
  Readiness requires human confirmation through \`kaddo ready\` or the MCP \`mark_work_item_ready\` action.
13297
13633
 
@@ -14436,6 +14772,16 @@ supporting notes as \`knowledge/tech/discovery/architecture-notes.md\` and decis
14436
14772
  under \`knowledge/tech/decisions/\`. Kaddo still reads the legacy root locations for backward
14437
14773
  compatibility, but new output should use \`discovery/\`.
14438
14774
 
14775
+ ## Project Resource candidates
14776
+
14777
+ When the knowledge describes an external system the project depends on (a database, cloud account,
14778
+ API, queue, storage, service\u2026), you may propose it as a **candidate** Project Resource \u2014 never create
14779
+ it silently. Discovery \u2260 creation: present candidates for human review and materialize only through
14780
+ \`kaddo_create_resource\` / \`kaddo resources create\` with explicit confirmation. Ground every
14781
+ candidate in the knowledge (no guessing from ambiguous names). Model the stable system, not its access
14782
+ interface (CLI/MCP/SQL/API are interfaces); keep \`resources\` separate from \`affected_modules\`; and
14783
+ never include secret values \u2014 only reference names.
14784
+
14439
14785
  ## Quality Checklist
14440
14786
 
14441
14787
  - Every component is backed by evidence from the context pack.
@@ -16979,6 +17325,8 @@ function getWorkItem(dir, workItemId) {
16979
17325
  path: match.relPath,
16980
17326
  // Faithful source of truth: the artifact body already read above, not reconstructed (WI-025).
16981
17327
  markdownBody: body.trim(),
17328
+ // External systems this WI relates to (WI-030), separate from affectedModules (code).
17329
+ resources: parseWorkItemResources2(fm),
16982
17330
  ...parseSystemImpact(dir, fm)
16983
17331
  };
16984
17332
  return { ...detail, refinement: computeRefinementStatus(detail) };
@@ -16987,8 +17335,8 @@ function parseGraphReason(v) {
16987
17335
  if (!v || typeof v !== "object") return null;
16988
17336
  const o2 = v;
16989
17337
  const relationship = typeof o2.relationship === "string" && o2.relationship.trim() ? o2.relationship.trim() : null;
16990
- const path42 = Array.isArray(o2.path) ? o2.path.map(String).filter(Boolean) : [];
16991
- return relationship || path42.length ? { relationship, path: path42 } : null;
17338
+ const path52 = Array.isArray(o2.path) ? o2.path.map(String).filter(Boolean) : [];
17339
+ return relationship || path52.length ? { relationship, path: path52 } : null;
16992
17340
  }
16993
17341
  function parseExplain(o2) {
16994
17342
  const refs = Array.isArray(o2.evidence) ? o2.evidence : Array.isArray(o2.evidence_refs) ? o2.evidence_refs : [];
@@ -17043,7 +17391,7 @@ function parseOriginalSnapshot(fm) {
17043
17391
  function readBody(filePath) {
17044
17392
  try {
17045
17393
  const raw = readFile2(filePath);
17046
- return matter72(raw).content;
17394
+ return matter92(raw).content;
17047
17395
  } catch {
17048
17396
  return "";
17049
17397
  }
@@ -17245,7 +17593,7 @@ function nextWorkItemId(dir) {
17245
17593
  let max = 0;
17246
17594
  const walk = (d3) => {
17247
17595
  if (!exists2(d3)) return;
17248
- for (const entry of fs32.readdirSync(d3)) {
17596
+ for (const entry of fs4.readdirSync(d3)) {
17249
17597
  const full = join3(d3, entry);
17250
17598
  if (isFile2(full)) {
17251
17599
  const m2 = entry.match(/WI-(\d+)/);
@@ -17258,15 +17606,15 @@ function nextWorkItemId(dir) {
17258
17606
  walk(wiDir);
17259
17607
  return `WI-${String(max + 1).padStart(3, "0")}`;
17260
17608
  }
17261
- function atomicWrite2(filePath, content) {
17262
- fs32.mkdirSync(path32.dirname(filePath), { recursive: true });
17609
+ function atomicWrite3(filePath, content) {
17610
+ fs4.mkdirSync(path4.dirname(filePath), { recursive: true });
17263
17611
  const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
17264
- fs32.writeFileSync(tmp, content, "utf-8");
17612
+ fs4.writeFileSync(tmp, content, "utf-8");
17265
17613
  try {
17266
- fs32.renameSync(tmp, filePath);
17614
+ fs4.renameSync(tmp, filePath);
17267
17615
  } catch (err) {
17268
17616
  try {
17269
- fs32.rmSync(tmp, { force: true });
17617
+ fs4.rmSync(tmp, { force: true });
17270
17618
  } catch {
17271
17619
  }
17272
17620
  throw err;
@@ -17327,8 +17675,8 @@ function ensureH1(body, title) {
17327
17675
 
17328
17676
  ${trimmed}`;
17329
17677
  }
17330
- function serialize(data, body) {
17331
- return matter82.stringify(`
17678
+ function serialize2(data, body) {
17679
+ return matter102.stringify(`
17332
17680
  ${body.trim()}
17333
17681
  `, data);
17334
17682
  }
@@ -17396,8 +17744,8 @@ function createWorkItem(dir, opts) {
17396
17744
  const relPath = `${WORK_ITEMS_DIR}/draft/${id}-${slugify22(title)}.md`;
17397
17745
  const filePath = join3(dir, relPath);
17398
17746
  if (exists2(filePath)) throw new WorkItemWriteError("INVALID_INPUT", `Work Item file already exists: ${relPath}`);
17399
- const raw = serialize(data, body);
17400
- atomicWrite2(filePath, raw);
17747
+ const raw = serialize2(data, body);
17748
+ atomicWrite3(filePath, raw);
17401
17749
  return { id, path: relPath, revision: revisionOf(raw) };
17402
17750
  }
17403
17751
  function dimensionOf(type) {
@@ -17722,6 +18070,7 @@ function buildImplementationHandoff(dir, workItemId) {
17722
18070
  const affectedModules = match.affectedModules;
17723
18071
  const domains = match.domains;
17724
18072
  const scopeConfidence = match.scopeConfidence;
18073
+ const wiResources = parseWorkItemResources2(match.rawFrontmatter);
17725
18074
  const lines = [
17726
18075
  `Implement Work Item ${wiId} \u2014 "${wiTitle}" \u2014 in project "${projectName}" using Kaddo.`,
17727
18076
  "",
@@ -17811,6 +18160,41 @@ function buildImplementationHandoff(dir, workItemId) {
17811
18160
  "Do not copy all legacy knowledge \u2014 assemble proportionally to scope."
17812
18161
  );
17813
18162
  }
18163
+ if (wiResources.length > 0) {
18164
+ lines.push(
18165
+ "",
18166
+ "--- Relevant Resources ---",
18167
+ "",
18168
+ "This Work Item relates to external project resources. The handoff INFORMS you about them \u2014 it",
18169
+ "never connects to them, runs any interface, or reveals credential values. Respect each boundary."
18170
+ );
18171
+ for (const rel of wiResources) {
18172
+ const res = getResource2(dir, rel.id);
18173
+ if (!res) {
18174
+ lines.push("", `- ${rel.id} (role: ${rel.role}) \u2014 definition not found under knowledge/tech/resources/.`);
18175
+ continue;
18176
+ }
18177
+ lines.push("", `- ${res.id} \u2014 ${res.title} (role: ${rel.role})`);
18178
+ if (res.resourceType || res.provider) {
18179
+ lines.push(` type: ${res.resourceType ?? "\u2014"}${res.provider ? ` \xB7 provider: ${res.provider}` : ""}`);
18180
+ }
18181
+ if (res.purpose) lines.push(` purpose: ${res.purpose}`);
18182
+ if (res.environments.length > 0) lines.push(` environments: ${res.environments.join(", ")}`);
18183
+ if (res.interfaces.length > 0) {
18184
+ const ifaces = res.interfaces.map((i) => i.tool || i.provider ? `${i.type} (${i.tool ?? i.provider})` : i.type);
18185
+ lines.push(` interfaces: ${ifaces.join(", ")}`);
18186
+ }
18187
+ for (const [env, ops] of Object.entries(res.boundaries)) {
18188
+ lines.push(` boundary (${env}): ${ops.join(", ")}`);
18189
+ }
18190
+ if (res.authRefs.length > 0) lines.push(` auth references (names only): ${res.authRefs.join(", ")}`);
18191
+ }
18192
+ lines.push(
18193
+ "",
18194
+ "Treat these as knowledge, not availability: an interface being declared does not mean it is",
18195
+ "installed or connected. Do not execute any interface or migration without explicit human confirmation."
18196
+ );
18197
+ }
17814
18198
  lines.push(
17815
18199
  "",
17816
18200
  "--- Implementation Plan ---",
@@ -17831,6 +18215,7 @@ function buildImplementationHandoff(dir, workItemId) {
17831
18215
  projectName,
17832
18216
  lifecycle,
17833
18217
  affectedModules,
18218
+ resources: wiResources,
17834
18219
  domains,
17835
18220
  scopeConfidence,
17836
18221
  hasUnmaterializedDecisions: unmaterialized,
@@ -18082,7 +18467,7 @@ function detectFormat(raw) {
18082
18467
  if (!trimmed) throw new ImportError("EMPTY_INPUT", "Import content is empty.");
18083
18468
  if (trimmed.startsWith("---")) {
18084
18469
  try {
18085
- const { data } = matter92(trimmed);
18470
+ const { data } = matter112(trimmed);
18086
18471
  if (data && typeof data === "object" && Object.keys(data).length > 0) {
18087
18472
  const d3 = data;
18088
18473
  if (d3.type === "work-item" || d3.id && typeof d3.id === "string" && /^WI-\d+/.test(d3.id)) {
@@ -18104,7 +18489,7 @@ function parseContent(raw) {
18104
18489
  let bodyText = trimmed;
18105
18490
  if (format === "kaddo-frontmatter" || format === "markdown-frontmatter") {
18106
18491
  try {
18107
- const parsed = matter92(trimmed);
18492
+ const parsed = matter112(trimmed);
18108
18493
  frontmatterFields = parsed.data ?? {};
18109
18494
  bodyText = parsed.content.trim();
18110
18495
  } catch {
@@ -18201,7 +18586,7 @@ function findDuplicateByHash(dir, hash) {
18201
18586
  const raw = readFile2(item.filePath);
18202
18587
  if (!raw) continue;
18203
18588
  try {
18204
- const { data } = matter92(raw);
18589
+ const { data } = matter112(raw);
18205
18590
  const source = data?.source;
18206
18591
  if (source && typeof source === "object" && !Array.isArray(source)) {
18207
18592
  const s = source;
@@ -18268,7 +18653,7 @@ function importWorkItem(dir, opts) {
18268
18653
  };
18269
18654
  }
18270
18655
  if (opts.onConflict === "replace") {
18271
- fs4.rmSync(existing.filePath, { force: true });
18656
+ fs5.rmSync(existing.filePath, { force: true });
18272
18657
  replacedWorkItem = existing.workItemId;
18273
18658
  }
18274
18659
  }
@@ -18710,8 +19095,8 @@ async function handleJiraResponse(res) {
18710
19095
  }
18711
19096
  return await res.json();
18712
19097
  }
18713
- async function jiraFetch(baseUrl, path42, auth, timeoutMs, params) {
18714
- const url = new URL(path42, baseUrl + "/");
19098
+ async function jiraFetch(baseUrl, path52, auth, timeoutMs, params) {
19099
+ const url = new URL(path52, baseUrl + "/");
18715
19100
  if (params) for (const [k2, v] of Object.entries(params)) url.searchParams.set(k2, v);
18716
19101
  const controller = new AbortController();
18717
19102
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -18729,8 +19114,8 @@ async function jiraFetch(baseUrl, path42, auth, timeoutMs, params) {
18729
19114
  clearTimeout(timer);
18730
19115
  }
18731
19116
  }
18732
- async function jiraPost(baseUrl, path42, auth, timeoutMs, body) {
18733
- const url = new URL(path42, baseUrl + "/");
19117
+ async function jiraPost(baseUrl, path52, auth, timeoutMs, body) {
19118
+ const url = new URL(path52, baseUrl + "/");
18734
19119
  const controller = new AbortController();
18735
19120
  const timer = setTimeout(() => controller.abort(), timeoutMs);
18736
19121
  try {
@@ -19096,7 +19481,7 @@ function buildInitiativeContent(id, input) {
19096
19481
  if (input.priority) data.priority = input.priority;
19097
19482
  if (input.source) data.source = input.source;
19098
19483
  if (input.sourceId) data.source_id = input.sourceId;
19099
- return matter112.stringify("\n" + (input.body ?? defaultBody(input.title.trim())), data);
19484
+ return matter13.stringify("\n" + (input.body ?? defaultBody(input.title.trim())), data);
19100
19485
  }
19101
19486
  function createInitiative(dir, input) {
19102
19487
  if (!input.title?.trim()) throw new InitiativeWriteError("Initiative title is required.");
@@ -19120,10 +19505,10 @@ function transitionInitiative(dir, id, to) {
19120
19505
  throw new InitiativeWriteError(`Invalid transition ${ini.status} \u2192 ${to}.`);
19121
19506
  }
19122
19507
  const raw = readFile2(ini.filePath);
19123
- const { data, content } = matter112(raw, {});
19508
+ const { data, content } = matter13(raw, {});
19124
19509
  data.status = to;
19125
19510
  if (to === "completed") data.completed_at = today2();
19126
- const newContent = matter112.stringify(content, data);
19511
+ const newContent = matter13.stringify(content, data);
19127
19512
  writeFile2(ini.filePath, newContent);
19128
19513
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19129
19514
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after transition.");
@@ -19133,7 +19518,7 @@ function addCandidate(dir, id, candidate) {
19133
19518
  const ini = getInitiative(dir, id);
19134
19519
  if (!ini) throw new InitiativeWriteError(`Initiative ${id} not found.`);
19135
19520
  const raw = readFile2(ini.filePath);
19136
- const { data, content } = matter112(raw, {});
19521
+ const { data, content } = matter13(raw, {});
19137
19522
  const existing = Array.isArray(data.candidates) ? data.candidates : [];
19138
19523
  const fallbackId = `WI-CANDIDATE-${String(existing.length + 1).padStart(3, "0")}`;
19139
19524
  const normalized = normalizeCandidate(candidate, fallbackId);
@@ -19141,7 +19526,7 @@ function addCandidate(dir, id, candidate) {
19141
19526
  throw new InitiativeWriteError(`Candidate ${normalized.id} already exists in ${id}.`);
19142
19527
  }
19143
19528
  data.candidates = [...existing, candidateToData(normalized)];
19144
- const newContent = matter112.stringify(content, data);
19529
+ const newContent = matter13.stringify(content, data);
19145
19530
  writeFile2(ini.filePath, newContent);
19146
19531
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19147
19532
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after adding a candidate.");
@@ -19151,7 +19536,7 @@ function updateInitiative(dir, id, patch) {
19151
19536
  const ini = getInitiative(dir, id);
19152
19537
  if (!ini) throw new InitiativeWriteError(`Initiative ${id} not found.`);
19153
19538
  const raw = readFile2(ini.filePath);
19154
- const { data, content } = matter112(raw, {});
19539
+ const { data, content } = matter13(raw, {});
19155
19540
  if (patch.title !== void 0) {
19156
19541
  if (!patch.title.trim()) throw new InitiativeWriteError("Initiative title cannot be empty.");
19157
19542
  data.title = patch.title.trim();
@@ -19161,7 +19546,7 @@ function updateInitiative(dir, id, patch) {
19161
19546
  if (patch.knowledgeLevel !== void 0) data.knowledge_level = patch.knowledgeLevel;
19162
19547
  if (patch.domains !== void 0) data.domains = patch.domains;
19163
19548
  if (patch.relatedCapabilities !== void 0) data.related_capabilities = patch.relatedCapabilities;
19164
- const newContent = matter112.stringify(content, data);
19549
+ const newContent = matter13.stringify(content, data);
19165
19550
  writeFile2(ini.filePath, newContent);
19166
19551
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19167
19552
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after update.");
@@ -19179,10 +19564,10 @@ function addExternalLink(dir, id, link) {
19179
19564
  throw new InitiativeWriteError(`External link ${link.integration}:${link.externalId} already exists on ${id}.`);
19180
19565
  }
19181
19566
  const raw = readFile2(ini.filePath);
19182
- const { data, content } = matter112(raw, {});
19567
+ const { data, content } = matter13(raw, {});
19183
19568
  const existing = Array.isArray(data.external_links) ? data.external_links : [];
19184
19569
  data.external_links = [...existing, externalLinkToData(link)];
19185
- const newContent = matter112.stringify(content, data);
19570
+ const newContent = matter13.stringify(content, data);
19186
19571
  writeFile2(ini.filePath, newContent);
19187
19572
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19188
19573
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after adding an external link.");
@@ -19251,7 +19636,7 @@ function buildWorkItemContent(wiId, iniId, candidate) {
19251
19636
  "_What did we learn? Update after completion._",
19252
19637
  ""
19253
19638
  ].join("\n");
19254
- return matter112.stringify("\n" + body, data);
19639
+ return matter13.stringify("\n" + body, data);
19255
19640
  }
19256
19641
  function materializeCandidate(dir, id, candidateId) {
19257
19642
  const ini = getInitiative(dir, id);
@@ -19268,13 +19653,13 @@ function materializeCandidate(dir, id, candidateId) {
19268
19653
  ensureDir2(join3(dir, DRAFT_DIR));
19269
19654
  writeFile2(join3(dir, DRAFT_DIR, fileName), buildWorkItemContent(wiId, ini.id, candidate));
19270
19655
  const raw = readFile2(ini.filePath);
19271
- const { data, content } = matter112(raw, {});
19656
+ const { data, content } = matter13(raw, {});
19272
19657
  const cands = Array.isArray(data.candidates) ? data.candidates : [];
19273
19658
  for (const c of cands) {
19274
19659
  if (c && typeof c === "object" && String(c.id) === candidateId) c.materialized_as = wiId;
19275
19660
  }
19276
19661
  data.candidates = cands;
19277
- const newContent = matter112.stringify(content, data);
19662
+ const newContent = matter13.stringify(content, data);
19278
19663
  writeFile2(ini.filePath, newContent);
19279
19664
  const updated = parseInitiative2(dir, ini.filePath, newContent);
19280
19665
  if (!updated) throw new InitiativeWriteError("Failed to parse the Initiative after materialization.");
@@ -19385,6 +19770,164 @@ function evaluateInitiativeCompletion(dir, ini) {
19385
19770
  };
19386
19771
  }
19387
19772
 
19773
+ // ../cli/src/core/resource-write.ts
19774
+ import fs6 from "fs";
19775
+ import path5 from "path";
19776
+ import matter14 from "gray-matter";
19777
+ var RESOURCES_DIR = "knowledge/tech/resources";
19778
+ var ResourceWriteError = class extends Error {
19779
+ constructor(code, message) {
19780
+ super(message);
19781
+ this.code = code;
19782
+ this.name = "ResourceWriteError";
19783
+ }
19784
+ code;
19785
+ };
19786
+ function slug4(s) {
19787
+ return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
19788
+ }
19789
+ function atomicWrite(filePath, content) {
19790
+ const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
19791
+ fs6.writeFileSync(tmp, content, "utf-8");
19792
+ fs6.renameSync(tmp, filePath);
19793
+ }
19794
+ function sanitizeRefs(refs) {
19795
+ if (!Array.isArray(refs)) return [];
19796
+ return refs.map((r2) => String(r2).trim()).filter((r2) => r2 && !r2.includes("=") && /^[A-Za-z0-9_.-]+$/.test(r2));
19797
+ }
19798
+ function allResourceArtifacts(dir) {
19799
+ return discoverKnowledge(dir).filter((a2) => a2.type === RESOURCE_ARTIFACT_TYPE);
19800
+ }
19801
+ function nextResourceId(dir, title, explicit) {
19802
+ const existing = new Set(allResourceArtifacts(dir).map((a2) => (a2.id || "").toUpperCase()));
19803
+ if (explicit && explicit.trim()) return explicit.trim();
19804
+ const base = `RES-${slug4(title) || "resource"}`;
19805
+ if (!existing.has(base.toUpperCase())) return base;
19806
+ for (let i = 2; i < 1e3; i++) {
19807
+ const candidate = `${base}-${i}`;
19808
+ if (!existing.has(candidate.toUpperCase())) return candidate;
19809
+ }
19810
+ throw new ResourceWriteError("ID_EXHAUSTED", `Could not derive a unique id for "${title}".`);
19811
+ }
19812
+ function validateResource(dir, input) {
19813
+ const findings = [];
19814
+ if (input.resourceType && !RESOURCE_TYPES.includes(input.resourceType)) {
19815
+ findings.push({ level: "warning", message: `Unknown resource_type "${input.resourceType}" (not in the known set).` });
19816
+ }
19817
+ for (const i of input.accessInterfaces ?? []) {
19818
+ if (i.type && !INTERFACE_TYPES.includes(i.type)) {
19819
+ findings.push({ level: "warning", message: `Unknown access interface type "${i.type}".` });
19820
+ }
19821
+ }
19822
+ const mapped = loadMappedModules(dir).map((m2) => m2.id);
19823
+ if (mapped.length > 0) {
19824
+ const refs = /* @__PURE__ */ new Set();
19825
+ if (input.scope?.type === "module" && input.scope.module) refs.add(input.scope.module);
19826
+ for (const m2 of input.modules ?? []) refs.add(m2);
19827
+ for (const m2 of refs) {
19828
+ if (!mapped.includes(m2)) findings.push({ level: "warning", message: `Module "${m2}" is not in .kaddo/modules.yml.` });
19829
+ }
19830
+ }
19831
+ if (input.scope && input.scope.type === "module" && !input.scope.module) {
19832
+ findings.push({ level: "error", message: 'scope.type "module" requires a module id.' });
19833
+ }
19834
+ return findings;
19835
+ }
19836
+ function buildFrontmatter(id, input, base = {}) {
19837
+ const data = { ...base };
19838
+ data.type = RESOURCE_ARTIFACT_TYPE;
19839
+ data.id = id;
19840
+ if (input.title != null) data.title = input.title;
19841
+ if (input.resourceType != null) data.resource_type = input.resourceType;
19842
+ if (input.provider != null) data.provider = input.provider;
19843
+ if (input.environments != null) data.environments = input.environments;
19844
+ if (input.scope != null) data.scope = input.scope.module ? { type: input.scope.type, module: input.scope.module } : { type: input.scope.type };
19845
+ if (input.modules != null) data.modules = input.modules;
19846
+ if (input.accessInterfaces != null) data.access_interfaces = input.accessInterfaces;
19847
+ if (input.accessBoundaries != null) data.access_boundaries = input.accessBoundaries;
19848
+ if (input.authentication != null) {
19849
+ const mode = input.authentication.mode;
19850
+ const refs = sanitizeRefs(input.authentication.refs);
19851
+ data.authentication = { ...mode ? { mode } : {}, ...refs.length ? { refs } : {} };
19852
+ }
19853
+ return data;
19854
+ }
19855
+ function serialize(data, body) {
19856
+ return matter14.stringify(`
19857
+ ${body.trim()}
19858
+ `, data);
19859
+ }
19860
+ function createResource(dir, input) {
19861
+ const title = (input.title ?? "").trim();
19862
+ if (!title) throw new ResourceWriteError("INVALID_INPUT", "A resource title is required.");
19863
+ const resourceType = (input.resourceType ?? "").trim();
19864
+ if (!resourceType) throw new ResourceWriteError("INVALID_INPUT", "A resource_type is required.");
19865
+ const findings = validateResource(dir, input);
19866
+ if (findings.some((f3) => f3.level === "error")) {
19867
+ throw new ResourceWriteError("VALIDATION_FAILED", findings.filter((f3) => f3.level === "error").map((f3) => f3.message).join("; "));
19868
+ }
19869
+ const existing = allResourceArtifacts(dir);
19870
+ if (input.id && existing.some((a2) => (a2.id || "").toUpperCase() === input.id.toUpperCase())) {
19871
+ throw new ResourceWriteError("DUPLICATE", `A resource with id "${input.id}" already exists.`);
19872
+ }
19873
+ if (existing.some((a2) => (a2.title || "").toLowerCase() === title.toLowerCase())) {
19874
+ throw new ResourceWriteError("DUPLICATE", `A resource titled "${title}" already exists.`);
19875
+ }
19876
+ const id = nextResourceId(dir, title, input.id);
19877
+ const data = buildFrontmatter(id, input);
19878
+ const body = `# Purpose
19879
+
19880
+ ${(input.purpose ?? "").trim() || "TODO: describe what this resource is for."}`;
19881
+ const relPath = `${RESOURCES_DIR}/${slug4(title) || slug4(id)}.md`;
19882
+ const filePath = join(dir, relPath);
19883
+ if (exists(filePath)) throw new ResourceWriteError("DUPLICATE", `Resource file already exists: ${relPath}`);
19884
+ ensureDir(path5.dirname(filePath));
19885
+ atomicWrite(filePath, serialize(data, body));
19886
+ return { id, path: relPath, findings };
19887
+ }
19888
+ function updateResource(dir, id, input) {
19889
+ const match = allResourceArtifacts(dir).find((a2) => a2.id === id || a2.title === id);
19890
+ if (!match) throw new ResourceWriteError("NOT_FOUND", `Project Resource "${id}" not found.`);
19891
+ const findings = validateResource(dir, input);
19892
+ if (findings.some((f3) => f3.level === "error")) {
19893
+ throw new ResourceWriteError("VALIDATION_FAILED", findings.filter((f3) => f3.level === "error").map((f3) => f3.message).join("; "));
19894
+ }
19895
+ const raw = readFile(match.filePath);
19896
+ const parsed = matter14(raw);
19897
+ const base = parsed.data ?? {};
19898
+ const data = buildFrontmatter(match.id || id, input, base);
19899
+ let body = parsed.content.trim();
19900
+ if (input.purpose != null) {
19901
+ const purpose = `# Purpose
19902
+
19903
+ ${input.purpose.trim()}`;
19904
+ body = /^#\s+Purpose/m.test(body) ? body.replace(/#\s+Purpose[\s\S]*?(?=\n#\s|\n*$)/, purpose + "\n") : `${purpose}
19905
+
19906
+ ${body}`.trim();
19907
+ }
19908
+ atomicWrite(match.filePath, serialize(data, body));
19909
+ return { id: match.id || id, path: match.relPath, findings };
19910
+ }
19911
+ function getResourceReferences(dir, id) {
19912
+ const refs = [];
19913
+ for (const wi of discoverWorkItems(dir)) {
19914
+ for (const rel of parseWorkItemResources(wi.rawFrontmatter)) {
19915
+ if (rel.id === id) refs.push({ kind: "work-item", id: wi.id || wi.title, role: rel.role, path: wi.relPath });
19916
+ }
19917
+ }
19918
+ return refs;
19919
+ }
19920
+ function deleteResource(dir, id, opts = {}) {
19921
+ const match = allResourceArtifacts(dir).find((a2) => a2.id === id || a2.title === id);
19922
+ if (!match) throw new ResourceWriteError("NOT_FOUND", `Project Resource "${id}" not found.`);
19923
+ const references = getResourceReferences(dir, match.id || id);
19924
+ if (!opts.confirm) {
19925
+ return { deleted: false, preview: { id: match.id || id, references } };
19926
+ }
19927
+ fs6.rmSync(match.filePath, { force: true });
19928
+ return { deleted: true, path: match.relPath };
19929
+ }
19930
+
19388
19931
  // src/tools.ts
19389
19932
  var ok = (data) => ({ ok: true, data });
19390
19933
  var fail = (message) => ({ ok: false, message });
@@ -19480,12 +20023,12 @@ function markWorkItemReady(root, id, confirm2) {
19480
20023
  }
19481
20024
  const raw = readText(root, item.path);
19482
20025
  if (!raw) return fail(`Could not read Work Item file: ${item.path}`);
19483
- const parsed = matter12(raw);
20026
+ const parsed = matter15(raw);
19484
20027
  const data = { ...parsed.data };
19485
20028
  const today3 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
19486
20029
  data.status = "ready";
19487
20030
  data.ready_at = today3;
19488
- const newContent = matter12.stringify(parsed.content, data);
20031
+ const newContent = matter15.stringify(parsed.content, data);
19489
20032
  const posixPath = item.path.replace(/\\/g, "/");
19490
20033
  let newRelPath = item.path;
19491
20034
  if (posixPath.includes("/work-items/draft/")) {
@@ -19648,11 +20191,72 @@ function setTelemetryConsentTool(root, args) {
19648
20191
  persistConsent(root, consent);
19649
20192
  return ok({ applied: true, consent, message: `Telemetry ${consent}.` });
19650
20193
  }
20194
+ function listResourcesTool(root) {
20195
+ return ok(getResources(root));
20196
+ }
20197
+ function getResourceTool(root, id) {
20198
+ const resource = getResource(root, id);
20199
+ if (!resource) return fail(`Project Resource "${id}" not found.`);
20200
+ return ok(resource);
20201
+ }
20202
+ function toResourceInput(args) {
20203
+ const input = {};
20204
+ if (args.title != null) input.title = args.title;
20205
+ if (args.resource_type != null) input.resourceType = args.resource_type;
20206
+ if (args.provider != null) input.provider = args.provider;
20207
+ if (args.environments != null) input.environments = args.environments;
20208
+ if (args.scope != null || args.module != null) {
20209
+ const type = args.scope || (args.module ? "module" : "system");
20210
+ input.scope = type === "module" ? { type: "module", module: args.module ?? null } : { type: "system" };
20211
+ }
20212
+ if (args.modules != null) input.modules = args.modules;
20213
+ if (args.purpose != null) input.purpose = args.purpose;
20214
+ if (args.access_interfaces != null) input.accessInterfaces = args.access_interfaces;
20215
+ if (args.authentication != null) input.authentication = args.authentication;
20216
+ return input;
20217
+ }
20218
+ function createResourceTool(root, args, confirm2) {
20219
+ const input = toResourceInput(args);
20220
+ const findings = validateResource(root, input);
20221
+ if (!confirm2) {
20222
+ return ok({ status: "needs_confirmation", action: "create", proposal: input, findings, message: "Re-call with confirm=true to create this resource." });
20223
+ }
20224
+ try {
20225
+ return ok({ status: "created", ...createResource(root, input) });
20226
+ } catch (e) {
20227
+ return fail(e instanceof ResourceWriteError ? e.message : String(e));
20228
+ }
20229
+ }
20230
+ function updateResourceTool(root, id, args, confirm2) {
20231
+ if (!getResource(root, id)) return fail(`Project Resource "${id}" not found.`);
20232
+ const input = toResourceInput(args);
20233
+ const findings = validateResource(root, input);
20234
+ if (!confirm2) {
20235
+ return ok({ status: "needs_confirmation", action: "update", id, changes: input, findings, message: "Re-call with confirm=true to apply this update." });
20236
+ }
20237
+ try {
20238
+ return ok({ status: "updated", ...updateResource(root, id, input) });
20239
+ } catch (e) {
20240
+ return fail(e instanceof ResourceWriteError ? e.message : String(e));
20241
+ }
20242
+ }
20243
+ function deleteResourceTool(root, id, confirm2) {
20244
+ if (!getResource(root, id)) return fail(`Project Resource "${id}" not found.`);
20245
+ if (!confirm2) {
20246
+ const references = getResourceReferences(root, id);
20247
+ return ok({ status: "needs_confirmation", action: "delete", id, references, message: "Re-call with confirm=true to delete. References in other artifacts are NOT removed." });
20248
+ }
20249
+ try {
20250
+ return ok({ status: "deleted", ...deleteResource(root, id, { confirm: true }) });
20251
+ } catch (e) {
20252
+ return fail(e instanceof ResourceWriteError ? e.message : String(e));
20253
+ }
20254
+ }
19651
20255
 
19652
20256
  // src/multirepo.ts
19653
- import fs5 from "fs";
19654
- import path4 from "path";
19655
- import matter13 from "gray-matter";
20257
+ import fs7 from "fs";
20258
+ import path6 from "path";
20259
+ import matter16 from "gray-matter";
19656
20260
  import { parse as parseYaml11, stringify as stringifyYaml7 } from "yaml";
19657
20261
  var ok2 = (data) => ({ ok: true, data });
19658
20262
  var fail2 = (message) => ({ ok: false, message });
@@ -19672,24 +20276,24 @@ function isCore2(config) {
19672
20276
  return projectRole2(config) === "core";
19673
20277
  }
19674
20278
  function readModuleFile(root, repoPath, relFile) {
19675
- const moduleDir = path4.resolve(root, repoPath);
19676
- const abs = path4.resolve(moduleDir, relFile);
20279
+ const moduleDir = path6.resolve(root, repoPath);
20280
+ const abs = path6.resolve(moduleDir, relFile);
19677
20281
  if (!abs.startsWith(moduleDir)) return null;
19678
- if (!fs5.existsSync(abs) || !fs5.statSync(abs).isFile()) return null;
19679
- return fs5.readFileSync(abs, "utf-8");
20282
+ if (!fs7.existsSync(abs) || !fs7.statSync(abs).isFile()) return null;
20283
+ return fs7.readFileSync(abs, "utf-8");
19680
20284
  }
19681
20285
  function detectModuleStatus2(root, repoPath) {
19682
- const moduleDir = path4.resolve(root, repoPath);
19683
- if (!fs5.existsSync(moduleDir)) {
20286
+ const moduleDir = path6.resolve(root, repoPath);
20287
+ if (!fs7.existsSync(moduleDir)) {
19684
20288
  return { status: "missing", configured: false, moduleContext: false, currentState: false, codebase: false, warning: `Directory \`${repoPath}\` does not exist.` };
19685
20289
  }
19686
- const configPath = path4.join(moduleDir, ".kaddo", "config.yml");
19687
- if (!fs5.existsSync(configPath)) {
20290
+ const configPath = path6.join(moduleDir, ".kaddo", "config.yml");
20291
+ if (!fs7.existsSync(configPath)) {
19688
20292
  return { status: "not_configured", configured: false, moduleContext: false, currentState: false, codebase: false, warning: `Run \`kaddo init\` inside \`${repoPath}\` and select multirepo \u2192 module.` };
19689
20293
  }
19690
20294
  let parsed = null;
19691
20295
  try {
19692
- parsed = parseYaml11(fs5.readFileSync(configPath, "utf-8"));
20296
+ parsed = parseYaml11(fs7.readFileSync(configPath, "utf-8"));
19693
20297
  } catch {
19694
20298
  }
19695
20299
  const role = parsed?.project?.role ?? parsed?.multirepo?.role;
@@ -19700,14 +20304,14 @@ function detectModuleStatus2(root, repoPath) {
19700
20304
  status: "configured",
19701
20305
  configured: true,
19702
20306
  role: "module",
19703
- moduleContext: fs5.existsSync(path4.join(moduleDir, "knowledge", "tech", "module", "module-context.md")) || fs5.existsSync(path4.join(moduleDir, "knowledge", "module", "module-context.md")),
19704
- currentState: fs5.existsSync(path4.join(moduleDir, "knowledge", "tech", "current-state.md")),
19705
- codebase: fs5.existsSync(path4.join(moduleDir, "knowledge", "tech", "codebase.md"))
20307
+ moduleContext: fs7.existsSync(path6.join(moduleDir, "knowledge", "tech", "module", "module-context.md")) || fs7.existsSync(path6.join(moduleDir, "knowledge", "module", "module-context.md")),
20308
+ currentState: fs7.existsSync(path6.join(moduleDir, "knowledge", "tech", "current-state.md")),
20309
+ codebase: fs7.existsSync(path6.join(moduleDir, "knowledge", "tech", "codebase.md"))
19706
20310
  };
19707
20311
  }
19708
20312
  function resolveModuleContextPath(root, repoPath) {
19709
- const moduleDir = path4.resolve(root, repoPath);
19710
- if (fs5.existsSync(path4.join(moduleDir, "knowledge", "tech", "module", "module-context.md"))) {
20313
+ const moduleDir = path6.resolve(root, repoPath);
20314
+ if (fs7.existsSync(path6.join(moduleDir, "knowledge", "tech", "module", "module-context.md"))) {
19711
20315
  return "knowledge/tech/module/module-context.md";
19712
20316
  }
19713
20317
  return "knowledge/module/module-context.md";
@@ -19804,7 +20408,7 @@ function validateWorkItemModulesTool(root, args) {
19804
20408
  if (!wi) return fail2(`Work Item "${args.workItemId}" not found.`);
19805
20409
  const raw = readText(root, wi.path);
19806
20410
  if (!raw) return fail2(`Could not read Work Item file.`);
19807
- const parsed = matter13(raw);
20411
+ const parsed = matter16(raw);
19808
20412
  const data = parsed.data;
19809
20413
  const affectedModules = Array.isArray(data.affected_modules) ? data.affected_modules.map(String) : [];
19810
20414
  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 +20461,7 @@ function getWorkItemContextTool(root, args) {
19857
20461
  if (!wi) return fail2(`Work Item "${args.workItemId}" not found.`);
19858
20462
  const raw = readText(root, wi.path);
19859
20463
  if (!raw) return fail2(`Could not read Work Item file.`);
19860
- const parsed = matter13(raw);
20464
+ const parsed = matter16(raw);
19861
20465
  const data = parsed.data;
19862
20466
  const affectedModules = Array.isArray(data.affected_modules) ? data.affected_modules.map(String) : [];
19863
20467
  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) : [] })) : [];
@@ -19911,15 +20515,15 @@ function extractSection(content, heading) {
19911
20515
  return out;
19912
20516
  }
19913
20517
  function buildBranchStrategy(wi, affectedModules, codeEntries, descriptor) {
19914
- const slug5 = slugify4(wi.title);
20518
+ const slug6 = slugify4(wi.title);
19915
20519
  const wiLower = wi.id.toLowerCase();
19916
- const coreBranch = `feature/${wiLower}-${slug5}`;
20520
+ const coreBranch = `feature/${wiLower}-${slug6}`;
19917
20521
  const moduleBranches = {};
19918
20522
  const commitMessages = {};
19919
20523
  for (const modId of affectedModules) {
19920
20524
  const mod = (descriptor.modules ?? []).find((m2) => m2.id === modId);
19921
20525
  const repo = mod ? moduleRepoPath(mod) || modId : modId;
19922
- moduleBranches[repo] = `feature/${wiLower}-${modId}-${slug5.slice(0, 20)}`;
20526
+ moduleBranches[repo] = `feature/${wiLower}-${modId}-${slug6.slice(0, 20)}`;
19923
20527
  commitMessages[repo] = `fix(${wiLower}): update ${modId}`;
19924
20528
  }
19925
20529
  commitMessages["core"] = `docs(${wiLower}): capture cross-repo implementation summary`;
@@ -19943,7 +20547,7 @@ function suggestBranchStrategyTool(root, args) {
19943
20547
  if (!wi) return fail2(`Work Item "${args.workItemId}" not found.`);
19944
20548
  const raw = readText(root, wi.path);
19945
20549
  if (!raw) return fail2(`Could not read Work Item file.`);
19946
- const parsed = matter13(raw);
20550
+ const parsed = matter16(raw);
19947
20551
  const data = parsed.data;
19948
20552
  const affectedModules = Array.isArray(data.affected_modules) ? data.affected_modules.map(String) : [];
19949
20553
  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) : [] })) : [];
@@ -20092,31 +20696,31 @@ function modulesDiscoverTool(root, args) {
20092
20696
  const sysName = config.system?.name ?? config.project?.name ?? "";
20093
20697
  const workspaceRoots2 = config.multirepo?.workspace_roots ?? [".."];
20094
20698
  const IGNORED = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", "vendor", ".next", "out", "tmp", "temp", ".kaddo"]);
20095
- const coreAbs = path4.resolve(root);
20699
+ const coreAbs = path6.resolve(root);
20096
20700
  const discovered = [];
20097
20701
  for (const wr of workspaceRoots2) {
20098
- const rootAbs = path4.resolve(root, wr);
20099
- if (!fs5.existsSync(rootAbs) || !fs5.statSync(rootAbs).isDirectory()) continue;
20702
+ const rootAbs = path6.resolve(root, wr);
20703
+ if (!fs7.existsSync(rootAbs) || !fs7.statSync(rootAbs).isDirectory()) continue;
20100
20704
  let entries;
20101
20705
  try {
20102
- entries = fs5.readdirSync(rootAbs);
20706
+ entries = fs7.readdirSync(rootAbs);
20103
20707
  } catch {
20104
20708
  continue;
20105
20709
  }
20106
20710
  for (const entry of entries) {
20107
20711
  if (IGNORED.has(entry)) continue;
20108
- const candidateAbs = path4.resolve(rootAbs, entry);
20109
- if (!fs5.statSync(candidateAbs).isDirectory()) continue;
20712
+ const candidateAbs = path6.resolve(rootAbs, entry);
20713
+ if (!fs7.statSync(candidateAbs).isDirectory()) continue;
20110
20714
  if (candidateAbs === coreAbs) continue;
20111
- const relPath = path4.relative(root, candidateAbs).replace(/\\/g, "/");
20112
- const cfgPath = path4.join(candidateAbs, ".kaddo", "config.yml");
20113
- if (!fs5.existsSync(cfgPath)) {
20715
+ const relPath = path6.relative(root, candidateAbs).replace(/\\/g, "/");
20716
+ const cfgPath = path6.join(candidateAbs, ".kaddo", "config.yml");
20717
+ if (!fs7.existsSync(cfgPath)) {
20114
20718
  discovered.push({ id: entry, path: relPath, status: "not_configured", eligibleForMapping: false });
20115
20719
  continue;
20116
20720
  }
20117
20721
  let parsed = null;
20118
20722
  try {
20119
- parsed = parseYaml11(fs5.readFileSync(cfgPath, "utf-8"));
20723
+ parsed = parseYaml11(fs7.readFileSync(cfgPath, "utf-8"));
20120
20724
  } catch {
20121
20725
  }
20122
20726
  const role = parsed?.project?.role ?? parsed?.multirepo?.role;
@@ -20190,8 +20794,8 @@ ${list3(data.owners)}
20190
20794
 
20191
20795
  // ../cli/src/core/system-topology.ts
20192
20796
  import { parse as parseYaml12, stringify as stringifyYaml8 } from "yaml";
20193
- import fs6 from "fs";
20194
- import path5 from "path";
20797
+ import fs8 from "fs";
20798
+ import path7 from "path";
20195
20799
  import crypto from "crypto";
20196
20800
  var TOPOLOGY_FILE2 = "knowledge/tech/system-topology.yml";
20197
20801
  var SYSTEM_ENTITY_KINDS2 = /* @__PURE__ */ new Set([
@@ -20563,16 +21167,16 @@ function findSystemPaths(dir, source, target, opts = {}) {
20563
21167
  out.get(r2.source).push(r2.target);
20564
21168
  }
20565
21169
  const paths = [];
20566
- const walk = (node, path7) => {
21170
+ const walk = (node, path9) => {
20567
21171
  if (paths.length >= 20) return;
20568
- if (node === target && path7.length > 1) {
20569
- paths.push([...path7]);
21172
+ if (node === target && path9.length > 1) {
21173
+ paths.push([...path9]);
20570
21174
  return;
20571
21175
  }
20572
- if (path7.length > maxDepth) return;
21176
+ if (path9.length > maxDepth) return;
20573
21177
  for (const nxt of out.get(node) ?? []) {
20574
- if (path7.includes(nxt)) continue;
20575
- walk(nxt, [...path7, nxt]);
21178
+ if (path9.includes(nxt)) continue;
21179
+ walk(nxt, [...path9, nxt]);
20576
21180
  }
20577
21181
  };
20578
21182
  walk(source, [source]);
@@ -20597,14 +21201,14 @@ function getImpactCandidates(dir, seedIds, opts = {}) {
20597
21201
  existing.reason.push(reason);
20598
21202
  continue;
20599
21203
  }
20600
- const path7 = findSystemPaths(dir, seed, n.id, { maxDepth: opts.maxDepth ?? DEFAULT_MAX_DEPTH })[0];
21204
+ const path9 = findSystemPaths(dir, seed, n.id, { maxDepth: opts.maxDepth ?? DEFAULT_MAX_DEPTH })[0];
20601
21205
  candidates.set(n.id, {
20602
21206
  nodeId: n.id,
20603
21207
  label: n.label,
20604
21208
  kind: n.type,
20605
21209
  ...n.moduleId ? { moduleId: n.moduleId } : {},
20606
21210
  reason: [reason],
20607
- ...path7 ? { graphPath: path7.map((id) => byId.get(id)?.label ?? id) } : {},
21211
+ ...path9 ? { graphPath: path9.map((id) => byId.get(id)?.label ?? id) } : {},
20608
21212
  ...n.implementationRefs?.length ? { implementationRefs: n.implementationRefs } : {},
20609
21213
  ...n.knowledgeRefs?.length ? { knowledgeRefs: n.knowledgeRefs } : {},
20610
21214
  status: "candidate",
@@ -20703,14 +21307,14 @@ function systemImpactCandidatesTool(root, args) {
20703
21307
  }
20704
21308
 
20705
21309
  // ../cli/src/services/integrations.ts
20706
- import matter15 from "gray-matter";
21310
+ import matter18 from "gray-matter";
20707
21311
  import { parse as parseYaml13, stringify as stringifyYaml9 } from "yaml";
20708
21312
 
20709
21313
  // ../cli/src/core/work-item-write.ts
20710
- import fs7 from "fs";
20711
- import path6 from "path";
21314
+ import fs9 from "fs";
21315
+ import path8 from "path";
20712
21316
  import crypto4 from "crypto";
20713
- import matter14 from "gray-matter";
21317
+ import matter17 from "gray-matter";
20714
21318
  var SECTION_ORDER2 = [
20715
21319
  "Actor",
20716
21320
  "Outcome",
@@ -21378,8 +21982,8 @@ async function handleJiraResponse2(res) {
21378
21982
  }
21379
21983
  return await res.json();
21380
21984
  }
21381
- async function jiraFetch2(baseUrl, path7, auth, timeoutMs, params) {
21382
- const url = new URL(path7, baseUrl + "/");
21985
+ async function jiraFetch2(baseUrl, path9, auth, timeoutMs, params) {
21986
+ const url = new URL(path9, baseUrl + "/");
21383
21987
  if (params) for (const [k2, v] of Object.entries(params)) url.searchParams.set(k2, v);
21384
21988
  const controller = new AbortController();
21385
21989
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -21397,8 +22001,8 @@ async function jiraFetch2(baseUrl, path7, auth, timeoutMs, params) {
21397
22001
  clearTimeout(timer);
21398
22002
  }
21399
22003
  }
21400
- async function jiraPost2(baseUrl, path7, auth, timeoutMs, body) {
21401
- const url = new URL(path7, baseUrl + "/");
22004
+ async function jiraPost2(baseUrl, path9, auth, timeoutMs, body) {
22005
+ const url = new URL(path9, baseUrl + "/");
21402
22006
  const controller = new AbortController();
21403
22007
  const timer = setTimeout(() => controller.abort(), timeoutMs);
21404
22008
  try {
@@ -21758,7 +22362,7 @@ async function integrationsWorkItemTool(root, args) {
21758
22362
  }
21759
22363
 
21760
22364
  // ../cli/src/core/context-pack.ts
21761
- import matter16 from "gray-matter";
22365
+ import matter19 from "gray-matter";
21762
22366
  var CONTEXT_PACK_VERSION = "1";
21763
22367
  var ARCH_DIR4 = "knowledge";
21764
22368
  function readScanJson(dir) {
@@ -21771,7 +22375,7 @@ function readScanJson(dir) {
21771
22375
  }
21772
22376
  }
21773
22377
  function firstParagraph4(markdown) {
21774
- const body = matter16(markdown).content.trim();
22378
+ const body = matter19(markdown).content.trim();
21775
22379
  const para = body.split("\n\n").map((p) => p.trim()).find((p) => p && !p.startsWith("#"));
21776
22380
  return para ?? "";
21777
22381
  }
@@ -21825,6 +22429,8 @@ function toContextWorkItem(a2) {
21825
22429
  wi.validationStatus = a2.validationStatus || (isHistorical ? "not-assessed" : void 0);
21826
22430
  wi.releaseStatus = a2.releaseStatus || (isHistorical ? "not-assessed" : void 0);
21827
22431
  if (a2.affectedModules.length > 0) wi.affectedModules = a2.affectedModules;
22432
+ const wiResources = parseWorkItemResources(a2.rawFrontmatter);
22433
+ if (wiResources.length > 0) wi.resources = wiResources;
21828
22434
  if (a2.scopeConfidence) wi.scopeConfidence = a2.scopeConfidence;
21829
22435
  if (a2.moduleCoverage) wi.moduleCoverage = a2.moduleCoverage;
21830
22436
  if (a2.impactAnalysis) wi.impactAnalysis = a2.impactAnalysis;
@@ -22736,7 +23342,7 @@ function requireKnowledge(root) {
22736
23342
  throw new KaddoMcpError("Knowledge repository not found. Run `kaddo bootstrap` first.");
22737
23343
  }
22738
23344
  }
22739
- function slug4(s) {
23345
+ function slug5(s) {
22740
23346
  return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
22741
23347
  }
22742
23348
  function generateContext(root) {
@@ -22869,7 +23475,7 @@ function generateCapsuleDraft(root) {
22869
23475
  const config = requireConfig(root);
22870
23476
  requireKnowledge(root);
22871
23477
  const capsule = buildCapsule(root, config);
22872
- const name = slug4(config.project.name);
23478
+ const name = slug5(config.project.name);
22873
23479
  const md = `.kaddo/exports/${name}.capsule.md`;
22874
23480
  const json = `.kaddo/exports/${name}.capsule.json`;
22875
23481
  writeDerived(root, md, renderCapsuleMarkdown(capsule));
@@ -23157,6 +23763,63 @@ function createServer(root) {
23157
23763
  },
23158
23764
  async (args) => toolText(guarded(root, () => markWorkItemReady(root, args.id, args.confirm)))
23159
23765
  );
23766
+ server.registerTool(
23767
+ "kaddo_list_resources",
23768
+ { title: "List Project Resources", description: "List external systems the project uses (databases, cloud, APIs\u2026), read-only, no secrets.", inputSchema: {} },
23769
+ async () => toolText(guarded(root, () => listResourcesTool(root)))
23770
+ );
23771
+ server.registerTool(
23772
+ "kaddo_get_resource",
23773
+ { 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() } },
23774
+ async (args) => toolText(guarded(root, () => getResourceTool(root, args.id)))
23775
+ );
23776
+ const resourceFields = {
23777
+ title: z4.string().optional(),
23778
+ resource_type: z4.string().optional(),
23779
+ provider: z4.string().optional(),
23780
+ environments: z4.array(z4.string()).optional(),
23781
+ scope: z4.string().optional(),
23782
+ module: z4.string().optional(),
23783
+ modules: z4.array(z4.string()).optional(),
23784
+ purpose: z4.string().optional(),
23785
+ access_interfaces: z4.array(z4.object({
23786
+ type: z4.string(),
23787
+ tool: z4.string().optional(),
23788
+ provider: z4.string().optional(),
23789
+ purpose: z4.string().optional(),
23790
+ operations: z4.array(z4.string()).optional(),
23791
+ environments: z4.array(z4.string()).optional(),
23792
+ constraints: z4.string().optional()
23793
+ })).optional(),
23794
+ authentication: z4.object({ mode: z4.string().optional(), refs: z4.array(z4.string()).optional() }).optional()
23795
+ };
23796
+ server.registerTool(
23797
+ "kaddo_create_resource",
23798
+ {
23799
+ title: "Create Project Resource",
23800
+ description: "Propose creating a Project Resource. Without confirm, returns a preview + validation findings (writes nothing). With confirm=true, Core persists it as Markdown. Never include credential values \u2014 only reference names.",
23801
+ inputSchema: { ...resourceFields, confirm: z4.boolean().optional() }
23802
+ },
23803
+ async (args) => toolText(guarded(root, () => createResourceTool(root, args, args.confirm)))
23804
+ );
23805
+ server.registerTool(
23806
+ "kaddo_update_resource",
23807
+ {
23808
+ title: "Update Project Resource",
23809
+ description: "Propose updating a Project Resource (no-lossy). Without confirm, returns a preview; with confirm=true, Core applies it. Never include credential values.",
23810
+ inputSchema: { id: z4.string(), ...resourceFields, confirm: z4.boolean().optional() }
23811
+ },
23812
+ async (args) => toolText(guarded(root, () => updateResourceTool(root, args.id, args, args.confirm)))
23813
+ );
23814
+ server.registerTool(
23815
+ "kaddo_delete_resource",
23816
+ {
23817
+ title: "Delete Project Resource",
23818
+ description: "Delete a Project Resource. Without confirm, returns what references it (writes nothing); with confirm=true, Core removes only the resource \u2014 references in other artifacts are left intact.",
23819
+ inputSchema: { id: z4.string(), confirm: z4.boolean().optional() }
23820
+ },
23821
+ async (args) => toolText(guarded(root, () => deleteResourceTool(root, args.id, args.confirm)))
23822
+ );
23160
23823
  server.registerTool(
23161
23824
  "kaddo_list_capsules",
23162
23825
  { title: "List Knowledge Capsules", description: "List registered external Knowledge Capsules.", inputSchema: {} },