@kaddo/cli 3.65.0 → 3.66.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 +263 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6401,7 +6401,14 @@ function parseArtifact(filePath, raw) {
6401
6401
  sourceId: String(data.source_id ?? ""),
6402
6402
  rawFrontmatter: data,
6403
6403
  decisions: Array.isArray(data.decisions) ? data.decisions.map(String).filter(Boolean) : [],
6404
- capsules: Array.isArray(data.capsules) ? data.capsules.map(String).filter(Boolean) : []
6404
+ capsules: Array.isArray(data.capsules) ? data.capsules.map(String).filter(Boolean) : [],
6405
+ implementationStatus: String(data.implementation_status ?? ""),
6406
+ validationStatus: String(data.validation_status ?? ""),
6407
+ releaseStatus: String(data.release_status ?? ""),
6408
+ affectedModules: Array.isArray(data.affected_modules) ? data.affected_modules.map(String).filter(Boolean) : [],
6409
+ refinedBy: String(data.refined_by ?? ""),
6410
+ implementedBy: String(data.implemented_by ?? ""),
6411
+ closedBy: String(data.closed_by ?? "")
6405
6412
  };
6406
6413
  } catch {
6407
6414
  return null;
@@ -6833,9 +6840,125 @@ function analyzeMetadataHealth(dir) {
6833
6840
  if (fileDrifted) drifted++;
6834
6841
  else healthy++;
6835
6842
  }
6843
+ const wiDir = join(dir, "knowledge/delivery/work-items");
6844
+ if (exists(wiDir)) {
6845
+ const walkWIs = (d) => {
6846
+ for (const entry of readDir(d)) {
6847
+ const p2 = join(d, entry);
6848
+ if (isFile(p2) && entry.endsWith(".md")) {
6849
+ try {
6850
+ const raw = readFile(p2);
6851
+ const fm2 = matter2(raw).data;
6852
+ if (fm2.status === "done") {
6853
+ const rel = p2.replace(dir + "/", "").replace(dir + "\\", "").replace(/\\/g, "/");
6854
+ findings.push({
6855
+ file: rel,
6856
+ field: "status",
6857
+ issue: "inconsistent",
6858
+ detail: "Legacy status `done` detected. Canonical status is `completed`."
6859
+ });
6860
+ }
6861
+ } catch {
6862
+ }
6863
+ } else if (!isFile(p2) && !entry.startsWith(".")) {
6864
+ walkWIs(p2);
6865
+ }
6866
+ }
6867
+ };
6868
+ walkWIs(wiDir);
6869
+ }
6836
6870
  return { findings, healthy, drifted };
6837
6871
  }
6838
6872
 
