tldr-experts 0.3.0 → 0.4.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/CHANGELOG.md +1957 -0
- package/README.md +99 -12
- package/dist/hooks/answer-capture.js +21 -12
- package/dist/hooks/budget-gate.js +95 -11
- package/dist/hooks/{chunk-0bt6yb2q.js → chunk-39zh2e44.js} +10 -1
- package/dist/hooks/chunk-4cp363kv.js +1766 -0
- package/dist/hooks/{chunk-t8tdv11p.js → chunk-7y2dq0pj.js} +1 -1
- package/dist/hooks/chunk-9zsqxr6y.js +213 -0
- package/dist/hooks/{chunk-a8p2rc94.js → chunk-b8kxzna2.js} +1 -1
- package/dist/hooks/{chunk-j234zf0t.js → chunk-c6t5nx0r.js} +37 -166
- package/dist/hooks/{chunk-g395gk7e.js → chunk-m3mewgnw.js} +36 -17
- package/dist/hooks/chunk-phmdk72a.js +164 -0
- package/dist/hooks/chunk-rpcxsqh3.js +199 -0
- package/dist/hooks/{chunk-azctppjh.js → chunk-rz541e2b.js} +7 -1
- package/dist/hooks/{chunk-y0jdr3et.js → chunk-s1c5h7yx.js} +71 -29
- package/dist/hooks/{chunk-p274ckxv.js → chunk-sq44k6g2.js} +155 -12
- package/dist/hooks/{chunk-x98qs959.js → chunk-t1ywrfr4.js} +35 -42
- package/dist/hooks/{chunk-1zwcxd3f.js → chunk-tzzwddct.js} +1 -1
- package/dist/hooks/claim-sources.js +31 -23
- package/dist/hooks/dod-gate.js +12 -37
- package/dist/hooks/no-reask.js +9 -9
- package/dist/hooks/session-start.js +176 -422
- package/dist/hooks/statusline.js +14 -11
- package/dist/tldrx.js +22798 -12309
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/tldrx/SKILL.md +16 -3
- package/stages/build/stage.md +5 -0
- package/stages/build/stage.yml +2 -1
- package/stages/plan/stage.md +11 -0
- package/templates/budget.yml +5 -0
- package/templates/epic.md +5 -3
- package/dist/hooks/chunk-ae6bkfs5.js +0 -0
- package/dist/hooks/chunk-kw4tffzf.js +0 -139
- package/dist/hooks/chunk-sdjnnmzz.js +0 -497
- package/dist/hooks/chunk-t56k6146.js +0 -14
|
@@ -1,36 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import"./chunk-
|
|
3
|
-
import"./chunk-x98qs959.js";
|
|
2
|
+
import"./chunk-t1ywrfr4.js";
|
|
4
3
|
import {
|
|
5
4
|
allow,
|
|
6
5
|
readPayload,
|
|
7
6
|
runHook,
|
|
8
7
|
sessionContext
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import"./chunk-
|
|
8
|
+
} from "./chunk-tzzwddct.js";
|
|
9
|
+
import"./chunk-b8kxzna2.js";
|
|
11
10
|
import {
|
|
12
11
|
RunStore,
|
|
13
12
|
isMovable,
|
|
14
13
|
openQuestions,
|
|
15
14
|
runSnapshot,
|
|
16
15
|
whatIsWaiting
|
|
17
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-s1c5h7yx.js";
|
|
18
17
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
evidencePath,
|
|
19
|
+
expertsDir,
|
|
20
|
+
gateEvidencePath,
|
|
21
|
+
loadExperts,
|
|
22
|
+
openRunViews,
|
|
23
|
+
parseEvidence,
|
|
24
|
+
pathsIntersect,
|
|
25
|
+
readExpertDomain,
|
|
26
|
+
stackExpertNames
|
|
27
|
+
} from "./chunk-4cp363kv.js";
|
|
28
|
+
import"./chunk-rz541e2b.js";
|
|
29
|
+
import"./chunk-9zsqxr6y.js";
|
|
30
|
+
import"./chunk-phmdk72a.js";
|
|
31
|
+
import"./chunk-c6t5nx0r.js";
|
|
23
32
|
import {
|
|
24
33
|
openBlocks,
|
|
25
34
|
parseQuestions
|
|
26
|
-
} from "./chunk-
|
|
27
|
-
import"./chunk-
|
|
28
|
-
import {
|
|
29
|
-
classifySrc
|
|
30
|
-
} from "./chunk-g395gk7e.js";
|
|
35
|
+
} from "./chunk-rpcxsqh3.js";
|
|
36
|
+
import"./chunk-m3mewgnw.js";
|
|
31
37
|
import {
|
|
32
38
|
isRecord
|
|
33
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-39zh2e44.js";
|
|
34
40
|
import {
|
|
35
41
|
PROJECT_FRAMEWORK_DIR,
|
|
36
42
|
PROJECT_WORK_DIR,
|
|
@@ -39,7 +45,7 @@ import {
|
|
|
39
45
|
findWorkspaceRoot,
|
|
40
46
|
listRunDirs,
|
|
41
47
|
parseYaml
|
|
42
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-sq44k6g2.js";
|
|
43
49
|
|
|
44
50
|
// src/core/status/PendingItem.ts
|
|
45
51
|
function isPending(item) {
|
|
@@ -97,39 +103,6 @@ function statusLineOf(text) {
|
|
|
97
103
|
}
|
|
98
104
|
var DOCUMENT_EXTENSIONS = new Set([".md", ".txt", ".markdown", ".rst", ".adoc", ".pdf", ".doc", ".docx"]);
|
|
99
105
|
|
|
100
|
-
// src/core/detect/walk.ts
|
|
101
|
-
var SKIPPED_DIRS = new Set([
|
|
102
|
-
".git",
|
|
103
|
-
"node_modules",
|
|
104
|
-
"dist",
|
|
105
|
-
"build",
|
|
106
|
-
"out",
|
|
107
|
-
"bin",
|
|
108
|
-
"obj",
|
|
109
|
-
"target",
|
|
110
|
-
".venv",
|
|
111
|
-
"venv",
|
|
112
|
-
"__pycache__",
|
|
113
|
-
".next",
|
|
114
|
-
".expo",
|
|
115
|
-
".gradle",
|
|
116
|
-
"vendor",
|
|
117
|
-
"coverage",
|
|
118
|
-
"graphify-out",
|
|
119
|
-
".tldrx",
|
|
120
|
-
"tldrx-work",
|
|
121
|
-
".idea",
|
|
122
|
-
".vscode",
|
|
123
|
-
"Pods",
|
|
124
|
-
"DerivedData",
|
|
125
|
-
".terraform",
|
|
126
|
-
".turbo",
|
|
127
|
-
".cache",
|
|
128
|
-
"storybook-static",
|
|
129
|
-
"playwright-report",
|
|
130
|
-
"test-results"
|
|
131
|
-
]);
|
|
132
|
-
|
|
133
106
|
// src/core/seed/collectSeed.ts
|
|
134
107
|
var MAX_SEED_BYTES = 2 * 1024 * 1024;
|
|
135
108
|
|
|
@@ -454,6 +427,125 @@ function chainsOf(inputs, runs) {
|
|
|
454
427
|
return chains;
|
|
455
428
|
}
|
|
456
429
|
|
|
430
|
+
// src/core/run/decisionCards.ts
|
|
431
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
432
|
+
import { join as join3 } from "node:path";
|
|
433
|
+
function questionsCard(ctx) {
|
|
434
|
+
const blocks = openQuestionBlocks(join3(ctx.runDir, ctx.phaseId, "questions.md"));
|
|
435
|
+
if (blocks.length === 0)
|
|
436
|
+
return null;
|
|
437
|
+
const recommendations = readRecommendations(ctx);
|
|
438
|
+
return {
|
|
439
|
+
kind: "questions",
|
|
440
|
+
run: ctx.runId,
|
|
441
|
+
gate: `${ctx.phaseId}/${ctx.stageId}`,
|
|
442
|
+
headline: "",
|
|
443
|
+
questions: blocks.map((block) => toQuestion(block, ctx.runId, recommendations)),
|
|
444
|
+
detail: [],
|
|
445
|
+
commands: []
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function toQuestion(block, runId, recommendations) {
|
|
449
|
+
const recommended = recommendations.get(block.id);
|
|
450
|
+
return {
|
|
451
|
+
id: block.id,
|
|
452
|
+
title: block.title,
|
|
453
|
+
whyAsked: block.whyAsked,
|
|
454
|
+
options: block.options.map(firstLine),
|
|
455
|
+
recommendation: recommended === undefined ? null : toRecommendation(recommended),
|
|
456
|
+
answerCommand: `tldrx answer ${block.id} "…" --run ${runId}`
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function firstLine(option) {
|
|
460
|
+
return { letter: option.letter, text: option.text.split(`
|
|
461
|
+
`)[0] ?? "" };
|
|
462
|
+
}
|
|
463
|
+
function toRecommendation(entry) {
|
|
464
|
+
return { option: entry.option, why: entry.why, src: entry.src };
|
|
465
|
+
}
|
|
466
|
+
function openQuestionBlocks(path) {
|
|
467
|
+
if (!existsSync3(path))
|
|
468
|
+
return [];
|
|
469
|
+
try {
|
|
470
|
+
return openBlocks(parseQuestions(readFileSync3(path, "utf8")).blocks);
|
|
471
|
+
} catch {
|
|
472
|
+
return [];
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function readRecommendations(ctx) {
|
|
476
|
+
const out = new Map;
|
|
477
|
+
const paths = [
|
|
478
|
+
evidencePath(ctx.runDir, ctx.stageId),
|
|
479
|
+
gateEvidencePath(ctx.runDir, ctx.phaseId, ctx.stageId)
|
|
480
|
+
];
|
|
481
|
+
for (const path of paths) {
|
|
482
|
+
const text = readOrNull(path);
|
|
483
|
+
if (text === null)
|
|
484
|
+
continue;
|
|
485
|
+
const front = parseEvidence(text).front;
|
|
486
|
+
if (front === null)
|
|
487
|
+
continue;
|
|
488
|
+
for (const entry of front.recommend) {
|
|
489
|
+
if (!out.has(entry.q))
|
|
490
|
+
out.set(entry.q, entry);
|
|
491
|
+
}
|
|
492
|
+
if (out.size > 0)
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
return out;
|
|
496
|
+
}
|
|
497
|
+
function readOrNull(path) {
|
|
498
|
+
if (!existsSync3(path))
|
|
499
|
+
return null;
|
|
500
|
+
try {
|
|
501
|
+
return readFileSync3(path, "utf8");
|
|
502
|
+
} catch {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/core/ui/decisionCard.ts
|
|
508
|
+
function decisionHeader(card) {
|
|
509
|
+
return `DECISION — ${card.run} · ${card.gate}`;
|
|
510
|
+
}
|
|
511
|
+
function renderDecisionCard(card) {
|
|
512
|
+
const lines = [decisionHeader(card)];
|
|
513
|
+
if (card.kind === "questions") {
|
|
514
|
+
card.questions.forEach((question, index) => {
|
|
515
|
+
if (index > 0)
|
|
516
|
+
lines.push("");
|
|
517
|
+
lines.push(...renderQuestion(question));
|
|
518
|
+
});
|
|
519
|
+
return lines;
|
|
520
|
+
}
|
|
521
|
+
if (card.headline !== "")
|
|
522
|
+
lines.push(card.headline);
|
|
523
|
+
for (const line of card.detail)
|
|
524
|
+
lines.push(` ${line}`);
|
|
525
|
+
for (const command of card.commands)
|
|
526
|
+
lines.push(` ${command}`);
|
|
527
|
+
return lines;
|
|
528
|
+
}
|
|
529
|
+
function renderQuestion(question) {
|
|
530
|
+
const lines = [`${question.id} · ${question.title}`];
|
|
531
|
+
if (question.whyAsked !== null)
|
|
532
|
+
lines.push(` Why asked: ${question.whyAsked}`);
|
|
533
|
+
for (const option of question.options)
|
|
534
|
+
lines.push(` ${option.letter}) ${option.text}`);
|
|
535
|
+
const recommendation = renderRecommendation(question.recommendation);
|
|
536
|
+
if (recommendation !== null)
|
|
537
|
+
lines.push(recommendation);
|
|
538
|
+
lines.push(` ${question.answerCommand}`);
|
|
539
|
+
return lines;
|
|
540
|
+
}
|
|
541
|
+
function renderRecommendation(recommendation) {
|
|
542
|
+
if (recommendation === null)
|
|
543
|
+
return null;
|
|
544
|
+
const why = recommendation.why.trim();
|
|
545
|
+
const head = `Recommends ${recommendation.option}${why === "" ? "" : ` — ${why}`}`;
|
|
546
|
+
return recommendation.src.trim() === "" ? head : `${head} [src: ${recommendation.src.trim()}]`;
|
|
547
|
+
}
|
|
548
|
+
|
|
457
549
|
// src/core/status/runItems.ts
|
|
458
550
|
var NEXT_MARK = "← next";
|
|
459
551
|
function dependencyInputs(root) {
|
|
@@ -508,8 +600,17 @@ function runItems(root) {
|
|
|
508
600
|
if (waiting.kind === "gate") {
|
|
509
601
|
details.push(`disagree instead: \`tldrx reject --run ${run.run} --note "<why>"\``);
|
|
510
602
|
}
|
|
511
|
-
if (waiting.kind === "answer"
|
|
512
|
-
|
|
603
|
+
if (waiting.kind === "answer") {
|
|
604
|
+
const card = questionsCard({
|
|
605
|
+
runDir: store.runDir,
|
|
606
|
+
runId: run.run,
|
|
607
|
+
phaseId: run.cursor.phase,
|
|
608
|
+
stageId: run.cursor.stage
|
|
609
|
+
});
|
|
610
|
+
if (card === null)
|
|
611
|
+
details.push(`open questions: ${waiting.questions.join(", ")}`);
|
|
612
|
+
else
|
|
613
|
+
details.push(...renderDecisionCard(card));
|
|
513
614
|
}
|
|
514
615
|
if (waiting.kind === "failed")
|
|
515
616
|
details.push(waiting.message);
|
|
@@ -593,356 +694,12 @@ function commandFor(run, waiting) {
|
|
|
593
694
|
return "";
|
|
594
695
|
}
|
|
595
696
|
}
|
|
596
|
-
// src/core/experts/loadExperts.ts
|
|
597
|
-
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
|
598
|
-
import { join as join3 } from "node:path";
|
|
599
|
-
|
|
600
|
-
// src/core/init/competencyLevel.ts
|
|
601
|
-
var EVIDENCE_KINDS = ["code", "run", "test", "doc", "answer"];
|
|
602
|
-
var ALLOWED_KINDS = EVIDENCE_KINDS.join(", ");
|
|
603
|
-
function isEvidenceKind(value) {
|
|
604
|
-
return EVIDENCE_KINDS.includes(value);
|
|
605
|
-
}
|
|
606
|
-
var EVIDENCE_CONFIDENCES = ["measured", "inferred", "assumed"];
|
|
607
|
-
function isEvidenceConfidence(value) {
|
|
608
|
-
return EVIDENCE_CONFIDENCES.includes(value);
|
|
609
|
-
}
|
|
610
|
-
var WEIGHTS = { code: 1, run: 1, test: 1, doc: 0.5, answer: 0.8 };
|
|
611
|
-
var CROSS_MULTIPLIER = 2;
|
|
612
|
-
var ASSUMED_MULTIPLIER = 0.5;
|
|
613
|
-
var THRESHOLDS = [0.5, 1.5, 3, 6, 20];
|
|
614
|
-
var RECENCY_FLOOR = 0.25;
|
|
615
|
-
var RECENCY_HALFLIFE_DAYS = 365;
|
|
616
|
-
var NO_RUN_CAP = 3;
|
|
617
|
-
var MIN_KINDS_FOR_TOP = 2;
|
|
618
|
-
var DAY_MS = 86400000;
|
|
619
|
-
function competencyLevel(evidence, now = new Date) {
|
|
620
|
-
if (evidence.length === 0)
|
|
621
|
-
return 0;
|
|
622
|
-
let weighted = 0;
|
|
623
|
-
const sources = new Set;
|
|
624
|
-
const kinds = new Set;
|
|
625
|
-
for (const item of evidence) {
|
|
626
|
-
const ageDays = daysBetween(item.at, now);
|
|
627
|
-
if (ageDays === null)
|
|
628
|
-
continue;
|
|
629
|
-
weighted += recency(ageDays) * weightOf(item);
|
|
630
|
-
sources.add(item.src);
|
|
631
|
-
kinds.add(item.kind);
|
|
632
|
-
}
|
|
633
|
-
let level = THRESHOLDS.filter((threshold) => weighted >= threshold).length;
|
|
634
|
-
if (!kinds.has("run"))
|
|
635
|
-
level = Math.min(level, NO_RUN_CAP);
|
|
636
|
-
if (level === 5 && kinds.size < MIN_KINDS_FOR_TOP)
|
|
637
|
-
level = 4;
|
|
638
|
-
return Math.min(level, sources.size);
|
|
639
|
-
}
|
|
640
|
-
function weightOf(item) {
|
|
641
|
-
const base = WEIGHTS[item.kind];
|
|
642
|
-
const cross = item.cross === true ? CROSS_MULTIPLIER : 1;
|
|
643
|
-
const confidence = item.confidence === "assumed" ? ASSUMED_MULTIPLIER : 1;
|
|
644
|
-
return base * cross * confidence;
|
|
645
|
-
}
|
|
646
|
-
function recency(ageDays) {
|
|
647
|
-
return Math.max(RECENCY_FLOOR, 1 - ageDays / RECENCY_HALFLIFE_DAYS);
|
|
648
|
-
}
|
|
649
|
-
function daysBetween(at, now) {
|
|
650
|
-
const then = Date.parse(at);
|
|
651
|
-
if (Number.isNaN(then))
|
|
652
|
-
return null;
|
|
653
|
-
return Math.max(0, (now.getTime() - then) / DAY_MS);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// src/core/experts/evidenceSrc.ts
|
|
657
|
-
function checkEvidenceSrc(kind, src) {
|
|
658
|
-
const parsed = classifySrc(src);
|
|
659
|
-
if ("message" in parsed)
|
|
660
|
-
return { reason: "malformed-src" };
|
|
661
|
-
return matchesKind(kind, parsed) ? null : { reason: "kind-mismatch" };
|
|
662
|
-
}
|
|
663
|
-
function matchesKind(kind, ref) {
|
|
664
|
-
switch (kind) {
|
|
665
|
-
case "code":
|
|
666
|
-
return ref.kind === "file";
|
|
667
|
-
case "run":
|
|
668
|
-
return ref.kind === "cmd" || isRunArtifact(ref);
|
|
669
|
-
case "test":
|
|
670
|
-
return ref.kind === "file" || ref.kind === "cmd";
|
|
671
|
-
case "doc":
|
|
672
|
-
return ref.kind === "doc";
|
|
673
|
-
case "answer":
|
|
674
|
-
return ref.kind === "fact";
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
function isRunArtifact(ref) {
|
|
678
|
-
return ref.kind === "file" && ref.repo === null && ref.path.startsWith("tldrx-work/");
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// src/core/experts/readEvidenceRows.ts
|
|
682
|
-
function readEvidenceRows(input) {
|
|
683
|
-
if (!Array.isArray(input))
|
|
684
|
-
return { evidence: [], ignored: [] };
|
|
685
|
-
const evidence = [];
|
|
686
|
-
const counts = new Map;
|
|
687
|
-
const refuse = (reason, kind, src) => {
|
|
688
|
-
const key = `${reason} ${reason === "unknown-kind" ? kind : `${kind} ${src}`}`;
|
|
689
|
-
const seen = counts.get(key);
|
|
690
|
-
counts.set(key, seen === undefined ? { reason, kind, src, count: 1 } : { ...seen, count: seen.count + 1 });
|
|
691
|
-
};
|
|
692
|
-
for (const raw of input) {
|
|
693
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
694
|
-
continue;
|
|
695
|
-
const row = raw;
|
|
696
|
-
const kind = str(row.kind);
|
|
697
|
-
const src = str(row.src);
|
|
698
|
-
const at = str(row.at);
|
|
699
|
-
if (src === "" || at === "")
|
|
700
|
-
continue;
|
|
701
|
-
if (!isEvidenceKind(kind)) {
|
|
702
|
-
refuse("unknown-kind", kind, "");
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
const problem = checkEvidenceSrc(kind, src);
|
|
706
|
-
if (problem !== null) {
|
|
707
|
-
refuse(problem.reason, kind, src);
|
|
708
|
-
continue;
|
|
709
|
-
}
|
|
710
|
-
const confidence = str(row.confidence);
|
|
711
|
-
evidence.push({
|
|
712
|
-
kind,
|
|
713
|
-
src,
|
|
714
|
-
at,
|
|
715
|
-
...row.cross === true ? { cross: true } : {},
|
|
716
|
-
...isEvidenceConfidence(confidence) ? { confidence } : {}
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
return { evidence, ignored: [...counts.values()] };
|
|
720
|
-
}
|
|
721
|
-
function str(value) {
|
|
722
|
-
return typeof value === "string" ? value : "";
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// src/core/experts/loadExperts.ts
|
|
726
|
-
var EXPERTS_DIRNAME = "experts";
|
|
727
|
-
var COMPETENCIES_FILE = "competencies.yml";
|
|
728
|
-
var EXPERT_FILE = "expert.md";
|
|
729
|
-
function expertsDir(root) {
|
|
730
|
-
return join3(root, PROJECT_FRAMEWORK_DIR, EXPERTS_DIRNAME);
|
|
731
|
-
}
|
|
732
|
-
function expertDir(root, name) {
|
|
733
|
-
return join3(expertsDir(root), name);
|
|
734
|
-
}
|
|
735
|
-
function loadExperts(root, now = new Date) {
|
|
736
|
-
const dir = expertsDir(root);
|
|
737
|
-
if (!existsSync3(dir))
|
|
738
|
-
return [];
|
|
739
|
-
const names = readdirSync2(dir).filter((entry) => statSync2(join3(dir, entry)).isDirectory()).sort();
|
|
740
|
-
return names.map((name) => loadExpert(root, name, now));
|
|
741
|
-
}
|
|
742
|
-
function loadExpert(root, name, now = new Date) {
|
|
743
|
-
const dir = expertDir(root, name);
|
|
744
|
-
const path = join3(dir, COMPETENCIES_FILE);
|
|
745
|
-
const empty = { name, dir, status: "unknown", lastTrained: null, areas: [], drifted: [] };
|
|
746
|
-
if (!existsSync3(path))
|
|
747
|
-
return { ...empty, error: `${COMPETENCIES_FILE} is missing` };
|
|
748
|
-
let doc;
|
|
749
|
-
try {
|
|
750
|
-
const parsed = parseYaml(readFileSync3(path, "utf8"));
|
|
751
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
752
|
-
return { ...empty, error: `${COMPETENCIES_FILE} is not a mapping` };
|
|
753
|
-
}
|
|
754
|
-
doc = parsed;
|
|
755
|
-
} catch (error) {
|
|
756
|
-
return { ...empty, error: error instanceof Error ? error.message : String(error) };
|
|
757
|
-
}
|
|
758
|
-
const areas = Array.isArray(doc.areas) ? doc.areas.map((area) => toArea(name, area, now)).filter((area) => area !== null) : [];
|
|
759
|
-
return {
|
|
760
|
-
name: typeof doc.expert === "string" && doc.expert !== "" ? doc.expert : name,
|
|
761
|
-
dir,
|
|
762
|
-
status: typeof doc.status === "string" ? doc.status : "unknown",
|
|
763
|
-
lastTrained: typeof doc.last_trained === "string" ? doc.last_trained : null,
|
|
764
|
-
areas,
|
|
765
|
-
drifted: areas.filter((area) => area.storedLevel !== null && area.storedLevel !== area.level),
|
|
766
|
-
error: null
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
function toArea(expert, input, now) {
|
|
770
|
-
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
771
|
-
return null;
|
|
772
|
-
const raw = input;
|
|
773
|
-
const id = str2(raw.id) !== "" ? str2(raw.id) : str2(raw.area);
|
|
774
|
-
if (id === "")
|
|
775
|
-
return null;
|
|
776
|
-
const { evidence, ignored } = readEvidenceRows(raw.evidence);
|
|
777
|
-
return {
|
|
778
|
-
id,
|
|
779
|
-
title: str2(raw.title) !== "" ? str2(raw.title) : id,
|
|
780
|
-
storedLevel: typeof raw.level === "number" ? raw.level : null,
|
|
781
|
-
level: competencyLevel(evidence, now),
|
|
782
|
-
trainPrompt: str2(raw.train_prompt) !== "" ? str2(raw.train_prompt) : `tldrx expert train ${expert} --area ${id} --mode light`,
|
|
783
|
-
evidence,
|
|
784
|
-
ignored,
|
|
785
|
-
newestEvidence: newestDate(evidence)
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
function newestDate(evidence) {
|
|
789
|
-
let newest = null;
|
|
790
|
-
for (const item of evidence) {
|
|
791
|
-
if (newest === null || item.at > newest)
|
|
792
|
-
newest = item.at;
|
|
793
|
-
}
|
|
794
|
-
return newest;
|
|
795
|
-
}
|
|
796
|
-
function str2(value) {
|
|
797
|
-
return typeof value === "string" ? value : "";
|
|
798
|
-
}
|
|
799
697
|
// src/core/experts/stageCoverage.ts
|
|
800
|
-
import { existsSync as
|
|
801
|
-
import { join as join6 } from "node:path";
|
|
802
|
-
|
|
803
|
-
// src/core/experts/stackExperts.ts
|
|
804
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
698
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync3 } from "node:fs";
|
|
805
699
|
import { join as join4 } from "node:path";
|
|
806
|
-
function stackExpertNames(root, repos) {
|
|
807
|
-
const path = join4(root, PROJECT_FRAMEWORK_DIR, "workspace.yml");
|
|
808
|
-
if (!existsSync4(path))
|
|
809
|
-
return [];
|
|
810
|
-
let doc;
|
|
811
|
-
try {
|
|
812
|
-
doc = parseYaml(readFileSync4(path, "utf8"));
|
|
813
|
-
} catch {
|
|
814
|
-
return [];
|
|
815
|
-
}
|
|
816
|
-
const list = doc?.repos;
|
|
817
|
-
if (!Array.isArray(list))
|
|
818
|
-
return [];
|
|
819
|
-
const names = [];
|
|
820
|
-
for (const row of list) {
|
|
821
|
-
if (typeof row?.name !== "string" || !repos.includes(row.name))
|
|
822
|
-
continue;
|
|
823
|
-
const stack = Array.isArray(row.stack) ? row.stack : [];
|
|
824
|
-
for (const language of stack) {
|
|
825
|
-
if (typeof language !== "string" || language === "")
|
|
826
|
-
continue;
|
|
827
|
-
const expert = `${language}-stack`;
|
|
828
|
-
if (!names.includes(expert))
|
|
829
|
-
names.push(expert);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
return names;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// src/core/experts/selectExperts.ts
|
|
836
|
-
import { existsSync as existsSync6, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
|
|
837
|
-
|
|
838
|
-
// src/core/experts/expertDocument.ts
|
|
839
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
840
|
-
import { join as join5 } from "node:path";
|
|
841
|
-
var H2_RE = /^##\s+(.+?)\s*$/;
|
|
842
|
-
function readExpertDocument(root, name) {
|
|
843
|
-
const path = join5(expertDir(root, name), EXPERT_FILE);
|
|
844
|
-
if (!existsSync5(path)) {
|
|
845
|
-
return { path, exists: false, frontMatter: new Map, body: "" };
|
|
846
|
-
}
|
|
847
|
-
const text = readFileSync5(path, "utf8");
|
|
848
|
-
const { frontMatter, body } = splitFrontMatter(text);
|
|
849
|
-
return { path, exists: true, frontMatter, body };
|
|
850
|
-
}
|
|
851
|
-
function splitFrontMatter(text) {
|
|
852
|
-
const frontMatter = new Map;
|
|
853
|
-
if (!text.startsWith(`---
|
|
854
|
-
`))
|
|
855
|
-
return { frontMatter, body: text };
|
|
856
|
-
const end = text.indexOf(`
|
|
857
|
-
---`, 3);
|
|
858
|
-
if (end === -1)
|
|
859
|
-
return { frontMatter, body: text };
|
|
860
|
-
for (const line of text.slice(4, end).split(`
|
|
861
|
-
`)) {
|
|
862
|
-
const colon = line.indexOf(":");
|
|
863
|
-
if (colon <= 0 || line.trimStart().startsWith("#"))
|
|
864
|
-
continue;
|
|
865
|
-
const key = line.slice(0, colon).trim();
|
|
866
|
-
const value = line.slice(colon + 1).replace(/#.*$/, "").trim().replace(/^"(.*)"$/, "$1");
|
|
867
|
-
if (key !== "")
|
|
868
|
-
frontMatter.set(key, value);
|
|
869
|
-
}
|
|
870
|
-
const newline = text.indexOf(`
|
|
871
|
-
`, end + 1);
|
|
872
|
-
return { frontMatter, body: newline === -1 ? "" : text.slice(newline + 1) };
|
|
873
|
-
}
|
|
874
|
-
function section(body, heading) {
|
|
875
|
-
const lines = body.split(`
|
|
876
|
-
`);
|
|
877
|
-
const collected = [];
|
|
878
|
-
let inside = false;
|
|
879
|
-
for (const line of lines) {
|
|
880
|
-
const match = H2_RE.exec(line);
|
|
881
|
-
if (match !== null) {
|
|
882
|
-
if (inside)
|
|
883
|
-
break;
|
|
884
|
-
inside = match[1] === heading;
|
|
885
|
-
continue;
|
|
886
|
-
}
|
|
887
|
-
if (inside)
|
|
888
|
-
collected.push(line);
|
|
889
|
-
}
|
|
890
|
-
return collected.join(`
|
|
891
|
-
`).trim();
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// src/core/experts/expertDomain.ts
|
|
895
|
-
var PLACEHOLDER_PATHS = new Set(["path/to/module", "path/to/other"]);
|
|
896
|
-
function readExpertDomain(root, name) {
|
|
897
|
-
const doc = readExpertDocument(root, name);
|
|
898
|
-
if (!doc.exists)
|
|
899
|
-
return { name, exists: false, kind: "", repos: [], paths: [] };
|
|
900
|
-
return {
|
|
901
|
-
name,
|
|
902
|
-
exists: true,
|
|
903
|
-
kind: doc.frontMatter.get("kind") ?? "",
|
|
904
|
-
repos: parseList(doc.frontMatter.get("repos") ?? ""),
|
|
905
|
-
paths: domainPaths(section(doc.body, "Domain"))
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
function parseList(value) {
|
|
909
|
-
const inner = value.trim().replace(/^\[/, "").replace(/\]$/, "");
|
|
910
|
-
return inner.split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter((item) => item !== "");
|
|
911
|
-
}
|
|
912
|
-
function domainPaths(body) {
|
|
913
|
-
const paths = [];
|
|
914
|
-
for (const line of body.split(`
|
|
915
|
-
`)) {
|
|
916
|
-
const bullet = /^\s*[-*]\s+(.*)$/.exec(line);
|
|
917
|
-
if (bullet === null)
|
|
918
|
-
continue;
|
|
919
|
-
const text = (bullet[1] ?? "").trim();
|
|
920
|
-
if (/^repo\s/i.test(text))
|
|
921
|
-
continue;
|
|
922
|
-
const code = /`([^`]+)`/.exec(text);
|
|
923
|
-
const raw = (code?.[1] ?? text.split(/\s+—\s+/)[0] ?? "").trim();
|
|
924
|
-
const path = normalisePath(raw);
|
|
925
|
-
if (path === "" || PLACEHOLDER_PATHS.has(path) || !path.includes("/"))
|
|
926
|
-
continue;
|
|
927
|
-
if (!paths.includes(path))
|
|
928
|
-
paths.push(path);
|
|
929
|
-
}
|
|
930
|
-
return paths;
|
|
931
|
-
}
|
|
932
|
-
function normalisePath(raw) {
|
|
933
|
-
return raw.trim().replace(/^[a-z0-9-]+:(?=[^/])/i, "").replace(/^\.\//, "").replace(/\/+$/, "").replace(/\\/g, "/");
|
|
934
|
-
}
|
|
935
|
-
function pathsIntersect(a, b) {
|
|
936
|
-
const left = normalisePath(a);
|
|
937
|
-
const right = normalisePath(b);
|
|
938
|
-
if (left === "" || right === "")
|
|
939
|
-
return false;
|
|
940
|
-
if (left === right)
|
|
941
|
-
return true;
|
|
942
|
-
return left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
943
|
-
}
|
|
944
700
|
|
|
945
701
|
// src/core/experts/selectExperts.ts
|
|
702
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
946
703
|
var MAX_DOMAIN_SELECTED = 8;
|
|
947
704
|
var LEGACY_STAGE_EXPERTS = ["domain", "stack"];
|
|
948
705
|
function selectExperts(input) {
|
|
@@ -982,18 +739,18 @@ function selectExperts(input) {
|
|
|
982
739
|
}
|
|
983
740
|
function expertNames(root) {
|
|
984
741
|
const dir = expertsDir(root);
|
|
985
|
-
if (!
|
|
742
|
+
if (!existsSync4(dir))
|
|
986
743
|
return [];
|
|
987
|
-
return
|
|
744
|
+
return readdirSync2(dir).filter((entry) => {
|
|
988
745
|
try {
|
|
989
|
-
return
|
|
746
|
+
return statSync2(`${dir}/${entry}`).isDirectory();
|
|
990
747
|
} catch {
|
|
991
748
|
return false;
|
|
992
749
|
}
|
|
993
750
|
}).sort();
|
|
994
751
|
}
|
|
995
752
|
function hasExpert(root, name) {
|
|
996
|
-
return
|
|
753
|
+
return existsSync4(`${expertsDir(root)}/${name}`);
|
|
997
754
|
}
|
|
998
755
|
var DIRECT_WEIGHT = 10;
|
|
999
756
|
var GRAPH_HOPS = 2;
|
|
@@ -1083,18 +840,18 @@ function stagesLoadingExperts(root) {
|
|
|
1083
840
|
}
|
|
1084
841
|
function stageIds(root) {
|
|
1085
842
|
const ids = new Set([
|
|
1086
|
-
...dirsIn(
|
|
843
|
+
...dirsIn(join4(root, PROJECT_FRAMEWORK_DIR, "stages")),
|
|
1087
844
|
...dirsIn(STAGES_DIR)
|
|
1088
845
|
]);
|
|
1089
846
|
return [...ids].sort();
|
|
1090
847
|
}
|
|
1091
848
|
function dirsIn(dir) {
|
|
1092
|
-
if (!
|
|
849
|
+
if (!existsSync5(dir))
|
|
1093
850
|
return [];
|
|
1094
851
|
try {
|
|
1095
|
-
return
|
|
852
|
+
return readdirSync3(dir).filter((entry) => {
|
|
1096
853
|
try {
|
|
1097
|
-
return
|
|
854
|
+
return statSync3(join4(dir, entry)).isDirectory() && existsSync5(join4(dir, entry, "stage.yml"));
|
|
1098
855
|
} catch {
|
|
1099
856
|
return false;
|
|
1100
857
|
}
|
|
@@ -1104,23 +861,23 @@ function dirsIn(dir) {
|
|
|
1104
861
|
}
|
|
1105
862
|
}
|
|
1106
863
|
function readStage(root, id) {
|
|
1107
|
-
const local =
|
|
1108
|
-
const path =
|
|
1109
|
-
if (!
|
|
864
|
+
const local = join4(root, PROJECT_FRAMEWORK_DIR, "stages", id, "stage.yml");
|
|
865
|
+
const path = existsSync5(local) ? local : join4(STAGES_DIR, id, "stage.yml");
|
|
866
|
+
if (!existsSync5(path))
|
|
1110
867
|
return null;
|
|
1111
868
|
try {
|
|
1112
|
-
const doc = parseYaml(
|
|
869
|
+
const doc = parseYaml(readFileSync4(path, "utf8"));
|
|
1113
870
|
return isRecord(doc) ? doc : null;
|
|
1114
871
|
} catch {
|
|
1115
872
|
return null;
|
|
1116
873
|
}
|
|
1117
874
|
}
|
|
1118
875
|
function workspaceRepos(root) {
|
|
1119
|
-
const path =
|
|
1120
|
-
if (!
|
|
876
|
+
const path = join4(root, PROJECT_FRAMEWORK_DIR, "workspace.yml");
|
|
877
|
+
if (!existsSync5(path))
|
|
1121
878
|
return [];
|
|
1122
879
|
try {
|
|
1123
|
-
const doc = parseYaml(
|
|
880
|
+
const doc = parseYaml(readFileSync4(path, "utf8"));
|
|
1124
881
|
const list = isRecord(doc) && Array.isArray(doc.repos) ? doc.repos : [];
|
|
1125
882
|
return list.map((row) => isRecord(row) && typeof row.name === "string" ? row.name : "").filter((name) => name !== "");
|
|
1126
883
|
} catch {
|
|
@@ -1135,17 +892,14 @@ function strings(value) {
|
|
|
1135
892
|
function evidenceCount(expert) {
|
|
1136
893
|
return expert.areas.reduce((total, area) => total + area.evidence.length, 0);
|
|
1137
894
|
}
|
|
1138
|
-
// src/core/experts/expertKnowledge.ts
|
|
1139
|
-
var DEFAULT_KNOWLEDGE_MAX_BYTES = 48 * 1024;
|
|
1140
|
-
|
|
1141
895
|
// src/core/experts/roleExperts.ts
|
|
1142
|
-
import { join as
|
|
1143
|
-
var ROLE_TEMPLATES_DIR =
|
|
896
|
+
import { join as join5 } from "node:path";
|
|
897
|
+
var ROLE_TEMPLATES_DIR = join5(TEMPLATES_DIR, "experts");
|
|
1144
898
|
// src/core/experts/domainRank.ts
|
|
1145
899
|
var MAX_GRAPH_BYTES = 64 * 1024 * 1024;
|
|
1146
900
|
// src/core/training/mineRuns.ts
|
|
1147
|
-
import { existsSync as
|
|
1148
|
-
import { join as
|
|
901
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4 } from "node:fs";
|
|
902
|
+
import { join as join6, relative as relative2 } from "node:path";
|
|
1149
903
|
var MAX_RUN_BYTES = 64 * 1024;
|
|
1150
904
|
var MINED_NAMES = ["handoff.md", "retro.md"];
|
|
1151
905
|
var MAX_DEPTH = 4;
|
|
@@ -1161,7 +915,7 @@ function findMined(dir, depth) {
|
|
|
1161
915
|
return [];
|
|
1162
916
|
let entries;
|
|
1163
917
|
try {
|
|
1164
|
-
entries =
|
|
918
|
+
entries = readdirSync4(dir);
|
|
1165
919
|
} catch {
|
|
1166
920
|
return [];
|
|
1167
921
|
}
|
|
@@ -1169,10 +923,10 @@ function findMined(dir, depth) {
|
|
|
1169
923
|
for (const entry of [...entries].sort()) {
|
|
1170
924
|
if (entry.startsWith("."))
|
|
1171
925
|
continue;
|
|
1172
|
-
const abs =
|
|
926
|
+
const abs = join6(dir, entry);
|
|
1173
927
|
let isDir = false;
|
|
1174
928
|
try {
|
|
1175
|
-
isDir =
|
|
929
|
+
isDir = statSync4(abs).isDirectory();
|
|
1176
930
|
} catch {
|
|
1177
931
|
continue;
|
|
1178
932
|
}
|