@kaddo/cli 3.65.0 → 3.67.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 +372 -25
  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,135 @@ 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 (validationStatus === "accepted-with-exceptions") {
6943
+ const hasExceptions = exceptions.length > 0;
6944
+ const hasValidationGate = releaseGates.some(
6945
+ (g) => (g.status === "pending" || g.status === "blocked") && (g.id.includes("validation") || g.id.includes("test"))
6946
+ );
6947
+ if (!hasExceptions && !hasValidationGate) {
6948
+ findings.push({ id, severity: "warning", message: "Validation status is accepted-with-exceptions, but no exception evidence was found." });
6949
+ }
6950
+ }
6951
+ if (raw.status === "done") {
6952
+ findings.push({ id, severity: "warning", message: "Legacy status `done` detected. Canonical status is `completed`." });
6953
+ }
6954
+ if (raw.refined_by && raw.implemented_by && raw.refined_by === raw.implemented_by) {
6955
+ findings.push({ id, severity: "fyi", message: "refined_by and implemented_by point to the same agent." });
6956
+ }
6957
+ const historicalDefault = lifecycle === "completed" || lifecycle === "archived" ? "not-assessed" : "not-started";
6958
+ return {
6959
+ id,
6960
+ lifecycle,
6961
+ implementationStatus: implementationStatus || historicalDefault,
6962
+ validationStatus: validationStatus || historicalDefault,
6963
+ releaseStatus: releaseStatus || "not-assessed",
6964
+ affectedModules,
6965
+ findings,
6966
+ releaseGates,
6967
+ completionExceptions: exceptions,
6968
+ repoEvidence
6969
+ };
6970
+ }
6971
+
6839
6972
  // src/commands/guard.ts
6840
6973
  import path4 from "path";
6841
6974
  import { parse as parseYaml7 } from "yaml";
@@ -7466,6 +7599,37 @@ async function runGuard(opts = {}) {
7466
7599
  }
7467
7600
  console.log("");
7468
7601
  }
7602
+ const registeredModuleIds = loadMappedModules(dir).map((m) => m.id);
7603
+ const modifiedRepoIds = workspaceScan ? [...new Set(workspaceScan.changedFiles.map((c) => c.repoId))] : [];
7604
+ const wiArtifacts = artifacts.filter((a) => a.isWorkItem && a.affectedModules.length > 0);
7605
+ if (wiArtifacts.length > 0) {
7606
+ const evidenceSummaries = [];
7607
+ for (const wi of wiArtifacts) {
7608
+ const summary = analyzeCrossRepoEvidence({
7609
+ id: wi.id || wi.title,
7610
+ lifecycle: wi.lifecycle ?? "ready",
7611
+ implementationStatus: wi.implementationStatus,
7612
+ validationStatus: wi.validationStatus,
7613
+ releaseStatus: wi.releaseStatus,
7614
+ affectedModules: wi.affectedModules,
7615
+ rawFrontmatter: wi.rawFrontmatter,
7616
+ registeredModuleIds,
7617
+ modifiedRepoIds
7618
+ });
7619
+ if (summary.findings.length > 0) evidenceSummaries.push(summary);
7620
+ }
7621
+ if (evidenceSummaries.length > 0) {
7622
+ console.log("Cross-repo implementation evidence:");
7623
+ for (const s of evidenceSummaries) {
7624
+ console.log(` ${s.id}:`);
7625
+ for (const f of s.findings) {
7626
+ const icon = f.severity === "blocking" ? "\u2717" : f.severity === "warning" ? "!" : "\xB7";
7627
+ console.log(` ${icon} ${f.message}`);
7628
+ }
7629
+ }
7630
+ console.log("");
7631
+ }
7632
+ }
7469
7633
  const ownerMap = loadOwners(dir);
7470
7634
  const matchedDomains = collectMatchedDomains(activeMatches.map((m) => m.artifact.domains));
