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