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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isRecord
|
|
3
|
+
} from "./chunk-39zh2e44.js";
|
|
4
|
+
import {
|
|
5
|
+
parseYaml
|
|
6
|
+
} from "./chunk-sq44k6g2.js";
|
|
7
|
+
|
|
8
|
+
// src/core/schemas/planCommon.ts
|
|
9
|
+
var STORY_ID_RE = /^S\d{1,4}$/;
|
|
10
|
+
var WAVE_ID_RE = /^W\d{1,3}$/;
|
|
11
|
+
var MAX_LIST_ITEMS = 64;
|
|
12
|
+
var MAX_ITEM_CHARS = 512;
|
|
13
|
+
var MAX_WAVES = 32;
|
|
14
|
+
var MAX_STORIES_PER_WAVE = 32;
|
|
15
|
+
var MAX_PLAN_STORIES = 200;
|
|
16
|
+
function requireStringList(value, path, issues, rules = {}) {
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
return [];
|
|
19
|
+
if (!Array.isArray(value)) {
|
|
20
|
+
issues.push({ path, message: `expected a list, got ${describe(value)}` });
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const max = rules.max ?? MAX_LIST_ITEMS;
|
|
24
|
+
if (value.length > max) {
|
|
25
|
+
issues.push({ path, message: `${value.length} items exceeds the ${max}-item cap` });
|
|
26
|
+
}
|
|
27
|
+
if (rules.nonEmpty === true && value.length === 0) {
|
|
28
|
+
issues.push({ path, message: "must not be empty" });
|
|
29
|
+
}
|
|
30
|
+
const seen = new Set;
|
|
31
|
+
const out = [];
|
|
32
|
+
value.forEach((item, i) => {
|
|
33
|
+
const at = `${path}[${i}]`;
|
|
34
|
+
if (typeof item !== "string") {
|
|
35
|
+
issues.push({ path: at, message: `expected a string, got ${describe(item)}` });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (item.trim() === "") {
|
|
39
|
+
issues.push({ path: at, message: "must not be blank" });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (item.length > MAX_ITEM_CHARS) {
|
|
43
|
+
issues.push({
|
|
44
|
+
path: at,
|
|
45
|
+
message: `${item.length} characters exceeds the ${MAX_ITEM_CHARS}-character cap on one list item ` + "— split it into several items"
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (rules.pattern !== undefined && !rules.pattern.test(item)) {
|
|
49
|
+
issues.push({ path: at, message: `expected ${rules.patternName ?? String(rules.pattern)}, got \`${item}\`` });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (rules.unique === true && seen.has(item)) {
|
|
53
|
+
issues.push({ path: at, message: `\`${item}\` is listed twice` });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
seen.add(item);
|
|
57
|
+
out.push(item);
|
|
58
|
+
});
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function requirePattern(value, pattern, patternName, path, issues) {
|
|
62
|
+
if (value === undefined)
|
|
63
|
+
return;
|
|
64
|
+
if (typeof value !== "string") {
|
|
65
|
+
issues.push({ path, message: `expected a string, got ${describe(value)}` });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!pattern.test(value)) {
|
|
69
|
+
issues.push({ path, message: `expected ${patternName}, got \`${value}\`` });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function requireVersion1(doc, issues) {
|
|
73
|
+
if (doc.version !== undefined && doc.version !== 1) {
|
|
74
|
+
issues.push({ path: "version", message: `unknown schema version ${String(doc.version)} (expected 1)` });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function describe(value) {
|
|
78
|
+
if (value === null)
|
|
79
|
+
return "null";
|
|
80
|
+
if (Array.isArray(value))
|
|
81
|
+
return "a list";
|
|
82
|
+
if (isRecord(value))
|
|
83
|
+
return "a mapping";
|
|
84
|
+
return typeof value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/core/schemas/frontMatter.ts
|
|
88
|
+
var FENCE = "---";
|
|
89
|
+
function splitFrontMatter(text) {
|
|
90
|
+
const lines = text.split(`
|
|
91
|
+
`);
|
|
92
|
+
if ((lines[0] ?? "").trimEnd() !== FENCE) {
|
|
93
|
+
return { present: false, raw: "", body: text, bodyLine: 1 };
|
|
94
|
+
}
|
|
95
|
+
for (let i = 1;i < lines.length; i++) {
|
|
96
|
+
if ((lines[i] ?? "").trimEnd() !== FENCE)
|
|
97
|
+
continue;
|
|
98
|
+
return {
|
|
99
|
+
present: true,
|
|
100
|
+
raw: lines.slice(1, i).join(`
|
|
101
|
+
`),
|
|
102
|
+
body: lines.slice(i + 1).join(`
|
|
103
|
+
`),
|
|
104
|
+
bodyLine: i + 2
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return { present: false, raw: "", body: text, bodyLine: 1 };
|
|
108
|
+
}
|
|
109
|
+
function parseFrontMatter(text) {
|
|
110
|
+
const frontMatter = splitFrontMatter(text);
|
|
111
|
+
if (!frontMatter.present) {
|
|
112
|
+
return {
|
|
113
|
+
frontMatter,
|
|
114
|
+
doc: null,
|
|
115
|
+
issue: {
|
|
116
|
+
path: "",
|
|
117
|
+
message: "no YAML front matter — the file must open with `---` and close the block with `---`"
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return { frontMatter, doc: parseYaml(frontMatter.raw), issue: null };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return {
|
|
125
|
+
frontMatter,
|
|
126
|
+
doc: null,
|
|
127
|
+
issue: { path: "", message: `front matter is not valid YAML: ${message(error)}` }
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function message(error) {
|
|
132
|
+
return error instanceof Error ? error.message.split(`
|
|
133
|
+
`)[0] ?? "parse error" : String(error);
|
|
134
|
+
}
|
|
135
|
+
// src/core/schemas/story.ts
|
|
136
|
+
var DOD_FENCE_OPEN_RE = /^\s*```+\s*dod\s*$/i;
|
|
137
|
+
var DOD_FENCE_CLOSE_RE = /^\s*```+\s*$/;
|
|
138
|
+
var FENCE_OPEN_RE = DOD_FENCE_OPEN_RE;
|
|
139
|
+
var FENCE_CLOSE_RE = DOD_FENCE_CLOSE_RE;
|
|
140
|
+
function parseDodBlock(text) {
|
|
141
|
+
const commands = [];
|
|
142
|
+
let present = false;
|
|
143
|
+
let inside = false;
|
|
144
|
+
for (const line of text.split(`
|
|
145
|
+
`)) {
|
|
146
|
+
if (inside) {
|
|
147
|
+
if (FENCE_CLOSE_RE.test(line)) {
|
|
148
|
+
inside = false;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const command = line.trim();
|
|
152
|
+
if (command !== "" && !command.startsWith("#"))
|
|
153
|
+
commands.push(command);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (FENCE_OPEN_RE.test(line)) {
|
|
157
|
+
present = true;
|
|
158
|
+
inside = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { present, commands };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { STORY_ID_RE, WAVE_ID_RE, MAX_WAVES, MAX_STORIES_PER_WAVE, MAX_PLAN_STORIES, requireStringList, requirePattern, requireVersion1, parseFrontMatter, parseDodBlock };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseSrcToken
|
|
3
|
+
} from "./chunk-m3mewgnw.js";
|
|
4
|
+
|
|
5
|
+
// src/core/text/questions.ts
|
|
6
|
+
var REQUIRED_METADATA_KEYS = ["id", "status", "area", "asked_by", "asked_at"];
|
|
7
|
+
var HEADING_RE = /^##\s+(Q\d{1,6})\s+·\s+(.+?)\s*$/;
|
|
8
|
+
var METADATA_RE = /^<!--\s*(.*?)\s*-->$/;
|
|
9
|
+
var WHY_RE = /^Why asked:\s*(.*)$/;
|
|
10
|
+
var OPTION_RE = /^-\s+([A-E])\)\s*(.*)$/;
|
|
11
|
+
var ANSWER_RE = /^\[Answer\]:[ \t]*(\S.*)$/;
|
|
12
|
+
var ANSWER_SLOT_RE = /^\[Answer\]:/;
|
|
13
|
+
var FOOTER_KEYS = ["answered_by", "answered_at", "fact"];
|
|
14
|
+
function parseQuestions(text) {
|
|
15
|
+
const trailingNewline = text.endsWith(`
|
|
16
|
+
`);
|
|
17
|
+
const body = trailingNewline ? text.slice(0, -1) : text;
|
|
18
|
+
const lines = body === "" ? [] : body.split(`
|
|
19
|
+
`);
|
|
20
|
+
const preamble = [];
|
|
21
|
+
const blocks = [];
|
|
22
|
+
let pending = null;
|
|
23
|
+
const close = () => {
|
|
24
|
+
if (pending === null)
|
|
25
|
+
return;
|
|
26
|
+
blocks.push(buildBlock(pending.start + 1, pending.lines));
|
|
27
|
+
pending = null;
|
|
28
|
+
};
|
|
29
|
+
for (let i = 0;i < lines.length; i++) {
|
|
30
|
+
const line = lines[i] ?? "";
|
|
31
|
+
if (HEADING_RE.test(line)) {
|
|
32
|
+
close();
|
|
33
|
+
pending = { start: i, lines: [line] };
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (pending === null)
|
|
37
|
+
preamble.push(line);
|
|
38
|
+
else
|
|
39
|
+
pending.lines.push(line);
|
|
40
|
+
}
|
|
41
|
+
close();
|
|
42
|
+
return { preamble, blocks, trailingNewline };
|
|
43
|
+
}
|
|
44
|
+
function buildBlock(startLine, lines) {
|
|
45
|
+
const heading = HEADING_RE.exec(lines[0] ?? "");
|
|
46
|
+
const id = heading?.[1] ?? "";
|
|
47
|
+
const title = heading?.[2] ?? "";
|
|
48
|
+
let metadata = null;
|
|
49
|
+
let metadataIndex = -1;
|
|
50
|
+
let whyAsked = null;
|
|
51
|
+
let whySrc = null;
|
|
52
|
+
const options = [];
|
|
53
|
+
let answer = "";
|
|
54
|
+
let answerIndex = -1;
|
|
55
|
+
let footer = null;
|
|
56
|
+
for (let i = 1;i < lines.length; i++) {
|
|
57
|
+
const line = lines[i] ?? "";
|
|
58
|
+
const comment = METADATA_RE.exec(line);
|
|
59
|
+
if (comment !== null && comment[1] !== undefined) {
|
|
60
|
+
const pairs = parsePipeComment(comment[1]);
|
|
61
|
+
if (metadata === null && pairs.some(([k]) => k === "id")) {
|
|
62
|
+
metadata = toMetadata(pairs);
|
|
63
|
+
metadataIndex = i;
|
|
64
|
+
} else if (footer === null && pairs.some(([k]) => k === "answered_by")) {
|
|
65
|
+
footer = toFooter(pairs);
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const why = WHY_RE.exec(line);
|
|
70
|
+
if (why !== null && whyAsked === null) {
|
|
71
|
+
whyAsked = why[1] ?? "";
|
|
72
|
+
whySrc = parseSrcToken(line);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const option = OPTION_RE.exec(line);
|
|
76
|
+
if (option !== null && option[1] !== undefined && option[2] !== undefined) {
|
|
77
|
+
options.push({ letter: option[1], text: option[2] });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (ANSWER_SLOT_RE.test(line) && answerIndex === -1) {
|
|
81
|
+
answerIndex = i;
|
|
82
|
+
const captured = ANSWER_RE.exec(line);
|
|
83
|
+
answer = (captured?.[1] ?? "").trim();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
id,
|
|
88
|
+
title,
|
|
89
|
+
metadata,
|
|
90
|
+
metadataIndex,
|
|
91
|
+
whyAsked,
|
|
92
|
+
whySrc,
|
|
93
|
+
options,
|
|
94
|
+
answer,
|
|
95
|
+
answerIndex,
|
|
96
|
+
footer,
|
|
97
|
+
startLine,
|
|
98
|
+
lines: [...lines]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function parsePipeComment(inner) {
|
|
102
|
+
const pairs = [];
|
|
103
|
+
for (const part of inner.split("|")) {
|
|
104
|
+
const colon = part.indexOf(":");
|
|
105
|
+
if (colon === -1)
|
|
106
|
+
continue;
|
|
107
|
+
pairs.push([part.slice(0, colon).trim(), part.slice(colon + 1).trim()]);
|
|
108
|
+
}
|
|
109
|
+
return pairs;
|
|
110
|
+
}
|
|
111
|
+
function toMetadata(pairs) {
|
|
112
|
+
const map = new Map(pairs);
|
|
113
|
+
const extra = pairs.filter(([k]) => !REQUIRED_METADATA_KEYS.includes(k));
|
|
114
|
+
return {
|
|
115
|
+
id: map.get("id") ?? "",
|
|
116
|
+
status: map.get("status") ?? "",
|
|
117
|
+
area: map.get("area") ?? "",
|
|
118
|
+
asked_by: map.get("asked_by") ?? "",
|
|
119
|
+
asked_at: map.get("asked_at") ?? "",
|
|
120
|
+
extra
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function toFooter(pairs) {
|
|
124
|
+
const map = new Map(pairs);
|
|
125
|
+
return {
|
|
126
|
+
answered_by: map.get("answered_by") ?? "",
|
|
127
|
+
answered_at: map.get("answered_at") ?? "",
|
|
128
|
+
fact: map.get("fact") ?? ""
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function serializeQuestions(doc) {
|
|
132
|
+
const out = [...doc.preamble];
|
|
133
|
+
for (const block of doc.blocks)
|
|
134
|
+
out.push(...block.lines);
|
|
135
|
+
const text = out.join(`
|
|
136
|
+
`);
|
|
137
|
+
return doc.trailingNewline ? `${text}
|
|
138
|
+
` : text;
|
|
139
|
+
}
|
|
140
|
+
function detectAnswered(blocks) {
|
|
141
|
+
return blocks.filter((b) => b.metadata?.status === "open" && b.answer !== "");
|
|
142
|
+
}
|
|
143
|
+
function openBlocks(blocks) {
|
|
144
|
+
return blocks.filter((b) => b.metadata?.status === "open");
|
|
145
|
+
}
|
|
146
|
+
function recordAnswer(block, footer) {
|
|
147
|
+
const lines = [...block.lines];
|
|
148
|
+
if (block.metadataIndex >= 0) {
|
|
149
|
+
const line = lines[block.metadataIndex] ?? "";
|
|
150
|
+
lines[block.metadataIndex] = line.replace(/(\|\s*status:\s*)open(\s*\|)/, "$1answered$2");
|
|
151
|
+
}
|
|
152
|
+
const footerLine = `<!-- ${FOOTER_KEYS.map((k) => `${k}: ${footer[k]}`).join(" | ")} -->`;
|
|
153
|
+
const insertAt = block.answerIndex >= 0 ? block.answerIndex + 1 : lines.length;
|
|
154
|
+
lines.splice(insertAt, 0, footerLine);
|
|
155
|
+
const metadata = block.metadata === null ? null : { ...block.metadata, status: "answered" };
|
|
156
|
+
return { ...block, lines, metadata, footer };
|
|
157
|
+
}
|
|
158
|
+
function replaceBlock(doc, block) {
|
|
159
|
+
return {
|
|
160
|
+
...doc,
|
|
161
|
+
blocks: doc.blocks.map((b) => b.id === block.id ? block : b)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/core/facts/Fact.ts
|
|
166
|
+
var FACT_KINDS = ["answer", "observed", "derived"];
|
|
167
|
+
var FACT_CONFIDENCES = ["measured", "inferred", "stated"];
|
|
168
|
+
var MAX_FACTS = 5000;
|
|
169
|
+
var MAX_FACT_CHARS = 2000;
|
|
170
|
+
function isRetired(fact) {
|
|
171
|
+
return fact.retired !== null && fact.retired.at !== null;
|
|
172
|
+
}
|
|
173
|
+
function isSuperseded(fact) {
|
|
174
|
+
return fact.superseded_by !== null;
|
|
175
|
+
}
|
|
176
|
+
function isLive(fact) {
|
|
177
|
+
return !isRetired(fact) && !isSuperseded(fact);
|
|
178
|
+
}
|
|
179
|
+
function factNumber(id) {
|
|
180
|
+
return Number.parseInt(id.slice(1), 10);
|
|
181
|
+
}
|
|
182
|
+
function formatFactId(n) {
|
|
183
|
+
return `F${String(n).padStart(3, "0")}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/core/schemas/deprecationNotice.ts
|
|
187
|
+
var said = new Set;
|
|
188
|
+
function noteDeprecations(file, outcome) {
|
|
189
|
+
for (const note of outcome.deprecations ?? []) {
|
|
190
|
+
const line = `${file}: ${note}`;
|
|
191
|
+
if (said.has(line))
|
|
192
|
+
continue;
|
|
193
|
+
said.add(line);
|
|
194
|
+
process.stderr.write(`${line}
|
|
195
|
+
`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export { FACT_KINDS, FACT_CONFIDENCES, MAX_FACTS, MAX_FACT_CHARS, isRetired, isLive, factNumber, formatFactId, noteDeprecations, parseQuestions, serializeQuestions, detectAnswered, openBlocks, recordAnswer, replaceBlock };
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
requireNumber,
|
|
6
6
|
requireString,
|
|
7
7
|
result
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-39zh2e44.js";
|
|
9
9
|
|
|
10
10
|
// src/core/events/EventLog.ts
|
|
11
11
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -17,6 +17,7 @@ var EVENT_TYPES = [
|
|
|
17
17
|
"run.closed",
|
|
18
18
|
"run.unlocked",
|
|
19
19
|
"run.cancelled",
|
|
20
|
+
"run.attended",
|
|
20
21
|
"phase.started",
|
|
21
22
|
"phase.done",
|
|
22
23
|
"stage.started",
|
|
@@ -33,6 +34,10 @@ var EVENT_TYPES = [
|
|
|
33
34
|
"gate.approved",
|
|
34
35
|
"gate.rejected",
|
|
35
36
|
"gate.revoked",
|
|
37
|
+
"gate.policy_changed",
|
|
38
|
+
"story.reopened",
|
|
39
|
+
"story.base_fastforwarded",
|
|
40
|
+
"operator_note",
|
|
36
41
|
"check.passed",
|
|
37
42
|
"check.failed",
|
|
38
43
|
"budget.warned",
|
|
@@ -40,6 +45,7 @@ var EVENT_TYPES = [
|
|
|
40
45
|
"budget.raised",
|
|
41
46
|
"fact.added",
|
|
42
47
|
"fact.retired",
|
|
48
|
+
"fact.superseded",
|
|
43
49
|
"map.refreshed",
|
|
44
50
|
"ticket.synced",
|
|
45
51
|
"error"
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_ECONOMY,
|
|
3
|
+
DEFAULT_ON_HOST_TOKENS_EXCEED,
|
|
2
4
|
asRunBudget,
|
|
3
5
|
asRunFile,
|
|
4
6
|
cursorStage,
|
|
5
7
|
derivePhaseStatus,
|
|
6
8
|
deriveRunStatus,
|
|
7
9
|
flatten,
|
|
10
|
+
isAttendedByHost,
|
|
11
|
+
isAttendedByHostView,
|
|
8
12
|
isFinished,
|
|
9
13
|
isTerminal,
|
|
10
14
|
nowRfc3339,
|
|
@@ -12,34 +16,37 @@ import {
|
|
|
12
16
|
stageAt,
|
|
13
17
|
validateRunBudget,
|
|
14
18
|
validateRunFile
|
|
15
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-4cp363kv.js";
|
|
16
20
|
import {
|
|
17
21
|
EventLog
|
|
18
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-rz541e2b.js";
|
|
19
23
|
import {
|
|
24
|
+
backupPathFor,
|
|
20
25
|
isAlive,
|
|
21
|
-
openBlocks,
|
|
22
|
-
parseQuestions,
|
|
23
26
|
readLock,
|
|
24
27
|
withWorkspaceLock,
|
|
25
28
|
workspaceRootOfRunDir,
|
|
29
|
+
writeAtomic,
|
|
26
30
|
yamlScalar
|
|
27
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-c6t5nx0r.js";
|
|
28
32
|
import {
|
|
29
|
-
noteDeprecations
|
|
30
|
-
|
|
33
|
+
noteDeprecations,
|
|
34
|
+
openBlocks,
|
|
35
|
+
parseQuestions
|
|
36
|
+
} from "./chunk-rpcxsqh3.js";
|
|
31
37
|
import {
|
|
32
38
|
listRunDirs,
|
|
33
|
-
parseYaml
|
|
34
|
-
|
|
39
|
+
parseYaml,
|
|
40
|
+
parseYamlRepairing
|
|
41
|
+
} from "./chunk-sq44k6g2.js";
|
|
35
42
|
|
|
36
43
|
// src/core/statusline/runSnapshot.ts
|
|
37
44
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
38
45
|
import { join as join2 } from "node:path";
|
|
39
46
|
|
|
40
47
|
// src/core/run/RunStore.ts
|
|
41
|
-
import { existsSync,
|
|
42
|
-
import { basename,
|
|
48
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
49
|
+
import { basename, join } from "node:path";
|
|
43
50
|
|
|
44
51
|
// src/core/run/emitRunYaml.ts
|
|
45
52
|
function inlineList(values) {
|
|
@@ -52,8 +59,12 @@ function gatesPolicy(policy) {
|
|
|
52
59
|
const entries = Object.entries(policy).map(([id, value]) => `${yamlScalar(id)}: ${yamlScalar(value)}`);
|
|
53
60
|
return `{${entries.join(", ")}}`;
|
|
54
61
|
}
|
|
62
|
+
function gateEvidence(e) {
|
|
63
|
+
return `{path: ${yamlScalar(e.path)}, role: ${yamlScalar(e.role)}, verdict: ${yamlScalar(e.verdict)}, ` + `sampled: ${String(e.sampled)}, of: ${String(e.of)}, resolved: ${String(e.resolved)}, ` + `refuted: ${String(e.refuted)}, outside_surface: ${String(e.outside_surface)}}`;
|
|
64
|
+
}
|
|
55
65
|
function gate(g) {
|
|
56
|
-
|
|
66
|
+
const evidence = g.evidence === undefined ? "" : `, evidence: ${gateEvidence(g.evidence)}`;
|
|
67
|
+
return `{type: ${yamlScalar(g.type)}, status: ${yamlScalar(g.status)}, by: ${yamlScalar(g.by)}, ` + `at: ${yamlScalar(g.at)}, note: ${yamlScalar(g.note)}${evidence}}`;
|
|
57
68
|
}
|
|
58
69
|
function task(t, indent) {
|
|
59
70
|
const inner = `${indent} `;
|
|
@@ -115,11 +126,18 @@ function emitRunYaml(run) {
|
|
|
115
126
|
lines.push(`triage: {split: ${yamlScalar(run.triage.split)}, depends_on: ${inlineList(run.triage.depends_on)}}`);
|
|
116
127
|
}
|
|
117
128
|
if (run.build !== undefined && run.build.epic_branch.length > 0) {
|
|
118
|
-
|
|
129
|
+
const model = run.build.branch_model === undefined ? "" : `, branch_model: ${yamlScalar(run.build.branch_model)}`;
|
|
130
|
+
lines.push(`build: {epic_branch: ${inlineList(run.build.epic_branch)}${model}}`);
|
|
119
131
|
}
|
|
120
132
|
if (run.cancelled !== undefined) {
|
|
121
133
|
lines.push(`cancelled: {by: ${yamlScalar(run.cancelled.by)}, at: ${yamlScalar(run.cancelled.at)}, ` + `note: ${yamlScalar(run.cancelled.note)}}`);
|
|
122
134
|
}
|
|
135
|
+
if (run.attended_by !== undefined) {
|
|
136
|
+
lines.push(`attended_by: ${yamlScalar(run.attended_by)}`);
|
|
137
|
+
}
|
|
138
|
+
if (run.keep_worktrees === true) {
|
|
139
|
+
lines.push("keep_worktrees: true");
|
|
140
|
+
}
|
|
123
141
|
if (run.gates_policy !== undefined && Object.keys(run.gates_policy).length > 0) {
|
|
124
142
|
lines.push(`gates_policy: ${gatesPolicy(run.gates_policy)}`);
|
|
125
143
|
}
|
|
@@ -149,10 +167,13 @@ function emitBudgetYaml(budget) {
|
|
|
149
167
|
`per_agent_max_usd: ${money(budget.per_agent_max_usd)}`,
|
|
150
168
|
`warn_at_pct: ${budget.warn_at_pct}`,
|
|
151
169
|
`on_exceed: ${yamlScalar(budget.on_exceed)}`,
|
|
170
|
+
...budget.economy === DEFAULT_ECONOMY ? [] : [`economy: ${yamlScalar(budget.economy)}`],
|
|
171
|
+
...budget.on_host_tokens_exceed === DEFAULT_ON_HOST_TOKENS_EXCEED ? [] : [`on_host_tokens_exceed: ${yamlScalar(budget.on_host_tokens_exceed)}`],
|
|
152
172
|
"phases:"
|
|
153
173
|
];
|
|
154
174
|
for (const phase of budget.phases) {
|
|
155
|
-
|
|
175
|
+
const economy = phase.economy === null ? "" : `, economy: ${yamlScalar(phase.economy)}`;
|
|
176
|
+
lines.push(` - {id: ${yamlScalar(phase.id)}, ceiling_usd: ${money(phase.ceiling_usd)}, ` + `spent_usd: ${money(phase.spent_usd)}${economy}}`);
|
|
156
177
|
}
|
|
157
178
|
return `${lines.join(`
|
|
158
179
|
`)}
|
|
@@ -179,8 +200,8 @@ class RunStore {
|
|
|
179
200
|
const runPath = join(runDir, "run.yml");
|
|
180
201
|
if (!existsSync(runPath))
|
|
181
202
|
throw new RunStoreError(`no run.yml in ${runDir}`);
|
|
182
|
-
const
|
|
183
|
-
const validation = validateRunFile(doc);
|
|
203
|
+
const run = parseStateFile(runPath);
|
|
204
|
+
const validation = validateRunFile(run.doc);
|
|
184
205
|
noteDeprecations(runPath, validation);
|
|
185
206
|
if (!validation.ok) {
|
|
186
207
|
const first = validation.issues[0];
|
|
@@ -189,14 +210,30 @@ class RunStore {
|
|
|
189
210
|
const budgetPath = join(runDir, "budget.yml");
|
|
190
211
|
if (!existsSync(budgetPath))
|
|
191
212
|
throw new RunStoreError(`no budget.yml in ${runDir}`);
|
|
192
|
-
const
|
|
193
|
-
const budgetValidation = validateRunBudget(
|
|
213
|
+
const budget = parseStateFile(budgetPath);
|
|
214
|
+
const budgetValidation = validateRunBudget(budget.doc);
|
|
194
215
|
noteDeprecations(budgetPath, budgetValidation);
|
|
195
216
|
if (!budgetValidation.ok) {
|
|
196
217
|
const first = budgetValidation.issues[0];
|
|
197
218
|
throw new RunStoreError(`invalid budget.yml (${budgetPath}): ${first?.path ?? ""} ${first?.message ?? "schema error"}`);
|
|
198
219
|
}
|
|
199
|
-
|
|
220
|
+
const store = new RunStore(runDir, asRunFile(run.doc), asRunBudget(budget.doc), EventLog.forRun(runDir));
|
|
221
|
+
if (run.repaired || budget.repaired)
|
|
222
|
+
store.healOnDisk(run.repaired, budget.repaired);
|
|
223
|
+
return store;
|
|
224
|
+
}
|
|
225
|
+
healOnDisk(runRepaired, budgetRepaired) {
|
|
226
|
+
try {
|
|
227
|
+
withWorkspaceLock(workspaceRootOfRunDir(this.runDir), () => {
|
|
228
|
+
if (runRepaired)
|
|
229
|
+
writeAtomic(join(this.runDir, "run.yml"), emitRunYaml(this.current));
|
|
230
|
+
if (budgetRepaired)
|
|
231
|
+
writeAtomic(join(this.runDir, "budget.yml"), emitBudgetYaml(this.currentBudget));
|
|
232
|
+
});
|
|
233
|
+
const which = [runRepaired ? "run.yml" : "", budgetRepaired ? "budget.yml" : ""].filter((name) => name !== "").join(" and ");
|
|
234
|
+
process.stderr.write(`tldrx: ${this.runDir}: ${which} held text broken across lines by an emitter bug ` + "since fixed. It was repaired in memory and rewritten correctly; the version that was " + `on disk is beside it as .bak. Nothing else about the run was changed.
|
|
235
|
+
`);
|
|
236
|
+
} catch {}
|
|
200
237
|
}
|
|
201
238
|
static find(root, runId) {
|
|
202
239
|
const dir = listRunDirs(root).find((d) => basename(d) === runId);
|
|
@@ -306,18 +343,21 @@ class RunStore {
|
|
|
306
343
|
}
|
|
307
344
|
}
|
|
308
345
|
}
|
|
309
|
-
function
|
|
310
|
-
const
|
|
311
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
346
|
+
function parseStateFile(path) {
|
|
347
|
+
const text = readFileSync(path, "utf8");
|
|
312
348
|
try {
|
|
313
|
-
|
|
314
|
-
renameSync(temp, path);
|
|
349
|
+
return parseYamlRepairing(text);
|
|
315
350
|
} catch (error) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
351
|
+
const backup = backupPathFor(path);
|
|
352
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
353
|
+
throw new RunStoreError([
|
|
354
|
+
`${path} does not parse: ${detail}`,
|
|
355
|
+
" every tldrx command on this run reads that file first, so all of them fail until it does",
|
|
356
|
+
existsSync(backup) ? ` the version the last save replaced is at ${backup} — recovery is MANUAL and tldrx ` + "will not do it for you:" + `
|
|
357
|
+
diff it, and if that version is the one you want, copy it back over ${basename(path)}` : ` there is no ${basename(backup)} beside it — this file has not been written since ` + "backups arrived, so there is no previous version to go back to",
|
|
358
|
+
" a hand-edit is the other way out, and it is the only one that keeps work the backup predates"
|
|
359
|
+
].join(`
|
|
360
|
+
`));
|
|
321
361
|
}
|
|
322
362
|
}
|
|
323
363
|
function rollUp(run) {
|
|
@@ -418,6 +458,7 @@ function fromStore(root) {
|
|
|
418
458
|
openCount: open.length,
|
|
419
459
|
autoGates: stages.filter((s) => s.gate.status === "approved" && s.gate.by === AUTO).length,
|
|
420
460
|
staleStages: stages.filter((s) => s.stale === true).length,
|
|
461
|
+
attendedByHost: isAttendedByHost(run),
|
|
421
462
|
source: "run-store"
|
|
422
463
|
};
|
|
423
464
|
}
|
|
@@ -446,6 +487,7 @@ function fromTolerantRead(root) {
|
|
|
446
487
|
openCount: open.length,
|
|
447
488
|
autoGates: 0,
|
|
448
489
|
staleStages: 0,
|
|
490
|
+
attendedByHost: isAttendedByHostView(view),
|
|
449
491
|
source: "tolerant"
|
|
450
492
|
};
|
|
451
493
|
}
|