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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runtime
3
- } from "./chunk-ztczwtj0.js";
3
+ } from "./chunk-sq44k6g2.js";
4
4
 
5
5
  // src/core/hooks/passthrough.ts
6
6
  async function readStdin() {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  PROJECT_FRAMEWORK_DIR,
3
3
  PROJECT_WORK_DIR
4
- } from "./chunk-ztczwtj0.js";
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 `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
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
- export { readLock, isAlive, workspaceRootOfRunDir, workspaceRootOfFactsPath, withWorkspaceLock, yamlScalar, emitFactsYaml };
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-ztczwtj0.js";
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
- return refused(`${ref.path} has ${lines} line(s); cited line ${highest}`, base.abs);
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 rel = ctx.repos.get(ref.repo) ?? "";
274
- return [{ label: repoLabel(ref.repo, rel), abs: normalize(join(ctx.root, rel, ref.path)) }];
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
- { label: "workspace root", abs: normalize(join(ctx.root, ref.path)) }
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
- const slash = ref.path.indexOf("/");
284
- if (slash > 0) {
285
- const name = ref.path.slice(0, slash);
286
- const rel = ctx.repos.get(name);
287
- if (rel !== undefined) {
288
- bases.push({
289
- label: repoLabel(name, rel),
290
- abs: normalize(join(ctx.root, rel, ref.path.slice(slash + 1)))
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-ztczwtj0.js";
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({ path: at, message: `${item.length} characters exceeds the ${MAX_ITEM_CHARS} cap` });
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 FENCE_OPEN_RE = /^\s*```+\s*dod\s*$/i;
134
- var FENCE_CLOSE_RE = /^\s*```+\s*$/;
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-sznsenee.js";
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-zdxgragg.js";
19
+ } from "./chunk-4cp363kv.js";
18
20
  import {
19
21
  EventLog
20
- } from "./chunk-ybacnpxd.js";
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-9kkm6q0t.js";
31
+ } from "./chunk-c6t5nx0r.js";
28
32
  import {
29
33
  noteDeprecations,
30
34
  openBlocks,
31
35
  parseQuestions
32
- } from "./chunk-3t91gvpp.js";
36
+ } from "./chunk-rpcxsqh3.js";
33
37
  import {
34
38
  listRunDirs,
35
- parseYaml
36
- } from "./chunk-ztczwtj0.js";
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, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
44
- import { basename, dirname, join } from "node:path";
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
- lines.push(`build: {epic_branch: ${inlineList(run.build.epic_branch)}}`);
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 doc = parseYaml(readFileSync(runPath, "utf8"));
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 budgetDoc = parseYaml(readFileSync(budgetPath, "utf8"));
204
- const budgetValidation = validateRunBudget(budgetDoc);
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
- return new RunStore(runDir, asRunFile(doc), asRunBudget(budgetDoc), EventLog.forRun(runDir));
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 writeAtomic(path, content) {
321
- const temp = `${path}.tmp-${String(process.pid)}`;
322
- mkdirSync(dirname(path), { recursive: true });
346
+ function parseStateFile(path) {
347
+ const text = readFileSync(path, "utf8");
323
348
  try {
324
- writeFileSync(temp, content, "utf8");
325
- renameSync(temp, path);
349
+ return parseYamlRepairing(text);
326
350
  } catch (error) {
327
- try {
328
- if (existsSync(temp))
329
- rmSync(temp, { force: true });
330
- } catch {}
331
- throw error;
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: false,
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 cmds = entry.commands;
7394
- if (cmds !== null && typeof cmds === "object") {
7395
- for (const [role, value] of Object.entries(cmds)) {
7396
- if (typeof value !== "string" || value.trim() === "")
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 };