@kaddo/cli 3.21.0 → 3.22.1

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 +2 -0
  2. package/dist/index.js +144 -45
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -508,6 +508,8 @@ create --from roadmap → owners → guard → explain`.
508
508
  | v3.19 | Read-only MCP server `@kaddo/mcp` (resources, tools, prompts over stdio) |
509
509
  | v3.20 | MCP derived tools: safe regeneration of context/explain/understand/graph/capsule-draft under `.kaddo/` |
510
510
  | v3.21 | Reusable Skills layer (`kaddo add skills`); skills in context/explain/understand, agent prompts and MCP |
511
+ | v3.22 | Guard & graph scope semantics: graph scope metadata, contextual-empty messaging, Guard ownership scope (active + completed; `--include-archived`) |
512
+ | v3.22.1 | Guard project-root path normalization: matches `code:` globs when the project is a subfolder of the Git root |
511
513
 
512
514
  **Optional modules (installed with `kaddo add`):**
513
515
 
package/dist/index.js CHANGED
@@ -592,8 +592,8 @@ function loadConfig(dir) {
592
592
  const parsed = configSchema.safeParse(raw ?? {});
593
593
  if (!parsed.success) {
594
594
  const issues = parsed.error.issues.map((i) => {
595
- const path5 = i.path.join(".");
596
- return path5 ? ` - ${path5}: ${i.message}` : ` - ${i.message}`;
595
+ const path6 = i.path.join(".");
596
+ return path6 ? ` - ${path6}: ${i.message}` : ` - ${i.message}`;
597
597
  }).join("\n");
598
598
  throw new ConfigError(`Invalid .kaddo/config.yml:
