@ctxr/skill-llm-wiki 1.0.1

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 +134 -0
  2. package/LICENSE +21 -0
  3. package/README.md +484 -0
  4. package/SKILL.md +252 -0
  5. package/guide/basics/concepts.md +74 -0
  6. package/guide/basics/index.md +45 -0
  7. package/guide/basics/schema.md +140 -0
  8. package/guide/cli.md +256 -0
  9. package/guide/correctness/index.md +45 -0
  10. package/guide/correctness/invariants.md +89 -0
  11. package/guide/correctness/safety.md +96 -0
  12. package/guide/history/diff.md +110 -0
  13. package/guide/history/hidden-git.md +130 -0
  14. package/guide/history/index.md +52 -0
  15. package/guide/history/remote-sync.md +113 -0
  16. package/guide/index.md +134 -0
  17. package/guide/isolation/coexistence.md +134 -0
  18. package/guide/isolation/index.md +44 -0
  19. package/guide/isolation/scale.md +251 -0
  20. package/guide/layout/in-place-mode.md +97 -0
  21. package/guide/layout/index.md +53 -0
  22. package/guide/layout/layout-contract.md +131 -0
  23. package/guide/layout/layout-modes.md +115 -0
  24. package/guide/operations/index.md +76 -0
  25. package/guide/operations/ingest/build.md +75 -0
  26. package/guide/operations/ingest/extend.md +61 -0
  27. package/guide/operations/ingest/index.md +54 -0
  28. package/guide/operations/ingest/join.md +65 -0
  29. package/guide/operations/maintain/fix.md +66 -0
  30. package/guide/operations/maintain/index.md +47 -0
  31. package/guide/operations/maintain/rebuild.md +86 -0
  32. package/guide/operations/validate.md +48 -0
  33. package/guide/substrate/index.md +47 -0
  34. package/guide/substrate/operators.md +96 -0
  35. package/guide/substrate/tiered-ai.md +363 -0
  36. package/guide/ux/index.md +44 -0
  37. package/guide/ux/preflight.md +150 -0
  38. package/guide/ux/user-intent.md +135 -0
  39. package/package.json +55 -0
  40. package/scripts/cli.mjs +893 -0
  41. package/scripts/commands/remote.mjs +93 -0
  42. package/scripts/commands/review.mjs +253 -0
  43. package/scripts/commands/sync.mjs +84 -0
  44. package/scripts/lib/chunk.mjs +421 -0
  45. package/scripts/lib/cluster-detect.mjs +516 -0
  46. package/scripts/lib/decision-log.mjs +343 -0
  47. package/scripts/lib/draft.mjs +158 -0
  48. package/scripts/lib/embeddings.mjs +366 -0
  49. package/scripts/lib/frontmatter.mjs +497 -0
  50. package/scripts/lib/git-commands.mjs +155 -0
  51. package/scripts/lib/git.mjs +486 -0
  52. package/scripts/lib/gitignore.mjs +62 -0
  53. package/scripts/lib/history.mjs +331 -0
  54. package/scripts/lib/indices.mjs +510 -0
  55. package/scripts/lib/ingest.mjs +258 -0
  56. package/scripts/lib/intent.mjs +713 -0
  57. package/scripts/lib/interactive.mjs +99 -0
  58. package/scripts/lib/migrate.mjs +126 -0
  59. package/scripts/lib/nest-applier.mjs +260 -0
  60. package/scripts/lib/operators.mjs +1365 -0
  61. package/scripts/lib/orchestrator.mjs +718 -0
  62. package/scripts/lib/paths.mjs +197 -0
  63. package/scripts/lib/preflight.mjs +213 -0
  64. package/scripts/lib/provenance.mjs +672 -0
  65. package/scripts/lib/quality-metric.mjs +269 -0
  66. package/scripts/lib/query-fixture.mjs +71 -0
  67. package/scripts/lib/rollback.mjs +95 -0
  68. package/scripts/lib/shape-check.mjs +172 -0
  69. package/scripts/lib/similarity-cache.mjs +126 -0
  70. package/scripts/lib/similarity.mjs +230 -0
  71. package/scripts/lib/snapshot.mjs +54 -0
  72. package/scripts/lib/source-frontmatter.mjs +85 -0
  73. package/scripts/lib/tier2-protocol.mjs +470 -0
  74. package/scripts/lib/tiered.mjs +453 -0
  75. package/scripts/lib/validate.mjs +362 -0
