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