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.
Files changed (36) hide show
  1. package/CHANGELOG.md +1957 -0
  2. package/README.md +99 -12
  3. package/dist/hooks/answer-capture.js +21 -12
  4. package/dist/hooks/budget-gate.js +95 -11
  5. package/dist/hooks/{chunk-0bt6yb2q.js → chunk-39zh2e44.js} +10 -1
  6. package/dist/hooks/chunk-4cp363kv.js +1766 -0
  7. package/dist/hooks/{chunk-t8tdv11p.js → chunk-7y2dq0pj.js} +1 -1
  8. package/dist/hooks/chunk-9zsqxr6y.js +213 -0
  9. package/dist/hooks/{chunk-a8p2rc94.js → chunk-b8kxzna2.js} +1 -1
  10. package/dist/hooks/{chunk-j234zf0t.js → chunk-c6t5nx0r.js} +37 -166
  11. package/dist/hooks/{chunk-g395gk7e.js → chunk-m3mewgnw.js} +36 -17
  12. package/dist/hooks/chunk-phmdk72a.js +164 -0
  13. package/dist/hooks/chunk-rpcxsqh3.js +199 -0
  14. package/dist/hooks/{chunk-azctppjh.js → chunk-rz541e2b.js} +7 -1
  15. package/dist/hooks/{chunk-y0jdr3et.js → chunk-s1c5h7yx.js} +71 -29
  16. package/dist/hooks/{chunk-p274ckxv.js → chunk-sq44k6g2.js} +155 -12
  17. package/dist/hooks/{chunk-x98qs959.js → chunk-t1ywrfr4.js} +35 -42
  18. package/dist/hooks/{chunk-1zwcxd3f.js → chunk-tzzwddct.js} +1 -1
  19. package/dist/hooks/claim-sources.js +31 -23
  20. package/dist/hooks/dod-gate.js +12 -37
  21. package/dist/hooks/no-reask.js +9 -9
  22. package/dist/hooks/session-start.js +176 -422
  23. package/dist/hooks/statusline.js +14 -11
  24. package/dist/tldrx.js +22798 -12309
  25. package/package.json +3 -1
  26. package/plugin/.claude-plugin/plugin.json +1 -1
  27. package/plugin/skills/tldrx/SKILL.md +16 -3
  28. package/stages/build/stage.md +5 -0
  29. package/stages/build/stage.yml +2 -1
  30. package/stages/plan/stage.md +11 -0
  31. package/templates/budget.yml +5 -0
  32. package/templates/epic.md +5 -3
  33. package/dist/hooks/chunk-ae6bkfs5.js +0 -0
  34. package/dist/hooks/chunk-kw4tffzf.js +0 -139
  35. package/dist/hooks/chunk-sdjnnmzz.js +0 -497
  36. package/dist/hooks/chunk-t56k6146.js +0 -14
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  toolInput
3
- } from "./chunk-1zwcxd3f.js";
3
+ } from "./chunk-tzzwddct.js";
4
4
 
5
5
  // src/hooks/lib/wouldBe.ts
6
6
  import { existsSync, readFileSync } from "node:fs";