7471
7635
  const affectedOwners = resolveAffectedOwners(matchedDomains, ownerMap);
@@ -10089,6 +10253,7 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
10089
10253
  const selectedWIs = workItems.filter((a) => a.lifecycle && includedSet.has(a.lifecycle));
10090
10254
  for (const wi of selectedWIs) {
10091
10255
  const id = wi.id || wi.title;
10256
+ if (!id || !id.trim()) continue;
10092
10257
  const wiNodeId = `wi:${id}`;
10093
10258
  addNode({
10094
10259
  id: wiNodeId,
@@ -10099,16 +10264,19 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
10099
10264
  knowledge_level: wi.knowledgeLevel || void 0
10100
10265
  });
10101
10266
  for (const glob of wi.codeGlobs) {
10267
+ if (!glob || !glob.trim()) continue;
10102
10268
  const codeId = `code:${glob}`;
10103
10269
  addNode({ id: codeId, type: "code-glob", label: glob });
10104
10270
  addEdge(wiNodeId, codeId, "owns");
10105
10271
  }
10106
10272
  for (const cap of wi.capabilities) {
10273
+ if (!cap || !cap.trim()) continue;
10107
10274
  const capId = `capability:${slug(cap) || cap}`;
10108
10275
  addNode({ id: capId, type: "capability", label: cap });
10109
10276
  addEdge(wiNodeId, capId, "implements");
10110
10277
  }
10111
10278
  for (const dec of wi.decisions) {
10279
+ if (!dec || !dec.trim()) continue;
10112
10280
  const adrId = `adr:${dec}`;
10113
10281
  addNode({ id: adrId, type: "decision", label: dec });
10114
10282
  addEdge(wiNodeId, adrId, "depends_on");
@@ -10125,16 +10293,19 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
10125
10293
  }
10126
10294
  }
