tldr-experts 0.3.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 (75) hide show
  1. package/CHANGELOG.md +913 -0
  2. package/LICENSE +21 -0
  3. package/README.md +174 -0
  4. package/dist/hooks/answer-capture.js +174 -0
  5. package/dist/hooks/budget-gate.js +173 -0
  6. package/dist/hooks/chunk-0bt6yb2q.js +88 -0
  7. package/dist/hooks/chunk-1zwcxd3f.js +66 -0
  8. package/dist/hooks/chunk-9gb21660.js +66 -0
  9. package/dist/hooks/chunk-a8p2rc94.js +20 -0
  10. package/dist/hooks/chunk-ae6bkfs5.js +0 -0
  11. package/dist/hooks/chunk-azctppjh.js +198 -0
  12. package/dist/hooks/chunk-g395gk7e.js +503 -0
  13. package/dist/hooks/chunk-j234zf0t.js +339 -0
  14. package/dist/hooks/chunk-kw4tffzf.js +139 -0
  15. package/dist/hooks/chunk-p274ckxv.js +7435 -0
  16. package/dist/hooks/chunk-sdjnnmzz.js +497 -0
  17. package/dist/hooks/chunk-t56k6146.js +14 -0
  18. package/dist/hooks/chunk-t8tdv11p.js +35 -0
  19. package/dist/hooks/chunk-x98qs959.js +302 -0
  20. package/dist/hooks/chunk-y0jdr3et.js +627 -0
  21. package/dist/hooks/claim-sources.js +72 -0
  22. package/dist/hooks/dod-gate.js +198 -0
  23. package/dist/hooks/no-reask.js +67 -0
  24. package/dist/hooks/session-start.js +1348 -0
  25. package/dist/hooks/statusline.js +116 -0
  26. package/dist/tldrx.js +34219 -0
  27. package/env.yml +79 -0
  28. package/package.json +60 -0
  29. package/plugin/.claude-plugin/plugin.json +9 -0
  30. package/plugin/README.md +113 -0
  31. package/plugin/agents/README.md +22 -0
  32. package/plugin/hooks/hooks.json +107 -0
  33. package/plugin/skills/tldrx/SKILL.md +170 -0
  34. package/stages/build/stage.md +70 -0
  35. package/stages/build/stage.yml +54 -0
  36. package/stages/how/stage.md +89 -0
  37. package/stages/how/stage.yml +53 -0
  38. package/stages/plan/stage.md +89 -0
  39. package/stages/plan/stage.yml +48 -0
  40. package/stages/watch/stage.md +85 -0
  41. package/stages/watch/stage.yml +61 -0
  42. package/stages/what/stage.md +89 -0
  43. package/stages/what/stage.yml +61 -0
  44. package/templates/budget.yml +17 -0
  45. package/templates/competencies.yml +14 -0
  46. package/templates/env.yml +19 -0
  47. package/templates/epic.md +38 -0
  48. package/templates/expert.md +51 -0
  49. package/templates/experts/architect.md +77 -0
  50. package/templates/experts/delivery.md +76 -0
  51. package/templates/experts/developer.md +78 -0
  52. package/templates/experts/operations.md +74 -0
  53. package/templates/experts/product.md +78 -0
  54. package/templates/facts.yml +18 -0
  55. package/templates/handoff.md +63 -0
  56. package/templates/process.yml +33 -0
  57. package/templates/questions.md +63 -0
  58. package/templates/run.yml +24 -0
  59. package/templates/story.md +55 -0
  60. package/templates/watcher.md +68 -0
  61. package/templates/waves.yml +17 -0
  62. package/templates/workspace.yml +30 -0
  63. package/workflows/bugfix.yml +26 -0
  64. package/workflows/docs.yml +23 -0
  65. package/workflows/feature.yml +22 -0
  66. package/workflows/hotfix.yml +24 -0
  67. package/workflows/integration.yml +24 -0
  68. package/workflows/migration.yml +24 -0
  69. package/workflows/performance.yml +23 -0
  70. package/workflows/prototype.yml +24 -0
  71. package/workflows/refactor.yml +24 -0
  72. package/workflows/retro.yml +20 -0
  73. package/workflows/security-patch.yml +23 -0
  74. package/workflows/spike.yml +23 -0
  75. package/workflows/upgrade.yml +23 -0
