@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,93 @@
1
+ // remote.mjs — `skill-llm-wiki remote <wiki> <subcommand> [args]`
2
+ //
3
+ // Thin wrapper around git's remote management, routed through the
4
+ // private repo's isolation env. Subcommands:
5
+ //
6
+ // add <name> <url> register a new remote
7
+ // remove <name> delete a configured remote
8
+ // list list all remotes with (fetch, push) URLs
9
+ //
10
+ // The skill never talks to a remote on its own. `remote add` just
11
+ // records the URL in the private repo's config; nothing is fetched,
12
+ // pushed, or authenticated here. The user invokes `skill-llm-wiki
13
+ // sync <wiki>` explicitly to actually exchange objects with the
14
+ // remote.
15
+ //
16
+ // All operations fail loudly on collision, missing args, or git
17
+ // errors — there is no silent "remote already exists, overwrite"
18
+ // behavior. If a caller needs idempotent add-or-replace they do
19
+ // `remove` then `add`.
20
+
21
+ import {
22
+ gitRemoteAdd,
23
+ gitRemoteList,
24
+ gitRemoteRemove,
25
+ redactUrl,
26
+ } from "../lib/git.mjs";
27
+
28
+ const REMOTE_SUBCOMMANDS = new Set(["add", "remove", "list"]);
29
+
30
+ export function cmdRemote(wikiRoot, { subcommand, args = [] }) {
31
+ if (!wikiRoot) {
32
+ process.stderr.write("remote: <wiki> is required\n");
33
+ return 1;
34
+ }
35
+ if (!subcommand || !REMOTE_SUBCOMMANDS.has(subcommand)) {
36
+ process.stderr.write(
37
+ `remote: subcommand must be one of ${[...REMOTE_SUBCOMMANDS].join(", ")}; got "${subcommand}"\n`,
38
+ );
39
+ return 1;
40
+ }
41
+ try {
42
+ switch (subcommand) {
43
+ case "add": {
44
+ const [name, url] = args;
45
+ // Defence in depth: reject whitespace-only or empty after
46
+ // trim so we never hand garbage to git.
47
+ if (!name || !name.trim() || !url || !url.trim()) {
48
+ process.stderr.write(
49
+ "remote add: <name> <url> are both required and non-empty\n",
50
+ );
51
+ return 1;
52
+ }
53
+ gitRemoteAdd(wikiRoot, name.trim(), url.trim());
54
+ // Always redact the URL before echoing — https URLs with
55
+ // embedded credentials like `https://token@host/repo.git`
56
+ // are common and must never surface on stdout.
57
+ process.stdout.write(
58
+ `remote ${name.trim()} added (${redactUrl(url.trim())})\n`,
59
+ );
60
+ return 0;
61
+ }
62
+ case "remove": {
63
+ const [name] = args;
64
+ if (!name) {
65
+ process.stderr.write("remote remove: <name> is required\n");
66
+ return 1;
67
+ }
68
+ gitRemoteRemove(wikiRoot, name);
69
+ process.stdout.write(`remote ${name} removed\n`);
70
+ return 0;
71
+ }
72
+ case "list": {
73
+ const remotes = gitRemoteList(wikiRoot);
74
+ if (remotes.length === 0) {
75
+ process.stdout.write("(no remotes configured)\n");
76
+ return 0;
77
+ }
78
+ for (const r of remotes) {
79
+ process.stdout.write(
80
+ `${r.name}\n` +
81
+ ` fetch: ${redactUrl(r.fetch ?? "(none)")}\n` +
82
+ ` push: ${redactUrl(r.push ?? "(none)")}\n`,
83
+ );
84
+ }
85
+ return 0;
86
+ }
87
+ }
88
+ } catch (err) {
89
+ process.stderr.write(`remote ${subcommand}: ${err.message}\n`);
90
+ return 1;
91
+ }
92
+ return 0;
93
+ }
@@ -0,0 +1,253 @@
1
+ // review.mjs — `skill-llm-wiki rebuild <wiki> --review`
2
+ //
3
+ // After the orchestrator's operator-convergence phase produces its
4
+ // per-iteration commits but before validation + commit-finalize
5
+ // run, the review flow:
6
+ //
7
+ // 1. Reads `git diff --stat pre-op/<id>..HEAD` and prints the
8
+ // file-level summary of what the operators touched.
9
+ // 2. Reads `git log --oneline pre-op/<id>..HEAD` and prints the
10
+ // per-iteration commit list.
11
+ // 3. Prompts the user: approve / abort / drop <iteration-N>.
12
+ //
13
+ // Approve: orchestrator proceeds to validation + commit-finalize
14
+ // as normal.
15
+ // Abort: `git reset --hard pre-op/<id>` + `git clean -fd`, roll
16
+ // everything back, exit with code 2 so the caller knows
17
+ // the op didn't land.
18
+ // Drop N: `git revert --no-edit <sha-of-iteration-N>` produces an
19
+ // inverse commit for iteration N directly in git history,
20
+ // then the loop re-prompts so the user can drop more
21
+ // iterations one at a time. The final outcome is
22
+ // `"approve"` with a populated `dropped[]` array; the
23
+ // revert commits are already in history by the time the
24
+ // orchestrator sees the result.
25
+ //
26
+ // The prompt is gated on `isInteractive()`. In non-interactive
27
+ // mode the review flow is a no-op that returns immediately, and
28
+ // the orchestrator proceeds as if `--review` hadn't been passed.
29
+
30
+ import {
31
+ gitClean,
32
+ gitResetHard,
33
+ gitRun,
34
+ gitRunChecked,
35
+ } from "../lib/git.mjs";
36
+ import { choose, isInteractive, NonInteractiveError } from "../lib/interactive.mjs";
37
+
38
+ export const REVIEW_APPROVE = "approve";
39
+ export const REVIEW_ABORT = "abort";
40
+ export const REVIEW_DROP = "drop";
41
+
42
+ // Pure logic: given the current commit list between pre-tag and HEAD,
43
+ // and the user's choice, produce an action record. Exported so tests
44
+ // can drive every branch without spawning git.
45
+ export function planReviewAction(choice, pendingCommits) {
46
+ if (choice === REVIEW_APPROVE) {
47
+ return { action: "approve" };
48
+ }
49
+ if (choice === REVIEW_ABORT) {
50
+ return { action: "abort" };
51
+ }
52
+ if (typeof choice === "string" && choice.startsWith("drop:")) {
53
+ const rest = choice.slice("drop:".length).trim();
54
+ if (rest === "") {
55
+ return {
56
+ action: "error",
57
+ error:
58
+ "drop: commit identifier missing (use drop:<sha> or drop:<subject-substring>)",
59
+ };
60
+ }
61
+ // Exact sha match always wins, even when an earlier commit's
62
+ // subject happens to contain the sha prefix as a substring.
63
+ const exact = pendingCommits.find((c) => c.sha === rest);
64
+ if (exact) {
65
+ return { action: "drop", commit: exact };
66
+ }
67
+ const matches = pendingCommits.filter((c) => c.subject.includes(rest));
68
+ if (matches.length === 0) {
69
+ return { action: "error", error: `no commit matches "${rest}"` };
70
+ }
71
+ if (matches.length > 1) {
72
+ const shas = matches.map((c) => c.sha.slice(0, 10)).join(", ");
73
+ return {
74
+ action: "error",
75
+ error: `ambiguous drop: "${rest}" matches ${matches.length} commits (${shas}) — use a full sha`,
76
+ };
77
+ }
78
+ return { action: "drop", commit: matches[0] };
79
+ }
80
+ return { action: "error", error: `unknown review choice: ${choice}` };
81
+ }
82
+
83
+ // Read the commit list between `pre-op/<opId>` and HEAD as an array
84
+ // of { sha, subject } records. Used by the review prompt and tests.
85
+ export function readPendingCommits(wikiRoot, opId) {
86
+ const r = gitRun(wikiRoot, [
87
+ "log",
88
+ "--oneline",
89
+ "--no-decorate",
90
+ "--format=%H\t%s",
91
+ `pre-op/${opId}..HEAD`,
92
+ ]);
93
+ if (r.status !== 0) {
94
+ throw new Error(
95
+ `review: git log pre-op/${opId}..HEAD exited ${r.status}: ${r.stderr.trim()}`,
96
+ );
97
+ }
98
+ const lines = r.stdout.trim().split(/\r?\n/).filter((l) => l.length > 0);
99
+ return lines.map((l) => {
100
+ const [sha, ...rest] = l.split("\t");
101
+ return { sha, subject: rest.join("\t") };
102
+ });
103
+ }
104
+
105
+ // Print the stat + commit list to stdout. Tests capture stdout to
106
+ // verify the output shape.
107
+ export function printReviewSummary(wikiRoot, opId) {
108
+ process.stdout.write(
109
+ `\n=== Review pending changes for op ${opId} ===\n\n`,
110
+ );
111
+ const diff = gitRun(wikiRoot, [
112
+ "diff",
113
+ "--stat",
114
+ "--find-renames",
115
+ `pre-op/${opId}..HEAD`,
116
+ ]);
117
+ process.stdout.write(diff.stdout || "(no diff)\n");
118
+ process.stdout.write("\nCommits (oldest first):\n");
119
+ const commits = readPendingCommits(wikiRoot, opId).reverse();
120
+ for (let i = 0; i < commits.length; i++) {
121
+ process.stdout.write(
122
+ ` ${i + 1}. ${commits[i].sha.slice(0, 10)} ${commits[i].subject}\n`,
123
+ );
124
+ }
125
+ process.stdout.write("\n");
126
+ return commits;
127
+ }
128
+
129
+ // Interactive prompt that returns one of REVIEW_APPROVE /
130
+ // REVIEW_ABORT / `drop:<n>`. Uses `choose()` from interactive.mjs
131
+ // so non-TTY mode throws NonInteractiveError that the caller
132
+ // translates into a pass-through (just approve the op).
133
+ export async function promptReviewChoice(commits, opts = {}) {
134
+ if (!isInteractive(opts)) {
135
+ throw new NonInteractiveError("review: non-interactive");
136
+ }
137
+ const options = [
138
+ { label: "approve — proceed to validation + commit-finalize", value: REVIEW_APPROVE },
139
+ { label: "abort — roll back to pre-op state", value: REVIEW_ABORT },
140
+ ];
141
+ for (let i = 0; i < commits.length; i++) {
142
+ options.push({
143
+ label: `drop ${i + 1} (${commits[i].subject})`,
144
+ value: `drop:${commits[i].sha}`,
145
+ });
146
+ }
147
+ return choose("What would you like to do with this review?", options, opts);
148
+ }
149
+
150
+ // Apply an abort: reset working tree to pre-op.
151
+ export function applyAbort(wikiRoot, opId) {
152
+ gitResetHard(wikiRoot, `pre-op/${opId}`);
153
+ gitClean(wikiRoot);
154
+ }
155
+
156
+ // Apply a drop: `git revert --no-edit <sha>` to produce an inverse
157
+ // commit for the dropped iteration. On a clean revert, returns
158
+ // `{ ok: true }`. On conflict (common for non-adjacent commits
159
+ // that touch the same files), calls `git revert --abort` to clean
160
+ // up the working tree and returns `{ ok: false, conflict: true,
161
+ // stderr }` so the review loop can re-prompt without leaving merge
162
+ // markers on disk. Throws only for true errors — the conflict
163
+ // path is normal and recoverable.
164
+ export function applyDrop(wikiRoot, commit) {
165
+ const r = gitRun(wikiRoot, ["revert", "--no-edit", commit.sha]);
166
+ if (r.status === 0) {
167
+ return { ok: true };
168
+ }
169
+ // Non-zero exit is typically a conflict. Abort the in-progress
170
+ // revert so the working tree is consistent, then report.
171
+ try {
172
+ gitRunChecked(wikiRoot, ["revert", "--abort"]);
173
+ } catch {
174
+ /* best effort; caller will reset on full abort */
175
+ }
176
+ return { ok: false, conflict: true, stderr: r.stderr.trim() };
177
+ }
178
+
179
+ // Full review cycle: print summary, prompt, apply. Loops until the
180
+ // user approves, aborts, or hits an error — so the user can drop
181
+ // multiple iterations one at a time. Each drop produces a revert
182
+ // commit and re-prints the updated summary before re-prompting.
183
+ //
184
+ // Returns one of:
185
+ // { outcome: "approve" }
186
+ // { outcome: "abort" }
187
+ // { outcome: "dropped", commits: [...] } ← all dropped commits
188
+ // { outcome: "non-interactive" }
189
+ //
190
+ // Tests may inject a `promptFn` seam to avoid a real TTY. For
191
+ // multi-drop scenarios the injected promptFn is called once per
192
+ // iteration of the loop and returns the next user decision.
193
+ export async function runReviewCycle(wikiRoot, opId, opts = {}) {
194
+ const {
195
+ promptFn = promptReviewChoice,
196
+ forceInteractive = false,
197
+ maxIterations = 32,
198
+ } = opts;
199
+ // `isInteractive` already honours `forceInteractive` — pulling
200
+ // that check out separately is dead code.
201
+ if (!isInteractive({ forceInteractive })) {
202
+ return { outcome: "non-interactive" };
203
+ }
204
+ const dropped = [];
205
+ for (let iter = 0; iter < maxIterations; iter++) {
206
+ const commits = printReviewSummary(wikiRoot, opId);
207
+ if (commits.length === 0) {
208
+ process.stdout.write("(no pending commits to review — approving)\n");
209
+ return { outcome: "approve" };
210
+ }
211
+ let choice;
212
+ try {
213
+ choice = await promptFn(commits, { forceInteractive });
214
+ } catch (err) {
215
+ if (err instanceof NonInteractiveError) {
216
+ return { outcome: "non-interactive" };
217
+ }
218
+ throw err;
219
+ }
220
+ const plan = planReviewAction(choice, commits);
221
+ switch (plan.action) {
222
+ case "approve":
223
+ return dropped.length > 0
224
+ ? { outcome: "approve", dropped }
225
+ : { outcome: "approve" };
226
+ case "abort":
227
+ applyAbort(wikiRoot, opId);
228
+ return { outcome: "abort" };
229
+ case "drop": {
230
+ const dropResult = applyDrop(wikiRoot, plan.commit);
231
+ if (dropResult.conflict) {
232
+ // Clean abort of the in-progress revert so the working
233
+ // tree is consistent before we re-prompt.
234
+ process.stderr.write(
235
+ `review: drop of ${plan.commit.sha.slice(0, 10)} conflicts with later commits ` +
236
+ "(git revert aborted). Pick a different iteration, " +
237
+ "approve to keep everything, or abort to roll back.\n",
238
+ );
239
+ continue;
240
+ }
241
+ dropped.push(plan.commit);
242
+ continue;
243
+ }
244
+ case "error":
245
+ default:
246
+ process.stderr.write(`review: ${plan.error}\n`);
247
+ continue;
248
+ }
249
+ }
250
+ throw new Error(
251
+ `review: exceeded maxIterations=${maxIterations} without reaching approve/abort`,
252
+ );
253
+ }
@@ -0,0 +1,84 @@
1
+ // sync.mjs — `skill-llm-wiki sync <wiki> [--remote <name>]
2
+ // [--push-branch <ref>]`
3
+ //
4
+ // Explicit, user-invoked object exchange with a configured remote.
5
+ // Fetches tags from the remote first (read side), then pushes the
6
+ // private repo's `op/*` and `pre-op/*` tags (write side). Nothing is
7
+ // ever pushed automatically — `sync` is always a conscious user
8
+ // action and the only way to propagate history to a shared location.
9
+ //
10
+ // By default the push refspec is `refs/tags/op/*` + `refs/tags/
11
+ // pre-op/*`, so the remote receives a read-only history mirror
12
+ // without any competing `main` branch head. A user who genuinely
13
+ // wants to push a branch passes `--push-branch <name>` and takes
14
+ // responsibility for that refspec.
15
+
16
+ import { gitFetch, gitPush, gitRemoteList } from "../lib/git.mjs";
17
+
18
+ // A branch name that `git` and our refspec interpolation will accept
19
+ // without surprise. Refs containing `:`, `+`, `*`, or leading `-` can
20
+ // change push semantics (force-push, refspec patterns, flag smuggling),
21
+ // so we refuse them at the gate. Matches the conservative subset of
22
+ // `git check-ref-format`'s rules that we care about. Phase 8 security
23
+ // sweep finding D7.
24
+ const SAFE_BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
25
+
26
+ export function cmdSync(wikiRoot, opts = {}) {
27
+ const { remote = "origin", pushBranch = null, skipFetch = false, skipPush = false } = opts;
28
+ if (!wikiRoot) {
29
+ process.stderr.write("sync: <wiki> is required\n");
30
+ return 1;
31
+ }
32
+ if (pushBranch !== null && !SAFE_BRANCH_RE.test(pushBranch)) {
33
+ process.stderr.write(
34
+ `sync: invalid --push-branch "${pushBranch}" — branch must match ` +
35
+ `[A-Za-z0-9][A-Za-z0-9._/-]*; refusing to build an unsafe refspec.\n`,
36
+ );
37
+ return 1;
38
+ }
39
+ // Verify the remote exists before we start. A typo or missing
40
+ // remote should be a loud "unknown remote" error, not a
41
+ // surprising git fatal from deep in the fetch path.
42
+ let remotes;
43
+ try {
44
+ remotes = gitRemoteList(wikiRoot);
45
+ } catch (err) {
46
+ process.stderr.write(`sync: could not list remotes: ${err.message}\n`);
47
+ return 1;
48
+ }
49
+ const known = new Set(remotes.map((r) => r.name));
50
+ if (!known.has(remote)) {
51
+ process.stderr.write(
52
+ `sync: unknown remote "${remote}" (configured: ${[...known].join(", ") || "none"})\n` +
53
+ " add one first: skill-llm-wiki remote <wiki> add <name> <url>\n",
54
+ );
55
+ return 1;
56
+ }
57
+
58
+ if (!skipFetch) {
59
+ try {
60
+ gitFetch(wikiRoot, remote);
61
+ process.stdout.write(`sync: fetched from ${remote}\n`);
62
+ } catch (err) {
63
+ process.stderr.write(`sync: fetch failed: ${err.message}\n`);
64
+ return 1;
65
+ }
66
+ }
67
+
68
+ if (!skipPush) {
69
+ const refspecs = pushBranch
70
+ ? ["refs/tags/op/*", "refs/tags/pre-op/*", `refs/heads/${pushBranch}`]
71
+ : ["refs/tags/op/*", "refs/tags/pre-op/*"];
72
+ try {
73
+ gitPush(wikiRoot, remote, { refspecs });
74
+ process.stdout.write(
75
+ `sync: pushed ${refspecs.join(", ")} to ${remote}\n`,
76
+ );
77
+ } catch (err) {
78
+ process.stderr.write(`sync: push failed: ${err.message}\n`);
79
+ return 1;
80
+ }
81
+ }
82
+
83
+ return 0;
84
+ }