@cr8rcho/alkahest 0.1.90 → 0.1.92
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.
- package/README.md +1 -0
- package/dist/cli.js +10 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/presets.d.ts +13 -0
- package/dist/commands/presets.js +68 -1
- package/dist/commands/presets.js.map +1 -1
- package/dist/commands/update.js +16 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/core/presetUpdate.d.ts +65 -0
- package/dist/core/presetUpdate.js +289 -0
- package/dist/core/presetUpdate.js.map +1 -0
- package/dist/core/presets.d.ts +33 -0
- package/dist/core/presets.js +3 -3
- package/dist/core/presets.js.map +1 -1
- package/dist/mcp/server.js +7 -1
- package/dist/mcp/server.js.map +1 -1
- package/package.json +4 -2
- package/presets/as-built/CHANGES.md +33 -0
- package/presets/as-built/CLAUDE-snippet.md +2 -1
- package/presets/as-built/history/0fe9dc4fc94d413f +71 -0
- package/presets/as-built/history/2eb353c13db7e5a7 +130 -0
- package/presets/as-built/history/32be1c85c4b2757d +124 -0
- package/presets/as-built/history/5a9ef521edb13aa1 +71 -0
- package/presets/as-built/history/6e0b36b4d353b6b6 +72 -0
- package/presets/as-built/history/87497464ab8cfa7d +72 -0
- package/presets/as-built/history/8a9e3845f322e51c +56 -0
- package/presets/as-built/history/8e8f6eebcb4b2a58 +24 -0
- package/presets/as-built/history/96497831c905de56 +25 -0
- package/presets/as-built/history/bd84de1dd9759c12 +65 -0
- package/presets/as-built/history/ed625ebd1209c061 +24 -0
- package/presets/as-built/history/f431a6fcf54a4906 +123 -0
- package/presets/as-built/history.json +60 -0
- package/presets/as-built/sync-docs-maps.mjs +6 -0
- package/presets/llm-wiki/CHANGES.md +8 -0
- package/presets/llm-wiki/history/44e1874c74476cd7 +57 -0
- package/presets/llm-wiki/history/59cf7b5051fce7e3 +93 -0
- package/presets/llm-wiki/history/b5f313af919c6236 +69 -0
- package/presets/llm-wiki/history.json +22 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Reference implementation (alkahest as-built preset) — mirror this repo's docs into the
|
|
3
|
+
// project's note maps:
|
|
4
|
+
// docs/decisions/NNN-*.md → note map `adr` (flat)
|
|
5
|
+
// docs/{system,components,features,modules}/*.md → note map `as-built` (folder = category)
|
|
6
|
+
//
|
|
7
|
+
// Staging transforms (the importer's rules: filename = title, frontmatter passes through):
|
|
8
|
+
// - title from the first H1 ("ADR-NNN: rest — tail" → "ADR-NNN rest"; as-built strips a
|
|
9
|
+
// "System|Component|Feature|Module — " class prefix and a " — " tail), sanitized;
|
|
10
|
+
// - the H1 line is dropped from the body (the note title renders it);
|
|
11
|
+
// - relative .md links WITHIN each set become [[wikilinks]] so the graph connects
|
|
12
|
+
// (cross-set links stay plain markdown);
|
|
13
|
+
// - the ORIGINAL repo path rides in as `source_path:` frontmatter — the staged filename
|
|
14
|
+
// is title-derived, so this is what keeps identity through a retitle (the importer
|
|
15
|
+
// matches source_path before title and renames the note in place).
|
|
16
|
+
//
|
|
17
|
+
// Re-running is safe: import is idempotent by source_path, then title.
|
|
18
|
+
// This script belongs to the repo it lives in — adapt titles/sets/maps to local conventions.
|
|
19
|
+
// Usage: node scripts/sync-docs-maps.mjs [--stage-only]
|
|
20
|
+
import { readdirSync, readFileSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
import { join, dirname, basename } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
|
|
26
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
|
+
const DOCS = join(ROOT, "docs");
|
|
28
|
+
const OUT = join(tmpdir(), "alkahest-docs-staging");
|
|
29
|
+
rmSync(OUT, { recursive: true, force: true });
|
|
30
|
+
|
|
31
|
+
const sanitize = (t) =>
|
|
32
|
+
t.replace(/\(\//g, "(").replace(/[/\\:]/g, "-").replace(/`/g, "").replace(/\s+/g, " ").trim();
|
|
33
|
+
const firstH1 = (src) => (src.match(/^#\s+(.+)$/m) ?? [null, ""])[1].trim();
|
|
34
|
+
// Frontmatter rides through to the staged file — the importer strips it from the stored body
|
|
35
|
+
// and harvests its keys into note props (`tags:` is the reserved one). Split it off first so
|
|
36
|
+
// stripH1 still sees the H1 at the head of what's left.
|
|
37
|
+
const splitFm = (src) => {
|
|
38
|
+
const m = src.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n+/);
|
|
39
|
+
return m ? [m[0], src.slice(m[0].length)] : ["", src];
|
|
40
|
+
};
|
|
41
|
+
const stripH1 = (src) => {
|
|
42
|
+
const [fm, body] = splitFm(src);
|
|
43
|
+
return fm + body.replace(/^#\s+.+\n+/, "");
|
|
44
|
+
};
|
|
45
|
+
// Rename-safe identity: inject the ORIGINAL repo path as source_path: frontmatter (merged
|
|
46
|
+
// into an existing block, or a new block is minted).
|
|
47
|
+
const withSource = (staged, rel) => {
|
|
48
|
+
const [fm, body] = splitFm(staged);
|
|
49
|
+
const line = `source_path: ${rel}\n`;
|
|
50
|
+
return fm ? fm.replace(/---\r?\n*$/, (close) => line + close) + body : `---\n${line}---\n\n${staged}`;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ---- ADRs ------------------------------------------------------------------
|
|
54
|
+
const adrDir = join(DOCS, "decisions");
|
|
55
|
+
const adrFiles = readdirSync(adrDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
|
|
56
|
+
const adrTitle = {};
|
|
57
|
+
for (const f of adrFiles) {
|
|
58
|
+
const h1 = firstH1(readFileSync(join(adrDir, f), "utf8"));
|
|
59
|
+
// Convention "ADR-NNN: title — tail"; the em-dash variant "ADR-NNN — title" also parses.
|
|
60
|
+
const m = h1.match(/^ADR-(\d{3})(?::|\s+—)\s*(.+)$/);
|
|
61
|
+
const num = m ? m[1] : f.slice(0, 3);
|
|
62
|
+
adrTitle[num] = sanitize(`ADR-${num} ${(m ? m[2] : h1).split(" — ")[0].trim()}`);
|
|
63
|
+
}
|
|
64
|
+
const linkAdrs = (body) =>
|
|
65
|
+
body.replace(/\[([^\]]*)\]\((?:\.\/)?(\d{3})-[^)#\s]*\.md(?:#[^)]*)?\)/g, (all, _label, num) =>
|
|
66
|
+
adrTitle[num] ? `[[${adrTitle[num]}]]` : all);
|
|
67
|
+
mkdirSync(join(OUT, "adr"), { recursive: true });
|
|
68
|
+
for (const f of adrFiles) {
|
|
69
|
+
const t = adrTitle[f.slice(0, 3)];
|
|
70
|
+
const staged = linkAdrs(stripH1(readFileSync(join(adrDir, f), "utf8")));
|
|
71
|
+
writeFileSync(join(OUT, "adr", `${t}.md`), withSource(staged, `docs/decisions/${f}`));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---- as-built --------------------------------------------------------------
|
|
75
|
+
const cats = ["system", "components", "features", "modules"];
|
|
76
|
+
const abTitle = {};
|
|
77
|
+
const abSrc = {};
|
|
78
|
+
for (const cat of cats) {
|
|
79
|
+
for (const f of readdirSync(join(DOCS, cat)).filter((x) => x.endsWith(".md"))) {
|
|
80
|
+
const src = readFileSync(join(DOCS, cat, f), "utf8");
|
|
81
|
+
let t = firstH1(src).replace(/^(System|Component|Feature|Module)\s*—\s*/i, "");
|
|
82
|
+
t = t.split(" — ")[0].trim() || firstH1(src);
|
|
83
|
+
abTitle[basename(f, ".md")] = sanitize(t);
|
|
84
|
+
abSrc[basename(f, ".md")] = { cat, src };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const linkAb = (body) =>
|
|
88
|
+
body.replace(/\[([^\]]*)\]\((?:\.\.\/(?:system|components|features|modules)\/|\.\/)?([a-z0-9-]+)\.md(?:#[^)]*)?\)/g,
|
|
89
|
+
(all, _label, name) => (abTitle[name] ? `[[${abTitle[name]}]]` : all));
|
|
90
|
+
for (const [name, { cat, src }] of Object.entries(abSrc)) {
|
|
91
|
+
mkdirSync(join(OUT, "as-built", cat), { recursive: true });
|
|
92
|
+
writeFileSync(join(OUT, "as-built", cat, `${abTitle[name]}.md`), withSource(linkAb(stripH1(src)), `docs/${cat}/${name}.md`));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(`staged ${adrFiles.length} ADRs + ${Object.keys(abSrc).length} as-built docs → ${OUT}`);
|
|
96
|
+
if (process.argv.includes("--stage-only")) process.exit(0);
|
|
97
|
+
|
|
98
|
+
// A full mirror is many sequential POSTs, so a transient failure is a question of when, not
|
|
99
|
+
// if. The CLI handles it per file (keeps going, prints `✗ <file>: <message>`, exits 1) — act
|
|
100
|
+
// on the exit code and retry the whole set: rows that already landed come back as updates.
|
|
101
|
+
const ATTEMPTS = 3;
|
|
102
|
+
const failed = [];
|
|
103
|
+
|
|
104
|
+
for (const [dir, map] of [["adr", "adr"], ["as-built", "as-built"]]) {
|
|
105
|
+
let ok = false;
|
|
106
|
+
for (let attempt = 1; attempt <= ATTEMPTS && !ok; attempt++) {
|
|
107
|
+
console.log(`\n== notes import ${dir} → map ${map}${attempt > 1 ? ` (retry ${attempt - 1})` : ""} ==`);
|
|
108
|
+
const run = spawnSync("alkahest", ["notes", "import", join(OUT, dir), "--map", map, "--path", ROOT], {
|
|
109
|
+
stdio: "inherit",
|
|
110
|
+
});
|
|
111
|
+
if (run.error) console.error(`[sync] could not run alkahest: ${run.error.message}`);
|
|
112
|
+
ok = !run.error && run.status === 0;
|
|
113
|
+
if (!ok && attempt < ATTEMPTS) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
|
|
114
|
+
}
|
|
115
|
+
if (!ok) failed.push(`${dir} → ${map}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (failed.length) {
|
|
119
|
+
console.error(`\n[sync] FAILED after ${ATTEMPTS} attempts: ${failed.join(", ")}`);
|
|
120
|
+
console.error("[sync] scroll up for the per-file '✗ <file>: <message>' lines from the importer.");
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
console.log("\n[sync] both maps up to date.");
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": {
|
|
3
|
+
"skills/as-built-docs.md": [
|
|
4
|
+
{
|
|
5
|
+
"sha": "8a9e3845f322e51c",
|
|
6
|
+
"since": "0.1.72"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"sha": "5a9ef521edb13aa1",
|
|
10
|
+
"since": "0.1.74"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"sha": "0fe9dc4fc94d413f",
|
|
14
|
+
"since": "0.1.76"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"skills/adr.md": [
|
|
18
|
+
{
|
|
19
|
+
"sha": "bd84de1dd9759c12",
|
|
20
|
+
"since": "0.1.72"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"sha": "6e0b36b4d353b6b6",
|
|
24
|
+
"since": "0.1.75"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"sha": "87497464ab8cfa7d",
|
|
28
|
+
"since": "0.1.76"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"sync-docs-maps.mjs": [
|
|
32
|
+
{
|
|
33
|
+
"sha": "f431a6fcf54a4906",
|
|
34
|
+
"since": "0.1.72"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"sha": "32be1c85c4b2757d",
|
|
38
|
+
"since": "0.1.90"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"sha": "2eb353c13db7e5a7",
|
|
42
|
+
"since": "0.1.92"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"CLAUDE-snippet.md": [
|
|
46
|
+
{
|
|
47
|
+
"sha": "8e8f6eebcb4b2a58",
|
|
48
|
+
"since": "0.1.72"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"sha": "ed625ebd1209c061",
|
|
52
|
+
"since": "0.1.76"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"sha": "96497831c905de56",
|
|
56
|
+
"since": "0.1.91"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -119,6 +119,12 @@ for (const [dir, map] of [["adr", "adr"], ["as-built", "as-built"]]) {
|
|
|
119
119
|
if (failed.length) {
|
|
120
120
|
console.error(`\n[sync] FAILED after ${ATTEMPTS} attempts: ${failed.join(", ")}`);
|
|
121
121
|
console.error("[sync] scroll up for the per-file '✗ <file>: <message>' lines from the importer.");
|
|
122
|
+
console.error(`[sync] staged files kept for inspection: ${OUT}`);
|
|
122
123
|
process.exit(1);
|
|
123
124
|
}
|
|
125
|
+
// The staging dir is this run's own (mkdtempSync), so removing it touches no other sync.
|
|
126
|
+
// Kept on --stage-only (that's what it's for) and on failure (above). Imported here rather
|
|
127
|
+
// than at the top so `alkahest preset update` can merge this into copies that reorganised
|
|
128
|
+
// their import block.
|
|
129
|
+
(await import("node:fs")).rmSync(OUT, { recursive: true, force: true });
|
|
124
130
|
console.log("\n[sync] both maps up to date.");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# llm-wiki preset — changes
|
|
2
|
+
|
|
3
|
+
Why each shipped file changed, newest first. `alkahest preset update` prints the entries newer
|
|
4
|
+
than the version an installed copy came from. One `## <version>` per release that touched the
|
|
5
|
+
preset; each bullet names its file.
|
|
6
|
+
|
|
7
|
+
## 0.1.88
|
|
8
|
+
- First release of the preset.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# alkahest/wiki-capture — keep what you just learned
|
|
2
|
+
|
|
3
|
+
The user says *"save this"*, *"keep that"*, *"put this in the wiki"*, *"이거 정리해서 넣어줘"*,
|
|
4
|
+
*"기억해 둬"* — or a piece of work just finished and something durable came out of it. This
|
|
5
|
+
skill turns knowledge that surfaced **during a session** into wiki pages. (A source the
|
|
6
|
+
user hands you — an article, a URL, a transcript — is an *ingest*, see `alkahest/wiki`.)
|
|
7
|
+
Read `alkahest/wiki` once first: it holds the layout, the tools and the rules this skill
|
|
8
|
+
builds on.
|
|
9
|
+
|
|
10
|
+
## Two modes — detect from the call
|
|
11
|
+
|
|
12
|
+
- **Pointed capture** — the user gestured at one specific thing ("save the part about X",
|
|
13
|
+
an argument naming a topic). The worth-it decision is **already made by the user**; do
|
|
14
|
+
not re-litigate it. Scope to exactly what they pointed at and write **one focused
|
|
15
|
+
note** (or one update). Do not sweep the rest of the session.
|
|
16
|
+
- **Sweep** — no specific pointer ("save anything useful from this", end of a task).
|
|
17
|
+
Scan the session for **durable, reusable** knowledge against the bar below. May yield
|
|
18
|
+
several notes. If nothing meets the bar, say so in a sentence and stop — no filler.
|
|
19
|
+
|
|
20
|
+
When the pointer is ambiguous, ask one short question instead of guessing.
|
|
21
|
+
|
|
22
|
+
## The worth-it bar (sweep; in pointed mode only to reject the clearly unfit)
|
|
23
|
+
|
|
24
|
+
Keep: a decision and its rationale · a reusable concept, definition or pattern · a fact
|
|
25
|
+
about a company, tool, product, person or process · a synthesis distilled from research ·
|
|
26
|
+
a non-obvious how-to or operating rule · a good answer the user is likely to want again.
|
|
27
|
+
|
|
28
|
+
Never keep: throwaway conversation, one-off debugging steps, anything already in the wiki,
|
|
29
|
+
anything derivable from code or git history, secrets.
|
|
30
|
+
|
|
31
|
+
## Steps
|
|
32
|
+
|
|
33
|
+
1. **Dedupe first** — `search` / `notes q="<topic>"` (matches full bodies), `get_note` the
|
|
34
|
+
candidates. A page on the topic exists → **`update_note`** it. If the new knowledge
|
|
35
|
+
conflicts with it, don't overwrite silently: state the difference and prefer the newer
|
|
36
|
+
claim.
|
|
37
|
+
2. **Route** — pick the folder by what the knowledge *is*: `wiki/concepts` (a definition or
|
|
38
|
+
pattern), `wiki/entities` (a company / tool / person), `wiki/topics` (an ongoing
|
|
39
|
+
subject), `wiki/syntheses` (something distilled from several pages), `outputs/…` for a
|
|
40
|
+
deliverable. Company-internal knowledge and general knowledge may live on different
|
|
41
|
+
note maps — check `maps` and the existing pages, and pass `map` accordingly.
|
|
42
|
+
3. **Write** — one topic per note; a small markdown document with `##` sections
|
|
43
|
+
(Definition / Why it matters / How to apply, or Summary / Key points / Relevance —
|
|
44
|
+
whatever fits) and code where it carries the knowledge. Meaning-first title.
|
|
45
|
+
`props.tags` from the existing vocabulary (`note_props`), 1–3 tags.
|
|
46
|
+
4. **Connect** — name 1–3 closest neighbours as `[[Title]]` references in the body (a
|
|
47
|
+
`## Related` list is the convention). The graph derives the edges; a note with no
|
|
48
|
+
references is an island — if you truly found no neighbour, say so in the report.
|
|
49
|
+
5. **Report** — two lines: what was kept, and the viewer link of each created/updated
|
|
50
|
+
note (`https://alkahest.app/p/<project>/<map>/<note-slug>`), plus any conflict you
|
|
51
|
+
noted against an existing page.
|
|
52
|
+
|
|
53
|
+
## Not this skill's job
|
|
54
|
+
|
|
55
|
+
- **No deletions.** Capture updates or adds; retiring pages is `alkahest/wiki-lint`.
|
|
56
|
+
- **No index or log notes.** The map and the history are those.
|
|
57
|
+
- **No raw pages.** What the user hands you as a source goes through ingest.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# alkahest/wiki — an LLM-maintained knowledge wiki
|
|
2
|
+
|
|
3
|
+
You maintain a **knowledge wiki** for the user: a hosted alkahest project whose notes are
|
|
4
|
+
markdown documents the LLM writes and keeps current, and whose note map draws the graph.
|
|
5
|
+
The user curates sources, asks questions and reads; **you do the bookkeeping** — summaries,
|
|
6
|
+
cross-references, filing, consistency. Knowledge is compiled once and kept current, not
|
|
7
|
+
re-derived from raw material on every question.
|
|
8
|
+
|
|
9
|
+
The pattern is Andrej Karpathy's *LLM Wiki* idea file
|
|
10
|
+
(<https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f>). This skill is that
|
|
11
|
+
pattern instantiated on alkahest: the three layers and three loops are his; the note pool,
|
|
12
|
+
the graph and the tools below are where they land.
|
|
13
|
+
|
|
14
|
+
## The three layers, on alkahest
|
|
15
|
+
|
|
16
|
+
| Layer | Karpathy | Here |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| Raw sources | `raw/` — immutable | notes under folder **`raw/…`** (`raw/articles`, `raw/notes`, `raw/transcripts`). You read them; you **never edit or delete them**. |
|
|
19
|
+
| The wiki | `wiki/` — LLM-owned | notes under **`wiki/…`**: `sources` (one page per ingested source), `concepts`, `entities` (people, companies, products, tools), `topics`, `comparisons`, `syntheses`, `overviews`. |
|
|
20
|
+
| The schema | `CLAUDE.md` / `AGENTS.md` | **this skill** plus its siblings `alkahest/wiki-capture` and `alkahest/wiki-lint`. Local preferences (language, extra folders) go in a note `wiki/overviews/Conventions` — read it when it exists. |
|
|
21
|
+
|
|
22
|
+
Deliverables (briefs, decks, lint reports) go under **`outputs/…`** (`outputs/briefs`, `outputs/lint`).
|
|
23
|
+
|
|
24
|
+
Two of Karpathy's files have no counterpart here, on purpose — **do not create them**:
|
|
25
|
+
- `index.md` → the note map, the tree sidebar (folders) and server-side search (`search`, `notes q=`) are the catalog.
|
|
26
|
+
- `log.md` → note history (revisions) and the activity journal already record what changed and when.
|
|
27
|
+
|
|
28
|
+
## Tools
|
|
29
|
+
|
|
30
|
+
`notes` (list — excerpts, `q` for full-body search, `map`) · `get_note` (one document + its links and backlinks)
|
|
31
|
+
· `add_note` (`title`, `body`, `folder`, `props`, `map`) · `update_note` (`body`/`folder`/`props`; `delete`+`reason`
|
|
32
|
+
and `restore` — lint only) · `note_props` (the notebook's property schema) · `search` (notes + issues + tasks in one).
|
|
33
|
+
|
|
34
|
+
**Note-to-note connections are `[[Title]]` references in the body.** The graph derives them at
|
|
35
|
+
read time; there is no separate edge to draw. A note nobody references and that references
|
|
36
|
+
nothing is an island — always name at least one neighbour in the body.
|
|
37
|
+
|
|
38
|
+
Properties: use `props.tags` (string array) for cross-cutting labels; keep a `source`
|
|
39
|
+
property on `wiki/sources` pages pointing at the raw note (`[[Raw title]]`). Keep the tag
|
|
40
|
+
vocabulary small — a tag on 3–12 notes is a useful hub, a tag on 1 is noise.
|
|
41
|
+
|
|
42
|
+
## Loop 1 — Ingest (a new source arrives)
|
|
43
|
+
|
|
44
|
+
1. **File the raw source** as a note under `raw/<kind>` with the original text (or the
|
|
45
|
+
text you fetched from the URL), the URL and date in a short frontmatter-style header.
|
|
46
|
+
Preserve the source's language. `props.tags: ["raw"]`.
|
|
47
|
+
2. **Read it and discuss** the key takeaways with the user before writing — one source at
|
|
48
|
+
a time, the user stays involved. (A batch ingest is fine when they ask for it.)
|
|
49
|
+
3. **Write the source page** under `wiki/sources`: what it claims, why it matters, key
|
|
50
|
+
quotes, and a `## Related` section of `[[…]]` references. Meaning-first title, not the
|
|
51
|
+
raw file name.
|
|
52
|
+
4. **Update the wiki pages it touches** — `notes q=` first, then `update_note` existing
|
|
53
|
+
concept/entity/topic pages (add the new evidence, cite the source page) and `add_note`
|
|
54
|
+
only for concepts that have no page yet. A single source can rightly touch 5–15 pages.
|
|
55
|
+
5. **Never erase a claim the new source contradicts.** State the difference in the page
|
|
56
|
+
("Source A (2025) says X; Source B (2026) says Y") and prefer the newer claim when you
|
|
57
|
+
present current understanding.
|
|
58
|
+
6. End with the map link and a one-paragraph summary of what changed.
|
|
59
|
+
|
|
60
|
+
## Loop 2 — Query (the user asks a question)
|
|
61
|
+
|
|
62
|
+
1. `search` / `notes q=` first, then `get_note` the pages that matter — read the wiki, not
|
|
63
|
+
the raw sources, unless the wiki is silent.
|
|
64
|
+
2. Answer with citations as `[[Page title]]` references.
|
|
65
|
+
3. **File the good answers back.** A comparison, an analysis, a connection the user
|
|
66
|
+
asked for is knowledge — save it under `wiki/comparisons` or `wiki/syntheses` with its
|
|
67
|
+
sources referenced, so explorations compound like ingested sources do. Ask when unsure
|
|
68
|
+
whether an answer is worth keeping; a throwaway answer stays in chat.
|
|
69
|
+
|
|
70
|
+
## Loop 3 — Lint
|
|
71
|
+
|
|
72
|
+
Periodically health-check the wiki: `alkahest/wiki-lint` has the checks, the self-trigger
|
|
73
|
+
rule and the delete policy. Capturing knowledge that surfaced *during a session* (not from
|
|
74
|
+
a source) is `alkahest/wiki-capture`.
|
|
75
|
+
|
|
76
|
+
## Rules
|
|
77
|
+
|
|
78
|
+
- **One topic per note.** Prefer updating an existing page to adding a near-duplicate —
|
|
79
|
+
always `notes q=` before `add_note`.
|
|
80
|
+
- **Meaning-first titles.** The title is the node label and the `[[…]]` key other pages
|
|
81
|
+
cite; date-prefixed titles are for `raw/` only.
|
|
82
|
+
- **Bodies are small markdown documents** — `##` sections, a `## Related` list, code where
|
|
83
|
+
it carries the knowledge. Consistent beats comprehensive.
|
|
84
|
+
- **Language**: raw stays as it came; wiki pages follow the user's language (say it in
|
|
85
|
+
`wiki/overviews/Conventions` if it isn't obvious from the existing pages).
|
|
86
|
+
- **Deletion is lint's job**, never ingest's or query's.
|
|
87
|
+
|
|
88
|
+
## Getting this read
|
|
89
|
+
|
|
90
|
+
Hosted skills are read when something asks for them. Tell your agent once, in whatever
|
|
91
|
+
rules file it reads (`CLAUDE.md`, `AGENTS.md`, a user-level rules file):
|
|
92
|
+
*"My knowledge wiki is the alkahest project `<slug>` — read the `alkahest/wiki` skill over MCP
|
|
93
|
+
before touching it."* Or just say "read the alkahest/wiki skill" when you start a session.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# alkahest/wiki-lint — keep the wiki healthy
|
|
2
|
+
|
|
3
|
+
A periodic health check of the wiki: find what has drifted, fix what is mechanical, propose
|
|
4
|
+
what needs the user, and leave a report the next lint can read. Read `alkahest/wiki` first
|
|
5
|
+
for the layout and the tools.
|
|
6
|
+
|
|
7
|
+
## When to run
|
|
8
|
+
|
|
9
|
+
Run a lint when the user asks ("lint the wiki", "위키 정리해줘", "health check"), and
|
|
10
|
+
**offer one yourself** when either is true:
|
|
11
|
+
- roughly ten sources have been ingested since the last lint, or
|
|
12
|
+
- the newest note under `outputs/lint` is older than two weeks (or there is none).
|
|
13
|
+
|
|
14
|
+
(A real schedule — a weekly agent routine — is the user's choice; this skill only makes
|
|
15
|
+
the agent raise its hand.)
|
|
16
|
+
|
|
17
|
+
## Start with the Trash
|
|
18
|
+
|
|
19
|
+
`notes` hides trashed pages, but the last lint may have sent pages there that the user
|
|
20
|
+
has not reviewed. Before doing anything new, read the previous `outputs/lint` report's
|
|
21
|
+
"Sent to the Trash" section: anything still inside its 30-day window is a pending
|
|
22
|
+
decision — mention it, don't pile more on top without asking.
|
|
23
|
+
|
|
24
|
+
## Checks
|
|
25
|
+
|
|
26
|
+
Read the map with `notes` (excerpts; `full_bodies` only when the wiki is small), then:
|
|
27
|
+
|
|
28
|
+
| Check | What to look for | Mechanical fix |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| **Orphans** | pages with no `[[…]]` backlinks and none outgoing (`get_note` shows both) | connect: add a `## Related` reference from the closest hub page, or into the orphan |
|
|
31
|
+
| **Duplicates** | two pages on one topic (`notes q=` around suspicious titles) | merge into the better page, then trash the other |
|
|
32
|
+
| **Empty / stub pages** | title only, or a body under a couple of lines with no references | fill from the source pages, or trash |
|
|
33
|
+
| **Contradictions** | pages that disagree; a source page newer than the claim it contradicts | **write the difference into the page**, prefer the newer claim — never delete the older claim |
|
|
34
|
+
| **Stale claims** | dated statements superseded by later sources | same as above; add "as of <date>" |
|
|
35
|
+
| **Missing pages** | an entity or concept referenced as `[[…]]` from several pages but with no page of its own (unresolved references stay literal — they are the tell) | propose; create only when the sources on the map already support a page |
|
|
36
|
+
| **Missing cross-references** | pages that obviously belong together but don't cite each other | add the references |
|
|
37
|
+
| **Data gaps** | questions the wiki cannot answer that a web search or a new source could | list as follow-up research in the report |
|
|
38
|
+
|
|
39
|
+
## Delete policy — three rules
|
|
40
|
+
|
|
41
|
+
Deleting is `update_note` with `delete: true` and a **`reason`**. It is a soft delete: the
|
|
42
|
+
page goes to the project Trash, restorable for 30 days from the web Trash view or with
|
|
43
|
+
`update_note restore: true`, and only then is purged. The activity journal shows every
|
|
44
|
+
deletion with its reason. That safety net is why lint may delete at all — and it is the
|
|
45
|
+
whole safety net, so:
|
|
46
|
+
|
|
47
|
+
1. **The reason says *why it should go*, in one line the user can judge without opening
|
|
48
|
+
the page** — `duplicate of [[X]], content merged`, `empty stub, topic covered by [[Y]]`,
|
|
49
|
+
`superseded by [[Z]] — lint 2026-09-19`. Never `cleanup`.
|
|
50
|
+
2. **`raw/` is never deleted or edited.** It is the source of truth the wiki is compiled
|
|
51
|
+
from; a raw page's only lint outcome is "unlinked — no source page yet".
|
|
52
|
+
3. **Contradictions and stale claims are written, not deleted.** "We used to think X" is
|
|
53
|
+
knowledge. Orphans are connected, not deleted.
|
|
54
|
+
|
|
55
|
+
On a shared wiki, a page created by someone else is **proposed, not trashed** — list it in
|
|
56
|
+
the report with the suggested action.
|
|
57
|
+
|
|
58
|
+
## The report
|
|
59
|
+
|
|
60
|
+
Write `outputs/lint/<YYYY-MM-DD> wiki lint` (`props.tags: ["lint"]`) with:
|
|
61
|
+
|
|
62
|
+
- **Fixed** — what you connected, merged, filled, annotated (page links)
|
|
63
|
+
- **Sent to the Trash** — each page with its reason, and one line on how to restore
|
|
64
|
+
- **Proposed** — what needs the user: pages to create, contradictions to resolve, other
|
|
65
|
+
people's pages that look retirable
|
|
66
|
+
- **Follow-up research** — the questions the wiki cannot answer yet
|
|
67
|
+
- **Counts** — pages / orphans / trashed, so the next lint sees the trend
|
|
68
|
+
|
|
69
|
+
Tell the user the report's link and the two or three items that most need their eye.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": {
|
|
3
|
+
"skills/wiki.md": [
|
|
4
|
+
{
|
|
5
|
+
"sha": "59cf7b5051fce7e3",
|
|
6
|
+
"since": "0.1.88"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"skills/wiki-capture.md": [
|
|
10
|
+
{
|
|
11
|
+
"sha": "44e1874c74476cd7",
|
|
12
|
+
"since": "0.1.88"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"skills/wiki-lint.md": [
|
|
16
|
+
{
|
|
17
|
+
"sha": "b5f313af919c6236",
|
|
18
|
+
"since": "0.1.88"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|