@kaddo/cli 3.56.0 → 3.58.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 +212 -34
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ import * as p from "@clack/prompts";
45
45
  var intro2 = (title) => p.intro(title);
46
46
  var outro2 = (msg) => p.outro(msg);
47
47
  var log2 = p.log;
48
+ var cancel2 = (msg) => p.cancel(msg);
48
49
  async function text2(opts) {
49
50
  const val = await p.text(opts);
50
51
  if (p.isCancel(val)) {
@@ -1145,8 +1146,8 @@ function loadConfig(dir) {
1145
1146
  const parsed = configSchema.safeParse(raw ?? {});
1146
1147
  if (!parsed.success) {
1147
1148
  const issues = parsed.error.issues.map((i) => {
1148
- const path6 = i.path.join(".");
1149
- return path6 ? ` - ${path6}: ${i.message}` : ` - ${i.message}`;
1149
+ const path7 = i.path.join(".");
1150
+ return path7 ? ` - ${path7}: ${i.message}` : ` - ${i.message}`;
1150
1151
  }).join("\n");
1151
1152
  throw new ConfigError(`Invalid .kaddo/config.yml:
1152
1153
  ${issues}`);
@@ -2231,6 +2232,8 @@ validation (how to test it), definition of done, open questions and dependencies
2231
2232
  - Do not expand scope without explicit confirmation.
2232
2233
  - Do not create mega Work Items \u2014 split when it covers multiple outcomes.
2233
2234
  - Keep acceptance criteria testable.
2235
+ - This skill refines scope and acceptance criteria but does not approve implementation readiness.
2236
+ Readiness requires human confirmation through \`kaddo ready\` or the MCP \`mark_work_item_ready\` action.
2234
2237
 
2235
2238
  ## Quality checklist
2236
2239
 
@@ -2636,9 +2639,9 @@ var RESPONSIBILITY_MATRIX = {
2636
2639
  agent: "work-item-agent",
2637
2640
  responsibleFor: ["Work Item refinement"],
2638
2641
  produces: ["knowledge/delivery/work-items/"],
2639
- canSuggest: ["implementation-agent"],
2642
+ canSuggest: ["kaddo ready", "mark_work_item_ready", "implementation-agent"],
2640
2643
  cannotSuggest: ["branches", "commits", "pull requests", "code"],
2641
- next: ["implementation-agent"]
2644
+ next: ["kaddo ready (human review)", "implementation-agent"]
2642
2645
  },
2643
2646
  "implementation-agent": {
2644
2647
  agent: "implementation-agent",
@@ -3651,7 +3654,16 @@ and \`blocked\` as active work; \`completed\` and \`archived\` are historical kn
3651
3654
 
3652
3655
  ## Handoff
3653
3656
 
3654
- When the Work Item is refined and ready to build, **hand off to the implementation-agent**.
3657
+ After refining a Work Item, **do not mark it ready automatically**. If the Work Item appears
3658
+ complete (acceptance criteria, validation, code globs, domains all present, open questions
3659
+ resolved), recommend human review and the ready transition:
3660
+
3661
+ - **CLI:** \`kaddo ready <WI-ID>\`
3662
+ - **MCP:** \`mark_work_item_ready({ id: "<WI-ID>" })\`
3663
+
3664
+ The transition to \`ready\` requires explicit human confirmation. Only after a human marks the
3665
+ Work Item as ready should it be handed off to the implementation-agent.
3666
+
3655
3667
  You do **not** suggest branches, commits or pull requests \u2014 implementation (including any Git
3656
3668
  branch suggestion) is the implementation-agent's responsibility, and only by respecting the
3657
3669
  project Git strategy. Your job ends at a clear, traceable Work Item that **states how to test it**.
@@ -6000,7 +6012,7 @@ function parseArtifact(filePath, raw) {
6000
6012
  status: String(data.status ?? ""),
6001
6013
  phase: String(data.phase ?? ""),
6002
6014
  initiative: String(data.initiative ?? data.source_initiative ?? ""),
6003
- source: data.source ? String(data.source) : "",
6015
+ source: data.source && typeof data.source === "object" ? String(data.source.type ?? "") : data.source ? String(data.source) : "",
6004
6016
  sourceId: String(data.source_id ?? ""),
6005
6017
  rawFrontmatter: data,
6006
6018
  decisions: Array.isArray(data.decisions) ? data.decisions.map(String).filter(Boolean) : [],
@@ -6192,10 +6204,10 @@ function analyzeGuard(touchedFiles, artifacts, silentWithoutOwnership) {
6192
6204
  import { parse as parseYaml4, stringify as stringifyYaml2 } from "yaml";
6193
6205
  var IGNORE_FILE = ".kaddo/ignores.yml";
6194
6206
  function loadIgnores(dir) {
6195
- const path6 = join(dir, IGNORE_FILE);
6196
- if (!exists(path6)) return [];
6207
+ const path7 = join(dir, IGNORE_FILE);
6208
+ if (!exists(path7)) return [];
6197
6209
  try {
6198
- const raw = readFile(path6);
6210
+ const raw = readFile(path7);
6199
6211
  const parsed = parseYaml4(raw);
6200
6212
  return Array.isArray(parsed) ? parsed : [];
6201
6213
  } catch {
@@ -6203,7 +6215,7 @@ function loadIgnores(dir) {
6203
6215
  }
6204
6216
  }
6205
6217
  function saveIgnore(dir, entry) {
6206
- const path6 = join(dir, IGNORE_FILE);
6218
+ const path7 = join(dir, IGNORE_FILE);
6207
6219
  const existing = loadIgnores(dir);
6208
6220
  const idx = existing.findIndex((e) => e.artifact_id === entry.artifact_id);
6209
6221
  if (idx >= 0) {
@@ -6211,17 +6223,17 @@ function saveIgnore(dir, entry) {
6211
6223
  } else {
6212
6224
  existing.push(entry);
6213
6225
  }
6214
- writeFile(path6, stringifyYaml2(existing));
6226
+ writeFile(path7, stringifyYaml2(existing));
6215
6227
  }
6216
6228
  function isIgnored(ignores, artifactId) {
6217
6229
  return ignores.find((e) => e.artifact_id === artifactId);
6218
6230
  }
6219
6231
  function removeIgnore(dir, artifactId) {
6220
- const path6 = join(dir, IGNORE_FILE);
6232
+ const path7 = join(dir, IGNORE_FILE);
6221
6233
  const existing = loadIgnores(dir);
6222
6234
  const filtered = existing.filter((e) => e.artifact_id !== artifactId);
6223
6235
  if (filtered.length === existing.length) return false;
6224
- writeFile(path6, stringifyYaml2(filtered));
6236
+ writeFile(path7, stringifyYaml2(filtered));
6225
6237
  return true;
6226
6238
  }
6227
6239
 
@@ -6243,11 +6255,11 @@ function moduleArtifactCoverage(dir, id) {
6243
6255
  };
6244
6256
  }
6245
6257
  function loadMappedModules(dir) {
6246
- const path6 = join(dir, DESCRIPTOR_PATH);
6247
- if (!exists(path6)) return [];
6258
+ const path7 = join(dir, DESCRIPTOR_PATH);
6259
+ if (!exists(path7)) return [];
6248
6260
  let parsed;
6249
6261
  try {
6250
- parsed = parseYaml5(readFile(path6));
6262
+ parsed = parseYaml5(readFile(path7));
6251
6263
  } catch {
6252
6264
  return [];
6253
6265
  }
@@ -6676,7 +6688,7 @@ function buildGuardHistory(dir) {
6676
6688
  const d = hotspotDir(t.code_path);
6677
6689
  hotspotMap.set(d, (hotspotMap.get(d) ?? 0) + 1);
6678
6690
  }
6679
- const hotspots = [...hotspotMap.entries()].map(([path6, warnings]) => ({ path: path6, warnings })).sort((a, b) => b.warnings - a.warnings);
6691
+ const hotspots = [...hotspotMap.entries()].map(([path7, warnings]) => ({ path: path7, warnings })).sort((a, b) => b.warnings - a.warnings);
6680
6692
  return {
6681
6693
  available: true,
6682
6694
  total_runs: sorted.length,
@@ -7377,7 +7389,7 @@ function sectionParagraph(md, title) {
7377
7389
  }
7378
7390
  return "";
7379
7391
  }
7380
- function parseCapsule(id, path6, md) {
7392
+ function parseCapsule(id, path7, md) {
7381
7393
  const { data } = matter3(md);
7382
7394
  const updatedAt = data.updated_at ? String(data.updated_at) : void 0;
7383
7395
  let ageDays = null;
@@ -7387,7 +7399,7 @@ function parseCapsule(id, path6, md) {
7387
7399
  }
7388
7400
  return {
7389
7401
  id,
7390
- path: path6,
7402
+ path: path7,
7391
7403
  system: data.system ? String(data.system) : id,
7392
7404
  owner: data.owner ? String(data.owner) : void 0,
7393
7405
  updatedAt,
@@ -7473,9 +7485,9 @@ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
7473
7485
  ];
7474
7486
  const presentLayers = [];
7475
7487
  for (const layer2 of layerDocs) {
7476
- const path6 = layer2.files.map((f) => `${KNOWLEDGE2}/${f}`).find((rel) => exists(join(dir, rel)));
7477
- if (path6) {
7478
- addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path6 });
7488
+ const path7 = layer2.files.map((f) => `${KNOWLEDGE2}/${f}`).find((rel) => exists(join(dir, rel)));
7489
+ if (path7) {
7490
+ addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path7 });
7479
7491
  presentLayers.push(layer2.id);
7480
7492
  }
7481
7493
  }
@@ -8903,6 +8915,7 @@ function buildDeliveryState(dir) {
8903
8915
  return {
8904
8916
  phase: "",
8905
8917
  draft_work_items: byState("draft"),
8918
+ refined_draft_work_items: wis.filter((w) => w.lifecycle === "draft" && w.rawFrontmatter.refined_by).length,
8906
8919
  ready_work_items: byState("ready"),
8907
8920
  in_progress_work_items: byState("in-progress"),
8908
8921
  blocked_work_items: byState("blocked"),
@@ -8983,10 +8996,10 @@ function resolveNextStep(dir, now = /* @__PURE__ */ new Date()) {
8983
8996
  const discovery = state === "pre-ai" || state === "legacy";
8984
8997
  const resolveAgent = (agent) => {
8985
8998
  const file = agent.endsWith(".md") ? agent : `${agent}.md`;
8986
- const path6 = agentInstallPath(file);
8987
- const installed = isFile(join(dir, path6));
8999
+ const path7 = agentInstallPath(file);
9000
+ const installed = isFile(join(dir, path7));
8988
9001
  const group = agentGroupOf(file);
8989
- return { agentPath: path6, agentInstalled: installed, installCommand: installed ? void 0 : `kaddo add agents --group ${group}` };
9002
+ return { agentPath: path7, agentInstalled: installed, installCommand: installed ? void 0 : `kaddo add agents --group ${group}` };
8990
9003
  };
8991
9004
  const refine = (id, agent, target, quality, verb = "complete") => {
8992
9005
  const ar = resolveAgent(agent);
@@ -9030,6 +9043,17 @@ function resolveNextStep(dir, now = /* @__PURE__ */ new Date()) {
9030
9043
  const secondary = buildSecondaryRecommendations(st);
9031
9044
  const roadmap = roadmapSignal(dir);
9032
9045
  if (roadmap !== "has-candidates" && st.draft_work_items > 0) {
9046
+ if (st.refined_draft_work_items > 0) {
9047
+ return {
9048
+ id: "review-work-item",
9049
+ phase: "Active Delivery",
9050
+ label: "Review the refined draft Work Item and mark it ready with `kaddo ready`.",
9051
+ command: "kaddo ready",
9052
+ mcpAction: "kaddo_mark_work_item_ready",
9053
+ reason: `There ${st.refined_draft_work_items === 1 ? "is" : "are"} ${st.refined_draft_work_items} refined draft Work Item${st.refined_draft_work_items === 1 ? "" : "s"} awaiting human review.`,
9054
+ ...secondary.length ? { secondary } : {}
9055
+ };
9056
+ }
9033
9057
  return {
9034
9058
  id: "refine-work-item",
9035
9059
  phase: "Active Delivery",
@@ -9835,7 +9859,8 @@ function mapNextStepId(id) {
9835
9859
  "refine-product": "define-product",
9836
9860
  "refine-capabilities": "discover-capabilities",
9837
9861
  "refine-current-state": "describe-architecture",
9838
- "refine-codebase": "describe-architecture"
9862
+ "refine-codebase": "describe-architecture",
9863
+ "review-work-item": "refine-work-item"
9839
9864
  };
9840
9865
  return MAP[id] ?? id;
9841
9866
  }
@@ -12760,10 +12785,10 @@ ${globs.map((g) => ` - ${g}`).join("\n")}`);
12760
12785
  import { parse as parseYaml13, stringify as stringifyYaml5 } from "yaml";
12761
12786
  var DESCRIPTOR_PATH2 = "knowledge/module.yml";
12762
12787
  function readDescriptor(dir) {
12763
- const path6 = join(dir, DESCRIPTOR_PATH2);
12764
- if (!exists(path6)) return null;
12788
+ const path7 = join(dir, DESCRIPTOR_PATH2);
12789
+ if (!exists(path7)) return null;
12765
12790
  try {
12766
- return parseYaml13(readFile(path6));
12791
+ return parseYaml13(readFile(path7));
12767
12792
  } catch {
12768
12793
  return null;
12769
12794
  }
@@ -14368,10 +14393,10 @@ function slugify4(name) {
14368
14393
  return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
14369
14394
  }
14370
14395
  function readModulesDescriptor(dir) {
14371
- const path6 = join(dir, DESCRIPTOR_PATH3);
14372
- if (!exists(path6)) return { version: 1, modules: [] };
14396
+ const path7 = join(dir, DESCRIPTOR_PATH3);
14397
+ if (!exists(path7)) return { version: 1, modules: [] };
14373
14398
  try {
14374
- const parsed = parseYaml14(readFile(path6));
14399
+ const parsed = parseYaml14(readFile(path7));
14375
14400
  return { version: parsed.version ?? 1, modules: parsed.modules ?? [] };
14376
14401
  } catch {
14377
14402
  return { version: 1, modules: [] };
@@ -16439,6 +16464,156 @@ function runAssetsUpdate(kind, opts = {}) {
16439
16464
  outro2(`${LABEL[kind]} update complete.`);
16440
16465
  }
16441
16466
 
16467
+ // src/commands/ready.ts
16468
+ import fs4 from "fs";
16469
+ import path6 from "path";
16470
+ import matter11 from "gray-matter";
16471
+ function validateReadiness(content, frontmatter) {
16472
+ const warnings = [];
16473
+ const body = content.replace(/^---[\s\S]*?---/, "").trim();
16474
+ const lower = body.toLowerCase();
16475
+ if (!frontmatter.domains || Array.isArray(frontmatter.domains) && frontmatter.domains.length === 0) {
16476
+ warnings.push({ field: "domains", message: "No domains declared." });
16477
+ }
16478
+ const code = frontmatter.code;
16479
+ if (!code || Array.isArray(code) && code.length === 0) {
16480
+ warnings.push({ field: "code", message: "No code ownership globs declared." });
16481
+ }
16482
+ const hasAC = /## acceptance criteria|## criterios de aceptación/i.test(body);
16483
+ if (!hasAC) {
16484
+ warnings.push({ field: "acceptance_criteria", message: "No acceptance criteria section found." });
16485
+ } else {
16486
+ const acSection = extractSection(body, /## acceptance criteria|## criterios de aceptación/i);
16487
+ if (acSection && acSection.trim().length === 0) {
16488
+ warnings.push({ field: "acceptance_criteria", message: "Acceptance criteria section is empty." });
16489
+ }
16490
+ }
16491
+ const hasValidation = /## validation|## validación/i.test(body);
16492
+ if (!hasValidation) {
16493
+ warnings.push({ field: "validation", message: "No validation section found." });
16494
+ }
16495
+ const hasOQ = /## open questions|## preguntas abiertas/i.test(body);
16496
+ if (hasOQ) {
16497
+ const oqSection = extractSection(body, /## open questions|## preguntas abiertas/i);
16498
+ if (oqSection && !isResolvedSection(oqSection)) {
16499
+ warnings.push({ field: "open_questions", message: "Open questions are still present." });
16500
+ }
16501
+ }
16502
+ return warnings;
16503
+ }
16504
+ function extractSection(body, headerPattern) {
16505
+ const lines = body.split(/\r?\n/);
16506
+ let capture = false;
16507
+ const result = [];
16508
+ for (const line of lines) {
16509
+ if (capture) {
16510
+ if (/^#{1,3}\s/.test(line)) break;
16511
+ result.push(line);
16512
+ } else if (headerPattern.test(line)) {
16513
+ capture = true;
16514
+ }
16515
+ }
16516
+ return capture ? result.join("\n").trim() : null;
16517
+ }
16518
+ function isResolvedSection(text3) {
16519
+ const t = text3.trim().toLowerCase();
16520
+ return t === "" || /^(none|ninguna|resolved|resueltas|deferred|diferidas)\.?$/i.test(t);
16521
+ }
16522
+ function transitionToReady(filePath) {
16523
+ const raw = readFile(filePath);
16524
+ const { data, content } = matter11(raw);
16525
+ const today2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
16526
+ data.status = "ready";
16527
+ data.ready_at = today2;
16528
+ const newContent = matter11.stringify(content, data);
16529
+ const posix = filePath.replace(/\\/g, "/");
16530
+ let newPath = filePath;
16531
+ if (posix.includes("/work-items/draft/")) {
16532
+ const readyDir = path6.dirname(filePath).replace(/[/\\]draft$/, path6.sep + "ready");
16533
+ ensureDir(readyDir);
16534
+ const fileName = path6.basename(filePath);
16535
+ newPath = path6.join(readyDir, fileName);
16536
+ }
16537
+ return { newPath, newContent };
16538
+ }
16539
+ function findWorkItem(dir, id) {
16540
+ const wis = discoverWorkItems(dir);
16541
+ return wis.find((w) => w.id.toLowerCase() === id.toLowerCase()) ?? null;
16542
+ }
16543
+ async function runReady(id, opts = {}) {
16544
+ const dir = process.cwd();
16545
+ intro2("kaddo ready " + id);
16546
+ const config = loadConfig(dir);
16547
+ if (!config) {
16548
+ log2.error("Kaddo is not initialized. Run `kaddo init` first.");
16549
+ process.exit(1);
16550
+ }
16551
+ const wi = findWorkItem(dir, id);
16552
+ if (!wi) {
16553
+ log2.error(`Work Item ${id} not found.`);
16554
+ log2.info("Run `kaddo history` or check knowledge/delivery/work-items/ for available IDs.");
16555
+ process.exit(1);
16556
+ }
16557
+ if (wi.lifecycle === "ready") {
16558
+ log2.info(`Work Item ${wi.id} is already ready.`);
16559
+ log2.info("Suggested next: kaddo context && kaddo understand");
16560
+ outro2("Nothing to do.");
16561
+ return;
16562
+ }
16563
+ if (wi.lifecycle !== "draft") {
16564
+ log2.error(`Work Item ${wi.id} is in state "${wi.lifecycle}", not "draft". Only draft Work Items can be marked as ready.`);
16565
+ process.exit(1);
16566
+ }
16567
+ const raw = readFile(wi.filePath);
16568
+ const { data } = matter11(raw);
16569
+ const source = parseWorkItemSource(data);
16570
+ const warnings = validateReadiness(raw, data);
16571
+ log2.info(`Work Item found:`);
16572
+ log2.info(` ${wi.id} \u2014 ${wi.title}`);
16573
+ log2.info("");
16574
+ log2.info(` Status: draft \u2192 ready`);
16575
+ log2.info(` Type: ${wi.type}`);
16576
+ log2.info(` Knowledge level: ${wi.knowledgeLevel || "unset"}`);
16577
+ log2.info(` Domains: ${wi.domains.length > 0 ? wi.domains.join(", ") : "none"}`);
16578
+ log2.info(` Source: ${renderSourceCompact(source)}`);
16579
+ if (wi.codeGlobs.length > 0) {
16580
+ log2.info(` Code ownership:`);
16581
+ for (const g of wi.codeGlobs) log2.info(` - ${g}`);
16582
+ } else {
16583
+ log2.info(` Code ownership: none`);
16584
+ }
16585
+ if (warnings.length > 0) {
16586
+ log2.warn("");
16587
+ log2.warn("This Work Item may not be ready:");
16588
+ for (const w of warnings) {
16589
+ log2.warn(` - ${w.message}`);
16590
+ }
16591
+ }
16592
+ if (!opts.yes) {
16593
+ log2.info("");
16594
+ const proceed = await confirm2({ message: "Mark this Work Item as ready for implementation?" });
16595
+ if (!proceed) {
16596
+ cancel2("Cancelled.");
16597
+ return;
16598
+ }
16599
+ }
16600
+ const { newPath, newContent } = transitionToReady(wi.filePath);
16601
+ if (newPath !== wi.filePath) {
16602
+ writeFile(newPath, newContent);
16603
+ fs4.unlinkSync(wi.filePath);
16604
+ log2.success(`Updated status: ready`);
16605
+ log2.success(`Moved file:`);
16606
+ log2.info(` ${path6.relative(dir, wi.filePath)}`);
16607
+ log2.info(` \u2192 ${path6.relative(dir, newPath)}`);
16608
+ } else {
16609
+ writeFile(wi.filePath, newContent);
16610
+ log2.success(`Updated status: ready`);
16611
+ }
16612
+ log2.info("");
16613
+ log2.info("Run `kaddo context && kaddo understand` to refresh the handoff.");
16614
+ outro2(`Work Item ${wi.id} is ready.`);
16615
+ }
16616
+
16442
16617
  // src/index.ts
16443
16618
  var require2 = createRequire(import.meta.url);
16444
16619
  var { version } = require2("../package.json");
@@ -16456,12 +16631,15 @@ program.command("bootstrap").description("Build the initial knowledge base for a
16456
16631
  program.command("create [type]").description("Create a work item (feature, bugfix, hotfix, spike, chore). Use --from roadmap to create from a roadmap candidate.").option("--from <source>", "Create from a source artifact (currently: roadmap)").action(async (type, opts) => {
16457
16632
  await runCreate(type ?? "", opts);
16458
16633
  });
16634
+ program.command("ready <id>").description("Mark a draft Work Item as ready for implementation after human review").option("--yes", "Skip confirmation prompt").action(async (id, opts) => {
16635
+ await runReady(id, opts);
16636
+ });
16459
16637
  var capsuleCmd = program.command("capsule").description("Export this project as a Knowledge Capsule, or import an external one as context");
16460
16638
  capsuleCmd.command("export").description("Write a Knowledge Capsule about this project to .kaddo/exports/").action(() => {
16461
16639
  runCapsuleExport();
16462
16640
  });
16463
- capsuleCmd.command("add <path>").description("Register an external Knowledge Capsule as project context (.kaddo/external.yml)").action((path6) => {
16464
- runCapsuleAdd(path6);
16641
+ capsuleCmd.command("add <path>").description("Register an external Knowledge Capsule as project context (.kaddo/external.yml)").action((path7) => {
16642
+ runCapsuleAdd(path7);
16465
16643
  });
16466
16644
  var graphCmd = program.command("graph").description("Export the lightweight, file-based knowledge graph of the project");
16467
16645
  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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaddo/cli",
3
- "version": "3.56.0",
3
+ "version": "3.58.0",
4
4
  "description": "Knowledge Driven Development toolkit",
5
5
  "license": "MIT",
6
6
  "repository": {