@@ -0,0 +1,213 @@
1
+ import {
2
+ hasSrcMarker,
3
+ parseSrcToken,
4
+ resolveSrc
5
+ } from "./chunk-m3mewgnw.js";
6
+
7
+ // src/core/text/handoff.ts
8
+ var HANDOFF_SECTIONS = ["Findings", "Decisions", "Unknowns", "Evidence ledger"];
9
+ var H2_RE = /^##\s+(.+?)\s*$/;
10
+ var BULLET_RE = /^(?: {0,3}-|\d{1,9}[.)])\s+(\S.*)$/;
11
+ var CONTINUATION_RE = /^\s+\S/;
12
+ function parseHandoff(text) {
13
+ const lines = text.split(`
14
+ `);
15
+ const sections = [];
16
+ const headings = [];
17
+ let current = null;
18
+ let pending = null;
19
+ const flushBullet = () => {
20
+ if (pending === null)
21
+ return;
22
+ if (current !== null) {
23
+ const joined = pending.parts.join(" ");
24
+ current.bullets.push({ line: pending.line, text: joined, token: parseSrcToken(joined) });
25
+ }
26
+ pending = null;
27
+ };
28
+ const flush = () => {
29
+ flushBullet();
30
+ if (current !== null)
31
+ sections.push(current);
32
+ current = null;
33
+ };
34
+ for (let i = 0;i < lines.length; i++) {
35
+ const line = lines[i] ?? "";
36
+ const heading = H2_RE.exec(line);
37
+ if (heading !== null && heading[1] !== undefined) {
38
+ flush();
39
+ headings.push(heading[1]);
40
+ current = { name: heading[1], headingLine: i + 1, bullets: [] };
41
+ continue;
42
+ }
43
+ if (line.startsWith("# ")) {
44
+ flush();
45
+ continue;
46
+ }
47
+ if (current === null)
48
+ continue;
49
+ const bullet = BULLET_RE.exec(line);
50
+ if (bullet !== null && bullet[1] !== undefined) {
51
+ flushBullet();
52
+ pending = { line: i + 1, parts: [bullet[1]] };
53
+ continue;
54
+ }
55
+ if (pending !== null && CONTINUATION_RE.test(line)) {
56
+ pending.parts.push(line.trim());
57
+ continue;
58
+ }
59
+ flushBullet();
60
+ }
61
+ flush();
62
+ return { sections, headings };
63
+ }
64
+ function parseItems(text) {
65
+ const items = [];
66
+ let section = "";
67
+ let pending = null;
68
+ const flush = () => {
69
+ if (pending === null)
70
+ return;
71
+ const joined = pending.parts.join(" ");
72
+ items.push({ line: pending.line, text: joined, section: pending.section, token: parseSrcToken(joined) });
73
+ pending = null;
74
+ };
75
+ const lines = text.split(`
76
+ `);
77
+ for (let i = 0;i < lines.length; i++) {
78
+ const line = lines[i] ?? "";
79
+ const heading = H2_RE.exec(line);
80
+ if (heading !== null && heading[1] !== undefined) {
81
+ flush();
82
+ section = heading[1];
83
+ continue;
84
+ }
85
+ const bullet = BULLET_RE.exec(line);
86
+ if (bullet !== null && bullet[1] !== undefined) {
87
+ flush();
88
+ pending = { line: i + 1, parts: [bullet[1]], section };
89
+ continue;
90
+ }
91
+ if (pending !== null && CONTINUATION_RE.test(line)) {
92
+ pending.parts.push(line.trim());
93
+ continue;
94
+ }
95
+ flush();
96
+ }
97
+ flush();
98
+ return items;
99
+ }
100
+ function isHandoff(text) {
101
+ return missingSections(parseHandoff(text)).length === 0;
102
+ }
103
+ function missingSections(handoff, required = HANDOFF_SECTIONS) {
104
+ const present = handoff.headings;
105
+ const missing = [];
106
+ let cursor = 0;
107
+ for (const name of required) {
108
+ const at = present.indexOf(name, cursor);
109
+ if (at === -1)
110
+ missing.push(name);
111
+ else
112
+ cursor = at + 1;
113
+ }
114
+ return missing;
115
+ }
116
+ var MAX_BULLETS = 200;
117
+ function validateSections(handoff, required, ctx, lineOffset = 0) {
118
+ const emptySections = [];
119
+ const unsourced = [];
120
+ const malformed = [];
121
+ const unresolved = [];
122
+ const unverified = [];
123
+ let bulletCount = 0;
124
+ const wanted = new Set(required);
125
+ for (const section of handoff.sections) {
126
+ if (!wanted.has(section.name))
127
+ continue;
128
+ if (section.bullets.length === 0) {
129
+ emptySections.push({ name: section.name, line: section.headingLine + lineOffset });
130
+ }
131
+ for (const bullet of section.bullets) {
132
+ bulletCount++;
133
+ const line = bullet.line + lineOffset;
134
+ if (bullet.token === null) {
135
+ if (hasSrcMarker(bullet.text)) {
136
+ malformed.push({
137
+ line,
138
+ message: "malformed citation — the `[src: …]` token must be the last thing on the line " + "(closing quotes, brackets and a final `.` are allowed after it, words are not)"
139
+ });
140
+ } else {
141
+ unsourced.push(line);
142
+ }
143
+ continue;
144
+ }
145
+ for (const error of bullet.token.errors) {
146
+ unresolved.push({ line, message: `[src: ${error.raw}] — ${error.message}` });
147
+ }
148
+ const claim = bullet.text.replace(bullet.token.raw, " ").trim();
149
+ for (const ref of bullet.token.refs) {
150
+ const resolution = resolveSrc(ref, ctx, section.name, claim);
151
+ const issue = { line, message: `[src: ${ref.raw}] — ${resolution.message ?? "unresolvable"}` };
152
+ if (resolution.outcome === "refused")
153
+ unresolved.push(issue);
154
+ else if (resolution.outcome === "unverified")
155
+ unverified.push(issue);
156
+ }
157
+ }
158
+ }
159
+ return { emptySections, unsourced, malformed, unresolved, unverified, bulletCount };
160
+ }
161
+ function validateHandoff(text, ctx) {
162
+ const handoff = parseHandoff(text);
163
+ const missing = missingSections(handoff);
164
+ const report = validateSections(handoff, HANDOFF_SECTIONS, ctx);
165
+ const unresolved = [...report.unresolved];
166
+ if (report.bulletCount > MAX_BULLETS) {
167
+ unresolved.push({ line: 0, message: `${report.bulletCount} bullets exceeds the ${MAX_BULLETS} cap` });
168
+ }
169
+ return {
170
+ ok: missing.length === 0 && report.emptySections.length === 0 && report.unsourced.length === 0 && report.malformed.length === 0 && unresolved.length === 0,
171
+ missingSections: missing,
172
+ emptySections: report.emptySections,
173
+ unsourced: report.unsourced,
174
+ malformed: report.malformed,
175
+ unresolved,
176
+ unverified: report.unverified,
177
+ bulletCount: report.bulletCount
178
+ };
179
+ }
180
+ function validateCitations(text, ctx) {
181
+ const malformed = [];
182
+ const unresolved = [];
183
+ const unverified = [];
184
+ let cited = 0;
185
+ for (const item of parseItems(text)) {
186
+ if (item.token === null) {
187
+ if (!hasSrcMarker(item.text))
188
+ continue;
189
+ cited++;
190
+ malformed.push({
191
+ line: item.line,
192
+ message: "malformed citation — the `[src: …]` token must be the last thing on the line " + "(closing quotes, brackets and a final `.` are allowed after it, words are not)"
193
+ });
194
+ continue;
195
+ }
196
+ cited++;
197
+ for (const error of item.token.errors) {
198
+ unresolved.push({ line: item.line, message: `[src: ${error.raw}] — ${error.message}` });
199
+ }
200
+ const claim = item.text.replace(item.token.raw, " ").trim();
201
+ for (const ref of item.token.refs) {
202
+ const resolution = resolveSrc(ref, ctx, item.section, claim);
203
+ const issue = { line: item.line, message: `[src: ${ref.raw}] — ${resolution.message ?? "unresolvable"}` };
204
+ if (resolution.outcome === "refused")
205
+ unresolved.push(issue);
206
+ else if (resolution.outcome === "unverified")
207
+ unverified.push(issue);
208
+ }
209
+ }
210
+ return { malformed, unresolved, unverified, cited };
211
+ }
212
+
213
+ export { parseHandoff, isHandoff, validateHandoff, validateCitations };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runtime
3
- } from "./chunk-p274ckxv.js";
3
+ } from "./chunk-sq44k6g2.js";
4
4
 