6873
+ // src/core/cross-repo-evidence.ts
6874
+ function analyzeCrossRepoEvidence(input) {
6875
+ const findings = [];
6876
+ const { id, lifecycle, implementationStatus, validationStatus, releaseStatus, affectedModules } = input;
6877
+ const raw = input.rawFrontmatter;
6878
+ const evidence = raw.implementation_evidence;
6879
+ const repos = evidence?.repositories ?? {};
6880
+ const releaseGates = Array.isArray(raw.release_gates) ? raw.release_gates : [];
6881
+ const exceptions = Array.isArray(raw.completion_exceptions) ? raw.completion_exceptions : [];
6882
+ const repoEvidence = {};
6883
+ for (const [repoId, data] of Object.entries(repos)) {
6884
+ const d = data;
6885
+ repoEvidence[repoId] = {
6886
+ role: String(d.role ?? (repoId === "core" ? "core" : "module")),
6887
+ status: String(d.status ?? "unknown")
6888
+ };
6889
+ }
6890
+ for (const mod of affectedModules) {
6891
+ if (mod === "core") continue;
6892
+ if (!input.registeredModuleIds.includes(mod)) {
6893
+ findings.push({ id, severity: "blocking", message: `Affected module "${mod}" is not registered in .kaddo/modules.yml.` });
6894
+ }
6895
+ }
6896
+ for (const repoId of input.modifiedRepoIds) {
6897
+ if (!affectedModules.includes(repoId) && repoId !== "core") {
6898
+ findings.push({ id, severity: "blocking", message: `Repository "${repoId}" was modified but not declared in affected_modules.` });
6899
+ }
6900
+ }
6901
+ for (const mod of affectedModules) {
6902
+ if (!repoEvidence[mod] && Object.keys(repos).length > 0) {
6903
+ findings.push({ id, severity: "warning", message: `Declared affected module "${mod}" has no implementation evidence.` });
6904
+ }
6905
+ }
6906
+ for (const [repoId, data] of Object.entries(repos)) {
6907
+ const d = data;
6908
+ const validations = Array.isArray(d.validations) ? d.validations : [];
6909
+ for (const v of validations) {
6910
+ if (v.status === "not-run") {
6911
+ findings.push({ id, severity: "warning", message: `Validation "${v.command}" in ${repoId} was not executed.` });
6912
+ }
6913
+ }
6914
+ const migrations = Array.isArray(d.migrations) ? d.migrations : [];
6915
+ for (const m of migrations) {
6916
+ if (m.status === "blocked") {
6917
+ findings.push({ id, severity: "warning", message: `Migration "${m.id}" (${m.environment}) in ${repoId} is blocked.` });
6918
+ }
6919
+ }
6920
+ }
6921
+ if (lifecycle === "completed") {
6922
+ const proposedExceptions = exceptions.filter((e) => e.status === "proposed");
6923
+ if (proposedExceptions.length > 0) {
6924
+ findings.push({ id, severity: "blocking", message: "Work Item is completed but has proposed (not accepted) exceptions." });
6925
+ }
6926
+ if (releaseStatus === "ready") {
6927
+ const blockedGates = releaseGates.filter((g) => g.status === "blocked" || g.status === "failed");
6928
+ if (blockedGates.length > 0) {
6929
+ findings.push({ id, severity: "blocking", message: `Release status is "ready" but ${blockedGates.length} gate(s) are blocked/failed.` });
6930
+ }
6931
+ }
6932
+ }
6933
+ for (const [repoId, data] of Object.entries(repos)) {
6934
+ const d = data;
6935
+ const validations = Array.isArray(d.validations) ? d.validations : [];
6936
+ for (const v of validations) {
6937
+ if (v.status === "passed" && (!v.command || !v.command.trim())) {
6938
+ findings.push({ id, severity: "blocking", message: `Evidence in ${repoId} marked "passed" without a command record.` });
6939
+ }
6940
+ }
6941
+ }
6942
+ if (raw.status === "done") {
6943
+ findings.push({ id, severity: "warning", message: "Legacy status `done` detected. Canonical status is `completed`." });
6944
+ }
6945
+ if (raw.refined_by && raw.implemented_by && raw.refined_by === raw.implemented_by) {
6946
+ findings.push({ id, severity: "fyi", message: "refined_by and implemented_by point to the same agent." });
6947
+ }
6948
+ return {
6949
+ id,
6950
+ lifecycle,
6951
+ implementationStatus: implementationStatus || "not-started",
6952
+ validationStatus: validationStatus || "not-started",
6953
+ releaseStatus: releaseStatus || "not-assessed",
6954
+ affectedModules,
6955
+ findings,
6956
+ releaseGates,
6957
+ completionExceptions: exceptions,
6958
+ repoEvidence
6959
+ };
6960
+ }
6961
+
6839
6962
  // src/commands/guard.ts
6840
6963
  import path4 from "path";
6841
6964
  import { parse as parseYaml7 } from "yaml";
@@ -7466,6 +7589,37 @@ async function runGuard(opts = {}) {
7466
7589
  }
7467
7590
  console.log("");
7468
7591
  }
