tldr-experts 0.3.1 → 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 +963 -0
- package/README.md +73 -5
- package/dist/hooks/answer-capture.js +8 -8
- package/dist/hooks/budget-gate.js +78 -14
- package/dist/hooks/{chunk-zdxgragg.js → chunk-4cp363kv.js} +118 -7
- package/dist/hooks/{chunk-afamdvyn.js → chunk-7y2dq0pj.js} +1 -1
- package/dist/hooks/{chunk-458wgg9j.js → chunk-9zsqxr6y.js} +70 -2
- package/dist/hooks/{chunk-s5qsb4k6.js → chunk-b8kxzna2.js} +1 -1
- package/dist/hooks/{chunk-9kkm6q0t.js → chunk-c6t5nx0r.js} +35 -3
- package/dist/hooks/{chunk-sznsenee.js → chunk-m3mewgnw.js} +36 -17
- package/dist/hooks/{chunk-0z27twdk.js → chunk-phmdk72a.js} +10 -5
- package/dist/hooks/{chunk-3t91gvpp.js → chunk-rpcxsqh3.js} +8 -2
- package/dist/hooks/{chunk-ybacnpxd.js → chunk-rz541e2b.js} +3 -0
- package/dist/hooks/{chunk-xpxe94qe.js → chunk-s1c5h7yx.js} +54 -25
- package/dist/hooks/{chunk-ztczwtj0.js → chunk-sq44k6g2.js} +152 -14
- package/dist/hooks/{chunk-ytvmc5ns.js → chunk-t1ywrfr4.js} +19 -23
- package/dist/hooks/{chunk-3g61yg59.js → chunk-tzzwddct.js} +1 -1
- package/dist/hooks/claim-sources.js +28 -20
- package/dist/hooks/dod-gate.js +5 -5
- package/dist/hooks/no-reask.js +8 -8
- package/dist/hooks/session-start.js +12 -12
- package/dist/hooks/statusline.js +9 -9
- package/dist/tldrx.js +10962 -6172
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/stages/build/stage.yml +2 -1
- package/stages/plan/stage.md +11 -0
- package/templates/epic.md +5 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROJECT_FRAMEWORK_DIR,
|
|
3
3
|
PROJECT_WORK_DIR
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-sq44k6g2.js";
|
|
5
5
|
|
|
6
6
|
// src/core/lock/workspaceLock.ts
|
|
7
7
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, rmSync as rmSync2, writeSync, closeSync } from "node:fs";
|
|
@@ -127,6 +127,8 @@ function withWorkspaceLock(root, fn, timeoutMs = WORKSPACE_LOCK_TIMEOUT_MS) {
|
|
|
127
127
|
// src/core/facts/emitFactsYaml.ts
|
|
128
128
|
var PLAIN_SAFE = /^[A-Za-z][A-Za-z0-9_./-]*$/;
|
|
129
129
|
var YAML_KEYWORDS = new Set(["true", "false", "null", "yes", "no", "on", "off", "~"]);
|
|
130
|
+
var LONE_HIGH_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])/g;
|
|
131
|
+
var LONE_LOW_SURROGATE = /(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
130
132
|
function yamlScalar(value) {
|
|
131
133
|
if (value === null)
|
|
132
134
|
return "null";
|
|
@@ -134,7 +136,7 @@ function yamlScalar(value) {
|
|
|
134
136
|
return String(value);
|
|
135
137
|
if (PLAIN_SAFE.test(value) && !YAML_KEYWORDS.has(value.toLowerCase()))
|
|
136
138
|
return value;
|
|
137
|
-
return
|
|
139
|
+
return JSON.stringify(value.replace(LONE_HIGH_SURROGATE, "�").replace(LONE_LOW_SURROGATE, "�"));
|
|
138
140
|
}
|
|
139
141
|
function inlineList(values) {
|
|
140
142
|
return `[${values.map((v) => yamlScalar(v)).join(", ")}]`;
|
|
@@ -175,4 +177,34 @@ function emitFactsYaml(file, header) {
|
|
|
175
177
|
`;
|
|
176
178
|
}
|
|
177
179
|
|
|
178
|
-
|
|
180
|
+
// src/core/fs/writeAtomic.ts
|
|
181
|
+
import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
182
|
+
import { dirname as dirname3 } from "node:path";
|
|
183
|
+
var BACKUP_SUFFIX = ".bak";
|
|
184
|
+
function backupPathFor(path) {
|
|
185
|
+
return `${path}${BACKUP_SUFFIX}`;
|
|
186
|
+
}
|
|
187
|
+
function writeAtomic(path, content) {
|
|
188
|
+
const temp = `${path}.tmp-${String(process.pid)}`;
|
|
189
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
190
|
+
try {
|
|
191
|
+
writeFileSync2(temp, content, "utf8");
|
|
192
|
+
keepPreviousVersion(path);
|
|
193
|
+
renameSync(temp, path);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
try {
|
|
196
|
+
if (existsSync3(temp))
|
|
197
|
+
rmSync3(temp, { force: true });
|
|
198
|
+
} catch {}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function keepPreviousVersion(path) {
|
|
203
|
+
if (!existsSync3(path))
|
|
204
|
+
return;
|
|
205
|
+
try {
|
|
206
|
+
copyFileSync(path, backupPathFor(path));
|
|
207
|
+
} catch {}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { readLock, isAlive, workspaceRootOfRunDir, workspaceRootOfFactsPath, withWorkspaceLock, yamlScalar, emitFactsYaml, backupPathFor, writeAtomic };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROJECT_FRAMEWORK_DIR,
|
|
3
3
|
parseYaml
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-sq44k6g2.js";
|
|
5
5
|
|
|
6
6
|
// src/core/text/srcToken.ts
|
|
7
7
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
@@ -164,16 +164,20 @@ function resolveSrc(ref, ctx, section, claim = "") {
|
|
|
164
164
|
return refused(`unknown repo \`${ref.repo}\` (not in workspace.yml)`);
|
|
165
165
|
}
|
|
166
166
|
const bases = fileBases(ref, ctx);
|
|
167
|
+
let short = null;
|
|
167
168
|
for (const base of bases) {
|
|
168
169
|
if (!existsSync(base.abs) || !statSync(base.abs).isFile())
|
|
169
170
|
continue;
|
|
170
171
|
const lines = countLines(base.abs);
|
|
171
172
|
const highest = ref.endLine ?? ref.startLine;
|
|
172
173
|
if (highest > lines) {
|
|
173
|
-
|
|
174
|
+
short ??= refused(`${ref.path} has ${lines} line(s); cited line ${highest}`, base.abs);
|
|
175
|
+
continue;
|
|
174
176
|
}
|
|
175
177
|
return { ok: true, outcome: "ok", resolved: base.abs };
|
|
176
178
|
}
|
|
179
|
+
if (short !== null)
|
|
180
|
+
return short;
|
|
177
181
|
const tried = bases.map((b) => b.label).join(", ");
|
|
178
182
|
return refused(`no such file: ${ref.path} — tried ${tried}`, bases[0]?.abs);
|
|
179
183
|
}
|
|
@@ -270,26 +274,41 @@ function fileBases(ref, ctx) {
|
|
|
270
274
|
return [{ label: "the absolute path as written", abs: normalize(ref.path) }];
|
|
271
275
|
}
|
|
272
276
|
if (ref.repo !== null) {
|
|
273
|
-
const
|
|
274
|
-
return [
|
|
277
|
+
const rel2 = ctx.repos.get(ref.repo) ?? "";
|
|
278
|
+
return [
|
|
279
|
+
...epicBases(ctx, ref.repo, ref.path),
|
|
280
|
+
{ label: repoLabel(ref.repo, rel2), abs: normalize(join(ctx.root, rel2, ref.path)) }
|
|
281
|
+
];
|
|
275
282
|
}
|
|
276
|
-
const bases = [
|
|
277
|
-
|
|
278
|
-
|
|
283
|
+
const bases = [];
|
|
284
|
+
const slash = ref.path.indexOf("/");
|
|
285
|
+
const named = slash > 0 ? ref.path.slice(0, slash) : "";
|
|
286
|
+
const rel = named === "" ? undefined : ctx.repos.get(named);
|
|
287
|
+
if (rel !== undefined)
|
|
288
|
+
bases.push(...epicBases(ctx, named, ref.path.slice(slash + 1)));
|
|
289
|
+
bases.push({ label: "workspace root", abs: normalize(join(ctx.root, ref.path)) });
|
|
279
290
|
const runDir = ctx.runDir ?? null;
|
|
280
291
|
if (runDir !== null && runDir !== "") {
|
|
281
292
|
bases.push({ label: `run dir ${displayPath(ctx.root, runDir)}`, abs: normalize(join(runDir, ref.path)) });
|
|
282
293
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
294
|
+
if (rel !== undefined) {
|
|
295
|
+
bases.push({
|
|
296
|
+
label: repoLabel(named, rel),
|
|
297
|
+
abs: normalize(join(ctx.root, rel, ref.path.slice(slash + 1)))
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return bases;
|
|
301
|
+
}
|
|
302
|
+
function epicBases(ctx, repo, path) {
|
|
303
|
+
const trees = ctx.epicWorktrees ?? [];
|
|
304
|
+
const bases = [];
|
|
305
|
+
for (const tree of trees) {
|
|
306
|
+
if (tree.repo !== repo)
|
|
307
|
+
continue;
|
|
308
|
+
bases.push({
|
|
309
|
+
label: `epic worktree ${displayPath(ctx.root, tree.dir)}`,
|
|
310
|
+
abs: normalize(join(tree.dir, path))
|
|
311
|
+
});
|
|
293
312
|
}
|
|
294
313
|
return bases;
|
|
295
314
|
}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-39zh2e44.js";
|
|
4
4
|
import {
|
|
5
5
|
parseYaml
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-sq44k6g2.js";
|
|
7
7
|
|
|
8
8
|
// src/core/schemas/planCommon.ts
|
|
9
9
|
var STORY_ID_RE = /^S\d{1,4}$/;
|
|
@@ -22,7 +22,7 @@ function requireStringList(value, path, issues, rules = {}) {
|
|
|
22
22
|
}
|
|
23
23
|
const max = rules.max ?? MAX_LIST_ITEMS;
|
|
24
24
|
if (value.length > max) {
|
|
25
|
-
issues.push({ path, message: `${value.length} items exceeds the ${max} cap` });
|
|
25
|
+
issues.push({ path, message: `${value.length} items exceeds the ${max}-item cap` });
|
|
26
26
|
}
|
|
27
27
|
if (rules.nonEmpty === true && value.length === 0) {
|
|
28
28
|
issues.push({ path, message: "must not be empty" });
|
|
@@ -40,7 +40,10 @@ function requireStringList(value, path, issues, rules = {}) {
|
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
if (item.length > MAX_ITEM_CHARS) {
|
|
43
|
-
issues.push({
|
|
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
|
+
});
|
|
44
47
|
}
|
|
45
48
|
if (rules.pattern !== undefined && !rules.pattern.test(item)) {
|
|
46
49
|
issues.push({ path: at, message: `expected ${rules.patternName ?? String(rules.pattern)}, got \`${item}\`` });
|
|
@@ -130,8 +133,10 @@ function message(error) {
|
|
|
130
133
|
`)[0] ?? "parse error" : String(error);
|
|
131
134
|
}
|
|
132
135
|
// src/core/schemas/story.ts
|
|
133
|
-
var
|
|
134
|
-
var
|
|
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;
|
|
135
140
|
function parseDodBlock(text) {
|
|
136
141
|
const commands = [];
|
|
137
142
|
let present = false;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
parseSrcToken
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-m3mewgnw.js";
|
|
4
4
|
|
|
5
5
|
// src/core/text/questions.ts
|
|
6
6
|
var REQUIRED_METADATA_KEYS = ["id", "status", "area", "asked_by", "asked_at"];
|
|
@@ -170,6 +170,12 @@ var MAX_FACT_CHARS = 2000;
|
|
|
170
170
|
function isRetired(fact) {
|
|
171
171
|
return fact.retired !== null && fact.retired.at !== null;
|
|
172
172
|
}
|
|
173
|
+
function isSuperseded(fact) {
|
|
174
|
+
return fact.superseded_by !== null;
|
|
175
|
+
}
|
|
176
|
+
function isLive(fact) {
|
|
177
|
+
return !isRetired(fact) && !isSuperseded(fact);
|
|
178
|
+
}
|
|
173
179
|
function factNumber(id) {
|
|
174
180
|
return Number.parseInt(id.slice(1), 10);
|
|
175
181
|
}
|
|
@@ -190,4 +196,4 @@ function noteDeprecations(file, outcome) {
|
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
|
|
193
|
-
export { FACT_KINDS, FACT_CONFIDENCES, MAX_FACTS, MAX_FACT_CHARS, isRetired, factNumber, formatFactId, noteDeprecations, parseQuestions, serializeQuestions, detectAnswered, openBlocks, recordAnswer, replaceBlock };
|
|
199
|
+
export { FACT_KINDS, FACT_CONFIDENCES, MAX_FACTS, MAX_FACT_CHARS, isRetired, isLive, factNumber, formatFactId, noteDeprecations, parseQuestions, serializeQuestions, detectAnswered, openBlocks, recordAnswer, replaceBlock };
|
|
@@ -34,8 +34,10 @@ var EVENT_TYPES = [
|
|
|
34
34
|
"gate.approved",
|
|
35
35
|
"gate.rejected",
|
|
36
36
|
"gate.revoked",
|
|
37
|
+
"gate.policy_changed",
|
|
37
38
|
"story.reopened",
|
|
38
39
|
"story.base_fastforwarded",
|
|
40
|
+
"operator_note",
|
|
39
41
|
"check.passed",
|
|
40
42
|
"check.failed",
|
|
41
43
|
"budget.warned",
|
|
@@ -43,6 +45,7 @@ var EVENT_TYPES = [
|
|
|
43
45
|
"budget.raised",
|
|
44
46
|
"fact.added",
|
|
45
47
|
"fact.retired",
|
|
48
|
+
"fact.superseded",
|
|
46
49
|
"map.refreshed",
|
|
47
50
|
"ticket.synced",
|
|
48
51
|
"error"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_ECONOMY,
|
|
3
|
+
DEFAULT_ON_HOST_TOKENS_EXCEED,
|
|
3
4
|
asRunBudget,
|
|
4
5
|
asRunFile,
|
|
5
6
|
cursorStage,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
deriveRunStatus,
|
|
8
9
|
flatten,
|
|
9
10
|
isAttendedByHost,
|
|
11
|
+
isAttendedByHostView,
|
|
10
12
|
isFinished,
|
|
11
13
|
isTerminal,
|
|
12
14
|
nowRfc3339,
|
|
@@ -14,34 +16,37 @@ import {
|
|
|
14
16
|
stageAt,
|
|
15
17
|
validateRunBudget,
|
|
16
18
|
validateRunFile
|
|
17
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-4cp363kv.js";
|
|
18
20
|
import {
|
|
19
21
|
EventLog
|
|
20
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-rz541e2b.js";
|
|
21
23
|
import {
|
|
24
|
+
backupPathFor,
|
|
22
25
|
isAlive,
|
|
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
33
|
noteDeprecations,
|
|
30
34
|
openBlocks,
|
|
31
35
|
parseQuestions
|
|
32
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-rpcxsqh3.js";
|
|
33
37
|
import {
|
|
34
38
|
listRunDirs,
|
|
35
|
-
parseYaml
|
|
36
|
-
|
|
39
|
+
parseYaml,
|
|
40
|
+
parseYamlRepairing
|
|
41
|
+
} from "./chunk-sq44k6g2.js";
|
|
37
42
|
|
|
38
43
|
// src/core/statusline/runSnapshot.ts
|
|
39
44
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
40
45
|
import { join as join2 } from "node:path";
|
|
41
46
|
|
|
42
47
|
// src/core/run/RunStore.ts
|
|
43
|
-
import { existsSync,
|
|
44
|
-
import { basename,
|
|
48
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
49
|
+
import { basename, join } from "node:path";
|
|
45
50
|
|
|
46
51
|
// src/core/run/emitRunYaml.ts
|
|
47
52
|
function inlineList(values) {
|
|
@@ -121,7 +126,8 @@ function emitRunYaml(run) {
|
|
|
121
126
|
lines.push(`triage: {split: ${yamlScalar(run.triage.split)}, depends_on: ${inlineList(run.triage.depends_on)}}`);
|
|
122
127
|
}
|
|
123
128
|
if (run.build !== undefined && run.build.epic_branch.length > 0) {
|
|
124
|
-
|
|
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}}`);
|
|
125
131
|
}
|
|
126
132
|
if (run.cancelled !== undefined) {
|
|
127
133
|
lines.push(`cancelled: {by: ${yamlScalar(run.cancelled.by)}, at: ${yamlScalar(run.cancelled.at)}, ` + `note: ${yamlScalar(run.cancelled.note)}}`);
|
|
@@ -129,6 +135,9 @@ function emitRunYaml(run) {
|
|
|
129
135
|
if (run.attended_by !== undefined) {
|
|
130
136
|
lines.push(`attended_by: ${yamlScalar(run.attended_by)}`);
|
|
131
137
|
}
|
|
138
|
+
if (run.keep_worktrees === true) {
|
|
139
|
+
lines.push("keep_worktrees: true");
|
|
140
|
+
}
|
|
132
141
|
if (run.gates_policy !== undefined && Object.keys(run.gates_policy).length > 0) {
|
|
133
142
|
lines.push(`gates_policy: ${gatesPolicy(run.gates_policy)}`);
|
|
134
143
|
}
|
|
@@ -159,6 +168,7 @@ function emitBudgetYaml(budget) {
|
|
|
159
168
|
`warn_at_pct: ${budget.warn_at_pct}`,
|
|
160
169
|
`on_exceed: ${yamlScalar(budget.on_exceed)}`,
|
|
161
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)}`],
|
|
162
172
|
"phases:"
|
|
163
173
|
];
|
|
164
174
|
for (const phase of budget.phases) {
|
|
@@ -190,8 +200,8 @@ class RunStore {
|
|
|
190
200
|
const runPath = join(runDir, "run.yml");
|
|
191
201
|
if (!existsSync(runPath))
|
|
192
202
|
throw new RunStoreError(`no run.yml in ${runDir}`);
|
|
193
|
-
const
|
|
194
|
-
const validation = validateRunFile(doc);
|
|
203
|
+
const run = parseStateFile(runPath);
|
|
204
|
+
const validation = validateRunFile(run.doc);
|
|
195
205
|
noteDeprecations(runPath, validation);
|
|
196
206
|
if (!validation.ok) {
|
|
197
207
|
const first = validation.issues[0];
|
|
@@ -200,14 +210,30 @@ class RunStore {
|
|
|
200
210
|
const budgetPath = join(runDir, "budget.yml");
|
|
201
211
|
if (!existsSync(budgetPath))
|
|
202
212
|
throw new RunStoreError(`no budget.yml in ${runDir}`);
|
|
203
|
-
const
|
|
204
|
-
const budgetValidation = validateRunBudget(
|
|
213
|
+
const budget = parseStateFile(budgetPath);
|
|
214
|
+
const budgetValidation = validateRunBudget(budget.doc);
|
|
205
215
|
noteDeprecations(budgetPath, budgetValidation);
|
|
206
216
|
if (!budgetValidation.ok) {
|
|
207
217
|
const first = budgetValidation.issues[0];
|
|
208
218
|
throw new RunStoreError(`invalid budget.yml (${budgetPath}): ${first?.path ?? ""} ${first?.message ?? "schema error"}`);
|
|
209
219
|
}
|
|
210
|
-
|
|
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 {}
|
|
211
237
|
}
|
|
212
238
|
static find(root, runId) {
|
|
213
239
|
const dir = listRunDirs(root).find((d) => basename(d) === runId);
|
|
@@ -317,18 +343,21 @@ class RunStore {
|
|
|
317
343
|
}
|
|
318
344
|
}
|
|
319
345
|
}
|
|
320
|
-
function
|
|
321
|
-
const
|
|
322
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
346
|
+
function parseStateFile(path) {
|
|
347
|
+
const text = readFileSync(path, "utf8");
|
|
323
348
|
try {
|
|
324
|
-
|
|
325
|
-
renameSync(temp, path);
|
|
349
|
+
return parseYamlRepairing(text);
|
|
326
350
|
} catch (error) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
+
`));
|
|
332
361
|
}
|
|
333
362
|
}
|
|
334
363
|
function rollUp(run) {
|
|
@@ -458,7 +487,7 @@ function fromTolerantRead(root) {
|
|
|
458
487
|
openCount: open.length,
|
|
459
488
|
autoGates: 0,
|
|
460
489
|
staleStages: 0,
|
|
461
|
-
attendedByHost:
|
|
490
|
+
attendedByHost: isAttendedByHostView(view),
|
|
462
491
|
source: "tolerant"
|
|
463
492
|
};
|
|
464
493
|
}
|
|
@@ -7284,6 +7284,93 @@ var runtime = onBun ? bunRuntime : nodeRuntime;
|
|
|
7284
7284
|
function parseYaml(text) {
|
|
7285
7285
|
return runtime.parseYaml(text);
|
|
7286
7286
|
}
|
|
7287
|
+
function parseYamlRepairing(text) {
|
|
7288
|
+
try {
|
|
7289
|
+
return { doc: parseYaml(text), repaired: false };
|
|
7290
|
+
} catch (original) {
|
|
7291
|
+
const mended = rejoinBrokenQuotedScalars(text);
|
|
7292
|
+
if (mended === null)
|
|
7293
|
+
throw original;
|
|
7294
|
+
try {
|
|
7295
|
+
return { doc: parseYaml(mended), repaired: true };
|
|
7296
|
+
} catch {
|
|
7297
|
+
throw original;
|
|
7298
|
+
}
|
|
7299
|
+
}
|
|
7300
|
+
}
|
|
7301
|
+
function rejoinBrokenQuotedScalars(text) {
|
|
7302
|
+
const out = [];
|
|
7303
|
+
let changed = false;
|
|
7304
|
+
let inDouble = false;
|
|
7305
|
+
let inSingle = false;
|
|
7306
|
+
for (let i = 0;i < text.length; i++) {
|
|
7307
|
+
const ch = text[i];
|
|
7308
|
+
if (inDouble) {
|
|
7309
|
+
if (ch === "\\") {
|
|
7310
|
+
out.push(ch, text[i + 1] ?? "");
|
|
7311
|
+
i++;
|
|
7312
|
+
continue;
|
|
7313
|
+
}
|
|
7314
|
+
if (ch === '"') {
|
|
7315
|
+
inDouble = false;
|
|
7316
|
+
out.push(ch);
|
|
7317
|
+
continue;
|
|
7318
|
+
}
|
|
7319
|
+
const escaped = CONTROL_ESCAPES[ch];
|
|
7320
|
+
if (escaped !== undefined) {
|
|
7321
|
+
out.push(escaped);
|
|
7322
|
+
changed = true;
|
|
7323
|
+
continue;
|
|
7324
|
+
}
|
|
7325
|
+
out.push(ch);
|
|
7326
|
+
continue;
|
|
7327
|
+
}
|
|
7328
|
+
if (inSingle) {
|
|
7329
|
+
if (ch === "'") {
|
|
7330
|
+
if (text[i + 1] === "'") {
|
|
7331
|
+
out.push(ch, ch);
|
|
7332
|
+
i++;
|
|
7333
|
+
} else {
|
|
7334
|
+
inSingle = false;
|
|
7335
|
+
out.push(ch);
|
|
7336
|
+
}
|
|
7337
|
+
continue;
|
|
7338
|
+
}
|
|
7339
|
+
out.push(ch);
|
|
7340
|
+
continue;
|
|
7341
|
+
}
|
|
7342
|
+
if (ch === '"')
|
|
7343
|
+
inDouble = true;
|
|
7344
|
+
else if (ch === "'")
|
|
7345
|
+
inSingle = true;
|
|
7346
|
+
else if (ch === "#" && (i === 0 || text[i - 1] === " " || text[i - 1] === `
|
|
7347
|
+
`)) {
|
|
7348
|
+
const end = text.indexOf(`
|
|
7349
|
+
`, i);
|
|
7350
|
+
const stop = end === -1 ? text.length : end;
|
|
7351
|
+
out.push(text.slice(i, stop));
|
|
7352
|
+
i = stop - 1;
|
|
7353
|
+
continue;
|
|
7354
|
+
}
|
|
7355
|
+
out.push(ch);
|
|
7356
|
+
}
|
|
7357
|
+
if (inDouble)
|
|
7358
|
+
return null;
|
|
7359
|
+
return changed ? out.join("") : null;
|
|
7360
|
+
}
|
|
7361
|
+
var CONTROL_ESCAPES = {
|
|
7362
|
+
"\n": "\\n",
|
|
7363
|
+
"\r": "\\r",
|
|
7364
|
+
"\v": "\\v",
|
|
7365
|
+
"\f": "\\f",
|
|
7366
|
+
"\x00": "\\0",
|
|
7367
|
+
"\x07": "\\a",
|
|
7368
|
+
"\b": "\\b",
|
|
7369
|
+
"\x1B": "\\e",
|
|
7370
|
+
"
": "\\N",
|
|
7371
|
+
"\u2028": "\\L",
|
|
7372
|
+
"\u2029": "\\P"
|
|
7373
|
+
};
|
|
7287
7374
|
|
|
7288
7375
|
// src/core/paths.ts
|
|
7289
7376
|
import { dirname as dirname2, join } from "node:path";
|
|
@@ -7313,10 +7400,15 @@ var TEMPLATES_DIR = join(FRAMEWORK_ROOT, "templates");
|
|
|
7313
7400
|
var PLUGIN_DIR = join(FRAMEWORK_ROOT, "plugin");
|
|
7314
7401
|
var PROJECT_FRAMEWORK_DIR = ".tldrx";
|
|
7315
7402
|
var PROJECT_WORK_DIR = "tldrx-work";
|
|
7403
|
+
var PROJECT_WORKTREES_DIR = "worktrees";
|
|
7404
|
+
var EPIC_WORKTREE_PREFIX = "_epic-";
|
|
7405
|
+
function isEpicWorktreeOf(name, runId) {
|
|
7406
|
+
return name.startsWith(`${EPIC_WORKTREE_PREFIX}${runId}-`);
|
|
7407
|
+
}
|
|
7316
7408
|
|
|
7317
7409
|
// src/hooks/lib/workspace.ts
|
|
7318
7410
|
import { existsSync as existsSync2, readdirSync, readFileSync, statSync as statSync2 } from "node:fs";
|
|
7319
|
-
import { dirname as dirname3, isAbsolute, join as join2, resolve, sep } from "node:path";
|
|
7411
|
+
import { basename, dirname as dirname3, isAbsolute, join as join2, resolve, sep } from "node:path";
|
|
7320
7412
|
function locateWork(filePath) {
|
|
7321
7413
|
if (filePath === "")
|
|
7322
7414
|
return null;
|
|
@@ -7348,6 +7440,27 @@ function findWorkspaceRoot(start) {
|
|
|
7348
7440
|
return null;
|
|
7349
7441
|
}
|
|
7350
7442
|
var FALLBACK_DEFAULT_BRANCH = "main";
|
|
7443
|
+
function commandRolesOf(doc) {
|
|
7444
|
+
const out = new Map;
|
|
7445
|
+
const list = doc?.repos;
|
|
7446
|
+
if (!Array.isArray(list))
|
|
7447
|
+
return out;
|
|
7448
|
+
for (const entry of list) {
|
|
7449
|
+
if (typeof entry?.name !== "string")
|
|
7450
|
+
continue;
|
|
7451
|
+
const roles = new Map;
|
|
7452
|
+
const cmds = entry.commands;
|
|
7453
|
+
if (cmds !== null && typeof cmds === "object") {
|
|
7454
|
+
for (const [role, value] of Object.entries(cmds)) {
|
|
7455
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
7456
|
+
continue;
|
|
7457
|
+
roles.set(role, value);
|
|
7458
|
+
}
|
|
7459
|
+
}
|
|
7460
|
+
out.set(entry.name, roles);
|
|
7461
|
+
}
|
|
7462
|
+
return out;
|
|
7463
|
+
}
|
|
7351
7464
|
function loadWorkspace(root) {
|
|
7352
7465
|
const repos = new Map;
|
|
7353
7466
|
const commands = new Set;
|
|
@@ -7383,23 +7496,18 @@ function loadWorkspace(root) {
|
|
|
7383
7496
|
const list = doc?.repos;
|
|
7384
7497
|
if (!Array.isArray(list))
|
|
7385
7498
|
return empty();
|
|
7499
|
+
const declared = commandRolesOf(doc);
|
|
7386
7500
|
for (const entry of list) {
|
|
7387
7501
|
if (typeof entry?.name !== "string")
|
|
7388
7502
|
continue;
|
|
7389
7503
|
repos.set(entry.name, typeof entry.path === "string" ? entry.path : ".");
|
|
7390
7504
|
defaultBranches.set(entry.name, typeof entry.default_branch === "string" && entry.default_branch !== "" ? entry.default_branch : FALLBACK_DEFAULT_BRANCH);
|
|
7391
7505
|
const own = [];
|
|
7392
|
-
const roles = new Map;
|
|
7393
|
-
const
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
continue;
|
|
7398
|
-
commands.add(value);
|
|
7399
|
-
roles.set(role, value);
|
|
7400
|
-
if (!own.includes(value))
|
|
7401
|
-
own.push(value);
|
|
7402
|
-
}
|
|
7506
|
+
const roles = declared.get(entry.name) ?? new Map;
|
|
7507
|
+
for (const value of roles.values()) {
|
|
7508
|
+
commands.add(value);
|
|
7509
|
+
if (!own.includes(value))
|
|
7510
|
+
own.push(value);
|
|
7403
7511
|
}
|
|
7404
7512
|
repoCommands.set(entry.name, own);
|
|
7405
7513
|
commandRoles.set(entry.name, roles);
|
|
@@ -7411,9 +7519,39 @@ function toSrcContext(workspace, runDir) {
|
|
|
7411
7519
|
root: workspace.root,
|
|
7412
7520
|
repos: workspace.repos,
|
|
7413
7521
|
commands: workspace.commands,
|
|
7414
|
-
runDir: runDir ?? null
|
|
7522
|
+
runDir: runDir ?? null,
|
|
7523
|
+
epicWorktrees: epicWorktreesOf(workspace, runDir ?? null)
|
|
7415
7524
|
};
|
|
7416
7525
|
}
|
|
7526
|
+
function epicWorktreesOf(workspace, runDir) {
|
|
7527
|
+
if (runDir === null || runDir === "")
|
|
7528
|
+
return [];
|
|
7529
|
+
const runId = basename(runDir);
|
|
7530
|
+
if (runId === "")
|
|
7531
|
+
return [];
|
|
7532
|
+
const trees = [];
|
|
7533
|
+
for (const repo of workspace.repos.keys()) {
|
|
7534
|
+
const dir = join2(workspace.root, PROJECT_FRAMEWORK_DIR, PROJECT_WORKTREES_DIR, repo);
|
|
7535
|
+
if (!existsSync2(dir))
|
|
7536
|
+
continue;
|
|
7537
|
+
let entries;
|
|
7538
|
+
try {
|
|
7539
|
+
entries = readdirSync(dir);
|
|
7540
|
+
} catch {
|
|
7541
|
+
continue;
|
|
7542
|
+
}
|
|
7543
|
+
for (const name of entries.slice().sort()) {
|
|
7544
|
+
if (!isEpicWorktreeOf(name, runId))
|
|
7545
|
+
continue;
|
|
7546
|
+
const abs = join2(dir, name);
|
|
7547
|
+
try {
|
|
7548
|
+
if (statSync2(abs).isDirectory())
|
|
7549
|
+
trees.push({ repo, dir: abs });
|
|
7550
|
+
} catch {}
|
|
7551
|
+
}
|
|
7552
|
+
}
|
|
7553
|
+
return trees;
|
|
7554
|
+
}
|
|
7417
7555
|
function repoPath(workspace, name) {
|
|
7418
7556
|
const rel = workspace.repos.get(name);
|
|
7419
7557
|
return rel === undefined ? null : join2(workspace.root, rel);
|
|
@@ -7437,4 +7575,4 @@ function listRunDirs(root) {
|
|
|
7437
7575
|
return dirs.sort().reverse();
|
|
7438
7576
|
}
|
|
7439
7577
|
|
|
7440
|
-
export { runtime, parseYaml, STAGES_DIR, TEMPLATES_DIR, PROJECT_FRAMEWORK_DIR, PROJECT_WORK_DIR, locateWork, findWorkspaceRoot, loadWorkspace, toSrcContext, repoPath, factsPath, stageYamlPath, listRunDirs };
|
|
7578
|
+
export { runtime, parseYaml, parseYamlRepairing, STAGES_DIR, TEMPLATES_DIR, PROJECT_FRAMEWORK_DIR, PROJECT_WORK_DIR, locateWork, findWorkspaceRoot, loadWorkspace, toSrcContext, repoPath, factsPath, stageYamlPath, listRunDirs };
|