5
5
  // src/core/hooks/passthrough.ts
6
6
  async function readStdin() {
@@ -1,170 +1,7 @@
1
- import {
2
- parseSrcToken
3
- } from "./chunk-g395gk7e.js";
4
1
  import {
5
2
  PROJECT_FRAMEWORK_DIR,
6
3
  PROJECT_WORK_DIR
7
- } from "./chunk-p274ckxv.js";
8
-
9
- // src/core/text/questions.ts
10
- var REQUIRED_METADATA_KEYS = ["id", "status", "area", "asked_by", "asked_at"];
11
- var HEADING_RE = /^##\s+(Q\d{1,6})\s+·\s+(.+?)\s*$/;
12
- var METADATA_RE = /^<!--\s*(.*?)\s*-->$/;
13
- var WHY_RE = /^Why asked:\s*(.*)$/;
14
- var OPTION_RE = /^-\s+([A-E])\)\s*(.*)$/;
15
- var ANSWER_RE = /^\[Answer\]:[ \t]*(\S.*)$/;
16
- var ANSWER_SLOT_RE = /^\[Answer\]:/;
17
- var FOOTER_KEYS = ["answered_by", "answered_at", "fact"];
18
- function parseQuestions(text) {
19
- const trailingNewline = text.endsWith(`
20
- `);
21
- const body = trailingNewline ? text.slice(0, -1) : text;
22
- const lines = body === "" ? [] : body.split(`
23
- `);
24
- const preamble = [];
25
- const blocks = [];
26
- let pending = null;
27
- const close = () => {
28
- if (pending === null)
29
- return;
30
- blocks.push(buildBlock(pending.start + 1, pending.lines));
31
- pending = null;
32
- };
33
- for (let i = 0;i < lines.length; i++) {
34
- const line = lines[i] ?? "";
35
- if (HEADING_RE.test(line)) {
36
- close();
37
- pending = { start: i, lines: [line] };
38
- continue;
39
- }
40
- if (pending === null)
41
- preamble.push(line);
42
- else
43
- pending.lines.push(line);
44
- }
45
- close();
46
- return { preamble, blocks, trailingNewline };
47
- }
48
- function buildBlock(startLine, lines) {
49
- const heading = HEADING_RE.exec(lines[0] ?? "");
50
- const id = heading?.[1] ?? "";
51
- const title = heading?.[2] ?? "";
52
- let metadata = null;
53
- let metadataIndex = -1;
54
- let whyAsked = null;
55
- let whySrc = null;
56
- const options = [];
57
- let answer = "";
58
- let answerIndex = -1;
59
- let footer = null;
60
- for (let i = 1;i < lines.length; i++) {
61
- const line = lines[i] ?? "";
62
- const comment = METADATA_RE.exec(line);
63
- if (comment !== null && comment[1] !== undefined) {
64
- const pairs = parsePipeComment(comment[1]);
65
- if (metadata === null && pairs.some(([k]) => k === "id")) {
66
- metadata = toMetadata(pairs);
67
- metadataIndex = i;
68
- } else if (footer === null && pairs.some(([k]) => k === "answered_by")) {
69
- footer = toFooter(pairs);
70
- }
71
- continue;
72
- }
73
- const why = WHY_RE.exec(line);
74
- if (why !== null && whyAsked === null) {
75
- whyAsked = why[1] ?? "";
76
- whySrc = parseSrcToken(line);
77
- continue;
78
- }
79
- const option = OPTION_RE.exec(line);
80
- if (option !== null && option[1] !== undefined && option[2] !== undefined) {
81
- options.push({ letter: option[1], text: option[2] });
82
- continue;
83
- }
84
- if (ANSWER_SLOT_RE.test(line) && answerIndex === -1) {
85
- answerIndex = i;
86
- const captured = ANSWER_RE.exec(line);
87
- answer = (captured?.[1] ?? "").trim();
88
- }
89
- }
90
- return {
91
- id,
92
- title,
93
- metadata,
94
- metadataIndex,
95
- whyAsked,
96
- whySrc,
97
- options,
98
- answer,
99
- answerIndex,
100
- footer,
101
- startLine,
102
- lines: [...lines]
103
- };
104
- }
105
- function parsePipeComment(inner) {
106
- const pairs = [];
107
- for (const part of inner.split("|")) {
108
- const colon = part.indexOf(":");
109
- if (colon === -1)
110
- continue;
111
- pairs.push([part.slice(0, colon).trim(), part.slice(colon + 1).trim()]);
112
- }
113
- return pairs;
114
- }
115
- function toMetadata(pairs) {
116
- const map = new Map(pairs);
117
- const extra = pairs.filter(([k]) => !REQUIRED_METADATA_KEYS.includes(k));
118
- return {
119
- id: map.get("id") ?? "",
120
- status: map.get("status") ?? "",
121
- area: map.get("area") ?? "",
122
- asked_by: map.get("asked_by") ?? "",
123
- asked_at: map.get("asked_at") ?? "",
124
- extra
125
- };
126
- }
127
- function toFooter(pairs) {
128
- const map = new Map(pairs);
129
- return {
130
- answered_by: map.get("answered_by") ?? "",
131
- answered_at: map.get("answered_at") ?? "",
132
- fact: map.get("fact") ?? ""
133
- };
134
- }
135
- function serializeQuestions(doc) {
136
- const out = [...doc.preamble];
137
- for (const block of doc.blocks)
138
- out.push(...block.lines);
139
- const text = out.join(`
140
- `);
141
- return doc.trailingNewline ? `${text}
142
- ` : text;
143
- }
144
- function detectAnswered(blocks) {
145
- return blocks.filter((b) => b.metadata?.status === "open" && b.answer !== "");
146
- }
147
- function openBlocks(blocks) {
148
- return blocks.filter((b) => b.metadata?.status === "open");
149
- }
150
- function recordAnswer(block, footer) {
151
- const lines = [...block.lines];
152
- if (block.metadataIndex >= 0) {
153
- const line = lines[block.metadataIndex] ?? "";
154
- lines[block.metadataIndex] = line.replace(/(\|\s*status:\s*)open(\s*\|)/, "$1answered$2");
155
- }
156
- const footerLine = `<!-- ${FOOTER_KEYS.map((k) => `${k}: ${footer[k]}`).join(" | ")} -->`;
157
- const insertAt = block.answerIndex >= 0 ? block.answerIndex + 1 : lines.length;
158
- lines.splice(insertAt, 0, footerLine);
159
- const metadata = block.metadata === null ? null : { ...block.metadata, status: "answered" };
160
- return { ...block, lines, metadata, footer };
161
- }
162
- function replaceBlock(doc, block) {
163
- return {
164
- ...doc,
165
- blocks: doc.blocks.map((b) => b.id === block.id ? block : b)
166
- };
167
- }
4
+ } from "./chunk-sq44k6g2.js";
168
5
 
