@kaddo/mcp 3.22.1 → 3.23.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 +2298 -1961
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -90,12 +90,13 @@ var DERIVED_WRITE_FILES = /* @__PURE__ */ new Set([
90
90
  ".kaddo/graph-hints.json"
91
91
  ]);
92
92
  var DERIVED_EXPORTS_RE = /^\.kaddo\/exports\/[^/]+\.capsule\.(md|json)$/;
93
+ var DERIVED_REPORTS_RE = /^\.kaddo\/reports\/[^/]+\.(md|json)$/;
93
94
  function assertMcpDerivedWritePath(relPath) {
94
95
  const p = toPosix(relPath).replace(/^\/+/, "");
95
96
  if (path.isAbsolute(relPath) || p.split("/").some((seg) => seg === "..")) {
96
97
  throw new KaddoMcpError("Blocked unsafe MCP derived write path.");
97
98
  }
98
- if (!DERIVED_WRITE_FILES.has(p) && !DERIVED_EXPORTS_RE.test(p)) {
99
+ if (!DERIVED_WRITE_FILES.has(p) && !DERIVED_EXPORTS_RE.test(p) && !DERIVED_REPORTS_RE.test(p)) {
99
100
  throw new KaddoMcpError("Blocked unsafe MCP derived write path.");
100
101
  }
101
102
  return p;
@@ -249,367 +250,51 @@ function getSkill(root, id) {
249
250
  return { ...info, content, description: firstHeadingOrLine2(content) || info.title };
250
251
  }
251
252
 
252
- // src/workitems.ts
253
- import matter3 from "gray-matter";
254
- var ACTIVE_STATES = /* @__PURE__ */ new Set(["draft", "ready", "in-progress", "blocked"]);
255
- function strArray(v) {
256
- return Array.isArray(v) ? v.map((x) => String(x)).filter(Boolean) : [];
253
+ // ../cli/src/utils/fs.ts
254
+ import fs2 from "fs";
255
+ import path2 from "path";
256
+ function exists(p) {
257
+ return fs2.existsSync(p);
257
258
  }
258
- function lifecycleOf(data, relPath) {
259
- const status = String(data.status ?? "").trim();
260
- if (status) return status;
261
- const segs = relPath.split("/");
262
- const folder = segs[segs.length - 2];
263
- return ACTIVE_STATES.has(folder) || folder === "completed" || folder === "archived" ? folder : "ready";
259
+ function readFile(p) {
260
+ return fs2.readFileSync(p, "utf-8");
264
261
  }
265
- function firstParagraph(body) {
266
- for (const block of body.split(/\n\s*\n/)) {
267
- const t = block.trim();
268
- if (t && !t.startsWith("#") && !t.startsWith(">")) return t.replace(/\s+/g, " ");
269
- }
270
- return "";
262
+ function readDir(dirPath) {
263
+ if (!exists(dirPath)) return [];
264
+ return fs2.readdirSync(dirPath);
271
265
  }
272
- function listWorkItems(root) {
273
- const files = listFiles(root, "knowledge/delivery/work-items", ".md");
274
- const items = [];
275
- for (const rel of files) {
276
- const raw = readText(root, rel);
277
- if (raw === null) continue;
278
- let parsed;
279
- try {
280
- parsed = matter3(raw);
281
- } catch {
282
- continue;
283
- }
284
- const data = parsed.data;
285
- if (!data.type) continue;
286
- items.push({
287
- id: String(data.id ?? "") || rel,
288
- title: String(data.title ?? ""),
289
- status: lifecycleOf(data, rel),
290
- type: String(data.type ?? ""),
291
- knowledge_level: String(data.knowledge_level ?? ""),
292
- path: rel,
293
- summary: String(data.summary ?? "") || firstParagraph(parsed.content),
294
- code: strArray(data.code),
295
- capabilities: strArray(data.capabilities),
296
- decisions: strArray(data.decisions),
297
- capsules: strArray(data.capsules)
298
- });
299
- }
300
- return items.sort((a, b) => a.id.localeCompare(b.id));
266
+ function isFile(p) {
267
+ return exists(p) && fs2.statSync(p).isFile();
301
268
  }
302
-
303
- // src/resources.ts
304
- function text(uri, body, mimeType = "text/markdown") {
305
- return [{ uri, text: body, mimeType }];
269
+ function isDir(p) {
270
+ return exists(p) && fs2.statSync(p).isDirectory();
306
271
  }
307
- function fileOrHint(root, uri, rel, hint, mimeType = "text/markdown") {
308
- const body = readText(root, rel);
309
- return text(uri, body ?? hint, body ? mimeType : "text/plain");
272
+ function join(...parts) {
273
+ return path2.join(...parts);
310
274
  }
311
- var RESOURCES = [
312
- {
313
- uri: "kaddo://context-pack",
314
- name: "Kaddo context pack",
315
- description: "Curated LLM context pack (.kaddo/context-pack.md).",
316
- mimeType: "text/markdown",
317
- read: (root) => fileOrHint(
318
- root,
319
- "kaddo://context-pack",
320
- ".kaddo/context-pack.md",
321
- "Context pack not found. Run `kaddo context` in the project first."
322
- )
323
- },
324
- {
325
- uri: "kaddo://explain",
326
- name: "Kaddo explain",
327
- description: "What Kaddo knows about the project (.kaddo/explain.md).",
328
- mimeType: "text/markdown",
329
- read: (root) => fileOrHint(
330
- root,
331
- "kaddo://explain",
332
- ".kaddo/explain.md",
333
- "Explain output not found. Run `kaddo explain` first."
334
- )
335
- },
336
- {
337
- uri: "kaddo://understand",
338
- name: "Kaddo understand",
339
- description: "Current phase and recommended next step (.kaddo/understand.md).",
340
- mimeType: "text/markdown",
341
- read: (root) => fileOrHint(
342
- root,
343
- "kaddo://understand",
344
- ".kaddo/understand.md",
345
- "Understand output not found. Run `kaddo understand` first."
346
- )
347
- },
348
- {
349
- uri: "kaddo://graph",
350
- name: "Kaddo knowledge graph",
351
- description: "Knowledge graph (.kaddo/graph.json + .kaddo/graph.mmd).",
352
- mimeType: "application/json",
353
- read: (root) => {
354
- const json = readText(root, ".kaddo/graph.json");
355
- const mmd = readText(root, ".kaddo/graph.mmd");
356
- if (!json && !mmd) {
357
- return text("kaddo://graph", "Knowledge graph not found. Run `kaddo graph export` first.", "text/plain");
358
- }
359
- const parts = [];
360
- if (json) parts.push({ uri: "kaddo://graph", text: json, mimeType: "application/json" });
361
- if (mmd) parts.push({ uri: "kaddo://graph.mmd", text: mmd, mimeType: "text/vnd.mermaid" });
362
- return parts;
363
- }
364
- },
365
- {
366
- uri: "kaddo://graph-hints",
367
- name: "Kaddo graph hints",
368
- description: "Graph relationship-quality hints (.kaddo/graph-hints.md + .json).",
369
- mimeType: "text/markdown",
370
- read: (root) => {
371
- const md = readText(root, ".kaddo/graph-hints.md");
372
- const json = readText(root, ".kaddo/graph-hints.json");
373
- if (!md && !json) {
374
- return text("kaddo://graph-hints", "Graph hints not found. Run `kaddo graph export` first.", "text/plain");
375
- }
376
- const parts = [];
377
- if (md) parts.push({ uri: "kaddo://graph-hints", text: md, mimeType: "text/markdown" });
378
- if (json) parts.push({ uri: "kaddo://graph-hints.json", text: json, mimeType: "application/json" });
379
- return parts;
380
- }
381
- },
382
- {
383
- uri: "kaddo://work-items",
384
- name: "Kaddo work items",
385
- description: "Summarized Work Items from knowledge/delivery/work-items/.",
386
- mimeType: "application/json",
387
- read: (root) => {
388
- if (!hasKnowledge(root)) {
389
- return text("kaddo://work-items", "Knowledge repository not found. Run `kaddo bootstrap` first.", "text/plain");
390
- }
391
- return [
392
- {
393
- uri: "kaddo://work-items",
394
- text: JSON.stringify(listWorkItems(root), null, 2),
395
- mimeType: "application/json"
396
- }
397
- ];
398
- }
399
- },
400
- {
401
- uri: "kaddo://roadmap",
402
- name: "Kaddo roadmap",
403
- description: "Delivery roadmap (knowledge/delivery/roadmap.md).",
404
- mimeType: "text/markdown",
405
- read: (root) => fileOrHint(
406
- root,
407
- "kaddo://roadmap",
408
- "knowledge/delivery/roadmap.md",
409
- "Roadmap not found. Create knowledge/delivery/roadmap.md (e.g. via the roadmap-agent) first."
410
- )
411
- },
412
- {
413
- uri: "kaddo://capsules",
414
- name: "Kaddo knowledge capsules",
415
- description: "External Knowledge Capsules from .kaddo/external.yml and external/.",
416
- mimeType: "application/json",
417
- read: (root) => [
418
- {
419
- uri: "kaddo://capsules",
420
- text: JSON.stringify(listCapsules(root), null, 2),
421
- mimeType: "application/json"
422
- }
423
- ]
424
- },
425
- {
426
- uri: "kaddo://agents",
427
- name: "Kaddo agents",
428
- description: "Installed agent prompts from knowledge/agents/.",
429
- mimeType: "application/json",
430
- read: (root) => [
431
- {
432
- uri: "kaddo://agents",
433
- text: JSON.stringify(listAgents(root), null, 2),
434
- mimeType: "application/json"
435
- }
436
- ]
437
- },
438
- {
439
- uri: "kaddo://skills",
440
- name: "Kaddo skills",
441
- description: "Installed reusable skills from knowledge/skills/ (empty if none).",
442
- mimeType: "application/json",
443
- read: (root) => [
444
- {
445
- uri: "kaddo://skills",
446
- text: JSON.stringify({ skills: listSkills(root) }, null, 2),
447
- mimeType: "application/json"
448
- }
449
- ]
450
- }
451
- ];
452
275
 
453
- // src/prompts.ts
454
- var EXTRA_INPUTS = {
455
- "graph-agent": [".kaddo/graph.json", ".kaddo/graph-hints.md"],
456
- "ownership-agent": ["knowledge/delivery/work-items/", "knowledge/inventory.md"],
457
- "capsule-agent": ["knowledge/product/capabilities.md", "knowledge/tech/decisions/"],
458
- "work-item-agent": ["knowledge/delivery/roadmap.md"],
459
- "implementation-agent": ["knowledge/tech/git-strategy.md"]
276
+ // ../cli/src/core/impact-report.ts
277
+ import matter6 from "gray-matter";
278
+
279
+ // ../cli/src/core/config.ts
280
+ import { z } from "zod";
281
+ import { parse as parseYaml2 } from "yaml";
282
+ var PROJECT_STATES = ["new", "pre-ai", "legacy"];
283
+ var TEAM_SIZES = ["indie", "small", "medium", "enterprise"];
284
+ var REPOSITORY_STRUCTURES = ["monorepo", "multirepo"];
285
+ var PROJECT_LANGUAGES = ["en", "es"];
286
+ var DEFAULTS = {
287
+ state: "pre-ai",
288
+ structure: "monorepo",
289
+ teamSize: "indie",
290
+ language: "en"
460
291
  };
461
- function recommendedInputs(name) {
462
- return [".kaddo/context-pack.md", ...EXTRA_INPUTS[name] ?? []];
463
- }
464
- function listPrompts(root) {
465
- return listAgents(root).map((a) => ({
466
- name: a.name,
467
- description: a.description || `Kaddo ${a.name}`,
468
- content: "",
469
- recommended_inputs: recommendedInputs(a.name)
470
- }));
292
+ function languageLabel(lang) {
293
+ return lang === "es" ? "Spanish" : "English";
471
294
  }
472
- function getPrompt(root, name) {
473
- const agent = getAgentPrompt(root, name);
474
- if (!agent) return null;
475
- return {
476
- name: agent.name,
477
- description: agent.description || `Kaddo ${agent.name}`,
478
- content: agent.content,
479
- recommended_inputs: recommendedInputs(agent.name)
480
- };
481
- }
482
-
483
- // src/tools.ts
484
- var ok = (data) => ({ ok: true, data });
485
- var fail = (message) => ({ ok: false, message });
486
- function projectStatus(root) {
487
- const explain = readJson(root, ".kaddo/explain.json");
488
- const hints = readJson(root, ".kaddo/graph-hints.json");
489
- const capsules = listCapsules(root);
490
- if (!explain) {
491
- return fail("Project status needs `.kaddo/explain.json`. Run `kaddo explain` first.");
492
- }
493
- const maturity = (explain.layers ?? []).map((l) => `${l.layer}: ${l.status}`);
494
- return ok({
495
- project: explain.project?.name ?? "unknown",
496
- state: explain.project?.state ?? "unknown",
497
- knowledgeMaturity: maturity,
498
- workItems: {
499
- total: explain.workItems?.total ?? 0,
500
- byState: explain.workItems?.byState ?? {},
501
- byType: explain.workItems?.byType ?? {}
502
- },
503
- ownership: {
504
- withOwnership: explain.ownership?.workItemsWithOwnership ?? 0,
505
- total: explain.ownership?.workItemsTotal ?? 0
506
- },
507
- graph: {
508
- scope: hints?.scope ?? "unknown (run `kaddo graph export`)",
509
- scope_reason: hints?.scope_reason ?? "",
510
- quality: hints?.quality ?? "unknown (run `kaddo graph export`)",
511
- hints: hints?.summary?.hints ?? 0
512
- },
513
- capsules: capsules.length
514
- });
515
- }
516
- function listWorkItemsTool(root, filter = {}) {
517
- let items = listWorkItems(root);
518
- if (filter.status) items = items.filter((w) => w.status === filter.status);
519
- if (filter.type) items = items.filter((w) => w.type === filter.type);
520
- if (filter.knowledge_level) items = items.filter((w) => w.knowledge_level === filter.knowledge_level);
521
- return ok(items);
522
- }
523
- function getWorkItem(root, id) {
524
- const item = listWorkItems(root).find((w) => w.id === id);
525
- if (!item) return fail(`Work Item "${id}" not found.`);
526
- const content = readText(root, item.path);
527
- return ok({ ...item, content: content ?? "" });
528
- }
529
- function listCapsulesTool(root) {
530
- return ok(listCapsules(root));
531
- }
532
- function getCapsuleTool(root, id) {
533
- const cap = getCapsule(root, id);
534
- if (!cap) return fail(`Knowledge Capsule "${id}" not found.`);
535
- return ok(cap);
536
- }
537
- function listAgentsTool(root) {
538
- return ok(listAgents(root));
539
- }
540
- function getAgentPromptTool(root, name) {
541
- const agent = getAgentPrompt(root, name);
542
- if (!agent) return fail(`Agent "${name}" is not installed. Run \`kaddo add agents\` first.`);
543
- return ok(agent);
544
- }
545
- function listSkillsTool(root) {
546
- return ok(listSkills(root));
547
- }
548
- function getSkillTool(root, id) {
549
- const skill = getSkill(root, id);
550
- if (!skill) return fail(`Skill "${id}" is not installed. Run \`kaddo add skills\` first.`);
551
- return ok(skill);
552
- }
553
- function listGraphHints(root, filter = {}) {
554
- const report = readJson(root, ".kaddo/graph-hints.json");
555
- if (!report) return fail("Graph hints not found. Run `kaddo graph export` first.");
556
- let hints = Array.isArray(report.hints) ? report.hints : [];
557
- if (filter.artifact_type) hints = hints.filter((h) => h.artifact_type === filter.artifact_type);
558
- if (filter.severity) hints = hints.filter((h) => h.severity === filter.severity);
559
- if (filter.active_only) hints = hints.filter((h) => h.artifact_type === "work-item");
560
- return ok({
561
- scope: report.scope ?? "active",
562
- scope_reason: report.scope_reason ?? "",
563
- quality: report.quality ?? "unknown",
564
- count: hints.length,
565
- hints
566
- });
567
- }
568
-
569
- // ../cli/src/core/config.ts
570
- import { z } from "zod";
571
-
572
- // ../cli/src/utils/fs.ts
573
- import fs2 from "fs";
574
- import path2 from "path";
575
- function exists(p) {
576
- return fs2.existsSync(p);
577
- }
578
- function readFile(p) {
579
- return fs2.readFileSync(p, "utf-8");
580
- }
581
- function readDir(dirPath) {
582
- if (!exists(dirPath)) return [];
583
- return fs2.readdirSync(dirPath);
584
- }
585
- function isFile(p) {
586
- return exists(p) && fs2.statSync(p).isFile();
587
- }
588
- function isDir(p) {
589
- return exists(p) && fs2.statSync(p).isDirectory();
590
- }
591
- function join(...parts) {
592
- return path2.join(...parts);
593
- }
594
-
595
- // ../cli/src/core/config.ts
596
- import { parse as parseYaml2 } from "yaml";
597
- var PROJECT_STATES = ["new", "pre-ai", "legacy"];
598
- var TEAM_SIZES = ["indie", "small", "medium", "enterprise"];
599
- var REPOSITORY_STRUCTURES = ["monorepo", "multirepo"];
600
- var PROJECT_LANGUAGES = ["en", "es"];
601
- var DEFAULTS = {
602
- state: "pre-ai",
603
- structure: "monorepo",
604
- teamSize: "indie",
605
- language: "en"
606
- };
607
- function languageLabel(lang) {
608
- return lang === "es" ? "Spanish" : "English";
609
- }
610
- function projectLanguage(config) {
611
- const lang = config.project.language;
612
- return lang === "es" ? "es" : "en";
295
+ function projectLanguage(config) {
296
+ const lang = config.project.language;
297
+ return lang === "es" ? "es" : "en";
613
298
  }
614
299
  var ConfigError = class extends Error {
615
300
  constructor(message) {
@@ -678,14 +363,11 @@ ${issues}`);
678
363
  return parsed.data;
679
364
  }
680
365
 
681
- // ../cli/src/core/context-pack.ts
682
- import matter7 from "gray-matter";
683
-
684
366
  // ../cli/src/services/artifact-reader.ts
685
- import matter4 from "gray-matter";
367
+ import matter3 from "gray-matter";
686
368
  function parseArtifact(filePath, raw) {
687
369
  try {
688
- const { data } = matter4(raw);
370
+ const { data } = matter3(raw);
689
371
  const globs = Array.isArray(data.code) ? data.code.filter(Boolean) : [];
690
372
  return {
691
373
  filePath,
@@ -737,7 +419,7 @@ var LIFECYCLE_STATES = [
737
419
  "completed",
738
420
  "archived"
739
421
  ];
740
- var ACTIVE_STATES2 = ["draft", "ready", "in-progress", "blocked"];
422
+ var ACTIVE_STATES = ["draft", "ready", "in-progress", "blocked"];
741
423
  var LEGACY_STATUS_MAP = {
742
424
  done: "completed",
743
425
  cancelled: "archived",
@@ -749,7 +431,7 @@ function isLifecycleState(s) {
749
431
  return STATE_SET.has(s);
750
432
  }
751
433
  function isActiveState(s) {
752
- return ACTIVE_STATES2.includes(s);
434
+ return ACTIVE_STATES.includes(s);
753
435
  }
754
436
  function lifecycleFolderOf(filePath) {
755
437
  const p = filePath.replace(/\\/g, "/");
@@ -816,944 +498,552 @@ function discoverWorkItems(dir) {
816
498
  return discoverKnowledge(dir).filter((a) => a.isWorkItem);
817
499
  }
818
500
 
819
- // ../cli/src/services/mapped-modules.ts
820
- import { parse as parseYaml3 } from "yaml";
821
- var DESCRIPTOR_PATH = ".kaddo/modules.yml";
822
- function toStringArray(value) {
823
- return Array.isArray(value) ? value.map((v) => String(v)).filter(Boolean) : [];
501
+ // ../cli/src/core/delivery-phase.ts
502
+ function layer(layers, name) {
503
+ return layers.find((l) => l.layer === name)?.status ?? "Missing";
824
504
  }
825
- function moduleArtifactCoverage(dir, id) {
826
- const base = join(dir, "knowledge", "tech", "modules", id);
827
- return {
828
- moduleDesign: exists(join(base, "module-design.md")),
829
- stack: exists(join(base, "stack.md")),
830
- security: exists(join(base, "security.md")),
831
- standards: exists(join(base, "standards.md")),
832
- diagrams: exists(join(base, "diagrams")),
833
- adrs: exists(join(base, "adrs"))
834
- };
505
+ function baseComplete(layers) {
506
+ return layer(layers, "Business") !== "Missing" && layer(layers, "Product") !== "Missing" && layer(layers, "Tech") !== "Missing";
835
507
  }
836
- function loadMappedModules(dir) {
837
- const path3 = join(dir, DESCRIPTOR_PATH);
838
- if (!exists(path3)) return [];
839
- let parsed;
840
- try {
841
- parsed = parseYaml3(readFile(path3));
842
- } catch {
843
- return [];
508
+ function determinePhase(input) {
509
+ const { roadmap, workItems } = input;
510
+ const active = workItems.byState.draft + workItems.byState.ready + workItems.byState["in-progress"] + workItems.byState.blocked;
511
+ if (!baseComplete(input.layers)) return "Discovery";
512
+ if (!roadmap.present) return "Planning";
513
+ if (workItems.total === 0) return "Delivery Preparation";
514
+ if (active > 0) return "Active Delivery";
515
+ return "Maintenance";
516
+ }
517
+ function ownershipPct(o) {
518
+ if (o.workItemsTotal === 0) return 100;
519
+ return Math.round(o.workItemsWithOwnership / o.workItemsTotal * 100);
520
+ }
521
+ function buildReasons(input) {
522
+ const reasons = [];
523
+ reasons.push(input.roadmap.present ? "Roadmap available" : "No roadmap yet");
524
+ if (input.roadmap.present) {
525
+ reasons.push(`${input.roadmap.materialized} materialized work item(s)`);
526
+ if (input.roadmap.remaining > 0) reasons.push(`${input.roadmap.remaining} roadmap candidate(s) remaining`);
844
527
  }
845
- const raw = Array.isArray(parsed?.modules) ? parsed.modules : [];
846
- const modules = [];
847
- for (const entry of raw) {
848
- if (!entry || typeof entry !== "object") continue;
849
- const m = entry;
850
- const id = typeof m.id === "string" ? m.id : "";
851
- if (!id) continue;
852
- modules.push({
853
- id,
854
- name: typeof m.name === "string" ? m.name : void 0,
855
- repoPath: typeof m.repoPath === "string" ? m.repoPath : "",
856
- type: typeof m.type === "string" ? m.type : void 0,
857
- mainTechnology: typeof m.mainTechnology === "string" ? m.mainTechnology : void 0,
858
- owner: typeof m.owner === "string" ? m.owner : void 0,
859
- capabilities: toStringArray(m.capabilities),
860
- code: toStringArray(m.code),
861
- status: typeof m.status === "string" ? m.status : void 0,
862
- artifacts: moduleArtifactCoverage(dir, id)
863
- });
528
+ const active = LIFECYCLE_STATES.filter((s) => input.workItems.byState[s] > 0 && s !== "completed" && s !== "archived");
529
+ if (active.length > 0) {
530
+ reasons.push(active.map((s) => `${s}: ${input.workItems.byState[s]}`).join(", "));
864
531
  }
865
- return modules;
532
+ if (input.workItems.total > 0) reasons.push(`Ownership coverage ${ownershipPct(input.ownership)}%`);
533
+ return reasons;
866
534
  }
867
- function presentArtifacts(coverage) {
868
- const present = [];
869
- if (coverage.moduleDesign) present.push("module-design");
870
- if (coverage.stack) present.push("stack");
871
- if (coverage.security) present.push("security");
872
- if (coverage.standards) present.push("standards");
873
- if (coverage.diagrams) present.push("diagrams");
874
- if (coverage.adrs) present.push("adrs");
875
- return present;
535
+ function firstMissingLayerAgent(layers) {
536
+ if (layer(layers, "Business") === "Missing")
537
+ return { agent: "business-agent", step: "Use business-agent to create knowledge/business/business.md" };
538
+ if (layer(layers, "Product") === "Missing")
539
+ return { agent: "capability-agent", step: "Use capability-agent to create knowledge/product/capabilities.md" };
540
+ return { agent: "architecture-agent", step: "Use architecture-agent to create knowledge/tech/current-state.md" };
876
541
  }
877
-
878
- // ../cli/src/core/knowledge-discovery.ts
879
- var KNOWLEDGE = "knowledge";
880
- var CONSOLIDATED_TYPE = {
881
- Business: "business",
882
- Product: "product",
883
- Tech: "codebase"
884
- };
885
- var STRUCTURED_TYPES = {
886
- Business: /* @__PURE__ */ new Set(["problem", "users", "value-proposition", "business-rules", "constraints", "glossary"]),
887
- Product: /* @__PURE__ */ new Set(["product-brief", "capabilities"]),
888
- Tech: /* @__PURE__ */ new Set([
889
- "current-state",
890
- "architecture-notes",
891
- "decision-candidates",
892
- "quality-attributes",
893
- "stack",
894
- "standards",
895
- "security",
896
- "git-strategy",
897
- "module-design",
898
- "adr",
899
- "decision"
900
- ]),
901
- Delivery: /* @__PURE__ */ new Set([])
902
- };
903
- var WORK_ITEM_TYPES = /* @__PURE__ */ new Set([
904
- "work-item",
905
- "feature",
906
- "bugfix",
907
- "hotfix",
908
- "spike"
909
- ]);
910
- function layerForType(type) {
911
- if (!type) return null;
912
- if (type === CONSOLIDATED_TYPE.Business || STRUCTURED_TYPES.Business.has(type)) return "Business";
913
- if (type === CONSOLIDATED_TYPE.Product || STRUCTURED_TYPES.Product.has(type)) return "Product";
914
- if (type === CONSOLIDATED_TYPE.Tech || STRUCTURED_TYPES.Tech.has(type) || type === "knowledge")
915
- return "Tech";
916
- if (type === "roadmap" || WORK_ITEM_TYPES.has(type)) return "Delivery";
917
- return null;
918
- }
919
- function layerFromPath(filePath) {
920
- const p = filePath.replace(/\\/g, "/");
921
- if (p.includes(`/${KNOWLEDGE}/business/`)) return "Business";
922
- if (p.includes(`/${KNOWLEDGE}/product/`)) return "Product";
923
- if (p.includes(`/${KNOWLEDGE}/tech/`)) return "Tech";
924
- if (p.includes(`/${KNOWLEDGE}/delivery/`)) return "Delivery";
925
- return null;
926
- }
927
- function basename(p) {
928
- return p.replace(/\\/g, "/").split("/").pop() ?? p;
929
- }
930
- function discoverLayers(dir) {
931
- const acc = {
932
- Business: blank(),
933
- Product: blank(),
934
- Tech: blank(),
935
- Delivery: blank()
936
- };
937
- const archDir = join(dir, KNOWLEDGE);
938
- const artifacts = exists(archDir) ? readArtifacts(archDir) : [];
939
- for (const a of artifacts) {
940
- const type = a.type;
941
- const layer2 = layerForType(type) ?? layerFromPath(a.filePath);
942
- if (!layer2) continue;
943
- const slot = acc[layer2];
944
- slot.detected.add(basename(a.filePath));
945
- if (layer2 === "Delivery") {
946
- if (type === "roadmap" || basename(a.filePath) === "roadmap.md") slot.hasRoadmap = true;
947
- if (WORK_ITEM_TYPES.has(type) || a.filePath.replace(/\\/g, "/").includes("/delivery/work-items/")) {
948
- if (a.filePath.replace(/\\/g, "/").includes("/delivery/work-items/")) slot.hasWorkItem = true;
542
+ function assessPhase(input) {
543
+ const phase = determinePhase(input);
544
+ const reasons = buildReasons(input);
545
+ const bs = input.workItems.byState;
546
+ const items = input.workItems.items;
547
+ const firstOf = (s) => items.find((i) => i.lifecycle === s);
548
+ let recommendedAgents = [];
549
+ let nextStep = "";
550
+ let llmInstructions = [];
551
+ switch (phase) {
552
+ case "Discovery": {
553
+ const m = firstMissingLayerAgent(input.layers);
554
+ recommendedAgents = [m.agent];
555
+ nextStep = m.step;
556
+ llmInstructions = [`Use the ${m.agent} to fill the missing base knowledge.`, "Do not write code."];
557
+ break;
558
+ }
559
+ case "Planning": {
560
+ recommendedAgents = ["roadmap-agent"];
561
+ nextStep = "Use roadmap-agent to create knowledge/delivery/roadmap.md";
562
+ llmInstructions = ["Use the roadmap-agent.", "Do not write code.", "Generate roadmap candidates."];
563
+ break;
564
+ }
565
+ case "Delivery Preparation": {
566
+ recommendedAgents = ["kaddo create --from roadmap", "work-item-agent"];
567
+ const n = input.roadmap.remaining || input.roadmap.candidates;
568
+ nextStep = `Run \`kaddo create --from roadmap\`${n ? ` (${n} candidate(s))` : ""}, then refine with work-item-agent`;
569
+ llmInstructions = [
570
+ "Materialize roadmap candidates with `kaddo create --from roadmap`.",
571
+ "Use the work-item-agent to refine them.",
572
+ "Do not implement yet."
573
+ ];
574
+ break;
575
+ }
576
+ case "Active Delivery": {
577
+ if (bs.ready > 0) {
578
+ const wi = firstOf("ready");
579
+ recommendedAgents = ["implementation-agent"];
580
+ nextStep = wi ? `Start ${wi.id} \u2014 ${wi.title} (ready \u2192 in-progress)` : "Start a ready Work Item";
581
+ llmInstructions = [
582
+ "Use the implementation-agent.",
583
+ "Suggest a branch name only.",
584
+ "Do not run git commands."
585
+ ];
586
+ } else if (bs["in-progress"] > 0) {
587
+ const wi = firstOf("in-progress");
588
+ recommendedAgents = ["implementation-agent", "kaddo scan", "kaddo owners suggest", "kaddo guard"];
589
+ nextStep = wi ? `Continue ${wi.id} \u2014 ${wi.title}; then run kaddo scan, owners suggest, guard` : "Continue the in-progress Work Item; then scan, owners suggest, guard";
590
+ llmInstructions = [
591
+ "Continue the in-progress Work Item with the implementation-agent.",
592
+ "After changes, run `kaddo scan`, `kaddo owners suggest` and `kaddo guard`.",
593
+ "Do not commit, push or merge without explicit human confirmation."
594
+ ];
595
+ } else if (bs.draft > 0) {
596
+ const wi = firstOf("draft");
597
+ recommendedAgents = ["work-item-agent"];
598
+ nextStep = wi ? `Refine ${wi.id} from draft to ready` : "Refine a draft Work Item to ready";
599
+ llmInstructions = [
600
+ "Refine draft Work Items to ready.",
601
+ "Use the work-item-agent.",
602
+ "Do not implement unless the user explicitly asks."
603
+ ];
604
+ } else if (bs.blocked > 0) {
605
+ const wi = firstOf("blocked");
606
+ recommendedAgents = ["work-item-agent"];
607
+ nextStep = wi ? `Resolve the blocker on ${wi.id} \u2014 ${wi.title}` : "Resolve the blockers on active work";
608
+ llmInstructions = [
609
+ "Resolve the blockers with the work-item-agent.",
610
+ "Do not implement blocked work."
611
+ ];
949
612
  }
950
- continue;
613
+ if (input.ownership.workItemsMissingOwnership > 0) {
614
+ recommendedAgents.push("kaddo owners suggest");
615
+ llmInstructions.push("Ownership is incomplete \u2014 propose `code:` globs (run `kaddo owners suggest`).");
616
+ }
617
+ break;
951
618
  }
952
- if (type === CONSOLIDATED_TYPE[layer2]) slot.consolidated = true;
953
- else if (STRUCTURED_TYPES[layer2].has(type)) slot.structured = true;
954
- else if (!type) {
955
- slot.consolidated = true;
619
+ case "Maintenance": {
620
+ if (input.roadmap.remaining > 0) {
621
+ recommendedAgents = ["kaddo create --from roadmap", "work-item-agent"];
622
+ nextStep = `Materialize ${input.roadmap.remaining} remaining roadmap candidate(s)`;
623
+ llmInstructions = ["Materialize the remaining roadmap candidates.", "Do not implement yet."];
624
+ } else {
625
+ recommendedAgents = ["roadmap-agent"];
626
+ nextStep = "No active work \u2014 use roadmap-agent to plan the next initiative";
627
+ llmInstructions = ["No active work.", "Use the roadmap-agent to plan the next initiative."];
628
+ }
629
+ break;
956
630
  }
957
- if (type === "adr" || type === "decision") slot.hasDecision = true;
958
631
  }
959
- if (existsDirWithMd(join(dir, KNOWLEDGE, "tech", "decisions"))) acc.Tech.structured = true;
960
- return ["Business", "Product", "Tech", "Delivery"].map((layer2) => ({
961
- layer: layer2,
962
- status: statusFor(layer2, acc[layer2]),
963
- detected: [...acc[layer2].detected].sort()
964
- }));
965
- }
966
- function blank() {
967
- return {
968
- consolidated: false,
969
- structured: false,
970
- hasRoadmap: false,
971
- hasWorkItem: false,
972
- hasDecision: false,
973
- detected: /* @__PURE__ */ new Set()
974
- };
632
+ return { phase, reasons, recommendedAgents, nextStep, llmInstructions };
975
633
  }
976
- function existsDirWithMd(d) {
977
- if (!exists(d)) return false;
634
+
635
+ // ../cli/src/core/capsule.ts
636
+ import matter4 from "gray-matter";
637
+ import { parse as parseYaml4, stringify as stringifyYaml } from "yaml";
638
+
639
+ // ../cli/src/services/owners.ts
640
+ import { parse as parseYaml3 } from "yaml";
641
+ var CONFIG_PATH = ".kaddo/config.yml";
642
+ function loadOwners(dir) {
643
+ const configPath = join(dir, CONFIG_PATH);
644
+ if (!exists(configPath)) return {};
978
645
  try {
979
- return readDir(d).some((e) => e.endsWith(".md") && isFile(join(d, e)));
646
+ const config = parseYaml3(readFile(configPath));
647
+ const raw = config.owners ?? {};
648
+ const result = {};
649
+ for (const [domain, people] of Object.entries(raw)) {
650
+ result[domain] = Array.isArray(people) ? people.map(String) : [String(people)];
651
+ }
652
+ return result;
980
653
  } catch {
981
- return false;
982
- }
983
- }
984
- function statusFor(layer2, a) {
985
- if (layer2 === "Delivery") {
986
- if (a.hasWorkItem) return "Traceable";
987
- if (a.hasRoadmap) return "Partial";
988
- return "Missing";
654
+ return {};
989
655
  }
990
- if (a.structured) return "Structured";
991
- if (a.consolidated) return "Consolidated";
992
- return "Missing";
993
656
  }
994
657
 
995
- // ../cli/src/core/layers.ts
996
- function knowledgeLayers(dir) {
997
- return discoverLayers(dir);
658
+ // ../cli/src/core/capsule.ts
659
+ var KNOWLEDGE = "knowledge";
660
+ function today() {
661
+ return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
998
662
  }
999
- function renderLayersMarkdown(layers) {
1000
- const lines = [];
1001
- for (const { layer: layer2, status, detected } of layers) {
1002
- lines.push(`### ${layer2} \u2014 ${status}`);
1003
- if (detected.length > 0) for (const d of detected) lines.push(`- \u2713 ${d}`);
1004
- else lines.push("- \u2717 none");
1005
- lines.push("");
663
+ function firstParagraph(md) {
664
+ const body = md.replace(/^---\n[\s\S]*?\n---\n/, "");
665
+ for (const block of body.split(/\n\s*\n/)) {
666
+ const t = block.trim();
667
+ if (t && !t.startsWith("#") && !t.startsWith(">")) return t.replace(/\s+/g, " ");
1006
668
  }
1007
- return lines.join("\n").trimEnd();
669
+ return "";
1008
670
  }
1009
-
1010
- // ../cli/src/core/roadmap.ts
1011
- var INITIATIVE_RE = /^#{2,4}\s+(RM-[\w.-]+)\s*[:\-–]?\s*(.*)$/;
1012
- var FIELD_RE = /^\*\*(.+?):\*\*\s*(.*)$/;
1013
- var CANDIDATE_RE = /^\s*[-*]\s+(WI-[\w-]+)\s*[:\-–]\s*(.+)$/;
1014
- var PROPERTY_RE = /^\s+[-*]\s+([\w /]+?)\s*:\s*(.+)$/;
1015
- var BULLET_RE = /^\s*[-*]\s+(.+)$/;
1016
- function normalizeKey(key) {
1017
- return key.trim().toLowerCase();
671
+ function readIf(dir, rel) {
672
+ const p = join(dir, rel);
673
+ return exists(p) ? readFile(p) : null;
1018
674
  }
1019
- function splitInitiatives(markdown) {
1020
- const blocks = [];
1021
- let current = null;
1022
- for (const line of markdown.split(/\r?\n/)) {
1023
- const m = line.match(INITIATIVE_RE);
1024
- if (m) {
1025
- if (current) blocks.push(current);
1026
- current = { initiative: { id: m[1], title: m[2].trim() || void 0 }, lines: [] };
1027
- continue;
1028
- }
1029
- if (current) current.lines.push(line);
1030
- }
1031
- if (current) blocks.push(current);
1032
- return blocks;
675
+ function headings(md) {
676
+ return md.split(/\r?\n/).map((l) => l.match(/^#{2,3}\s+(.+?)\s*$/)).filter((m) => Boolean(m)).map((m) => m[1].trim()).filter((h) => !/^(summary|resumen|overview|risks|owners|out of scope)$/i.test(h));
1033
677
  }
1034
- function parseBlock(block) {
1035
- const meta = { relatedCapabilities: [], dependencies: [], openQuestions: [] };
1036
- const rawCandidates = [];
1037
- let listField = null;
1038
- let inCandidateSection = false;
1039
- let current = null;
1040
- const flush = () => {
1041
- if (current) {
1042
- current.rawMarkdown = current.rawMarkdown.trimEnd();
1043
- rawCandidates.push(current);
1044
- current = null;
1045
- }
678
+ function buildCapsule(dir, config) {
679
+ const ownerMap = loadOwners(dir);
680
+ const owners = [...new Set(Object.values(ownerMap).flat())];
681
+ const all = discoverKnowledge(dir);
682
+ const purposeSrc = readIf(dir, `${KNOWLEDGE}/business/business.md`) ?? readIf(dir, `${KNOWLEDGE}/knowledge.md`) ?? "";
683
+ const capsMd = readIf(dir, `${KNOWLEDGE}/product/capabilities.md`);
684
+ const risksMd = readIf(dir, `${KNOWLEDGE}/legacy/risks.md`);
685
+ const adrs = all.filter((a) => a.filePath.replace(/\\/g, "/").includes("/tech/decisions/") && Boolean(a.type)).map((a) => a.title || a.id).filter(Boolean);
686
+ return {
687
+ system: config.project.name,
688
+ version: 1,
689
+ updatedAt: today(),
690
+ owner: owners[0] ?? "unknown",
691
+ sourceProject: config.project.name,
692
+ purpose: firstParagraph(purposeSrc),
693
+ responsibilities: [],
694
+ capabilities: capsMd ? headings(capsMd) : [],
695
+ contracts: [],
696
+ dependencies: [],
697
+ knownRisks: risksMd ? headings(risksMd) : [],
698
+ adrs,
699
+ owners,
700
+ outOfScope: [],
701
+ usageNotes: []
1046
702
  };
1047
- for (const line of block.lines) {
1048
- const candMatch = line.match(CANDIDATE_RE);
1049
- if (candMatch && inCandidateSection) {
1050
- flush();
1051
- current = { id: candMatch[1], title: candMatch[2].trim(), rawMarkdown: line.trim() + "\n" };
1052
- listField = null;
1053
- continue;
1054
- }
1055
- if (current) {
1056
- const propMatch = line.match(PROPERTY_RE);
1057
- if (propMatch) {
1058
- current.rawMarkdown += line.trim() + "\n";
1059
- const key = normalizeKey(propMatch[1]);
1060
- const value = propMatch[2].trim();
1061
- if (key === "type") current.type = value;
1062
- else if (key === "suggested knowledge level" || key === "knowledge level")
1063
- current.suggestedKnowledgeLevel = value;
1064
- else if (key === "expected value") current.expectedValue = value;
1065
- else if (key === "notes") current.notes = value;
1066
- continue;
1067
- }
1068
- if (line.trim() === "") {
1069
- flush();
1070
- continue;
1071
- }
1072
- }
1073
- const fieldMatch = line.match(FIELD_RE);
1074
- if (fieldMatch) {
1075
- const key = normalizeKey(fieldMatch[1]);
1076
- const value = fieldMatch[2].trim();
1077
- listField = null;
1078
- inCandidateSection = false;
1079
- if (key === "candidate work items") {
1080
- inCandidateSection = true;
1081
- } else if (key === "impact") {
1082
- meta.impact = value || void 0;
1083
- } else if (key === "risk") {
1084
- meta.risk = value || void 0;
1085
- } else if (key === "project area / domain" || key === "domain" || key === "project area") {
1086
- meta.domain = value || void 0;
1087
- } else if (key === "related capabilities") {
1088
- listField = "relatedCapabilities";
1089
- if (value) meta.relatedCapabilities.push(value);
1090
- } else if (key === "dependencies") {
1091
- listField = "dependencies";
1092
- if (value) meta.dependencies.push(value);
1093
- } else if (key === "open questions") {
1094
- listField = "openQuestions";
1095
- if (value) meta.openQuestions.push(value);
1096
- }
1097
- continue;
1098
- }
1099
- if (listField) {
1100
- const bulletMatch = line.match(BULLET_RE);
1101
- if (bulletMatch) {
1102
- const item = bulletMatch[1].trim().replace(/^candidate\s*:\s*/i, "");
1103
- meta[listField].push(item);
1104
- continue;
1105
- }
1106
- if (line.trim() === "") {
1107
- listField = null;
1108
- continue;
1109
- }
1110
- }
1111
- }
1112
- flush();
1113
- return rawCandidates.map((c) => ({
1114
- ...c,
1115
- initiative: block.initiative.id || block.initiative.title ? { ...block.initiative } : void 0,
1116
- relatedCapabilities: meta.relatedCapabilities.length ? [...meta.relatedCapabilities] : void 0,
1117
- domain: meta.domain,
1118
- impact: meta.impact,
1119
- risk: meta.risk,
1120
- dependencies: meta.dependencies.length ? [...meta.dependencies] : void 0,
1121
- openQuestions: meta.openQuestions.length ? [...meta.openQuestions] : void 0
1122
- }));
1123
703
  }
1124
- var WI_ID_RE = /\bWI-[A-Za-z0-9-]*\d/;
1125
- var HEADING_RE = /^#{2,4}\s+(.*)$/;
1126
- var FLEX_BULLET_RE = /^\s*[-*]\s+(?:\[[ xX]?\]\s+)?(WI-[A-Za-z0-9-]*\d)\b[\s:.\-–)]*\s*(.*)$/;
1127
- var SEPARATOR_CELL_RE = /^:?-{2,}:?$/;
1128
- function splitRow(line) {
1129
- return line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim());
704
+ function section(title, items, placeholder) {
705
+ if (items.length === 0) return `## ${title}
706
+
707
+ _${placeholder}_
708
+ `;
709
+ return `## ${title}
710
+
711
+ ${items.map((i) => `- ${i}`).join("\n")}
712
+ `;
1130
713
  }
1131
- function isSeparatorRow(cells) {
1132
- return cells.length > 0 && cells.every((c) => SEPARATOR_CELL_RE.test(c) || c === "");
714
+ function renderCapsuleMarkdown(c) {
715
+ const fm = [
716
+ "---",
717
+ "type: knowledge-capsule",
718
+ `system: ${c.system}`,
719
+ `version: ${c.version}`,
720
+ `updated_at: ${c.updatedAt}`,
721
+ `owner: ${c.owner}`,
722
+ `source_project: ${c.sourceProject}`,
723
+ ...c.sourceCommit ? [`source_commit: ${c.sourceCommit}`] : [],
724
+ "---"
725
+ ].join("\n");
726
+ const parts = [
727
+ fm,
728
+ "",
729
+ `# ${c.system} \u2014 Knowledge Capsule`,
730
+ "",
731
+ `## Purpose
732
+
733
+ ${c.purpose || "_To be completed by the capsule-agent._"}
734
+ `,
735
+ section("Responsibilities", c.responsibilities, "List what this system is responsible for."),
736
+ section("Exposed Capabilities", c.capabilities, "List the capabilities this system exposes."),
737
+ section("Public Contracts", c.contracts, "List public APIs and events (e.g. `POST /orders`, `OrderCreated`)."),
738
+ section("Dependencies", c.dependencies, "List systems this one depends on."),
739
+ section("Known Risks", c.knownRisks, "List integration risks consumers should know."),
740
+ section("Relevant ADRs", c.adrs, "List decisions that affect how to integrate."),
741
+ section("Owners", c.owners, "Who owns this system."),
742
+ section("Out of Scope", c.outOfScope, "What this capsule deliberately does not cover."),
743
+ section("Usage Notes", c.usageNotes, "How consumers should integrate."),
744
+ "> Security: this capsule must never contain secrets, tokens, credentials, PII or source code.",
745
+ ""
746
+ ];
747
+ return parts.join("\n");
1133
748
  }
1134
- function initiativeFromHeading(text2) {
1135
- const rm = text2.match(/^(RM-[\w.-]+)\s*[:\-–]?\s*(.*)$/);
1136
- if (rm) return { id: rm[1], title: rm[2].trim() || void 0 };
1137
- return { title: text2.trim() || void 0 };
749
+ function serializeCapsuleJson(c) {
750
+ return JSON.stringify({ type: "knowledge-capsule", ...c }, null, 2) + "\n";
1138
751
  }
1139
- function extractDeps(cell) {
1140
- if (!cell) return void 0;
1141
- const ids = cell.match(/WI-[A-Za-z0-9-]*\d/g);
1142
- return ids && ids.length ? [...new Set(ids)] : void 0;
752
+ var EXTERNAL_PATH = ".kaddo/external.yml";
753
+ function loadExternalRegistry(dir) {
754
+ const p = join(dir, EXTERNAL_PATH);
755
+ if (!exists(p)) return [];
756
+ try {
757
+ const parsed = parseYaml4(readFile(p));
758
+ return Array.isArray(parsed?.external) ? parsed.external : [];
759
+ } catch {
760
+ return [];
761
+ }
1143
762
  }
1144
- function parseFlexible(markdown) {
763
+ function sectionList(md, title) {
764
+ const re = new RegExp(`^##\\s+${title}\\s*$`, "im");
765
+ const lines = md.split(/\r?\n/);
766
+ const start = lines.findIndex((l) => re.test(l));
767
+ if (start < 0) return [];
1145
768
  const out = [];
1146
- const seen = /* @__PURE__ */ new Set();
1147
- let initiative;
1148
- let cols = null;
1149
- const push = (id, title, deps, raw) => {
1150
- if (seen.has(id)) return;
1151
- seen.add(id);
1152
- out.push({
1153
- id,
1154
- title: title.trim() || id,
1155
- dependencies: deps,
1156
- initiative: initiative?.id || initiative?.title ? { ...initiative } : void 0,
1157
- rawMarkdown: raw.trim()
1158
- });
769
+ for (let i = start + 1; i < lines.length; i++) {
770
+ if (/^##\s+/.test(lines[i])) break;
771
+ const m = lines[i].match(/^\s*-\s+(.+?)\s*$/);
772
+ if (m && !/^_.*_$/.test(m[1])) out.push(m[1].trim());
773
+ }
774
+ return out;
775
+ }
776
+ function sectionParagraph2(md, title) {
777
+ const re = new RegExp(`^##\\s+${title}\\s*$`, "im");
778
+ const lines = md.split(/\r?\n/);
779
+ const start = lines.findIndex((l) => re.test(l));
780
+ if (start < 0) return "";
781
+ for (let i = start + 1; i < lines.length; i++) {
782
+ if (/^##\s+/.test(lines[i])) break;
783
+ const t = lines[i].trim();
784
+ if (t && !/^_.*_$/.test(t)) return t;
785
+ }
786
+ return "";
787
+ }
788
+ function parseCapsule(id, path3, md) {
789
+ const { data } = matter4(md);
790
+ const updatedAt = data.updated_at ? String(data.updated_at) : void 0;
791
+ let ageDays = null;
792
+ if (updatedAt) {
793
+ const d = Date.parse(updatedAt);
794
+ if (!Number.isNaN(d)) ageDays = Math.max(0, Math.floor((Date.now() - d) / 864e5));
795
+ }
796
+ return {
797
+ id,
798
+ path: path3,
799
+ system: data.system ? String(data.system) : id,
800
+ owner: data.owner ? String(data.owner) : void 0,
801
+ updatedAt,
802
+ purpose: sectionParagraph2(md, "Purpose"),
803
+ capabilities: sectionList(md, "Exposed Capabilities"),
804
+ contracts: sectionList(md, "Public Contracts"),
805
+ knownRisks: sectionList(md, "Known Risks"),
806
+ ageDays
1159
807
  };
1160
- for (const line of markdown.split(/\r?\n/)) {
1161
- const h = line.match(HEADING_RE);
1162
- if (h) {
1163
- initiative = initiativeFromHeading(h[1]);
1164
- cols = null;
1165
- continue;
1166
- }
1167
- if (line.includes("|")) {
1168
- const cells = splitRow(line);
1169
- if (isSeparatorRow(cells)) continue;
1170
- const lower = cells.map((c) => c.toLowerCase());
1171
- const idIdx = lower.findIndex((c) => c === "id" || c === "wi" || c === "work item id");
1172
- if (idIdx >= 0 && !WI_ID_RE.test(line)) {
1173
- cols = {
1174
- id: idIdx,
1175
- title: lower.findIndex((c) => /work item|title|item|name|description/.test(c)),
1176
- deps: lower.findIndex((c) => /depend/.test(c))
1177
- };
1178
- continue;
1179
- }
1180
- if (WI_ID_RE.test(line)) {
1181
- let idCell = cols && cols.id >= 0 ? cells[cols.id] : cells.find((c) => /^WI-/.test(c));
1182
- const idMatch = (idCell ?? line).match(/WI-[A-Za-z0-9-]*\d/);
1183
- if (!idMatch) continue;
1184
- const id = idMatch[0];
1185
- const title = cols && cols.title >= 0 ? cells[cols.title] ?? "" : cells.find((c) => !/^WI-/.test(c) && c) ?? "";
1186
- const deps = cols && cols.deps >= 0 ? extractDeps(cells[cols.deps]) : void 0;
1187
- push(id, title, deps, line);
1188
- continue;
1189
- }
1190
- continue;
1191
- }
1192
- const b = line.match(FLEX_BULLET_RE);
1193
- if (b) {
1194
- push(b[1], b[2] ?? "", void 0, line);
1195
- continue;
808
+ }
809
+ function loadExternalCapsules(dir) {
810
+ const out = [];
811
+ for (const entry of loadExternalRegistry(dir)) {
812
+ const full = join(dir, entry.path);
813
+ if (!exists(full) || !isFile(full)) continue;
814
+ try {
815
+ out.push(parseCapsule(entry.id, entry.path, readFile(full)));
816
+ } catch {
1196
817
  }
1197
818
  }
1198
819
  return out;
1199
820
  }
1200
- function parseRoadmapCandidates(markdown) {
1201
- const strict = splitInitiatives(markdown).flatMap(parseBlock);
1202
- if (strict.length > 0) return strict;
1203
- return parseFlexible(markdown);
821
+
822
+ // ../cli/src/core/graph.ts
823
+ var KNOWLEDGE2 = "knowledge";
824
+ var ACTIVE_STATUSES = ["draft", "ready", "in-progress", "blocked"];
825
+ var ALL_STATUSES = ["draft", "ready", "in-progress", "blocked", "completed"];
826
+ function scopeStatuses(scope) {
827
+ return scope === "all" ? { included: ALL_STATUSES, excluded: ["archived"] } : { included: ACTIVE_STATUSES, excluded: ["completed", "archived"] };
1204
828
  }
1205
- function roadmapStats(markdown, materialized) {
1206
- if (markdown == null) return { present: false, candidates: 0, materialized, remaining: 0 };
1207
- const candidates = parseRoadmapCandidates(markdown).length;
1208
- return { present: true, candidates, materialized, remaining: Math.max(0, candidates - materialized) };
829
+ function toPosix3(p) {
830
+ return p.replace(/\\/g, "/");
1209
831
  }
1210
-
1211
- // ../cli/src/core/delivery-phase.ts
1212
- function layer(layers, name) {
1213
- return layers.find((l) => l.layer === name)?.status ?? "Missing";
1214
- }
1215
- function baseComplete(layers) {
1216
- return layer(layers, "Business") !== "Missing" && layer(layers, "Product") !== "Missing" && layer(layers, "Tech") !== "Missing";
1217
- }
1218
- function determinePhase(input) {
1219
- const { roadmap, workItems } = input;
1220
- const active = workItems.byState.draft + workItems.byState.ready + workItems.byState["in-progress"] + workItems.byState.blocked;
1221
- if (!baseComplete(input.layers)) return "Discovery";
1222
- if (!roadmap.present) return "Planning";
1223
- if (workItems.total === 0) return "Delivery Preparation";
1224
- if (active > 0) return "Active Delivery";
1225
- return "Maintenance";
832
+ function slug(s) {
833
+ return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1226
834
  }
1227
- function ownershipPct(o) {
1228
- if (o.workItemsTotal === 0) return 100;
1229
- return Math.round(o.workItemsWithOwnership / o.workItemsTotal * 100);
835
+ function isAdr(a) {
836
+ return toPosix3(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
1230
837
  }
1231
- function buildReasons(input) {
1232
- const reasons = [];
1233
- reasons.push(input.roadmap.present ? "Roadmap available" : "No roadmap yet");
1234
- if (input.roadmap.present) {
1235
- reasons.push(`${input.roadmap.materialized} materialized work item(s)`);
1236
- if (input.roadmap.remaining > 0) reasons.push(`${input.roadmap.remaining} roadmap candidate(s) remaining`);
838
+ function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
839
+ const scope = opts.scope ?? "active";
840
+ const nodes = /* @__PURE__ */ new Map();
841
+ const edges = [];
842
+ const edgeKeys = /* @__PURE__ */ new Set();
843
+ const addNode = (node) => {
844
+ if (!nodes.has(node.id)) nodes.set(node.id, node);
845
+ };
846
+ const addEdge = (from, to, type) => {
847
+ const key = `${from}|${to}|${type}`;
848
+ if (edgeKeys.has(key)) return;
849
+ edgeKeys.add(key);
850
+ edges.push({ from, to, type });
851
+ };
852
+ const layerDocs = [
853
+ { id: "business:business", type: "business", label: "Business", files: ["business/business.md"] },
854
+ { id: "product:product", type: "product", label: "Product", files: ["product/product.md", "product/capabilities.md"] },
855
+ { id: "tech:tech", type: "tech", label: "Tech", files: ["tech/current-state.md", "tech/codebase.md"] },
856
+ { id: "delivery:delivery", type: "delivery", label: "Delivery", files: ["delivery/roadmap.md"] }
857
+ ];
858
+ const presentLayers = [];
859
+ for (const layer2 of layerDocs) {
860
+ const path3 = layer2.files.map((f) => `${KNOWLEDGE2}/${f}`).find((rel) => exists(join(dir, rel)));
861
+ if (path3) {
862
+ addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path3 });
863
+ presentLayers.push(layer2.id);
864
+ }
1237
865
  }
1238
- const active = LIFECYCLE_STATES.filter((s) => input.workItems.byState[s] > 0 && s !== "completed" && s !== "archived");
1239
- if (active.length > 0) {
1240
- reasons.push(active.map((s) => `${s}: ${input.workItems.byState[s]}`).join(", "));
866
+ for (let i = 0; i < presentLayers.length - 1; i++) {
867
+ addEdge(presentLayers[i], presentLayers[i + 1], "informs");
1241
868
  }
1242
- if (input.workItems.total > 0) reasons.push(`Ownership coverage ${ownershipPct(input.ownership)}%`);
1243
- return reasons;
1244
- }
1245
- function firstMissingLayerAgent(layers) {
1246
- if (layer(layers, "Business") === "Missing")
1247
- return { agent: "business-agent", step: "Use business-agent to create knowledge/business/business.md" };
1248
- if (layer(layers, "Product") === "Missing")
1249
- return { agent: "capability-agent", step: "Use capability-agent to create knowledge/product/capabilities.md" };
1250
- return { agent: "architecture-agent", step: "Use architecture-agent to create knowledge/tech/current-state.md" };
1251
- }
1252
- function assessPhase(input) {
1253
- const phase = determinePhase(input);
1254
- const reasons = buildReasons(input);
1255
- const bs = input.workItems.byState;
1256
- const items = input.workItems.items;
1257
- const firstOf = (s) => items.find((i) => i.lifecycle === s);
1258
- let recommendedAgents = [];
1259
- let nextStep = "";
1260
- let llmInstructions = [];
1261
- switch (phase) {
1262
- case "Discovery": {
1263
- const m = firstMissingLayerAgent(input.layers);
1264
- recommendedAgents = [m.agent];
1265
- nextStep = m.step;
1266
- llmInstructions = [`Use the ${m.agent} to fill the missing base knowledge.`, "Do not write code."];
1267
- break;
869
+ const all = discoverKnowledge(dir);
870
+ const workItems = all.filter((a) => a.isWorkItem);
871
+ const { included, excluded } = scopeStatuses(scope);
872
+ const includedSet = new Set(included);
873
+ const activeWICount = workItems.filter((a) => a.lifecycle && isActiveState(a.lifecycle)).length;
874
+ const selectedWIs = workItems.filter((a) => a.lifecycle && includedSet.has(a.lifecycle));
875
+ for (const wi of selectedWIs) {
876
+ const id = wi.id || wi.title;
877
+ const wiNodeId = `wi:${id}`;
878
+ addNode({
879
+ id: wiNodeId,
880
+ type: "work-item",
881
+ label: `${id} ${wi.title}`.trim(),
882
+ path: wi.relPath,
883
+ status: wi.lifecycle,
884
+ knowledge_level: wi.knowledgeLevel || void 0
885
+ });
886
+ for (const glob of wi.codeGlobs) {
887
+ const codeId = `code:${glob}`;
888
+ addNode({ id: codeId, type: "code-glob", label: glob });
889
+ addEdge(wiNodeId, codeId, "owns");
1268
890
  }
1269
- case "Planning": {
1270
- recommendedAgents = ["roadmap-agent"];
1271
- nextStep = "Use roadmap-agent to create knowledge/delivery/roadmap.md";
1272
- llmInstructions = ["Use the roadmap-agent.", "Do not write code.", "Generate roadmap candidates."];
1273
- break;
891
+ for (const cap of wi.capabilities) {
892
+ const capId = `capability:${slug(cap) || cap}`;
893
+ addNode({ id: capId, type: "capability", label: cap });
894
+ addEdge(wiNodeId, capId, "implements");
1274
895
  }
1275
- case "Delivery Preparation": {
1276
- recommendedAgents = ["kaddo create --from roadmap", "work-item-agent"];
1277
- const n = input.roadmap.remaining || input.roadmap.candidates;
1278
- nextStep = `Run \`kaddo create --from roadmap\`${n ? ` (${n} candidate(s))` : ""}, then refine with work-item-agent`;
1279
- llmInstructions = [
1280
- "Materialize roadmap candidates with `kaddo create --from roadmap`.",
1281
- "Use the work-item-agent to refine them.",
1282
- "Do not implement yet."
1283
- ];
1284
- break;
896
+ for (const dec of wi.decisions) {
897
+ const adrId = `adr:${dec}`;
898
+ addNode({ id: adrId, type: "decision", label: dec });
899
+ addEdge(wiNodeId, adrId, "depends_on");
1285
900
  }
1286
- case "Active Delivery": {
1287
- if (bs.ready > 0) {
1288
- const wi = firstOf("ready");
1289
- recommendedAgents = ["implementation-agent"];
1290
- nextStep = wi ? `Start ${wi.id} \u2014 ${wi.title} (ready \u2192 in-progress)` : "Start a ready Work Item";
1291
- llmInstructions = [
1292
- "Use the implementation-agent.",
1293
- "Suggest a branch name only.",
1294
- "Do not run git commands."
1295
- ];
1296
- } else if (bs["in-progress"] > 0) {
1297
- const wi = firstOf("in-progress");
1298
- recommendedAgents = ["implementation-agent", "kaddo scan", "kaddo owners suggest", "kaddo guard"];
1299
- nextStep = wi ? `Continue ${wi.id} \u2014 ${wi.title}; then run kaddo scan, owners suggest, guard` : "Continue the in-progress Work Item; then scan, owners suggest, guard";
1300
- llmInstructions = [
1301
- "Continue the in-progress Work Item with the implementation-agent.",
1302
- "After changes, run `kaddo scan`, `kaddo owners suggest` and `kaddo guard`.",
1303
- "Do not commit, push or merge without explicit human confirmation."
1304
- ];
1305
- } else if (bs.draft > 0) {
1306
- const wi = firstOf("draft");
1307
- recommendedAgents = ["work-item-agent"];
1308
- nextStep = wi ? `Refine ${wi.id} from draft to ready` : "Refine a draft Work Item to ready";
1309
- llmInstructions = [
1310
- "Refine draft Work Items to ready.",
1311
- "Use the work-item-agent.",
1312
- "Do not implement unless the user explicitly asks."
1313
- ];
1314
- } else if (bs.blocked > 0) {
1315
- const wi = firstOf("blocked");
1316
- recommendedAgents = ["work-item-agent"];
1317
- nextStep = wi ? `Resolve the blocker on ${wi.id} \u2014 ${wi.title}` : "Resolve the blockers on active work";
1318
- llmInstructions = [
1319
- "Resolve the blockers with the work-item-agent.",
1320
- "Do not implement blocked work."
1321
- ];
1322
- }
1323
- if (input.ownership.workItemsMissingOwnership > 0) {
1324
- recommendedAgents.push("kaddo owners suggest");
1325
- llmInstructions.push("Ownership is incomplete \u2014 propose `code:` globs (run `kaddo owners suggest`).");
1326
- }
1327
- break;
901
+ if (wi.initiative) {
902
+ const initId = `initiative:${slug(wi.initiative) || wi.initiative}`;
903
+ addNode({ id: initId, type: "initiative", label: wi.initiative });
904
+ addEdge(wiNodeId, initId, "belongs_to");
1328
905
  }
1329
- case "Maintenance": {
1330
- if (input.roadmap.remaining > 0) {
1331
- recommendedAgents = ["kaddo create --from roadmap", "work-item-agent"];
1332
- nextStep = `Materialize ${input.roadmap.remaining} remaining roadmap candidate(s)`;
1333
- llmInstructions = ["Materialize the remaining roadmap candidates.", "Do not implement yet."];
1334
- } else {
1335
- recommendedAgents = ["roadmap-agent"];
1336
- nextStep = "No active work \u2014 use roadmap-agent to plan the next initiative";
1337
- llmInstructions = ["No active work.", "Use the roadmap-agent to plan the next initiative."];
906
+ if (wi.source === "roadmap" && wi.sourceId) {
907
+ const candId = `candidate:${wi.sourceId}`;
908
+ addNode({ id: candId, type: "roadmap-candidate", label: wi.sourceId });
909
+ addEdge(candId, wiNodeId, "materialized_as");
910
+ }
911
+ }
912
+ for (const adr of all.filter(isAdr)) {
913
+ const adrId = `adr:${adr.id || adr.title}`;
914
+ const referenced = nodes.has(adrId);
915
+ if (scope === "all" || referenced) {
916
+ nodes.set(adrId, {
917
+ id: adrId,
918
+ type: "decision",
919
+ label: `${adr.id} ${adr.title}`.trim() || adr.id || adr.title,
920
+ path: adr.relPath
921
+ });
922
+ for (const glob of adr.codeGlobs) {
923
+ const codeId = `code:${glob}`;
924
+ addNode({ id: codeId, type: "code-glob", label: glob });
925
+ addEdge(adrId, codeId, "governs");
1338
926
  }
1339
- break;
1340
927
  }
1341
928
  }
1342
- return { phase, reasons, recommendedAgents, nextStep, llmInstructions };
1343
- }
1344
-
1345
- // ../cli/src/core/capsule.ts
1346
- import matter5 from "gray-matter";
1347
- import { parse as parseYaml5, stringify as stringifyYaml } from "yaml";
1348
-
1349
- // ../cli/src/services/owners.ts
1350
- import { parse as parseYaml4 } from "yaml";
1351
- var CONFIG_PATH = ".kaddo/config.yml";
1352
- function loadOwners(dir) {
1353
- const configPath = join(dir, CONFIG_PATH);
1354
- if (!exists(configPath)) return {};
1355
- try {
1356
- const config = parseYaml4(readFile(configPath));
1357
- const raw = config.owners ?? {};
1358
- const result = {};
1359
- for (const [domain, people] of Object.entries(raw)) {
1360
- result[domain] = Array.isArray(people) ? people.map(String) : [String(people)];
929
+ const capsules = loadExternalRegistry(dir);
930
+ if (capsules.length > 0) {
931
+ const projId = `project:${slug(config.project.name) || "project"}`;
932
+ addNode({ id: projId, type: "project", label: config.project.name });
933
+ for (const cap of capsules) {
934
+ const capId = `capsule:${cap.id}`;
935
+ addNode({ id: capId, type: "knowledge-capsule", label: cap.id, path: cap.path });
936
+ addEdge(capId, projId, "provides_external_context");
937
+ for (const wi of selectedWIs) {
938
+ if (wi.capsules.includes(cap.id)) {
939
+ addEdge(`wi:${wi.id || wi.title}`, capId, "uses_external_knowledge");
940
+ }
941
+ }
1361
942
  }
1362
- return result;
1363
- } catch {
1364
- return {};
1365
943
  }
944
+ 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.";
945
+ return {
946
+ generated_at: now.toISOString(),
947
+ project: {
948
+ name: config.project.name,
949
+ state: config.project.state,
950
+ structure: config.project.structure
951
+ },
952
+ scope,
953
+ scope_reason: scopeReason,
954
+ included_statuses: included,
955
+ excluded_statuses: excluded,
956
+ nodes: [...nodes.values()],
957
+ edges
958
+ };
1366
959
  }
1367
-
1368
- // ../cli/src/core/capsule.ts
1369
- var KNOWLEDGE2 = "knowledge";
1370
- function today() {
1371
- return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1372
- }
1373
- function firstParagraph2(md) {
1374
- const body = md.replace(/^---\n[\s\S]*?\n---\n/, "");
1375
- for (const block of body.split(/\n\s*\n/)) {
1376
- const t = block.trim();
1377
- if (t && !t.startsWith("#") && !t.startsWith(">")) return t.replace(/\s+/g, " ");
1378
- }
1379
- return "";
1380
- }
1381
- function readIf(dir, rel) {
1382
- const p = join(dir, rel);
1383
- return exists(p) ? readFile(p) : null;
1384
- }
1385
- function headings(md) {
1386
- return md.split(/\r?\n/).map((l) => l.match(/^#{2,3}\s+(.+?)\s*$/)).filter((m) => Boolean(m)).map((m) => m[1].trim()).filter((h) => !/^(summary|resumen|overview|risks|owners|out of scope)$/i.test(h));
960
+ function serializeGraphJson(graph) {
961
+ return JSON.stringify(graph, null, 2) + "\n";
1387
962
  }
1388
- function buildCapsule(dir, config) {
1389
- const ownerMap = loadOwners(dir);
1390
- const owners = [...new Set(Object.values(ownerMap).flat())];
1391
- const all = discoverKnowledge(dir);
1392
- const purposeSrc = readIf(dir, `${KNOWLEDGE2}/business/business.md`) ?? readIf(dir, `${KNOWLEDGE2}/knowledge.md`) ?? "";
1393
- const capsMd = readIf(dir, `${KNOWLEDGE2}/product/capabilities.md`);
1394
- const risksMd = readIf(dir, `${KNOWLEDGE2}/legacy/risks.md`);
1395
- const adrs = all.filter((a) => a.filePath.replace(/\\/g, "/").includes("/tech/decisions/") && Boolean(a.type)).map((a) => a.title || a.id).filter(Boolean);
1396
- return {
1397
- system: config.project.name,
1398
- version: 1,
1399
- updatedAt: today(),
1400
- owner: owners[0] ?? "unknown",
1401
- sourceProject: config.project.name,
1402
- purpose: firstParagraph2(purposeSrc),
1403
- responsibilities: [],
1404
- capabilities: capsMd ? headings(capsMd) : [],
1405
- contracts: [],
1406
- dependencies: [],
1407
- knownRisks: risksMd ? headings(risksMd) : [],
1408
- adrs,
1409
- owners,
1410
- outOfScope: [],
1411
- usageNotes: []
963
+ function renderGraphMermaid(graph) {
964
+ const safeIds = /* @__PURE__ */ new Map();
965
+ const used = /* @__PURE__ */ new Set();
966
+ const safe = (id) => {
967
+ const existing = safeIds.get(id);
968
+ if (existing) return existing;
969
+ let base = id.replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
970
+ if (!base) base = "n";
971
+ let candidate = base;
972
+ let i = 1;
973
+ while (used.has(candidate)) candidate = `${base}_${i++}`;
974
+ used.add(candidate);
975
+ safeIds.set(id, candidate);
976
+ return candidate;
1412
977
  };
1413
- }
1414
- function section(title, items, placeholder) {
1415
- if (items.length === 0) return `## ${title}
1416
-
1417
- _${placeholder}_
1418
- `;
1419
- return `## ${title}
1420
-
1421
- ${items.map((i) => `- ${i}`).join("\n")}
1422
- `;
1423
- }
1424
- function renderCapsuleMarkdown(c) {
1425
- const fm = [
1426
- "---",
1427
- "type: knowledge-capsule",
1428
- `system: ${c.system}`,
1429
- `version: ${c.version}`,
1430
- `updated_at: ${c.updatedAt}`,
1431
- `owner: ${c.owner}`,
1432
- `source_project: ${c.sourceProject}`,
1433
- ...c.sourceCommit ? [`source_commit: ${c.sourceCommit}`] : [],
1434
- "---"
1435
- ].join("\n");
1436
- const parts = [
1437
- fm,
1438
- "",
1439
- `# ${c.system} \u2014 Knowledge Capsule`,
1440
- "",
1441
- `## Purpose
1442
-
1443
- ${c.purpose || "_To be completed by the capsule-agent._"}
1444
- `,
1445
- section("Responsibilities", c.responsibilities, "List what this system is responsible for."),
1446
- section("Exposed Capabilities", c.capabilities, "List the capabilities this system exposes."),
1447
- section("Public Contracts", c.contracts, "List public APIs and events (e.g. `POST /orders`, `OrderCreated`)."),
1448
- section("Dependencies", c.dependencies, "List systems this one depends on."),
1449
- section("Known Risks", c.knownRisks, "List integration risks consumers should know."),
1450
- section("Relevant ADRs", c.adrs, "List decisions that affect how to integrate."),
1451
- section("Owners", c.owners, "Who owns this system."),
1452
- section("Out of Scope", c.outOfScope, "What this capsule deliberately does not cover."),
1453
- section("Usage Notes", c.usageNotes, "How consumers should integrate."),
1454
- "> Security: this capsule must never contain secrets, tokens, credentials, PII or source code.",
1455
- ""
1456
- ];
1457
- return parts.join("\n");
1458
- }
1459
- function serializeCapsuleJson(c) {
1460
- return JSON.stringify({ type: "knowledge-capsule", ...c }, null, 2) + "\n";
1461
- }
1462
- var EXTERNAL_PATH = ".kaddo/external.yml";
1463
- function loadExternalRegistry(dir) {
1464
- const p = join(dir, EXTERNAL_PATH);
1465
- if (!exists(p)) return [];
1466
- try {
1467
- const parsed = parseYaml5(readFile(p));
1468
- return Array.isArray(parsed?.external) ? parsed.external : [];
1469
- } catch {
1470
- return [];
1471
- }
1472
- }
1473
- function sectionList(md, title) {
1474
- const re = new RegExp(`^##\\s+${title}\\s*$`, "im");
1475
- const lines = md.split(/\r?\n/);
1476
- const start = lines.findIndex((l) => re.test(l));
1477
- if (start < 0) return [];
1478
- const out = [];
1479
- for (let i = start + 1; i < lines.length; i++) {
1480
- if (/^##\s+/.test(lines[i])) break;
1481
- const m = lines[i].match(/^\s*-\s+(.+?)\s*$/);
1482
- if (m && !/^_.*_$/.test(m[1])) out.push(m[1].trim());
1483
- }
1484
- return out;
1485
- }
1486
- function sectionParagraph2(md, title) {
1487
- const re = new RegExp(`^##\\s+${title}\\s*$`, "im");
1488
- const lines = md.split(/\r?\n/);
1489
- const start = lines.findIndex((l) => re.test(l));
1490
- if (start < 0) return "";
1491
- for (let i = start + 1; i < lines.length; i++) {
1492
- if (/^##\s+/.test(lines[i])) break;
1493
- const t = lines[i].trim();
1494
- if (t && !/^_.*_$/.test(t)) return t;
978
+ const escapeLabel = (s) => s.replace(/"/g, "'");
979
+ const lines = ["flowchart LR"];
980
+ for (const node of graph.nodes) {
981
+ lines.push(` ${safe(node.id)}["${escapeLabel(node.label)}"]`);
1495
982
  }
1496
- return "";
1497
- }
1498
- function parseCapsule(id, path3, md) {
1499
- const { data } = matter5(md);
1500
- const updatedAt = data.updated_at ? String(data.updated_at) : void 0;
1501
- let ageDays = null;
1502
- if (updatedAt) {
1503
- const d = Date.parse(updatedAt);
1504
- if (!Number.isNaN(d)) ageDays = Math.max(0, Math.floor((Date.now() - d) / 864e5));
983
+ if (graph.edges.length > 0) lines.push("");
984
+ for (const edge of graph.edges) {
985
+ lines.push(` ${safe(edge.from)} -->|${edge.type}| ${safe(edge.to)}`);
1505
986
  }
1506
- return {
1507
- id,
1508
- path: path3,
1509
- system: data.system ? String(data.system) : id,
1510
- owner: data.owner ? String(data.owner) : void 0,
1511
- updatedAt,
1512
- purpose: sectionParagraph2(md, "Purpose"),
1513
- capabilities: sectionList(md, "Exposed Capabilities"),
1514
- contracts: sectionList(md, "Public Contracts"),
1515
- knownRisks: sectionList(md, "Known Risks"),
1516
- ageDays
1517
- };
987
+ return lines.join("\n") + "\n";
1518
988
  }
1519
- function loadExternalCapsules(dir) {
1520
- const out = [];
1521
- for (const entry of loadExternalRegistry(dir)) {
1522
- const full = join(dir, entry.path);
1523
- if (!exists(full) || !isFile(full)) continue;
1524
- try {
1525
- out.push(parseCapsule(entry.id, entry.path, readFile(full)));
1526
- } catch {
1527
- }
989
+ var ACTIVE_WI_STATES = /* @__PURE__ */ new Set(["draft", "ready", "in-progress", "blocked"]);
990
+ function loadGraphSummary(dir) {
991
+ const p = join(dir, ".kaddo", "graph.json");
992
+ if (!exists(p)) return null;
993
+ try {
994
+ const graph = JSON.parse(readFile(p));
995
+ const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
996
+ const edges = Array.isArray(graph.edges) ? graph.edges : [];
997
+ const activeWiIds = new Set(
998
+ nodes.filter((n) => n.type === "work-item" && (!n.status || ACTIVE_WI_STATES.has(n.status))).map((n) => n.id)
999
+ );
1000
+ const connected = new Set(
1001
+ edges.filter((e) => e.type === "owns" && activeWiIds.has(e.from)).map((e) => e.from)
1002
+ );
1003
+ const scope = graph.scope === "all" ? "all" : "active";
1004
+ return {
1005
+ generatedAt: String(graph.generated_at ?? ""),
1006
+ scope,
1007
+ scopeReason: String(graph.scope_reason ?? ""),
1008
+ includedStatuses: Array.isArray(graph.included_statuses) ? graph.included_statuses : [],
1009
+ excludedStatuses: Array.isArray(graph.excluded_statuses) ? graph.excluded_statuses : [],
1010
+ nodes: nodes.length,
1011
+ edges: edges.length,
1012
+ activeWorkItemsConnectedToCode: connected.size
1013
+ };
1014
+ } catch {
1015
+ return null;
1528
1016
  }
1529
- return out;
1530
1017
  }
1531
1018
 
1532
- // ../cli/src/core/graph.ts
1019
+ // ../cli/src/core/graph-hints.ts
1533
1020
  var KNOWLEDGE3 = "knowledge";
1534
- var ACTIVE_STATUSES = ["draft", "ready", "in-progress", "blocked"];
1535
- var ALL_STATUSES = ["draft", "ready", "in-progress", "blocked", "completed"];
1536
- function scopeStatuses(scope) {
1537
- return scope === "all" ? { included: ALL_STATUSES, excluded: ["archived"] } : { included: ACTIVE_STATUSES, excluded: ["completed", "archived"] };
1538
- }
1539
- function toPosix3(p) {
1021
+ function toPosix4(p) {
1540
1022
  return p.replace(/\\/g, "/");
1541
1023
  }
1542
- function slug(s) {
1024
+ function slug2(s) {
1543
1025
  return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1544
1026
  }
1545
- function isAdr(a) {
1546
- return toPosix3(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
1027
+ function isAdr2(a) {
1028
+ return toPosix4(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
1547
1029
  }
1548
- function buildGraph(dir, config, opts = {}, now = /* @__PURE__ */ new Date()) {
1549
- const scope = opts.scope ?? "active";
1550
- const nodes = /* @__PURE__ */ new Map();
1551
- const edges = [];
1552
- const edgeKeys = /* @__PURE__ */ new Set();
1553
- const addNode = (node) => {
1554
- if (!nodes.has(node.id)) nodes.set(node.id, node);
1555
- };
1556
- const addEdge = (from, to, type) => {
1557
- const key = `${from}|${to}|${type}`;
1558
- if (edgeKeys.has(key)) return;
1559
- edgeKeys.add(key);
1560
- edges.push({ from, to, type });
1561
- };
1562
- const layerDocs = [
1563
- { id: "business:business", type: "business", label: "Business", files: ["business/business.md"] },
1564
- { id: "product:product", type: "product", label: "Product", files: ["product/product.md", "product/capabilities.md"] },
1565
- { id: "tech:tech", type: "tech", label: "Tech", files: ["tech/current-state.md", "tech/codebase.md"] },
1566
- { id: "delivery:delivery", type: "delivery", label: "Delivery", files: ["delivery/roadmap.md"] }
1567
- ];
1568
- const presentLayers = [];
1569
- for (const layer2 of layerDocs) {
1570
- const path3 = layer2.files.map((f) => `${KNOWLEDGE3}/${f}`).find((rel) => exists(join(dir, rel)));
1571
- if (path3) {
1572
- addNode({ id: layer2.id, type: layer2.type, label: layer2.label, path: path3 });
1573
- presentLayers.push(layer2.id);
1574
- }
1575
- }
1576
- for (let i = 0; i < presentLayers.length - 1; i++) {
1577
- addEdge(presentLayers[i], presentLayers[i + 1], "informs");
1578
- }
1579
- const all = discoverKnowledge(dir);
1580
- const workItems = all.filter((a) => a.isWorkItem);
1581
- const { included, excluded } = scopeStatuses(scope);
1582
- const includedSet = new Set(included);
1583
- const activeWICount = workItems.filter((a) => a.lifecycle && isActiveState(a.lifecycle)).length;
1584
- const selectedWIs = workItems.filter((a) => a.lifecycle && includedSet.has(a.lifecycle));
1585
- for (const wi of selectedWIs) {
1586
- const id = wi.id || wi.title;
1587
- const wiNodeId = `wi:${id}`;
1588
- addNode({
1589
- id: wiNodeId,
1590
- type: "work-item",
1591
- label: `${id} ${wi.title}`.trim(),
1592
- path: wi.relPath,
1593
- status: wi.lifecycle,
1594
- knowledge_level: wi.knowledgeLevel || void 0
1595
- });
1596
- for (const glob of wi.codeGlobs) {
1597
- const codeId = `code:${glob}`;
1598
- addNode({ id: codeId, type: "code-glob", label: glob });
1599
- addEdge(wiNodeId, codeId, "owns");
1600
- }
1601
- for (const cap of wi.capabilities) {
1602
- const capId = `capability:${slug(cap) || cap}`;
1603
- addNode({ id: capId, type: "capability", label: cap });
1604
- addEdge(wiNodeId, capId, "implements");
1605
- }
1606
- for (const dec of wi.decisions) {
1607
- const adrId = `adr:${dec}`;
1608
- addNode({ id: adrId, type: "decision", label: dec });
1609
- addEdge(wiNodeId, adrId, "depends_on");
1610
- }
1611
- if (wi.initiative) {
1612
- const initId = `initiative:${slug(wi.initiative) || wi.initiative}`;
1613
- addNode({ id: initId, type: "initiative", label: wi.initiative });
1614
- addEdge(wiNodeId, initId, "belongs_to");
1615
- }
1616
- if (wi.source === "roadmap" && wi.sourceId) {
1617
- const candId = `candidate:${wi.sourceId}`;
1618
- addNode({ id: candId, type: "roadmap-candidate", label: wi.sourceId });
1619
- addEdge(candId, wiNodeId, "materialized_as");
1620
- }
1621
- }
1622
- for (const adr of all.filter(isAdr)) {
1623
- const adrId = `adr:${adr.id || adr.title}`;
1624
- const referenced = nodes.has(adrId);
1625
- if (scope === "all" || referenced) {
1626
- nodes.set(adrId, {
1627
- id: adrId,
1628
- type: "decision",
1629
- label: `${adr.id} ${adr.title}`.trim() || adr.id || adr.title,
1630
- path: adr.relPath
1631
- });
1632
- for (const glob of adr.codeGlobs) {
1633
- const codeId = `code:${glob}`;
1634
- addNode({ id: codeId, type: "code-glob", label: glob });
1635
- addEdge(adrId, codeId, "governs");
1636
- }
1637
- }
1638
- }
1639
- const capsules = loadExternalRegistry(dir);
1640
- if (capsules.length > 0) {
1641
- const projId = `project:${slug(config.project.name) || "project"}`;
1642
- addNode({ id: projId, type: "project", label: config.project.name });
1643
- for (const cap of capsules) {
1644
- const capId = `capsule:${cap.id}`;
1645
- addNode({ id: capId, type: "knowledge-capsule", label: cap.id, path: cap.path });
1646
- addEdge(capId, projId, "provides_external_context");
1647
- for (const wi of selectedWIs) {
1648
- if (wi.capsules.includes(cap.id)) {
1649
- addEdge(`wi:${wi.id || wi.title}`, capId, "uses_external_knowledge");
1650
- }
1651
- }
1652
- }
1653
- }
1654
- 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.";
1655
- return {
1656
- generated_at: now.toISOString(),
1657
- project: {
1658
- name: config.project.name,
1659
- state: config.project.state,
1660
- structure: config.project.structure
1661
- },
1662
- scope,
1663
- scope_reason: scopeReason,
1664
- included_statuses: included,
1665
- excluded_statuses: excluded,
1666
- nodes: [...nodes.values()],
1667
- edges
1668
- };
1669
- }
1670
- function serializeGraphJson(graph) {
1671
- return JSON.stringify(graph, null, 2) + "\n";
1672
- }
1673
- function renderGraphMermaid(graph) {
1674
- const safeIds = /* @__PURE__ */ new Map();
1675
- const used = /* @__PURE__ */ new Set();
1676
- const safe = (id) => {
1677
- const existing = safeIds.get(id);
1678
- if (existing) return existing;
1679
- let base = id.replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
1680
- if (!base) base = "n";
1681
- let candidate = base;
1682
- let i = 1;
1683
- while (used.has(candidate)) candidate = `${base}_${i++}`;
1684
- used.add(candidate);
1685
- safeIds.set(id, candidate);
1686
- return candidate;
1687
- };
1688
- const escapeLabel = (s) => s.replace(/"/g, "'");
1689
- const lines = ["flowchart LR"];
1690
- for (const node of graph.nodes) {
1691
- lines.push(` ${safe(node.id)}["${escapeLabel(node.label)}"]`);
1692
- }
1693
- if (graph.edges.length > 0) lines.push("");
1694
- for (const edge of graph.edges) {
1695
- lines.push(` ${safe(edge.from)} -->|${edge.type}| ${safe(edge.to)}`);
1696
- }
1697
- return lines.join("\n") + "\n";
1698
- }
1699
- var ACTIVE_WI_STATES = /* @__PURE__ */ new Set(["draft", "ready", "in-progress", "blocked"]);
1700
- function loadGraphSummary(dir) {
1701
- const p = join(dir, ".kaddo", "graph.json");
1702
- if (!exists(p)) return null;
1703
- try {
1704
- const graph = JSON.parse(readFile(p));
1705
- const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
1706
- const edges = Array.isArray(graph.edges) ? graph.edges : [];
1707
- const activeWiIds = new Set(
1708
- nodes.filter((n) => n.type === "work-item" && (!n.status || ACTIVE_WI_STATES.has(n.status))).map((n) => n.id)
1709
- );
1710
- const connected = new Set(
1711
- edges.filter((e) => e.type === "owns" && activeWiIds.has(e.from)).map((e) => e.from)
1712
- );
1713
- const scope = graph.scope === "all" ? "all" : "active";
1714
- return {
1715
- generatedAt: String(graph.generated_at ?? ""),
1716
- scope,
1717
- scopeReason: String(graph.scope_reason ?? ""),
1718
- includedStatuses: Array.isArray(graph.included_statuses) ? graph.included_statuses : [],
1719
- excludedStatuses: Array.isArray(graph.excluded_statuses) ? graph.excluded_statuses : [],
1720
- nodes: nodes.length,
1721
- edges: edges.length,
1722
- activeWorkItemsConnectedToCode: connected.size
1723
- };
1724
- } catch {
1725
- return null;
1726
- }
1727
- }
1728
-
1729
- // ../cli/src/core/graph-hints.ts
1730
- var KNOWLEDGE4 = "knowledge";
1731
- function toPosix4(p) {
1732
- return p.replace(/\\/g, "/");
1733
- }
1734
- function slug2(s) {
1735
- return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1736
- }
1737
- function isAdr2(a) {
1738
- return toPosix4(a.filePath).includes("/tech/decisions/") && Boolean(a.type);
1739
- }
1740
- function capabilityHeadings(dir) {
1741
- const p = join(dir, KNOWLEDGE4, "product", "capabilities.md");
1742
- if (!exists(p)) return [];
1743
- return readFile(p).split(/\r?\n/).map((l) => l.match(/^#{2,3}\s+(.+?)\s*$/)).filter((m) => Boolean(m)).map((m) => m[1].trim()).filter((h) => !/^(summary|resumen|overview|capabilities|capacidades)$/i.test(h));
1744
- }
1745
- function humanMissing(field) {
1746
- switch (field) {
1747
- case "code":
1748
- return "code ownership";
1749
- case "capabilities":
1750
- return "linked capability";
1751
- case "decisions":
1752
- return "linked decision";
1753
- case "source":
1754
- return "roadmap link";
1755
- default:
1756
- return field;
1030
+ function capabilityHeadings(dir) {
1031
+ const p = join(dir, KNOWLEDGE3, "product", "capabilities.md");
1032
+ if (!exists(p)) return [];
1033
+ return readFile(p).split(/\r?\n/).map((l) => l.match(/^#{2,3}\s+(.+?)\s*$/)).filter((m) => Boolean(m)).map((m) => m[1].trim()).filter((h) => !/^(summary|resumen|overview|capabilities|capacidades)$/i.test(h));
1034
+ }
1035
+ function humanMissing(field) {
1036
+ switch (field) {
1037
+ case "code":
1038
+ return "code ownership";
1039
+ case "capabilities":
1040
+ return "linked capability";
1041
+ case "decisions":
1042
+ return "linked decision";
1043
+ case "source":
1044
+ return "roadmap link";
1045
+ default:
1046
+ return field;
1757
1047
  }
1758
1048
  }
1759
1049
  function buildGraphHints(dir, graph, now = /* @__PURE__ */ new Date()) {
@@ -1970,7 +1260,7 @@ function loadGraphHints(dir) {
1970
1260
  }
1971
1261
 
1972
1262
  // ../cli/src/services/installed-skills.ts
1973
- import matter6 from "gray-matter";
1263
+ import matter5 from "gray-matter";
1974
1264
  var SKILLS_DIR = join("knowledge", "skills");
1975
1265
  function discoverInstalledSkills(dir) {
1976
1266
  const base = join(dir, SKILLS_DIR);
@@ -1980,7 +1270,7 @@ function discoverInstalledSkills(dir) {
1980
1270
  const skillFile = join(base, entry, "skill.md");
1981
1271
  if (!isFile(skillFile)) continue;
1982
1272
  try {
1983
- const { data } = matter6(readFile(skillFile));
1273
+ const { data } = matter5(readFile(skillFile));
1984
1274
  if (data.type && String(data.type) !== "skill") continue;
1985
1275
  const id = String(data.id ?? entry);
1986
1276
  out.push({
@@ -2001,815 +1291,1829 @@ function skillGroupCounts(skills) {
2001
1291
  return counts;
2002
1292
  }
2003
1293
 
2004
- // ../cli/src/core/context-pack.ts
2005
- var CONTEXT_PACK_VERSION = "1";
2006
- var ARCH_DIR2 = "knowledge";
2007
- function readScanJson(dir) {
2008
- const scanPath = join(dir, ".kaddo", "scan.json");
2009
- if (!exists(scanPath)) return null;
2010
- try {
2011
- return JSON.parse(readFile(scanPath));
2012
- } catch {
2013
- return null;
2014
- }
1294
+ // ../cli/src/services/mapped-modules.ts
1295
+ import { parse as parseYaml5 } from "yaml";
1296
+ var DESCRIPTOR_PATH = ".kaddo/modules.yml";
1297
+ function toStringArray(value) {
1298
+ return Array.isArray(value) ? value.map((v) => String(v)).filter(Boolean) : [];
2015
1299
  }
2016
- function firstParagraph3(markdown) {
2017
- const body = matter7(markdown).content.trim();
2018
- const para = body.split("\n\n").map((p) => p.trim()).find((p) => p && !p.startsWith("#"));
2019
- return para ?? "";
1300
+ function moduleArtifactCoverage(dir, id) {
1301
+ const base = join(dir, "knowledge", "tech", "modules", id);
1302
+ return {
1303
+ moduleDesign: exists(join(base, "module-design.md")),
1304
+ stack: exists(join(base, "stack.md")),
1305
+ security: exists(join(base, "security.md")),
1306
+ standards: exists(join(base, "standards.md")),
1307
+ diagrams: exists(join(base, "diagrams")),
1308
+ adrs: exists(join(base, "adrs"))
1309
+ };
2020
1310
  }
2021
- function readMarkdownSummary(dir, file) {
2022
- const p = join(dir, ARCH_DIR2, file);
2023
- if (!exists(p)) return null;
1311
+ function loadMappedModules(dir) {
1312
+ const path3 = join(dir, DESCRIPTOR_PATH);
1313
+ if (!exists(path3)) return [];
1314
+ let parsed;
2024
1315
  try {
2025
- return firstParagraph3(readFile(p));
1316
+ parsed = parseYaml5(readFile(path3));
2026
1317
  } catch {
2027
- return null;
1318
+ return [];
2028
1319
  }
2029
- }
2030
- function recommendedAgentsForState(state) {
2031
- switch (state) {
2032
- case "new":
2033
- return ["roadmap-agent", "architecture-agent"];
2034
- case "legacy":
2035
- return ["legacy-agent", "architecture-agent", "capability-agent"];
2036
- case "pre-ai":
2037
- default:
2038
- return ["capability-agent", "architecture-agent", "roadmap-agent"];
1320
+ const raw = Array.isArray(parsed?.modules) ? parsed.modules : [];
1321
+ const modules = [];
1322
+ for (const entry of raw) {
1323
+ if (!entry || typeof entry !== "object") continue;
1324
+ const m = entry;
1325
+ const id = typeof m.id === "string" ? m.id : "";
1326
+ if (!id) continue;
1327
+ modules.push({
1328
+ id,
1329
+ name: typeof m.name === "string" ? m.name : void 0,
1330
+ repoPath: typeof m.repoPath === "string" ? m.repoPath : "",
1331
+ type: typeof m.type === "string" ? m.type : void 0,
1332
+ mainTechnology: typeof m.mainTechnology === "string" ? m.mainTechnology : void 0,
1333
+ owner: typeof m.owner === "string" ? m.owner : void 0,
1334
+ capabilities: toStringArray(m.capabilities),
1335
+ code: toStringArray(m.code),
1336
+ status: typeof m.status === "string" ? m.status : void 0,
1337
+ artifacts: moduleArtifactCoverage(dir, id)
1338
+ });
2039
1339
  }
1340
+ return modules;
2040
1341
  }
2041
- function nextStepsForState(state) {
2042
- switch (state) {
2043
- case "new":
2044
- return [
2045
- "Use roadmap-agent to shape an initial roadmap.",
2046
- "Use architecture-agent to outline the intended architecture."
2047
- ];
2048
- case "legacy":
2049
- return [
2050
- "Use legacy-agent to surface risks and unknowns before changing code.",
2051
- "Use architecture-agent to reconstruct the current architecture.",
2052
- "Use capability-agent to map existing capabilities."
2053
- ];
2054
- case "pre-ai":
2055
- default:
2056
- return [
2057
- "Use capability-agent to extract system capabilities.",
2058
- "Use architecture-agent to reconstruct the current architecture.",
2059
- "Use roadmap-agent to propose roadmap candidates."
2060
- ];
2061
- }
1342
+ function presentArtifacts(coverage) {
1343
+ const present = [];
1344
+ if (coverage.moduleDesign) present.push("module-design");
1345
+ if (coverage.stack) present.push("stack");
1346
+ if (coverage.security) present.push("security");
1347
+ if (coverage.standards) present.push("standards");
1348
+ if (coverage.diagrams) present.push("diagrams");
1349
+ if (coverage.adrs) present.push("adrs");
1350
+ return present;
2062
1351
  }
2063
- var LLM_INSTRUCTIONS = [
2064
- "Use this context pack as the project baseline.",
2065
- "Do not write code yet.",
2066
- "First extract: system capabilities, architecture notes, risks, open questions and roadmap candidates.",
2067
- "This pack is deterministic CLI output \u2014 it does not interpret the system. That is your job."
2068
- ];
2069
- var OPERATING_RULES = [
2070
- "**Never run `git commit`, `git push` or `git merge` without explicit human confirmation.**",
2071
- "Never push or merge automatically \u2014 ever. Suggest a Conventional Commit message and wait.",
2072
- "When implementing a Work Item, create a branch FIRST (per the project Git strategy, `.kaddo/git.yml`, default `feature/<id>-<slug>`). Never work directly on `main`.",
2073
- "After significant changes run `kaddo scan`, `kaddo owners suggest` and `kaddo guard`, and update the affected knowledge (ADR / capabilities / current-state).",
2074
- "Kaddo itself never calls an LLM and never runs git \u2014 every git action is the human\u2019s."
2075
- ];
2076
- function toContextWorkItem(a) {
2077
- return {
2078
- id: a.id,
2079
- type: a.type,
2080
- title: a.title,
2081
- status: a.status,
2082
- lifecycle: lifecycleStateOf({ status: a.status, filePath: a.filePath }),
2083
- knowledgeLevel: a.knowledgeLevel,
2084
- domains: a.domains
2085
- };
1352
+
1353
+ // ../cli/src/core/knowledge-discovery.ts
1354
+ var KNOWLEDGE4 = "knowledge";
1355
+ var CONSOLIDATED_TYPE = {
1356
+ Business: "business",
1357
+ Product: "product",
1358
+ Tech: "codebase"
1359
+ };
1360
+ var STRUCTURED_TYPES = {
1361
+ Business: /* @__PURE__ */ new Set(["problem", "users", "value-proposition", "business-rules", "constraints", "glossary"]),
1362
+ Product: /* @__PURE__ */ new Set(["product-brief", "capabilities"]),
1363
+ Tech: /* @__PURE__ */ new Set([
1364
+ "current-state",
1365
+ "architecture-notes",
1366
+ "decision-candidates",
1367
+ "quality-attributes",
1368
+ "stack",
1369
+ "standards",
1370
+ "security",
1371
+ "git-strategy",
1372
+ "module-design",
1373
+ "adr",
1374
+ "decision"
1375
+ ]),
1376
+ Delivery: /* @__PURE__ */ new Set([])
1377
+ };
1378
+ var WORK_ITEM_TYPES = /* @__PURE__ */ new Set([
1379
+ "work-item",
1380
+ "feature",
1381
+ "bugfix",
1382
+ "hotfix",
1383
+ "spike"
1384
+ ]);
1385
+ function layerForType(type) {
1386
+ if (!type) return null;
1387
+ if (type === CONSOLIDATED_TYPE.Business || STRUCTURED_TYPES.Business.has(type)) return "Business";
1388
+ if (type === CONSOLIDATED_TYPE.Product || STRUCTURED_TYPES.Product.has(type)) return "Product";
1389
+ if (type === CONSOLIDATED_TYPE.Tech || STRUCTURED_TYPES.Tech.has(type) || type === "knowledge")
1390
+ return "Tech";
1391
+ if (type === "roadmap" || WORK_ITEM_TYPES.has(type)) return "Delivery";
1392
+ return null;
2086
1393
  }
2087
- function toContextArtifact(a) {
2088
- return { id: a.id, type: a.type, title: a.title, summary: a.summary, codeGlobs: a.codeGlobs };
1394
+ function layerFromPath(filePath) {
1395
+ const p = filePath.replace(/\\/g, "/");
1396
+ if (p.includes(`/${KNOWLEDGE4}/business/`)) return "Business";
1397
+ if (p.includes(`/${KNOWLEDGE4}/product/`)) return "Product";
1398
+ if (p.includes(`/${KNOWLEDGE4}/tech/`)) return "Tech";
1399
+ if (p.includes(`/${KNOWLEDGE4}/delivery/`)) return "Delivery";
1400
+ return null;
2089
1401
  }
2090
- function isDeliveryWorkItem(a) {
2091
- return a.filePath.replace(/\\/g, "/").includes("/delivery/work-items/") && Boolean(a.type);
1402
+ function basename(p) {
1403
+ return p.replace(/\\/g, "/").split("/").pop() ?? p;
2092
1404
  }
2093
- function buildContextPack(dir, config, now = /* @__PURE__ */ new Date()) {
2094
- const missing = [];
2095
- const scanJson = readScanJson(dir);
2096
- if (!scanJson) {
2097
- missing.push("Scan baseline missing. Run `kaddo scan` for better context.");
2098
- }
2099
- const detected = scanJson?.detected ?? {};
2100
- const inventoryAvailable = exists(join(dir, ARCH_DIR2, "inventory.md"));
2101
- if (!inventoryAvailable) {
2102
- missing.push("No technical inventory found. Run `kaddo scan` to generate it.");
2103
- }
2104
- const knowledgeSummary = readMarkdownSummary(dir, "knowledge.md") ?? "";
2105
- if (!knowledgeSummary) {
2106
- missing.push("No project knowledge summary found yet.");
2107
- }
2108
- const roadmapSummary = readMarkdownSummary(dir, "delivery/roadmap.md") ?? "";
2109
- if (!roadmapSummary) {
2110
- missing.push("No roadmap baseline found.");
2111
- }
2112
- const allArtifacts = discoverKnowledge(dir);
2113
- const workItems = allArtifacts.filter(
2114
- (a) => isDeliveryWorkItem(a) && isActiveState(lifecycleStateOf({ status: a.status, filePath: a.filePath }))
2115
- );
2116
- if (workItems.length === 0) {
2117
- missing.push("No work items found.");
2118
- }
2119
- const state = config.project.state;
2120
- const roadmapPath = join(dir, ARCH_DIR2, "delivery", "roadmap.md");
2121
- const materialized = allArtifacts.filter(
2122
- (a) => a.filePath.replace(/\\/g, "/").includes("/delivery/work-items/") && Boolean(a.type)
2123
- ).length;
2124
- const roadmap = roadmapStats(exists(roadmapPath) ? readFile(roadmapPath) : null, materialized);
2125
- const deliveryMix = {};
2126
- for (const a of allArtifacts) {
2127
- const p = a.filePath.replace(/\\/g, "/");
2128
- if (!a.type || !p.includes("/delivery/work-items/")) continue;
2129
- if (!isActiveState(lifecycleStateOf({ status: a.status, filePath: a.filePath }))) continue;
2130
- deliveryMix[a.type] = (deliveryMix[a.type] ?? 0) + 1;
2131
- }
2132
- const mappedModules = loadMappedModules(dir);
2133
- const layers = knowledgeLayers(dir);
2134
- const allWorkItems = allArtifacts.filter((a) => a.isWorkItem);
2135
- const wiWithOwnership = allWorkItems.filter((a) => a.codeGlobs.length > 0).length;
2136
- const phase = assessPhase({
2137
- layers,
2138
- roadmap,
2139
- workItems: {
2140
- total: allWorkItems.length,
2141
- byState: lifecycleCounts(
2142
- allWorkItems.map((a) => lifecycleStateOf({ status: a.status, filePath: a.filePath }))
2143
- ),
2144
- items: allWorkItems.map((a) => ({
2145
- id: a.id || a.title,
2146
- title: a.title,
2147
- lifecycle: lifecycleStateOf({ status: a.status, filePath: a.filePath })
2148
- }))
2149
- },
2150
- ownership: {
2151
- workItemsTotal: allWorkItems.length,
2152
- workItemsWithOwnership: wiWithOwnership,
2153
- workItemsMissingOwnership: allWorkItems.length - wiWithOwnership
1405
+ function discoverLayers(dir) {
1406
+ const acc = {
1407
+ Business: blank(),
1408
+ Product: blank(),
1409
+ Tech: blank(),
1410
+ Delivery: blank()
1411
+ };
1412
+ const archDir = join(dir, KNOWLEDGE4);
1413
+ const artifacts = exists(archDir) ? readArtifacts(archDir) : [];
1414
+ for (const a of artifacts) {
1415
+ const type = a.type;
1416
+ const layer2 = layerForType(type) ?? layerFromPath(a.filePath);
1417
+ if (!layer2) continue;
1418
+ const slot = acc[layer2];
1419
+ slot.detected.add(basename(a.filePath));
1420
+ if (layer2 === "Delivery") {
1421
+ if (type === "roadmap" || basename(a.filePath) === "roadmap.md") slot.hasRoadmap = true;
1422
+ if (WORK_ITEM_TYPES.has(type) || a.filePath.replace(/\\/g, "/").includes("/delivery/work-items/")) {
1423
+ if (a.filePath.replace(/\\/g, "/").includes("/delivery/work-items/")) slot.hasWorkItem = true;
1424
+ }
1425
+ continue;
2154
1426
  }
2155
- });
2156
- return {
2157
- version: CONTEXT_PACK_VERSION,
2158
- generatedAt: now.toISOString(),
2159
- project: {
2160
- name: config.project.name,
2161
- state,
2162
- teamSize: config.team.size,
2163
- structure: config.project.structure,
2164
- language: languageLabel(projectLanguage(config))
2165
- },
2166
- scan: {
2167
- available: scanJson !== null,
2168
- languages: detected.languages ?? [],
2169
- frameworks: detected.frameworks ?? [],
2170
- packageManagers: detected.packageManagers ?? [],
2171
- sourceDirectories: detected.sourceDirectories ?? [],
2172
- migrationDirectories: detected.migrationDirectories ?? [],
2173
- contractFiles: detected.contractFiles ?? [],
2174
- infrastructureFiles: detected.infrastructureFiles ?? []
2175
- },
2176
- knowledge: {
2177
- summary: knowledgeSummary,
2178
- roadmapSummary,
2179
- inventoryAvailable,
2180
- workItems: workItems.map(toContextWorkItem),
2181
- artifacts: allArtifacts.filter((a) => a.codeGlobs.length > 0).map(toContextArtifact)
2182
- },
2183
- layers,
2184
- roadmap,
2185
- phase,
2186
- deliveryMix,
2187
- external: loadExternalCapsules(dir),
2188
- graph: loadGraphSummary(dir),
2189
- graphHints: loadGraphHints(dir),
2190
- skills: discoverInstalledSkills(dir).map((s) => s.id),
2191
- mappedModules,
2192
- missing,
2193
- // VS-052: the handoff is driven by the REAL phase, not project.state, so the pack never
2194
- // contradicts the Current Phase block above it.
2195
- handoff: {
2196
- recommendedAgents: phase.recommendedAgents.length > 0 ? phase.recommendedAgents : recommendedAgentsForState(state),
2197
- nextSteps: phase.nextStep ? [phase.nextStep] : nextStepsForState(state),
2198
- instructions: phase.llmInstructions.length > 0 ? phase.llmInstructions : LLM_INSTRUCTIONS,
2199
- operatingRules: OPERATING_RULES
1427
+ if (type === CONSOLIDATED_TYPE[layer2]) slot.consolidated = true;
1428
+ else if (STRUCTURED_TYPES[layer2].has(type)) slot.structured = true;
1429
+ else if (!type) {
1430
+ slot.consolidated = true;
2200
1431
  }
1432
+ if (type === "adr" || type === "decision") slot.hasDecision = true;
1433
+ }
1434
+ if (existsDirWithMd(join(dir, KNOWLEDGE4, "tech", "decisions"))) acc.Tech.structured = true;
1435
+ return ["Business", "Product", "Tech", "Delivery"].map((layer2) => ({
1436
+ layer: layer2,
1437
+ status: statusFor(layer2, acc[layer2]),
1438
+ detected: [...acc[layer2].detected].sort()
1439
+ }));
1440
+ }
1441
+ function blank() {
1442
+ return {
1443
+ consolidated: false,
1444
+ structured: false,
1445
+ hasRoadmap: false,
1446
+ hasWorkItem: false,
1447
+ hasDecision: false,
1448
+ detected: /* @__PURE__ */ new Set()
2201
1449
  };
2202
1450
  }
2203
- function serializeContextPackJson(pack) {
2204
- return JSON.stringify(pack, null, 2) + "\n";
1451
+ function existsDirWithMd(d) {
1452
+ if (!exists(d)) return false;
1453
+ try {
1454
+ return readDir(d).some((e) => e.endsWith(".md") && isFile(join(d, e)));
1455
+ } catch {
1456
+ return false;
1457
+ }
2205
1458
  }
2206
-
2207
- // ../cli/src/templates/context-pack-template.ts
2208
- function renderContextPack(pack) {
2209
- const { project, scan, knowledge, missing, handoff } = pack;
2210
- const parts = [];
2211
- parts.push("# Kaddo Context Pack\n");
2212
- parts.push(
2213
- "> Generated by `kaddo context`. Deterministic CLI output \u2014 not an interpretation of the system.\n"
2214
- );
2215
- parts.push("## Operating Rules (read first)\n");
2216
- parts.push(handoff.operatingRules.map((r) => `- ${r}`).join("\n") + "\n");
2217
- parts.push("## Project Metadata\n");
2218
- parts.push(
2219
- [
2220
- `- Name: ${project.name}`,
2221
- `- State: ${project.state}`,
2222
- `- Team size: ${project.teamSize}`,
2223
- `- Structure: ${project.structure}`,
2224
- `- Language: ${project.language}`
2225
- ].join("\n") + "\n"
2226
- );
2227
- parts.push("## Current Phase\n");
2228
- parts.push(`Phase: ${pack.phase.phase}
2229
- `);
2230
- if (pack.phase.reasons.length > 0) {
2231
- parts.push("Reason:\n");
2232
- parts.push(pack.phase.reasons.map((r) => `- ${r}`).join("\n") + "\n");
1459
+ function statusFor(layer2, a) {
1460
+ if (layer2 === "Delivery") {
1461
+ if (a.hasWorkItem) return "Traceable";
1462
+ if (a.hasRoadmap) return "Partial";
1463
+ return "Missing";
2233
1464
  }
2234
- if (pack.phase.recommendedAgents.length > 0) {
2235
- parts.push(`Recommended next: ${pack.phase.recommendedAgents.join(", ")}
2236
- `);
1465
+ if (a.structured) return "Structured";
1466
+ if (a.consolidated) return "Consolidated";
1467
+ return "Missing";
1468
+ }
1469
+
1470
+ // ../cli/src/core/layers.ts
1471
+ function knowledgeLayers(dir) {
1472
+ return discoverLayers(dir);
1473
+ }
1474
+ function renderLayersMarkdown(layers) {
1475
+ const lines = [];
1476
+ for (const { layer: layer2, status, detected } of layers) {
1477
+ lines.push(`### ${layer2} \u2014 ${status}`);
1478
+ if (detected.length > 0) for (const d of detected) lines.push(`- \u2713 ${d}`);
1479
+ else lines.push("- \u2717 none");
1480
+ lines.push("");
2237
1481
  }
2238
- if (pack.phase.nextStep) {
2239
- parts.push(`Next step: ${pack.phase.nextStep}
2240
- `);
1482
+ return lines.join("\n").trimEnd();
1483
+ }
1484
+
1485
+ // ../cli/src/core/roadmap.ts
1486
+ var INITIATIVE_RE = /^#{2,4}\s+(RM-[\w.-]+)\s*[:\-–]?\s*(.*)$/;
1487
+ var FIELD_RE = /^\*\*(.+?):\*\*\s*(.*)$/;
1488
+ var CANDIDATE_RE = /^\s*[-*]\s+(WI-[\w-]+)\s*[:\-–]\s*(.+)$/;
1489
+ var PROPERTY_RE = /^\s+[-*]\s+([\w /]+?)\s*:\s*(.+)$/;
1490
+ var BULLET_RE = /^\s*[-*]\s+(.+)$/;
1491
+ function normalizeKey(key) {
1492
+ return key.trim().toLowerCase();
1493
+ }
1494
+ function splitInitiatives(markdown) {
1495
+ const blocks = [];
1496
+ let current = null;
1497
+ for (const line of markdown.split(/\r?\n/)) {
1498
+ const m = line.match(INITIATIVE_RE);
1499
+ if (m) {
1500
+ if (current) blocks.push(current);
1501
+ current = { initiative: { id: m[1], title: m[2].trim() || void 0 }, lines: [] };
1502
+ continue;
1503
+ }
1504
+ if (current) current.lines.push(line);
2241
1505
  }
2242
- parts.push("## Knowledge Layers\n");
2243
- parts.push(
2244
- "Project knowledge is organized in four layers: **Business \u2192 Product \u2192 Tech \u2192 Delivery**.\n"
2245
- );
2246
- const maturity = pack.layers.map((l) => `${l.layer}: ${l.status}`).join(" \xB7 ");
2247
- parts.push(`Knowledge maturity \u2014 ${maturity}
2248
- `);
2249
- parts.push(renderLayersMarkdown(pack.layers) + "\n");
2250
- parts.push("## Technical Inventory\n");
2251
- if (scan.available) {
2252
- const lines = [
2253
- `- Language: ${scan.languages.join(", ") || "unknown"}`,
2254
- `- Framework: ${scan.frameworks.join(", ") || "unknown"}`,
2255
- `- Package manager: ${scan.packageManagers.join(", ") || "unknown"}`
2256
- ];
2257
- if (scan.sourceDirectories.length > 0) {
2258
- lines.push("- Source directories:");
2259
- scan.sourceDirectories.forEach((d) => lines.push(` - ${d}`));
1506
+ if (current) blocks.push(current);
1507
+ return blocks;
1508
+ }
1509
+ function parseBlock(block) {
1510
+ const meta = { relatedCapabilities: [], dependencies: [], openQuestions: [] };
1511
+ const rawCandidates = [];
1512
+ let listField = null;
1513
+ let inCandidateSection = false;
1514
+ let current = null;
1515
+ const flush = () => {
1516
+ if (current) {
1517
+ current.rawMarkdown = current.rawMarkdown.trimEnd();
1518
+ rawCandidates.push(current);
1519
+ current = null;
2260
1520
  }
2261
- if (scan.migrationDirectories.length > 0) {
2262
- lines.push("- Migration directories:");
2263
- scan.migrationDirectories.forEach((d) => lines.push(` - ${d}`));
1521
+ };
1522
+ for (const line of block.lines) {
1523
+ const candMatch = line.match(CANDIDATE_RE);
1524
+ if (candMatch && inCandidateSection) {
1525
+ flush();
1526
+ current = { id: candMatch[1], title: candMatch[2].trim(), rawMarkdown: line.trim() + "\n" };
1527
+ listField = null;
1528
+ continue;
2264
1529
  }
2265
- if (scan.contractFiles.length > 0) {
2266
- lines.push("- Contracts:");
2267
- scan.contractFiles.forEach((f) => lines.push(` - ${f}`));
1530
+ if (current) {
1531
+ const propMatch = line.match(PROPERTY_RE);
1532
+ if (propMatch) {
1533
+ current.rawMarkdown += line.trim() + "\n";
1534
+ const key = normalizeKey(propMatch[1]);
1535
+ const value = propMatch[2].trim();
1536
+ if (key === "type") current.type = value;
1537
+ else if (key === "suggested knowledge level" || key === "knowledge level")
1538
+ current.suggestedKnowledgeLevel = value;
1539
+ else if (key === "expected value") current.expectedValue = value;
1540
+ else if (key === "notes") current.notes = value;
1541
+ continue;
1542
+ }
1543
+ if (line.trim() === "") {
1544
+ flush();
1545
+ continue;
1546
+ }
2268
1547
  }
2269
- if (scan.infrastructureFiles.length > 0) {
2270
- lines.push("- Infrastructure:");
2271
- scan.infrastructureFiles.forEach((f) => lines.push(` - ${f}`));
1548
+ const fieldMatch = line.match(FIELD_RE);
1549
+ if (fieldMatch) {
1550
+ const key = normalizeKey(fieldMatch[1]);
1551
+ const value = fieldMatch[2].trim();
1552
+ listField = null;
1553
+ inCandidateSection = false;
1554
+ if (key === "candidate work items") {
1555
+ inCandidateSection = true;
1556
+ } else if (key === "impact") {
1557
+ meta.impact = value || void 0;
1558
+ } else if (key === "risk") {
1559
+ meta.risk = value || void 0;
1560
+ } else if (key === "project area / domain" || key === "domain" || key === "project area") {
1561
+ meta.domain = value || void 0;
1562
+ } else if (key === "related capabilities") {
1563
+ listField = "relatedCapabilities";
1564
+ if (value) meta.relatedCapabilities.push(value);
1565
+ } else if (key === "dependencies") {
1566
+ listField = "dependencies";
1567
+ if (value) meta.dependencies.push(value);
1568
+ } else if (key === "open questions") {
1569
+ listField = "openQuestions";
1570
+ if (value) meta.openQuestions.push(value);
1571
+ }
1572
+ continue;
2272
1573
  }
2273
- parts.push(lines.join("\n") + "\n");
2274
- } else {
2275
- parts.push("Scan baseline missing. Run `kaddo scan` for better context.\n");
2276
- }
2277
- parts.push("## Current Knowledge\n");
2278
- parts.push((knowledge.summary || "No project knowledge summary found yet.") + "\n");
2279
- parts.push("## Roadmap\n");
2280
- if (pack.roadmap.present) {
2281
- parts.push(
2282
- [
2283
- `- Roadmap candidates: ${pack.roadmap.candidates}`,
2284
- `- Materialized work items: ${pack.roadmap.materialized}`,
2285
- `- Remaining candidates: ${pack.roadmap.remaining}`
2286
- ].join("\n") + "\n"
2287
- );
2288
- if (pack.roadmap.remaining > 0) {
2289
- parts.push(
2290
- "Candidates are not yet Work Items. Materialize them with `kaddo create --from roadmap`.\n"
2291
- );
1574
+ if (listField) {
1575
+ const bulletMatch = line.match(BULLET_RE);
1576
+ if (bulletMatch) {
1577
+ const item = bulletMatch[1].trim().replace(/^candidate\s*:\s*/i, "");
1578
+ meta[listField].push(item);
1579
+ continue;
1580
+ }
1581
+ if (line.trim() === "") {
1582
+ listField = null;
1583
+ continue;
1584
+ }
2292
1585
  }
2293
1586
  }
2294
- parts.push((knowledge.roadmapSummary || "No roadmap baseline found.") + "\n");
2295
- parts.push("## Active Work Items\n");
2296
- if (knowledge.workItems.length > 0) {
2297
- const lines = knowledge.workItems.map((wi) => {
2298
- const level = wi.knowledgeLevel ? ` [${wi.knowledgeLevel}]` : "";
2299
- const status = wi.lifecycle ? ` (${wi.lifecycle})` : wi.status ? ` (${wi.status})` : "";
2300
- const domains = wi.domains.length > 0 ? ` \xB7 domains: ${wi.domains.join(", ")}` : "";
2301
- return `- ${wi.id || wi.title} [${wi.type}]${level}${status} \u2014 ${wi.title}${domains}`;
1587
+ flush();
1588
+ return rawCandidates.map((c) => ({
1589
+ ...c,
1590
+ initiative: block.initiative.id || block.initiative.title ? { ...block.initiative } : void 0,
1591
+ relatedCapabilities: meta.relatedCapabilities.length ? [...meta.relatedCapabilities] : void 0,
1592
+ domain: meta.domain,
1593
+ impact: meta.impact,
1594
+ risk: meta.risk,
1595
+ dependencies: meta.dependencies.length ? [...meta.dependencies] : void 0,
1596
+ openQuestions: meta.openQuestions.length ? [...meta.openQuestions] : void 0
1597
+ }));
1598
+ }
1599
+ var WI_ID_RE = /\bWI-[A-Za-z0-9-]*\d/;
1600
+ var HEADING_RE = /^#{2,4}\s+(.*)$/;
1601
+ var FLEX_BULLET_RE = /^\s*[-*]\s+(?:\[[ xX]?\]\s+)?(WI-[A-Za-z0-9-]*\d)\b[\s:.\-–)]*\s*(.*)$/;
1602
+ var SEPARATOR_CELL_RE = /^:?-{2,}:?$/;
1603
+ function splitRow(line) {
1604
+ return line.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim());
1605
+ }
1606
+ function isSeparatorRow(cells) {
1607
+ return cells.length > 0 && cells.every((c) => SEPARATOR_CELL_RE.test(c) || c === "");
1608
+ }
1609
+ function initiativeFromHeading(text2) {
1610
+ const rm = text2.match(/^(RM-[\w.-]+)\s*[:\-–]?\s*(.*)$/);
1611
+ if (rm) return { id: rm[1], title: rm[2].trim() || void 0 };
1612
+ return { title: text2.trim() || void 0 };
1613
+ }
1614
+ function extractDeps(cell) {
1615
+ if (!cell) return void 0;
1616
+ const ids = cell.match(/WI-[A-Za-z0-9-]*\d/g);
1617
+ return ids && ids.length ? [...new Set(ids)] : void 0;
1618
+ }
1619
+ function parseFlexible(markdown) {
1620
+ const out = [];
1621
+ const seen = /* @__PURE__ */ new Set();
1622
+ let initiative;
1623
+ let cols = null;
1624
+ const push = (id, title, deps, raw) => {
1625
+ if (seen.has(id)) return;
1626
+ seen.add(id);
1627
+ out.push({
1628
+ id,
1629
+ title: title.trim() || id,
1630
+ dependencies: deps,
1631
+ initiative: initiative?.id || initiative?.title ? { ...initiative } : void 0,
1632
+ rawMarkdown: raw.trim()
2302
1633
  });
2303
- parts.push(lines.join("\n") + "\n");
2304
- } else {
2305
- parts.push("No active work items found.\n");
2306
- }
2307
- const mixEntries = Object.entries(pack.deliveryMix).sort((a, b) => b[1] - a[1]);
2308
- if (mixEntries.length > 0) {
2309
- parts.push("## Delivery Mix\n");
2310
- parts.push("Active Work Items by type:\n");
2311
- const label = (t) => {
2312
- const c = t.charAt(0).toUpperCase() + t.slice(1);
2313
- return c.endsWith("s") ? c : `${c}s`;
2314
- };
2315
- parts.push(mixEntries.map(([t, n]) => `- ${label(t)}: ${n}`).join("\n") + "\n");
1634
+ };
1635
+ for (const line of markdown.split(/\r?\n/)) {
1636
+ const h = line.match(HEADING_RE);
1637
+ if (h) {
1638
+ initiative = initiativeFromHeading(h[1]);
1639
+ cols = null;
1640
+ continue;
1641
+ }
1642
+ if (line.includes("|")) {
1643
+ const cells = splitRow(line);
1644
+ if (isSeparatorRow(cells)) continue;
1645
+ const lower = cells.map((c) => c.toLowerCase());
1646
+ const idIdx = lower.findIndex((c) => c === "id" || c === "wi" || c === "work item id");
1647
+ if (idIdx >= 0 && !WI_ID_RE.test(line)) {
1648
+ cols = {
1649
+ id: idIdx,
1650
+ title: lower.findIndex((c) => /work item|title|item|name|description/.test(c)),
1651
+ deps: lower.findIndex((c) => /depend/.test(c))
1652
+ };
1653
+ continue;
1654
+ }
1655
+ if (WI_ID_RE.test(line)) {
1656
+ let idCell = cols && cols.id >= 0 ? cells[cols.id] : cells.find((c) => /^WI-/.test(c));
1657
+ const idMatch = (idCell ?? line).match(/WI-[A-Za-z0-9-]*\d/);
1658
+ if (!idMatch) continue;
1659
+ const id = idMatch[0];
1660
+ const title = cols && cols.title >= 0 ? cells[cols.title] ?? "" : cells.find((c) => !/^WI-/.test(c) && c) ?? "";
1661
+ const deps = cols && cols.deps >= 0 ? extractDeps(cells[cols.deps]) : void 0;
1662
+ push(id, title, deps, line);
1663
+ continue;
1664
+ }
1665
+ continue;
1666
+ }
1667
+ const b = line.match(FLEX_BULLET_RE);
1668
+ if (b) {
1669
+ push(b[1], b[2] ?? "", void 0, line);
1670
+ continue;
1671
+ }
2316
1672
  }
2317
- parts.push("## Artifacts and Ownership\n");
2318
- if (knowledge.artifacts.length > 0) {
2319
- const lines = knowledge.artifacts.map(
2320
- (a) => `- ${a.id || a.title} [${a.type}] owns: ${a.codeGlobs.join(", ")}`
2321
- );
2322
- parts.push(lines.join("\n") + "\n");
2323
- } else {
2324
- parts.push("No artifacts declare code ownership yet.\n");
1673
+ return out;
1674
+ }
1675
+ function parseRoadmapCandidates(markdown) {
1676
+ const strict = splitInitiatives(markdown).flatMap(parseBlock);
1677
+ if (strict.length > 0) return strict;
1678
+ return parseFlexible(markdown);
1679
+ }
1680
+ function roadmapStats(markdown, materialized) {
1681
+ if (markdown == null) return { present: false, candidates: 0, materialized, remaining: 0 };
1682
+ const candidates = parseRoadmapCandidates(markdown).length;
1683
+ return { present: true, candidates, materialized, remaining: Math.max(0, candidates - materialized) };
1684
+ }
1685
+
1686
+ // ../cli/src/core/project-explain.ts
1687
+ var ARCH_DIR2 = "knowledge";
1688
+ function normalizeTitle(t) {
1689
+ return t.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, " ").trim();
1690
+ }
1691
+ function findDuplicateWorkItems(items) {
1692
+ const groups = [];
1693
+ const seen = /* @__PURE__ */ new Set();
1694
+ const bucket = (key, reason, pred) => {
1695
+ const matched = items.filter(pred);
1696
+ if (matched.length > 1) {
1697
+ const id = `${reason}:${key}:${matched.map((m) => m.id).sort().join(",")}`;
1698
+ if (!seen.has(id)) {
1699
+ seen.add(id);
1700
+ groups.push({ reason, items: matched.map((m) => ({ id: m.id, title: m.title })) });
1701
+ }
1702
+ }
1703
+ };
1704
+ for (const sid of new Set(items.map((i) => i.sourceId).filter(Boolean))) {
1705
+ bucket(sid, `same source candidate (${sid})`, (i) => i.sourceId === sid);
2325
1706
  }
2326
- if (pack.mappedModules.length > 0) {
2327
- parts.push("## Mapped Modules\n");
2328
- parts.push("This project has mapped modules registered in `.kaddo/modules.yml`.\n");
2329
- const header = "| Module | Type | Repo path | Owner | Capabilities | Artifacts |";
2330
- const sep = "|---|---|---|---|---|---|";
2331
- const rows = pack.mappedModules.map((m) => {
2332
- const caps = m.capabilities.length > 0 ? m.capabilities.join(", ") : "\u2014";
2333
- const arts = presentArtifacts(m.artifacts);
2334
- return `| ${m.id} | ${m.type ?? "\u2014"} | ${m.repoPath || "\u2014"} | ${m.owner ?? "\u2014"} | ${caps} | ${arts.length > 0 ? arts.join(", ") : "none"} |`;
2335
- });
2336
- parts.push([header, sep, ...rows].join("\n") + "\n");
2337
- parts.push(
2338
- "Note: Kaddo does not scan secondary repositories during `context`. Mapped modules come from `.kaddo/modules.yml` and module artifacts only.\n"
2339
- );
1707
+ for (const nt of new Set(items.map((i) => normalizeTitle(i.title)).filter(Boolean))) {
1708
+ bucket(nt, "same normalized title", (i) => normalizeTitle(i.title) === nt);
2340
1709
  }
2341
- if (pack.external.length > 0) {
2342
- parts.push("## External Knowledge\n");
2343
- parts.push(
2344
- "Imported Knowledge Capsules \u2014 minimal context about external systems (not their source).\n"
2345
- );
2346
- for (const cap of pack.external) {
2347
- const lines = [`### ${cap.system}`, ""];
2348
- if (cap.purpose) lines.push(`- Purpose: ${cap.purpose}`);
2349
- if (cap.capabilities.length) lines.push(`- Capabilities: ${cap.capabilities.join(", ")}`);
2350
- if (cap.contracts.length) lines.push(`- Contracts: ${cap.contracts.join(", ")}`);
2351
- if (cap.owner) lines.push(`- Owner: ${cap.owner}`);
2352
- if (cap.knownRisks.length) lines.push(`- Known risks: ${cap.knownRisks.join("; ")}`);
2353
- parts.push(lines.join("\n") + "\n");
2354
- }
1710
+ return groups;
1711
+ }
1712
+ function first(values) {
1713
+ if (Array.isArray(values)) {
1714
+ const v = values.find((x) => typeof x === "string" && x);
1715
+ return v ? String(v) : void 0;
2355
1716
  }
2356
- if (pack.graph) {
2357
- parts.push("## Knowledge Graph\n");
2358
- parts.push(
2359
- [
2360
- "- Available: yes",
2361
- `- Scope: ${pack.graph.scope}`,
2362
- `- Nodes: ${pack.graph.nodes}`,
2363
- `- Edges: ${pack.graph.edges}`,
2364
- ...pack.graphHints ? [`- Quality: ${pack.graphHints.quality}`] : [],
2365
- `- Active Work Items connected to code: ${pack.graph.activeWorkItemsConnectedToCode}`,
2366
- ...pack.graph.scopeReason ? [`- Reason: ${pack.graph.scopeReason}`] : []
2367
- ].join("\n") + "\n"
2368
- );
2369
- parts.push("Full graph: `.kaddo/graph.json` / `.kaddo/graph.mmd` (run `kaddo graph export` to refresh).\n");
1717
+ return typeof values === "string" && values ? values : void 0;
1718
+ }
1719
+ function toStringArray2(value) {
1720
+ return Array.isArray(value) ? value.map((v) => String(v)).filter(Boolean) : [];
1721
+ }
1722
+ function loadScan(dir) {
1723
+ const scanPath = join(dir, ".kaddo", "scan.json");
1724
+ if (!exists(scanPath)) return null;
1725
+ try {
1726
+ const parsed = JSON.parse(readFile(scanPath));
1727
+ const detected = parsed.detected ?? {};
1728
+ return {
1729
+ language: first(detected.languages),
1730
+ framework: first(detected.frameworks),
1731
+ packageManager: first(detected.packageManagers),
1732
+ sourceDirectories: toStringArray2(detected.sourceDirectories),
1733
+ migrationDirectories: toStringArray2(detected.migrationDirectories),
1734
+ contractFiles: toStringArray2(detected.contractFiles),
1735
+ infrastructureFiles: toStringArray2(detected.infrastructureFiles)
1736
+ };
1737
+ } catch {
1738
+ return null;
2370
1739
  }
2371
- if (pack.graphHints && pack.graphHints.totalHints > 0) {
2372
- parts.push("## Graph Hints\n");
2373
- parts.push(
2374
- [
2375
- `Graph relationship quality: ${pack.graphHints.quality}`,
2376
- `Active hints: ${pack.graphHints.activeWorkItemHints}`,
2377
- "Suggested agent: graph-agent"
2378
- ].join("\n") + "\n"
2379
- );
2380
- const shown = pack.graphHints.messages.slice(0, 3);
2381
- parts.push(shown.map((m) => `- ${m}`).join("\n") + "\n");
2382
- if (pack.graphHints.totalHints > shown.length) {
2383
- parts.push(`(+${pack.graphHints.totalHints - shown.length} more in \`.kaddo/graph-hints.md\`)
2384
- `);
1740
+ }
1741
+ function hasAgents(dir) {
1742
+ const agentsDir = join(dir, ARCH_DIR2, "agents");
1743
+ if (!exists(agentsDir)) return false;
1744
+ function hasAgentMd(d) {
1745
+ for (const e of readDir(d)) {
1746
+ const full = join(d, e);
1747
+ if (isFile(full)) {
1748
+ if (e.endsWith("-agent.md")) return true;
1749
+ } else if (hasAgentMd(full)) {
1750
+ return true;
1751
+ }
2385
1752
  }
1753
+ return false;
2386
1754
  }
2387
- if (pack.skills.length > 0) {
2388
- parts.push("## Skills\n");
2389
- parts.push("Available reusable skills (agents apply these; content is not inlined):\n");
2390
- parts.push(pack.skills.map((s) => `- ${s}`).join("\n") + "\n");
2391
- parts.push("Read full skill definitions in `knowledge/skills/` or via the Kaddo MCP server (`kaddo://skills`).\n");
2392
- }
2393
- parts.push("## Missing Context\n");
2394
- if (missing.length > 0) {
2395
- parts.push(missing.map((m) => `- ${m}`).join("\n") + "\n");
2396
- } else {
2397
- parts.push("_None \u2014 all expected context is present._\n");
1755
+ return hasAgentMd(agentsDir);
1756
+ }
1757
+ function buildProjectExplanation(dir) {
1758
+ const config = loadConfig(dir);
1759
+ const project = {
1760
+ name: config?.project.name ?? "unknown",
1761
+ state: config?.project.state ?? "unknown",
1762
+ teamSize: config?.team.size ?? "unknown",
1763
+ structure: config?.project.structure ?? "unknown",
1764
+ language: config ? languageLabel(projectLanguage(config)) : "English"
1765
+ };
1766
+ const scan = loadScan(dir);
1767
+ const stack = scan ? {
1768
+ language: scan.language,
1769
+ framework: scan.framework,
1770
+ packageManager: scan.packageManager,
1771
+ sourceDirectories: scan.sourceDirectories,
1772
+ migrationDirectories: scan.migrationDirectories,
1773
+ contractFiles: scan.contractFiles,
1774
+ infrastructureFiles: scan.infrastructureFiles
1775
+ } : null;
1776
+ const layers = knowledgeLayers(dir);
1777
+ const layerStatus = (name) => layers.find((l) => l.layer === name)?.status ?? "Missing";
1778
+ const knowledge = {
1779
+ hasScan: scan !== null,
1780
+ hasInventory: exists(join(dir, ARCH_DIR2, "inventory.md")),
1781
+ hasContextPack: exists(join(dir, ".kaddo", "context-pack.md")),
1782
+ hasUnderstand: exists(join(dir, ".kaddo", "understand.md")),
1783
+ hasCapabilities: layerStatus("Product") !== "Missing",
1784
+ hasArchitecture: layerStatus("Tech") !== "Missing",
1785
+ hasRoadmap: layerStatus("Delivery") !== "Missing",
1786
+ hasAgents: hasAgents(dir)
1787
+ };
1788
+ const workItemArtifacts = discoverWorkItems(dir);
1789
+ const items = workItemArtifacts.map((a) => ({
1790
+ id: a.id || a.title,
1791
+ title: a.title,
1792
+ type: a.type,
1793
+ status: a.status,
1794
+ lifecycle: lifecycleStateOf({ status: a.status, filePath: a.filePath }),
1795
+ initiative: a.initiative,
1796
+ knowledgeLevel: a.knowledgeLevel,
1797
+ hasOwnership: a.codeGlobs.length > 0,
1798
+ domains: a.domains
1799
+ }));
1800
+ const byState = lifecycleCounts(items.map((i) => i.lifecycle));
1801
+ const byType = {};
1802
+ for (const i of items) {
1803
+ const t = i.type || "unknown";
1804
+ byType[t] = (byType[t] ?? 0) + 1;
2398
1805
  }
2399
- parts.push("## Recommended Agent Handoff\n");
2400
- parts.push(`Recommended next for the **${pack.phase.phase}** phase:
2401
- `);
2402
- parts.push(handoff.recommendedAgents.map((a, i) => `${i + 1}. ${a}`).join("\n") + "\n");
2403
- if (handoff.nextSteps.length > 0) {
2404
- parts.push("Next step:\n");
2405
- parts.push(handoff.nextSteps.map((s) => `- ${s}`).join("\n") + "\n");
1806
+ const initiativeMap = /* @__PURE__ */ new Map();
1807
+ for (const i of items) {
1808
+ const name = i.initiative || "Unassigned";
1809
+ if (!initiativeMap.has(name)) initiativeMap.set(name, emptyLifecycleCounts());
1810
+ initiativeMap.get(name)[i.lifecycle]++;
2406
1811
  }
2407
- parts.push("## Instructions for the LLM\n");
2408
- parts.push(handoff.instructions.map((i) => `- ${i}`).join("\n") + "\n");
2409
- return parts.join("\n");
1812
+ const initiatives = [...initiativeMap.entries()].map(([name, states]) => ({ name, states })).sort((a, b) => a.name.localeCompare(b.name));
1813
+ const workItems = {
1814
+ total: items.length,
1815
+ inProgress: items.filter((i) => i.lifecycle === "in-progress").length,
1816
+ done: items.filter((i) => i.status === "done").length,
1817
+ cancelled: items.filter((i) => i.status === "cancelled").length,
1818
+ byState,
1819
+ byType,
1820
+ initiatives,
1821
+ items
1822
+ };
1823
+ const withOwnership = items.filter((i) => i.hasOwnership).length;
1824
+ const ownership = {
1825
+ workItemsTotal: items.length,
1826
+ workItemsWithOwnership: withOwnership,
1827
+ workItemsMissingOwnership: items.length - withOwnership
1828
+ };
1829
+ const domains = [...new Set(workItemArtifacts.flatMap((a) => a.domains))].filter(Boolean);
1830
+ const duplicateWorkItems = findDuplicateWorkItems(
1831
+ workItemArtifacts.map((a) => ({ id: a.id || a.title, title: a.title, sourceId: a.sourceId }))
1832
+ );
1833
+ const roadmapPath = join(dir, ARCH_DIR2, "delivery", "roadmap.md");
1834
+ const roadmapMd = exists(roadmapPath) ? readFile(roadmapPath) : null;
1835
+ const roadmap = roadmapStats(roadmapMd, items.length);
1836
+ const mappedModules = loadMappedModules(dir);
1837
+ const missingKnowledge = [];
1838
+ if (!knowledge.hasScan) missingKnowledge.push("Scan baseline (.kaddo/scan.json)");
1839
+ if (!knowledge.hasContextPack) missingKnowledge.push("Context pack (.kaddo/context-pack.md)");
1840
+ if (!knowledge.hasInventory) missingKnowledge.push("Inventory (knowledge/inventory.md)");
1841
+ if (!knowledge.hasCapabilities) missingKnowledge.push("Product knowledge (knowledge/product/)");
1842
+ if (!knowledge.hasArchitecture) missingKnowledge.push("Tech knowledge (knowledge/tech/)");
1843
+ if (!knowledge.hasRoadmap) missingKnowledge.push("Roadmap (knowledge/delivery/roadmap.md)");
1844
+ if (!knowledge.hasAgents) missingKnowledge.push("Agents (knowledge/agents/)");
1845
+ if (items.length === 0) missingKnowledge.push("Work items (knowledge/delivery/work-items/)");
1846
+ const suggestedNextSteps = [];
1847
+ if (!knowledge.hasScan) {
1848
+ suggestedNextSteps.push("Run `kaddo scan` to detect the technical stack.");
1849
+ } else if (!knowledge.hasContextPack) {
1850
+ suggestedNextSteps.push("Run `kaddo context` to prepare an LLM context pack.");
1851
+ }
1852
+ if (!knowledge.hasAgents) {
1853
+ suggestedNextSteps.push("Run `kaddo add agents` to install knowledge agents.");
1854
+ }
1855
+ if (!knowledge.hasCapabilities) {
1856
+ suggestedNextSteps.push("Use capability-agent to generate knowledge/product/capabilities.md.");
1857
+ }
1858
+ if (!knowledge.hasArchitecture) {
1859
+ suggestedNextSteps.push("Use architecture-agent to generate knowledge/tech/current-state.md.");
1860
+ }
1861
+ if (!knowledge.hasRoadmap) {
1862
+ suggestedNextSteps.push("Use roadmap-agent to generate knowledge/delivery/roadmap.md.");
1863
+ } else if (roadmap.remaining > 0) {
1864
+ suggestedNextSteps.push(
1865
+ `Materialize ${roadmap.remaining} roadmap candidate(s) with \`kaddo create --from roadmap\`.`
1866
+ );
1867
+ }
1868
+ if (items.length === 0 && !roadmap.present) {
1869
+ suggestedNextSteps.push("Create your first Work Item with `kaddo create`.");
1870
+ } else if (ownership.workItemsMissingOwnership > 0) {
1871
+ suggestedNextSteps.push(
1872
+ "Run `kaddo owners suggest` for Work Items without code ownership."
1873
+ );
1874
+ }
1875
+ return {
1876
+ project,
1877
+ stack,
1878
+ knowledge,
1879
+ workItems,
1880
+ ownership,
1881
+ domains,
1882
+ duplicateWorkItems,
1883
+ externalCapsules: loadExternalCapsules(dir),
1884
+ graph: loadGraphSummary(dir),
1885
+ graphHints: loadGraphHints(dir),
1886
+ skills: (() => {
1887
+ const installed = discoverInstalledSkills(dir);
1888
+ return { total: installed.length, byGroup: skillGroupCounts(installed) };
1889
+ })(),
1890
+ layers,
1891
+ roadmap,
1892
+ mappedModules,
1893
+ missingKnowledge,
1894
+ suggestedNextSteps
1895
+ };
1896
+ }
1897
+ function stateLabel(state) {
1898
+ return state === "pre-ai" ? "pre-ai" : state;
1899
+ }
1900
+ function typeLabel(type) {
1901
+ const cap = type.charAt(0).toUpperCase() + type.slice(1);
1902
+ return cap.endsWith("s") ? cap : `${cap}s`;
1903
+ }
1904
+ var LIFECYCLE_LABEL = {
1905
+ draft: "Draft",
1906
+ ready: "Ready",
1907
+ "in-progress": "In Progress",
1908
+ blocked: "Blocked",
1909
+ completed: "Completed",
1910
+ archived: "Archived"
1911
+ };
1912
+ function renderExplanationHuman(exp) {
1913
+ const lines = [];
1914
+ lines.push("# Project Explanation");
1915
+ lines.push("");
1916
+ lines.push("## Project");
1917
+ lines.push(`- Name: ${exp.project.name}`);
1918
+ lines.push(`- State: ${stateLabel(exp.project.state)}`);
1919
+ lines.push(`- Team: ${exp.project.teamSize}`);
1920
+ lines.push(`- Structure: ${exp.project.structure}`);
1921
+ lines.push(`- Project language: ${exp.project.language}`);
1922
+ lines.push("");
1923
+ lines.push("## Knowledge Layers");
1924
+ lines.push(renderLayersMarkdown(exp.layers));
1925
+ lines.push("");
1926
+ if (exp.stack) {
1927
+ lines.push("## Detected Stack");
1928
+ if (exp.stack.language) lines.push(`- Language: ${exp.stack.language}`);
1929
+ if (exp.stack.framework) lines.push(`- Framework: ${exp.stack.framework}`);
1930
+ if (exp.stack.packageManager) lines.push(`- Package manager: ${exp.stack.packageManager}`);
1931
+ if (exp.stack.sourceDirectories.length > 0)
1932
+ lines.push(`- Source: ${exp.stack.sourceDirectories.join(", ")}`);
1933
+ if (exp.stack.migrationDirectories.length > 0)
1934
+ lines.push(`- Migrations: ${exp.stack.migrationDirectories.join(", ")}`);
1935
+ if (exp.stack.infrastructureFiles.length > 0)
1936
+ lines.push(`- Infrastructure: ${exp.stack.infrastructureFiles.join(", ")}`);
1937
+ lines.push("");
1938
+ }
1939
+ const ls = (name) => exp.layers.find((l) => l.layer === name)?.status ?? "Missing";
1940
+ lines.push("## Knowledge Status");
1941
+ lines.push(`- Inventory: ${exp.knowledge.hasInventory ? "available" : "missing"}`);
1942
+ lines.push(`- Context pack: ${exp.knowledge.hasContextPack ? "available" : "missing"}`);
1943
+ lines.push(`- Business: ${ls("Business")}`);
1944
+ lines.push(`- Product: ${ls("Product")}`);
1945
+ lines.push(`- Tech: ${ls("Tech")}`);
1946
+ lines.push(`- Delivery: ${ls("Delivery")}`);
1947
+ lines.push(`- Agents: ${exp.knowledge.hasAgents ? "available" : "missing"}`);
1948
+ if (exp.roadmap.present) {
1949
+ lines.push(`- Roadmap candidates: ${exp.roadmap.candidates}`);
1950
+ lines.push(`- Materialized work items: ${exp.roadmap.materialized}`);
1951
+ if (exp.roadmap.remaining > 0)
1952
+ lines.push(`- Remaining candidates: ${exp.roadmap.remaining}`);
1953
+ } else {
1954
+ lines.push(`- Work items: ${exp.workItems.total}`);
1955
+ }
1956
+ lines.push(
1957
+ `- Ownership coverage: ${exp.ownership.workItemsWithOwnership}/${exp.ownership.workItemsTotal} work items`
1958
+ );
1959
+ lines.push("");
1960
+ if (exp.workItems.total > 0) {
1961
+ lines.push("## Work Items");
1962
+ for (const s of LIFECYCLE_STATES) {
1963
+ lines.push(`- ${LIFECYCLE_LABEL[s]}: ${exp.workItems.byState[s]}`);
1964
+ }
1965
+ lines.push("");
1966
+ const typeEntries = Object.entries(exp.workItems.byType).sort((a, b) => b[1] - a[1]);
1967
+ if (typeEntries.length > 0) {
1968
+ lines.push("## Work Items by Type");
1969
+ for (const [t, n] of typeEntries) lines.push(`- ${typeLabel(t)}: ${n}`);
1970
+ lines.push("");
1971
+ }
1972
+ const grouped = exp.workItems.initiatives.filter(
1973
+ (g) => LIFECYCLE_STATES.some((s) => g.states[s] > 0)
1974
+ );
1975
+ if (grouped.length > 0) {
1976
+ lines.push("## Work Items by Initiative");
1977
+ for (const g of grouped) {
1978
+ const parts = LIFECYCLE_STATES.filter((s) => g.states[s] > 0).map(
1979
+ (s) => `${LIFECYCLE_LABEL[s]}: ${g.states[s]}`
1980
+ );
1981
+ lines.push(`- ${g.name} \u2014 ${parts.join(" \xB7 ")}`);
1982
+ }
1983
+ lines.push("");
1984
+ }
1985
+ }
1986
+ const active = exp.workItems.items.filter((i) => i.lifecycle === "in-progress");
1987
+ if (active.length > 0) {
1988
+ lines.push("## In Progress");
1989
+ for (const wi of active) {
1990
+ const level = wi.knowledgeLevel ? ` [${wi.knowledgeLevel}]` : "";
1991
+ const owned = wi.hasOwnership ? " \u25CF" : " \u25CB";
1992
+ lines.push(`-${owned} ${wi.id}${level} \u2014 ${wi.title}`);
1993
+ }
1994
+ lines.push("");
1995
+ }
1996
+ if (exp.domains.length > 0) {
1997
+ lines.push("## Domains");
1998
+ lines.push(`- ${exp.domains.join(", ")}`);
1999
+ lines.push("");
2000
+ }
2001
+ lines.push("## Mapped Modules");
2002
+ if (exp.mappedModules.length > 0) {
2003
+ for (const m of exp.mappedModules) {
2004
+ const owner = m.owner ? ` \u2014 owner: ${m.owner}` : "";
2005
+ lines.push(`- ${m.id} \u2014 ${m.type ?? "unknown"} \u2014 ${m.repoPath || "\u2014"}${owner}`);
2006
+ }
2007
+ lines.push("");
2008
+ lines.push("## Module Artifact Coverage");
2009
+ for (const m of exp.mappedModules) {
2010
+ const present = presentArtifacts(m.artifacts);
2011
+ lines.push(`- ${m.id}: ${present.length > 0 ? present.join(", ") : "none"}`);
2012
+ }
2013
+ lines.push("");
2014
+ } else {
2015
+ lines.push("- Mapped modules: 0");
2016
+ lines.push("");
2017
+ }
2018
+ if (exp.externalCapsules.length > 0) {
2019
+ lines.push(`## External Knowledge Capsules: ${exp.externalCapsules.length}`);
2020
+ for (const cap of exp.externalCapsules) {
2021
+ const owner = cap.owner ? ` \u2014 owner: ${cap.owner}` : "";
2022
+ lines.push(`- ${cap.system}${owner}`);
2023
+ if (cap.ageDays !== null && cap.ageDays >= 90) {
2024
+ lines.push(` \u26A0 capsule last updated ${cap.ageDays} days ago \u2014 it may be stale.`);
2025
+ }
2026
+ }
2027
+ lines.push("");
2028
+ }
2029
+ if (exp.graph) {
2030
+ lines.push("## Knowledge Graph");
2031
+ lines.push(`- Scope: ${exp.graph.scope}`);
2032
+ lines.push(`- Nodes: ${exp.graph.nodes}`);
2033
+ lines.push(`- Edges: ${exp.graph.edges}`);
2034
+ if (exp.graphHints) {
2035
+ lines.push(`- Quality: ${exp.graphHints.quality}`);
2036
+ lines.push(`- Hints: ${exp.graphHints.totalHints}`);
2037
+ }
2038
+ if (exp.graph.scopeReason) lines.push(`- Reason: ${exp.graph.scopeReason}`);
2039
+ if (exp.graph.generatedAt) lines.push(`- Last exported: ${exp.graph.generatedAt}`);
2040
+ if (exp.graph.scope === "active" && exp.graphHints?.quality === "empty") {
2041
+ lines.push("- Tip: Run `kaddo graph export --scope all` to include completed Work Items");
2042
+ }
2043
+ lines.push("");
2044
+ }
2045
+ if (exp.skills.total > 0) {
2046
+ lines.push(`## Skills installed: ${exp.skills.total}`);
2047
+ const groups = Object.entries(exp.skills.byGroup).sort((a, b) => a[0].localeCompare(b[0]));
2048
+ if (groups.length > 0) {
2049
+ lines.push("Groups:");
2050
+ for (const [g, n] of groups) lines.push(`- ${g}: ${n}`);
2051
+ }
2052
+ lines.push("");
2053
+ }
2054
+ if (exp.missingKnowledge.length > 0) {
2055
+ lines.push("## Missing Knowledge");
2056
+ for (const m of exp.missingKnowledge) lines.push(`- ${m}`);
2057
+ lines.push("");
2058
+ }
2059
+ if (exp.duplicateWorkItems.length > 0) {
2060
+ lines.push("## Possible Duplicate Work Items");
2061
+ for (const g of exp.duplicateWorkItems) {
2062
+ lines.push(`- ${g.reason}:`);
2063
+ for (const i of g.items) lines.push(` - ${i.id} \u2014 ${i.title}`);
2064
+ }
2065
+ lines.push("Review before continuing (non-blocking).");
2066
+ lines.push("");
2067
+ }
2068
+ const assessment = assessPhase(exp);
2069
+ lines.push("## Phase");
2070
+ lines.push(`- Phase: ${assessment.phase}`);
2071
+ if (assessment.reasons.length > 0) {
2072
+ lines.push("- Reason:");
2073
+ for (const r of assessment.reasons) lines.push(` - ${r}`);
2074
+ }
2075
+ if (assessment.nextStep) lines.push(`- Next step: ${assessment.nextStep}`);
2076
+ lines.push("");
2077
+ if (exp.suggestedNextSteps.length > 0) {
2078
+ lines.push("## Suggested Next Steps");
2079
+ exp.suggestedNextSteps.forEach((s, i) => lines.push(`${i + 1}. ${s}`));
2080
+ lines.push("");
2081
+ }
2082
+ return lines.join("\n").trimEnd() + "\n";
2083
+ }
2084
+ function renderExplanationAgent(exp) {
2085
+ const { mappedModules, ...rest } = exp;
2086
+ return JSON.stringify({ ...rest, mapped_modules: mappedModules }, null, 2) + "\n";
2087
+ }
2088
+
2089
+ // ../cli/src/core/impact-report.ts
2090
+ var BROAD_GLOB = /^[^/*]+\/\*\*$/;
2091
+ function levelFromRatio(r) {
2092
+ if (r >= 0.9) return "Very High";
2093
+ if (r >= 0.7) return "High";
2094
+ if (r >= 0.4) return "Medium";
2095
+ return "Low";
2096
+ }
2097
+ function hasSection(body, re) {
2098
+ return body.split(/\r?\n/).some((l) => /^#{1,6}\s+/.test(l) && re.test(l));
2099
+ }
2100
+ function buildImpactReport(dir, opts = {}, now = /* @__PURE__ */ new Date()) {
2101
+ const exp = buildProjectExplanation(dir);
2102
+ const wis = discoverWorkItems(dir);
2103
+ const total = wis.length;
2104
+ let withOwnership = 0;
2105
+ let withSource = 0;
2106
+ let withInitiative = 0;
2107
+ let withLevel = 0;
2108
+ let withAcceptance = 0;
2109
+ let withDoD = 0;
2110
+ let connectedRoadmap = 0;
2111
+ const globCounts = /* @__PURE__ */ new Map();
2112
+ for (const wi of wis) {
2113
+ if (wi.codeGlobs.length > 0) withOwnership++;
2114
+ const hasSource = Boolean(wi.sourceId) || wi.source === "roadmap";
2115
+ if (hasSource) withSource++;
2116
+ if (hasSource || wi.initiative) connectedRoadmap++;
2117
+ if (wi.initiative) withInitiative++;
2118
+ if (wi.knowledgeLevel) withLevel++;
2119
+ for (const g2 of wi.codeGlobs) globCounts.set(g2, (globCounts.get(g2) ?? 0) + 1);
2120
+ try {
2121
+ const body = matter6(readFile(wi.filePath)).content;
2122
+ if (hasSection(body, /acceptance/i)) withAcceptance++;
2123
+ if (hasSection(body, /definition of done|^#{1,6}\s*done\b/i)) withDoD++;
2124
+ } catch {
2125
+ }
2126
+ }
2127
+ const ownedCodePaths = [...globCounts.values()].reduce((a, b) => a + b, 0);
2128
+ const broadGlobs = [...globCounts.keys()].filter((g2) => BROAD_GLOB.test(g2)).length;
2129
+ const ownershipOverlaps = [...globCounts.values()].filter((n) => n > 1).length;
2130
+ let g;
2131
+ if (opts.scope) {
2132
+ const config = loadConfig(dir);
2133
+ if (config) {
2134
+ const graph = buildGraph(dir, config, { scope: opts.scope }, now);
2135
+ const hints = buildGraphHints(dir, graph, now);
2136
+ g = { available: true, scope: graph.scope, scopeReason: graph.scope_reason, nodes: graph.nodes.length, edges: graph.edges.length, quality: hints.quality, hints: hints.summary.hints, generatedAt: graph.generated_at };
2137
+ } else {
2138
+ g = { available: false, scope: opts.scope, scopeReason: "", nodes: 0, edges: 0, quality: "unknown", hints: 0, generatedAt: "" };
2139
+ }
2140
+ } else if (exp.graph) {
2141
+ g = { available: true, scope: exp.graph.scope, scopeReason: exp.graph.scopeReason, nodes: exp.graph.nodes, edges: exp.graph.edges, quality: exp.graphHints?.quality ?? "unknown", hints: exp.graphHints?.totalHints ?? 0, generatedAt: exp.graph.generatedAt };
2142
+ } else {
2143
+ g = { available: false, scope: "n/a", scopeReason: "", nodes: 0, edges: 0, quality: "unknown", hints: 0, generatedAt: "" };
2144
+ }
2145
+ const layerStatus = (name) => exp.layers.find((l) => l.layer === name)?.status ?? "Missing";
2146
+ const byState = exp.workItems.byState;
2147
+ const skills = discoverInstalledSkills(dir);
2148
+ const knowledge_health = {
2149
+ Business: layerStatus("Business"),
2150
+ Product: layerStatus("Product"),
2151
+ Tech: layerStatus("Tech"),
2152
+ Delivery: layerStatus("Delivery"),
2153
+ Inventory: exp.knowledge.hasInventory ? "available" : "missing",
2154
+ "Context pack": exp.knowledge.hasContextPack ? "available" : "missing",
2155
+ Agents: exp.knowledge.hasAgents ? "available" : "missing",
2156
+ Skills: skills.length > 0 ? "available" : "missing"
2157
+ };
2158
+ const knowledge_coverage = [
2159
+ { label: "Work Items with ownership", have: withOwnership, total },
2160
+ { label: "Work Items with source", have: withSource, total },
2161
+ { label: "Work Items with initiative", have: withInitiative, total },
2162
+ { label: "Work Items with acceptance criteria", have: withAcceptance, total },
2163
+ { label: "Work Items with Definition of Done", have: withDoD, total },
2164
+ { label: "Work Items with knowledge level", have: withLevel, total }
2165
+ ];
2166
+ const coverageRatio = (have) => total > 0 ? have / total : 0;
2167
+ const deliveryTraceable = ["Traceable", "Structured", "Consolidated"].includes(layerStatus("Delivery"));
2168
+ const ctxReasons = [];
2169
+ let ctxScore = 0;
2170
+ if (["Consolidated", "Structured"].includes(layerStatus("Business"))) {
2171
+ ctxScore++;
2172
+ ctxReasons.push("Business knowledge is present.");
2173
+ }
2174
+ if (["Consolidated", "Structured"].includes(layerStatus("Product"))) {
2175
+ ctxScore++;
2176
+ ctxReasons.push("Product knowledge is present.");
2177
+ }
2178
+ if (layerStatus("Tech") !== "Missing") {
2179
+ ctxScore++;
2180
+ ctxReasons.push("Tech knowledge is structured.");
2181
+ }
2182
+ if (deliveryTraceable) {
2183
+ ctxScore++;
2184
+ ctxReasons.push("Delivery is traceable.");
2185
+ }
2186
+ if (exp.knowledge.hasContextPack) {
2187
+ ctxScore++;
2188
+ ctxReasons.push("Context pack is available.");
2189
+ }
2190
+ if (g.available && g.quality !== "empty") {
2191
+ ctxScore++;
2192
+ ctxReasons.push(`Graph quality is ${g.quality}.`);
2193
+ }
2194
+ const ctxLevel = ctxScore >= 6 ? "Very High" : ctxScore >= 4 ? "High" : ctxScore >= 2 ? "Medium" : "Low";
2195
+ const ambiguityRatio = total > 0 ? (withAcceptance + withDoD + withSource) / (3 * total) : 0;
2196
+ const aiReady = exp.knowledge.hasContextPack && g.available && g.quality !== "empty" && skills.length > 0 && deliveryTraceable ? "High" : exp.knowledge.hasContextPack && g.available && g.quality !== "empty" ? "Medium" : "Low";
2197
+ const onboarding = levelFromRatio(
2198
+ [layerStatus("Business"), layerStatus("Product"), layerStatus("Tech"), layerStatus("Delivery")].filter((s) => s !== "Missing").length / 4 * (exp.knowledge.hasContextPack ? 1 : 0.6)
2199
+ );
2200
+ const deliveryTrace = levelFromRatio(
2201
+ coverageRatio(withOwnership) * 0.5 + (g.available && g.edges > 0 ? 1 : 0) * 0.25 + (exp.roadmap.present ? 1 : 0) * 0.25
2202
+ );
2203
+ const impact_signals = {
2204
+ ambiguity_reduction: levelFromRatio(ambiguityRatio),
2205
+ drift_prevention: withOwnership > 0 ? "Active" : "Limited",
2206
+ onboarding_support: onboarding,
2207
+ delivery_traceability: deliveryTrace,
2208
+ ai_context_readiness: aiReady,
2209
+ maintenance_readiness: levelFromRatio((coverageRatio(withOwnership) + (deliveryTraceable ? 1 : 0)) / 2)
2210
+ };
2211
+ let score = null;
2212
+ if (total > 0) {
2213
+ const healthPts = [layerStatus("Business"), layerStatus("Product"), layerStatus("Tech"), layerStatus("Delivery")].filter((s) => s !== "Missing").length / 4 * 20;
2214
+ const covAvg = knowledge_coverage.reduce((a, c) => a + coverageRatio(c.have), 0) / knowledge_coverage.length;
2215
+ const coveragePts = covAvg * 20;
2216
+ const ownPts = coverageRatio(withOwnership) * 15;
2217
+ const traceBlend = (exp.roadmap.candidates > 0 ? exp.roadmap.materialized / exp.roadmap.candidates : 1) * 0.5 + coverageRatio(withOwnership) * 0.5;
2218
+ const tracePts = traceBlend * 20;
2219
+ const qualityMap = { good: 1, partial: 0.6, sparse: 0.3, empty: 0, unknown: 0 };
2220
+ const graphPts = (g.available ? qualityMap[g.quality] ?? 0 : 0) * 15;
2221
+ const ctxPts = { Low: 0.25, Medium: 0.5, High: 0.8, "Very High": 1 }[ctxLevel] * 10;
2222
+ score = Math.round(healthPts + coveragePts + ownPts + tracePts + graphPts + ctxPts);
2223
+ }
2224
+ const actions = [];
2225
+ if (!g.available) actions.push("Run `kaddo graph export --scope all` to inspect full traceability.");
2226
+ else if (g.scope === "active" && g.quality === "empty") actions.push("Run `kaddo graph export --scope all` to include completed Work Items.");
2227
+ if (exp.roadmap.remaining > 0) actions.push(`Materialize the ${exp.roadmap.remaining} remaining roadmap candidate(s) with \`kaddo create --from roadmap\`.`);
2228
+ if (total > 0 && withOwnership < total) actions.push("Add code ownership to Work Items without `code:` globs (`kaddo owners suggest`).");
2229
+ if (total > 0 && withInitiative < total) actions.push("Add an initiative to Work Items missing one.");
2230
+ if (g.available && g.hints > 0) actions.push("Review graph hints (`kaddo://graph-hints` or `.kaddo/graph-hints.md`).");
2231
+ if (skills.length === 0) actions.push("Install reusable skills with `kaddo add skills`.");
2232
+ actions.push("Run `kaddo guard` before committing to catch knowledge drift.");
2233
+ const summary = [];
2234
+ if (total > 0) summary.push(`${withOwnership}/${total} Work Items include code ownership.`);
2235
+ if (g.available) summary.push(`The knowledge graph is ${g.quality} with ${g.nodes} nodes and ${g.edges} edges (${g.scope} scope).`);
2236
+ else summary.push("The knowledge graph has not been exported yet.");
2237
+ if (g.available) summary.push(g.hints === 0 ? "No graph hints are currently open." : `${g.hints} graph hint(s) are open.`);
2238
+ if (exp.roadmap.present) summary.push(`Delivery: ${exp.roadmap.materialized} materialized, ${exp.roadmap.remaining} roadmap candidate(s) remaining.`);
2239
+ summary.push(`Context readiness: ${ctxLevel}.`);
2240
+ return {
2241
+ generated_at: now.toISOString(),
2242
+ project: exp.project.name,
2243
+ scope: g.scope,
2244
+ executive_summary: summary,
2245
+ knowledge_health,
2246
+ knowledge_coverage,
2247
+ ownership_coverage: {
2248
+ coverage_percent: total > 0 ? Math.round(withOwnership / total * 100) : 0,
2249
+ work_items_with_ownership: `${withOwnership}/${total}`,
2250
+ owned_code_paths: ownedCodePaths,
2251
+ broad_globs: broadGlobs,
2252
+ ownership_overlaps: ownershipOverlaps
2253
+ },
2254
+ traceability: {
2255
+ roadmap_candidates: exp.roadmap.candidates,
2256
+ materialized_work_items: exp.roadmap.materialized,
2257
+ remaining_candidates: exp.roadmap.remaining,
2258
+ completed_work_items: byState.completed,
2259
+ work_items_connected_to_roadmap: `${connectedRoadmap}/${total}`,
2260
+ work_items_connected_to_code: `${withOwnership}/${total}`,
2261
+ graph_quality: g.available ? g.quality : "not available",
2262
+ graph_nodes: g.available ? g.nodes : null,
2263
+ graph_edges: g.available ? g.edges : null,
2264
+ graph_hints: g.available ? g.hints : null
2265
+ },
2266
+ context_readiness: { level: ctxLevel, reasons: ctxReasons },
2267
+ work_item_readiness: {
2268
+ draft: byState.draft,
2269
+ ready: byState.ready,
2270
+ in_progress: byState["in-progress"],
2271
+ blocked: byState.blocked,
2272
+ completed: byState.completed,
2273
+ ready_quality: byState.ready > 0 ? `${byState.ready} ready` : "not applicable"
2274
+ },
2275
+ graph_quality: g.available ? { available: true, scope: g.scope, quality: g.quality, nodes: g.nodes, edges: g.edges, hints: g.hints, reason: g.scopeReason, last_exported: g.generatedAt } : { available: false, suggestion: "Run `kaddo graph export --scope all`." },
2276
+ guard_activity: {
2277
+ available: false,
2278
+ note: "Guard history is not persisted. Run `kaddo guard` to check drift; future versions may store runs for trend analysis."
2279
+ },
2280
+ impact_signals,
2281
+ score,
2282
+ suggested_actions: actions
2283
+ };
2284
+ }
2285
+ function ratio(have, total) {
2286
+ return `${have}/${total}`;
2287
+ }
2288
+ function renderImpactMarkdown(r) {
2289
+ const L = [];
2290
+ L.push("# Kaddo Knowledge Impact Report", "");
2291
+ L.push(`Generated at: ${r.generated_at}`);
2292
+ L.push(`Project: ${r.project}`);
2293
+ L.push(`Scope: ${r.scope}`);
2294
+ if (r.score !== null) L.push(`Knowledge Impact Score: ${r.score}/100`);
2295
+ else L.push("Knowledge Impact Score: not available");
2296
+ L.push("");
2297
+ L.push("## Executive Summary", "");
2298
+ for (const s2 of r.executive_summary) L.push(`- ${s2}`);
2299
+ L.push("");
2300
+ L.push("## Knowledge Health", "");
2301
+ for (const [k, v] of Object.entries(r.knowledge_health)) L.push(`- ${k}: ${v}`);
2302
+ L.push("");
2303
+ L.push("## Knowledge Coverage", "");
2304
+ for (const c of r.knowledge_coverage) L.push(`- ${c.label}: ${ratio(c.have, c.total)}`);
2305
+ L.push("");
2306
+ L.push("## Ownership Coverage", "");
2307
+ L.push(`- Coverage: ${r.ownership_coverage.coverage_percent}%`);
2308
+ L.push(`- Work Items with code ownership: ${r.ownership_coverage.work_items_with_ownership}`);
2309
+ L.push(`- Owned code paths: ${r.ownership_coverage.owned_code_paths}`);
2310
+ L.push(`- Broad globs detected: ${r.ownership_coverage.broad_globs}`);
2311
+ L.push(`- Ownership overlaps: ${r.ownership_coverage.ownership_overlaps}`);
2312
+ L.push("");
2313
+ L.push("## Traceability", "");
2314
+ const t = r.traceability;
2315
+ L.push(`- Roadmap candidates: ${t.roadmap_candidates}`);
2316
+ L.push(`- Materialized Work Items: ${t.materialized_work_items}`);
2317
+ L.push(`- Remaining candidates: ${t.remaining_candidates}`);
2318
+ L.push(`- Completed Work Items: ${t.completed_work_items}`);
2319
+ L.push(`- Work Items connected to roadmap: ${t.work_items_connected_to_roadmap}`);
2320
+ L.push(`- Work Items connected to code: ${t.work_items_connected_to_code}`);
2321
+ L.push(`- Graph quality: ${t.graph_quality}`);
2322
+ if (t.graph_nodes !== null) L.push(`- Graph nodes: ${t.graph_nodes}`);
2323
+ if (t.graph_edges !== null) L.push(`- Graph edges: ${t.graph_edges}`);
2324
+ if (t.graph_hints !== null) L.push(`- Graph hints: ${t.graph_hints}`);
2325
+ L.push("");
2326
+ L.push("## Context Readiness", "");
2327
+ L.push(`Context Readiness: ${r.context_readiness.level}`);
2328
+ if (r.context_readiness.reasons.length > 0) {
2329
+ L.push("", "Reason:");
2330
+ for (const reason of r.context_readiness.reasons) L.push(`- ${reason}`);
2331
+ }
2332
+ L.push("");
2333
+ L.push("## Work Item Readiness", "");
2334
+ const w = r.work_item_readiness;
2335
+ L.push(`- Draft: ${w.draft}`);
2336
+ L.push(`- Ready: ${w.ready}`);
2337
+ L.push(`- In Progress: ${w.in_progress}`);
2338
+ L.push(`- Blocked: ${w.blocked}`);
2339
+ L.push(`- Completed: ${w.completed}`);
2340
+ L.push(`- Ready quality: ${w.ready_quality}`);
2341
+ L.push("");
2342
+ L.push("## Graph Quality", "");
2343
+ if (r.graph_quality.available) {
2344
+ const gq = r.graph_quality;
2345
+ L.push(`- Scope: ${gq.scope}`);
2346
+ L.push(`- Quality: ${gq.quality}`);
2347
+ L.push(`- Nodes: ${gq.nodes}`);
2348
+ L.push(`- Edges: ${gq.edges}`);
2349
+ L.push(`- Hints: ${gq.hints}`);
2350
+ if (gq.reason) L.push(`- Reason: ${gq.reason}`);
2351
+ if (gq.last_exported) L.push(`- Last exported: ${gq.last_exported}`);
2352
+ } else {
2353
+ L.push("- Graph data not available.");
2354
+ L.push(`- Tip: ${r.graph_quality.suggestion}`);
2355
+ }
2356
+ L.push("");
2357
+ L.push("## Guard Activity", "");
2358
+ L.push("- Guard history: not available");
2359
+ L.push(`- Note: ${r.guard_activity.note}`);
2360
+ L.push("");
2361
+ L.push("## Impact Signals", "");
2362
+ const s = r.impact_signals;
2363
+ L.push(`- Ambiguity reduction: ${s.ambiguity_reduction}`);
2364
+ L.push(`- Drift prevention: ${s.drift_prevention}`);
2365
+ L.push(`- Onboarding support: ${s.onboarding_support}`);
2366
+ L.push(`- Delivery traceability: ${s.delivery_traceability}`);
2367
+ L.push(`- AI context readiness: ${s.ai_context_readiness}`);
2368
+ L.push(`- Maintenance readiness: ${s.maintenance_readiness}`);
2369
+ L.push("");
2370
+ L.push("## Suggested Actions", "");
2371
+ r.suggested_actions.forEach((a, i) => L.push(`${i + 1}. ${a}`));
2372
+ L.push("");
2373
+ return L.join("\n");
2374
+ }
2375
+ function serializeImpactJson(r) {
2376
+ return JSON.stringify(r, null, 2) + "\n";
2377
+ }
2378
+
2379
+ // src/workitems.ts
2380
+ import matter7 from "gray-matter";
2381
+ var ACTIVE_STATES2 = /* @__PURE__ */ new Set(["draft", "ready", "in-progress", "blocked"]);
2382
+ function strArray(v) {
2383
+ return Array.isArray(v) ? v.map((x) => String(x)).filter(Boolean) : [];
2384
+ }
2385
+ function lifecycleOf(data, relPath) {
2386
+ const status = String(data.status ?? "").trim();
2387
+ if (status) return status;
2388
+ const segs = relPath.split("/");
2389
+ const folder = segs[segs.length - 2];
2390
+ return ACTIVE_STATES2.has(folder) || folder === "completed" || folder === "archived" ? folder : "ready";
2391
+ }
2392
+ function firstParagraph2(body) {
2393
+ for (const block of body.split(/\n\s*\n/)) {
2394
+ const t = block.trim();
2395
+ if (t && !t.startsWith("#") && !t.startsWith(">")) return t.replace(/\s+/g, " ");
2396
+ }
2397
+ return "";
2398
+ }
2399
+ function listWorkItems(root) {
2400
+ const files = listFiles(root, "knowledge/delivery/work-items", ".md");
2401
+ const items = [];
2402
+ for (const rel of files) {
2403
+ const raw = readText(root, rel);
2404
+ if (raw === null) continue;
2405
+ let parsed;
2406
+ try {
2407
+ parsed = matter7(raw);
2408
+ } catch {
2409
+ continue;
2410
+ }
2411
+ const data = parsed.data;
2412
+ if (!data.type) continue;
2413
+ items.push({
2414
+ id: String(data.id ?? "") || rel,
2415
+ title: String(data.title ?? ""),
2416
+ status: lifecycleOf(data, rel),
2417
+ type: String(data.type ?? ""),
2418
+ knowledge_level: String(data.knowledge_level ?? ""),
2419
+ path: rel,
2420
+ summary: String(data.summary ?? "") || firstParagraph2(parsed.content),
2421
+ code: strArray(data.code),
2422
+ capabilities: strArray(data.capabilities),
2423
+ decisions: strArray(data.decisions),
2424
+ capsules: strArray(data.capsules)
2425
+ });
2426
+ }
2427
+ return items.sort((a, b) => a.id.localeCompare(b.id));
2428
+ }
2429
+
2430
+ // src/resources.ts
2431
+ function text(uri, body, mimeType = "text/markdown") {
2432
+ return [{ uri, text: body, mimeType }];
2433
+ }
2434
+ function fileOrHint(root, uri, rel, hint, mimeType = "text/markdown") {
2435
+ const body = readText(root, rel);
2436
+ return text(uri, body ?? hint, body ? mimeType : "text/plain");
2437
+ }
2438
+ var RESOURCES = [
2439
+ {
2440
+ uri: "kaddo://context-pack",
2441
+ name: "Kaddo context pack",
2442
+ description: "Curated LLM context pack (.kaddo/context-pack.md).",
2443
+ mimeType: "text/markdown",
2444
+ read: (root) => fileOrHint(
2445
+ root,
2446
+ "kaddo://context-pack",
2447
+ ".kaddo/context-pack.md",
2448
+ "Context pack not found. Run `kaddo context` in the project first."
2449
+ )
2450
+ },
2451
+ {
2452
+ uri: "kaddo://explain",
2453
+ name: "Kaddo explain",
2454
+ description: "What Kaddo knows about the project (.kaddo/explain.md).",
2455
+ mimeType: "text/markdown",
2456
+ read: (root) => fileOrHint(
2457
+ root,
2458
+ "kaddo://explain",
2459
+ ".kaddo/explain.md",
2460
+ "Explain output not found. Run `kaddo explain` first."
2461
+ )
2462
+ },
2463
+ {
2464
+ uri: "kaddo://understand",
2465
+ name: "Kaddo understand",
2466
+ description: "Current phase and recommended next step (.kaddo/understand.md).",
2467
+ mimeType: "text/markdown",
2468
+ read: (root) => fileOrHint(
2469
+ root,
2470
+ "kaddo://understand",
2471
+ ".kaddo/understand.md",
2472
+ "Understand output not found. Run `kaddo understand` first."
2473
+ )
2474
+ },
2475
+ {
2476
+ uri: "kaddo://graph",
2477
+ name: "Kaddo knowledge graph",
2478
+ description: "Knowledge graph (.kaddo/graph.json + .kaddo/graph.mmd).",
2479
+ mimeType: "application/json",
2480
+ read: (root) => {
2481
+ const json = readText(root, ".kaddo/graph.json");
2482
+ const mmd = readText(root, ".kaddo/graph.mmd");
2483
+ if (!json && !mmd) {
2484
+ return text("kaddo://graph", "Knowledge graph not found. Run `kaddo graph export` first.", "text/plain");
2485
+ }
2486
+ const parts = [];
2487
+ if (json) parts.push({ uri: "kaddo://graph", text: json, mimeType: "application/json" });
2488
+ if (mmd) parts.push({ uri: "kaddo://graph.mmd", text: mmd, mimeType: "text/vnd.mermaid" });
2489
+ return parts;
2490
+ }
2491
+ },
2492
+ {
2493
+ uri: "kaddo://graph-hints",
2494
+ name: "Kaddo graph hints",
2495
+ description: "Graph relationship-quality hints (.kaddo/graph-hints.md + .json).",
2496
+ mimeType: "text/markdown",
2497
+ read: (root) => {
2498
+ const md = readText(root, ".kaddo/graph-hints.md");
2499
+ const json = readText(root, ".kaddo/graph-hints.json");
2500
+ if (!md && !json) {
2501
+ return text("kaddo://graph-hints", "Graph hints not found. Run `kaddo graph export` first.", "text/plain");
2502
+ }
2503
+ const parts = [];
2504
+ if (md) parts.push({ uri: "kaddo://graph-hints", text: md, mimeType: "text/markdown" });
2505
+ if (json) parts.push({ uri: "kaddo://graph-hints.json", text: json, mimeType: "application/json" });
2506
+ return parts;
2507
+ }
2508
+ },
2509
+ {
2510
+ uri: "kaddo://work-items",
2511
+ name: "Kaddo work items",
2512
+ description: "Summarized Work Items from knowledge/delivery/work-items/.",
2513
+ mimeType: "application/json",
2514
+ read: (root) => {
2515
+ if (!hasKnowledge(root)) {
2516
+ return text("kaddo://work-items", "Knowledge repository not found. Run `kaddo bootstrap` first.", "text/plain");
2517
+ }
2518
+ return [
2519
+ {
2520
+ uri: "kaddo://work-items",
2521
+ text: JSON.stringify(listWorkItems(root), null, 2),
2522
+ mimeType: "application/json"
2523
+ }
2524
+ ];
2525
+ }
2526
+ },
2527
+ {
2528
+ uri: "kaddo://roadmap",
2529
+ name: "Kaddo roadmap",
2530
+ description: "Delivery roadmap (knowledge/delivery/roadmap.md).",
2531
+ mimeType: "text/markdown",
2532
+ read: (root) => fileOrHint(
2533
+ root,
2534
+ "kaddo://roadmap",
2535
+ "knowledge/delivery/roadmap.md",
2536
+ "Roadmap not found. Create knowledge/delivery/roadmap.md (e.g. via the roadmap-agent) first."
2537
+ )
2538
+ },
2539
+ {
2540
+ uri: "kaddo://capsules",
2541
+ name: "Kaddo knowledge capsules",
2542
+ description: "External Knowledge Capsules from .kaddo/external.yml and external/.",
2543
+ mimeType: "application/json",
2544
+ read: (root) => [
2545
+ {
2546
+ uri: "kaddo://capsules",
2547
+ text: JSON.stringify(listCapsules(root), null, 2),
2548
+ mimeType: "application/json"
2549
+ }
2550
+ ]
2551
+ },
2552
+ {
2553
+ uri: "kaddo://agents",
2554
+ name: "Kaddo agents",
2555
+ description: "Installed agent prompts from knowledge/agents/.",
2556
+ mimeType: "application/json",
2557
+ read: (root) => [
2558
+ {
2559
+ uri: "kaddo://agents",
2560
+ text: JSON.stringify(listAgents(root), null, 2),
2561
+ mimeType: "application/json"
2562
+ }
2563
+ ]
2564
+ },
2565
+ {
2566
+ uri: "kaddo://skills",
2567
+ name: "Kaddo skills",
2568
+ description: "Installed reusable skills from knowledge/skills/ (empty if none).",
2569
+ mimeType: "application/json",
2570
+ read: (root) => [
2571
+ {
2572
+ uri: "kaddo://skills",
2573
+ text: JSON.stringify({ skills: listSkills(root) }, null, 2),
2574
+ mimeType: "application/json"
2575
+ }
2576
+ ]
2577
+ },
2578
+ {
2579
+ uri: "kaddo://impact-report",
2580
+ name: "Kaddo impact report",
2581
+ description: "Knowledge Impact Report \u2014 the last written report, or generated in memory (read-only).",
2582
+ mimeType: "text/markdown",
2583
+ read: (root) => {
2584
+ const saved = readText(root, ".kaddo/reports/impact-report.md");
2585
+ if (saved) return [{ uri: "kaddo://impact-report", text: saved, mimeType: "text/markdown" }];
2586
+ if (!hasKnowledge(root)) {
2587
+ return text("kaddo://impact-report", "Knowledge repository not found. Run `kaddo bootstrap` first.", "text/plain");
2588
+ }
2589
+ return [{ uri: "kaddo://impact-report", text: renderImpactMarkdown(buildImpactReport(root, { scope: "all" })), mimeType: "text/markdown" }];
2590
+ }
2591
+ }
2592
+ ];
2593
+
2594
+ // src/prompts.ts
2595
+ var EXTRA_INPUTS = {
2596
+ "graph-agent": [".kaddo/graph.json", ".kaddo/graph-hints.md"],
2597
+ "ownership-agent": ["knowledge/delivery/work-items/", "knowledge/inventory.md"],
2598
+ "capsule-agent": ["knowledge/product/capabilities.md", "knowledge/tech/decisions/"],
2599
+ "work-item-agent": ["knowledge/delivery/roadmap.md"],
2600
+ "implementation-agent": ["knowledge/tech/git-strategy.md"]
2601
+ };
2602
+ function recommendedInputs(name) {
2603
+ return [".kaddo/context-pack.md", ...EXTRA_INPUTS[name] ?? []];
2604
+ }
2605
+ function listPrompts(root) {
2606
+ return listAgents(root).map((a) => ({
2607
+ name: a.name,
2608
+ description: a.description || `Kaddo ${a.name}`,
2609
+ content: "",
2610
+ recommended_inputs: recommendedInputs(a.name)
2611
+ }));
2612
+ }
2613
+ function getPrompt(root, name) {
2614
+ const agent = getAgentPrompt(root, name);
2615
+ if (!agent) return null;
2616
+ return {
2617
+ name: agent.name,
2618
+ description: agent.description || `Kaddo ${agent.name}`,
2619
+ content: agent.content,
2620
+ recommended_inputs: recommendedInputs(agent.name)
2621
+ };
2622
+ }
2623
+
2624
+ // src/tools.ts
2625
+ var ok = (data) => ({ ok: true, data });
2626
+ var fail = (message) => ({ ok: false, message });
2627
+ function projectStatus(root) {
2628
+ const explain = readJson(root, ".kaddo/explain.json");
2629
+ const hints = readJson(root, ".kaddo/graph-hints.json");
2630
+ const capsules = listCapsules(root);
2631
+ if (!explain) {
2632
+ return fail("Project status needs `.kaddo/explain.json`. Run `kaddo explain` first.");
2633
+ }
2634
+ const maturity = (explain.layers ?? []).map((l) => `${l.layer}: ${l.status}`);
2635
+ return ok({
2636
+ project: explain.project?.name ?? "unknown",
2637
+ state: explain.project?.state ?? "unknown",
2638
+ knowledgeMaturity: maturity,
2639
+ workItems: {
2640
+ total: explain.workItems?.total ?? 0,
2641
+ byState: explain.workItems?.byState ?? {},
2642
+ byType: explain.workItems?.byType ?? {}
2643
+ },
2644
+ ownership: {
2645
+ withOwnership: explain.ownership?.workItemsWithOwnership ?? 0,
2646
+ total: explain.ownership?.workItemsTotal ?? 0
2647
+ },
2648
+ graph: {
2649
+ scope: hints?.scope ?? "unknown (run `kaddo graph export`)",
2650
+ scope_reason: hints?.scope_reason ?? "",
2651
+ quality: hints?.quality ?? "unknown (run `kaddo graph export`)",
2652
+ hints: hints?.summary?.hints ?? 0
2653
+ },
2654
+ capsules: capsules.length
2655
+ });
2656
+ }
2657
+ function listWorkItemsTool(root, filter = {}) {
2658
+ let items = listWorkItems(root);
2659
+ if (filter.status) items = items.filter((w) => w.status === filter.status);
2660
+ if (filter.type) items = items.filter((w) => w.type === filter.type);
2661
+ if (filter.knowledge_level) items = items.filter((w) => w.knowledge_level === filter.knowledge_level);
2662
+ return ok(items);
2410
2663
  }
2411
-
2412
- // ../cli/src/core/project-explain.ts
2413
- var ARCH_DIR3 = "knowledge";
2414
- function normalizeTitle(t) {
2415
- return t.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, " ").trim();
2664
+ function getWorkItem(root, id) {
2665
+ const item = listWorkItems(root).find((w) => w.id === id);
2666
+ if (!item) return fail(`Work Item "${id}" not found.`);
2667
+ const content = readText(root, item.path);
2668
+ return ok({ ...item, content: content ?? "" });
2416
2669
  }
2417
- function findDuplicateWorkItems(items) {
2418
- const groups = [];
2419
- const seen = /* @__PURE__ */ new Set();
2420
- const bucket = (key, reason, pred) => {
2421
- const matched = items.filter(pred);
2422
- if (matched.length > 1) {
2423
- const id = `${reason}:${key}:${matched.map((m) => m.id).sort().join(",")}`;
2424
- if (!seen.has(id)) {
2425
- seen.add(id);
2426
- groups.push({ reason, items: matched.map((m) => ({ id: m.id, title: m.title })) });
2427
- }
2428
- }
2429
- };
2430
- for (const sid of new Set(items.map((i) => i.sourceId).filter(Boolean))) {
2431
- bucket(sid, `same source candidate (${sid})`, (i) => i.sourceId === sid);
2432
- }
2433
- for (const nt of new Set(items.map((i) => normalizeTitle(i.title)).filter(Boolean))) {
2434
- bucket(nt, "same normalized title", (i) => normalizeTitle(i.title) === nt);
2435
- }
2436
- return groups;
2670
+ function listCapsulesTool(root) {
2671
+ return ok(listCapsules(root));
2437
2672
  }
2438
- function first(values) {
2439
- if (Array.isArray(values)) {
2440
- const v = values.find((x) => typeof x === "string" && x);
2441
- return v ? String(v) : void 0;
2442
- }
2443
- return typeof values === "string" && values ? values : void 0;
2673
+ function getCapsuleTool(root, id) {
2674
+ const cap = getCapsule(root, id);
2675
+ if (!cap) return fail(`Knowledge Capsule "${id}" not found.`);
2676
+ return ok(cap);
2444
2677
  }
2445
- function toStringArray2(value) {
2446
- return Array.isArray(value) ? value.map((v) => String(v)).filter(Boolean) : [];
2678
+ function listAgentsTool(root) {
2679
+ return ok(listAgents(root));
2447
2680
  }
2448
- function loadScan(dir) {
2681
+ function getAgentPromptTool(root, name) {
2682
+ const agent = getAgentPrompt(root, name);
2683
+ if (!agent) return fail(`Agent "${name}" is not installed. Run \`kaddo add agents\` first.`);
2684
+ return ok(agent);
2685
+ }
2686
+ function listSkillsTool(root) {
2687
+ return ok(listSkills(root));
2688
+ }
2689
+ function getSkillTool(root, id) {
2690
+ const skill = getSkill(root, id);
2691
+ if (!skill) return fail(`Skill "${id}" is not installed. Run \`kaddo add skills\` first.`);
2692
+ return ok(skill);
2693
+ }
2694
+ function listGraphHints(root, filter = {}) {
2695
+ const report = readJson(root, ".kaddo/graph-hints.json");
2696
+ if (!report) return fail("Graph hints not found. Run `kaddo graph export` first.");
2697
+ let hints = Array.isArray(report.hints) ? report.hints : [];
2698
+ if (filter.artifact_type) hints = hints.filter((h) => h.artifact_type === filter.artifact_type);
2699
+ if (filter.severity) hints = hints.filter((h) => h.severity === filter.severity);
2700
+ if (filter.active_only) hints = hints.filter((h) => h.artifact_type === "work-item");
2701
+ return ok({
2702
+ scope: report.scope ?? "active",
2703
+ scope_reason: report.scope_reason ?? "",
2704
+ quality: report.quality ?? "unknown",
2705
+ count: hints.length,
2706
+ hints
2707
+ });
2708
+ }
2709
+
2710
+ // ../cli/src/core/context-pack.ts
2711
+ import matter8 from "gray-matter";
2712
+ var CONTEXT_PACK_VERSION = "1";
2713
+ var ARCH_DIR3 = "knowledge";
2714
+ function readScanJson(dir) {
2449
2715
  const scanPath = join(dir, ".kaddo", "scan.json");
2450
2716
  if (!exists(scanPath)) return null;
2451
2717
  try {
2452
- const parsed = JSON.parse(readFile(scanPath));
2453
- const detected = parsed.detected ?? {};
2454
- return {
2455
- language: first(detected.languages),
2456
- framework: first(detected.frameworks),
2457
- packageManager: first(detected.packageManagers),
2458
- sourceDirectories: toStringArray2(detected.sourceDirectories),
2459
- migrationDirectories: toStringArray2(detected.migrationDirectories),
2460
- contractFiles: toStringArray2(detected.contractFiles),
2461
- infrastructureFiles: toStringArray2(detected.infrastructureFiles)
2462
- };
2718
+ return JSON.parse(readFile(scanPath));
2463
2719
  } catch {
2464
2720
  return null;
2465
2721
  }
2466
2722
  }
2467
- function hasAgents(dir) {
2468
- const agentsDir = join(dir, ARCH_DIR3, "agents");
2469
- if (!exists(agentsDir)) return false;
2470
- function hasAgentMd(d) {
2471
- for (const e of readDir(d)) {
2472
- const full = join(d, e);
2473
- if (isFile(full)) {
2474
- if (e.endsWith("-agent.md")) return true;
2475
- } else if (hasAgentMd(full)) {
2476
- return true;
2477
- }
2478
- }
2479
- return false;
2723
+ function firstParagraph3(markdown) {
2724
+ const body = matter8(markdown).content.trim();
2725
+ const para = body.split("\n\n").map((p) => p.trim()).find((p) => p && !p.startsWith("#"));
2726
+ return para ?? "";
2727
+ }
2728
+ function readMarkdownSummary(dir, file) {
2729
+ const p = join(dir, ARCH_DIR3, file);
2730
+ if (!exists(p)) return null;
2731
+ try {
2732
+ return firstParagraph3(readFile(p));
2733
+ } catch {
2734
+ return null;
2480
2735
  }
2481
- return hasAgentMd(agentsDir);
2482
2736
  }
2483
- function buildProjectExplanation(dir) {
2484
- const config = loadConfig(dir);
2485
- const project = {
2486
- name: config?.project.name ?? "unknown",
2487
- state: config?.project.state ?? "unknown",
2488
- teamSize: config?.team.size ?? "unknown",
2489
- structure: config?.project.structure ?? "unknown",
2490
- language: config ? languageLabel(projectLanguage(config)) : "English"
2491
- };
2492
- const scan = loadScan(dir);
2493
- const stack = scan ? {
2494
- language: scan.language,
2495
- framework: scan.framework,
2496
- packageManager: scan.packageManager,
2497
- sourceDirectories: scan.sourceDirectories,
2498
- migrationDirectories: scan.migrationDirectories,
2499
- contractFiles: scan.contractFiles,
2500
- infrastructureFiles: scan.infrastructureFiles
2501
- } : null;
2502
- const layers = knowledgeLayers(dir);
2503
- const layerStatus = (name) => layers.find((l) => l.layer === name)?.status ?? "Missing";
2504
- const knowledge = {
2505
- hasScan: scan !== null,
2506
- hasInventory: exists(join(dir, ARCH_DIR3, "inventory.md")),
2507
- hasContextPack: exists(join(dir, ".kaddo", "context-pack.md")),
2508
- hasUnderstand: exists(join(dir, ".kaddo", "understand.md")),
2509
- hasCapabilities: layerStatus("Product") !== "Missing",
2510
- hasArchitecture: layerStatus("Tech") !== "Missing",
2511
- hasRoadmap: layerStatus("Delivery") !== "Missing",
2512
- hasAgents: hasAgents(dir)
2513
- };
2514
- const workItemArtifacts = discoverWorkItems(dir);
2515
- const items = workItemArtifacts.map((a) => ({
2516
- id: a.id || a.title,
2517
- title: a.title,
2518
- type: a.type,
2519
- status: a.status,
2520
- lifecycle: lifecycleStateOf({ status: a.status, filePath: a.filePath }),
2521
- initiative: a.initiative,
2522
- knowledgeLevel: a.knowledgeLevel,
2523
- hasOwnership: a.codeGlobs.length > 0,
2524
- domains: a.domains
2525
- }));
2526
- const byState = lifecycleCounts(items.map((i) => i.lifecycle));
2527
- const byType = {};
2528
- for (const i of items) {
2529
- const t = i.type || "unknown";
2530
- byType[t] = (byType[t] ?? 0) + 1;
2737
+ function recommendedAgentsForState(state) {
2738
+ switch (state) {
2739
+ case "new":
2740
+ return ["roadmap-agent", "architecture-agent"];
2741
+ case "legacy":
2742
+ return ["legacy-agent", "architecture-agent", "capability-agent"];
2743
+ case "pre-ai":
2744
+ default:
2745
+ return ["capability-agent", "architecture-agent", "roadmap-agent"];
2531
2746
  }
2532
- const initiativeMap = /* @__PURE__ */ new Map();
2533
- for (const i of items) {
2534
- const name = i.initiative || "Unassigned";
2535
- if (!initiativeMap.has(name)) initiativeMap.set(name, emptyLifecycleCounts());
2536
- initiativeMap.get(name)[i.lifecycle]++;
2747
+ }
2748
+ function nextStepsForState(state) {
2749
+ switch (state) {
2750
+ case "new":
2751
+ return [
2752
+ "Use roadmap-agent to shape an initial roadmap.",
2753
+ "Use architecture-agent to outline the intended architecture."
2754
+ ];
2755
+ case "legacy":
2756
+ return [
2757
+ "Use legacy-agent to surface risks and unknowns before changing code.",
2758
+ "Use architecture-agent to reconstruct the current architecture.",
2759
+ "Use capability-agent to map existing capabilities."
2760
+ ];
2761
+ case "pre-ai":
2762
+ default:
2763
+ return [
2764
+ "Use capability-agent to extract system capabilities.",
2765
+ "Use architecture-agent to reconstruct the current architecture.",
2766
+ "Use roadmap-agent to propose roadmap candidates."
2767
+ ];
2537
2768
  }
2538
- const initiatives = [...initiativeMap.entries()].map(([name, states]) => ({ name, states })).sort((a, b) => a.name.localeCompare(b.name));
2539
- const workItems = {
2540
- total: items.length,
2541
- inProgress: items.filter((i) => i.lifecycle === "in-progress").length,
2542
- done: items.filter((i) => i.status === "done").length,
2543
- cancelled: items.filter((i) => i.status === "cancelled").length,
2544
- byState,
2545
- byType,
2546
- initiatives,
2547
- items
2548
- };
2549
- const withOwnership = items.filter((i) => i.hasOwnership).length;
2550
- const ownership = {
2551
- workItemsTotal: items.length,
2552
- workItemsWithOwnership: withOwnership,
2553
- workItemsMissingOwnership: items.length - withOwnership
2554
- };
2555
- const domains = [...new Set(workItemArtifacts.flatMap((a) => a.domains))].filter(Boolean);
2556
- const duplicateWorkItems = findDuplicateWorkItems(
2557
- workItemArtifacts.map((a) => ({ id: a.id || a.title, title: a.title, sourceId: a.sourceId }))
2558
- );
2559
- const roadmapPath = join(dir, ARCH_DIR3, "delivery", "roadmap.md");
2560
- const roadmapMd = exists(roadmapPath) ? readFile(roadmapPath) : null;
2561
- const roadmap = roadmapStats(roadmapMd, items.length);
2562
- const mappedModules = loadMappedModules(dir);
2563
- const missingKnowledge = [];
2564
- if (!knowledge.hasScan) missingKnowledge.push("Scan baseline (.kaddo/scan.json)");
2565
- if (!knowledge.hasContextPack) missingKnowledge.push("Context pack (.kaddo/context-pack.md)");
2566
- if (!knowledge.hasInventory) missingKnowledge.push("Inventory (knowledge/inventory.md)");
2567
- if (!knowledge.hasCapabilities) missingKnowledge.push("Product knowledge (knowledge/product/)");
2568
- if (!knowledge.hasArchitecture) missingKnowledge.push("Tech knowledge (knowledge/tech/)");
2569
- if (!knowledge.hasRoadmap) missingKnowledge.push("Roadmap (knowledge/delivery/roadmap.md)");
2570
- if (!knowledge.hasAgents) missingKnowledge.push("Agents (knowledge/agents/)");
2571
- if (items.length === 0) missingKnowledge.push("Work items (knowledge/delivery/work-items/)");
2572
- const suggestedNextSteps = [];
2573
- if (!knowledge.hasScan) {
2574
- suggestedNextSteps.push("Run `kaddo scan` to detect the technical stack.");
2575
- } else if (!knowledge.hasContextPack) {
2576
- suggestedNextSteps.push("Run `kaddo context` to prepare an LLM context pack.");
2769
+ }
2770
+ var LLM_INSTRUCTIONS = [
2771
+ "Use this context pack as the project baseline.",
2772
+ "Do not write code yet.",
2773
+ "First extract: system capabilities, architecture notes, risks, open questions and roadmap candidates.",
2774
+ "This pack is deterministic CLI output \u2014 it does not interpret the system. That is your job."
2775
+ ];
2776
+ var OPERATING_RULES = [
2777
+ "**Never run `git commit`, `git push` or `git merge` without explicit human confirmation.**",
2778
+ "Never push or merge automatically \u2014 ever. Suggest a Conventional Commit message and wait.",
2779
+ "When implementing a Work Item, create a branch FIRST (per the project Git strategy, `.kaddo/git.yml`, default `feature/<id>-<slug>`). Never work directly on `main`.",
2780
+ "After significant changes run `kaddo scan`, `kaddo owners suggest` and `kaddo guard`, and update the affected knowledge (ADR / capabilities / current-state).",
2781
+ "Kaddo itself never calls an LLM and never runs git \u2014 every git action is the human\u2019s."
2782
+ ];
2783
+ function toContextWorkItem(a) {
2784
+ return {
2785
+ id: a.id,
2786
+ type: a.type,
2787
+ title: a.title,
2788
+ status: a.status,
2789
+ lifecycle: lifecycleStateOf({ status: a.status, filePath: a.filePath }),
2790
+ knowledgeLevel: a.knowledgeLevel,
2791
+ domains: a.domains
2792
+ };
2793
+ }
2794
+ function toContextArtifact(a) {
2795
+ return { id: a.id, type: a.type, title: a.title, summary: a.summary, codeGlobs: a.codeGlobs };
2796
+ }
2797
+ function isDeliveryWorkItem(a) {
2798
+ return a.filePath.replace(/\\/g, "/").includes("/delivery/work-items/") && Boolean(a.type);
2799
+ }
2800
+ function buildContextPack(dir, config, now = /* @__PURE__ */ new Date()) {
2801
+ const missing = [];
2802
+ const scanJson = readScanJson(dir);
2803
+ if (!scanJson) {
2804
+ missing.push("Scan baseline missing. Run `kaddo scan` for better context.");
2577
2805
  }
2578
- if (!knowledge.hasAgents) {
2579
- suggestedNextSteps.push("Run `kaddo add agents` to install knowledge agents.");
2806
+ const detected = scanJson?.detected ?? {};
2807
+ const inventoryAvailable = exists(join(dir, ARCH_DIR3, "inventory.md"));
2808
+ if (!inventoryAvailable) {
2809
+ missing.push("No technical inventory found. Run `kaddo scan` to generate it.");
2580
2810
  }
2581
- if (!knowledge.hasCapabilities) {
2582
- suggestedNextSteps.push("Use capability-agent to generate knowledge/product/capabilities.md.");
2811
+ const knowledgeSummary = readMarkdownSummary(dir, "knowledge.md") ?? "";
2812
+ if (!knowledgeSummary) {
2813
+ missing.push("No project knowledge summary found yet.");
2583
2814
  }
2584
- if (!knowledge.hasArchitecture) {
2585
- suggestedNextSteps.push("Use architecture-agent to generate knowledge/tech/current-state.md.");
2815
+ const roadmapSummary = readMarkdownSummary(dir, "delivery/roadmap.md") ?? "";
2816
+ if (!roadmapSummary) {
2817
+ missing.push("No roadmap baseline found.");
2586
2818
  }
2587
- if (!knowledge.hasRoadmap) {
2588
- suggestedNextSteps.push("Use roadmap-agent to generate knowledge/delivery/roadmap.md.");
2589
- } else if (roadmap.remaining > 0) {
2590
- suggestedNextSteps.push(
2591
- `Materialize ${roadmap.remaining} roadmap candidate(s) with \`kaddo create --from roadmap\`.`
2592
- );
2819
+ const allArtifacts = discoverKnowledge(dir);
2820
+ const workItems = allArtifacts.filter(
2821
+ (a) => isDeliveryWorkItem(a) && isActiveState(lifecycleStateOf({ status: a.status, filePath: a.filePath }))
2822
+ );
2823
+ if (workItems.length === 0) {
2824
+ missing.push("No work items found.");
2593
2825
  }
2594
- if (items.length === 0 && !roadmap.present) {
2595
- suggestedNextSteps.push("Create your first Work Item with `kaddo create`.");
2596
- } else if (ownership.workItemsMissingOwnership > 0) {
2597
- suggestedNextSteps.push(
2598
- "Run `kaddo owners suggest` for Work Items without code ownership."
2599
- );
2826
+ const state = config.project.state;
2827
+ const roadmapPath = join(dir, ARCH_DIR3, "delivery", "roadmap.md");
2828
+ const materialized = allArtifacts.filter(
2829
+ (a) => a.filePath.replace(/\\/g, "/").includes("/delivery/work-items/") && Boolean(a.type)
2830
+ ).length;
2831
+ const roadmap = roadmapStats(exists(roadmapPath) ? readFile(roadmapPath) : null, materialized);
2832
+ const deliveryMix = {};
2833
+ for (const a of allArtifacts) {
2834
+ const p = a.filePath.replace(/\\/g, "/");
2835
+ if (!a.type || !p.includes("/delivery/work-items/")) continue;
2836
+ if (!isActiveState(lifecycleStateOf({ status: a.status, filePath: a.filePath }))) continue;
2837
+ deliveryMix[a.type] = (deliveryMix[a.type] ?? 0) + 1;
2600
2838
  }
2839
+ const mappedModules = loadMappedModules(dir);
2840
+ const layers = knowledgeLayers(dir);
2841
+ const allWorkItems = allArtifacts.filter((a) => a.isWorkItem);
2842
+ const wiWithOwnership = allWorkItems.filter((a) => a.codeGlobs.length > 0).length;
2843
+ const phase = assessPhase({
2844
+ layers,
2845
+ roadmap,
2846
+ workItems: {
2847
+ total: allWorkItems.length,
2848
+ byState: lifecycleCounts(
2849
+ allWorkItems.map((a) => lifecycleStateOf({ status: a.status, filePath: a.filePath }))
2850
+ ),
2851
+ items: allWorkItems.map((a) => ({
2852
+ id: a.id || a.title,
2853
+ title: a.title,
2854
+ lifecycle: lifecycleStateOf({ status: a.status, filePath: a.filePath })
2855
+ }))
2856
+ },
2857
+ ownership: {
2858
+ workItemsTotal: allWorkItems.length,
2859
+ workItemsWithOwnership: wiWithOwnership,
2860
+ workItemsMissingOwnership: allWorkItems.length - wiWithOwnership
2861
+ }
2862
+ });
2601
2863
  return {
2602
- project,
2603
- stack,
2604
- knowledge,
2605
- workItems,
2606
- ownership,
2607
- domains,
2608
- duplicateWorkItems,
2609
- externalCapsules: loadExternalCapsules(dir),
2610
- graph: loadGraphSummary(dir),
2611
- graphHints: loadGraphHints(dir),
2612
- skills: (() => {
2613
- const installed = discoverInstalledSkills(dir);
2614
- return { total: installed.length, byGroup: skillGroupCounts(installed) };
2615
- })(),
2864
+ version: CONTEXT_PACK_VERSION,
2865
+ generatedAt: now.toISOString(),
2866
+ project: {
2867
+ name: config.project.name,
2868
+ state,
2869
+ teamSize: config.team.size,
2870
+ structure: config.project.structure,
2871
+ language: languageLabel(projectLanguage(config))
2872
+ },
2873
+ scan: {
2874
+ available: scanJson !== null,
2875
+ languages: detected.languages ?? [],
2876
+ frameworks: detected.frameworks ?? [],
2877
+ packageManagers: detected.packageManagers ?? [],
2878
+ sourceDirectories: detected.sourceDirectories ?? [],
2879
+ migrationDirectories: detected.migrationDirectories ?? [],
2880
+ contractFiles: detected.contractFiles ?? [],
2881
+ infrastructureFiles: detected.infrastructureFiles ?? []
2882
+ },
2883
+ knowledge: {
2884
+ summary: knowledgeSummary,
2885
+ roadmapSummary,
2886
+ inventoryAvailable,
2887
+ workItems: workItems.map(toContextWorkItem),
2888
+ artifacts: allArtifacts.filter((a) => a.codeGlobs.length > 0).map(toContextArtifact)
2889
+ },
2616
2890
  layers,
2617
2891
  roadmap,
2892
+ phase,
2893
+ deliveryMix,
2894
+ external: loadExternalCapsules(dir),
2895
+ graph: loadGraphSummary(dir),
2896
+ graphHints: loadGraphHints(dir),
2897
+ skills: discoverInstalledSkills(dir).map((s) => s.id),
2618
2898
  mappedModules,
2619
- missingKnowledge,
2620
- suggestedNextSteps
2899
+ missing,
2900
+ // VS-052: the handoff is driven by the REAL phase, not project.state, so the pack never
2901
+ // contradicts the Current Phase block above it.
2902
+ handoff: {
2903
+ recommendedAgents: phase.recommendedAgents.length > 0 ? phase.recommendedAgents : recommendedAgentsForState(state),
2904
+ nextSteps: phase.nextStep ? [phase.nextStep] : nextStepsForState(state),
2905
+ instructions: phase.llmInstructions.length > 0 ? phase.llmInstructions : LLM_INSTRUCTIONS,
2906
+ operatingRules: OPERATING_RULES
2907
+ }
2621
2908
  };
2622
2909
  }
2623
- function stateLabel(state) {
2624
- return state === "pre-ai" ? "pre-ai" : state;
2625
- }
2626
- function typeLabel(type) {
2627
- const cap = type.charAt(0).toUpperCase() + type.slice(1);
2628
- return cap.endsWith("s") ? cap : `${cap}s`;
2910
+ function serializeContextPackJson(pack) {
2911
+ return JSON.stringify(pack, null, 2) + "\n";
2629
2912
  }
2630
- var LIFECYCLE_LABEL = {
2631
- draft: "Draft",
2632
- ready: "Ready",
2633
- "in-progress": "In Progress",
2634
- blocked: "Blocked",
2635
- completed: "Completed",
2636
- archived: "Archived"
2637
- };
2638
- function renderExplanationHuman(exp) {
2639
- const lines = [];
2640
- lines.push("# Project Explanation");
2641
- lines.push("");
2642
- lines.push("## Project");
2643
- lines.push(`- Name: ${exp.project.name}`);
2644
- lines.push(`- State: ${stateLabel(exp.project.state)}`);
2645
- lines.push(`- Team: ${exp.project.teamSize}`);
2646
- lines.push(`- Structure: ${exp.project.structure}`);
2647
- lines.push(`- Project language: ${exp.project.language}`);
2648
- lines.push("");
2649
- lines.push("## Knowledge Layers");
2650
- lines.push(renderLayersMarkdown(exp.layers));
2651
- lines.push("");
2652
- if (exp.stack) {
2653
- lines.push("## Detected Stack");
2654
- if (exp.stack.language) lines.push(`- Language: ${exp.stack.language}`);
2655
- if (exp.stack.framework) lines.push(`- Framework: ${exp.stack.framework}`);
2656
- if (exp.stack.packageManager) lines.push(`- Package manager: ${exp.stack.packageManager}`);
2657
- if (exp.stack.sourceDirectories.length > 0)
2658
- lines.push(`- Source: ${exp.stack.sourceDirectories.join(", ")}`);
2659
- if (exp.stack.migrationDirectories.length > 0)
2660
- lines.push(`- Migrations: ${exp.stack.migrationDirectories.join(", ")}`);
2661
- if (exp.stack.infrastructureFiles.length > 0)
2662
- lines.push(`- Infrastructure: ${exp.stack.infrastructureFiles.join(", ")}`);
2663
- lines.push("");
2913
+
2914
+ // ../cli/src/templates/context-pack-template.ts
2915
+ function renderContextPack(pack) {
2916
+ const { project, scan, knowledge, missing, handoff } = pack;
2917
+ const parts = [];
2918
+ parts.push("# Kaddo Context Pack\n");
2919
+ parts.push(
2920
+ "> Generated by `kaddo context`. Deterministic CLI output \u2014 not an interpretation of the system.\n"
2921
+ );
2922
+ parts.push("## Operating Rules (read first)\n");
2923
+ parts.push(handoff.operatingRules.map((r) => `- ${r}`).join("\n") + "\n");
2924
+ parts.push("## Project Metadata\n");
2925
+ parts.push(
2926
+ [
2927
+ `- Name: ${project.name}`,
2928
+ `- State: ${project.state}`,
2929
+ `- Team size: ${project.teamSize}`,
2930
+ `- Structure: ${project.structure}`,
2931
+ `- Language: ${project.language}`
2932
+ ].join("\n") + "\n"
2933
+ );
2934
+ parts.push("## Current Phase\n");
2935
+ parts.push(`Phase: ${pack.phase.phase}
2936
+ `);
2937
+ if (pack.phase.reasons.length > 0) {
2938
+ parts.push("Reason:\n");
2939
+ parts.push(pack.phase.reasons.map((r) => `- ${r}`).join("\n") + "\n");
2664
2940
  }
2665
- const ls = (name) => exp.layers.find((l) => l.layer === name)?.status ?? "Missing";
2666
- lines.push("## Knowledge Status");
2667
- lines.push(`- Inventory: ${exp.knowledge.hasInventory ? "available" : "missing"}`);
2668
- lines.push(`- Context pack: ${exp.knowledge.hasContextPack ? "available" : "missing"}`);
2669
- lines.push(`- Business: ${ls("Business")}`);
2670
- lines.push(`- Product: ${ls("Product")}`);
2671
- lines.push(`- Tech: ${ls("Tech")}`);
2672
- lines.push(`- Delivery: ${ls("Delivery")}`);
2673
- lines.push(`- Agents: ${exp.knowledge.hasAgents ? "available" : "missing"}`);
2674
- if (exp.roadmap.present) {
2675
- lines.push(`- Roadmap candidates: ${exp.roadmap.candidates}`);
2676
- lines.push(`- Materialized work items: ${exp.roadmap.materialized}`);
2677
- if (exp.roadmap.remaining > 0)
2678
- lines.push(`- Remaining candidates: ${exp.roadmap.remaining}`);
2679
- } else {
2680
- lines.push(`- Work items: ${exp.workItems.total}`);
2941
+ if (pack.phase.recommendedAgents.length > 0) {
2942
+ parts.push(`Recommended next: ${pack.phase.recommendedAgents.join(", ")}
2943
+ `);
2681
2944
  }
2682
- lines.push(
2683
- `- Ownership coverage: ${exp.ownership.workItemsWithOwnership}/${exp.ownership.workItemsTotal} work items`
2945
+ if (pack.phase.nextStep) {
2946
+ parts.push(`Next step: ${pack.phase.nextStep}
2947
+ `);
2948
+ }
2949
+ parts.push("## Knowledge Layers\n");
2950
+ parts.push(
2951
+ "Project knowledge is organized in four layers: **Business \u2192 Product \u2192 Tech \u2192 Delivery**.\n"
2684
2952
  );
2685
- lines.push("");
2686
- if (exp.workItems.total > 0) {
2687
- lines.push("## Work Items");
2688
- for (const s of LIFECYCLE_STATES) {
2689
- lines.push(`- ${LIFECYCLE_LABEL[s]}: ${exp.workItems.byState[s]}`);
2953
+ const maturity = pack.layers.map((l) => `${l.layer}: ${l.status}`).join(" \xB7 ");
2954
+ parts.push(`Knowledge maturity \u2014 ${maturity}
2955
+ `);
2956
+ parts.push(renderLayersMarkdown(pack.layers) + "\n");
2957
+ parts.push("## Technical Inventory\n");
2958
+ if (scan.available) {
2959
+ const lines = [
2960
+ `- Language: ${scan.languages.join(", ") || "unknown"}`,
2961
+ `- Framework: ${scan.frameworks.join(", ") || "unknown"}`,
2962
+ `- Package manager: ${scan.packageManagers.join(", ") || "unknown"}`
2963
+ ];
2964
+ if (scan.sourceDirectories.length > 0) {
2965
+ lines.push("- Source directories:");
2966
+ scan.sourceDirectories.forEach((d) => lines.push(` - ${d}`));
2690
2967
  }
2691
- lines.push("");
2692
- const typeEntries = Object.entries(exp.workItems.byType).sort((a, b) => b[1] - a[1]);
2693
- if (typeEntries.length > 0) {
2694
- lines.push("## Work Items by Type");
2695
- for (const [t, n] of typeEntries) lines.push(`- ${typeLabel(t)}: ${n}`);
2696
- lines.push("");
2968
+ if (scan.migrationDirectories.length > 0) {
2969
+ lines.push("- Migration directories:");
2970
+ scan.migrationDirectories.forEach((d) => lines.push(` - ${d}`));
2697
2971
  }
2698
- const grouped = exp.workItems.initiatives.filter(
2699
- (g) => LIFECYCLE_STATES.some((s) => g.states[s] > 0)
2972
+ if (scan.contractFiles.length > 0) {
2973
+ lines.push("- Contracts:");
2974
+ scan.contractFiles.forEach((f) => lines.push(` - ${f}`));
2975
+ }
2976
+ if (scan.infrastructureFiles.length > 0) {
2977
+ lines.push("- Infrastructure:");
2978
+ scan.infrastructureFiles.forEach((f) => lines.push(` - ${f}`));
2979
+ }
2980
+ parts.push(lines.join("\n") + "\n");
2981
+ } else {
2982
+ parts.push("Scan baseline missing. Run `kaddo scan` for better context.\n");
2983
+ }
2984
+ parts.push("## Current Knowledge\n");
2985
+ parts.push((knowledge.summary || "No project knowledge summary found yet.") + "\n");
2986
+ parts.push("## Roadmap\n");
2987
+ if (pack.roadmap.present) {
2988
+ parts.push(
2989
+ [
2990
+ `- Roadmap candidates: ${pack.roadmap.candidates}`,
2991
+ `- Materialized work items: ${pack.roadmap.materialized}`,
2992
+ `- Remaining candidates: ${pack.roadmap.remaining}`
2993
+ ].join("\n") + "\n"
2700
2994
  );
2701
- if (grouped.length > 0) {
2702
- lines.push("## Work Items by Initiative");
2703
- for (const g of grouped) {
2704
- const parts = LIFECYCLE_STATES.filter((s) => g.states[s] > 0).map(
2705
- (s) => `${LIFECYCLE_LABEL[s]}: ${g.states[s]}`
2706
- );
2707
- lines.push(`- ${g.name} \u2014 ${parts.join(" \xB7 ")}`);
2708
- }
2709
- lines.push("");
2995
+ if (pack.roadmap.remaining > 0) {
2996
+ parts.push(
2997
+ "Candidates are not yet Work Items. Materialize them with `kaddo create --from roadmap`.\n"
2998
+ );
2710
2999
  }
2711
3000
  }
2712
- const active = exp.workItems.items.filter((i) => i.lifecycle === "in-progress");
2713
- if (active.length > 0) {
2714
- lines.push("## In Progress");
2715
- for (const wi of active) {
3001
+ parts.push((knowledge.roadmapSummary || "No roadmap baseline found.") + "\n");
3002
+ parts.push("## Active Work Items\n");
3003
+ if (knowledge.workItems.length > 0) {
3004
+ const lines = knowledge.workItems.map((wi) => {
2716
3005
  const level = wi.knowledgeLevel ? ` [${wi.knowledgeLevel}]` : "";
2717
- const owned = wi.hasOwnership ? " \u25CF" : " \u25CB";
2718
- lines.push(`-${owned} ${wi.id}${level} \u2014 ${wi.title}`);
2719
- }
2720
- lines.push("");
3006
+ const status = wi.lifecycle ? ` (${wi.lifecycle})` : wi.status ? ` (${wi.status})` : "";
3007
+ const domains = wi.domains.length > 0 ? ` \xB7 domains: ${wi.domains.join(", ")}` : "";
3008
+ return `- ${wi.id || wi.title} [${wi.type}]${level}${status} \u2014 ${wi.title}${domains}`;
3009
+ });
3010
+ parts.push(lines.join("\n") + "\n");
3011
+ } else {
3012
+ parts.push("No active work items found.\n");
2721
3013
  }
2722
- if (exp.domains.length > 0) {
2723
- lines.push("## Domains");
2724
- lines.push(`- ${exp.domains.join(", ")}`);
2725
- lines.push("");
3014
+ const mixEntries = Object.entries(pack.deliveryMix).sort((a, b) => b[1] - a[1]);
3015
+ if (mixEntries.length > 0) {
3016
+ parts.push("## Delivery Mix\n");
3017
+ parts.push("Active Work Items by type:\n");
3018
+ const label = (t) => {
3019
+ const c = t.charAt(0).toUpperCase() + t.slice(1);
3020
+ return c.endsWith("s") ? c : `${c}s`;
3021
+ };
3022
+ parts.push(mixEntries.map(([t, n]) => `- ${label(t)}: ${n}`).join("\n") + "\n");
2726
3023
  }
2727
- lines.push("## Mapped Modules");
2728
- if (exp.mappedModules.length > 0) {
2729
- for (const m of exp.mappedModules) {
2730
- const owner = m.owner ? ` \u2014 owner: ${m.owner}` : "";
2731
- lines.push(`- ${m.id} \u2014 ${m.type ?? "unknown"} \u2014 ${m.repoPath || "\u2014"}${owner}`);
2732
- }
2733
- lines.push("");
2734
- lines.push("## Module Artifact Coverage");
2735
- for (const m of exp.mappedModules) {
2736
- const present = presentArtifacts(m.artifacts);
2737
- lines.push(`- ${m.id}: ${present.length > 0 ? present.join(", ") : "none"}`);
2738
- }
2739
- lines.push("");
3024
+ parts.push("## Artifacts and Ownership\n");
3025
+ if (knowledge.artifacts.length > 0) {
3026
+ const lines = knowledge.artifacts.map(
3027
+ (a) => `- ${a.id || a.title} [${a.type}] owns: ${a.codeGlobs.join(", ")}`
3028
+ );
3029
+ parts.push(lines.join("\n") + "\n");
2740
3030
  } else {
2741
- lines.push("- Mapped modules: 0");
2742
- lines.push("");
2743
- }
2744
- if (exp.externalCapsules.length > 0) {
2745
- lines.push(`## External Knowledge Capsules: ${exp.externalCapsules.length}`);
2746
- for (const cap of exp.externalCapsules) {
2747
- const owner = cap.owner ? ` \u2014 owner: ${cap.owner}` : "";
2748
- lines.push(`- ${cap.system}${owner}`);
2749
- if (cap.ageDays !== null && cap.ageDays >= 90) {
2750
- lines.push(` \u26A0 capsule last updated ${cap.ageDays} days ago \u2014 it may be stale.`);
2751
- }
2752
- }
2753
- lines.push("");
3031
+ parts.push("No artifacts declare code ownership yet.\n");
2754
3032
  }
2755
- if (exp.graph) {
2756
- lines.push("## Knowledge Graph");
2757
- lines.push(`- Scope: ${exp.graph.scope}`);
2758
- lines.push(`- Nodes: ${exp.graph.nodes}`);
2759
- lines.push(`- Edges: ${exp.graph.edges}`);
2760
- if (exp.graphHints) {
2761
- lines.push(`- Quality: ${exp.graphHints.quality}`);
2762
- lines.push(`- Hints: ${exp.graphHints.totalHints}`);
2763
- }
2764
- if (exp.graph.scopeReason) lines.push(`- Reason: ${exp.graph.scopeReason}`);
2765
- if (exp.graph.generatedAt) lines.push(`- Last exported: ${exp.graph.generatedAt}`);
2766
- if (exp.graph.scope === "active" && exp.graphHints?.quality === "empty") {
2767
- lines.push("- Tip: Run `kaddo graph export --scope all` to include completed Work Items");
2768
- }
2769
- lines.push("");
3033
+ if (pack.mappedModules.length > 0) {
3034
+ parts.push("## Mapped Modules\n");
3035
+ parts.push("This project has mapped modules registered in `.kaddo/modules.yml`.\n");
3036
+ const header = "| Module | Type | Repo path | Owner | Capabilities | Artifacts |";
3037
+ const sep = "|---|---|---|---|---|---|";
3038
+ const rows = pack.mappedModules.map((m) => {
3039
+ const caps = m.capabilities.length > 0 ? m.capabilities.join(", ") : "\u2014";
3040
+ const arts = presentArtifacts(m.artifacts);
3041
+ return `| ${m.id} | ${m.type ?? "\u2014"} | ${m.repoPath || "\u2014"} | ${m.owner ?? "\u2014"} | ${caps} | ${arts.length > 0 ? arts.join(", ") : "none"} |`;
3042
+ });
3043
+ parts.push([header, sep, ...rows].join("\n") + "\n");
3044
+ parts.push(
3045
+ "Note: Kaddo does not scan secondary repositories during `context`. Mapped modules come from `.kaddo/modules.yml` and module artifacts only.\n"
3046
+ );
2770
3047
  }
2771
- if (exp.skills.total > 0) {
2772
- lines.push(`## Skills installed: ${exp.skills.total}`);
2773
- const groups = Object.entries(exp.skills.byGroup).sort((a, b) => a[0].localeCompare(b[0]));
2774
- if (groups.length > 0) {
2775
- lines.push("Groups:");
2776
- for (const [g, n] of groups) lines.push(`- ${g}: ${n}`);
3048
+ if (pack.external.length > 0) {
3049
+ parts.push("## External Knowledge\n");
3050
+ parts.push(
3051
+ "Imported Knowledge Capsules \u2014 minimal context about external systems (not their source).\n"
3052
+ );
3053
+ for (const cap of pack.external) {
3054
+ const lines = [`### ${cap.system}`, ""];
3055
+ if (cap.purpose) lines.push(`- Purpose: ${cap.purpose}`);
3056
+ if (cap.capabilities.length) lines.push(`- Capabilities: ${cap.capabilities.join(", ")}`);
3057
+ if (cap.contracts.length) lines.push(`- Contracts: ${cap.contracts.join(", ")}`);
3058
+ if (cap.owner) lines.push(`- Owner: ${cap.owner}`);
3059
+ if (cap.knownRisks.length) lines.push(`- Known risks: ${cap.knownRisks.join("; ")}`);
3060
+ parts.push(lines.join("\n") + "\n");
2777
3061
  }
2778
- lines.push("");
2779
3062
  }
2780
- if (exp.missingKnowledge.length > 0) {
2781
- lines.push("## Missing Knowledge");
2782
- for (const m of exp.missingKnowledge) lines.push(`- ${m}`);
2783
- lines.push("");
3063
+ if (pack.graph) {
3064
+ parts.push("## Knowledge Graph\n");
3065
+ parts.push(
3066
+ [
3067
+ "- Available: yes",
3068
+ `- Scope: ${pack.graph.scope}`,
3069
+ `- Nodes: ${pack.graph.nodes}`,
3070
+ `- Edges: ${pack.graph.edges}`,
3071
+ ...pack.graphHints ? [`- Quality: ${pack.graphHints.quality}`] : [],
3072
+ `- Active Work Items connected to code: ${pack.graph.activeWorkItemsConnectedToCode}`,
3073
+ ...pack.graph.scopeReason ? [`- Reason: ${pack.graph.scopeReason}`] : []
3074
+ ].join("\n") + "\n"
3075
+ );
3076
+ parts.push("Full graph: `.kaddo/graph.json` / `.kaddo/graph.mmd` (run `kaddo graph export` to refresh).\n");
2784
3077
  }
2785
- if (exp.duplicateWorkItems.length > 0) {
2786
- lines.push("## Possible Duplicate Work Items");
2787
- for (const g of exp.duplicateWorkItems) {
2788
- lines.push(`- ${g.reason}:`);
2789
- for (const i of g.items) lines.push(` - ${i.id} \u2014 ${i.title}`);
3078
+ if (pack.graphHints && pack.graphHints.totalHints > 0) {
3079
+ parts.push("## Graph Hints\n");
3080
+ parts.push(
3081
+ [
3082
+ `Graph relationship quality: ${pack.graphHints.quality}`,
3083
+ `Active hints: ${pack.graphHints.activeWorkItemHints}`,
3084
+ "Suggested agent: graph-agent"
3085
+ ].join("\n") + "\n"
3086
+ );
3087
+ const shown = pack.graphHints.messages.slice(0, 3);
3088
+ parts.push(shown.map((m) => `- ${m}`).join("\n") + "\n");
3089
+ if (pack.graphHints.totalHints > shown.length) {
3090
+ parts.push(`(+${pack.graphHints.totalHints - shown.length} more in \`.kaddo/graph-hints.md\`)
3091
+ `);
2790
3092
  }
2791
- lines.push("Review before continuing (non-blocking).");
2792
- lines.push("");
2793
3093
  }
2794
- const assessment = assessPhase(exp);
2795
- lines.push("## Phase");
2796
- lines.push(`- Phase: ${assessment.phase}`);
2797
- if (assessment.reasons.length > 0) {
2798
- lines.push("- Reason:");
2799
- for (const r of assessment.reasons) lines.push(` - ${r}`);
3094
+ if (pack.skills.length > 0) {
3095
+ parts.push("## Skills\n");
3096
+ parts.push("Available reusable skills (agents apply these; content is not inlined):\n");
3097
+ parts.push(pack.skills.map((s) => `- ${s}`).join("\n") + "\n");
3098
+ parts.push("Read full skill definitions in `knowledge/skills/` or via the Kaddo MCP server (`kaddo://skills`).\n");
2800
3099
  }
2801
- if (assessment.nextStep) lines.push(`- Next step: ${assessment.nextStep}`);
2802
- lines.push("");
2803
- if (exp.suggestedNextSteps.length > 0) {
2804
- lines.push("## Suggested Next Steps");
2805
- exp.suggestedNextSteps.forEach((s, i) => lines.push(`${i + 1}. ${s}`));
2806
- lines.push("");
3100
+ parts.push("## Missing Context\n");
3101
+ if (missing.length > 0) {
3102
+ parts.push(missing.map((m) => `- ${m}`).join("\n") + "\n");
3103
+ } else {
3104
+ parts.push("_None \u2014 all expected context is present._\n");
2807
3105
  }
2808
- return lines.join("\n").trimEnd() + "\n";
2809
- }
2810
- function renderExplanationAgent(exp) {
2811
- const { mappedModules, ...rest } = exp;
2812
- return JSON.stringify({ ...rest, mapped_modules: mappedModules }, null, 2) + "\n";
3106
+ parts.push("## Recommended Agent Handoff\n");
3107
+ parts.push(`Recommended next for the **${pack.phase.phase}** phase:
3108
+ `);
3109
+ parts.push(handoff.recommendedAgents.map((a, i) => `${i + 1}. ${a}`).join("\n") + "\n");
3110
+ if (handoff.nextSteps.length > 0) {
3111
+ parts.push("Next step:\n");
3112
+ parts.push(handoff.nextSteps.map((s) => `- ${s}`).join("\n") + "\n");
3113
+ }
3114
+ parts.push("## Instructions for the LLM\n");
3115
+ parts.push(handoff.instructions.map((i) => `- ${i}`).join("\n") + "\n");
3116
+ return parts.join("\n");
2813
3117
  }
2814
3118
 
2815
3119
  // ../cli/src/agents/groups.ts
@@ -3046,6 +3350,22 @@ function generateGraph(root, scope = "active") {
3046
3350
  next_suggested_resources: ["kaddo://graph", "kaddo://graph-hints"]
3047
3351
  };
3048
3352
  }
3353
+ function generateImpactReport(root, opts = {}) {
3354
+ requireConfig(root);
3355
+ requireKnowledge(root);
3356
+ const format = opts.format ?? "markdown";
3357
+ const report = buildImpactReport(root, { scope: opts.scope ?? "all" });
3358
+ const content = format === "json" ? serializeImpactJson(report) : renderImpactMarkdown(report);
3359
+ const out = opts.output ?? `.kaddo/reports/impact-report.${format === "json" ? "json" : "md"}`;
3360
+ writeDerived(root, out, content);
3361
+ return {
3362
+ status: "ok",
3363
+ files_written: [out.replace(/\\/g, "/")],
3364
+ summary: `Impact report generated (${format}, ${report.scope} scope).`,
3365
+ warnings: report.score === null ? ["Impact score not available \u2014 add Work Items to compute it."] : [],
3366
+ next_suggested_resources: ["kaddo://impact-report"]
3367
+ };
3368
+ }
3049
3369
  function generateCapsuleDraft(root) {
3050
3370
  const config = requireConfig(root);
3051
3371
  requireKnowledge(root);
@@ -3229,6 +3549,23 @@ function createServer(root) {
3229
3549
  },
3230
3550
  async (args) => generated(root, "generate knowledge graph", () => generateGraph(root, args.scope ?? "active"))
3231
3551
  );
3552
+ server.registerTool(
3553
+ "kaddo_generate_impact_report",
3554
+ {
3555
+ title: "Generate impact report",
3556
+ description: `Write the Knowledge Impact Report under .kaddo/reports/. format: markdown (default) or json; scope: active or all (default all). ${DERIVED_NOTE}`,
3557
+ inputSchema: {
3558
+ format: z2.enum(["markdown", "json"]).optional(),
3559
+ scope: z2.enum(["active", "all"]).optional(),
3560
+ output: z2.string().optional()
3561
+ }
3562
+ },
3563
+ async (args) => generated(
3564
+ root,
3565
+ "generate impact report",
3566
+ () => generateImpactReport(root, { format: args.format, scope: args.scope, output: args.output })
3567
+ )
3568
+ );
3232
3569
  server.registerTool(
3233
3570
  "kaddo_generate_capsule_draft",
3234
3571
  { title: "Generate capsule draft", description: `Write a capsule DRAFT under .kaddo/exports/ (never registers it). ${DERIVED_NOTE}`, inputSchema: {} },