tldr-experts 0.5.0 → 0.6.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 +792 -0
- package/README.md +19 -5
- package/dist/hooks/answer-capture.js +136 -12
- package/dist/hooks/budget-gate.js +9 -9
- package/dist/hooks/{chunk-889tybxc.js → chunk-17gv74sr.js} +3 -3
- package/dist/hooks/{chunk-3w55tp71.js → chunk-5k4dggq0.js} +51 -17
- package/dist/hooks/{chunk-14zn51kh.js → chunk-7eqhfddw.js} +28 -569
- package/dist/hooks/{chunk-nadqsr3w.js → chunk-97ncnegd.js} +1 -1
- package/dist/hooks/{chunk-a6rpj2cp.js → chunk-9w64mxdh.js} +345 -22
- package/dist/hooks/{chunk-a5dq2dcp.js → chunk-dhbxzjfs.js} +2 -4
- package/dist/hooks/{chunk-hcrbr430.js → chunk-g4db8rbs.js} +2 -30
- package/dist/hooks/chunk-h7151g8w.js +803 -0
- package/dist/hooks/{chunk-5556vjt5.js → chunk-ka6bkb64.js} +1 -1
- package/dist/hooks/chunk-rrkdfk7s.js +30 -0
- package/dist/hooks/{chunk-qw73rdbr.js → chunk-v4ay50nq.js} +1 -1
- package/dist/hooks/{chunk-v1c1hpb8.js → chunk-vrhczzxs.js} +1 -1
- package/dist/hooks/{chunk-bvm6vjrt.js → chunk-x6n8b5fp.js} +1 -1
- package/dist/hooks/claim-sources.js +8 -10
- package/dist/hooks/dod-gate.js +7 -7
- package/dist/hooks/no-reask.js +8 -9
- package/dist/hooks/session-start.js +23 -18
- package/dist/hooks/statusline.js +10 -10
- package/dist/tldrx.js +2539 -1006
- package/env.yml +9 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/tldrx/SKILL.md +8 -1
- package/templates/questions.md +8 -0
- package/dist/hooks/chunk-q8d3sff9.js +0 -220
- package/dist/hooks/chunk-sae7sqty.js +0 -206
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EVIDENCE_FILE,
|
|
3
|
+
gateEvidenceRelPath
|
|
4
|
+
} from "./chunk-h7151g8w.js";
|
|
1
5
|
import {
|
|
2
6
|
MAX_PLAN_STORIES,
|
|
3
7
|
MAX_STORIES_PER_WAVE,
|
|
4
8
|
MAX_WAVES,
|
|
5
9
|
STORY_ID_RE,
|
|
6
10
|
WAVE_ID_RE,
|
|
7
|
-
parseFrontMatter,
|
|
8
11
|
requirePattern,
|
|
9
12
|
requireStringList,
|
|
10
13
|
requireVersion1
|
|
11
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-g4db8rbs.js";
|
|
12
15
|
import {
|
|
13
16
|
asDocument,
|
|
14
17
|
isRecord,
|
|
@@ -21,9 +24,6 @@ import {
|
|
|
21
24
|
requireVersion,
|
|
22
25
|
result
|
|
23
26
|
} from "./chunk-39zh2e44.js";
|
|
24
|
-
import {
|
|
25
|
-
parseHandoff
|
|
26
|
-
} from "./chunk-q8d3sff9.js";
|
|
27
27
|
import {
|
|
28
28
|
PROJECT_FRAMEWORK_DIR,
|
|
29
29
|
SRC_PATTERNS,
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
listRunDirs,
|
|
32
32
|
parseYaml,
|
|
33
33
|
readableSource
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-9w64mxdh.js";
|
|
35
35
|
|
|
36
36
|
// src/hooks/lib/runFile.ts
|
|
37
37
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -424,19 +424,25 @@ var SKIPPED_DIRS = new Set([
|
|
|
424
424
|
"test-results"
|
|
425
425
|
]);
|
|
426
426
|
|
|
427
|
-
// src/core/
|
|
428
|
-
|
|
429
|
-
function
|
|
430
|
-
return
|
|
427
|
+
// src/core/facilitator/paths.ts
|
|
428
|
+
import { join as join2, relative, sep } from "node:path";
|
|
429
|
+
function agentDir(runDir, stageId) {
|
|
430
|
+
return join2(runDir, ".agent", stageId);
|
|
431
|
+
}
|
|
432
|
+
function evidencePath(runDir, stageId) {
|
|
433
|
+
return join2(agentDir(runDir, stageId), EVIDENCE_FILE);
|
|
434
|
+
}
|
|
435
|
+
function gateEvidencePath(runDir, phaseId, stageId) {
|
|
436
|
+
return join2(runDir, ...gateEvidenceRelPath(phaseId, stageId).split("/"));
|
|
431
437
|
}
|
|
432
438
|
|
|
433
439
|
// src/core/experts/expertDocument.ts
|
|
434
440
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
435
|
-
import { join as
|
|
441
|
+
import { join as join4 } from "node:path";
|
|
436
442
|
|
|
437
443
|
// src/core/experts/loadExperts.ts
|
|
438
444
|
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
439
|
-
import { join as
|
|
445
|
+
import { join as join3 } from "node:path";
|
|
440
446
|
|
|
441
447
|
// src/core/init/competencyLevel.ts
|
|
442
448
|
var EVIDENCE_KINDS = ["code", "run", "test", "doc", "answer"];
|
|
@@ -568,21 +574,21 @@ var EXPERTS_DIRNAME = "experts";
|
|
|
568
574
|
var COMPETENCIES_FILE = "competencies.yml";
|
|
569
575
|
var EXPERT_FILE = "expert.md";
|
|
570
576
|
function expertsDir(root) {
|
|
571
|
-
return
|
|
577
|
+
return join3(root, PROJECT_FRAMEWORK_DIR, EXPERTS_DIRNAME);
|
|
572
578
|
}
|
|
573
579
|
function expertDir(root, name) {
|
|
574
|
-
return
|
|
580
|
+
return join3(expertsDir(root), name);
|
|
575
581
|
}
|
|
576
582
|
function loadExperts(root, now = new Date) {
|
|
577
583
|
const dir = expertsDir(root);
|
|
578
584
|
if (!existsSync2(dir))
|
|
579
585
|
return [];
|
|
580
|
-
const names = readdirSync(dir).filter((entry) => statSync(
|
|
586
|
+
const names = readdirSync(dir).filter((entry) => statSync(join3(dir, entry)).isDirectory()).sort();
|
|
581
587
|
return names.map((name) => loadExpert(root, name, now));
|
|
582
588
|
}
|
|
583
589
|
function loadExpert(root, name, now = new Date) {
|
|
584
590
|
const dir = expertDir(root, name);
|
|
585
|
-
const path =
|
|
591
|
+
const path = join3(dir, COMPETENCIES_FILE);
|
|
586
592
|
const empty = { name, dir, status: "unknown", lastTrained: null, areas: [], drifted: [] };
|
|
587
593
|
if (!existsSync2(path))
|
|
588
594
|
return { ...empty, error: `${COMPETENCIES_FILE} is missing` };
|
|
@@ -641,7 +647,7 @@ function str3(value) {
|
|
|
641
647
|
// src/core/experts/expertDocument.ts
|
|
642
648
|
var H2_RE = /^##\s+(.+?)\s*$/;
|
|
643
649
|
function readExpertDocument(root, name) {
|
|
644
|
-
const path =
|
|
650
|
+
const path = join4(expertDir(root, name), EXPERT_FILE);
|
|
645
651
|
if (!existsSync3(path)) {
|
|
646
652
|
return { path, exists: false, frontMatter: new Map, body: "" };
|
|
647
653
|
}
|
|
@@ -746,218 +752,6 @@ function pathsIntersect(a, b) {
|
|
|
746
752
|
// src/core/experts/expertKnowledge.ts
|
|
747
753
|
var DEFAULT_KNOWLEDGE_MAX_BYTES = 48 * 1024;
|
|
748
754
|
|
|
749
|
-
// src/core/facilitator/paths.ts
|
|
750
|
-
import { join as join4, relative, sep } from "node:path";
|
|
751
|
-
|
|
752
|
-
// src/core/text/evidence.ts
|
|
753
|
-
var EVIDENCE_VERSION = 1;
|
|
754
|
-
var EVIDENCE_FILE = "evidence.md";
|
|
755
|
-
var GATE_EVIDENCE_DIR = "gate-evidence";
|
|
756
|
-
function gateEvidenceRelPath(phaseId, stageId) {
|
|
757
|
-
return `${phaseId}/${GATE_EVIDENCE_DIR}/${stageId}.md`;
|
|
758
|
-
}
|
|
759
|
-
var EVIDENCE_VERDICTS = ["sign", "sign-with-fixlist", "refuse"];
|
|
760
|
-
var EVIDENCE_ROLES = ["agent"];
|
|
761
|
-
var DIFF_VS_STORIES = ["matches", "diverges", "n-a"];
|
|
762
|
-
var REQUIRED_EVIDENCE_KEYS = [
|
|
763
|
-
"version",
|
|
764
|
-
"gate",
|
|
765
|
-
"role",
|
|
766
|
-
"by",
|
|
767
|
-
"at",
|
|
768
|
-
"verdict",
|
|
769
|
-
"read",
|
|
770
|
-
"citations",
|
|
771
|
-
"touches",
|
|
772
|
-
"diff_vs_stories"
|
|
773
|
-
];
|
|
774
|
-
function parseEvidence(text) {
|
|
775
|
-
const parsed = parseFrontMatter(text);
|
|
776
|
-
const body = parseHandoff(parsed.frontMatter.body);
|
|
777
|
-
const bodyLine = parsed.frontMatter.bodyLine;
|
|
778
|
-
if (parsed.doc === null) {
|
|
779
|
-
return {
|
|
780
|
-
front: null,
|
|
781
|
-
frontIssues: [front(0, parsed.issue?.message ?? "front matter could not be read")],
|
|
782
|
-
body,
|
|
783
|
-
bodyLine
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
if (!isRecord2(parsed.doc)) {
|
|
787
|
-
return {
|
|
788
|
-
front: null,
|
|
789
|
-
frontIssues: [front(0, "front matter is empty — it must be a mapping of the keys design §A.5 names")],
|
|
790
|
-
body,
|
|
791
|
-
bodyLine
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
const issues = [];
|
|
795
|
-
const doc = parsed.doc;
|
|
796
|
-
for (const key of REQUIRED_EVIDENCE_KEYS) {
|
|
797
|
-
if (!(key in doc) || doc[key] === undefined) {
|
|
798
|
-
issues.push(front(0, `front matter is missing required key \`${key}\``));
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
const version = doc.version;
|
|
802
|
-
if (version !== undefined && version !== EVIDENCE_VERSION) {
|
|
803
|
-
issues.push(front(0, `\`version\` must be ${String(EVIDENCE_VERSION)}, got ${describe(version)}`));
|
|
804
|
-
}
|
|
805
|
-
const gate = requireText(doc.gate, "gate", issues);
|
|
806
|
-
const role = requireEnum2(doc.role, EVIDENCE_ROLES, "role", issues);
|
|
807
|
-
const by = requireText(doc.by, "by", issues);
|
|
808
|
-
const at = requireText(doc.at, "at", issues);
|
|
809
|
-
const verdict = requireEnum2(doc.verdict, EVIDENCE_VERDICTS, "verdict", issues);
|
|
810
|
-
const read = requireStrings(doc.read, "read", issues);
|
|
811
|
-
const citations = requireCitations(doc.citations, issues);
|
|
812
|
-
const touches = requireTouches(doc.touches, issues);
|
|
813
|
-
const diff = requireEnum2(doc.diff_vs_stories, DIFF_VS_STORIES, "diff_vs_stories", issues);
|
|
814
|
-
const caveats = doc.caveats === undefined ? [] : requireStrings(doc.caveats, "caveats", issues);
|
|
815
|
-
const recommend = requireRecommendations(doc.recommend, issues);
|
|
816
|
-
if (issues.length > 0)
|
|
817
|
-
return { front: null, frontIssues: issues, body, bodyLine };
|
|
818
|
-
return {
|
|
819
|
-
front: {
|
|
820
|
-
version: EVIDENCE_VERSION,
|
|
821
|
-
gate,
|
|
822
|
-
role,
|
|
823
|
-
by,
|
|
824
|
-
at,
|
|
825
|
-
verdict,
|
|
826
|
-
read,
|
|
827
|
-
citations,
|
|
828
|
-
touches,
|
|
829
|
-
diff_vs_stories: diff,
|
|
830
|
-
caveats,
|
|
831
|
-
recommend
|
|
832
|
-
},
|
|
833
|
-
frontIssues: [],
|
|
834
|
-
body,
|
|
835
|
-
bodyLine
|
|
836
|
-
};
|
|
837
|
-
}
|
|
838
|
-
var GUIDANCE = {
|
|
839
|
-
Read: "One bullet per file or artefact you opened, each ending with a `[src: …]` token for it.",
|
|
840
|
-
"Citations checked": "Which of this stage's citations you re-checked, and what each one turned out to be. " + "Sample honestly: `citations.sampled` above must match what is listed here.",
|
|
841
|
-
"Touches audited": "The touched paths you looked at, and every path outside the surface the What and the " + "stories declared. Name a new area rather than absorbing it.",
|
|
842
|
-
Verdict: "SIGN, SIGN-with-fixlist or REFUSE, and the measurement behind it — a command you ran, " + "a file you read. A verdict with no source is an opinion."
|
|
843
|
-
};
|
|
844
|
-
function front(line, message) {
|
|
845
|
-
return { kind: "front-matter", line, message };
|
|
846
|
-
}
|
|
847
|
-
function isRecord2(value) {
|
|
848
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
849
|
-
}
|
|
850
|
-
function requireText(value, key, issues) {
|
|
851
|
-
if (typeof value === "string" && value.trim() !== "")
|
|
852
|
-
return value.trim();
|
|
853
|
-
if (value !== undefined)
|
|
854
|
-
issues.push(front(0, `\`${key}\` must be a non-empty string, got ${describe(value)}`));
|
|
855
|
-
return "";
|
|
856
|
-
}
|
|
857
|
-
function requireEnum2(value, allowed, key, issues) {
|
|
858
|
-
if (typeof value === "string" && allowed.includes(value))
|
|
859
|
-
return value;
|
|
860
|
-
if (value !== undefined) {
|
|
861
|
-
issues.push(front(0, `\`${key}\` must be one of ${allowed.join(" | ")}, got ${describe(value)}`));
|
|
862
|
-
}
|
|
863
|
-
return allowed[0];
|
|
864
|
-
}
|
|
865
|
-
function requireStrings(value, key, issues) {
|
|
866
|
-
if (!Array.isArray(value)) {
|
|
867
|
-
if (value !== undefined)
|
|
868
|
-
issues.push(front(0, `\`${key}\` must be a list, got ${describe(value)}`));
|
|
869
|
-
return [];
|
|
870
|
-
}
|
|
871
|
-
const out = [];
|
|
872
|
-
for (const [i, entry] of value.entries()) {
|
|
873
|
-
if (typeof entry === "string")
|
|
874
|
-
out.push(entry);
|
|
875
|
-
else
|
|
876
|
-
issues.push(front(0, `\`${key}[${String(i)}]\` must be a string, got ${describe(entry)}`));
|
|
877
|
-
}
|
|
878
|
-
return out;
|
|
879
|
-
}
|
|
880
|
-
function requireCount(value, key, issues) {
|
|
881
|
-
if (typeof value === "number" && Number.isInteger(value) && value >= 0)
|
|
882
|
-
return value;
|
|
883
|
-
issues.push(front(0, `\`${key}\` must be a whole number of 0 or more, got ${describe(value)}`));
|
|
884
|
-
return 0;
|
|
885
|
-
}
|
|
886
|
-
function requireCitations(value, issues) {
|
|
887
|
-
if (!isRecord2(value)) {
|
|
888
|
-
if (value !== undefined) {
|
|
889
|
-
issues.push(front(0, "`citations` must be a mapping `{sampled, of, resolved, refuted}`, got " + describe(value)));
|
|
890
|
-
}
|
|
891
|
-
return { sampled: 0, of: 0, resolved: 0, refuted: 0 };
|
|
892
|
-
}
|
|
893
|
-
return {
|
|
894
|
-
sampled: requireCount(value.sampled, "citations.sampled", issues),
|
|
895
|
-
of: requireCount(value.of, "citations.of", issues),
|
|
896
|
-
resolved: requireCount(value.resolved, "citations.resolved", issues),
|
|
897
|
-
refuted: requireCount(value.refuted, "citations.refuted", issues)
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
function requireTouches(value, issues) {
|
|
901
|
-
if (!isRecord2(value)) {
|
|
902
|
-
if (value !== undefined) {
|
|
903
|
-
issues.push(front(0, "`touches` must be a mapping `{audited, outside_surface, new_areas}`, got " + describe(value)));
|
|
904
|
-
}
|
|
905
|
-
return { audited: 0, outside_surface: 0, new_areas: [] };
|
|
906
|
-
}
|
|
907
|
-
return {
|
|
908
|
-
audited: requireCount(value.audited, "touches.audited", issues),
|
|
909
|
-
outside_surface: requireCount(value.outside_surface, "touches.outside_surface", issues),
|
|
910
|
-
new_areas: requireStrings(value.new_areas, "touches.new_areas", issues)
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
function requireRecommendations(value, issues) {
|
|
914
|
-
if (value === undefined)
|
|
915
|
-
return [];
|
|
916
|
-
if (!Array.isArray(value)) {
|
|
917
|
-
issues.push(front(0, `\`recommend\` must be a list of {q, option, why, src}, got ${describe(value)}`));
|
|
918
|
-
return [];
|
|
919
|
-
}
|
|
920
|
-
const out = [];
|
|
921
|
-
for (const [i, entry] of value.entries()) {
|
|
922
|
-
if (!isRecord2(entry)) {
|
|
923
|
-
issues.push(front(0, `\`recommend[${String(i)}]\` must be a mapping {q, option, why, src}`));
|
|
924
|
-
continue;
|
|
925
|
-
}
|
|
926
|
-
const path = `recommend[${String(i)}]`;
|
|
927
|
-
for (const key of ["q", "option", "why", "src"]) {
|
|
928
|
-
if (!(key in entry))
|
|
929
|
-
issues.push(front(0, `\`${path}\` is missing \`${key}\``));
|
|
930
|
-
}
|
|
931
|
-
out.push({
|
|
932
|
-
q: requireText(entry.q, `${path}.q`, issues),
|
|
933
|
-
option: requireText(entry.option, `${path}.option`, issues),
|
|
934
|
-
why: requireText(entry.why, `${path}.why`, issues),
|
|
935
|
-
src: requireText(entry.src, `${path}.src`, issues)
|
|
936
|
-
});
|
|
937
|
-
}
|
|
938
|
-
return out;
|
|
939
|
-
}
|
|
940
|
-
function describe(value) {
|
|
941
|
-
if (value === null || value === undefined)
|
|
942
|
-
return "nothing";
|
|
943
|
-
if (Array.isArray(value))
|
|
944
|
-
return "a list";
|
|
945
|
-
if (typeof value === "object")
|
|
946
|
-
return "a mapping";
|
|
947
|
-
return typeof value === "string" ? `\`${value}\`` : String(value);
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// src/core/facilitator/paths.ts
|
|
951
|
-
function agentDir(runDir, stageId) {
|
|
952
|
-
return join4(runDir, ".agent", stageId);
|
|
953
|
-
}
|
|
954
|
-
function evidencePath(runDir, stageId) {
|
|
955
|
-
return join4(agentDir(runDir, stageId), EVIDENCE_FILE);
|
|
956
|
-
}
|
|
957
|
-
function gateEvidencePath(runDir, phaseId, stageId) {
|
|
958
|
-
return join4(runDir, ...gateEvidenceRelPath(phaseId, stageId).split("/"));
|
|
959
|
-
}
|
|
960
|
-
|
|
961
755
|
// src/core/facilitator/dispatchNotes.ts
|
|
962
756
|
var DISPATCH_NOTES_MAX_BYTES = 8 * 1024;
|
|
963
757
|
|
|
@@ -1001,7 +795,10 @@ var KINDS = {
|
|
|
1001
795
|
fact: { shape: `\`F<nnn>\` (${readableSource(SRC_PATTERNS.fact)})`, example: "F102" },
|
|
1002
796
|
cmd: { shape: `\`$ <command> → exit <n>\` (${readableSource(SRC_PATTERNS.cmd)})`, example: "$ bun test → exit 0" },
|
|
1003
797
|
graph: { shape: "`graph:<node id>`", example: "graph:hunt-engine" },
|
|
1004
|
-
absent: {
|
|
798
|
+
absent: {
|
|
799
|
+
shape: "`absent:<the path you looked at>` — add `#<what you searched for>` to have it CHECKED",
|
|
800
|
+
example: "absent:docs/retention.md#retention"
|
|
801
|
+
},
|
|
1005
802
|
aidlc: {
|
|
1006
803
|
shape: `\`aidlc:<file>:<line>\` or \`aidlc:<file>#Q<n>\` (${readableSource(SRC_PATTERNS.aidlcLine)})`,
|
|
1007
804
|
example: "aidlc:intents/260821/design.md:14"
|
|
@@ -1461,344 +1258,6 @@ function wouldExceedHostTokens(budget, phaseId, spentTokens) {
|
|
|
1461
1258
|
};
|
|
1462
1259
|
}
|
|
1463
1260
|
|
|
1464
|
-
// src/core/run/gatePolicy.ts
|
|
1465
|
-
var GATE_POLICIES = ["human", "auto", "agent"];
|
|
1466
|
-
function isGatePolicy(value) {
|
|
1467
|
-
return typeof value === "string" && GATE_POLICIES.includes(value);
|
|
1468
|
-
}
|
|
1469
|
-
function validateGatesPolicy(value, stageIds, issues) {
|
|
1470
|
-
if (value === undefined || value === null)
|
|
1471
|
-
return;
|
|
1472
|
-
if (!isRecord(value)) {
|
|
1473
|
-
issues.push({
|
|
1474
|
-
path: "gates_policy",
|
|
1475
|
-
message: `expected a mapping of stage id -> ${GATE_POLICIES.join(" | ")}`
|
|
1476
|
-
});
|
|
1477
|
-
return;
|
|
1478
|
-
}
|
|
1479
|
-
const known = new Set(stageIds);
|
|
1480
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
1481
|
-
if (!isGatePolicy(raw)) {
|
|
1482
|
-
issues.push({
|
|
1483
|
-
path: `gates_policy.${key}`,
|
|
1484
|
-
message: `expected one of ${GATE_POLICIES.join(" | ")}, got ${JSON.stringify(raw)}`
|
|
1485
|
-
});
|
|
1486
|
-
}
|
|
1487
|
-
if (known.size > 0 && !known.has(key)) {
|
|
1488
|
-
issues.push({ path: `gates_policy.${key}`, message: `names no stage in this run` });
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
// src/core/run/RunFile.ts
|
|
1494
|
-
var STAGE_STATUSES = [
|
|
1495
|
-
"pending",
|
|
1496
|
-
"ready",
|
|
1497
|
-
"running",
|
|
1498
|
-
"awaiting_answer",
|
|
1499
|
-
"awaiting_gate",
|
|
1500
|
-
"blocked",
|
|
1501
|
-
"done",
|
|
1502
|
-
"failed",
|
|
1503
|
-
"skipped",
|
|
1504
|
-
"cancelled"
|
|
1505
|
-
];
|
|
1506
|
-
var TERMINAL_STATUSES2 = ["done", "failed", "skipped", "cancelled"];
|
|
1507
|
-
var GATE_TYPES = ["approve", "checks", "auto"];
|
|
1508
|
-
var GATE_STATUSES = ["pending", "approved", "rejected", "n-a"];
|
|
1509
|
-
var ATTENDED_BY = ["host"];
|
|
1510
|
-
var RUN_ID_RE = /^\d{6}-[a-z0-9-]{1,40}$/;
|
|
1511
|
-
var PHASE_ID_RE = /^0[1-5]-[a-z]+$/;
|
|
1512
|
-
var MAX_PHASES2 = 5;
|
|
1513
|
-
var MAX_STAGES = 40;
|
|
1514
|
-
var MAX_TASKS = 200;
|
|
1515
|
-
function isTerminal2(status) {
|
|
1516
|
-
return TERMINAL_STATUSES2.includes(status);
|
|
1517
|
-
}
|
|
1518
|
-
function isFinished(status) {
|
|
1519
|
-
return status === "done" || status === "cancelled";
|
|
1520
|
-
}
|
|
1521
|
-
function isAttendedByHost(run) {
|
|
1522
|
-
return run.attended_by === "host";
|
|
1523
|
-
}
|
|
1524
|
-
function flatten(run) {
|
|
1525
|
-
const out = [];
|
|
1526
|
-
for (const phase of run.phases)
|
|
1527
|
-
for (const stage of phase.stages)
|
|
1528
|
-
out.push({ phase, stage });
|
|
1529
|
-
return out;
|
|
1530
|
-
}
|
|
1531
|
-
function stageAt(run, cursor) {
|
|
1532
|
-
return flatten(run).find((e) => e.phase.id === cursor.phase && e.stage.id === cursor.stage) ?? null;
|
|
1533
|
-
}
|
|
1534
|
-
function deriveRunStatus(run) {
|
|
1535
|
-
const all = flatten(run);
|
|
1536
|
-
if (run.cancelled !== undefined)
|
|
1537
|
-
return "cancelled";
|
|
1538
|
-
if (all.some((e) => e.stage.status === "failed"))
|
|
1539
|
-
return "failed";
|
|
1540
|
-
if (all.length > 0 && all.every((e) => isTerminal2(e.stage.status)))
|
|
1541
|
-
return "done";
|
|
1542
|
-
return stageAt(run, run.cursor)?.stage.status ?? "pending";
|
|
1543
|
-
}
|
|
1544
|
-
function derivePhaseStatus(phase) {
|
|
1545
|
-
if (phase.stages.length === 0)
|
|
1546
|
-
return "skipped";
|
|
1547
|
-
if (phase.stages.some((s) => s.status === "failed"))
|
|
1548
|
-
return "failed";
|
|
1549
|
-
if (phase.stages.every((s) => isTerminal2(s.status)))
|
|
1550
|
-
return "done";
|
|
1551
|
-
const live = phase.stages.find((s) => !isTerminal2(s.status));
|
|
1552
|
-
return live?.status ?? "pending";
|
|
1553
|
-
}
|
|
1554
|
-
function validateRunFile(input) {
|
|
1555
|
-
const issues = [];
|
|
1556
|
-
const deprecations = [];
|
|
1557
|
-
const doc = asDocument(input, issues);
|
|
1558
|
-
if (!doc)
|
|
1559
|
-
return result(issues);
|
|
1560
|
-
requireVersion(doc, issues, deprecations);
|
|
1561
|
-
requireKeys(doc, ["run", "title", "scope", "workflow", "repos", "created_at", "updated_at", "status", "cursor", "budget", "phases"], "", issues);
|
|
1562
|
-
if (typeof doc.run !== "string" || !RUN_ID_RE.test(doc.run)) {
|
|
1563
|
-
issues.push({ path: "run", message: "run id must match ^\\d{6}-[a-z0-9-]{1,40}$" });
|
|
1564
|
-
}
|
|
1565
|
-
requireString(doc.title, "title", issues);
|
|
1566
|
-
requireString(doc.scope, "scope", issues);
|
|
1567
|
-
requireString(doc.workflow, "workflow", issues);
|
|
1568
|
-
requireString(doc.created_at, "created_at", issues);
|
|
1569
|
-
requireString(doc.updated_at, "updated_at", issues);
|
|
1570
|
-
requireEnum(doc.status, STAGE_STATUSES, "status", issues);
|
|
1571
|
-
requireArray(doc.repos, "repos", issues);
|
|
1572
|
-
let cursor = null;
|
|
1573
|
-
if (isRecord(doc.cursor)) {
|
|
1574
|
-
requireKeys(doc.cursor, ["phase", "stage", "task"], "cursor", issues);
|
|
1575
|
-
cursor = {
|
|
1576
|
-
phase: String(doc.cursor.phase ?? ""),
|
|
1577
|
-
stage: String(doc.cursor.stage ?? ""),
|
|
1578
|
-
task: typeof doc.cursor.task === "string" ? doc.cursor.task : null
|
|
1579
|
-
};
|
|
1580
|
-
} else if (doc.cursor !== undefined) {
|
|
1581
|
-
issues.push({ path: "cursor", message: "expected a mapping" });
|
|
1582
|
-
}
|
|
1583
|
-
if (isRecord(doc.budget)) {
|
|
1584
|
-
requireKeys(doc.budget, ["ceiling_usd", "spent_usd", "per_agent_max_usd"], "budget", issues);
|
|
1585
|
-
requireNumber(doc.budget.ceiling_usd, "budget.ceiling_usd", issues);
|
|
1586
|
-
requireNumber(doc.budget.spent_usd, "budget.spent_usd", issues);
|
|
1587
|
-
requireNumber(doc.budget.per_agent_max_usd, "budget.per_agent_max_usd", issues);
|
|
1588
|
-
} else if (doc.budget !== undefined) {
|
|
1589
|
-
issues.push({ path: "budget", message: "expected a mapping" });
|
|
1590
|
-
}
|
|
1591
|
-
if (doc.build !== undefined) {
|
|
1592
|
-
if (isRecord(doc.build)) {
|
|
1593
|
-
requireKeys(doc.build, ["epic_branch"], "build", issues);
|
|
1594
|
-
if (requireArray(doc.build.epic_branch, "build.epic_branch", issues)) {
|
|
1595
|
-
doc.build.epic_branch.forEach((branch, i) => {
|
|
1596
|
-
requireString(branch, `build.epic_branch[${i}]`, issues);
|
|
1597
|
-
});
|
|
1598
|
-
}
|
|
1599
|
-
if (doc.build.branch_model !== undefined && !isBranchModelKind(doc.build.branch_model)) {
|
|
1600
|
-
issues.push({
|
|
1601
|
-
path: "build.branch_model",
|
|
1602
|
-
message: `expected one of ${BRANCH_MODELS.join(" | ")}`
|
|
1603
|
-
});
|
|
1604
|
-
}
|
|
1605
|
-
} else {
|
|
1606
|
-
issues.push({ path: "build", message: "expected a mapping" });
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
if (doc.cancelled !== undefined) {
|
|
1610
|
-
if (isRecord(doc.cancelled)) {
|
|
1611
|
-
requireKeys(doc.cancelled, ["by", "at", "note"], "cancelled", issues);
|
|
1612
|
-
requireString(doc.cancelled.by, "cancelled.by", issues);
|
|
1613
|
-
requireString(doc.cancelled.at, "cancelled.at", issues);
|
|
1614
|
-
requireString(doc.cancelled.note, "cancelled.note", issues);
|
|
1615
|
-
} else {
|
|
1616
|
-
issues.push({ path: "cancelled", message: "expected a mapping" });
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
if (doc.triage !== undefined) {
|
|
1620
|
-
if (isRecord(doc.triage)) {
|
|
1621
|
-
requireKeys(doc.triage, ["split", "depends_on"], "triage", issues);
|
|
1622
|
-
requireString(doc.triage.split, "triage.split", issues);
|
|
1623
|
-
if (requireArray(doc.triage.depends_on, "triage.depends_on", issues)) {
|
|
1624
|
-
doc.triage.depends_on.forEach((slug, i) => {
|
|
1625
|
-
requireString(slug, `triage.depends_on[${i}]`, issues);
|
|
1626
|
-
});
|
|
1627
|
-
}
|
|
1628
|
-
} else {
|
|
1629
|
-
issues.push({ path: "triage", message: "expected a mapping" });
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
if (doc.attended_by !== undefined)
|
|
1633
|
-
requireEnum(doc.attended_by, ATTENDED_BY, "attended_by", issues);
|
|
1634
|
-
if (doc.keep_worktrees !== undefined && typeof doc.keep_worktrees !== "boolean") {
|
|
1635
|
-
issues.push({ path: "keep_worktrees", message: `expected a boolean, got ${typeof doc.keep_worktrees}` });
|
|
1636
|
-
}
|
|
1637
|
-
if (!requireArray(doc.phases, "phases", issues))
|
|
1638
|
-
return result(issues, deprecations);
|
|
1639
|
-
const phases = doc.phases;
|
|
1640
|
-
if (phases.length > MAX_PHASES2) {
|
|
1641
|
-
issues.push({ path: "phases", message: `${phases.length} phases exceeds the ${MAX_PHASES2} cap` });
|
|
1642
|
-
}
|
|
1643
|
-
const declaredStageIds = [];
|
|
1644
|
-
for (const phase of phases) {
|
|
1645
|
-
if (!isRecord(phase) || !Array.isArray(phase.stages))
|
|
1646
|
-
continue;
|
|
1647
|
-
for (const stage of phase.stages) {
|
|
1648
|
-
if (isRecord(stage) && typeof stage.id === "string")
|
|
1649
|
-
declaredStageIds.push(stage.id);
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
validateGatesPolicy(doc.gates_policy, declaredStageIds, issues);
|
|
1653
|
-
let stageCount = 0;
|
|
1654
|
-
let taskCount = 0;
|
|
1655
|
-
let running = 0;
|
|
1656
|
-
let cursorResolves = false;
|
|
1657
|
-
let spentFromTasks = 0;
|
|
1658
|
-
const phaseIds = new Set;
|
|
1659
|
-
phases.forEach((phase, i) => {
|
|
1660
|
-
const base = `phases[${i}]`;
|
|
1661
|
-
if (!isRecord(phase)) {
|
|
1662
|
-
issues.push({ path: base, message: "expected a mapping" });
|
|
1663
|
-
return;
|
|
1664
|
-
}
|
|
1665
|
-
requireKeys(phase, ["id", "status", "stages"], base, issues);
|
|
1666
|
-
const phaseId = typeof phase.id === "string" ? phase.id : "";
|
|
1667
|
-
if (!PHASE_ID_RE.test(phaseId)) {
|
|
1668
|
-
issues.push({ path: `${base}.id`, message: "phase id must match ^0[1-5]-[a-z]+$" });
|
|
1669
|
-
}
|
|
1670
|
-
if (phaseIds.has(phaseId))
|
|
1671
|
-
issues.push({ path: `${base}.id`, message: `duplicate phase id ${phaseId}` });
|
|
1672
|
-
phaseIds.add(phaseId);
|
|
1673
|
-
requireEnum(phase.status, STAGE_STATUSES, `${base}.status`, issues);
|
|
1674
|
-
if (!requireArray(phase.stages, `${base}.stages`, issues))
|
|
1675
|
-
return;
|
|
1676
|
-
const stageIds = new Set;
|
|
1677
|
-
phase.stages.forEach((stage, j) => {
|
|
1678
|
-
const path = `${base}.stages[${j}]`;
|
|
1679
|
-
stageCount++;
|
|
1680
|
-
if (!isRecord(stage)) {
|
|
1681
|
-
issues.push({ path, message: "expected a mapping" });
|
|
1682
|
-
return;
|
|
1683
|
-
}
|
|
1684
|
-
requireKeys(stage, ["id", "status", "expert", "model", "budget_usd", "cost_usd", "started_at", "ended_at", "inputs", "outputs", "gate", "tasks"], path, issues);
|
|
1685
|
-
const stageId = typeof stage.id === "string" ? stage.id : "";
|
|
1686
|
-
if (stageIds.has(stageId))
|
|
1687
|
-
issues.push({ path: `${path}.id`, message: `duplicate stage id ${stageId}` });
|
|
1688
|
-
stageIds.add(stageId);
|
|
1689
|
-
requireEnum(stage.status, STAGE_STATUSES, `${path}.status`, issues);
|
|
1690
|
-
if (stage.status === "running")
|
|
1691
|
-
running++;
|
|
1692
|
-
requireNumber(stage.budget_usd, `${path}.budget_usd`, issues);
|
|
1693
|
-
requireNumber(stage.cost_usd, `${path}.cost_usd`, issues);
|
|
1694
|
-
requireArray(stage.inputs, `${path}.inputs`, issues);
|
|
1695
|
-
requireArray(stage.outputs, `${path}.outputs`, issues);
|
|
1696
|
-
if (stage.stale !== undefined && typeof stage.stale !== "boolean") {
|
|
1697
|
-
issues.push({ path: `${path}.stale`, message: "expected true or false" });
|
|
1698
|
-
}
|
|
1699
|
-
checkOrder(stage.started_at, stage.ended_at, path, issues);
|
|
1700
|
-
if (cursor !== null && cursor.phase === phaseId && cursor.stage === stageId)
|
|
1701
|
-
cursorResolves = true;
|
|
1702
|
-
if (isRecord(stage.gate)) {
|
|
1703
|
-
const gate = stage.gate;
|
|
1704
|
-
requireKeys(gate, ["type", "status", "by", "at", "note"], `${path}.gate`, issues);
|
|
1705
|
-
requireEnum(gate.type, GATE_TYPES, `${path}.gate.type`, issues);
|
|
1706
|
-
requireEnum(gate.status, GATE_STATUSES, `${path}.gate.status`, issues);
|
|
1707
|
-
if (gate.status === "approved" && (typeof gate.by !== "string" || typeof gate.at !== "string")) {
|
|
1708
|
-
issues.push({ path: `${path}.gate`, message: "an approved gate needs both `by` and `at`" });
|
|
1709
|
-
}
|
|
1710
|
-
validateGateEvidence(gate.evidence, `${path}.gate.evidence`, issues);
|
|
1711
|
-
} else if (stage.gate !== undefined) {
|
|
1712
|
-
issues.push({ path: `${path}.gate`, message: "expected a mapping" });
|
|
1713
|
-
}
|
|
1714
|
-
if (requireArray(stage.tasks, `${path}.tasks`, issues)) {
|
|
1715
|
-
const taskIds = new Set;
|
|
1716
|
-
stage.tasks.forEach((task, k) => {
|
|
1717
|
-
const tp = `${path}.tasks[${k}]`;
|
|
1718
|
-
taskCount++;
|
|
1719
|
-
if (!isRecord(task)) {
|
|
1720
|
-
issues.push({ path: tp, message: "expected a mapping" });
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
requireKeys(task, ["id", "status", "cost_usd", "error", "session_id"], tp, issues);
|
|
1724
|
-
const taskId = typeof task.id === "string" ? task.id : "";
|
|
1725
|
-
if (!/^t\d+$/.test(taskId))
|
|
1726
|
-
issues.push({ path: `${tp}.id`, message: "task id must match ^t\\d+$" });
|
|
1727
|
-
if (taskIds.has(taskId))
|
|
1728
|
-
issues.push({ path: `${tp}.id`, message: `duplicate task id ${taskId}` });
|
|
1729
|
-
taskIds.add(taskId);
|
|
1730
|
-
requireEnum(task.status, STAGE_STATUSES, `${tp}.status`, issues);
|
|
1731
|
-
if (task.cost_usd !== null)
|
|
1732
|
-
requireNumber(task.cost_usd, `${tp}.cost_usd`, issues);
|
|
1733
|
-
if (task.metered !== undefined && typeof task.metered !== "boolean") {
|
|
1734
|
-
issues.push({ path: `${tp}.metered`, message: "expected true or false" });
|
|
1735
|
-
}
|
|
1736
|
-
if (task.cost_usd === null && task.metered !== false) {
|
|
1737
|
-
issues.push({ path: `${tp}.metered`, message: "a null cost_usd must be marked `metered: false`" });
|
|
1738
|
-
}
|
|
1739
|
-
if (typeof task.cost_usd === "number")
|
|
1740
|
-
spentFromTasks += task.cost_usd;
|
|
1741
|
-
checkOrder(task.started_at, task.ended_at, tp, issues);
|
|
1742
|
-
});
|
|
1743
|
-
}
|
|
1744
|
-
});
|
|
1745
|
-
});
|
|
1746
|
-
if (stageCount > MAX_STAGES)
|
|
1747
|
-
issues.push({ path: "phases", message: `${stageCount} stages exceeds the ${MAX_STAGES} cap` });
|
|
1748
|
-
if (taskCount > MAX_TASKS)
|
|
1749
|
-
issues.push({ path: "phases", message: `${taskCount} tasks exceeds the ${MAX_TASKS} cap` });
|
|
1750
|
-
if (running > 1)
|
|
1751
|
-
issues.push({ path: "phases", message: `${running} stages are running; tldrx is single-writer` });
|
|
1752
|
-
if (cursor !== null && !cursorResolves) {
|
|
1753
|
-
issues.push({ path: "cursor", message: `cursor ${cursor.phase}/${cursor.stage} does not resolve to a stage` });
|
|
1754
|
-
}
|
|
1755
|
-
if (isRecord(doc.budget) && typeof doc.budget.spent_usd === "number") {
|
|
1756
|
-
if (Math.abs(doc.budget.spent_usd - spentFromTasks) > 0.01) {
|
|
1757
|
-
issues.push({
|
|
1758
|
-
path: "budget.spent_usd",
|
|
1759
|
-
message: `${doc.budget.spent_usd} does not match the task total ${round5(spentFromTasks)} (tolerance 0.01)`
|
|
1760
|
-
});
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
return result(issues, deprecations);
|
|
1764
|
-
}
|
|
1765
|
-
function checkOrder(started, ended, path, issues) {
|
|
1766
|
-
if (typeof started === "string" && typeof ended === "string" && ended < started) {
|
|
1767
|
-
issues.push({ path: `${path}.ended_at`, message: "ended_at is before started_at" });
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
function round5(n) {
|
|
1771
|
-
return Math.round(n * 100) / 100;
|
|
1772
|
-
}
|
|
1773
|
-
function asRunFile(input) {
|
|
1774
|
-
return input;
|
|
1775
|
-
}
|
|
1776
|
-
var GATE_EVIDENCE_KEYS = [
|
|
1777
|
-
"path",
|
|
1778
|
-
"role",
|
|
1779
|
-
"verdict",
|
|
1780
|
-
"sampled",
|
|
1781
|
-
"of",
|
|
1782
|
-
"resolved",
|
|
1783
|
-
"refuted",
|
|
1784
|
-
"outside_surface"
|
|
1785
|
-
];
|
|
1786
|
-
function validateGateEvidence(value, base, issues) {
|
|
1787
|
-
if (value === undefined || value === null)
|
|
1788
|
-
return;
|
|
1789
|
-
if (!isRecord(value)) {
|
|
1790
|
-
issues.push({ path: base, message: "expected a mapping" });
|
|
1791
|
-
return;
|
|
1792
|
-
}
|
|
1793
|
-
requireKeys(value, GATE_EVIDENCE_KEYS, base, issues);
|
|
1794
|
-
requireString(value.path, `${base}.path`, issues);
|
|
1795
|
-
requireEnum(value.role, EVIDENCE_ROLES, `${base}.role`, issues);
|
|
1796
|
-
requireEnum(value.verdict, EVIDENCE_VERDICTS, `${base}.verdict`, issues);
|
|
1797
|
-
for (const key of ["sampled", "of", "resolved", "refuted", "outside_surface"]) {
|
|
1798
|
-
requireNumber(value[key], `${base}.${key}`, issues);
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
1261
|
// src/core/budget/budgetView.ts
|
|
1803
1262
|
function raiseCommand(runId, phaseId, amountUsd) {
|
|
1804
1263
|
return `tldrx budget raise ${phaseId} ${amountUsd.toFixed(2)} --run ${runId}`;
|
|
@@ -1807,4 +1266,4 @@ function shortBy(estimate, remaining) {
|
|
|
1807
1266
|
return Math.max(0.01, Math.ceil((estimate - remaining) * 100) / 100);
|
|
1808
1267
|
}
|
|
1809
1268
|
|
|
1810
|
-
export { loadRunView, isAttendedByHostView, runSpend, hostTokensIn, renderRunEconomies, cursorStage, openRunViews, newestActiveRun, currentActor, nowRfc3339, DEFAULT_ON_HOST_TOKENS_EXCEED, DEFAULT_ECONOMY, economyFor, isHostTokens, validateRunBudget, asRunBudget, expertsDir, loadExperts, readExpertDomain, pathsIntersect,
|
|
1269
|
+
export { loadRunView, isAttendedByHostView, runSpend, hostTokensIn, renderRunEconomies, cursorStage, openRunViews, newestActiveRun, currentActor, nowRfc3339, DEFAULT_ON_HOST_TOKENS_EXCEED, DEFAULT_ECONOMY, economyFor, isHostTokens, validateRunBudget, asRunBudget, evidencePath, gateEvidencePath, expertsDir, loadExperts, readExpertDomain, pathsIntersect, stackExpertNames, remainingWork, wouldExceed, wouldExceedHostTokens, raiseCommand, shortBy };
|