7592
+ const registeredModuleIds = loadMappedModules(dir).map((m) => m.id);
7593
+ const modifiedRepoIds = workspaceScan ? [...new Set(workspaceScan.changedFiles.map((c) => c.repoId))] : [];
7594
+ const wiArtifacts = artifacts.filter((a) => a.isWorkItem && a.affectedModules.length > 0);
7595
+ if (wiArtifacts.length > 0) {
7596
+ const evidenceSummaries = [];
7597
+ for (const wi of wiArtifacts) {
7598
+ const summary = analyzeCrossRepoEvidence({
7599
+ id: wi.id || wi.title,
7600
+ lifecycle: wi.lifecycle ?? "ready",
7601
+ implementationStatus: wi.implementationStatus,
7602
+ validationStatus: wi.validationStatus,
7603
+ releaseStatus: wi.releaseStatus,
7604
+ affectedModules: wi.affectedModules,
7605
+ rawFrontmatter: wi.rawFrontmatter,
7606
+ registeredModuleIds,
7607
+ modifiedRepoIds
7608
+ });
7609
+ if (summary.findings.length > 0) evidenceSummaries.push(summary);
7610
+ }
7611
+ if (evidenceSummaries.length > 0) {
7612
+ console.log("Cross-repo implementation evidence:");
7613
+ for (const s of evidenceSummaries) {
7614
+ console.log(` ${s.id}:`);
7615
+ for (const f of s.findings) {
7616
+ const icon = f.severity === "blocking" ? "\u2717" : f.severity === "warning" ? "!" : "\xB7";
7617
+ console.log(` ${icon} ${f.message}`);
7618
+ }
7619
+ }
7620
+ console.log("");
7621
+ }
7622
+ }
7469
7623
  const ownerMap = loadOwners(dir);
7470
7624
  const matchedDomains = collectMatchedDomains(activeMatches.map((m) => m.artifact.domains));
7471
7625
  const affectedOwners = resolveAffectedOwners(matchedDomains, ownerMap);
@@ -10089,6 +10243,7 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
10089
10243
  const selectedWIs = workItems.filter((a) => a.lifecycle && includedSet.has(a.lifecycle));
10090
10244
  for (const wi of selectedWIs) {
10091
10245
  const id = wi.id || wi.title;
10246
+ if (!id || !id.trim()) continue;
10092
10247
  const wiNodeId = `wi:${id}`;
10093
10248
  addNode({
10094
10249
  id: wiNodeId,
@@ -10099,16 +10254,19 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
10099
10254
  knowledge_level: wi.knowledgeLevel || void 0
10100
10255
  });
10101
10256
  for (const glob of wi.codeGlobs) {
10257
+ if (!glob || !glob.trim()) continue;
10102
10258
  const codeId = `code:${glob}`;
10103
10259
  addNode({ id: codeId, type: "code-glob", label: glob });
10104
10260
  addEdge(wiNodeId, codeId, "owns");
10105
10261
  }
10106
10262
  for (const cap of wi.capabilities) {
10263
+ if (!cap || !cap.trim()) continue;
10107
10264
  const capId = `capability:${slug(cap) || cap}`;
10108
10265
  addNode({ id: capId, type: "capability", label: cap });
10109
10266
  addEdge(wiNodeId, capId, "implements");
10110
10267
  }
10111
10268
  for (const dec of wi.decisions) {
10269
+ if (!dec || !dec.trim()) continue;
10112
10270
  const adrId = `adr:${dec}`;
10113
10271
  addNode({ id: adrId, type: "decision", label: dec });
10114
10272
  addEdge(wiNodeId, adrId, "depends_on");
@@ -10125,16 +10283,19 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
10125
10283
  }
10126
10284
  }
