@kaddo/cli 3.16.5 → 3.18.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 (3) hide show
  1. package/README.md +27 -0
  2. package/dist/index.js +617 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -437,6 +437,31 @@ contracts, risks, ADRs, owners — never source code or secrets); refine it with
437
437
  the capsules (warning when one looks stale). See the
438
438
  [Knowledge Capsules guide](https://kaddo.trycatch.tv/knowledge-capsules/).
439
439
 
440
+ ---
441
+
442
+ ### `kaddo graph export`
443
+
444
+ Export the connections that already exist between your knowledge artifacts as a **lightweight,
445
+ file-based knowledge graph** — for onboarding, impact analysis and context selection.
446
+
447
+ ```bash
448
+ kaddo graph export # → .kaddo/graph.json + .kaddo/graph.mmd
449
+ kaddo graph export --scope all # include every artifact (default: active)
450
+ kaddo graph export --format mermaid # mermaid only (or --format json)
451
+ ```
452
+
453
+ Nodes come from knowledge layers, Work Items, code globs, capabilities, ADRs and Knowledge
454
+ Capsules; edges come from front matter (`code`, `capabilities`, `decisions`, `source_id`,
455
+ `source_initiative`) and the external registry. It never reads `src/`, never reads source code
456
+ and never calls an LLM. `kaddo explain` and `kaddo context` show a graph **summary** once it has
457
+ been exported (they never generate it).
458
+
459
+ Every export also rates **relationship quality** and writes non-blocking metadata hints
460
+ (`.kaddo/graph-hints.md` + `.json`) — detecting active Work Items without `code`/`capabilities`,
461
+ ADRs without governed `code`, capabilities/capsules with no Work Item link, and more. The
462
+ `graph-agent` turns those hints into precise front matter you confirm and apply. See the
463
+ [Knowledge Graph Export guide](https://kaddo.trycatch.tv/knowledge-graph-export/).
464
+
440
465
  ## Roadmap
441
466
 
442
467
  The full knowledge loop ships today: `scan → context → agents → understand → roadmap →
@@ -478,6 +503,8 @@ create --from roadmap → owners → guard → explain`.
478
503
  | v3.14 | Project knowledge language (`project.language: en\|es`): knowledge in your language, CLI stays English; all agents respect it |
479
504
  | v3.15 | Delivery context consistency: phase-based handoff + per-phase LLM instructions; assisted `owners suggest` (normalize/validate globs); new `ownership-agent`; guard untracked-files warning; duplicate Work Item detection |
480
505
  | v3.16 | Knowledge Capsules: `kaddo capsule export/add`, External Knowledge in context/explain, new `capsule-agent` |
506
+ | v3.17 | Knowledge Graph Export: `kaddo graph export` (`.kaddo/graph.json` + `.mmd`, `--scope`/`--format`); graph summary in context/explain |
507
+ | v3.18 | Graph relationship quality & metadata hints: `graph-hints.md`/`.json`, quality levels, `graph-agent`; hints in context/explain/understand |
481
508
 
482
509
  **Optional modules (installed with `kaddo add`):**
483
510
 
package/dist/index.js CHANGED
@@ -680,6 +680,10 @@ var COMMAND_HELP = {
680
680
  "capsule add": {
681
681
  question: "How do I consume another system as external context?",
682
682
  next: "kaddo context (the pack now includes External Knowledge)"
683
+ },
684
+ "graph export": {
685
+ question: "How is my project knowledge connected?",
686
+ next: "Open .kaddo/graph.mmd, or run kaddo explain for a summary"
683
687
  }
684
688
  };
685
689
  function commandFooterLines(name) {
@@ -1648,6 +1652,19 @@ var RESPONSIBILITY_MATRIX = {
1648
1652
  ],
1649
1653
  next: ["kaddo capsule export"]
1650
1654
  },
1655
+ "graph-agent": {
1656
+ agent: "graph-agent",
1657
+ responsibleFor: ["Reviewing graph hints", "Proposing precise relationship front matter"],
1658
+ produces: ["proposed front matter (code/capabilities/decisions/source/capsules)"],
1659
+ canSuggest: ["kaddo graph export", "kaddo owners suggest"],
1660
+ cannotSuggest: [
1661
+ "code",
1662
+ "git",
1663
+ "modifying files without confirmation",
1664
+ "inventing relationships"
1665
+ ],
1666
+ next: ["kaddo graph export"]
1667
+ },
1651
1668
  "ownership-agent": {
1652
1669
  agent: "ownership-agent",
1653
1670
  responsibleFor: ["Precise code: ownership for Work Items and artifacts"],
@@ -3203,6 +3220,74 @@ edit the roadmap yourself).
3203
3220
  - Duplicates, overlaps and dependencies are flagged.
3204
3221
  - The response ends with a human-decision handoff \u2014 no agent is auto-executed.
3205
3222
  `;
3223
+ var GRAPH_AGENT = `# Graph Agent
3224
+
3225
+ ## Role
3226
+
3227
+ You are the Kaddo Graph Agent. Your job is to review the **graph hints** produced by
3228
+ \`kaddo graph export\` and propose **precise relationship front matter** so the knowledge graph
3229
+ becomes more connected and useful.
3230
+
3231
+ You do not write code, you do not modify files, and you never run Git. You propose; the human
3232
+ confirms and edits the artifact front matter, then re-runs \`kaddo graph export\`.
3233
+
3234
+ ## When to Use
3235
+
3236
+ Use this agent when \`kaddo graph export\` reports relationship quality \`partial\`, \`sparse\` or
3237
+ \`empty\`, or when \`kaddo understand\` recommends reviewing graph hints during Active Delivery.
3238
+
3239
+ ## Input Required
3240
+
3241
+ Provide \`.kaddo/context-pack.md\`, \`.kaddo/graph.json\` and \`.kaddo/graph-hints.md\` as the primary
3242
+ inputs, plus the Work Items under \`knowledge/delivery/work-items/\`, the ADRs under
3243
+ \`knowledge/tech/decisions/\` and \`knowledge/product/capabilities.md\` when they exist.
3244
+
3245
+ ## Expected Output
3246
+
3247
+ For each hint, a concrete front matter proposal for the affected artifact, e.g.:
3248
+
3249
+ \`\`\`yaml
3250
+ code:
3251
+ - src/cli/**
3252
+ capabilities:
3253
+ - task-management
3254
+ decisions:
3255
+ - ADR-001
3256
+ \`\`\`
3257
+
3258
+ ## Instructions
3259
+
3260
+ 1. Work through the hints in \`.kaddo/graph-hints.md\` one artifact at a time.
3261
+ 2. Propose only relationships you can justify from existing knowledge \u2014 never invent paths,
3262
+ capabilities, ADRs or capsules.
3263
+ 3. Prefer narrow, accurate values (e.g. \`src/payments/**\`, not \`src/**\`).
3264
+ 4. Mark uncertain proposals explicitly and ask the human to confirm.
3265
+ 5. Tell the human to apply the front matter and re-run \`kaddo graph export\` to verify.
3266
+
3267
+ ## Constraints
3268
+
3269
+ - Do **not** modify files \u2014 propose front matter for the human to apply.
3270
+ - Do **not** invent relationships, paths or IDs.
3271
+ - Do **not** read the full source tree; rely on declared knowledge and the inventory.
3272
+ - Do **not** run Git or make commits.
3273
+
3274
+ ## Output Format
3275
+
3276
+ Per artifact: the artifact id, the proposed front matter block, and a one-line reason. End with a
3277
+ note to re-run \`kaddo graph export\`.
3278
+
3279
+ ## Where to Save the Result
3280
+
3281
+ Nothing is saved automatically. The human edits the affected artifact front matter (Work Items,
3282
+ ADRs) and re-runs \`kaddo graph export\`.
3283
+
3284
+ ## Quality Checklist
3285
+
3286
+ - Every proposal maps to a real artifact, path, capability, ADR or capsule.
3287
+ - Globs are narrow and accurate; uncertainty is marked.
3288
+ - No files were modified; no Git was run.
3289
+ - The human is asked to confirm and re-export the graph.
3290
+ `;
3206
3291
  var AGENT_PROMPTS = [
3207
3292
  { fileName: "capability-agent.md", content: CAPABILITY_AGENT },
3208
3293
  { fileName: "architecture-agent.md", content: ARCHITECTURE_AGENT },
@@ -3226,7 +3311,9 @@ var AGENT_PROMPTS = [
3226
3311
  // Ownership proposals (precise code: globs — VS-052)
3227
3312
  { fileName: "ownership-agent.md", content: OWNERSHIP_AGENT },
3228
3313
  // Knowledge Capsule refinement (external context — VS-054)
3229
- { fileName: "capsule-agent.md", content: CAPSULE_AGENT }
3314
+ { fileName: "capsule-agent.md", content: CAPSULE_AGENT },
3315
+ // Graph relationship quality (metadata hints → precise front matter — VS-056)
3316
+ { fileName: "graph-agent.md", content: GRAPH_AGENT }
3230
3317
  // Every official prompt ends with its responsibility boundaries + Agent Trace footer.
3231
3318
  ].map((p2) => ({ fileName: p2.fileName, content: withResponsibilityTrace(p2.fileName, p2.content) }));
3232
3319
 
@@ -3242,7 +3329,8 @@ var AGENT_GROUPS = {
3242
3329
  "standards-agent.md",
3243
3330
  "module-design-agent.md",
3244
3331
  "adr-agent.md",
3245
- "capsule-agent.md"
3332
+ "capsule-agent.md",
3333
+ "graph-agent.md"
3246
3334
  ],
3247
3335
  delivery: [
3248
3336
  "backlog-agent.md",
@@ -3387,7 +3475,8 @@ var agentReadme = {
3387
3475
  "- `standards-agent.md` \u2014 propose lightweight coding/docs/architecture standards.",
3388
3476
  "- `stack-agent.md` \u2014 document technologies and stack decisions.",
3389
3477
  "- `module-design-agent.md` \u2014 document the design of a mapped module.",
3390
- "- `capsule-agent.md` \u2014 refine a Knowledge Capsule for external sharing (no secrets/source)."
3478
+ "- `capsule-agent.md` \u2014 refine a Knowledge Capsule for external sharing (no secrets/source).",
3479
+ "- `graph-agent.md` \u2014 review `kaddo graph export` hints and propose precise relationship front matter."
3391
3480
  ].join("\n")
3392
3481
  };
3393
3482
  var agentFiles = AGENT_PROMPTS.map((a) => ({
@@ -4157,8 +4246,8 @@ async function runCreate(type, opts = {}) {
4157
4246
  answers[question.frontMatterField] = answer.trim();
4158
4247
  }
4159
4248
  const id = nextWorkItemId(dir);
4160
- const slug2 = slugify(title);
4161
- const fileName = `${id}-${slug2}.md`;
4249
+ const slug4 = slugify(title);
4250
+ const fileName = `${id}-${slug4}.md`;
4162
4251
  const filePath = join(dir, DRAFT_DIR, fileName);
4163
4252
  const frontMatter2 = buildFrontMatter(id, workItemType, level, title.trim(), answers);
4164
4253
  const body = buildBody(workItemType, level, title.trim(), answers, levelDef.qualityGate);
@@ -4198,8 +4287,8 @@ async function runCreateModule(dir, modType) {
4198
4287
  answers[q.frontMatterField] = answer.trim();
4199
4288
  }
4200
4289
  const id = nextWorkItemId(dir);
4201
- const slug2 = slugify(title);
4202
- const fileName = `${id}-${slug2}.md`;
4290
+ const slug4 = slugify(title);
4291
+ const fileName = `${id}-${slug4}.md`;
4203
4292
  const filePath = join(dir, DRAFT_DIR, fileName);
4204
4293
  const frontMatter2 = buildModuleFrontMatter(id, modType, title.trim(), answers);
4205
4294
  const body = buildModuleBody(modType, title.trim(), answers);
@@ -4331,8 +4420,8 @@ function buildRoadmapWorkItem(opts) {
4331
4420
  const { id, type, level, candidate } = opts;
4332
4421
  const answers = opts.answers ?? {};
4333
4422
  const title = candidate.title.trim();
4334
- const slug2 = slugify(title);
4335
- const fileName = `${id}-${slug2}.md`;
4423
+ const slug4 = slugify(title);
4424
+ const fileName = `${id}-${slug4}.md`;
4336
4425
  const qualityGate = getLevel(level).qualityGate;
4337
4426
  const frontMatter2 = buildRoadmapFrontMatter(id, type, level, title, candidate, answers);
4338
4427
  const body = buildRoadmapBody(type, level, title, candidate, answers, qualityGate);
@@ -4480,7 +4569,9 @@ function parseArtifact(filePath, raw) {
4480
4569
  phase: String(data.phase ?? ""),
4481
4570
  initiative: String(data.initiative ?? data.source_initiative ?? ""),
4482
4571
  source: data.source ? String(data.source) : "",
4483
- sourceId: String(data.source_id ?? "")
4572
+ sourceId: String(data.source_id ?? ""),
4573
+ decisions: Array.isArray(data.decisions) ? data.decisions.map(String).filter(Boolean) : [],
4574
+ capsules: Array.isArray(data.capsules) ? data.capsules.map(String).filter(Boolean) : []
4484
4575
  };
4485
4576
  } catch {
4486
4577
  return null;
@@ -5585,8 +5676,416 @@ function loadExternalCapsules(dir) {
5585
5676
  return out;
5586
5677
  }
5587
5678
 
5588
- // src/core/knowledge-discovery.ts
5679
+ // src/core/graph.ts
5589
5680
  var KNOWLEDGE2 = "knowledge";
5681
+ function toPosix2(p2) {
5682
+ return p2.replace(/\\/g, "/");
5683
+ }
5684
+ function slug(s) {
5685
+ return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
5686
+ }
5687
+ function isAdr(a) {
5688
+ return toPosix2(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
5689
+ }
5690
+ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
5691
+ const scope = opts.scope ?? "active";
5692
+ const nodes = /* @__PURE__ */ new Map();
5693
+ const edges = [];
5694
+ const edgeKeys = /* @__PURE__ */ new Set();
5695
+ const addNode = (node) => {
5696
+ if (!nodes.has(node.id)) nodes.set(node.id, node);
5697
+ };
5698
+ const addEdge = (from, to, type) => {
5699
+ const key = `${from}|${to}|${type}`;
5700
+ if (edgeKeys.has(key)) return;
5701
+ edgeKeys.add(key);
5702
+ edges.push({ from, to, type });
5703
+ };
5704
+ const layerDocs = [
5705
+ { id: "business:business", type: "business", label: "Business", files: ["business/business.md"] },
5706
+ { id: "product:product", type: "product", label: "Product", files: ["product/product.md", "product/capabilities.md"] },
5707
+ { id: "tech:tech", type: "tech", label: "Tech", files: ["tech/current-state.md", "tech/codebase.md"] },
5708
+ { id: "delivery:delivery", type: "delivery", label: "Delivery", files: ["delivery/roadmap.md"] }
5709
+ ];
5710
+ const presentLayers = [];
5711
+ for (const layer2 of layerDocs) {
5712
+ const path5 = layer2.files.map((f) => `${KNOWLEDGE2}/${f}`).find((rel) => exists(join(dir, rel)));
5713
+ if (path5) {
5714
+ addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path5 });
5715
+ presentLayers.push(layer2.id);
5716
+ }
5717
+ }
5718
+ for (let i = 0; i < presentLayers.length - 1; i++) {
5719
+ addEdge(presentLayers[i], presentLayers[i + 1], "informs");
5720
+ }
5721
+ const all = discoverKnowledge(dir);
5722
+ const workItems = all.filter((a) => a.isWorkItem);
5723
+ const selectedWIs = scope === "active" ? workItems.filter((a) => a.lifecycle && isActiveState(a.lifecycle)) : workItems;
5724
+ for (const wi of selectedWIs) {
5725
+ const id = wi.id || wi.title;
5726
+ const wiNodeId = `wi:${id}`;
5727
+ addNode({
5728
+ id: wiNodeId,
5729
+ type: "work-item",
5730
+ label: `${id} ${wi.title}`.trim(),
5731
+ path: wi.relPath,
5732
+ status: wi.lifecycle,
5733
+ knowledge_level: wi.knowledgeLevel || void 0
5734
+ });
5735
+ for (const glob of wi.codeGlobs) {
5736
+ const codeId = `code:${glob}`;
5737
+ addNode({ id: codeId, type: "code-glob", label: glob });
5738
+ addEdge(wiNodeId, codeId, "owns");
5739
+ }
5740
+ for (const cap of wi.capabilities) {
5741
+ const capId = `capability:${slug(cap) || cap}`;
5742
+ addNode({ id: capId, type: "capability", label: cap });
5743
+ addEdge(wiNodeId, capId, "implements");
5744
+ }
5745
+ for (const dec of wi.decisions) {
5746
+ const adrId = `adr:${dec}`;
5747
+ addNode({ id: adrId, type: "decision", label: dec });
5748
+ addEdge(wiNodeId, adrId, "depends_on");
5749
+ }
5750
+ if (wi.initiative) {
5751
+ const initId = `initiative:${slug(wi.initiative) || wi.initiative}`;
5752
+ addNode({ id: initId, type: "initiative", label: wi.initiative });
5753
+ addEdge(wiNodeId, initId, "belongs_to");
5754
+ }
5755
+ if (wi.source === "roadmap" && wi.sourceId) {
5756
+ const candId = `candidate:${wi.sourceId}`;
5757
+ addNode({ id: candId, type: "roadmap-candidate", label: wi.sourceId });
5758
+ addEdge(candId, wiNodeId, "materialized_as");
5759
+ }
5760
+ }
5761
+ for (const adr of all.filter(isAdr)) {
5762
+ const adrId = `adr:${adr.id || adr.title}`;
5763
+ const referenced = nodes.has(adrId);
5764
+ if (scope === "all" || referenced) {
5765
+ nodes.set(adrId, {
5766
+ id: adrId,
5767
+ type: "decision",
5768
+ label: `${adr.id} ${adr.title}`.trim() || adr.id || adr.title,
5769
+ path: adr.relPath
5770
+ });
5771
+ for (const glob of adr.codeGlobs) {
5772
+ const codeId = `code:${glob}`;
5773
+ addNode({ id: codeId, type: "code-glob", label: glob });
5774
+ addEdge(adrId, codeId, "governs");
5775
+ }
5776
+ }
5777
+ }
5778
+ const capsules = loadExternalRegistry(dir);
5779
+ if (capsules.length > 0) {
5780
+ const projId = `project:${slug(config.project.name) || "project"}`;
5781
+ addNode({ id: projId, type: "project", label: config.project.name });
5782
+ for (const cap of capsules) {
5783
+ const capId = `capsule:${cap.id}`;
5784
+ addNode({ id: capId, type: "knowledge-capsule", label: cap.id, path: cap.path });
5785
+ addEdge(capId, projId, "provides_external_context");
5786
+ for (const wi of selectedWIs) {
5787
+ if (wi.capsules.includes(cap.id)) {
5788
+ addEdge(`wi:${wi.id || wi.title}`, capId, "uses_external_knowledge");
5789
+ }
5790
+ }
5791
+ }
5792
+ }
5793
+ return {
5794
+ generated_at: now.toISOString(),
5795
+ project: {
5796
+ name: config.project.name,
5797
+ state: config.project.state,
5798
+ structure: config.project.structure
5799
+ },
5800
+ nodes: [...nodes.values()],
5801
+ edges
5802
+ };
5803
+ }
5804
+ function serializeGraphJson(graph) {
5805
+ return JSON.stringify(graph, null, 2) + "\n";
5806
+ }
5807
+ function renderGraphMermaid(graph) {
5808
+ const safeIds = /* @__PURE__ */ new Map();
5809
+ const used = /* @__PURE__ */ new Set();
5810
+ const safe = (id) => {
5811
+ const existing = safeIds.get(id);
5812
+ if (existing) return existing;
5813
+ let base = id.replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
5814
+ if (!base) base = "n";
5815
+ let candidate = base;
5816
+ let i = 1;
5817
+ while (used.has(candidate)) candidate = `${base}_${i++}`;
5818
+ used.add(candidate);
5819
+ safeIds.set(id, candidate);
5820
+ return candidate;
5821
+ };
5822
+ const escapeLabel = (s) => s.replace(/"/g, "'");
5823
+ const lines = ["flowchart LR"];
5824
+ for (const node of graph.nodes) {
5825
+ lines.push(` ${safe(node.id)}["${escapeLabel(node.label)}"]`);
5826
+ }
5827
+ if (graph.edges.length > 0) lines.push("");
5828
+ for (const edge of graph.edges) {
5829
+ lines.push(` ${safe(edge.from)} -->|${edge.type}| ${safe(edge.to)}`);
5830
+ }
5831
+ return lines.join("\n") + "\n";
5832
+ }
5833
+ var ACTIVE_WI_STATES = /* @__PURE__ */ new Set(["draft", "ready", "in-progress", "blocked"]);
5834
+ function loadGraphSummary(dir) {
5835
+ const p2 = join(dir, ".kaddo", "graph.json");
5836
+ if (!exists(p2)) return null;
5837
+ try {
5838
+ const graph = JSON.parse(readFile(p2));
5839
+ const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
5840
+ const edges = Array.isArray(graph.edges) ? graph.edges : [];
5841
+ const activeWiIds = new Set(
5842
+ nodes.filter((n) => n.type === "work-item" && (!n.status || ACTIVE_WI_STATES.has(n.status))).map((n) => n.id)
5843
+ );
5844
+ const connected = new Set(
5845
+ edges.filter((e) => e.type === "owns" && activeWiIds.has(e.from)).map((e) => e.from)
5846
+ );
5847
+ return {
5848
+ generatedAt: String(graph.generated_at ?? ""),
5849
+ nodes: nodes.length,
5850
+ edges: edges.length,
5851
+ activeWorkItemsConnectedToCode: connected.size
5852
+ };
5853
+ } catch {
5854
+ return null;
5855
+ }
5856
+ }
5857
+
5858
+ // src/core/graph-hints.ts
5859
+ var KNOWLEDGE3 = "knowledge";
5860
+ function toPosix3(p2) {
5861
+ return p2.replace(/\\/g, "/");
5862
+ }
5863
+ function slug2(s) {
5864
+ return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
5865
+ }
5866
+ function isAdr2(a) {
5867
+ return toPosix3(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
5868
+ }
5869
+ function capabilityHeadings(dir) {
5870
+ const p2 = join(dir, KNOWLEDGE3, "product", "capabilities.md");
5871
+ if (!exists(p2)) return [];
5872
+ return readFile(p2).split(/\r?\n/).map((l) => l.match(/^#{2,3}\s+(.+?)\s*$/)).filter((m) => Boolean(m)).map((m) => m[1].trim()).filter((h) => !/^(summary|resumen|overview|capabilities|capacidades)$/i.test(h));
5873
+ }
5874
+ function humanMissing(field) {
5875
+ switch (field) {
5876
+ case "code":
5877
+ return "code ownership";
5878
+ case "capabilities":
5879
+ return "linked capability";
5880
+ case "decisions":
5881
+ return "linked decision";
5882
+ case "source":
5883
+ return "roadmap link";
5884
+ default:
5885
+ return field;
5886
+ }
5887
+ }
5888
+ function buildGraphHints(dir, graph, now = /* @__PURE__ */ new Date()) {
5889
+ const artifacts = discoverKnowledge(dir);
5890
+ const workItems = artifacts.filter((a) => a.isWorkItem);
5891
+ const activeWIs = workItems.filter((a) => a.lifecycle && isActiveState(a.lifecycle));
5892
+ const adrs = artifacts.filter(isAdr2);
5893
+ const capsules = loadExternalRegistry(dir);
5894
+ const hints = [];
5895
+ const referencedCapabilitySlugs = new Set(
5896
+ workItems.flatMap((w) => w.capabilities.map((c) => slug2(c)))
5897
+ );
5898
+ const referencedCapsuleIds = new Set(workItems.flatMap((w) => w.capsules.map((c) => c)));
5899
+ let activeWithoutCode = 0;
5900
+ let activeWithoutCapabilities = 0;
5901
+ let wisWithoutSource = 0;
5902
+ for (const wi of activeWIs) {
5903
+ const id = wi.id || wi.title;
5904
+ const missing = [];
5905
+ const suggested = {};
5906
+ if (wi.codeGlobs.length === 0) {
5907
+ missing.push("code");
5908
+ suggested.code = ["src/<area>/**"];
5909
+ activeWithoutCode++;
5910
+ }
5911
+ if (wi.capabilities.length === 0) {
5912
+ missing.push("capabilities");
5913
+ suggested.capabilities = ["<capability>"];
5914
+ activeWithoutCapabilities++;
5915
+ }
5916
+ if (wi.decisions.length === 0) {
5917
+ missing.push("decisions");
5918
+ suggested.decisions = ["ADR-XXX"];
5919
+ }
5920
+ const hasSource = Boolean(wi.sourceId) || Boolean(wi.initiative);
5921
+ if (!hasSource) {
5922
+ missing.push("source");
5923
+ wisWithoutSource++;
5924
+ }
5925
+ if (missing.length === 0) continue;
5926
+ hints.push({
5927
+ artifact_id: id,
5928
+ artifact_type: "work-item",
5929
+ path: wi.relPath,
5930
+ severity: "info",
5931
+ missing,
5932
+ reason: "Active Work Item has limited graph relationships.",
5933
+ message: `${id} has no ${missing.map(humanMissing).join(", ")}.`,
5934
+ suggested_front_matter: Object.keys(suggested).length > 0 ? suggested : void 0
5935
+ });
5936
+ }
5937
+ let adrsWithoutCode = 0;
5938
+ for (const adr of adrs) {
5939
+ if (adr.codeGlobs.length > 0) continue;
5940
+ adrsWithoutCode++;
5941
+ const id = adr.id || adr.title;
5942
+ hints.push({
5943
+ artifact_id: id,
5944
+ artifact_type: "decision",
5945
+ path: adr.relPath,
5946
+ severity: "info",
5947
+ missing: ["code"],
5948
+ reason: "This ADR defines technical decisions but does not declare which paths it governs.",
5949
+ message: `${id} has no governed code paths.`,
5950
+ suggested_front_matter: { code: ["<path/to/code>"] }
5951
+ });
5952
+ }
5953
+ for (const cap of capabilityHeadings(dir)) {
5954
+ if (referencedCapabilitySlugs.has(slug2(cap))) continue;
5955
+ hints.push({
5956
+ artifact_id: cap,
5957
+ artifact_type: "capability",
5958
+ severity: "info",
5959
+ missing: ["work-item"],
5960
+ reason: "This capability is declared but no Work Item references it yet.",
5961
+ message: `Capability "${cap}" is not linked to any Work Item.`
5962
+ });
5963
+ }
5964
+ let capsulesWithoutWi = 0;
5965
+ for (const cap of capsules) {
5966
+ if (referencedCapsuleIds.has(cap.id)) continue;
5967
+ capsulesWithoutWi++;
5968
+ hints.push({
5969
+ artifact_id: cap.id,
5970
+ artifact_type: "knowledge-capsule",
5971
+ path: cap.path,
5972
+ severity: "info",
5973
+ missing: ["work-item"],
5974
+ reason: "This Knowledge Capsule is available but no Work Item declares `capsules:` for it.",
5975
+ message: `Knowledge Capsule "${cap.id}" is not linked to any Work Item.`
5976
+ });
5977
+ }
5978
+ const inEdge = /* @__PURE__ */ new Set();
5979
+ for (const e of graph.edges) {
5980
+ inEdge.add(e.from);
5981
+ inEdge.add(e.to);
5982
+ }
5983
+ const nodes = graph.nodes.length;
5984
+ const connected = graph.nodes.filter((n) => inEdge.has(n.id)).length;
5985
+ const relationshipEdges = graph.edges.filter((e) => e.type !== "informs").length;
5986
+ const metrics = {
5987
+ nodes_count: nodes,
5988
+ edges_count: graph.edges.length,
5989
+ connected_nodes_count: connected,
5990
+ isolated_nodes_count: nodes - connected,
5991
+ active_work_items_without_code: activeWithoutCode,
5992
+ active_work_items_without_capabilities: activeWithoutCapabilities,
5993
+ work_items_without_source: wisWithoutSource,
5994
+ adrs_without_code: adrsWithoutCode,
5995
+ capsules_without_related_work_items: capsulesWithoutWi
5996
+ };
5997
+ const quality = assessQuality(nodes, relationshipEdges, hints.length);
5998
+ return {
5999
+ generated_at: now.toISOString(),
6000
+ quality,
6001
+ summary: { nodes, edges: graph.edges.length, hints: hints.length },
6002
+ metrics,
6003
+ hints
6004
+ };
6005
+ }
6006
+ function assessQuality(nodes, relationshipEdges, hintCount) {
6007
+ if (nodes === 0 || relationshipEdges === 0) return "empty";
6008
+ if (relationshipEdges / nodes < 0.25) return "sparse";
6009
+ if (hintCount > 0) return "partial";
6010
+ return "good";
6011
+ }
6012
+ var QUALITY_NOTE = {
6013
+ good: "Most active artifacts have meaningful relationships.",
6014
+ partial: "Some active artifacts have missing relationship metadata.",
6015
+ sparse: "The graph has many nodes but few meaningful edges.",
6016
+ empty: "The graph has almost no relationships."
6017
+ };
6018
+ function yamlBlock(fm) {
6019
+ const lines = ["```yaml"];
6020
+ for (const [k, vals] of Object.entries(fm)) {
6021
+ lines.push(`${k}:`);
6022
+ for (const v of vals) lines.push(` - ${v}`);
6023
+ }
6024
+ lines.push("```");
6025
+ return lines;
6026
+ }
6027
+ function renderGraphHintsMarkdown(report) {
6028
+ const lines = [];
6029
+ lines.push("# Kaddo Graph Hints");
6030
+ lines.push("");
6031
+ lines.push("Generated by `kaddo graph export`. Suggestions only \u2014 Kaddo never edits your artifacts.");
6032
+ lines.push("");
6033
+ lines.push("## Summary");
6034
+ lines.push("");
6035
+ lines.push(`- Relationship quality: ${report.quality} \u2014 ${QUALITY_NOTE[report.quality]}`);
6036
+ lines.push(`- Nodes: ${report.summary.nodes}`);
6037
+ lines.push(`- Edges: ${report.summary.edges}`);
6038
+ lines.push(`- Hints: ${report.summary.hints}`);
6039
+ lines.push("");
6040
+ if (report.hints.length === 0) {
6041
+ lines.push("No hints \u2014 the declared relationships look healthy. \u{1F389}");
6042
+ lines.push("");
6043
+ return lines.join("\n");
6044
+ }
6045
+ lines.push("## Hints");
6046
+ lines.push("");
6047
+ for (const h of report.hints) {
6048
+ lines.push(`### ${h.artifact_id}${h.path ? ` \u2014 \`${h.path}\`` : ""}`);
6049
+ lines.push("");
6050
+ lines.push("Missing metadata:");
6051
+ lines.push("");
6052
+ for (const m of h.missing) lines.push(`- \`${m}\``);
6053
+ lines.push("");
6054
+ if (h.suggested_front_matter) {
6055
+ lines.push("Suggested front matter:");
6056
+ lines.push("");
6057
+ lines.push(...yamlBlock(h.suggested_front_matter));
6058
+ lines.push("");
6059
+ }
6060
+ lines.push(`Reason: ${h.reason}`);
6061
+ lines.push("");
6062
+ }
6063
+ lines.push("> Use the `graph-agent` to turn these hints into precise front matter \u2014 you confirm and apply.");
6064
+ lines.push("");
6065
+ return lines.join("\n");
6066
+ }
6067
+ function serializeGraphHintsJson(report) {
6068
+ return JSON.stringify(report, null, 2) + "\n";
6069
+ }
6070
+ function loadGraphHints(dir) {
6071
+ const p2 = join(dir, ".kaddo", "graph-hints.json");
6072
+ if (!exists(p2)) return null;
6073
+ try {
6074
+ const report = JSON.parse(readFile(p2));
6075
+ const hints = Array.isArray(report.hints) ? report.hints : [];
6076
+ return {
6077
+ quality: report.quality ?? "empty",
6078
+ totalHints: hints.length,
6079
+ activeWorkItemHints: hints.filter((h) => h.artifact_type === "work-item").length,
6080
+ messages: hints.map((h) => h.message).filter(Boolean)
6081
+ };
6082
+ } catch {
6083
+ return null;
6084
+ }
6085
+ }
6086
+
6087
+ // src/core/knowledge-discovery.ts
6088
+ var KNOWLEDGE4 = "knowledge";
5590
6089
  var CONSOLIDATED_TYPE = {
5591
6090
  Business: "business",
5592
6091
  Product: "product",
@@ -5628,10 +6127,10 @@ function layerForType(type) {
5628
6127
  }
5629
6128
  function layerFromPath(filePath) {
5630
6129
  const p2 = filePath.replace(/\\/g, "/");
5631
- if (p2.includes(`/${KNOWLEDGE2}/business/`)) return "Business";
5632
- if (p2.includes(`/${KNOWLEDGE2}/product/`)) return "Product";
5633
- if (p2.includes(`/${KNOWLEDGE2}/tech/`)) return "Tech";
5634
- if (p2.includes(`/${KNOWLEDGE2}/delivery/`)) return "Delivery";
6130
+ if (p2.includes(`/${KNOWLEDGE4}/business/`)) return "Business";
6131
+ if (p2.includes(`/${KNOWLEDGE4}/product/`)) return "Product";
6132
+ if (p2.includes(`/${KNOWLEDGE4}/tech/`)) return "Tech";
6133
+ if (p2.includes(`/${KNOWLEDGE4}/delivery/`)) return "Delivery";
5635
6134
  return null;
5636
6135
  }
5637
6136
  function basename(p2) {
@@ -5644,7 +6143,7 @@ function discoverLayers(dir) {
5644
6143
  Tech: blank(),
5645
6144
  Delivery: blank()
5646
6145
  };
5647
- const archDir = join(dir, KNOWLEDGE2);
6146
+ const archDir = join(dir, KNOWLEDGE4);
5648
6147
  const artifacts = exists(archDir) ? readArtifacts(archDir) : [];
5649
6148
  for (const a of artifacts) {
5650
6149
  const type = a.type;
@@ -5666,7 +6165,7 @@ function discoverLayers(dir) {
5666
6165
  }
5667
6166
  if (type === "adr" || type === "decision") slot.hasDecision = true;
5668
6167
  }
5669
- if (existsDirWithMd(join(dir, KNOWLEDGE2, "tech", "decisions"))) acc.Tech.structured = true;
6168
+ if (existsDirWithMd(join(dir, KNOWLEDGE4, "tech", "decisions"))) acc.Tech.structured = true;
5670
6169
  return ["Business", "Product", "Tech", "Delivery"].map((layer2) => ({
5671
6170
  layer: layer2,
5672
6171
  status: statusFor(layer2, acc[layer2]),
@@ -5915,6 +6414,8 @@ function buildProjectExplanation(dir) {
5915
6414
  domains,
5916
6415
  duplicateWorkItems,
5917
6416
  externalCapsules: loadExternalCapsules(dir),
6417
+ graph: loadGraphSummary(dir),
6418
+ graphHints: loadGraphHints(dir),
5918
6419
  layers,
5919
6420
  roadmap,
5920
6421
  mappedModules,
@@ -6054,6 +6555,17 @@ function renderExplanationHuman(exp) {
6054
6555
  }
6055
6556
  lines.push("");
6056
6557
  }
6558
+ if (exp.graph) {
6559
+ lines.push("## Knowledge Graph");
6560
+ lines.push(`- Nodes: ${exp.graph.nodes}`);
6561
+ lines.push(`- Edges: ${exp.graph.edges}`);
6562
+ if (exp.graphHints) {
6563
+ lines.push(`- Quality: ${exp.graphHints.quality}`);
6564
+ lines.push(`- Hints: ${exp.graphHints.totalHints}`);
6565
+ }
6566
+ if (exp.graph.generatedAt) lines.push(`- Last exported: ${exp.graph.generatedAt}`);
6567
+ lines.push("");
6568
+ }
6057
6569
  if (exp.missingKnowledge.length > 0) {
6058
6570
  lines.push("## Missing Knowledge");
6059
6571
  for (const m of exp.missingKnowledge) lines.push(`- ${m}`);
@@ -6461,6 +6973,8 @@ function buildContextPack(dir, config, now = /* @__PURE__ */ new Date()) {
6461
6973
  phase,
6462
6974
  deliveryMix,
6463
6975
  external: loadExternalCapsules(dir),
6976
+ graph: loadGraphSummary(dir),
6977
+ graphHints: loadGraphHints(dir),
6464
6978
  mappedModules,
6465
6979
  missing,
6466
6980
  // VS-052: the handoff is driven by the REAL phase, not project.state, so the pack never
@@ -6626,6 +7140,34 @@ function renderContextPack(pack) {
6626
7140
  parts.push(lines.join("\n") + "\n");
6627
7141
  }
6628
7142
  }
7143
+ if (pack.graph) {
7144
+ parts.push("## Knowledge Graph\n");
7145
+ parts.push(
7146
+ [
7147
+ "- Available: yes",
7148
+ `- Nodes: ${pack.graph.nodes}`,
7149
+ `- Edges: ${pack.graph.edges}`,
7150
+ `- Active Work Items connected to code: ${pack.graph.activeWorkItemsConnectedToCode}`
7151
+ ].join("\n") + "\n"
7152
+ );
7153
+ parts.push("Full graph: `.kaddo/graph.json` / `.kaddo/graph.mmd` (run `kaddo graph export` to refresh).\n");
7154
+ }
7155
+ if (pack.graphHints && pack.graphHints.totalHints > 0) {
7156
+ parts.push("## Graph Hints\n");
7157
+ parts.push(
7158
+ [
7159
+ `Graph relationship quality: ${pack.graphHints.quality}`,
7160
+ `Active hints: ${pack.graphHints.activeWorkItemHints}`,
7161
+ "Suggested agent: graph-agent"
7162
+ ].join("\n") + "\n"
7163
+ );
7164
+ const shown = pack.graphHints.messages.slice(0, 3);
7165
+ parts.push(shown.map((m) => `- ${m}`).join("\n") + "\n");
7166
+ if (pack.graphHints.totalHints > shown.length) {
7167
+ parts.push(`(+${pack.graphHints.totalHints - shown.length} more in \`.kaddo/graph-hints.md\`)
7168
+ `);
7169
+ }
7170
+ }
6629
7171
  parts.push("## Missing Context\n");
6630
7172
  if (missing.length > 0) {
6631
7173
  parts.push(missing.map((m) => `- ${m}`).join("\n") + "\n");
@@ -6955,6 +7497,15 @@ function runUnderstand() {
6955
7497
  }
6956
7498
  console.log(" \u2192 Review the relevant capsule before changing integration behavior with it.");
6957
7499
  }
7500
+ const graphHints = loadGraphHints(dir);
7501
+ if (assessment.phase === "Active Delivery" && graphHints && graphHints.activeWorkItemHints > 0) {
7502
+ console.log("");
7503
+ console.log(
7504
+ `Graph hints: ${graphHints.activeWorkItemHints} active Work Item(s) have limited graph relationships (quality: ${graphHints.quality}).`
7505
+ );
7506
+ console.log(" \u2192 Review graph hints before continuing with implementation.");
7507
+ console.log(" Suggested agent: graph-agent (see .kaddo/graph-hints.md)");
7508
+ }
6958
7509
  const active = activeWorkItems(dir);
6959
7510
  if (active.length > 0) {
6960
7511
  console.log("");
@@ -8067,7 +8618,7 @@ ${QUALITY}
8067
8618
  - [ ] Capabilities describe outcomes, not implementation.
8068
8619
  - [ ] Each capability cites evidence or is flagged as an assumption.
8069
8620
  `;
8070
- var KNOWLEDGE3 = `---
8621
+ var KNOWLEDGE5 = `---
8071
8622
  type: current-state
8072
8623
  updated_at: YYYY-MM-DD
8073
8624
  ---
@@ -9077,7 +9628,7 @@ var KADDO_TEMPLATES = [
9077
9628
  description: "What is true about the product right now.",
9078
9629
  whenToUse: "Created by `kaddo init`; keep it current as the product evolves.",
9079
9630
  relatedCommand: "kaddo init",
9080
- content: KNOWLEDGE3
9631
+ content: KNOWLEDGE5
9081
9632
  },
9082
9633
  // business / product (bootstrap — consolidated, minimal)
9083
9634
  {
@@ -9729,7 +10280,7 @@ async function runBootstrap(dir = cwd()) {
9729
10280
  }
9730
10281
 
9731
10282
  // src/commands/capsule.ts
9732
- function slug(s) {
10283
+ function slug3(s) {
9733
10284
  return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
9734
10285
  }
9735
10286
  function runCapsuleExport() {
@@ -9737,7 +10288,7 @@ function runCapsuleExport() {
9737
10288
  const config = requireConfig(dir);
9738
10289
  intro2("kaddo capsule export");
9739
10290
  const capsule = buildCapsule(dir, config);
9740
- const name = slug(config.project.name);
10291
+ const name = slug3(config.project.name);
9741
10292
  const mdPath = join(".kaddo", "exports", `${name}.capsule.md`);
9742
10293
  const jsonPath = join(".kaddo", "exports", `${name}.capsule.json`);
9743
10294
  writeFile(join(dir, mdPath), renderCapsuleMarkdown(capsule));
@@ -9773,6 +10324,47 @@ function runCapsuleAdd(srcPath) {
9773
10324
  outro2("External capsule registered.");
9774
10325
  }
9775
10326
 
10327
+ // src/commands/graph.ts
10328
+ function runGraphExport(opts = {}) {
10329
+ const dir = cwd();
10330
+ const config = requireConfig(dir);
10331
+ intro2("kaddo graph export");
10332
+ const scope = opts.scope === "all" ? "all" : "active";
10333
+ const format = opts.format;
10334
+ const writeJson = format !== "mermaid";
10335
+ const writeMermaid = format !== "json";
10336
+ const graph = buildGraph(dir, config, { scope });
10337
+ const hints = buildGraphHints(dir, graph);
10338
+ const written = [];
10339
+ if (writeJson) {
10340
+ const rel = join(".kaddo", "graph.json");
10341
+ writeFile(join(dir, rel), serializeGraphJson(graph));
10342
+ written.push(rel.replace(/\\/g, "/"));
10343
+ }
10344
+ if (writeMermaid) {
10345
+ const rel = join(".kaddo", "graph.mmd");
10346
+ writeFile(join(dir, rel), renderGraphMermaid(graph));
10347
+ written.push(rel.replace(/\\/g, "/"));
10348
+ }
10349
+ writeFile(join(dir, ".kaddo", "graph-hints.md"), renderGraphHintsMarkdown(hints));
10350
+ writeFile(join(dir, ".kaddo", "graph-hints.json"), serializeGraphHintsJson(hints));
10351
+ written.push(".kaddo/graph-hints.md", ".kaddo/graph-hints.json");
10352
+ log2.success("Knowledge graph exported.");
10353
+ log2.info(`Scope: ${scope} \xB7 Nodes: ${graph.nodes.length} \xB7 Edges: ${graph.edges.length}`);
10354
+ log2.info(`Relationship quality: ${hints.quality}`);
10355
+ if (hints.hints.length > 0) {
10356
+ const shown = hints.hints.slice(0, 5);
10357
+ log2.warn(`${hints.hints.length} metadata hint(s) available:`);
10358
+ for (const h of shown) log2.info(`- ${h.message}`);
10359
+ if (hints.hints.length > shown.length) {
10360
+ log2.info(`\u2026and ${hints.hints.length - shown.length} more \u2014 see .kaddo/graph-hints.md`);
10361
+ }
10362
+ }
10363
+ for (const f of written) log2.info(`- ${f}`);
10364
+ printCommandFooter("graph export");
10365
+ outro2("Knowledge graph ready.");
10366
+ }
10367
+
9776
10368
  // src/index.ts
9777
10369
  var require2 = createRequire(import.meta.url);
9778
10370
  var { version } = require2("../package.json");
@@ -9797,6 +10389,10 @@ capsuleCmd.command("export").description("Write a Knowledge Capsule about this p
9797
10389
  capsuleCmd.command("add <path>").description("Register an external Knowledge Capsule as project context (.kaddo/external.yml)").action((path5) => {
9798
10390
  runCapsuleAdd(path5);
9799
10391
  });
10392
+ var graphCmd = program.command("graph").description("Export the lightweight, file-based knowledge graph of the project");
10393
+ graphCmd.command("export").description("Write the knowledge graph to .kaddo/graph.json and .kaddo/graph.mmd").option("--scope <scope>", "Graph scope: active (default) or all").option("--format <format>", "Output format: json, mermaid (default: both)").action((opts) => {
10394
+ runGraphExport(opts);
10395
+ });
9800
10396
  program.command("guard").description("Check if modified code has related artifacts that were not updated").option("--staged", "Check only staged files").option("--no-interactive", "Disable interactive ignore prompts").option("--ci", "CI mode: output JSON, no prompts, non-blocking").option("--json", "Output JSON (alias for --ci)").option("--workspace", "Also check local mapped module repos from .kaddo/modules.yml (opt-in)").action(async (opts) => {
9801
10397
  await runGuard(opts);
9802
10398
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/cli",
3
- "version": "3.16.5",
3
+ "version": "3.18.0",
4
4
  "description": "Knowledge Driven Development toolkit",
5
5
  "license": "MIT",
6
6
  "repository": {