169
6
  // src/core/lock/workspaceLock.ts
170
7
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, rmSync as rmSync2, writeSync, closeSync } from "node:fs";
@@ -290,6 +127,8 @@ function withWorkspaceLock(root, fn, timeoutMs = WORKSPACE_LOCK_TIMEOUT_MS) {
290
127
  // src/core/facts/emitFactsYaml.ts
291
128
  var PLAIN_SAFE = /^[A-Za-z][A-Za-z0-9_./-]*$/;
292
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;
293
132
  function yamlScalar(value) {
294
133
  if (value === null)
295
134
  return "null";
@@ -297,7 +136,7 @@ function yamlScalar(value) {
297
136
  return String(value);
298
137
  if (PLAIN_SAFE.test(value) && !YAML_KEYWORDS.has(value.toLowerCase()))
299
138
  return value;
300
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
139
+ return JSON.stringify(value.replace(LONE_HIGH_SURROGATE, "�").replace(LONE_LOW_SURROGATE, "�"));
301
140
  }
302
141
  function inlineList(values) {
303
142
  return `[${values.map((v) => yamlScalar(v)).join(", ")}]`;
@@ -315,6 +154,8 @@ function emitFact(fact, indent = " ") {
315
154
  `${inner}supersedes: ${yamlScalar(fact.supersedes)}`,
316
155
  `${inner}superseded_by: ${yamlScalar(fact.superseded_by)}`
317
156
  ];
157
+ if (fact.truncated === true)
158
+ lines.push(`${inner}truncated: true`);
318
159
  if (fact.retired === null) {
319
160
  lines.push(`${inner}retired: null`);
320
161
  } else {
@@ -336,4 +177,34 @@ function emitFactsYaml(file, header) {
336
177
  `;
337
178
  }
338
179
 
339
- export { readLock, isAlive, workspaceRootOfRunDir, workspaceRootOfFactsPath, withWorkspaceLock, yamlScalar, emitFactsYaml, parseQuestions, serializeQuestions, detectAnswered, openBlocks, recordAnswer, replaceBlock };
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-p274ckxv.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
  }