10127
10285
  for (const adr of all.filter(isAdr)) {
10128
- const adrId = `adr:${adr.id || adr.title}`;
10286
+ const adrLabel = adr.id || adr.title;
10287
+ if (!adrLabel || !adrLabel.trim()) continue;
10288
+ const adrId = `adr:${adrLabel}`;
10129
10289
  const referenced = nodes.has(adrId);
10130
10290
  if (scope === "all" || referenced) {
10131
10291
  nodes.set(adrId, {
10132
10292
  id: adrId,
10133
10293
  type: "decision",
10134
- label: `${adr.id} ${adr.title}`.trim() || adr.id || adr.title,
10294
+ label: `${adr.id} ${adr.title}`.trim() || adrLabel,
10135
10295
  path: adr.relPath
10136
10296
  });
10137
10297
  for (const glob of adr.codeGlobs) {
10298
+ if (!glob || !glob.trim()) continue;
10138
10299
  const codeId = `code:${glob}`;
10139
10300
  addNode({ id: codeId, type: "code-glob", label: glob });
10140
10301
  addEdge(adrId, codeId, "governs");
@@ -10190,13 +10351,20 @@ function renderGraphMermaid(graph) {
10190
10351
  safeIds.set(id, candidate);
10191
10352
  return candidate;
10192
10353
  };
10193
- const escapeLabel = (s) => s.replace(/"/g, "'");
10354
+ const escapeLabel = (s) => s.replace(/"/g, "'").replace(/\[/g, "(").replace(/\]/g, ")").replace(/\n/g, " ");
10355
+ const validNodes = graph.nodes.filter(
10356
+ (n) => n.id && n.id.trim() !== "" && n.label && n.label.trim() !== ""
10357
+ );
10358
+ const validNodeIds = new Set(validNodes.map((n) => n.id));
10194
10359
  const lines = ["flowchart LR"];
10195
- for (const node of graph.nodes) {
10360
+ for (const node of validNodes) {
10196
10361
  lines.push(` ${safe(node.id)}["${escapeLabel(node.label)}"]`);
10197
10362
  }
10198
- if (graph.edges.length > 0) lines.push("");
10199
- for (const edge of graph.edges) {
10363
+ const validEdges = graph.edges.filter(
10364
+ (e) => validNodeIds.has(e.from) && validNodeIds.has(e.to)
10365
+ );
10366
+ if (validEdges.length > 0) lines.push("");
10367
+ for (const edge of validEdges) {
10200
10368
  lines.push(` ${safe(edge.from)} -->|${edge.type}| ${safe(edge.to)}`);
10201
10369
  }
10202
10370
  return lines.join("\n") + "\n";
@@ -13041,7 +13209,30 @@ function buildProjectExplanation(dir) {
13041
13209
  projectRoute: buildProjectRoute(dir),
13042
13210
  scanSignals: loadScanSignals(dir),
13043
13211
  metadataHealth: analyzeMetadataHealth(dir),
13044
- isModuleRepo: config != null && isModule(config)
13212
+ isModuleRepo: config != null && isModule(config),
13213
+ implementationEvidence: workItemArtifacts.filter((a) => a.affectedModules.length > 0).map((a) => {
13214
+ const fm2 = a.rawFrontmatter;
13215
+ const evidence = fm2.implementation_evidence;
13216
+ const repos = evidence?.repositories ?? {};
13217
+ const repoEvidence = {};
13218
+ for (const [repoId, data] of Object.entries(repos)) {
13219
+ repoEvidence[repoId] = {
13220
+ role: String(data.role ?? (repoId === "core" ? "core" : "module")),
13221
+ status: String(data.status ?? "unknown")
13222
+ };
13223
+ }
13224
+ return {
13225
+ id: a.id || a.title,
13226
+ lifecycle: lifecycleStateOf({ status: a.status, filePath: a.filePath }),
13227
+ implementationStatus: a.implementationStatus || "not-started",
13228
+ validationStatus: a.validationStatus || "not-started",
13229
+ releaseStatus: a.releaseStatus || "not-assessed",
13230
+ affectedModules: a.affectedModules,
13231
+ releaseGates: Array.isArray(fm2.release_gates) ? fm2.release_gates : [],
13232
+ completionExceptions: Array.isArray(fm2.completion_exceptions) ? fm2.completion_exceptions : [],
13233
+ repoEvidence
13234
+ };
13235
+ })
13045
13236
  };
13046
13237
  }
13047
13238
  function stateLabel(state) {
@@ -13167,6 +13358,36 @@ function renderExplanationHuman(exp) {
13167
13358
  }
13168
13359
  lines.push("");
13169
13360
  }
13361
+ if (exp.implementationEvidence.length > 0) {
13362
+ lines.push("## Implementation Evidence");
13363
+ for (const ev of exp.implementationEvidence) {
13364
+ lines.push("");
13365
+ lines.push(`### ${ev.id}`);
13366
+ lines.push(`- Lifecycle: ${ev.lifecycle.charAt(0).toUpperCase() + ev.lifecycle.slice(1)}`);
13367
+ lines.push(`- Implementation: ${ev.implementationStatus}`);
13368
+ lines.push(`- Validation: ${ev.validationStatus}`);
13369
+ lines.push(`- Release: ${ev.releaseStatus}`);
13370
+ if (Object.keys(ev.repoEvidence).length > 0) {
13371
+ lines.push("Repositories:");
13372
+ for (const [repoId, info] of Object.entries(ev.repoEvidence)) {
13373
+ lines.push(`- ${repoId} \u2014 ${info.status}`);
13374
+ }
13375
+ } else if (ev.affectedModules.length > 0) {
13376
+ lines.push(`Affected modules: ${ev.affectedModules.join(", ")}`);
13377
+ }
13378
+ const pendingGates = ev.releaseGates.filter((g) => g.status === "blocked" || g.status === "pending");
13379
+ if (pendingGates.length > 0) {
13380
+ lines.push("Pending release gates:");
13381
+ for (const g of pendingGates) lines.push(`- ${g.id}${g.reason ? ` \u2014 ${g.reason}` : ""}`);
13382
+ }
13383
+ const acceptedExceptions = ev.completionExceptions.filter((e) => e.status === "accepted" || e.status === "deferred");
13384
+ if (acceptedExceptions.length > 0) {
13385
+ lines.push("Completion exceptions:");
13386
+ for (const e of acceptedExceptions) lines.push(`- ${e.id}: ${e.status}${e.reason ? ` \u2014 ${e.reason}` : ""}`);
13387
+ }
13388
+ }
13389
+ lines.push("");
13390
+ }
13170
13391
  if (exp.domains.length > 0) {
13171
13392
  lines.push("## Domains");
13172
13393
  lines.push(`- ${exp.domains.join(", ")}`);
@@ -13624,7 +13845,7 @@ var OPERATING_RULES = [
13624
13845
  "Kaddo itself never calls an LLM and never runs git \u2014 every git action is the human\u2019s."
13625
13846
  ];
13626
13847
  function toContextWorkItem(a) {
13627
- return {
13848
+ const wi = {
13628
13849
  id: a.id,
13629
13850
  type: a.type,
13630
13851
  title: a.title,
@@ -13634,6 +13855,11 @@ function toContextWorkItem(a) {
13634
13855
  domains: a.domains,
13635
13856
  source: parseWorkItemSource(a.rawFrontmatter)
13636
13857
  };
13858
+ if (a.implementationStatus) wi.implementationStatus = a.implementationStatus;
13859
+ if (a.validationStatus) wi.validationStatus = a.validationStatus;
13860
+ if (a.releaseStatus) wi.releaseStatus = a.releaseStatus;
13861
+ if (a.affectedModules.length > 0) wi.affectedModules = a.affectedModules;
13862
+ return wi;
13637
13863
  }
13638
13864
  function toContextArtifact(a) {
13639
13865
  return { id: a.id, type: a.type, title: a.title, summary: a.summary, codeGlobs: a.codeGlobs };
@@ -15142,7 +15368,7 @@ function findWorkItemFile(dir, id) {
15142
15368
  function updateWorkItemFile(filePath, learning) {
15143
15369
  const raw = readFile(filePath);
15144
15370
  const { data, content } = matter8(raw);
15145
- data.status = "done";
15371
+ data.status = "completed";
15146
15372
  data.completed_at = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
15147
15373
  let updatedContent = content;
15148
15374
  if (content.includes("_What did we learn from this change? Update after completion._")) {
@@ -15177,24 +15403,24 @@ async function runLearn(artifactId) {
15177
15403
  }
15178
15404
  intro2("kaddo learn");
15179
15405
  const artifacts = readArtifacts(join(dir, ARCH_DIR9));
15180
- const inProgress = artifacts.filter(
15181
- (a) => a.status === "in-progress" && a.type !== "current-state" && a.type !== "roadmap"
15406
+ const closable = artifacts.filter(
15407
+ (a) => (a.status === "in-progress" || a.status === "completed" || a.status === "done") && a.type !== "current-state" && a.type !== "roadmap"
15182
15408
  );
15183
- if (inProgress.length === 0) {
15184
- log2.warn("No in-progress work items found.");
15409
+ if (closable.length === 0) {
15410
+ log2.warn("No in-progress or completed work items found.");
15185
15411
  outro2("Nothing to close.");
15186
15412
  return;
15187
15413
  }
15188
15414
  let targetId;
15189
15415
  if (artifactId) {
15190
15416
  targetId = artifactId;
15191
- } else if (inProgress.length === 1) {
15192
- targetId = inProgress[0].id || inProgress[0].title;
15193
- log2.info(`Closing: ${targetId} \u2014 ${inProgress[0].summary || inProgress[0].title}`);
15417
+ } else if (closable.length === 1) {
15418
+ targetId = closable[0].id || closable[0].title;
15419
+ log2.info(`Closing: ${targetId} \u2014 ${closable[0].summary || closable[0].title}`);
15194
15420
  } else {
15195
15421
  const chosen = await select2({
15196
15422
  message: "Which work item are you closing?",
15197
- options: inProgress.map((a) => ({
15423
+ options: closable.map((a) => ({
15198
15424
  value: a.id || a.title,
15199
15425
  label: `${a.id || a.title} \u2014 ${a.summary || a.title}`
15200
15426
  }))
@@ -15211,9 +15437,26 @@ async function runLearn(artifactId) {
15211
15437
  placeholder: "e.g. The retry logic needed a separate queue to avoid blocking the main flow",
15212
15438
  validate: (v) => v.trim().length === 0 ? "Learning is required." : void 0
15213
15439
  });
15214
- updateWorkItemFile(filePath, learning.trim());
15215
- log2.success(`${targetId} marked as done`);
15440
+ const wiRaw = readFile(filePath);
15441
+ const wiData = matter8(wiRaw).data;
15442
+ const hasExceptions = wiData.validation_status === "accepted-with-exceptions" || Array.isArray(wiData.completion_exceptions) && wiData.completion_exceptions.length > 0;
15443
+ const releaseBlocked = wiData.release_status === "blocked";
15444
+ let enrichedLearning = learning.trim();
15445
+ if (hasExceptions || releaseBlocked) {
15446
+ const notes = [];
15447
+ if (hasExceptions) notes.push("Validation exceptions were accepted for this Work Item.");
15448
+ if (releaseBlocked) {
15449
+ const gates = Array.isArray(wiData.release_gates) ? wiData.release_gates.filter((g) => g.status === "blocked" || g.status === "pending").map((g) => g.id) : [];
15450
+ notes.push(gates.length > 0 ? `Release gates remain: ${gates.join(", ")}.` : "Production release remains blocked.");
15451
+ }
15452
+ enrichedLearning += "\n\n> " + notes.join(" ");
15453
+ }
15454
+ updateWorkItemFile(filePath, enrichedLearning);
15455
+ log2.success(`${targetId} marked as completed`);
15216
15456
  log2.success(`Learning recorded in ${filePath.replace(dir + "/", "")}`);
15457
+ if (hasExceptions) {
15458
+ log2.warn("Learning captured from a Work Item completed with validation exceptions.");
15459
+ }
15217
15460
  log2.info("Consider updating knowledge/knowledge.md if this changes the current state.");
15218
15461
  outro2("Work item closed.");
15219
15462
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/cli",
3
- "version": "3.65.0",
3
+ "version": "3.66.0",
4
4
  "description": "Knowledge Driven Development toolkit",
5
5
  "license": "MIT",
6
6
  "repository": {