599
599
  ${issues}`);
@@ -4902,6 +4902,14 @@ async function isGitRepo(dir) {
4902
4902
  return false;
4903
4903
  }
4904
4904
  }
4905
+ async function getGitRoot(dir) {
4906
+ try {
4907
+ const result = await execa("git", ["rev-parse", "--show-toplevel"], { cwd: dir });
4908
+ return result.stdout.trim() || null;
4909
+ } catch {
4910
+ return null;
4911
+ }
4912
+ }
4905
4913
  async function getUntrackedFiles() {
4906
4914
  try {
4907
4915
  const result = await execa("git", ["ls-files", "--others", "--exclude-standard"]);
@@ -5048,6 +5056,20 @@ function discoverWorkItems(dir) {
5048
5056
 
5049
5057
  // src/core/diff-analysis.ts
5050
5058
  import { minimatch } from "minimatch";
5059
+ import path3 from "path";
5060
+ function toPosix2(p2) {
5061
+ return p2.replace(/\\/g, "/");
5062
+ }
5063
+ function normalizeTouchedPathForProject(touchedPath, gitRoot, projectRoot) {
5064
+ const t = toPosix2(touchedPath).replace(/^\.\//, "").replace(/^\/+/, "");
5065
+ if (!gitRoot) return t;
5066
+ const prefix = toPosix2(path3.relative(gitRoot, projectRoot)).replace(/^\.\//, "").replace(/\/+$/, "");
5067
+ if (prefix === "" || prefix === ".") return t;
5068
+ if (prefix.startsWith("..")) return t;
5069
+ if (t === prefix) return null;
5070
+ if (t.startsWith(prefix + "/")) return t.slice(prefix.length + 1);
5071
+ return null;
5072
+ }
5051
5073
  function normalizePath(p2) {
5052
5074
  return p2.replace(/\\/g, "/");
5053
5075
  }
@@ -5107,10 +5129,10 @@ function analyzeGuard(touchedFiles, artifacts, silentWithoutOwnership) {
5107
5129
  import { parse as parseYaml4, stringify as stringifyYaml2 } from "yaml";
5108
5130
  var IGNORE_FILE = ".kaddo/ignores.yml";
5109
5131
  function loadIgnores(dir) {
5110
- const path5 = join(dir, IGNORE_FILE);
5111
- if (!exists(path5)) return [];
5132
+ const path6 = join(dir, IGNORE_FILE);
5133
+ if (!exists(path6)) return [];
5112
5134
  try {
5113
- const raw = readFile(path5);
5135
+ const raw = readFile(path6);
5114
5136
  const parsed = parseYaml4(raw);
5115
5137
  return Array.isArray(parsed) ? parsed : [];
5116
5138
  } catch {
@@ -5118,7 +5140,7 @@ function loadIgnores(dir) {
5118
5140
  }
5119
5141
  }
5120
5142
  function saveIgnore(dir, entry) {
5121
- const path5 = join(dir, IGNORE_FILE);
5143
+ const path6 = join(dir, IGNORE_FILE);
5122
5144
  const existing = loadIgnores(dir);
5123
5145
  const idx = existing.findIndex((e) => e.artifact_id === entry.artifact_id);
5124
5146
  if (idx >= 0) {
@@ -5126,17 +5148,17 @@ function saveIgnore(dir, entry) {
5126
5148
  } else {
5127
5149
  existing.push(entry);
5128
5150
  }
5129
- writeFile(path5, stringifyYaml2(existing));
5151
+ writeFile(path6, stringifyYaml2(existing));
5130
5152
  }
5131
5153
  function isIgnored(ignores, artifactId) {
5132
5154
  return ignores.find((e) => e.artifact_id === artifactId);
5133
5155
  }
5134
5156
  function removeIgnore(dir, artifactId) {
5135
- const path5 = join(dir, IGNORE_FILE);
5157
+ const path6 = join(dir, IGNORE_FILE);
5136
5158
  const existing = loadIgnores(dir);
5137
5159
  const filtered = existing.filter((e) => e.artifact_id !== artifactId);
5138
5160
  if (filtered.length === existing.length) return false;
5139
- writeFile(path5, stringifyYaml2(filtered));
5161
+ writeFile(path6, stringifyYaml2(filtered));
5140
5162
  return true;
5141
5163
  }
5142
5164
 
@@ -5158,11 +5180,11 @@ function moduleArtifactCoverage(dir, id) {
5158
5180
  };
5159
5181
  }
5160
5182
  function loadMappedModules(dir) {
5161
- const path5 = join(dir, DESCRIPTOR_PATH);
5162
- if (!exists(path5)) return [];
5183
+ const path6 = join(dir, DESCRIPTOR_PATH);
5184
+ if (!exists(path6)) return [];
5163
5185
  let parsed;
5164
5186
  try {
5165
- parsed = parseYaml5(readFile(path5));
5187
+ parsed = parseYaml5(readFile(path6));
5166
5188
  } catch {
5167
5189
  return [];
5168
5190
  }
@@ -5254,7 +5276,7 @@ async function collectWorkspaceChanges(dir, mode = "head", deps = defaultDeps) {
5254
5276
  }
5255
5277
 
5256
5278
  // src/commands/guard.ts
5257
- import path3 from "path";
5279
+ import path4 from "path";
5258
5280
  import { parse as parseYaml7 } from "yaml";
5259
5281
 
5260
5282
  // src/plugins/prisma.ts
@@ -5458,7 +5480,7 @@ function printHeader(touchedFiles) {
5458
5480
  function artifactLabel(artifact, dir) {
5459
5481
  if (artifact.id) return artifact.id;
5460
5482
  if (artifact.title) return artifact.title;
5461
- const rel = path3.relative(dir, artifact.filePath).split(path3.sep).join("/");
5483
+ const rel = path4.relative(dir, artifact.filePath).split(path4.sep).join("/");
5462
5484
  return rel || artifact.filePath;
5463
5485
  }
5464
5486
  function printWorkspaceHeader(scan2) {
@@ -5524,10 +5546,17 @@ async function offerIgnore(dir, match) {
5524
5546
  log2.success(`${id} added to .kaddo/ignores.yml`);
5525
5547
  return true;
5526
5548
  }
5527
- function printCIJson(dir, touchedFiles, activeMatches, ignoredCount, pluginSignals, affectedOwners, workspace) {
5549
+ function printCIJson(dir, touchedFiles, activeMatches, ignoredCount, pluginSignals, affectedOwners, workspace, includeArchived, normalizedFiles, outsideProjectFiles) {
5528
5550
  const output = {
5529
5551
  kaddo_guard: true,
5530
5552
  ci: true,
5553
+ ownership_scope: {
5554
+ included: includeArchived ? ["draft", "ready", "in-progress", "blocked", "completed", "archived"] : ["draft", "ready", "in-progress", "blocked", "completed"],
5555
+ excluded: includeArchived ? [] : ["archived"]
5556
+ },
5557
+ // Git-root-relative → project-root-relative mapping used for matching (VS-060.1).
5558
+ normalized_files: normalizedFiles,
5559
+ files_outside_project: outsideProjectFiles,
5531
5560
  touched_files: touchedFiles.length,
5532
5561
  fyi_count: activeMatches.length,
5533
5562
  ignored_count: ignoredCount,
@@ -5556,6 +5585,12 @@ function printCIJson(dir, touchedFiles, activeMatches, ignoredCount, pluginSigna
5556
5585
  }
5557
5586
  console.log(JSON.stringify(output, null, 2));
5558
5587
  }
5588
+ function printOwnershipScope(includeArchived) {
5589
+ console.log("Ownership scope:");
5590
+ console.log("- Active and completed Work Items");
5591
+ console.log(includeArchived ? "- Archived Work Items included (--include-archived)" : "- Archived Work Items excluded");
5592
+ console.log("");
5593
+ }
5559
5594
  async function runGuard(opts = {}) {
5560
5595
  const dir = cwd();
5561
5596
  const interactive = opts.interactive !== false && !opts.ci && !opts.json;
@@ -5570,8 +5605,16 @@ async function runGuard(opts = {}) {
5570
5605
  const ignores = loadIgnores(dir);
5571
5606
  const plugins = resolvePlugins(config.plugins ?? []);
5572
5607
  const mode = opts.staged ? "staged" : "head";
5573
- const currentFiles = await getModifiedFiles(mode);
5574
- const untrackedFiles = opts.staged ? [] : await getUntrackedFiles();
5608
+ const currentFilesRaw = await getModifiedFiles(mode);
5609
+ const gitRoot = await getGitRoot(dir);
5610
+ const currentPairs = currentFilesRaw.map((raw) => ({
5611
+ raw,
5612
+ project: normalizeTouchedPathForProject(raw, gitRoot, dir)
5613
+ }));
5614
+ const inProjectPairs = currentPairs.filter((p2) => p2.project !== null);
5615
+ const currentFiles = inProjectPairs.map((p2) => p2.project);
5616
+ const outsideProjectFiles = currentPairs.filter((p2) => p2.project === null).map((p2) => p2.raw);
5617
+ const untrackedFiles = opts.staged ? [] : (await getUntrackedFiles()).map((f) => normalizeTouchedPathForProject(f, gitRoot, dir)).filter((f) => f !== null);
5575
5618
  const printUntracked = () => {
5576
5619
  if (untrackedFiles.length === 0 || jsonMode) return;
5577
5620
  console.log("");
@@ -5586,6 +5629,7 @@ async function runGuard(opts = {}) {
5586
5629
  }
5587
5630
  const workspaceFiles = workspaceScan ? workspaceScan.changedFiles.map((c) => c.normalizedPath) : [];
5588
5631
  const touchedFiles = [.../* @__PURE__ */ new Set([...currentFiles, ...workspaceFiles])];
5632
+ const displayTouched = [.../* @__PURE__ */ new Set([...inProjectPairs.map((p2) => p2.raw), ...workspaceFiles])];
5589
5633
  if (touchedFiles.length === 0) {
5590
5634
  if (workspaceScan) printWorkspaceHeader(workspaceScan);
5591
5635
  console.log("kaddo guard: no modified files detected.");
@@ -5597,7 +5641,10 @@ async function runGuard(opts = {}) {
5597
5641
  console.log("kaddo guard: no knowledge/ directory found. Run `kaddo init` first.");
5598
5642
  return;
5599
5643
  }
5600
- const artifacts = discoverKnowledge(dir);
5644
+ const includeArchived = opts.includeArchived === true;
5645
+ const artifacts = discoverKnowledge(dir).filter(
5646
+ (a) => !(a.isWorkItem && a.lifecycle === "archived" && !includeArchived)
5647
+ );
5601
5648
  const result = analyzeGuard(touchedFiles, artifacts, silentWithoutOwnership);
5602
5649
  const pluginSignals = runPlugins(plugins, currentFiles, (filePath) => {
5603
5650
  const abs = join(dir, filePath);
@@ -5618,7 +5665,7 @@ async function runGuard(opts = {}) {
5618
5665
  const ownerMapCI = loadOwners(dir);
5619
5666
  const matchedDomainsCI = collectMatchedDomains(activeMatches.map((m) => m.artifact.domains));
5620
5667
  const affectedOwnersCI = resolveAffectedOwners(matchedDomainsCI, ownerMapCI);
5621
- printCIJson(dir, touchedFiles, activeMatches, alreadyIgnoredMatches.length, pluginSignals, affectedOwnersCI, workspaceScan);
5668
+ printCIJson(dir, touchedFiles, activeMatches, alreadyIgnoredMatches.length, pluginSignals, affectedOwnersCI, workspaceScan, includeArchived, inProjectPairs.map((p2) => ({ raw_path: p2.raw, project_path: p2.project })), outsideProjectFiles);
5622
5669
  return;
5623
5670
  }
5624
5671
  if (result.silenced && pluginSignals.length === 0) {
@@ -5627,13 +5674,19 @@ async function runGuard(opts = {}) {
5627
5674
  }
5628
5675
  if (result.matches.length === 0 && pluginSignals.length === 0) {
5629
5676
  if (workspaceScan) printWorkspaceHeader(workspaceScan);
5630
- printHeader(touchedFiles);
5677
+ printHeader(displayTouched);
5678
+ printOwnershipScope(includeArchived);
5631
5679
  console.log(" No artifact ownership matches found.");
5680
+ console.log("");
5681
+ console.log("Note:");
5682
+ console.log("Guard checks ownership from active and completed Work Items.");
5683
+ console.log("Run `kaddo explain` to inspect ownership coverage.");
5632
5684
  printUntracked();
5633
5685
  return;
5634
5686
  }
5635
5687
  if (workspaceScan) printWorkspaceHeader(workspaceScan);
5636
- printHeader(touchedFiles);
5688
+ printHeader(displayTouched);
5689
+ printOwnershipScope(includeArchived);
5637
5690
  printUntracked();
5638
5691
  for (const match of activeMatches) {
5639
5692
  printFYI(match, dir);
@@ -5980,7 +6033,7 @@ function sectionParagraph(md, title) {
5980
6033
  }
5981
6034
  return "";
5982
6035
  }
5983
- function parseCapsule(id, path5, md) {
6036
+ function parseCapsule(id, path6, md) {
5984
6037
  const { data } = matter2(md);
5985
6038
  const updatedAt = data.updated_at ? String(data.updated_at) : void 0;
5986
6039
  let ageDays = null;
@@ -5990,7 +6043,7 @@ function parseCapsule(id, path5, md) {
5990
6043
  }
5991
6044
  return {
5992
6045
  id,
5993
- path: path5,
6046
+ path: path6,
5994
6047
  system: data.system ? String(data.system) : id,
5995
6048
  owner: data.owner ? String(data.owner) : void 0,
5996
6049
  updatedAt,
@@ -6040,14 +6093,19 @@ function loadExternalCapsules(dir) {
6040
6093
 
6041
6094
  // src/core/graph.ts
6042
6095
  var KNOWLEDGE2 = "knowledge";
6043
- function toPosix2(p2) {
6096
+ var ACTIVE_STATUSES = ["draft", "ready", "in-progress", "blocked"];
6097
+ var ALL_STATUSES = ["draft", "ready", "in-progress", "blocked", "completed"];
6098
+ function scopeStatuses(scope) {
6099
+ return scope === "all" ? { included: ALL_STATUSES, excluded: ["archived"] } : { included: ACTIVE_STATUSES, excluded: ["completed", "archived"] };
6100
+ }
6101
+ function toPosix3(p2) {
6044
6102
  return p2.replace(/\\/g, "/");
6045
6103
  }
6046
6104
  function slug(s) {
6047
6105
  return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
6048
6106
  }
6049
6107
  function isAdr(a) {
6050
- return toPosix2(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
6108
+ return toPosix3(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
6051
6109
  }
6052
6110
  function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
6053
6111
  const scope = opts.scope ?? "active";
@@ -6071,9 +6129,9 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
6071
6129
  ];
6072
6130
  const presentLayers = [];
6073
6131
  for (const layer2 of layerDocs) {
6074
- const path5 = layer2.files.map((f) => `${KNOWLEDGE2}/${f}`).find((rel) => exists(join(dir, rel)));
6075
- if (path5) {
6076
- addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path5 });
6132
+ const path6 = layer2.files.map((f) => `${KNOWLEDGE2}/${f}`).find((rel) => exists(join(dir, rel)));
6133
+ if (path6) {
6134
+ addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path6 });
6077
6135
  presentLayers.push(layer2.id);
6078
6136
  }
6079
6137
  }
@@ -6082,7 +6140,10 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
6082
6140
  }
6083
6141
  const all = discoverKnowledge(dir);
6084
6142
  const workItems = all.filter((a) => a.isWorkItem);
6085
- const selectedWIs = scope === "active" ? workItems.filter((a) => a.lifecycle && isActiveState(a.lifecycle)) : workItems;
6143
+ const { included, excluded } = scopeStatuses(scope);
6144
+ const includedSet = new Set(included);
6145
+ const activeWICount = workItems.filter((a) => a.lifecycle && isActiveState(a.lifecycle)).length;
6146
+ const selectedWIs = workItems.filter((a) => a.lifecycle && includedSet.has(a.lifecycle));
6086
6147
  for (const wi of selectedWIs) {
6087
6148
  const id = wi.id || wi.title;
6088
6149
  const wiNodeId = `wi:${id}`;
@@ -6152,6 +6213,7 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
6152
6213
  }
6153
6214
  }
6154
6215
  }
6216
+ const scopeReason = scope === "all" ? "All supported Work Item statuses are included." : activeWICount === 0 ? "No active Work Items found. Completed Work Items are excluded from active scope." : "Active Work Items only; completed and archived are excluded.";
6155
6217
  return {
6156
6218
  generated_at: now.toISOString(),
6157
6219
  project: {
@@ -6159,6 +6221,10 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
6159
6221
  state: config.project.state,
6160
6222
  structure: config.project.structure
6161
6223
  },
6224
+ scope,
6225
+ scope_reason: scopeReason,
6226
+ included_statuses: included,
6227
+ excluded_statuses: excluded,
6162
6228
  nodes: [...nodes.values()],
6163
6229
  edges
6164
6230
  };
@@ -6206,8 +6272,13 @@ function loadGraphSummary(dir) {
6206
6272
  const connected = new Set(
6207
6273
  edges.filter((e) => e.type === "owns" && activeWiIds.has(e.from)).map((e) => e.from)
6208
6274
  );
6275
+ const scope = graph.scope === "all" ? "all" : "active";
6209
6276
  return {
6210
6277
  generatedAt: String(graph.generated_at ?? ""),
6278
+ scope,
6279
+ scopeReason: String(graph.scope_reason ?? ""),
6280
+ includedStatuses: Array.isArray(graph.included_statuses) ? graph.included_statuses : [],
6281
+ excludedStatuses: Array.isArray(graph.excluded_statuses) ? graph.excluded_statuses : [],
6211
6282
  nodes: nodes.length,
6212
6283
  edges: edges.length,
6213
6284
  activeWorkItemsConnectedToCode: connected.size
@@ -6219,14 +6290,14 @@ function loadGraphSummary(dir) {
6219
6290
 
6220
6291
  // src/core/graph-hints.ts
6221
6292
  var KNOWLEDGE3 = "knowledge";
6222
- function toPosix3(p2) {
6293
+ function toPosix4(p2) {
6223
6294
  return p2.replace(/\\/g, "/");
6224
6295
  }
6225
6296
  function slug2(s) {
6226
6297
  return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
6227
6298
  }
6228
6299
  function isAdr2(a) {
6229
- return toPosix3(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
6300
+ return toPosix4(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
6230
6301
  }
6231
6302
  function capabilityHeadings(dir) {
6232
6303
  const p2 = join(dir, KNOWLEDGE3, "product", "capabilities.md");
@@ -6359,6 +6430,8 @@ function buildGraphHints(dir, graph, now = /* @__PURE__ */ new Date()) {
6359
6430
  const quality = assessQuality(nodes, relationshipEdges, hints.length);
6360
6431
  return {
6361
6432
  generated_at: now.toISOString(),
6433
+ scope: graph.scope,
6434
+ scope_reason: graph.scope_reason,
6362
6435
  quality,
6363
6436
  summary: { nodes, edges: graph.edges.length, hints: hints.length },
6364
6437
  metrics,
@@ -6394,13 +6467,23 @@ function renderGraphHintsMarkdown(report) {
6394
6467
  lines.push("");
6395
6468
  lines.push("## Summary");
6396
6469
  lines.push("");
6470
+ lines.push(`- Scope: ${report.scope} \u2014 ${report.scope_reason}`);
6397
6471
  lines.push(`- Relationship quality: ${report.quality} \u2014 ${QUALITY_NOTE[report.quality]}`);
6398
6472
  lines.push(`- Nodes: ${report.summary.nodes}`);
6399
6473
  lines.push(`- Edges: ${report.summary.edges}`);
6400
6474
  lines.push(`- Hints: ${report.summary.hints}`);
6401
6475
  lines.push("");
6402
6476
  if (report.hints.length === 0) {
6403
- lines.push("No hints \u2014 the declared relationships look healthy. \u{1F389}");
6477
+ if (report.quality === "empty") {
6478
+ lines.push("No active relationship hints were generated.");
6479
+ lines.push("");
6480
+ lines.push(`The graph is ${report.quality} for the **${report.scope}** scope \u2014 ${report.scope_reason}`);
6481
+ if (report.scope !== "all") {
6482
+ lines.push("Run `kaddo graph export --scope all` to inspect completed Work Items and historical relationships.");
6483
+ }
6484
+ } else {
6485
+ lines.push("No hints \u2014 the declared relationships look healthy. \u{1F389}");
6486
+ }
6404
6487
  lines.push("");
6405
6488
  return lines.join("\n");
6406
6489
  }
@@ -6436,6 +6519,8 @@ function loadGraphHints(dir) {
6436
6519
  const report = JSON.parse(readFile(p2));
6437
6520
  const hints = Array.isArray(report.hints) ? report.hints : [];
6438
6521
  return {
6522
+ scope: String(report.scope ?? "active"),
6523
+ scopeReason: String(report.scope_reason ?? ""),
6439
6524
  quality: report.quality ?? "empty",
6440
6525
  totalHints: hints.length,
6441
6526
  activeWorkItemHints: hints.filter((h) => h.artifact_type === "work-item").length,
@@ -6959,13 +7044,18 @@ function renderExplanationHuman(exp) {
6959
7044
  }
6960
7045
  if (exp.graph) {
6961
7046
  lines.push("## Knowledge Graph");
7047
+ lines.push(`- Scope: ${exp.graph.scope}`);
6962
7048
  lines.push(`- Nodes: ${exp.graph.nodes}`);
6963
7049
  lines.push(`- Edges: ${exp.graph.edges}`);
6964
7050
  if (exp.graphHints) {
6965
7051
  lines.push(`- Quality: ${exp.graphHints.quality}`);
6966
7052
  lines.push(`- Hints: ${exp.graphHints.totalHints}`);
6967
7053
  }
7054
+ if (exp.graph.scopeReason) lines.push(`- Reason: ${exp.graph.scopeReason}`);
6968
7055
  if (exp.graph.generatedAt) lines.push(`- Last exported: ${exp.graph.generatedAt}`);
7056
+ if (exp.graph.scope === "active" && exp.graphHints?.quality === "empty") {
7057
+ lines.push("- Tip: Run `kaddo graph export --scope all` to include completed Work Items");
7058
+ }
6969
7059
  lines.push("");
6970
7060
  }
6971
7061
  if (exp.skills.total > 0) {
@@ -7557,9 +7647,12 @@ function renderContextPack(pack) {
7557
7647
  parts.push(
7558
7648
  [
7559
7649
  "- Available: yes",
7650
+ `- Scope: ${pack.graph.scope}`,
7560
7651
  `- Nodes: ${pack.graph.nodes}`,
7561
7652
  `- Edges: ${pack.graph.edges}`,
7562
- `- Active Work Items connected to code: ${pack.graph.activeWorkItemsConnectedToCode}`
7653
+ ...pack.graphHints ? [`- Quality: ${pack.graphHints.quality}`] : [],
7654
+ `- Active Work Items connected to code: ${pack.graph.activeWorkItemsConnectedToCode}`,
7655
+ ...pack.graph.scopeReason ? [`- Reason: ${pack.graph.scopeReason}`] : []
7563
7656
  ].join("\n") + "\n"
7564
7657
  );
7565
7658
  parts.push("Full graph: `.kaddo/graph.json` / `.kaddo/graph.mmd` (run `kaddo graph export` to refresh).\n");
@@ -7948,7 +8041,7 @@ function runUnderstand() {
7948
8041
  }
7949
8042
 
7950
8043
  // src/core/classifier.ts
7951
- import path4 from "path";
8044
+ import path5 from "path";
7952
8045
  var MIGRATION_PATTERNS = [
7953
8046
  /supabase\/migrations\//,
7954
8047
  /prisma\/migrations\//,
@@ -7987,7 +8080,7 @@ var DEP_PATTERNS = [
7987
8080
  /^pom\.xml$/
7988
8081
  ];
7989
8082
  function normalizePath2(p2) {
7990
- return p2.split(path4.sep).join("/");
8083
+ return p2.split(path5.sep).join("/");
7991
8084
  }
7992
8085
  function matchesAny(file, patterns) {
7993
8086
  const normalized = normalizePath2(file);
@@ -8819,10 +8912,10 @@ ${globs.map((g) => ` - ${g}`).join("\n")}`);
8819
8912
  import { parse as parseYaml13, stringify as stringifyYaml5 } from "yaml";