@@ -0,0 +1,497 @@
1
+ // Minimal, dependency-free YAML frontmatter parser/writer.
2
+ //
3
+ // Supports exactly the subset the methodology's index.md and leaf frontmatter
4
+ // use: scalars (string/number/bool/null), block mappings, block sequences of
5
+ // scalars, block sequences of maps, nested mappings. No flow maps, no anchors,
6
+ // no multi-document streams. Deterministic roundtrip: parse → render on an
7
+ // unchanged tree yields byte-identical output modulo key ordering (which is
8
+ // fixed by a canonical key order when rendering).
9
+ //
10
+ // We ship zero runtime dependencies because `kit install` does not populate
11
+ // `node_modules` inside an installed skill directory — scripts must run with
12
+ // just Node built-ins.
13
+
14
+ import { readFileSync, writeFileSync } from "node:fs";
15
+
16
+ const FM = "---";
17
+
18
+ export function readFrontmatter(filePath) {
19
+ const raw = readFileSync(filePath, "utf8");
20
+ return parseFrontmatter(raw, filePath);
21
+ }
22
+
23
+ export function parseFrontmatter(raw, filePath = "<buffer>") {
24
+ if (!raw.startsWith(FM + "\n") && raw !== FM + "\n") {
25
+ return { data: {}, body: raw };
26
+ }
27
+ const lines = raw.split("\n");
28
+ let closeIdx = -1;
29
+ for (let i = 1; i < lines.length; i++) {
30
+ if (lines[i] === FM) {
31
+ closeIdx = i;
32
+ break;
33
+ }
34
+ }
35
+ if (closeIdx === -1) {
36
+ throw new Error(`${filePath}: frontmatter opens with --- but never closes`);
37
+ }
38
+ const yamlLines = lines.slice(1, closeIdx);
39
+ const bodyLines = lines.slice(closeIdx + 1);
40
+ const data = parseYaml(yamlLines, filePath);
41
+ return { data, body: bodyLines.join("\n") };
42
+ }
43
+
44
+ export function writeFrontmatter(filePath, data, body) {
45
+ const yaml = renderYaml(data, 0);
46
+ const trimmedBody = body.startsWith("\n") ? body : "\n" + body;
47
+ writeFileSync(filePath, `${FM}\n${yaml}${FM}${trimmedBody}`, "utf8");
48
+ }
49
+
50
+ export function renderFrontmatter(data, body = "") {
51
+ const yaml = renderYaml(data, 0);
52
+ const trimmedBody = body.startsWith("\n") ? body : "\n" + body;
53
+ return `${FM}\n${yaml}${FM}${trimmedBody}`;
54
+ }
55
+
56
+ // ─── Parser ──────────────────────────────────────────────────────────────
57
+
58
+ class Parser {
59
+ constructor(lines, filePath) {
60
+ this.lines = lines;
61
+ this.pos = 0;
62
+ this.filePath = filePath;
63
+ }
64
+
65
+ peek() {
66
+ while (this.pos < this.lines.length) {
67
+ const raw = this.lines[this.pos];
68
+ if (raw.trim() === "" || raw.trimStart().startsWith("#")) {
69
+ this.pos++;
70
+ continue;
71
+ }
72
+ const indent = raw.length - raw.trimStart().length;
73
+ return { raw, indent, text: raw.slice(indent) };
74
+ }
75
+ return null;
76
+ }
77
+
78
+ advance() {
79
+ this.pos++;
80
+ }
81
+
82
+ error(msg, line) {
83
+ throw new Error(`${this.filePath}:${this.pos + 1}: ${msg} — "${line ?? ""}"`);
84
+ }
85
+ }
86
+
87
+ function parseYaml(lines, filePath) {
88
+ const p = new Parser(lines, filePath);
89
+ return parseMap(p, 0);
90
+ }
91
+
92
+ // Keys that can poison the parsed object's prototype. These are refused
93
+ // at parse time so adversarial frontmatter (e.g. from a shared wiki a
94
+ // user received from a third party) cannot plant properties on
95
+ // Object.prototype or swap the instance's [[Prototype]] via the
96
+ // `__proto__` setter. See `tests/unit/frontmatter-pollution.test.mjs`.
97
+ const POLLUTION_KEYS = new Set(["__proto__", "constructor", "prototype"]);
98
+
99
+ function safeAssign(out, key, value, p, tok) {
100
+ if (POLLUTION_KEYS.has(key)) {
101
+ p.error(`forbidden YAML key "${key}"`, tok?.raw ?? key);
102
+ }
103
+ // Defence in depth: always write via defineProperty so the __proto__
104
+ // setter cannot fire even if the key check above is ever loosened.
105
+ Object.defineProperty(out, key, {
106
+ value,
107
+ writable: true,
108
+ enumerable: true,
109
+ configurable: true,
110
+ });
111
+ }
112
+
113
+ function parseMap(p, baseIndent) {
114
+ const out = {};
115
+ while (true) {
116
+ const tok = p.peek();
117
+ if (!tok) return out;
118
+ if (tok.indent < baseIndent) return out;
119
+ if (tok.indent > baseIndent) {
120
+ p.error("unexpected indent", tok.raw);
121
+ }
122
+ const { text } = tok;
123
+ const colon = findKeyColon(text);
124
+ if (colon === -1) p.error("expected key:", tok.raw);
125
+ const key = text.slice(0, colon).trim();
126
+ const rest = text.slice(colon + 1).trim();
127
+ p.advance();
128
+
129
+ if (rest === "|" || rest === ">") {
130
+ safeAssign(out, key, parseBlockScalar(p, baseIndent, rest === "|"), p, tok);
131
+ continue;
132
+ }
133
+ if (rest !== "") {
134
+ safeAssign(out, key, parseScalarInline(rest), p, tok);
135
+ continue;
136
+ }
137
+
138
+ // Nested structure — peek indent to decide.
139
+ const next = p.peek();
140
+ if (!next || next.indent <= baseIndent) {
141
+ safeAssign(out, key, null, p, tok);
142
+ continue;
143
+ }
144
+ if (next.text.startsWith("- ") || next.text === "-") {
145
+ safeAssign(out, key, parseSeq(p, next.indent), p, tok);
146
+ } else {
147
+ safeAssign(out, key, parseMap(p, next.indent), p, tok);
148
+ }
149
+ }
150
+ }
151
+
152
+ function parseSeq(p, baseIndent) {
153
+ const out = [];
154
+ while (true) {
155
+ const tok = p.peek();
156
+ if (!tok) return out;
157
+ if (tok.indent < baseIndent) return out;
158
+ if (tok.indent > baseIndent) p.error("unexpected indent in sequence", tok.raw);
159
+ if (!tok.text.startsWith("-")) return out;
160
+
161
+ // Item starts at baseIndent with `- `. The rest of the line is either
162
+ // empty, a scalar, or a first key of an inline map.
163
+ const afterDash = tok.text === "-" ? "" : tok.text.slice(2);
164
+ p.advance();
165
+
166
+ if (afterDash === "") {
167
+ // Nested content under a bare `-`
168
+ const next = p.peek();
169
+ if (next && next.indent > baseIndent) {
170
+ if (next.text.startsWith("- ")) {
171
+ out.push(parseSeq(p, next.indent));
172
+ } else {
173
+ out.push(parseMap(p, next.indent));
174
+ }
175
+ } else {
176
+ out.push(null);
177
+ }
178
+ continue;
179
+ }
180
+
181
+ const colon = findKeyColon(afterDash);
182
+ if (colon === -1) {
183
+ out.push(parseScalarInline(afterDash));
184
+ continue;
185
+ }
186
+
187
+ // `- key: value` possibly followed by more keys at indent baseIndent+2
188
+ const firstKey = afterDash.slice(0, colon).trim();
189
+ const firstRest = afterDash.slice(colon + 1).trim();
190
+ const item = {};
191
+
192
+ if (firstRest === "|" || firstRest === ">") {
193
+ item[firstKey] = parseBlockScalar(p, baseIndent + 2, firstRest === "|");
194
+ } else if (firstRest !== "") {
195
+ item[firstKey] = parseScalarInline(firstRest);
196
+ } else {
197
+ // Nested structure under first key
198
+ const nested = p.peek();
199
+ if (nested && nested.indent > baseIndent + 2) {
200
+ if (nested.text.startsWith("- ")) {
201
+ item[firstKey] = parseSeq(p, nested.indent);
202
+ } else {
203
+ item[firstKey] = parseMap(p, nested.indent);
204
+ }
205
+ } else if (nested && nested.indent === baseIndent + 2) {
206
+ // First key had nested sub-map at +2 (legal when firstRest is empty)
207
+ if (nested.text.startsWith("- ")) {
208
+ item[firstKey] = parseSeq(p, nested.indent);
209
+ } else {
210
+ item[firstKey] = parseMap(p, nested.indent);
211
+ }
212
+ } else {
213
+ item[firstKey] = null;
214
+ }
215
+ }
216
+
217
+ // Additional keys at baseIndent+2
218
+ while (true) {
219
+ const cont = p.peek();
220
+ if (!cont) break;
221
+ if (cont.indent < baseIndent + 2) break;
222
+ if (cont.indent > baseIndent + 2) break;
223
+ if (cont.text.startsWith("- ")) break;
224
+ const subColon = findKeyColon(cont.text);
225
+ if (subColon === -1) break;
226
+ const subKey = cont.text.slice(0, subColon).trim();
227
+ const subRest = cont.text.slice(subColon + 1).trim();
228
+ p.advance();
229
+ if (subRest === "") {
230
+ const nested2 = p.peek();
231
+ if (nested2 && nested2.indent > baseIndent + 2) {
232
+ if (nested2.text.startsWith("- ")) {
233
+ item[subKey] = parseSeq(p, nested2.indent);
234
+ } else {
235
+ item[subKey] = parseMap(p, nested2.indent);
236
+ }
237
+ } else {
238
+ item[subKey] = null;
239
+ }
240
+ } else if (subRest === "|" || subRest === ">") {
241
+ item[subKey] = parseBlockScalar(p, baseIndent + 2, subRest === "|");
242
+ } else {
243
+ item[subKey] = parseScalarInline(subRest);
244
+ }
245
+ }
246
+
247
+ out.push(item);
248
+ }
249
+ }
250
+
251
+ function parseBlockScalar(p, baseIndent, literal) {
252
+ const collected = [];
253
+ while (p.pos < p.lines.length) {
254
+ const raw = p.lines[p.pos];
255
+ if (raw.trim() === "") {
256
+ collected.push("");
257
+ p.pos++;
258
+ continue;
259
+ }
260
+ const indent = raw.length - raw.trimStart().length;
261
+ if (indent <= baseIndent) break;
262
+ collected.push(raw.slice(baseIndent + 2));
263
+ p.pos++;
264
+ }
265
+ // Trim trailing empty lines
266
+ while (collected.length > 0 && collected[collected.length - 1] === "") {
267
+ collected.pop();
268
+ }
269
+ return literal ? collected.join("\n") : collected.join(" ").trim();
270
+ }
271
+
272
+ function parseScalarInline(raw) {
273
+ const s = raw.trim();
274
+ if (s === "") return null;
275
+ if (s === "null" || s === "~") return null;
276
+ if (s === "true") return true;
277
+ if (s === "false") return false;
278
+ if (/^-?\d+$/.test(s)) return Number(s);
279
+ if (/^-?\d+\.\d+$/.test(s)) return Number(s);
280
+ if (s.startsWith('"') && s.endsWith('"') && s.length >= 2) {
281
+ return s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\").replace(/\\n/g, "\n");
282
+ }
283
+ if (s.startsWith("'") && s.endsWith("'") && s.length >= 2) {
284
+ return s.slice(1, -1).replace(/''/g, "'");
285
+ }
286
+ if (s.startsWith("[") && s.endsWith("]")) {
287
+ const inner = s.slice(1, -1).trim();
288
+ if (inner === "") return [];
289
+ return splitFlow(inner).map((x) => parseScalarInline(x));
290
+ }
291
+ return s;
292
+ }
293
+
294
+ function splitFlow(inner) {
295
+ // Split on commas that are not inside quotes.
296
+ const out = [];
297
+ let depth = 0;
298
+ let cur = "";
299
+ let inSingle = false;
300
+ let inDouble = false;
301
+ for (const c of inner) {
302
+ if (c === "'" && !inDouble) inSingle = !inSingle;
303
+ else if (c === '"' && !inSingle) inDouble = !inDouble;
304
+ else if ((c === "[" || c === "{") && !inSingle && !inDouble) depth++;
305
+ else if ((c === "]" || c === "}") && !inSingle && !inDouble) depth--;
306
+ if (c === "," && !inSingle && !inDouble && depth === 0) {
307
+ out.push(cur.trim());
308
+ cur = "";
309
+ continue;
310
+ }
311
+ cur += c;
312
+ }
313
+ if (cur.trim() !== "") out.push(cur.trim());
314
+ return out;
315
+ }
316
+
317
+ function findKeyColon(text) {
318
+ let inSingle = false;
319
+ let inDouble = false;
320
+ for (let i = 0; i < text.length; i++) {
321
+ const c = text[i];
322
+ if (c === "'" && !inDouble) inSingle = !inSingle;
323
+ else if (c === '"' && !inSingle) inDouble = !inDouble;
324
+ else if (c === ":" && !inSingle && !inDouble) {
325
+ if (i === text.length - 1) return i;
326
+ if (text[i + 1] === " " || text[i + 1] === "\t") return i;
327
+ }
328
+ }
329
+ return -1;
330
+ }
331
+
332
+ // ─── Renderer ────────────────────────────────────────────────────────────
333
+
334
+ // Canonical top-level key order. Hand-tuned to match the authored shape
335
+ // in `guide/` — field order is a stable convention so that rebuilt
336
+ // frontmatter diffs cleanly against authored frontmatter. Keys not in
337
+ // this list retain insertion order after all known keys have been
338
+ // emitted, so unknown fields land at the bottom rather than scrambling
339
+ // the known ones.
340
+ const CANONICAL_KEY_ORDER = [
341
+ "id",
342
+ "type",
343
+ "depth_role",
344
+ "depth",
345
+ "focus",
346
+ "parents",
347
+ "shared_covers",
348
+ "covers",
349
+ "tags",
350
+ "domains",
351
+ "aliases",
352
+ "overlay_targets",
353
+ "nests_into",
354
+ "activation",
355
+ "activation_defaults",
356
+ "links",
357
+ "source",
358
+ "orientation",
359
+ "generator",
360
+ "mode",
361
+ "layout_contract_path",
362
+ "rebuild_needed",
363
+ "rebuild_reasons",
364
+ "rebuild_command",
365
+ "entries",
366
+ "children",
367
+ ];
368
+
369
+ function orderedKeys(data, canonical) {
370
+ const known = new Set(canonical);
371
+ const present = new Set(Object.keys(data));
372
+ const out = [];
373
+ for (const key of canonical) {
374
+ if (present.has(key)) out.push(key);
375
+ }
376
+ for (const key of Object.keys(data)) {
377
+ if (!known.has(key)) out.push(key);
378
+ }
379
+ return out;
380
+ }
381
+
382
+ function renderYaml(data, indent) {
383
+ if (data == null || typeof data !== "object") return "";
384
+ let out = "";
385
+ // Only the top level (indent 0) is reordered canonically. Nested
386
+ // objects (e.g. activation.keyword_matches) keep insertion order so
387
+ // authored ordering inside complex values is never rewritten.
388
+ const keys =
389
+ indent === 0 ? orderedKeys(data, CANONICAL_KEY_ORDER) : Object.keys(data);
390
+ for (const key of keys) {
391
+ out += renderKey(key, data[key], indent);
392
+ }
393
+ return out;
394
+ }
395
+
396
+ function renderKey(key, val, indent) {
397
+ const pad = " ".repeat(indent);
398
+ if (val === null || val === undefined) return `${pad}${key}: null\n`;
399
+ if (typeof val === "boolean" || typeof val === "number") {
400
+ return `${pad}${key}: ${val}\n`;
401
+ }
402
+ if (typeof val === "string") {
403
+ if (val.includes("\n")) {
404
+ const childPad = " ".repeat(indent + 1);
405
+ const lines = val.split("\n");
406
+ return `${pad}${key}: |\n${lines.map((l) => (l === "" ? "" : childPad + l)).join("\n")}\n`;
407
+ }
408
+ return `${pad}${key}: ${renderScalar(val)}\n`;
409
+ }
410
+ if (Array.isArray(val)) {
411
+ if (val.length === 0) return `${pad}${key}: []\n`;
412
+ let out = `${pad}${key}:\n`;
413
+ for (const item of val) {
414
+ out += renderSeqItem(item, indent + 1);
415
+ }
416
+ return out;
417
+ }
418
+ // Plain object
419
+ const keys = Object.keys(val);
420
+ if (keys.length === 0) return `${pad}${key}: {}\n`;
421
+ let out = `${pad}${key}:\n`;
422
+ for (const k of keys) {
423
+ out += renderKey(k, val[k], indent + 1);
424
+ }
425
+ return out;
426
+ }
427
+
428
+ function renderSeqItem(item, indent) {
429
+ const pad = " ".repeat(indent);
430
+ if (item === null || item === undefined) return `${pad}- null\n`;
431
+ if (typeof item === "boolean" || typeof item === "number") {
432
+ return `${pad}- ${item}\n`;
433
+ }
434
+ if (typeof item === "string") {
435
+ if (item.includes("\n")) {
436
+ // Rare in our frontmatter; fall back to scalar-with-escapes
437
+ return `${pad}- ${renderScalar(item.replace(/\n/g, "\\n"))}\n`;
438
+ }
439
+ return `${pad}- ${renderScalar(item)}\n`;
440
+ }
441
+ if (Array.isArray(item)) {
442
+ // Nested sequence inside a sequence — rare; recursive emission
443
+ let out = `${pad}-\n`;
444
+ for (const inner of item) out += renderSeqItem(inner, indent + 1);
445
+ return out;
446
+ }
447
+ // Map item
448
+ const keys = Object.keys(item);
449
+ if (keys.length === 0) return `${pad}- {}\n`;
450
+ const firstKey = keys[0];
451
+ const firstVal = item[firstKey];
452
+ let out = "";
453
+ // Emit first key inline with the dash
454
+ if (
455
+ firstVal === null ||
456
+ typeof firstVal === "boolean" ||
457
+ typeof firstVal === "number" ||
458
+ (typeof firstVal === "string" && !firstVal.includes("\n"))
459
+ ) {
460
+ out += `${pad}- ${firstKey}: ${firstVal === null ? "null" : typeof firstVal === "string" ? renderScalar(firstVal) : firstVal}\n`;
461
+ } else {
462
+ // Non-scalar first value: emit on separate line
463
+ out += `${pad}-\n`;
464
+ out += renderKey(firstKey, firstVal, indent + 1);
465
+ for (let i = 1; i < keys.length; i++) {
466
+ out += renderKey(keys[i], item[keys[i]], indent + 1);
467
+ }
468
+ return out;
469
+ }
470
+ // Subsequent keys at indent+1
471
+ for (let i = 1; i < keys.length; i++) {
472
+ out += renderKey(keys[i], item[keys[i]], indent + 1);
473
+ }
474
+ return out;
475
+ }
476
+
477
+ function renderScalar(v) {
478
+ if (typeof v !== "string") return String(v);
479
+ if (v === "") return '""';
480
+ if (v === "null" || v === "true" || v === "false" || v === "~") return `"${v}"`;
481
+ if (/^-?\d+(\.\d+)?$/.test(v)) return `"${v}"`;
482
+ // YAML-unsafe chars — must be quoted for correctness.
483
+ if (/[:#\[\]{}&*!|>'"`%@\t]/.test(v)) {
484
+ return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
485
+ }
486
+ // Defensive quoting: strings containing parens or slashes are quoted
487
+ // even when YAML does not strictly require it. Matches the hand-
488
+ // authored style in `guide/` and keeps rebuilt frontmatter textually
489
+ // close to authored frontmatter so diffs stay clean across rebuilds.
490
+ if (/[\/()]/.test(v)) {
491
+ return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
492
+ }
493
+ if (/^\s|\s$/.test(v) || /^[-?]/.test(v)) {
494
+ return `"${v}"`;
495
+ }
496
+ return v;
497
+ }
@@ -0,0 +1,155 @@
1
+ // git-commands.mjs — thin passthrough wrappers for the hidden-git
2
+ // subcommands exposed to Claude at runtime: log, show, diff, blame,
3
+ // reflog, history. These all run against the wiki's private repo via
4
+ // the isolation env in git.mjs; no user-git is ever consulted.
5
+ //
6
+ // The `diff` wrapper has extra sugar: a `--op <id>` flag expands to the
7
+ // commit range `pre-op/<id>..op/<id>`, which is the most common user
8
+ // question ("what did this operation do?"). Everything else is a plain
9
+ // passthrough so users can use their existing knowledge of git flags.
10
+ //
11
+ // `history <entry-id>` is a higher-level wrapper that walks the op-log
12
+ // plus `git log --follow` so Claude can answer "when and why did this
13
+ // entry change?" without stitching the two sources together itself.
14
+
15
+ import { gitRefExists, gitRun, redactUrl } from "./git.mjs";
16
+ import { readOpLog } from "./history.mjs";
17
+
18
+ // Shared: call git with the passed args and stream stdout to the caller.
19
+ // Exit code is the git exit code. We return it rather than exiting so
20
+ // the CLI can wrap errors in its usual way. stderr is routed through
21
+ // `redactUrl` on the off chance a remote URL surfaces in an error
22
+ // stream (D5 defence-in-depth from the Phase 8 security sweep).
23
+ function runPassthrough(wikiRoot, args) {
24
+ const r = gitRun(wikiRoot, args);
25
+ if (r.stdout) process.stdout.write(redactUrl(r.stdout));
26
+ if (r.stderr) process.stderr.write(redactUrl(r.stderr));
27
+ return r.status ?? 0;
28
+ }
29
+
30
+ // diff <wiki> [--op <id>] [extra git-diff args...]
31
+ // Defaults add --find-renames --find-copies so rename detection is on.
32
+ export function cmdDiff(wikiRoot, { op, args }) {
33
+ const gitArgs = ["diff", "--find-renames", "--find-copies"];
34
+ if (op) {
35
+ const preTag = `pre-op/${op}`;
36
+ const finalTag = `op/${op}`;
37
+ if (!gitRefExists(wikiRoot, preTag)) {
38
+ process.stderr.write(
39
+ `diff: tag ${preTag} not found in ${wikiRoot}/.llmwiki/git\n`,
40
+ );
41
+ return 2;
42
+ }
43
+ // If the final tag exists, show pre..final. Otherwise show what has
44
+ // landed since pre (typical during an in-flight operation).
45
+ if (gitRefExists(wikiRoot, finalTag)) {
46
+ gitArgs.push(`${preTag}..${finalTag}`);
47
+ } else {
48
+ gitArgs.push(`${preTag}..HEAD`);
49
+ }
50
+ }
51
+ if (args && args.length > 0) gitArgs.push(...args);
52
+ return runPassthrough(wikiRoot, gitArgs);
53
+ }
54
+
55
+ // log <wiki> [--op <id>] [extra args...]
56
+ // Default: one-line oneline view of the op history. `--op <id>` narrows
57
+ // the output to the commits between `pre-op/<id>` and `op/<id>` (or
58
+ // `pre-op/<id>..HEAD` for an in-flight operation), matching the sugar
59
+ // `cmdDiff` already provides. Callers can pass any git-log arg to
60
+ // override the default format.
61
+ export function cmdLog(wikiRoot, { op, args }) {
62
+ const gitArgs = ["log"];
63
+ let rangeAppended = false;
64
+ if (op) {
65
+ const preTag = `pre-op/${op}`;
66
+ const finalTag = `op/${op}`;
67
+ if (!gitRefExists(wikiRoot, preTag)) {
68
+ process.stderr.write(
69
+ `log: tag ${preTag} not found in ${wikiRoot}/.llmwiki/git\n`,
70
+ );
71
+ return 2;
72
+ }
73
+ const range = gitRefExists(wikiRoot, finalTag)
74
+ ? `${preTag}..${finalTag}`
75
+ : `${preTag}..HEAD`;
76
+ gitArgs.push("--oneline", "--decorate", range);
77
+ rangeAppended = true;
78
+ } else if (!args || args.length === 0) {
79
+ gitArgs.push("--oneline", "--decorate", "--all");
80
+ }
81
+ if (args && args.length > 0) {
82
+ // When --op was passed, still accept extra git-log args after the
83
+ // range; when no --op, the args replace the default shape entirely.
84
+ if (rangeAppended) {
85
+ gitArgs.push(...args);
86
+ } else {
87
+ gitArgs.length = 1;
88
+ gitArgs.push(...args);
89
+ }
90
+ }
91
+ return runPassthrough(wikiRoot, gitArgs);
92
+ }
93
+
94
+ // show <wiki> <ref> [-- <path>]
95
+ export function cmdShow(wikiRoot, { ref, args }) {
96
+ if (!ref) {
97
+ process.stderr.write("show: <ref> is required\n");
98
+ return 1;
99
+ }
100
+ return runPassthrough(wikiRoot, ["show", ref, ...(args || [])]);
101
+ }
102
+
103
+ // blame <wiki> <path>
104
+ export function cmdBlame(wikiRoot, { path, args }) {
105
+ if (!path) {
106
+ process.stderr.write("blame: <path> is required\n");
107
+ return 1;
108
+ }
109
+ return runPassthrough(wikiRoot, ["blame", ...(args || []), path]);
110
+ }
111
+
112
+ // reflog <wiki>
113
+ export function cmdReflog(wikiRoot, { args }) {
114
+ return runPassthrough(wikiRoot, ["reflog", ...(args || [])]);
115
+ }
116
+
117
+ // history <wiki> <entry-id>
118
+ // Higher-level: walk the op-log first (so Claude sees the op-level
119
+ // lineage), then run `git log --follow` on the entry's current path if
120
+ // we can resolve it, catching renames across operations.
121
+ export function cmdHistory(wikiRoot, { entryId }) {
122
+ if (!entryId) {
123
+ process.stderr.write("history: <entry-id> is required\n");
124
+ return 1;
125
+ }
126
+ const opLog = readOpLog(wikiRoot);
127
+ process.stdout.write(`# Op-log entries mentioning ${entryId}\n\n`);
128
+ let opHits = 0;
129
+ for (const entry of opLog) {
130
+ const summary = (entry.summary || "").includes(entryId);
131
+ if (summary || entry.op_id.includes(entryId)) {
132
+ opHits++;
133
+ process.stdout.write(
134
+ `${entry.op_id} ${entry.operation} ${entry.finished}\n` +
135
+ ` ${entry.summary}\n\n`,
136
+ );
137
+ }
138
+ }
139
+ if (opHits === 0) {
140
+ process.stdout.write(" (no op-log entries match)\n\n");
141
+ }
142
+ process.stdout.write(`# Git history for files matching **/${entryId}.md\n\n`);
143
+ // `git log --follow` needs a path. We don't know the exact path — try
144
+ // the wildcard pattern and fall back to a ref-log search if empty.
145
+ const r = gitRun(wikiRoot, [
146
+ "log",
147
+ "--oneline",
148
+ "--follow",
149
+ "--",
150
+ `*${entryId}.md`,
151
+ ]);
152
+ if (r.stdout) process.stdout.write(r.stdout);
153
+ else process.stdout.write(" (no tracked file matches)\n");
154
+ return 0;
155
+ }