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,339 @@
1
+ import {
2
+ parseSrcToken
3
+ } from "./chunk-g395gk7e.js";
4
+ import {
5
+ PROJECT_FRAMEWORK_DIR,
6
+ 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
+ }
168
+
169
+ // src/core/lock/workspaceLock.ts
170
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, rmSync as rmSync2, writeSync, closeSync } from "node:fs";
171
+ import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
172
+
173
+ // src/core/facilitator/Lock.ts
174
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
175
+ import { dirname, join } from "node:path";
176
+ var LOCK_FILE = ".lock";
177
+ function lockPath(runDir) {
178
+ return join(runDir, LOCK_FILE);
179
+ }
180
+ function readLock(runDir) {
181
+ const path = lockPath(runDir);
182
+ if (!existsSync(path))
183
+ return null;
184
+ try {
185
+ const doc = JSON.parse(readFileSync(path, "utf8"));
186
+ const pid = typeof doc.pid === "number" ? doc.pid : Number.NaN;
187
+ if (!Number.isInteger(pid))
188
+ return null;
189
+ return { pid, at: typeof doc.at === "string" ? doc.at : "" };
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+ function isAlive(pid) {
195
+ if (!Number.isInteger(pid) || pid <= 0)
196
+ return false;
197
+ try {
198
+ process.kill(pid, 0);
199
+ return true;
200
+ } catch (error) {
201
+ return error.code === "EPERM";
202
+ }
203
+ }
204
+
205
+ // src/core/lock/workspaceLock.ts
206
+ var WORKSPACE_LOCK_FILE = ".lock";
207
+ var WORKSPACE_LOCK_TIMEOUT_MS = 5000;
208
+ var SPIN_MS = 25;
209
+
210
+ class WorkspaceLockError extends Error {
211
+ }
212
+ function workspaceLockPath(root) {
213
+ return join2(resolve(root), PROJECT_FRAMEWORK_DIR, WORKSPACE_LOCK_FILE);
214
+ }
215
+ function workspaceRootOfRunDir(runDir) {
216
+ const parent = dirname2(resolve(runDir));
217
+ return parent.endsWith(`/${PROJECT_WORK_DIR}`) || parent.endsWith(`\\${PROJECT_WORK_DIR}`) ? dirname2(parent) : resolve(runDir);
218
+ }
219
+ function workspaceRootOfFactsPath(factsFile) {
220
+ const full = resolve(factsFile);
221
+ const memory = dirname2(full);
222
+ const framework = dirname2(memory);
223
+ return basename(memory) === "memory" && basename(framework) === PROJECT_FRAMEWORK_DIR ? dirname2(framework) : memory;
224
+ }
225
+ var held = new Map;
226
+ function sleepSync(ms) {
227
+ const buffer = new Int32Array(new SharedArrayBuffer(4));
228
+ Atomics.wait(buffer, 0, 0, ms);
229
+ }
230
+ function acquire(path, timeoutMs) {
231
+ const deadline = Date.now() + timeoutMs;
232
+ mkdirSync2(dirname2(path), { recursive: true });
233
+ for (;; ) {
234
+ try {
235
+ const fd = openSync(path, "wx");
236
+ try {
237
+ writeSync(fd, `${JSON.stringify({ pid: process.pid, at: new Date().toISOString() }, null, 2)}
238
+ `);
239
+ } finally {
240
+ closeSync(fd);
241
+ }
242
+ return;
243
+ } catch (error) {
244
+ if (error.code !== "EEXIST")
245
+ throw error;
246
+ }
247
+ const holder = readHolder(path);
248
+ if (holder === null || !isAlive(holder.pid)) {
249
+ rmSync2(path, { force: true });
250
+ continue;
251
+ }
252
+ if (Date.now() >= deadline) {
253
+ throw new WorkspaceLockError(`${path} is held by live pid ${String(holder.pid)}` + (holder.at === "" ? "" : ` since ${holder.at}`) + ` — waited ${String(timeoutMs)}ms. Wait for it, or remove the file if that process is gone.`);
254
+ }
255
+ sleepSync(SPIN_MS);
256
+ }
257
+ }
258
+ function readHolder(path) {
259
+ if (!existsSync2(path))
260
+ return null;
261
+ try {
262
+ const doc = JSON.parse(readFileSync2(path, "utf8"));
263
+ const pid = typeof doc.pid === "number" ? doc.pid : Number.NaN;
264
+ if (!Number.isInteger(pid))
265
+ return null;
266
+ return { pid, at: typeof doc.at === "string" ? doc.at : "" };
267
+ } catch {
268
+ return null;
269
+ }
270
+ }
271
+ function withWorkspaceLock(root, fn, timeoutMs = WORKSPACE_LOCK_TIMEOUT_MS) {
272
+ const path = workspaceLockPath(root);
273
+ const depth = held.get(path) ?? 0;
274
+ if (depth === 0)
275
+ acquire(path, timeoutMs);
276
+ held.set(path, depth + 1);
277
+ try {
278
+ return fn();
279
+ } finally {
280
+ const now = (held.get(path) ?? 1) - 1;
281
+ if (now <= 0) {
282
+ held.delete(path);
283
+ rmSync2(path, { force: true });
284
+ } else {
285
+ held.set(path, now);
286
+ }
287
+ }
288
+ }
289
+
290
+ // src/core/facts/emitFactsYaml.ts
291
+ var PLAIN_SAFE = /^[A-Za-z][A-Za-z0-9_./-]*$/;
292
+ var YAML_KEYWORDS = new Set(["true", "false", "null", "yes", "no", "on", "off", "~"]);
293
+ function yamlScalar(value) {
294
+ if (value === null)
295
+ return "null";
296
+ if (typeof value === "number")
297
+ return String(value);
298
+ if (PLAIN_SAFE.test(value) && !YAML_KEYWORDS.has(value.toLowerCase()))
299
+ return value;
300
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
301
+ }
302
+ function inlineList(values) {
303
+ return `[${values.map((v) => yamlScalar(v)).join(", ")}]`;
304
+ }
305
+ function emitFact(fact, indent = " ") {
306
+ const inner = `${indent} `;
307
+ const lines = [
308
+ `${indent}- id: ${yamlScalar(fact.id)}`,
309
+ `${inner}fact: ${yamlScalar(fact.fact)}`,
310
+ `${inner}area: ${yamlScalar(fact.area)}`,
311
+ `${inner}repos: ${inlineList(fact.repos)}`,
312
+ `${inner}kind: ${yamlScalar(fact.kind)}`,
313
+ `${inner}confidence: ${yamlScalar(fact.confidence)}`,
314
+ `${inner}source: {who: ${yamlScalar(fact.source.who)}, when: ${yamlScalar(fact.source.when)}, ` + `run: ${yamlScalar(fact.source.run)}, q: ${yamlScalar(fact.source.q)}}`,
315
+ `${inner}supersedes: ${yamlScalar(fact.supersedes)}`,
316
+ `${inner}superseded_by: ${yamlScalar(fact.superseded_by)}`
317
+ ];
318
+ if (fact.retired === null) {
319
+ lines.push(`${inner}retired: null`);
320
+ } else {
321
+ lines.push(`${inner}retired: {at: ${yamlScalar(fact.retired.at)}, by: ${yamlScalar(fact.retired.by)}, ` + `reason: ${yamlScalar(fact.retired.reason)}}`);
322
+ }
323
+ return lines.join(`
324
+ `);
325
+ }
326
+ function emitFactsYaml(file, header) {
327
+ const out = [];
328
+ if (header !== undefined && header !== "")
329
+ out.push(header.replace(/\n$/, ""));
330
+ out.push(`version: ${file.version}`);
331
+ out.push("facts:");
332
+ for (const fact of file.facts)
333
+ out.push(emitFact(fact));
334
+ return `${out.join(`
335
+ `)}
336
+ `;
337
+ }
338
+
339
+ export { readLock, isAlive, workspaceRootOfRunDir, workspaceRootOfFactsPath, withWorkspaceLock, yamlScalar, emitFactsYaml, parseQuestions, serializeQuestions, detectAnswered, openBlocks, recordAnswer, replaceBlock };
@@ -0,0 +1,139 @@
1
+ import {
2
+ hasSrcMarker,
3
+ parseSrcToken,
4
+ resolveSrc
5
+ } from "./chunk-g395gk7e.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 isHandoff(text) {
65
+ return missingSections(parseHandoff(text)).length === 0;
66
+ }
67
+ function missingSections(handoff) {
68
+ const present = handoff.headings;
69
+ const missing = [];
70
+ let cursor = 0;
71
+ for (const required of HANDOFF_SECTIONS) {
72
+ const at = present.indexOf(required, cursor);
73
+ if (at === -1)
74
+ missing.push(required);
75
+ else
76
+ cursor = at + 1;
77
+ }
78
+ return missing;
79
+ }
80
+ var MAX_BULLETS = 200;
81
+ function validateHandoff(text, ctx) {
82
+ const handoff = parseHandoff(text);
83
+ const missing = missingSections(handoff);
84
+ const emptySections = [];
85
+ const unsourced = [];
86
+ const malformed = [];
87
+ const unresolved = [];
88
+ const unverified = [];
89
+ let bulletCount = 0;
90
+ const required = new Set(HANDOFF_SECTIONS);
91
+ for (const section of handoff.sections) {
92
+ if (!required.has(section.name))
93
+ continue;
94
+ if (section.bullets.length === 0) {
95
+ emptySections.push({ name: section.name, line: section.headingLine });
96
+ }
97
+ for (const bullet of section.bullets) {
98
+ bulletCount++;
99
+ if (bullet.token === null) {
100
+ if (hasSrcMarker(bullet.text)) {
101
+ malformed.push({
102
+ line: bullet.line,
103
+ 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)"
104
+ });
105
+ } else {
106
+ unsourced.push(bullet.line);
107
+ }
108
+ continue;
109
+ }
110
+ for (const error of bullet.token.errors) {
111
+ unresolved.push({ line: bullet.line, message: `[src: ${error.raw}] — ${error.message}` });
112
+ }
113
+ const claim = bullet.text.replace(bullet.token.raw, " ").trim();
114
+ for (const ref of bullet.token.refs) {
115
+ const resolution = resolveSrc(ref, ctx, section.name, claim);
116
+ const issue = { line: bullet.line, message: `[src: ${ref.raw}] — ${resolution.message ?? "unresolvable"}` };
117
+ if (resolution.outcome === "refused")
118
+ unresolved.push(issue);
119
+ else if (resolution.outcome === "unverified")
120
+ unverified.push(issue);
121
+ }
122
+ }
123
+ }
124
+ if (bulletCount > MAX_BULLETS) {
125
+ unresolved.push({ line: 0, message: `${bulletCount} bullets exceeds the ${MAX_BULLETS} cap` });
126
+ }
127
+ return {
128
+ ok: missing.length === 0 && emptySections.length === 0 && unsourced.length === 0 && malformed.length === 0 && unresolved.length === 0,
129
+ missingSections: missing,
130
+ emptySections,
131
+ unsourced,
132
+ malformed,
133
+ unresolved,
134
+ unverified,
135
+ bulletCount
136
+ };
137
+ }
138
+
139
+ export { isHandoff, validateHandoff };