10127
10295
  for (const adr of all.filter(isAdr)) {
10128
- const adrId = `adr:${adr.id || adr.title}`;
10296
+ const adrLabel = adr.id || adr.title;
10297
+ if (!adrLabel || !adrLabel.trim()) continue;
10298
+ const adrId = `adr:${adrLabel}`;
10129
10299
  const referenced = nodes.has(adrId);
10130
10300
  if (scope === "all" || referenced) {
10131
10301
  nodes.set(adrId, {
10132
10302
  id: adrId,
10133
10303
  type: "decision",
10134
- label: `${adr.id} ${adr.title}`.trim() || adr.id || adr.title,
10304
+ label: `${adr.id} ${adr.title}`.trim() || adrLabel,
10135
10305
  path: adr.relPath
10136
10306
  });
10137
10307
  for (const glob of adr.codeGlobs) {
10308
+ if (!glob || !glob.trim()) continue;
10138
10309
  const codeId = `code:${glob}`;
10139
10310
  addNode({ id: codeId, type: "code-glob", label: glob });
10140
10311
  addEdge(adrId, codeId, "governs");
@@ -10190,13 +10361,20 @@ function renderGraphMermaid(graph) {
10190
10361
  safeIds.set(id, candidate);
10191
10362
  return candidate;
10192
10363
  };
10193
- const escapeLabel = (s) => s.replace(/"/g, "'");
10364
+ const escapeLabel = (s) => s.replace(/"/g, "'").replace(/\[/g, "(").replace(/\]/g, ")").replace(/\n/g, " ");
10365
+ const validNodes = graph.nodes.filter(
10366
+ (n) => n.id && n.id.trim() !== "" && n.label && n.label.trim() !== ""
10367
+ );
10368
+ const validNodeIds = new Set(validNodes.map((n) => n.id));
10194
10369
  const lines = ["flowchart LR"];
10195
- for (const node of graph.nodes) {
10370
+ for (const node of validNodes) {
10196
10371
  lines.push(` ${safe(node.id)}["${escapeLabel(node.label)}"]`);
10197
10372
  }
10198
- if (graph.edges.length > 0) lines.push("");
10199
- for (const edge of graph.edges) {
10373
+ const validEdges = graph.edges.filter(
10374
+ (e) => validNodeIds.has(e.from) && validNodeIds.has(e.to)
10375
+ );
10376
+ if (validEdges.length > 0) lines.push("");
10377
+ for (const edge of validEdges) {
10200
10378
  lines.push(` ${safe(edge.from)} -->|${edge.type}| ${safe(edge.to)}`);
10201
10379
  }
10202
10380
  return lines.join("\n") + "\n";
@@ -11573,6 +11751,8 @@ function workItemsSignal(dir) {
11573
11751
  if (wis.length === 0) return "none";
11574
11752
  if (wis.some((w) => w.lifecycle === "in-progress")) return "in-progress";
11575
11753
  if (wis.some((w) => w.lifecycle === "ready")) return "ready";
11754
+ const hasActive = wis.some((w) => w.lifecycle === "draft" || w.lifecycle === "blocked");
11755
+ if (!hasActive && wis.some((w) => w.lifecycle === "completed" || w.lifecycle === "archived")) return "completed-only";
11576
11756
  return "none-ready";
11577
11757
  }
11578
11758
  function installedAdapters(dir) {
@@ -12001,6 +12181,8 @@ function buildReadinessReport(dir, now = /* @__PURE__ */ new Date()) {
12001
12181
  else if (understand === "missing") overall = "scanned";
12002
12182
  else if (firstWeak) overall = "knowledge-incomplete";
12003
12183
  else if (oq.summary.blocking_open > 0) overall = "needs-decisions";
12184
+ else if (work_items === "in-progress") overall = "active-implementation";
12185
+ else if (work_items === "completed-only") overall = deriveDeliveryCompletedStatus(dir);
12004
12186
  else if (roadmap !== "has-candidates") overall = "ready-for-roadmap";
12005
12187
  else if (work_items === "none" || work_items === "none-ready") overall = "ready-for-work-item";
12006
12188
  else overall = "ready-for-implementation";
@@ -12013,6 +12195,16 @@ function buildReadinessReport(dir, now = /* @__PURE__ */ new Date()) {
12013
12195
  nextStepRecommendation: rec
12014
12196
  };
12015
12197
  }
12198
+ function deriveDeliveryCompletedStatus(dir) {
12199
+ const wis = discoverWorkItems(dir);
12200
+ const completed = wis.filter((w) => lifecycleStateOf({ status: w.status, filePath: w.filePath }) === "completed");
12201
+ if (completed.length === 0) return "delivery-completed";
12202
+ const hasReleaseBlocked = completed.some((w) => w.releaseStatus === "blocked");
12203
+ const hasReleaseReady = completed.some((w) => w.releaseStatus === "ready" || w.releaseStatus === "released");
12204
+ if (hasReleaseBlocked) return "delivery-completed-release-blocked";
12205
+ if (hasReleaseReady) return "delivery-completed-release-ready";
12206
+ return "delivery-completed";
12207
+ }
12016
12208
 
12017
12209
  // src/core/assets.ts
12018
12210
  import matter5 from "gray-matter";
@@ -12376,7 +12568,7 @@ var refineWorkItem = {
12376
12568
  id: "refine-work-item",
12377
12569
  label: "Refine Work Item",
12378
12570
  evaluate: (ctx) => {
12379
- if (ctx.readyWorkItems > 0 || ctx.inProgressWorkItems > 0) return { status: "done" };
12571
+ if (ctx.readyWorkItems > 0 || ctx.inProgressWorkItems > 0 || ctx.blockedWorkItems > 0 || ctx.completedWorkItems > 0 || ctx.archivedWorkItems > 0) return { status: "done" };
12380
12572
  if (ctx.draftWorkItems > 0) {
12381
12573
  return {
12382
12574
  status: "current",
@@ -12627,7 +12819,9 @@ function buildRouteContext(dir) {
12627
12819
  draftWorkItems: byState("draft"),
12628
12820
  readyWorkItems: byState("ready"),
12629
12821
  inProgressWorkItems: byState("in-progress"),
12822
+ blockedWorkItems: byState("blocked"),
12630
12823
  completedWorkItems: byState("completed"),
12824
+ archivedWorkItems: byState("archived"),
12631
12825
  ownershipCoverage: `${withOwnership}/${total}`,
12632
12826
  ownershipComplete: total > 0 && withOwnership >= total,
12633
12827
  decisionCandidates: td.candidates,
@@ -13041,7 +13235,46 @@ function buildProjectExplanation(dir) {
13041
13235
  projectRoute: buildProjectRoute(dir),
13042
13236
  scanSignals: loadScanSignals(dir),
13043
13237
  metadataHealth: analyzeMetadataHealth(dir),
13044
- isModuleRepo: config != null && isModule(config)
13238
+ isModuleRepo: config != null && isModule(config),
13239
+ deliverySummary: (() => {
13240
+ const completed = workItemArtifacts.filter((a) => lifecycleStateOf({ status: a.status, filePath: a.filePath }) === "completed");
13241
+ const archived = workItemArtifacts.filter((a) => lifecycleStateOf({ status: a.status, filePath: a.filePath }) === "archived");
13242
+ const active = workItemArtifacts.filter((a) => isActiveState(lifecycleStateOf({ status: a.status, filePath: a.filePath })));
13243
+ if (completed.length === 0 && archived.length === 0) return null;
13244
+ return {
13245
+ completedWorkItems: completed.length,
13246
+ archivedWorkItems: archived.length,
13247
+ activeWorkItems: active.length,
13248
+ implementationCompleted: completed.filter((a) => a.implementationStatus === "completed").length,
13249
+ releaseBlocked: completed.filter((a) => a.releaseStatus === "blocked").length,
13250
+ releaseReady: completed.filter((a) => a.releaseStatus === "ready" || a.releaseStatus === "released").length
13251
+ };
13252
+ })(),
13253
+ implementationEvidence: workItemArtifacts.filter((a) => a.affectedModules.length > 0).map((a) => {
13254
+ const fm2 = a.rawFrontmatter;
13255
+ const evidence = fm2.implementation_evidence;
13256
+ const repos = evidence?.repositories ?? {};
13257
+ const repoEvidence = {};
13258
+ for (const [repoId, data] of Object.entries(repos)) {
13259
+ repoEvidence[repoId] = {
13260
+ role: String(data.role ?? (repoId === "core" ? "core" : "module")),
13261
+ status: String(data.status ?? "unknown")
13262
+ };
13263
+ }
13264
+ const lc = lifecycleStateOf({ status: a.status, filePath: a.filePath });
13265
+ const historicalDefault = lc === "completed" || lc === "archived" ? "not-assessed" : "not-started";
13266
+ return {
13267
+ id: a.id || a.title,
13268
+ lifecycle: lc,
13269
+ implementationStatus: a.implementationStatus || historicalDefault,
13270
+ validationStatus: a.validationStatus || historicalDefault,
13271
+ releaseStatus: a.releaseStatus || "not-assessed",
13272
+ affectedModules: a.affectedModules,
13273
+ releaseGates: Array.isArray(fm2.release_gates) ? fm2.release_gates : [],
13274
+ completionExceptions: Array.isArray(fm2.completion_exceptions) ? fm2.completion_exceptions : [],
13275
+ repoEvidence
13276
+ };
13277
+ })
13045
13278
  };
13046
13279
  }
13047
13280
  function stateLabel(state) {
@@ -13167,6 +13400,36 @@ function renderExplanationHuman(exp) {
13167
13400
  }
13168
13401
  lines.push("");
13169
13402
  }
13403
+ if (exp.implementationEvidence.length > 0) {
13404
+ lines.push("## Implementation Evidence");
13405
+ for (const ev of exp.implementationEvidence) {
13406
+ lines.push("");
13407
+ lines.push(`### ${ev.id}`);
13408
+ lines.push(`- Lifecycle: ${ev.lifecycle.charAt(0).toUpperCase() + ev.lifecycle.slice(1)}`);
13409
+ lines.push(`- Implementation: ${ev.implementationStatus}`);
13410
+ lines.push(`- Validation: ${ev.validationStatus}`);
13411
+ lines.push(`- Release: ${ev.releaseStatus}`);
13412
+ if (Object.keys(ev.repoEvidence).length > 0) {
13413
+ lines.push("Repositories:");
13414
+ for (const [repoId, info] of Object.entries(ev.repoEvidence)) {
13415
+ lines.push(`- ${repoId} \u2014 ${info.status}`);
13416
+ }
13417
+ } else if (ev.affectedModules.length > 0) {
13418
+ lines.push(`Affected modules: ${ev.affectedModules.join(", ")}`);
13419
+ }
13420
+ const pendingGates = ev.releaseGates.filter((g) => g.status === "blocked" || g.status === "pending");
13421
+ if (pendingGates.length > 0) {
13422
+ lines.push("Pending release gates:");
13423
+ for (const g of pendingGates) lines.push(`- ${g.id}${g.reason ? ` \u2014 ${g.reason}` : ""}`);
13424
+ }
13425
+ const acceptedExceptions = ev.completionExceptions.filter((e) => e.status === "accepted" || e.status === "deferred");
13426
+ if (acceptedExceptions.length > 0) {
13427
+ lines.push("Completion exceptions:");
13428
+ for (const e of acceptedExceptions) lines.push(`- ${e.id}: ${e.status}${e.reason ? ` \u2014 ${e.reason}` : ""}`);
13429
+ }
13430
+ }
13431
+ lines.push("");
13432
+ }
13170
13433
  if (exp.domains.length > 0) {
13171
13434
  lines.push("## Domains");
13172
13435
  lines.push(`- ${exp.domains.join(", ")}`);
@@ -13249,6 +13512,17 @@ function renderExplanationHuman(exp) {
13249
13512
  }
13250
13513
  lines.push(`- Next step: ${exp.readiness.recommended_next_step.label}`);
13251
13514
  lines.push("");
13515
+ if (exp.deliverySummary) {
13516
+ const ds = exp.deliverySummary;
13517
+ lines.push("## Delivery Summary");
13518
+ lines.push(`- Completed Work Items: ${ds.completedWorkItems}`);
13519
+ if (ds.archivedWorkItems > 0) lines.push(`- Archived Work Items: ${ds.archivedWorkItems}`);
13520
+ lines.push(`- Active Work Items: ${ds.activeWorkItems}`);
13521
+ if (ds.implementationCompleted > 0) lines.push(`- Implementation completed: ${ds.implementationCompleted}`);
13522
+ if (ds.releaseBlocked > 0) lines.push(`- Release blocked: ${ds.releaseBlocked}`);
13523
+ if (ds.releaseReady > 0) lines.push(`- Release ready: ${ds.releaseReady}`);
13524
+ lines.push("");
13525
+ }
13252
13526
  const rec = exp.nextStepRecommendation;
13253
13527
  const secondary = rec.secondary ?? [];
13254
13528
  const steps = secondary.length > 0 || /Delivery|Active|Maintenance/.test(rec.phase) ? [rec.label, ...secondary.map((s2) => s2.label)] : exp.suggestedNextSteps;
@@ -13283,6 +13557,10 @@ function renderExplanationHuman(exp) {
13283
13557
  if (!exp.isModuleRepo) {
13284
13558
  lines.push(`- roadmap: ${s.roadmap}`);
13285
13559
  lines.push(`- work-items: ${s.work_items}`);
13560
+ if (exp.deliverySummary) {
13561
+ lines.push(`- work-items completed: ${exp.deliverySummary.completedWorkItems}`);
13562
+ lines.push(`- work-items active: ${exp.deliverySummary.activeWorkItems}`);
13563
+ }
13286
13564
  lines.push(`- adapters: ${s.adapters.length > 0 ? s.adapters.join(", ") + " installed" : "none installed"}`);
13287
13565
  } else {
13288
13566
  lines.push("- roadmap: managed-by-core");
@@ -13624,16 +13902,23 @@ var OPERATING_RULES = [
13624
13902
  "Kaddo itself never calls an LLM and never runs git \u2014 every git action is the human\u2019s."
13625
13903
  ];
13626
13904
  function toContextWorkItem(a) {
13627
- return {
13905
+ const lifecycle = lifecycleStateOf({ status: a.status, filePath: a.filePath });
13906
+ const isHistorical = lifecycle === "completed" || lifecycle === "archived";
13907
+ const wi = {
13628
13908
  id: a.id,
13629
13909
  type: a.type,
13630
13910
  title: a.title,
13631
13911
  status: a.status,
13632
- lifecycle: lifecycleStateOf({ status: a.status, filePath: a.filePath }),
13912
+ lifecycle,
13633
13913
  knowledgeLevel: a.knowledgeLevel,
13634
13914
  domains: a.domains,
13635
13915
  source: parseWorkItemSource(a.rawFrontmatter)
13636
13916
  };
13917
+ wi.implementationStatus = a.implementationStatus || (isHistorical ? "not-assessed" : void 0);
13918
+ wi.validationStatus = a.validationStatus || (isHistorical ? "not-assessed" : void 0);
13919
+ wi.releaseStatus = a.releaseStatus || (isHistorical ? "not-assessed" : void 0);
13920
+ if (a.affectedModules.length > 0) wi.affectedModules = a.affectedModules;
13921
+ return wi;
13637
13922
  }
13638
13923
  function toContextArtifact(a) {
13639
13924
  return { id: a.id, type: a.type, title: a.title, summary: a.summary, codeGlobs: a.codeGlobs };
@@ -13662,10 +13947,17 @@ function buildContextPack(dir, config, now = /* @__PURE__ */ new Date()) {
13662
13947
  missing.push("No roadmap baseline found.");
13663
13948
  }
13664
13949
  const allArtifacts = discoverKnowledge(dir);
13665
- const workItems = allArtifacts.filter(
13666
- (a) => isDeliveryWorkItem(a) && isActiveState(lifecycleStateOf({ status: a.status, filePath: a.filePath }))
13950
+ const allWorkItemArtifacts = allArtifacts.filter((a) => isDeliveryWorkItem(a));
13951
+ const workItems = allWorkItemArtifacts.filter(
13952
+ (a) => isActiveState(lifecycleStateOf({ status: a.status, filePath: a.filePath }))
13953
+ );
13954
+ const completedWorkItems = allWorkItemArtifacts.filter(
13955
+ (a) => lifecycleStateOf({ status: a.status, filePath: a.filePath }) === "completed"
13956
+ );
13957
+ const archivedWorkItems = allWorkItemArtifacts.filter(
13958
+ (a) => lifecycleStateOf({ status: a.status, filePath: a.filePath }) === "archived"
13667
13959
  );
13668
- if (workItems.length === 0 && !moduleRepo) {
13960
+ if (allWorkItemArtifacts.length === 0 && !moduleRepo) {
13669
13961
  missing.push("No work items found.");
13670
13962
  }
13671
13963
  const state = config.project.state;
@@ -13780,6 +14072,10 @@ function buildContextPack(dir, config, now = /* @__PURE__ */ new Date()) {
13780
14072
  roadmapSummary,
13781
14073
  inventoryAvailable,
13782
14074
  workItems: workItems.map(toContextWorkItem),
14075
+ activeWorkItems: workItems.map(toContextWorkItem),
14076
+ completedWorkItems: completedWorkItems.map(toContextWorkItem),
14077
+ archivedWorkItems: archivedWorkItems.map(toContextWorkItem),
14078
+ allWorkItems: allWorkItemArtifacts.map(toContextWorkItem),
13783
14079
  artifacts: allArtifacts.filter((a) => a.codeGlobs.length > 0).map(toContextArtifact)
13784
14080
  },
13785
14081
  layers,
@@ -13796,6 +14092,30 @@ function buildContextPack(dir, config, now = /* @__PURE__ */ new Date()) {
13796
14092
  const compact = (a) => ({ total: a.total, installed: a.total - a.missing, outdated: a.outdated, unknown_version: a.unknown_version, modified: a.modified });
13797
14093
  return { version: s.version, agents: compact(s.agents), skills: compact(s.skills) };
13798
14094
  })(),
14095
+ workItemSummary: (() => {
14096
+ const lc = (s) => allWorkItemArtifacts.filter(
14097
+ (a) => lifecycleStateOf({ status: a.status, filePath: a.filePath }) === s
14098
+ ).length;
14099
+ const draft = lc("draft");
14100
+ const ready = lc("ready");
14101
+ const inProgress = lc("in-progress");
14102
+ const blocked = lc("blocked");
14103
+ const completed = lc("completed");
14104
+ const archived = lc("archived");
14105
+ const active = draft + ready + inProgress + blocked;
14106
+ const total = allWorkItemArtifacts.length;
14107
+ let summary = "none";
14108
+ if (total === 0) summary = "none";
14109
+ else if (active === 0 && completed > 0 && archived === 0) summary = "completed-only";
14110
+ else if (active === 0 && archived > 0 && completed === 0) summary = "archived-only";
14111
+ else if (active === 0 && completed + archived > 0) summary = "historical-only";
14112
+ else if (inProgress > 0) summary = "active";
14113
+ else if (blocked > 0 && ready === 0 && inProgress === 0) summary = "blocked";
14114
+ else if (ready > 0) summary = "ready";
14115
+ else if (draft > 0 && ready === 0) summary = "draft-only";
14116
+ else summary = "mixed-active";
14117
+ return { total, active, draft, ready, in_progress: inProgress, blocked, completed, archived, summary };
14118
+ })(),
13799
14119
  deliveryMix,
13800
14120
  external: loadExternalCapsules(dir),
13801
14121
  graph: loadGraphSummary(dir),
@@ -14724,6 +15044,16 @@ function runUnderstand() {
14724
15044
  } else if (rec.agentPath) {
14725
15045
  console.log(` Agent prompt: ${rec.agentPath}`);
14726
15046
  }
15047
+ if (exp.deliverySummary) {
15048
+ const ds2 = exp.deliverySummary;
15049
+ console.log("");
15050
+ console.log("Previous delivery:");
15051
+ console.log(` - Completed Work Items: ${ds2.completedWorkItems}`);
15052
+ if (ds2.implementationCompleted > 0) console.log(` - Implementation completed: ${ds2.implementationCompleted}`);
15053
+ if (ds2.releaseBlocked > 0) console.log(` - Release blocked: ${ds2.releaseBlocked}`);
15054
+ if (ds2.releaseReady > 0) console.log(` - Release ready: ${ds2.releaseReady}`);
15055
+ console.log(` - Current active work: ${ds2.activeWorkItems > 0 ? ds2.activeWorkItems : "none"}`);
15056
+ }
14727
15057
  if (rec.skill) console.log(`Recommended skill: ${rec.skill}`);
14728
15058
  console.log(`Next step: ${rec.label}`);
14729
15059
  if (rec.reason) console.log(`Why: ${rec.reason}`);
@@ -15142,7 +15472,7 @@ function findWorkItemFile(dir, id) {
15142
15472
  function updateWorkItemFile(filePath, learning) {
15143
15473
  const raw = readFile(filePath);
15144
15474
  const { data, content } = matter8(raw);
15145
- data.status = "done";
15475
+ data.status = "completed";
15146
15476
  data.completed_at = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
15147
15477
  let updatedContent = content;
15148
15478
  if (content.includes("_What did we learn from this change? Update after completion._")) {
@@ -15177,24 +15507,24 @@ async function runLearn(artifactId) {
15177
15507
  }
15178
15508
  intro2("kaddo learn");
15179
15509
  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"
15510
+ const closable = artifacts.filter(
15511
+ (a) => (a.status === "in-progress" || a.status === "completed" || a.status === "done") && a.type !== "current-state" && a.type !== "roadmap"
15182
15512
  );
15183
- if (inProgress.length === 0) {
15184
- log2.warn("No in-progress work items found.");
15513
+ if (closable.length === 0) {
15514
+ log2.warn("No in-progress or completed work items found.");
15185
15515
  outro2("Nothing to close.");
15186
15516
  return;
15187
15517
  }
15188
15518
  let targetId;
15189
15519
  if (artifactId) {
15190
15520
  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}`);
15521
+ } else if (closable.length === 1) {
15522
+ targetId = closable[0].id || closable[0].title;
15523
+ log2.info(`Closing: ${targetId} \u2014 ${closable[0].summary || closable[0].title}`);
15194
15524
  } else {
15195
15525
  const chosen = await select2({
15196
15526
  message: "Which work item are you closing?",
15197
- options: inProgress.map((a) => ({
15527
+ options: closable.map((a) => ({
15198
15528
  value: a.id || a.title,
15199
15529
  label: `${a.id || a.title} \u2014 ${a.summary || a.title}`
15200
15530
  }))
@@ -15211,9 +15541,26 @@ async function runLearn(artifactId) {
15211
15541
  placeholder: "e.g. The retry logic needed a separate queue to avoid blocking the main flow",
15212
15542
  validate: (v) => v.trim().length === 0 ? "Learning is required." : void 0
15213
15543
  });
15214
- updateWorkItemFile(filePath, learning.trim());
15215
- log2.success(`${targetId} marked as done`);
15544
+ const wiRaw = readFile(filePath);
15545
+ const wiData = matter8(wiRaw).data;
15546
+ const hasExceptions = wiData.validation_status === "accepted-with-exceptions" || Array.isArray(wiData.completion_exceptions) && wiData.completion_exceptions.length > 0;
15547
+ const releaseBlocked = wiData.release_status === "blocked";
15548
+ let enrichedLearning = learning.trim();
15549
+ if (hasExceptions || releaseBlocked) {
15550
+ const notes = [];
15551
+ if (hasExceptions) notes.push("Validation exceptions were accepted for this Work Item.");
15552
+ if (releaseBlocked) {
15553
+ const gates = Array.isArray(wiData.release_gates) ? wiData.release_gates.filter((g) => g.status === "blocked" || g.status === "pending").map((g) => g.id) : [];
15554
+ notes.push(gates.length > 0 ? `Release gates remain: ${gates.join(", ")}.` : "Production release remains blocked.");
15555
+ }
15556
+ enrichedLearning += "\n\n> " + notes.join(" ");
15557
+ }
15558
+ updateWorkItemFile(filePath, enrichedLearning);
15559
+ log2.success(`${targetId} marked as completed`);
15216
15560
  log2.success(`Learning recorded in ${filePath.replace(dir + "/", "")}`);
15561
+ if (hasExceptions) {
15562
+ log2.warn("Learning captured from a Work Item completed with validation exceptions.");
15563
+ }
15217
15564
  log2.info("Consider updating knowledge/knowledge.md if this changes the current state.");
15218
15565
  outro2("Work item closed.");
15219
15566
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/cli",
3
- "version": "3.65.0",
3
+ "version": "3.67.0",
4
4
  "description": "Knowledge Driven Development toolkit",
5
5
  "license": "MIT",
6
6
  "repository": {