@@ -0,0 +1,1348 @@
1
+ #!/usr/bin/env node
2
+ import"./chunk-kw4tffzf.js";
3
+ import"./chunk-x98qs959.js";
4
+ import {
5
+ allow,
6
+ readPayload,
7
+ runHook,
8
+ sessionContext
9
+ } from "./chunk-1zwcxd3f.js";
10
+ import"./chunk-a8p2rc94.js";
11
+ import {
12
+ RunStore,
13
+ isMovable,
14
+ openQuestions,
15
+ runSnapshot,
16
+ whatIsWaiting
17
+ } from "./chunk-y0jdr3et.js";
18
+ import {
19
+ openRunViews
20
+ } from "./chunk-sdjnnmzz.js";
21
+ import"./chunk-azctppjh.js";
22
+ import"./chunk-ae6bkfs5.js";
23
+ import {
24
+ openBlocks,
25
+ parseQuestions
26
+ } from "./chunk-j234zf0t.js";
27
+ import"./chunk-t56k6146.js";
28
+ import {
29
+ classifySrc
30
+ } from "./chunk-g395gk7e.js";
31
+ import {
32
+ isRecord
33
+ } from "./chunk-0bt6yb2q.js";
34
+ import {
35
+ PROJECT_FRAMEWORK_DIR,
36
+ PROJECT_WORK_DIR,
37
+ STAGES_DIR,
38
+ TEMPLATES_DIR,
39
+ findWorkspaceRoot,
40
+ listRunDirs,
41
+ parseYaml
42
+ } from "./chunk-p274ckxv.js";
43
+
44
+ // src/core/status/PendingItem.ts
45
+ function isPending(item) {
46
+ return item.kind !== "none";
47
+ }
48
+ // src/core/status/initQuestionsItem.ts
49
+ import { existsSync, readFileSync } from "node:fs";
50
+ import { join } from "node:path";
51
+
52
+ // src/core/init/questions.ts
53
+ var QUESTIONS_FILE = ".tldrx/init-questions.md";
54
+
55
+ // src/core/status/initQuestionsItem.ts
56
+ var MAX_LISTED = 5;
57
+ function initQuestionsItem(root) {
58
+ const path = join(root, ...QUESTIONS_FILE.split("/"));
59
+ if (!existsSync(path))
60
+ return null;
61
+ let open;
62
+ try {
63
+ open = openBlocks(parseQuestions(readFileSync(path, "utf8")).blocks);
64
+ } catch {
65
+ return null;
66
+ }
67
+ if (open.length === 0)
68
+ return null;
69
+ const details = open.slice(0, MAX_LISTED).map((block) => `${block.id} · ${block.title}`);
70
+ if (open.length > MAX_LISTED) {
71
+ details.push(`… and ${String(open.length - MAX_LISTED)} more in ${QUESTIONS_FILE}`);
72
+ }
73
+ details.push("these are facts about your project the tool could not detect; every run reuses the answers");
74
+ return {
75
+ kind: "init-questions",
76
+ summary: `${String(open.length)} setup question${open.length === 1 ? "" : "s"} ` + "about your project have never been answered",
77
+ command: "tldrx interview --init",
78
+ details
79
+ };
80
+ }
81
+
82
+ // src/core/status/seedSplitItems.ts
83
+ import { closeSync, existsSync as existsSync2, openSync, readSync, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
84
+ import { basename, join as join2, relative } from "node:path";
85
+
86
+ // src/core/seed/triageInventory.ts
87
+ var STATUS_RE = /^\s*(?:[-*+]\s+)?(?:\*\*|__)?\s*status\s*(?:\*\*|__)?\s*:\s*(?:\*\*|__)?\s*([A-Za-z][A-Za-z-]*)/i;
88
+ function statusLineOf(text) {
89
+ for (const line of text.split(`
90
+ `)) {
91
+ const match = STATUS_RE.exec(line);
92
+ if (match !== null && match[1] !== undefined) {
93
+ return { status: match[1].toLowerCase(), line: line.trim() };
94
+ }
95
+ }
96
+ return null;
97
+ }
98
+ var DOCUMENT_EXTENSIONS = new Set([".md", ".txt", ".markdown", ".rst", ".adoc", ".pdf", ".doc", ".docx"]);
99
+
100
+ // src/core/detect/walk.ts
101
+ var SKIPPED_DIRS = new Set([
102
+ ".git",
103
+ "node_modules",
104
+ "dist",
105
+ "build",
106
+ "out",
107
+ "bin",
108
+ "obj",
109
+ "target",
110
+ ".venv",
111
+ "venv",
112
+ "__pycache__",
113
+ ".next",
114
+ ".expo",
115
+ ".gradle",
116
+ "vendor",
117
+ "coverage",
118
+ "graphify-out",
119
+ ".tldrx",
120
+ "tldrx-work",
121
+ ".idea",
122
+ ".vscode",
123
+ "Pods",
124
+ "DerivedData",
125
+ ".terraform",
126
+ ".turbo",
127
+ ".cache",
128
+ "storybook-static",
129
+ "playwright-report",
130
+ "test-results"
131
+ ]);
132
+
133
+ // src/core/seed/collectSeed.ts
134
+ var MAX_SEED_BYTES = 2 * 1024 * 1024;
135
+
136
+ // src/core/seed/splitFile.ts
137
+ var SPLIT_YML = "split.yml";
138
+ var SPLIT_MD = "split.md";
139
+ var SPLIT_SIZES = ["S", "M", "L"];
140
+ var SPLIT_STATUSES = ["proposed", "applying", "applied"];
141
+ function isAnswered2(question) {
142
+ return typeof question.answer === "string" && question.answer.trim() !== "";
143
+ }
144
+ class SplitError extends Error {
145
+ }
146
+ function readSplitFile(doc) {
147
+ if (!isRecord(doc))
148
+ throw new SplitError("split.yml: expected a mapping at the document root");
149
+ const status = doc.status;
150
+ if (typeof status !== "string" || !SPLIT_STATUSES.includes(status)) {
151
+ throw new SplitError(`split.yml: status must be one of ${SPLIT_STATUSES.join(" | ")}, got ${String(status)}`);
152
+ }
153
+ if (typeof doc.source !== "string")
154
+ throw new SplitError("split.yml: `source` must be a string");
155
+ return doc;
156
+ }
157
+ var SPLIT_SCHEMA = {
158
+ type: "object",
159
+ additionalProperties: false,
160
+ required: ["shared_context", "exclude", "runs", "questions"],
161
+ properties: {
162
+ shared_context: { type: "array", items: { type: "string" } },
163
+ exclude: {
164
+ type: "array",
165
+ items: {
166
+ type: "object",
167
+ additionalProperties: false,
168
+ required: ["path", "reason"],
169
+ properties: { path: { type: "string" }, reason: { type: "string" } }
170
+ }
171
+ },
172
+ runs: {
173
+ type: "array",
174
+ items: {
175
+ type: "object",
176
+ additionalProperties: false,
177
+ required: ["slug", "scope", "goal", "seeds", "depends_on", "size", "budget_usd", "why"],
178
+ properties: {
179
+ slug: { type: "string" },
180
+ scope: { type: "string" },
181
+ goal: { type: "string" },
182
+ seeds: { type: "array", items: { type: "string" } },
183
+ depends_on: { type: "array", items: { type: "string" } },
184
+ size: { type: "string", enum: [...SPLIT_SIZES] },
185
+ budget_usd: { type: "number" },
186
+ why: {
187
+ type: "array",
188
+ items: {
189
+ type: "object",
190
+ additionalProperties: false,
191
+ required: ["claim", "src"],
192
+ properties: { claim: { type: "string" }, src: { type: "string" } }
193
+ }
194
+ }
195
+ }
196
+ }
197
+ },
198
+ questions: {
199
+ type: "array",
200
+ items: {
201
+ type: "object",
202
+ additionalProperties: false,
203
+ required: ["id", "text"],
204
+ properties: {
205
+ id: { type: "string" },
206
+ text: { type: "string" },
207
+ options: { type: "array", items: { type: "string" } }
208
+ }
209
+ }
210
+ }
211
+ }
212
+ };
213
+
214
+ // src/core/status/seedSplitItems.ts
215
+ var TRIAGE_DIRNAME = "triage";
216
+ var MAX_LISTED2 = 5;
217
+ var STATUS_HEAD_BYTES = 4096;
218
+ function proposedSplits(root) {
219
+ const triage = join2(root, PROJECT_FRAMEWORK_DIR, TRIAGE_DIRNAME);
220
+ let entries;
221
+ try {
222
+ entries = readdirSync(triage).sort();
223
+ } catch {
224
+ return [];
225
+ }
226
+ const found = [];
227
+ for (const entry of entries) {
228
+ const dir = join2(triage, entry);
229
+ const path = join2(dir, SPLIT_YML);
230
+ try {
231
+ if (!statSync(dir).isDirectory() || !existsSync2(path))
232
+ continue;
233
+ const file = readSplitFile(parseYaml(readFileSync2(path, "utf8")));
234
+ if (file.status !== "proposed" && file.status !== "applying")
235
+ continue;
236
+ found.push({ path, rel: toRel(root, path), dir, file });
237
+ } catch {
238
+ continue;
239
+ }
240
+ }
241
+ return found;
242
+ }
243
+ function seedSplitItems(root) {
244
+ return proposedSplits(root).map((split) => splitItem(root, split));
245
+ }
246
+ function splitItem(root, split) {
247
+ const file = split.file;
248
+ const open = file.questions.filter((question) => !isAnswered2(question));
249
+ const rels = seedDocuments(split.dir, file);
250
+ const proposed = proposedDocs(root, split.dir, rels);
251
+ const decisions = rels.filter((rel) => /^DECISIONS.*\.md$/i.test(basename(rel)));
252
+ const details = [
253
+ `${String(file.runs.length)} run(s) would be created: ${file.runs.map((run) => run.slug).join(", ")}`
254
+ ];
255
+ if (open.length === 0) {
256
+ details.push("no unanswered questions on the split itself");
257
+ } else {
258
+ details.push(`${String(open.length)} question(s) the proposal could not answer:`);
259
+ for (const question of open.slice(0, MAX_LISTED2)) {
260
+ details.push(` ${question.id} ${question.text}`);
261
+ }
262
+ if (open.length > MAX_LISTED2)
263
+ details.push(` … and ${String(open.length - MAX_LISTED2)} more`);
264
+ details.push(` answer one with \`tldrx seed answer ${split.rel} <Qid> "<text>"\``);
265
+ }
266
+ if (proposed.length > 0) {
267
+ details.push(`${String(proposed.length)} seed document(s) are still \`proposed\` — decisions nobody has made:`);
268
+ for (const doc of proposed.slice(0, MAX_LISTED2)) {
269
+ details.push(` ${doc.rel} — ${doc.line.replace(/^[-*+]\s+/, "")}`);
270
+ }
271
+ if (proposed.length > MAX_LISTED2)
272
+ details.push(` … and ${String(proposed.length - MAX_LISTED2)} more`);
273
+ }
274
+ for (const rel of decisions)
275
+ details.push(`decisions are collected in ${rel}`);
276
+ details.push(`read ${toRel(root, join2(split.dir, SPLIT_MD))}, edit ${split.rel} if you disagree, then apply it`);
277
+ if (file.status === "applying") {
278
+ const done = file.created_runs ?? [];
279
+ return {
280
+ kind: "seed-split",
281
+ summary: `\`tldrx seed apply ${split.rel}\` stopped at run ${String(done.length + 1)} of ` + `${String(file.runs.length)} — ${done.length === 0 ? "nothing" : `${String(done.length)} run(s)`} ` + "created, the rest were not",
282
+ command: `tldrx run status`,
283
+ details: [
284
+ done.length === 0 ? "no run was created before it stopped" : `created and left in place: ${done.join(", ")}`,
285
+ `still to create: ${file.runs.map((run) => run.slug).filter((slug) => !done.some((id) => id === slug || id.endsWith(`-${slug}`))).join(", ")}`,
286
+ `remove those run dirs (or \`tldrx run cancel\` them), set ${split.rel} back to` + " `status: proposed`, and apply again"
287
+ ]
288
+ };
289
+ }
290
+ return {
291
+ kind: "seed-split",
292
+ summary: `a proposed split of \`${file.source}\` into ${String(file.runs.length)} ` + "piece(s) of work is waiting for your decision — nothing has been created yet",
293
+ command: `tldrx seed apply ${split.rel} --dry-run`,
294
+ details
295
+ };
296
+ }
297
+ function seedDocuments(dir, file) {
298
+ const inventory = join2(dir, "inventory.json");
299
+ if (existsSync2(inventory)) {
300
+ try {
301
+ const doc = JSON.parse(readFileSync2(inventory, "utf8"));
302
+ const rels2 = (doc.documents ?? []).map((row) => row.rel).filter((rel) => typeof rel === "string");
303
+ if (rels2.length > 0)
304
+ return rels2;
305
+ } catch {}
306
+ }
307
+ const rels = new Set(file.shared_context);
308
+ for (const entry of file.exclude)
309
+ rels.add(entry.path);
310
+ for (const run of file.runs)
311
+ for (const rel of run.seeds)
312
+ rels.add(rel);
313
+ return [...rels].sort();
314
+ }
315
+ function proposedDocs(root, dir, rels) {
316
+ const cached = cachedStatuses(dir);
317
+ const found = [];
318
+ for (const rel of rels) {
319
+ const head = readHead(join2(root, rel));
320
+ if (head === null) {
321
+ if (cached.get(rel) === "proposed")
322
+ found.push({ rel, line: "Status: proposed (from inventory.json)" });
323
+ continue;
324
+ }
325
+ const status = statusLineOf(head);
326
+ if (status !== null && status.status === "proposed")
327
+ found.push({ rel, line: status.line });
328
+ }
329
+ return found;
330
+ }
331
+ function cachedStatuses(dir) {
332
+ const out = new Map;
333
+ const inventory = join2(dir, "inventory.json");
334
+ if (!existsSync2(inventory))
335
+ return out;
336
+ try {
337
+ const doc = JSON.parse(readFileSync2(inventory, "utf8"));
338
+ for (const row of doc.documents ?? []) {
339
+ if (typeof row.rel === "string" && typeof row.adrStatus === "string")
340
+ out.set(row.rel, row.adrStatus);
341
+ }
342
+ } catch {
343
+ return out;
344
+ }
345
+ return out;
346
+ }
347
+ function readHead(path) {
348
+ let fd = null;
349
+ try {
350
+ fd = openSync(path, "r");
351
+ const buffer = Buffer.alloc(STATUS_HEAD_BYTES);
352
+ const read = readSync(fd, buffer, 0, STATUS_HEAD_BYTES, 0);
353
+ return buffer.subarray(0, read).toString("utf8");
354
+ } catch {
355
+ return null;
356
+ } finally {
357
+ if (fd !== null)
358
+ closeSync(fd);
359
+ }
360
+ }
361
+ function toRel(root, path) {
362
+ const rel = relative(root, path);
363
+ return rel === "" || rel.startsWith("..") ? path : rel.split("\\").join("/");
364
+ }
365
+
366
+ // src/core/status/runItems.ts
367
+ import { basename as basename2 } from "node:path";
368
+
369
+ // src/core/run/dependencies.ts
370
+ var MAX_CHAINS = 24;
371
+ function slugOfRun(runId) {
372
+ return /^\d{6}-(.+)$/.exec(runId)?.[1] ?? runId;
373
+ }
374
+ function resolveDependencies(inputs) {
375
+ const idBySlug = new Map;
376
+ const statusById = new Map;
377
+ for (const input of inputs) {
378
+ statusById.set(input.id, input.status);
379
+ const slug = slugOfRun(input.id);
380
+ if (!idBySlug.has(slug))
381
+ idBySlug.set(slug, input.id);
382
+ }
383
+ const runs = inputs.map((input) => {
384
+ const dependsOn = input.dependsOn.map((slug) => idBySlug.get(slug) ?? slug);
385
+ const blockedBy = dependsOn.filter((id) => statusById.get(id) !== "done");
386
+ return { id: input.id, dependsOn, blockedBy, runnable: blockedBy.length === 0 && input.movable };
387
+ });
388
+ return { runs, order: orderOf(inputs, runs), chains: chainsOf(inputs, runs) };
389
+ }
390
+ function orderOf(inputs, runs) {
391
+ const known = new Set(inputs.map((input) => input.id));
392
+ const byId = new Map(runs.map((run) => [run.id, run]));
393
+ const inputById = new Map(inputs.map((input) => [input.id, input]));
394
+ const waitingOn = new Map;
395
+ for (const run of runs) {
396
+ waitingOn.set(run.id, new Set(run.dependsOn.filter((id) => known.has(id) && id !== run.id)));
397
+ }
398
+ const rank = (a, b) => {
399
+ const left2 = byId.get(a);
400
+ const right = byId.get(b);
401
+ const leftRunnable = left2 !== undefined && left2.runnable;
402
+ const rightRunnable = right !== undefined && right.runnable;
403
+ if (leftRunnable !== rightRunnable)
404
+ return leftRunnable ? -1 : 1;
405
+ const leftAt = inputById.get(a)?.updatedAt ?? "";
406
+ const rightAt = inputById.get(b)?.updatedAt ?? "";
407
+ if (leftAt !== rightAt)
408
+ return rightAt.localeCompare(leftAt);
409
+ return a.localeCompare(b);
410
+ };
411
+ const placed = [];
412
+ const left = new Set(known);
413
+ while (left.size > 0) {
414
+ const ready = [...left].filter((id) => (waitingOn.get(id)?.size ?? 0) === 0).sort(rank);
415
+ const next = ready[0];
416
+ if (next === undefined)
417
+ break;
418
+ placed.push(next);
419
+ left.delete(next);
420
+ for (const id of left)
421
+ waitingOn.get(id)?.delete(next);
422
+ }
423
+ return [...placed, ...[...left].sort(rank)];
424
+ }
425
+ function chainsOf(inputs, runs) {
426
+ const known = new Set(inputs.map((input) => input.id));
427
+ const parents = new Map;
428
+ const children = new Map;
429
+ for (const id of known)
430
+ children.set(id, []);
431
+ for (const run of runs) {
432
+ const edges = run.dependsOn.filter((id) => known.has(id) && id !== run.id);
433
+ parents.set(run.id, edges);
434
+ for (const parent of edges)
435
+ children.get(parent)?.push(run.id);
436
+ }
437
+ const roots = [...known].filter((id) => (parents.get(id) ?? []).length === 0).sort();
438
+ const chains = [];
439
+ const walk = (path) => {
440
+ if (chains.length >= MAX_CHAINS)
441
+ return;
442
+ const head = path[path.length - 1] ?? "";
443
+ const next = (children.get(head) ?? []).filter((id) => !path.includes(id)).sort();
444
+ if (next.length === 0) {
445
+ if (path.length > 1)
446
+ chains.push([...path]);
447
+ return;
448
+ }
449
+ for (const child of next)
450
+ walk([...path, child]);
451
+ };
452
+ for (const root of roots)
453
+ walk([root]);
454
+ return chains;
455
+ }
456
+
457
+ // src/core/status/runItems.ts
458
+ var NEXT_MARK = "← next";
459
+ function dependencyInputs(root) {
460
+ const out = [];
461
+ for (const dir of listRunDirs(root)) {
462
+ let store;
463
+ try {
464
+ store = RunStore.open(dir);
465
+ } catch {
466
+ continue;
467
+ }
468
+ const run = store.run;
469
+ out.push({
470
+ id: run.run === "" ? basename2(dir) : run.run,
471
+ status: run.status,
472
+ dependsOn: run.triage?.depends_on ?? [],
473
+ movable: isMovable(whatIsWaiting(run, store.runDir).kind),
474
+ updatedAt: run.updated_at
475
+ });
476
+ }
477
+ return out;
478
+ }
479
+ function runItems(root) {
480
+ const open = RunStore.findOpen(root);
481
+ const unreadable = unreadableItem(root);
482
+ if (open.length === 0)
483
+ return unreadable === null ? [] : [unreadable];
484
+ const inputs = dependencyInputs(root);
485
+ const known = new Set(inputs.map((input) => input.id));
486
+ const statuses = new Map(inputs.map((input) => [input.id, input.status]));
487
+ const resolved = new Map(resolveDependencies(inputs).runs.map((run) => [run.id, run]));
488
+ const several = open.length > 1;
489
+ let markedNext = false;
490
+ const items = open.map((store) => {
491
+ const run = store.run;
492
+ const waiting = whatIsWaiting(run, store.runDir);
493
+ const blockedIds = resolved.get(run.run)?.blockedBy ?? [];
494
+ const blocked = blockedIds.map(slugOfRun);
495
+ const runnable = resolved.get(run.run)?.runnable ?? isMovable(waiting.kind);
496
+ const isNext = runnable && !markedNext;
497
+ if (isNext)
498
+ markedNext = true;
499
+ const details = [];
500
+ if (isNext) {
501
+ details.push(`${NEXT_MARK} — the first run you can move; \`tldrx run auto ${run.run}\` runs it headless ` + "until something genuinely needs a person");
502
+ }
503
+ details.push(`at ${run.cursor.phase} / ${run.cursor.stage} · run status ${run.status} · waiting: ${waiting.kind}`);
504
+ for (const id of blockedIds) {
505
+ const status = known.has(id) ? statuses.get(id) : undefined;
506
+ details.push(`blocked by ${slugOfRun(id)} — ${status === undefined ? "no run exists for it" : `it is ${status}`}`);
507
+ }
508
+ if (waiting.kind === "gate") {
509
+ details.push(`disagree instead: \`tldrx reject --run ${run.run} --note "<why>"\``);
510
+ }
511
+ if (waiting.kind === "answer" && waiting.questions.length > 1) {
512
+ details.push(`open questions: ${waiting.questions.join(", ")}`);
513
+ }
514
+ if (waiting.kind === "failed")
515
+ details.push(waiting.message);
516
+ details.push(...machineSignedDetails(run));
517
+ if (several)
518
+ details.push("several runs are open, so every command here names its run id");
519
+ return {
520
+ kind: "run",
521
+ summary: summaryOf(run, waiting, blocked),
522
+ command: blocked.length > 0 ? "" : commandFor(run, waiting),
523
+ details
524
+ };
525
+ });
526
+ return unreadable === null ? items : [...items, unreadable];
527
+ }
528
+ function unreadableItem(root) {
529
+ const broken = [];
530
+ for (const dir of listRunDirs(root)) {
531
+ try {
532
+ RunStore.open(dir);
533
+ } catch (error) {
534
+ broken.push(`${basename2(dir)} — ${error instanceof Error ? error.message : String(error)}`);
535
+ }
536
+ }
537
+ if (broken.length === 0)
538
+ return null;
539
+ const first = broken[0]?.split(" — ")[0] ?? "";
540
+ return {
541
+ kind: "run",
542
+ summary: `${String(broken.length)} run folder(s) in ${PROJECT_WORK_DIR}/ could not be read, ` + "so nothing here can say what they are waiting on",
543
+ command: `tldrx run status ${first}`,
544
+ details: broken
545
+ };
546
+ }
547
+ var AUTO = "auto";
548
+ function machineSignedDetails(run) {
549
+ const stages = run.phases.flatMap((phase) => phase.stages.map((stage) => ({ phase, stage })));
550
+ const auto = stages.filter((e) => e.stage.gate.status === "approved" && e.stage.gate.by === AUTO);
551
+ const stale = stages.filter((e) => e.stage.stale === true);
552
+ const details = [];
553
+ if (auto.length > 0) {
554
+ const named = auto.map((e) => `${e.phase.id}/${e.stage.id}`).join(", ");
555
+ const first = auto[0];
556
+ details.push(`${auto.length} gate(s) signed \`by: auto\`, not by a person — ${named}. ` + `Take one back with \`tldrx reject --run ${run.run} ` + `--stage ${first?.phase.id ?? ""}/${first?.stage.id ?? ""} --note "<why>"\``);
557
+ }
558
+ if (stale.length > 0) {
559
+ details.push(`${stale.length} stage(s) marked stale by a revoked approval — ` + `${stale.map((e) => `${e.phase.id}/${e.stage.id}`).join(", ")}. Their files are still on disk ` + "and were derived from a decision that has since been withdrawn.");
560
+ }
561
+ return details;
562
+ }
563
+ function summaryOf(run, waiting, blocked) {
564
+ const what = run.title === "" ? run.run : `${run.run} ("${run.title}")`;
565
+ if (blocked.length > 0) {
566
+ return `run ${what} cannot start yet — it was proposed to follow ${blocked.join(", ")}`;
567
+ }
568
+ switch (waiting.kind) {
569
+ case "gate":
570
+ return `run ${what} finished a stage and is waiting for you to approve it`;
571
+ case "answer":
572
+ return `run ${what} asked you ${String(waiting.questions.length)} question(s) and stopped for the answer`;
573
+ case "failed":
574
+ return `run ${what} failed at ${run.cursor.phase}/${run.cursor.stage} and is waiting to be retried or dropped`;
575
+ case "ready":
576
+ return `run ${what} is ready to run its next stage`;
577
+ case "done":
578
+ return `run ${what} has no stage left to run but is still open`;
579
+ default:
580
+ return `run ${what} is blocked at ${run.cursor.phase}/${run.cursor.stage}`;
581
+ }
582
+ }
583
+ function commandFor(run, waiting) {
584
+ switch (waiting.kind) {
585
+ case "gate":
586
+ return `tldrx approve --run ${run.run}`;
587
+ case "answer":
588
+ return `tldrx answer ${waiting.questions[0] ?? "Q1"} "<your answer>" --run ${run.run}`;
589
+ case "ready":
590
+ case "failed":
591
+ return `tldrx next ${run.run}`;
592
+ default:
593
+ return "";
594
+ }
595
+ }
596
+ // src/core/experts/loadExperts.ts
597
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
598
+ import { join as join3 } from "node:path";
599
+
600
+ // src/core/init/competencyLevel.ts
601
+ var EVIDENCE_KINDS = ["code", "run", "test", "doc", "answer"];
602
+ var ALLOWED_KINDS = EVIDENCE_KINDS.join(", ");
603
+ function isEvidenceKind(value) {
604
+ return EVIDENCE_KINDS.includes(value);
605
+ }
606
+ var EVIDENCE_CONFIDENCES = ["measured", "inferred", "assumed"];
607
+ function isEvidenceConfidence(value) {
608
+ return EVIDENCE_CONFIDENCES.includes(value);
609
+ }
610
+ var WEIGHTS = { code: 1, run: 1, test: 1, doc: 0.5, answer: 0.8 };
611
+ var CROSS_MULTIPLIER = 2;
612
+ var ASSUMED_MULTIPLIER = 0.5;
613
+ var THRESHOLDS = [0.5, 1.5, 3, 6, 20];
614
+ var RECENCY_FLOOR = 0.25;
615
+ var RECENCY_HALFLIFE_DAYS = 365;
616
+ var NO_RUN_CAP = 3;
617
+ var MIN_KINDS_FOR_TOP = 2;
618
+ var DAY_MS = 86400000;
619
+ function competencyLevel(evidence, now = new Date) {
620
+ if (evidence.length === 0)
621
+ return 0;
622
+ let weighted = 0;
623
+ const sources = new Set;
624
+ const kinds = new Set;
625
+ for (const item of evidence) {
626
+ const ageDays = daysBetween(item.at, now);
627
+ if (ageDays === null)
628
+ continue;
629
+ weighted += recency(ageDays) * weightOf(item);
630
+ sources.add(item.src);
631
+ kinds.add(item.kind);
632
+ }
633
+ let level = THRESHOLDS.filter((threshold) => weighted >= threshold).length;
634
+ if (!kinds.has("run"))
635
+ level = Math.min(level, NO_RUN_CAP);
636
+ if (level === 5 && kinds.size < MIN_KINDS_FOR_TOP)
637
+ level = 4;
638
+ return Math.min(level, sources.size);
639
+ }
640
+ function weightOf(item) {
641
+ const base = WEIGHTS[item.kind];
642
+ const cross = item.cross === true ? CROSS_MULTIPLIER : 1;
643
+ const confidence = item.confidence === "assumed" ? ASSUMED_MULTIPLIER : 1;
644
+ return base * cross * confidence;
645
+ }
646
+ function recency(ageDays) {
647
+ return Math.max(RECENCY_FLOOR, 1 - ageDays / RECENCY_HALFLIFE_DAYS);
648
+ }
649
+ function daysBetween(at, now) {
650
+ const then = Date.parse(at);
651
+ if (Number.isNaN(then))
652
+ return null;
653
+ return Math.max(0, (now.getTime() - then) / DAY_MS);
654
+ }
655
+
656
+ // src/core/experts/evidenceSrc.ts
657
+ function checkEvidenceSrc(kind, src) {
658
+ const parsed = classifySrc(src);
659
+ if ("message" in parsed)
660
+ return { reason: "malformed-src" };
661
+ return matchesKind(kind, parsed) ? null : { reason: "kind-mismatch" };
662
+ }
663
+ function matchesKind(kind, ref) {
664
+ switch (kind) {
665
+ case "code":
666
+ return ref.kind === "file";
667
+ case "run":
668
+ return ref.kind === "cmd" || isRunArtifact(ref);
669
+ case "test":
670
+ return ref.kind === "file" || ref.kind === "cmd";
671
+ case "doc":
672
+ return ref.kind === "doc";
673
+ case "answer":
674
+ return ref.kind === "fact";
675
+ }
676
+ }
677
+ function isRunArtifact(ref) {
678
+ return ref.kind === "file" && ref.repo === null && ref.path.startsWith("tldrx-work/");
679
+ }
680
+
681
+ // src/core/experts/readEvidenceRows.ts
682
+ function readEvidenceRows(input) {
683
+ if (!Array.isArray(input))
684
+ return { evidence: [], ignored: [] };
685
+ const evidence = [];
686
+ const counts = new Map;
687
+ const refuse = (reason, kind, src) => {
688
+ const key = `${reason} ${reason === "unknown-kind" ? kind : `${kind} ${src}`}`;
689
+ const seen = counts.get(key);
690
+ counts.set(key, seen === undefined ? { reason, kind, src, count: 1 } : { ...seen, count: seen.count + 1 });
691
+ };
692
+ for (const raw of input) {
693
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
694
+ continue;
695
+ const row = raw;
696
+ const kind = str(row.kind);
697
+ const src = str(row.src);
698
+ const at = str(row.at);
699
+ if (src === "" || at === "")
700
+ continue;
701
+ if (!isEvidenceKind(kind)) {
702
+ refuse("unknown-kind", kind, "");
703
+ continue;
704
+ }
705
+ const problem = checkEvidenceSrc(kind, src);
706
+ if (problem !== null) {
707
+ refuse(problem.reason, kind, src);
708
+ continue;
709
+ }
710
+ const confidence = str(row.confidence);
711
+ evidence.push({
712
+ kind,
713
+ src,
714
+ at,
715
+ ...row.cross === true ? { cross: true } : {},
716
+ ...isEvidenceConfidence(confidence) ? { confidence } : {}
717
+ });
718
+ }
719
+ return { evidence, ignored: [...counts.values()] };
720
+ }
721
+ function str(value) {
722
+ return typeof value === "string" ? value : "";
723
+ }
724
+
725
+ // src/core/experts/loadExperts.ts
726
+ var EXPERTS_DIRNAME = "experts";
727
+ var COMPETENCIES_FILE = "competencies.yml";
728
+ var EXPERT_FILE = "expert.md";
729
+ function expertsDir(root) {
730
+ return join3(root, PROJECT_FRAMEWORK_DIR, EXPERTS_DIRNAME);
731
+ }
732
+ function expertDir(root, name) {
733
+ return join3(expertsDir(root), name);
734
+ }
735
+ function loadExperts(root, now = new Date) {
736
+ const dir = expertsDir(root);
737
+ if (!existsSync3(dir))
738
+ return [];
739
+ const names = readdirSync2(dir).filter((entry) => statSync2(join3(dir, entry)).isDirectory()).sort();
740
+ return names.map((name) => loadExpert(root, name, now));
741
+ }
742
+ function loadExpert(root, name, now = new Date) {
743
+ const dir = expertDir(root, name);
744
+ const path = join3(dir, COMPETENCIES_FILE);
745
+ const empty = { name, dir, status: "unknown", lastTrained: null, areas: [], drifted: [] };
746
+ if (!existsSync3(path))
747
+ return { ...empty, error: `${COMPETENCIES_FILE} is missing` };
748
+ let doc;
749
+ try {
750
+ const parsed = parseYaml(readFileSync3(path, "utf8"));
751
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
752
+ return { ...empty, error: `${COMPETENCIES_FILE} is not a mapping` };
753
+ }
754
+ doc = parsed;
755
+ } catch (error) {
756
+ return { ...empty, error: error instanceof Error ? error.message : String(error) };
757
+ }
758
+ const areas = Array.isArray(doc.areas) ? doc.areas.map((area) => toArea(name, area, now)).filter((area) => area !== null) : [];
759
+ return {
760
+ name: typeof doc.expert === "string" && doc.expert !== "" ? doc.expert : name,
761
+ dir,
762
+ status: typeof doc.status === "string" ? doc.status : "unknown",
763
+ lastTrained: typeof doc.last_trained === "string" ? doc.last_trained : null,
764
+ areas,
765
+ drifted: areas.filter((area) => area.storedLevel !== null && area.storedLevel !== area.level),
766
+ error: null
767
+ };
768
+ }
769
+ function toArea(expert, input, now) {
770
+ if (typeof input !== "object" || input === null || Array.isArray(input))
771
+ return null;
772
+ const raw = input;
773
+ const id = str2(raw.id) !== "" ? str2(raw.id) : str2(raw.area);
774
+ if (id === "")
775
+ return null;
776
+ const { evidence, ignored } = readEvidenceRows(raw.evidence);
777
+ return {
778
+ id,
779
+ title: str2(raw.title) !== "" ? str2(raw.title) : id,
780
+ storedLevel: typeof raw.level === "number" ? raw.level : null,
781
+ level: competencyLevel(evidence, now),
782
+ trainPrompt: str2(raw.train_prompt) !== "" ? str2(raw.train_prompt) : `tldrx expert train ${expert} --area ${id} --mode light`,
783
+ evidence,
784
+ ignored,
785
+ newestEvidence: newestDate(evidence)
786
+ };
787
+ }
788
+ function newestDate(evidence) {
789
+ let newest = null;
790
+ for (const item of evidence) {
791
+ if (newest === null || item.at > newest)
792
+ newest = item.at;
793
+ }
794
+ return newest;
795
+ }
796
+ function str2(value) {
797
+ return typeof value === "string" ? value : "";
798
+ }
799
+ // src/core/experts/stageCoverage.ts
800
+ import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync4 } from "node:fs";
801
+ import { join as join6 } from "node:path";
802
+
803
+ // src/core/experts/stackExperts.ts
804
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
805
+ import { join as join4 } from "node:path";
806
+ function stackExpertNames(root, repos) {
807
+ const path = join4(root, PROJECT_FRAMEWORK_DIR, "workspace.yml");
808
+ if (!existsSync4(path))
809
+ return [];
810
+ let doc;
811
+ try {
812
+ doc = parseYaml(readFileSync4(path, "utf8"));
813
+ } catch {
814
+ return [];
815
+ }
816
+ const list = doc?.repos;
817
+ if (!Array.isArray(list))
818
+ return [];
819
+ const names = [];
820
+ for (const row of list) {
821
+ if (typeof row?.name !== "string" || !repos.includes(row.name))
822
+ continue;
823
+ const stack = Array.isArray(row.stack) ? row.stack : [];
824
+ for (const language of stack) {
825
+ if (typeof language !== "string" || language === "")
826
+ continue;
827
+ const expert = `${language}-stack`;
828
+ if (!names.includes(expert))
829
+ names.push(expert);
830
+ }
831
+ }
832
+ return names;
833
+ }
834
+
835
+ // src/core/experts/selectExperts.ts
836
+ import { existsSync as existsSync6, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
837
+
838
+ // src/core/experts/expertDocument.ts
839
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
840
+ import { join as join5 } from "node:path";
841
+ var H2_RE = /^##\s+(.+?)\s*$/;
842
+ function readExpertDocument(root, name) {
843
+ const path = join5(expertDir(root, name), EXPERT_FILE);
844
+ if (!existsSync5(path)) {
845
+ return { path, exists: false, frontMatter: new Map, body: "" };
846
+ }
847
+ const text = readFileSync5(path, "utf8");
848
+ const { frontMatter, body } = splitFrontMatter(text);
849
+ return { path, exists: true, frontMatter, body };
850
+ }
851
+ function splitFrontMatter(text) {
852
+ const frontMatter = new Map;
853
+ if (!text.startsWith(`---
854
+ `))
855
+ return { frontMatter, body: text };
856
+ const end = text.indexOf(`
857
+ ---`, 3);
858
+ if (end === -1)
859
+ return { frontMatter, body: text };
860
+ for (const line of text.slice(4, end).split(`
861
+ `)) {
862
+ const colon = line.indexOf(":");
863
+ if (colon <= 0 || line.trimStart().startsWith("#"))
864
+ continue;
865
+ const key = line.slice(0, colon).trim();
866
+ const value = line.slice(colon + 1).replace(/#.*$/, "").trim().replace(/^"(.*)"$/, "$1");
867
+ if (key !== "")
868
+ frontMatter.set(key, value);
869
+ }
870
+ const newline = text.indexOf(`
871
+ `, end + 1);
872
+ return { frontMatter, body: newline === -1 ? "" : text.slice(newline + 1) };
873
+ }
874
+ function section(body, heading) {
875
+ const lines = body.split(`
876
+ `);
877
+ const collected = [];
878
+ let inside = false;
879
+ for (const line of lines) {
880
+ const match = H2_RE.exec(line);
881
+ if (match !== null) {
882
+ if (inside)
883
+ break;
884
+ inside = match[1] === heading;
885
+ continue;
886
+ }
887
+ if (inside)
888
+ collected.push(line);
889
+ }
890
+ return collected.join(`
891
+ `).trim();
892
+ }
893
+
894
+ // src/core/experts/expertDomain.ts
895
+ var PLACEHOLDER_PATHS = new Set(["path/to/module", "path/to/other"]);
896
+ function readExpertDomain(root, name) {
897
+ const doc = readExpertDocument(root, name);
898
+ if (!doc.exists)
899
+ return { name, exists: false, kind: "", repos: [], paths: [] };
900
+ return {
901
+ name,
902
+ exists: true,
903
+ kind: doc.frontMatter.get("kind") ?? "",
904
+ repos: parseList(doc.frontMatter.get("repos") ?? ""),
905
+ paths: domainPaths(section(doc.body, "Domain"))
906
+ };
907
+ }
908
+ function parseList(value) {
909
+ const inner = value.trim().replace(/^\[/, "").replace(/\]$/, "");
910
+ return inner.split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter((item) => item !== "");
911
+ }
912
+ function domainPaths(body) {
913
+ const paths = [];
914
+ for (const line of body.split(`
915
+ `)) {
916
+ const bullet = /^\s*[-*]\s+(.*)$/.exec(line);
917
+ if (bullet === null)
918
+ continue;
919
+ const text = (bullet[1] ?? "").trim();
920
+ if (/^repo\s/i.test(text))
921
+ continue;
922
+ const code = /`([^`]+)`/.exec(text);
923
+ const raw = (code?.[1] ?? text.split(/\s+—\s+/)[0] ?? "").trim();
924
+ const path = normalisePath(raw);
925
+ if (path === "" || PLACEHOLDER_PATHS.has(path) || !path.includes("/"))
926
+ continue;
927
+ if (!paths.includes(path))
928
+ paths.push(path);
929
+ }
930
+ return paths;
931
+ }
932
+ function normalisePath(raw) {
933
+ return raw.trim().replace(/^[a-z0-9-]+:(?=[^/])/i, "").replace(/^\.\//, "").replace(/\/+$/, "").replace(/\\/g, "/");
934
+ }
935
+ function pathsIntersect(a, b) {
936
+ const left = normalisePath(a);
937
+ const right = normalisePath(b);
938
+ if (left === "" || right === "")
939
+ return false;
940
+ if (left === right)
941
+ return true;
942
+ return left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
943
+ }
944
+
945
+ // src/core/experts/selectExperts.ts
946
+ var MAX_DOMAIN_SELECTED = 8;
947
+ var LEGACY_STAGE_EXPERTS = ["domain", "stack"];
948
+ function selectExperts(input) {
949
+ const experts = [];
950
+ const seen = new Set;
951
+ const missing = [];
952
+ const legacy = [];
953
+ for (const name of input.staged) {
954
+ if (seen.has(name))
955
+ continue;
956
+ seen.add(name);
957
+ if (!hasExpert(input.root, name)) {
958
+ if (LEGACY_STAGE_EXPERTS.includes(name))
959
+ legacy.push(name);
960
+ else
961
+ missing.push(name);
962
+ continue;
963
+ }
964
+ experts.push({ name, reason: "stage", relevant: true });
965
+ }
966
+ if (input.stackExperts) {
967
+ for (const name of input.stackNames) {
968
+ if (seen.has(name))
969
+ continue;
970
+ seen.add(name);
971
+ if (!hasExpert(input.root, name))
972
+ continue;
973
+ experts.push({ name, reason: "stack", relevant: true });
974
+ }
975
+ }
976
+ const { picked, overflow } = domainMatches(input, seen);
977
+ for (const match of picked) {
978
+ seen.add(match.name);
979
+ experts.push(match);
980
+ }
981
+ return { experts, missing, overflow, legacy };
982
+ }
983
+ function expertNames(root) {
984
+ const dir = expertsDir(root);
985
+ if (!existsSync6(dir))
986
+ return [];
987
+ return readdirSync3(dir).filter((entry) => {
988
+ try {
989
+ return statSync3(`${dir}/${entry}`).isDirectory();
990
+ } catch {
991
+ return false;
992
+ }
993
+ }).sort();
994
+ }
995
+ function hasExpert(root, name) {
996
+ return existsSync6(`${expertsDir(root)}/${name}`);
997
+ }
998
+ var DIRECT_WEIGHT = 10;
999
+ var GRAPH_HOPS = 2;
1000
+ function domainMatches(input, seen) {
1001
+ if (input.repos.length === 0 && (input.citedPaths ?? []).length === 0) {
1002
+ return { picked: [], overflow: [] };
1003
+ }
1004
+ const repoIsEvidence = (input.workspaceRepoCount ?? 2) >= 2;
1005
+ const cited = input.citedPaths ?? [];
1006
+ const nearby = input.nearbyPaths ?? new Set;
1007
+ const scored = [];
1008
+ for (const name of expertNames(input.root)) {
1009
+ if (seen.has(name))
1010
+ continue;
1011
+ const declared = readExpertDomain(input.root, name);
1012
+ if (declared.kind !== "domain")
1013
+ continue;
1014
+ const direct = countMatches(declared, cited);
1015
+ const near = direct > 0 ? 0 : countMatches(declared, [...nearby]);
1016
+ const score = direct * DIRECT_WEIGHT + near;
1017
+ if (score > 0) {
1018
+ const match = direct > 0 ? matchedPath(declared, cited) ?? "path" : `${String(near)} path(s) within ${String(GRAPH_HOPS)} hops in graph.json`;
1019
+ scored.push({ expert: { name, reason: "domain", match, relevant: true }, score });
1020
+ continue;
1021
+ }
1022
+ if (!repoIsEvidence)
1023
+ continue;
1024
+ const repo = declared.repos.find((r) => input.repos.includes(r));
1025
+ if (repo !== undefined) {
1026
+ scored.push({
1027
+ expert: { name, reason: "domain", match: `repo ${repo}`, relevant: false },
1028
+ score: 0
1029
+ });
1030
+ }
1031
+ }
1032
+ const ranked = [...scored].sort((a, b) => b.score - a.score || (a.expert.name < b.expert.name ? -1 : a.expert.name > b.expert.name ? 1 : 0)).map((entry) => entry.expert);
1033
+ return {
1034
+ picked: ranked.slice(0, MAX_DOMAIN_SELECTED),
1035
+ overflow: ranked.slice(MAX_DOMAIN_SELECTED).map((item) => item.name)
1036
+ };
1037
+ }
1038
+ function countMatches(declared, paths) {
1039
+ let count = 0;
1040
+ for (const cited of paths) {
1041
+ if (matchedPath(declared, [cited]) !== null)
1042
+ count += 1;
1043
+ }
1044
+ return count;
1045
+ }
1046
+ function matchedPath(declared, citedPaths) {
1047
+ for (const cited of citedPaths) {
1048
+ for (const path of declared.paths) {
1049
+ if (pathsIntersect(path, cited))
1050
+ return cited;
1051
+ for (const repo of declared.repos) {
1052
+ if (pathsIntersect(`${repo}/${path}`, cited))
1053
+ return cited;
1054
+ }
1055
+ }
1056
+ }
1057
+ return null;
1058
+ }
1059
+
1060
+ // src/core/experts/stageCoverage.ts
1061
+ function stagesLoadingExperts(root) {
1062
+ const repos = workspaceRepos(root);
1063
+ const stackNames = stackExpertNames(root, repos);
1064
+ const byExpert = new Map;
1065
+ for (const stage of stageIds(root)) {
1066
+ const doc = readStage(root, stage);
1067
+ if (doc === null)
1068
+ continue;
1069
+ const selection = selectExperts({
1070
+ root,
1071
+ staged: strings(doc.experts),
1072
+ repos,
1073
+ stackExperts: typeof doc.stack_experts === "boolean" ? doc.stack_experts : true,
1074
+ stackNames
1075
+ });
1076
+ for (const chosen of selection.experts) {
1077
+ const rows = byExpert.get(chosen.name) ?? [];
1078
+ rows.push({ stage, reason: chosen.reason });
1079
+ byExpert.set(chosen.name, rows);
1080
+ }
1081
+ }
1082
+ return byExpert;
1083
+ }
1084
+ function stageIds(root) {
1085
+ const ids = new Set([
1086
+ ...dirsIn(join6(root, PROJECT_FRAMEWORK_DIR, "stages")),
1087
+ ...dirsIn(STAGES_DIR)
1088
+ ]);
1089
+ return [...ids].sort();
1090
+ }
1091
+ function dirsIn(dir) {
1092
+ if (!existsSync7(dir))
1093
+ return [];
1094
+ try {
1095
+ return readdirSync4(dir).filter((entry) => {
1096
+ try {
1097
+ return statSync4(join6(dir, entry)).isDirectory() && existsSync7(join6(dir, entry, "stage.yml"));
1098
+ } catch {
1099
+ return false;
1100
+ }
1101
+ });
1102
+ } catch {
1103
+ return [];
1104
+ }
1105
+ }
1106
+ function readStage(root, id) {
1107
+ const local = join6(root, PROJECT_FRAMEWORK_DIR, "stages", id, "stage.yml");
1108
+ const path = existsSync7(local) ? local : join6(STAGES_DIR, id, "stage.yml");
1109
+ if (!existsSync7(path))
1110
+ return null;
1111
+ try {
1112
+ const doc = parseYaml(readFileSync6(path, "utf8"));
1113
+ return isRecord(doc) ? doc : null;
1114
+ } catch {
1115
+ return null;
1116
+ }
1117
+ }
1118
+ function workspaceRepos(root) {
1119
+ const path = join6(root, PROJECT_FRAMEWORK_DIR, "workspace.yml");
1120
+ if (!existsSync7(path))
1121
+ return [];
1122
+ try {
1123
+ const doc = parseYaml(readFileSync6(path, "utf8"));
1124
+ const list = isRecord(doc) && Array.isArray(doc.repos) ? doc.repos : [];
1125
+ return list.map((row) => isRecord(row) && typeof row.name === "string" ? row.name : "").filter((name) => name !== "");
1126
+ } catch {
1127
+ return [];
1128
+ }
1129
+ }
1130
+ function strings(value) {
1131
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
1132
+ }
1133
+
1134
+ // src/core/experts/renderExpertList.ts
1135
+ function evidenceCount(expert) {
1136
+ return expert.areas.reduce((total, area) => total + area.evidence.length, 0);
1137
+ }
1138
+ // src/core/experts/expertKnowledge.ts
1139
+ var DEFAULT_KNOWLEDGE_MAX_BYTES = 48 * 1024;
1140
+
1141
+ // src/core/experts/roleExperts.ts
1142
+ import { join as join7 } from "node:path";
1143
+ var ROLE_TEMPLATES_DIR = join7(TEMPLATES_DIR, "experts");
1144
+ // src/core/experts/domainRank.ts
1145
+ var MAX_GRAPH_BYTES = 64 * 1024 * 1024;
1146
+ // src/core/training/mineRuns.ts
1147
+ import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
1148
+ import { join as join8, relative as relative2 } from "node:path";
1149
+ var MAX_RUN_BYTES = 64 * 1024;
1150
+ var MINED_NAMES = ["handoff.md", "retro.md"];
1151
+ var MAX_DEPTH = 4;
1152
+ function hasMinableFiles(root) {
1153
+ for (const runDir of listRunDirs(root)) {
1154
+ if (findMined(runDir, 0).length > 0)
1155
+ return true;
1156
+ }
1157
+ return false;
1158
+ }
1159
+ function findMined(dir, depth) {
1160
+ if (depth > MAX_DEPTH)
1161
+ return [];
1162
+ let entries;
1163
+ try {
1164
+ entries = readdirSync5(dir);
1165
+ } catch {
1166
+ return [];
1167
+ }
1168
+ const found = [];
1169
+ for (const entry of [...entries].sort()) {
1170
+ if (entry.startsWith("."))
1171
+ continue;
1172
+ const abs = join8(dir, entry);
1173
+ let isDir = false;
1174
+ try {
1175
+ isDir = statSync5(abs).isDirectory();
1176
+ } catch {
1177
+ continue;
1178
+ }
1179
+ if (isDir)
1180
+ found.push(...findMined(abs, depth + 1));
1181
+ else if (MINED_NAMES.includes(entry))
1182
+ found.push(abs);
1183
+ }
1184
+ return found;
1185
+ }
1186
+
1187
+ // src/core/status/expertItems.ts
1188
+ var MAX_EXPERT_NAMES = 5;
1189
+ function expertAdvice(root) {
1190
+ const loads = stagesLoadingExperts(root);
1191
+ const untrained = loadExperts(root).filter((expert) => expert.error === null).filter((expert) => (loads.get(expert.name) ?? []).length > 0).filter((expert) => evidenceCount(expert) === 0).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1192
+ if (untrained.length === 0)
1193
+ return [];
1194
+ const minable = hasMinableFiles(root);
1195
+ const trainable = [];
1196
+ const waiting = [];
1197
+ const arealess = [];
1198
+ for (const expert of untrained) {
1199
+ if (expert.areas[0] === undefined)
1200
+ arealess.push(expert.name);
1201
+ else if (isRole(root, expert) && !minable)
1202
+ waiting.push(expert.name);
1203
+ else
1204
+ trainable.push(expert.name);
1205
+ }
1206
+ const details = [];
1207
+ if (trainable.length > 0) {
1208
+ const shown = trainable.slice(0, MAX_EXPERT_NAMES);
1209
+ const more = trainable.length - shown.length;
1210
+ details.push(`train the ones a stage will load: ${shown.join(", ")}` + (more === 0 ? "" : ` (and ${String(more)} more)`));
1211
+ const first = untrained.find((expert) => expert.name === shown[0]);
1212
+ const area = first?.areas[0];
1213
+ if (first !== undefined && area !== undefined) {
1214
+ const mode = isRole(root, first) ? "full" : "light";
1215
+ details.push(`e.g. tldrx expert train ${first.name} --area ${area.id} --mode ${mode} --print-prompt`);
1216
+ }
1217
+ }
1218
+ if (waiting.length > 0) {
1219
+ details.push(waiting.length === 1 ? `${waiting[0] ?? ""} is a role expert: it trains from past runs' handoffs, ` + "and this workspace has none yet" : `${waiting.join(", ")} are role experts: they train from past runs' handoffs, ` + "and this workspace has none yet");
1220
+ }
1221
+ if (arealess.length > 0) {
1222
+ details.push(`${arealess.join(", ")} ${arealess.length === 1 ? "has" : "have"} no competency areas, ` + "so nothing can be trained into them yet");
1223
+ }
1224
+ details.push("an untrained expert reads like a trained one inside the prompt; it just has nothing to say");
1225
+ return [{
1226
+ kind: "expert",
1227
+ summary: `${String(untrained.length)} expert(s) a stage will load have no evidence yet`,
1228
+ command: "tldrx expert list",
1229
+ details
1230
+ }];
1231
+ }
1232
+ function isRole(root, expert) {
1233
+ return readExpertDomain(root, expert.name).kind === "role";
1234
+ }
1235
+
1236
+ // src/core/status/workspaceStatus.ts
1237
+ var NOTHING_PENDING = {
1238
+ kind: "none",
1239
+ summary: "nothing pending — open work with `tldrx run new <slug> --seed <path>` or `tldrx seed triage <path>`",
1240
+ command: "",
1241
+ details: []
1242
+ };
1243
+ function buildWorkspaceStatus(root) {
1244
+ const items = [
1245
+ ...safely(() => {
1246
+ const item = initQuestionsItem(root);
1247
+ return item === null ? [] : [item];
1248
+ }),
1249
+ ...safely(() => seedSplitItems(root)),
1250
+ ...safely(() => runItems(root))
1251
+ ];
1252
+ const advice = safely(() => expertAdvice(root));
1253
+ return items.length === 0 ? { root, items: [NOTHING_PENDING], advice, pending: 0 } : { root, items, advice, pending: items.length };
1254
+ }
1255
+ function safely(build) {
1256
+ try {
1257
+ return build();
1258
+ } catch {
1259
+ return [];
1260
+ }
1261
+ }
1262
+ // src/core/status/renderWorkspaceStatus.ts
1263
+ function sessionStartLines(status, max) {
1264
+ const pending = status.items.filter(isPending);
1265
+ if (pending.length === 0 || max <= 0)
1266
+ return [];
1267
+ const lines = [
1268
+ `tldrx: ${String(pending.length)} pending — \`tldrx status\` for all of it, or \`/tldrx\` to be walked through it`
1269
+ ];
1270
+ for (const item of pending) {
1271
+ if (lines.length >= max)
1272
+ break;
1273
+ lines.push(`tldrx: [${String(lines.length)}] ${item.summary}`);
1274
+ }
1275
+ return lines;
1276
+ }
1277
+ // src/hooks/session-start.ts
1278
+ var MAX_LINES = 3;
1279
+ var MAX_PENDING_LINES = 3;
1280
+ var MAX_OPEN_LISTED = 8;
1281
+ await runHook("session-start", async () => {
1282
+ const payload = await readPayload();
1283
+ const root = findWorkspaceRoot(payload.cwd ?? process.cwd());
1284
+ if (root === null)
1285
+ return;
1286
+ const pending = renderPending(root);
1287
+ const snapshot = runSnapshot(root);
1288
+ if (snapshot === null) {
1289
+ if (pending.length > 0)
1290
+ sessionContext(pending.join(`
1291
+ `));
1292
+ return;
1293
+ }
1294
+ sessionContext([...renderOpenRuns(root, snapshot), ...renderStatus(snapshot), ...pending].join(`
1295
+ `));
1296
+ });
1297
+ function renderPending(root) {
1298
+ try {
1299
+ return sessionStartLines(buildWorkspaceStatus(root), MAX_PENDING_LINES);
1300
+ } catch {
1301
+ return [];
1302
+ }
1303
+ }
1304
+ function renderOpenRuns(root, newest) {
1305
+ if (newest.openCount < 2)
1306
+ return [];
1307
+ const open = openRunViews(root).slice(0, MAX_OPEN_LISTED);
1308
+ const lines = [
1309
+ `tldrx: ${String(newest.openCount)} runs are open — pass a run id to next/answer/approve/…`
1310
+ ];
1311
+ for (const view of open) {
1312
+ const cursor = view.cursor === null ? "?" : `${view.cursor.phase}/${view.cursor.stage}`;
1313
+ const mark = view.run === newest.run ? " (newest)" : "";
1314
+ lines.push(`tldrx: ${view.run} · ${view.status || "unknown"} · ${cursor}${mark}`);
1315
+ }
1316
+ if (newest.openCount > open.length) {
1317
+ lines.push(`tldrx: … and ${String(newest.openCount - open.length)} more — \`tldrx run status\``);
1318
+ }
1319
+ return lines;
1320
+ }
1321
+ function renderStatus(snapshot) {
1322
+ const lines = [];
1323
+ const title = snapshot.title === "" ? "" : ` — "${snapshot.title}"`;
1324
+ const scope = snapshot.scope === "" ? "" : ` (${snapshot.scope})`;
1325
+ lines.push(`tldrx: run ${snapshot.run}${title}${scope} · ${snapshot.status || "unknown"}`);
1326
+ if (snapshot.stage !== "") {
1327
+ const expert = snapshot.expert === null ? "" : ` — ${snapshot.expert}`;
1328
+ lines.push(`tldrx: at ${snapshot.phase} / ${snapshot.stage}${expert} · ${snapshot.stageStatus}`);
1329
+ }
1330
+ const pending = pendingLine(snapshot);
1331
+ if (pending !== null)
1332
+ lines.push(pending);
1333
+ return lines.slice(0, MAX_LINES);
1334
+ }
1335
+ function pendingLine(snapshot) {
1336
+ const open = openQuestions(snapshot);
1337
+ const bits = [];
1338
+ if (open.length > 0) {
1339
+ bits.push(`${String(open.length)} open question${open.length === 1 ? "" : "s"} (${open.slice(0, 3).join(", ")})`);
1340
+ }
1341
+ if (snapshot.status === "awaiting_gate" || snapshot.stageStatus === "awaiting_gate") {
1342
+ bits.push("gate pending: `tldrx approve`");
1343
+ }
1344
+ if (bits.length === 0)
1345
+ return null;
1346
+ return `tldrx: ${bits.join(" · ")}`;
1347
+ }
1348
+ allow();