8820
8913
  var DESCRIPTOR_PATH2 = "knowledge/module.yml";
8821
8914
  function readDescriptor(dir) {
8822
- const path5 = join(dir, DESCRIPTOR_PATH2);
8823
- if (!exists(path5)) return null;
8915
+ const path6 = join(dir, DESCRIPTOR_PATH2);
8916
+ if (!exists(path6)) return null;
8824
8917
  try {
8825
- return parseYaml13(readFile(path5));
8918
+ return parseYaml13(readFile(path6));
8826
8919
  } catch {
8827
8920
  return null;
8828
8921
  }
@@ -10427,10 +10520,10 @@ function slugify3(name) {
10427
10520
  return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
10428
10521
  }
10429
10522
  function readModulesDescriptor(dir) {
10430
- const path5 = join(dir, DESCRIPTOR_PATH3);
10431
- if (!exists(path5)) return { version: 1, modules: [] };
10523
+ const path6 = join(dir, DESCRIPTOR_PATH3);
10524
+ if (!exists(path6)) return { version: 1, modules: [] };
10432
10525
  try {
10433
- const parsed = parseYaml14(readFile(path5));
10526
+ const parsed = parseYaml14(readFile(path6));
10434
10527
  return { version: parsed.version ?? 1, modules: parsed.modules ?? [] };
10435
10528
  } catch {
10436
10529
  return { version: 1, modules: [] };
@@ -10797,9 +10890,15 @@ function runGraphExport(opts = {}) {
10797
10890
  writeFile(join(dir, ".kaddo", "graph-hints.md"), renderGraphHintsMarkdown(hints));
10798
10891
  writeFile(join(dir, ".kaddo", "graph-hints.json"), serializeGraphHintsJson(hints));
10799
10892
  written.push(".kaddo/graph-hints.md", ".kaddo/graph-hints.json");
10800
- log2.success("Knowledge graph exported.");
10893
+ log2.success(`Knowledge graph exported with ${scope} scope.`);
10801
10894
  log2.info(`Scope: ${scope} \xB7 Nodes: ${graph.nodes.length} \xB7 Edges: ${graph.edges.length}`);
10802
10895
  log2.info(`Relationship quality: ${hints.quality}`);
10896
+ const hasWorkItemNodes = graph.nodes.some((n) => n.type === "work-item");
10897
+ if (scope === "active" && !hasWorkItemNodes) {
10898
+ log2.warn("No active Work Items found.");
10899
+ log2.info("The active graph only includes knowledge layers.");
10900
+ log2.info("Run `kaddo graph export --scope all` to include completed Work Items.");
10901
+ }
10803
10902
  if (hints.hints.length > 0) {
10804
10903
  const shown = hints.hints.slice(0, 5);
10805
10904
  log2.warn(`${hints.hints.length} metadata hint(s) available:`);
@@ -10834,14 +10933,14 @@ var capsuleCmd = program.command("capsule").description("Export this project as
10834
10933
  capsuleCmd.command("export").description("Write a Knowledge Capsule about this project to .kaddo/exports/").action(() => {
10835
10934
  runCapsuleExport();
10836
10935
  });
10837
- capsuleCmd.command("add <path>").description("Register an external Knowledge Capsule as project context (.kaddo/external.yml)").action((path5) => {
10838
- runCapsuleAdd(path5);
10936
+ capsuleCmd.command("add <path>").description("Register an external Knowledge Capsule as project context (.kaddo/external.yml)").action((path6) => {
10937
+ runCapsuleAdd(path6);
10839
10938
  });
10840
10939
  var graphCmd = program.command("graph").description("Export the lightweight, file-based knowledge graph of the project");
10841
10940
  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) => {
10842
10941
  runGraphExport(opts);
10843
10942
  });
10844
- 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) => {
10943
+ 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)").option("--include-archived", "Include archived Work Items in ownership matching (excluded by default)").action(async (opts) => {
10845
10944
  await runGuard(opts);
10846
10945
  });
10847
10946
  var ignoreCmd = program.command("ignore").description("Manage guard ignore list");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/cli",
3
- "version": "3.21.0",
3
+ "version": "3.22.1",
4
4
  "description": "Knowledge Driven Development toolkit",
5
5
  "license": "MIT",
6
6
  "repository": {