Agents configuration

This commit is contained in:
Michał Kopeć 2026-07-15 09:55:47 +02:00
parent c56348b15b
commit 3dabfa0c0b
12 changed files with 1081 additions and 292 deletions

View file

@ -0,0 +1,170 @@
---
name: cascade-kb-init
description: Bootstrap a brand-new Cascade Knowledge Base - the same directory structure, AGENTS.md/CLAUDE.md system prompt, and empty wiki/ scaffold as this project - inside a target folder (typically empty, or a new project that doesn't have one yet). Use when the user asks to "set up a new wiki like this one", "initialize a new cascade KB", "bootstrap a wiki here", "create a knowledge base with this schema", or calls it a "wiki initializer". Do not confuse with a generic `init` skill that documents an existing codebase - this one creates the Cascade KB pattern itself, empty, ready for its first ingest.
---
# Cascade KB init skill
## Purpose
Copy this project's Cascade Knowledge Base *schema* - not its content - into
a new target folder: the directory structure, the `AGENTS.md`/`CLAUDE.md`
system prompt that defines how the KB behaves, and the empty `wiki/`
scaffold (routing table, overview, log, error book, entity/graph indexes).
The result is a new, empty KB that behaves exactly like this one, ready for
its first `raw/inbox/` drop and "Ingest."
This is a one-way copy from this repo's own `AGENTS.md`/`wiki/` template
files into a different folder. It never reads or writes anything in this
repo's `raw/`, `wiki/entities/`, `wiki/graph/edges.json`, or `outputs/` -
those hold this project's actual accumulated knowledge, which is exactly
what should *not* travel into a fresh KB.
## Trigger phrases
- "set up a new wiki like this one" / "initialize a new cascade KB"
- "bootstrap a wiki here" / "create a knowledge base with this schema"
- "wiki initializer" / "clone this KB structure into a new project"
## How to run this skill
### Step 1 - Confirm the target folder
Ask (if not already given): "Which folder should I initialize the new
knowledge base in?" Resolve to an absolute path. This is a filesystem
action outside the current repo, so confirm the resolved path back to the
user before writing anything - do not assume a relative path means
"somewhere under the current project."
If the folder doesn't exist yet, create it after confirming the path. If it
exists, check its contents before doing anything else (Step 2).
### Step 2 - Don't clobber an existing KB or unrelated project
If the target already contains a `wiki/` directory, or an `AGENTS.md` /
`CLAUDE.md`, stop and ask: "This folder already looks like it has a
knowledge base (found `<what>`). Initializing here could overwrite it. Do
you want to proceed anyway, pick a different folder, or only add whatever
scaffold pieces are missing?" Never silently overwrite an existing
`AGENTS.md` or populated `wiki/` tree.
If the target has other, unrelated files (e.g. it's an existing code
project without a KB yet) - that's fine, proceed; the scaffold is added
alongside them. Note any top-level name collisions (e.g. an existing
`wiki/` folder used for something else) and ask before touching those
specifically.
### Step 3 - Default skill set (no need to ask)
The bare scaffold (directory structure + `AGENTS.md`/`CLAUDE.md` + empty
`wiki/` templates) is always included, and so is the reusable KB skill set
- these operate purely on the `wiki/` structure, so they carry over
cleanly and are part of "the schema" as far as this skill is concerned:
`export-okf`, `export-starlight`, `sync-changes`, `extract-transcript`,
`project-summary`. Don't ask about these - just include them.
`ghost-writer` and `clouddrift-docx` are not part of the default set (a
general writing tool and a brand-specific export skill respectively, not
KB-schema-native) - only include either if the user explicitly asks for it,
e.g. "also bring over ghost-writer."
Record the final skill list (default five, plus anything explicitly added)
- this affects Steps 6 and 7.
### Step 4 - Create the directory structure
Under the target folder, create:
```
libs/
linked/
outputs/
raw/inbox/
raw/archive/
tmp/
wiki/entities/
wiki/graph/
workload/
```
`libs/`, `linked/`, and `tmp/` are gitignored per the schema (Step 6) and
stay empty. `raw/inbox/`, `raw/archive/`, and `workload/` are meant to be
tracked but start empty - add a placeholder `.gitkeep` file to each so they
survive a fresh `git init` + first commit rather than vanishing as empty
directories.
### Step 5 - Write `AGENTS.md` and the `CLAUDE.md` symlink
Copy this repo's `AGENTS.md` verbatim into the target as `AGENTS.md` - it
is already fully generic (no project-specific content; it *is* the
schema definition). Then create `CLAUDE.md` in the target as a symlink to
`AGENTS.md`, matching this repo's own convention (one source of truth,
readable under either filename).
### Step 6 - Write the empty `wiki/` scaffold
Create these files in the target, using this repo's current versions as
the template and stripping every reference to this project's actual
content (Grant Thornton, Cloud Drift, specific entities, etc.) down to the
generic structure:
- **`wiki/index.md`** - frontmatter with `kb_schema_version: "1.1"` only.
Body: the routing table with just its four fixed infrastructure rows
(Overview, Log, Error Book, Entities, Graph) and no entity rows, plus the
"## Entity Pages" section with its placeholder note. Use today's date
where the template needs one.
- **`wiki/overview.md`** - copy verbatim from this repo (it's already
generic - directory tree, cascade priority, frontmatter summary, no
project content). Set `last_updated` to today.
- **`wiki/log.md`** - header and explanation only, no entries.
- **`wiki/error-book.md`** - copy verbatim (already generic: empty table
+ placeholder note). Set `last_updated` to today.
- **`wiki/entities/index.md`** - header + placeholder note, no entries.
- **`wiki/graph/index.md`** - header + pointer to `edges.json`, with a
generic "Current graph coverage: (none yet)" line instead of this
repo's specific bullet list.
- **`wiki/graph/edges.json`** - `{"version": 1, "last_updated": "<today>", "edges": []}`.
Do not carry over any entity pages, graph edges, log entries, or overview
content specific to this project - the whole point is an empty KB with the
same shape.
### Step 7 - Write `.gitignore`
Base rules (always): `libs/`, `linked/`, `tmp/`, `.DS_Store`. Since
`export-starlight` and `export-okf` are in the default skill set (Step 3),
also always add `outputs/starlight` and `outputs/okf` - both exist to be
gitignored precisely because those two skills are present by default.
### Step 8 - Copy the skill set from Step 3
Copy each skill's folder from this repo's `.agents/skills/<name>/` into the
target's `.agents/skills/<name>/` unchanged - the default five, plus
anything explicitly added. Then create `.claude/skills` in the target as a
symlink to `../.agents/skills`, matching this repo's convention - do this
once, after copying the whole set, not per-skill.
### Step 9 - Report
Tell the user:
- The resolved target path.
- The directory tree created.
- Whether `AGENTS.md`/`CLAUDE.md` were written or (per Step 2) skipped/merged.
- Which skills were copied (the default five, plus anything explicitly added).
- Next step: "Drop material into `raw/inbox/` and say 'Ingest' to populate the wiki for the first time."
## Edge cases
- **Target is the current repo itself, or a parent/ancestor of it** - refuse
and explain why: this would either overwrite the live KB you're running
from, or nest a KB inside itself. Ask for a genuinely separate path.
- **Target is on a different filesystem/drive or requires elevated
permissions** - if directory creation fails, report the exact error
rather than retrying with escalated permissions.
- **User wants only *some* of the wiki template files** (e.g. just the
directory structure, no `AGENTS.md`) - honor that; the steps above are
the default full scaffold, not an all-or-nothing bundle.
- **This repo's own `AGENTS.md` or template `wiki/` files have since
drifted from each other** (e.g. one mentions a directory the other
doesn't) - fix the drift in *this* repo first if noticed, then copy the
corrected version. Don't propagate a known inconsistency into a new KB.

View file

@ -0,0 +1,147 @@
---
name: export-starlight
description: Export the local wiki/ knowledge base into an Astro + Starlight-consumable form at outputs/starlight/, producing a human-readable documentation website. Use when the user asks to "export the wiki to Starlight", "generate the docs site", "export as Astro Starlight", "build a human-readable wiki site", or "publish the knowledge base as a website".
---
# Export to Astro Starlight skill
## Purpose
Regenerate `outputs/starlight/` from the current `wiki/` tree so it can be
built and served as an Astro + Starlight documentation website — a
human-readable, browsable version of this knowledge base (unlike
`export-okf`, which targets machine/tool consumption). This is a one-way,
on-demand export — `wiki/` stays the authoritative source; `outputs/starlight/`
is always a derived artifact of it, never edited by hand and never fed back
in.
This skill only runs when explicitly invoked — it is deliberately not part
of the always-loaded `CLAUDE.md`/`AGENTS.md` Ingest/Lint workflows.
The heavy lifting (frontmatter remapping, link rewriting, asset copying,
sidebar generation) is done by a deterministic Python script, not by
reading and rewriting every page by hand — the wiki now has 60+ pages, and
a mechanical transform like this belongs in code, not in per-page model
reasoning. Nothing in this skill needs an LLM to run correctly; the model's
job is just to ask the two setup questions below, invoke the script, and
relay its report.
## Trigger phrases
Use this skill when the user says things like:
- "export the wiki to Starlight" / "export as Astro Starlight"
- "generate the docs site" / "build the Starlight site"
- "publish the knowledge base as a website"
- "sync outputs/starlight"
## How to run this skill
### Step 1 — Ask the two setup questions, every time
The user has explicitly said they want to choose these on every invocation
— do not assume a default or reuse an answer from a previous run. Use
`AskUserQuestion` with:
1. **Export scope:**
- "Full runnable scaffold" → `--mode full` — regenerates the whole
`outputs/starlight/` project (package.json, astro.config.mjs with
sidebar, tsconfig.json, src/content.config.ts, splash `index.mdx`)
plus all content. Fully deletes and rebuilds `outputs/starlight/`.
- "Content only" → `--mode content-only` — regenerates only
`src/content/docs/**`, `public/assets/**`, and a `SIDEBAR-SNIPPET.md`
with the sidebar config to paste in. Leaves any existing scaffold
files (package.json, astro.config.mjs, tsconfig.json,
content.config.ts, index.mdx) untouched — use this if the user is
dropping the output into a Starlight project they already maintain
elsewhere.
2. **Meta pages:**
- "Include them" → add `--include-meta` — publishes `wiki/log.md` as a
Change Log page and `wiki/error-book.md` as an Error Book page.
- "Exclude them" → omit the flag — only entities, sources, graph, and
overview get published.
### Step 2 — Run the script
From the repository root:
```bash
python3 "<skill-dir>/scripts/export_starlight.py" --mode <full|content-only> [--include-meta]
```
Resolve `<skill-dir>` to this skill's own directory. Optional extra flag:
`--site-title "Custom Title"` overrides the auto-derived site title (which
otherwise comes from `wiki/overview.md`'s H1 — a serviceable but generic
default like "Knowledge Base Overview"). Consider offering this to the user
rather than always silently accepting the default, since it's the one
piece of branding a reader sees first.
### Step 3 — Relay the script's report
The script prints, and you should summarize back to the user:
- Mode used and page count exported.
- Assets (images) copied.
- Which meta pages were skipped (if `--include-meta` was omitted).
- Any unresolved `[[wikilinks]]` (bare wikilinks with no matching page
title — these get silently degraded to plain text in the output; a
small number is normal wiki-content noise, but flag the list so the user
can fix the source page if it's not).
- Any broken internal links the validation pass found (a link that was
rewritten to a route with no corresponding generated file) — this is a
real problem in the source `wiki/` content (a stale or malformed
cross-reference), not a bug in the export, and is worth reporting back
as something to fix at the source.
- A reminder that `outputs/starlight/` has been regenerated on disk but
not staged, committed, or `npm install`ed — those are separate steps for
the user. For `--mode full`, the site is immediately runnable with
`cd outputs/starlight && npm install && npm run dev`.
## What the transform does (for context, not something you need to re-derive)
- **Frontmatter:** `tldr``description`; the first `# H1` in the body
becomes Starlight's required `title` (and is removed from the body, since
Starlight renders `title` as the page heading itself — leaving the H1 in
would duplicate it). `type`/`confidence`/`quality`/`retention`/
`last_updated`/`resource` have no native Starlight field, so they're
rendered as a `:::note[Knowledge base metadata]` aside at the top of the
page body instead of being dropped.
- **Badges:** a page gets a `sidebar.badge` — "Superseded" (if
`superseded_by` is set), else "Stale" (if `last_updated` +
`freshness_window_days` has elapsed), else "Low confidence" (if
`confidence` < 0.5) priority in that order, one badge max.
- **Links:** dual-linked cross-references (`[text](path) ([[Wikilink]])`,
or the older `[[Wikilink]] / [text](path)` ordering) collapse to just the
markdown link, since Starlight has no wikilink concept. Bare `[[Wikilinks]]`
are resolved against every page's H1 title and converted to a proper link
where a match exists, otherwise degraded to plain text. Root-absolute
`/wiki/...` links become site routes (`/wiki/entities/foo.md`
`/entities/foo/`; a link to a directory's `index.md` maps to the bare
directory route, e.g. `/entities/`). `linked/...`/`libs/...` cascade
references (pointing outside any self-contained export) become plain
text noting the path, not a broken link.
- **Assets:** person photos referenced as `../assets/people/x.jpg` are
copied to `public/assets/people/x.jpg` and rewritten to `/assets/people/x.jpg`.
- **Graph:** `wiki/graph/edges.json` is not copied verbatim (Starlight
doesn't render JSON as a page) — its content is fully rendered as a
Markdown table appended to `graph/index.md`, sorted by `from`, so no
information is lost.
- **Sidebar:** one `autogenerate` group per top-level `wiki/` subdirectory
(so new pages added to `entities/`/`sources`/`graph`/any future topic
folder show up automatically on the next export without touching the
sidebar config), plus explicit entries for the handful of root-level
pages (Overview, the routing table, and the two optional meta pages).
## Edge cases
- **A future `wiki/<newtopic>/` subdirectory:** handled automatically —
the script discovers top-level `wiki/` subdirectories dynamically and
adds a sidebar `autogenerate` group for each; no script changes needed.
- **Re-running with no wiki changes:** should produce byte-identical
output (directory listings are sorted before writing). If asset copying
or link rewriting ever introduces nondeterminism, that's a bug in the
script, not expected behavior.
- **A wiki page with no `tldr`:** `description` is simply omitted from
that page's frontmatter — Starlight tolerates a missing description.
- **A wiki page with no H1:** falls back to a slugified filename as the
title (e.g. `foo-bar.md` → "Foo Bar"), same fallback rule `export-okf`
uses.

View file

@ -0,0 +1,573 @@
#!/usr/bin/env python3
"""Export wiki/ into an Astro + Starlight-consumable form at outputs/starlight/.
Usage:
python3 export_starlight.py --mode {full,content-only} [--include-meta] [--today YYYY-MM-DD]
Run from the repository root (the directory containing wiki/ and outputs/).
--mode full regenerate the whole outputs/starlight/ project: scaffold
(package.json, astro.config.mjs, tsconfig.json,
src/content.config.ts, splash index.mdx) plus content.
--mode content-only regenerate only src/content/docs/** and public/assets/**
(and a SIDEBAR-SNIPPET.md), leaving any existing scaffold
files untouched. Use this if dropping the output into a
Starlight project you already have.
--include-meta also publish wiki/log.md (as Change Log) and
wiki/error-book.md (as Error Book). Omit to keep those
internal-only.
--today override "today" for staleness checks (default: real
today). Mainly useful for reproducible test runs.
"""
import argparse
import json
import re
import shutil
import sys
from datetime import date
from pathlib import Path
REPO_ROOT = Path.cwd()
WIKI = REPO_ROOT / "wiki"
OUT = REPO_ROOT / "outputs" / "starlight"
RESERVED_ROOT_ROUTES = {
"index.md": "routing-table",
"overview.md": "overview",
"log.md": "changelog",
"error-book.md": "error-book",
}
def yaml_quote(s):
s = str(s).replace("\\", "\\\\").replace('"', '\\"')
return f'"{s}"'
def parse_frontmatter(text):
"""Minimal parser for this repo's flat key: value frontmatter (no lists/nesting)."""
if not text.startswith("---\n"):
return {}, text
end = text.find("\n---", 4)
if end == -1:
return {}, text
fm_block = text[4:end]
body = text[end + 4 :].lstrip("\n")
fm = {}
for line in fm_block.splitlines():
line = line.strip()
if not line or ":" not in line:
continue
key, _, value = line.partition(":")
key = key.strip()
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
value = value[1:-1]
fm[key] = value
return fm, body
def extract_h1(body):
lines = body.splitlines()
for i, line in enumerate(lines):
stripped = line.strip()
if stripped.startswith("# "):
title = stripped[2:].strip()
remaining = lines[:i] + lines[i + 1 :]
# drop one immediately-following blank line so we don't leave a gap
if i < len(remaining) and remaining[i].strip() == "":
remaining = remaining[:i] + remaining[i + 1 :]
return title, "\n".join(remaining).lstrip("\n")
if stripped:
break # first non-blank line isn't an H1 - stop looking
return None, body
def wiki_relpath_to_route(relpath):
"""relpath is a path string relative to wiki/, e.g. 'entities/adam-betts.md'."""
relpath = relpath.lstrip("/")
if relpath in RESERVED_ROOT_ROUTES:
return "/" + RESERVED_ROOT_ROUTES[relpath] + "/"
p = Path(relpath)
parts = list(p.parts)
if parts and parts[-1].endswith(".md"):
parts[-1] = parts[-1][:-3]
if parts and parts[-1] == "index":
parts = parts[:-1]
if not parts:
return "/"
return "/" + "/".join(parts) + "/"
def out_path_for(relpath):
"""Where a wiki/<relpath> file's exported page lives under src/content/docs/."""
if relpath in RESERVED_ROOT_ROUTES:
return Path(RESERVED_ROOT_ROUTES[relpath] + ".md")
return Path(relpath)
def collect_wiki_pages():
pages = [] # list of (relpath_str, absolute_path)
for path in sorted(WIKI.rglob("*.md")):
relpath = path.relative_to(WIKI).as_posix()
pages.append((relpath, path))
return pages
def build_title_index(pages):
"""title -> route, for resolving bare [[Wikilinks]]."""
index = {}
for relpath, path in pages:
text = path.read_text(encoding="utf-8")
_, body = parse_frontmatter(text)
title, _ = extract_h1(body)
route = wiki_relpath_to_route(relpath)
if title:
index.setdefault(title, route)
index.setdefault(title.lower(), route)
slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
if slug:
index.setdefault(slug, route)
rel_no_ext = relpath[:-3] if relpath.endswith(".md") else relpath
index.setdefault(rel_no_ext, route)
index.setdefault(rel_no_ext.lower(), route)
stem = Path(relpath).stem
if stem != "index":
index.setdefault(stem, route)
index.setdefault(stem.lower(), route)
return index
WIKILINK_ADJACENT_RE = re.compile(r"(\[[^\]]+\]\([^)]+\))\s*\(\[\[[^\]]+\]\]\)")
WIKILINK_BEFORE_LINK_RE = re.compile(r"\[\[[^\]]+\]\]\s*/\s*(\[[^\]]+\]\([^)]+\))")
BARE_WIKILINK_RE = re.compile(r"\[\[([^\]]+)\]\]")
WIKI_ABS_NON_MD_LINK_RE = re.compile(r"\[([^\]]+)\]\(/wiki/([^)#]+)(#[^)]*)?\)")
WIKI_ABS_LINK_RE = re.compile(r"\]\(/wiki/([^)#]+)(#[^)]*)?\)")
CASCADE_LINK_RE = re.compile(r"\[([^\]]+)\]\((?:linked|libs)/[^)]+\)")
IMAGE_REL_RE = re.compile(r"!\[([^\]]*)\]\(\.\./assets/([^)]+)\)")
def rewrite_body(body, title_index, unresolved_wikilinks, copied_assets):
# 1. strip wikilinks that ride alongside a markdown link (either order:
# "[text](path) ([[Wikilink]])" or the legacy "[[Wikilink]] / [text](path)")
body = WIKILINK_ADJACENT_RE.sub(r"\1", body)
body = WIKILINK_BEFORE_LINK_RE.sub(r"\1", body)
# 2. resolve (or strip) any remaining bare [[Wikilinks]]
def _bare(m):
text = m.group(1)
route = title_index.get(text) or title_index.get(text.lower())
if route:
return f"[{text}]({route})"
unresolved_wikilinks.append(text)
return text
body = BARE_WIKILINK_RE.sub(_bare, body)
# 3. defuse /wiki/... links to non-Markdown source files not exported as pages
def _abs_non_md_link(m):
text = m.group(1)
relpath = m.group(2)
if relpath.endswith(".md"):
return m.group(0)
if relpath == "graph/edges.json":
return f"`{text}` (rendered below)"
return f"{text} (source file: `/wiki/{relpath}`)"
body = WIKI_ABS_NON_MD_LINK_RE.sub(_abs_non_md_link, body)
# 4. rewrite /wiki/... absolute links to site routes
def _abs_link(m):
relpath = m.group(1)
frag = m.group(2) or ""
route = wiki_relpath_to_route(relpath)
if frag:
route = route.rstrip("/") + "/" + frag
return f"]({route})"
body = WIKI_ABS_LINK_RE.sub(_abs_link, body)
# 5. defuse linked/ and libs/ cascade references (not part of this export)
def _cascade(m):
text, target = m.group(1), m.group(0)
path = re.search(r"\((?:linked|libs)/([^)]+)\)", target).group(0)[1:-1]
return f"{text} (upstream reference: `{path}`, not included in this export)"
body = CASCADE_LINK_RE.sub(_cascade, body)
# 6. copy relative person/asset images and rewrite to /assets/...
def _image(m):
alt, rel = m.group(1), m.group(2)
src = WIKI / "assets" / rel
if src.is_file():
dest = OUT / "public" / "assets" / rel
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(src, dest)
copied_assets.append(rel)
return f"![{alt}](/assets/{rel})"
body = IMAGE_REL_RE.sub(_image, body)
return body
def build_metadata_block(fm, today):
lines = []
meta_bits = []
for label, key in (("Type", "type"), ("Confidence", "confidence"),
("Quality", "quality"), ("Last updated", "last_updated"),
("Retention", "retention")):
if fm.get(key):
meta_bits.append(f"**{label}:** {fm[key]}")
if meta_bits:
lines.append(":::note[Knowledge base metadata]")
lines.append(" · ".join(meta_bits))
lines.append(":::")
lines.append("")
if fm.get("resource"):
resource = fm["resource"]
if resource.startswith("http://") or resource.startswith("https://"):
lines.append(f":::note[Source]\n[{resource}]({resource})\n:::\n")
else:
lines.append(f":::note[Source]\n`{resource}`\n:::\n")
if fm.get("superseded_by"):
lines.append(
f":::caution[Superseded]\nThis page is superseded by "
f"`{fm['superseded_by']}`.\n:::\n"
)
if fm.get("supersedes"):
lines.append(
f":::tip[Supersedes]\nThis page supersedes `{fm['supersedes']}`.\n:::\n"
)
return "\n".join(lines)
def compute_badge(fm, today):
if fm.get("superseded_by"):
return {"text": "Superseded", "variant": "danger"}
try:
if fm.get("last_updated") and fm.get("freshness_window_days"):
last_updated = date.fromisoformat(fm["last_updated"])
window = int(fm["freshness_window_days"])
if (today - last_updated).days > window:
return {"text": "Stale", "variant": "caution"}
except ValueError:
pass
try:
if fm.get("confidence") and float(fm["confidence"]) < 0.5:
return {"text": "Low confidence", "variant": "note"}
except ValueError:
pass
return None
def render_page(relpath, path, title_index, today, report):
text = path.read_text(encoding="utf-8")
fm, body = parse_frontmatter(text)
h1_title, body = extract_h1(body)
title = h1_title or Path(relpath).stem.replace("-", " ").title()
unresolved = []
body = rewrite_body(body, title_index, unresolved, report["copied_assets"])
report["unresolved_wikilinks"].extend(
f"{relpath}: [[{t}]]" for t in unresolved
)
meta_block = build_metadata_block(fm, today)
badge = compute_badge(fm, today)
fm_lines = ["---", f"title: {yaml_quote(title)}"]
if fm.get("tldr"):
fm_lines.append(f"description: {yaml_quote(fm['tldr'])}")
if badge:
fm_lines.append("sidebar:")
fm_lines.append(" badge:")
fm_lines.append(f" text: {yaml_quote(badge['text'])}")
fm_lines.append(f" variant: {badge['variant']}")
fm_lines.append("---")
out_body = (meta_block + "\n" + body) if meta_block else body
return "\n".join(fm_lines) + "\n\n" + out_body.strip() + "\n"
def normalize_edges(raw_edges):
if isinstance(raw_edges, dict):
raw_edges = raw_edges.get("edges", [])
if not isinstance(raw_edges, list):
return []
normalized = []
for edge in raw_edges:
if not isinstance(edge, dict):
continue
normalized.append(
{
"from": edge.get("from") or edge.get("source") or "",
"type": edge.get("type") or edge.get("relationship") or "",
"to": edge.get("to") or edge.get("target") or "",
"note": edge.get("note") or edge.get("evidence") or "",
"source_files": edge.get("source_files") or [],
}
)
return normalized
def table_cell(value):
return str(value).replace("|", "\\|").replace("\n", " ")
def render_edges_table(edges):
edges = normalize_edges(edges)
if not edges:
return "\n*No edges recorded yet.*\n"
rows = sorted(edges, key=lambda e: (e.get("from", ""), e.get("type", "")))
lines = ["", "## All relationships", "",
"| From | Relationship | To | Evidence | Sources |", "|---|---|---|---|---|"]
for e in rows:
sources = ", ".join(f"`{table_cell(s)}`" for s in e.get("source_files", []))
lines.append(
f"| `{table_cell(e.get('from', ''))}` | {table_cell(e.get('type', ''))} | "
f"`{table_cell(e.get('to', ''))}` | {table_cell(e.get('note', ''))} | {sources} |"
)
return "\n".join(lines) + "\n"
def derive_site_title():
overview = WIKI / "overview.md"
if overview.is_file():
_, body = parse_frontmatter(overview.read_text(encoding="utf-8"))
title, _ = extract_h1(body)
if title:
return title
return REPO_ROOT.name
SPLASH_TEMPLATE = """---
title: {title}
description: {description}
template: splash
hero:
tagline: {tagline}
actions:
- text: Browse entities
link: /entities/
icon: right-arrow
- text: Browse sources
link: /sources/
icon: right-arrow
- text: Knowledge base index
link: /routing-table/
icon: document
variant: minimal
---
import {{ CardGrid, Card }} from '@astrojs/starlight/components';
<CardGrid>
\t<Card title="Entities" icon="document">
\t\tPeople, organisations, products and concepts tracked in this knowledge base.
\t</Card>
\t<Card title="Sources" icon="open-book">
\t\tOne-page summaries of ingested raw material, with provenance back to the original source.
\t</Card>
\t<Card title="Graph" icon="random">
\t\tTyped relationships between entities and sources - who works for whom, what depends on what, what contradicts what.
\t</Card>
</CardGrid>
"""
PACKAGE_JSON_TEMPLATE = """{{
"name": "{name}",
"type": "module",
"version": "0.0.1",
"scripts": {{
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview"
}},
"dependencies": {{
"@astrojs/starlight": "^0.41.3",
"astro": "^7.0.9"
}}
}}
"""
TSCONFIG_TEMPLATE = """{
"extends": "astro/tsconfigs/strict"
}
"""
CONTENT_CONFIG_TEMPLATE = """import { defineCollection } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
};
"""
def build_sidebar_js(top_dirs, include_meta):
items = []
items.append(" { label: 'Knowledge Base Index', slug: 'routing-table' },")
items.append(" { label: 'Overview', slug: 'overview' },")
if include_meta:
items.append(" { label: 'Error Book', slug: 'error-book' },")
items.append(" { label: 'Change Log', slug: 'changelog' },")
for d in sorted(top_dirs):
label = d.replace("-", " ").title()
items.append(
f" {{ label: '{label}', items: [{{ autogenerate: {{ directory: '{d}' }} }}] }},"
)
return "[\n" + "\n".join(items) + "\n ]"
def astro_config_template(site_title, top_dirs, include_meta):
sidebar_js = build_sidebar_js(top_dirs, include_meta)
return f"""import {{ defineConfig }} from 'astro/config';
import starlight from '@astrojs/starlight';
export default defineConfig({{
integrations: [
starlight({{
title: {yaml_quote(site_title)},
sidebar: {sidebar_js},
}}),
],
}});
"""
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=["full", "content-only"], required=True)
parser.add_argument("--include-meta", action="store_true")
parser.add_argument("--today", default=None)
parser.add_argument(
"--site-title", default=None,
help="Override the site title (default: derived from wiki/overview.md's H1)",
)
args = parser.parse_args()
if not WIKI.is_dir():
sys.exit(f"No wiki/ directory found under {REPO_ROOT} - run this from the repo root.")
today = date.fromisoformat(args.today) if args.today else date.today()
pages = collect_wiki_pages()
title_index = build_title_index(pages)
docs_dir = OUT / "src" / "content" / "docs"
assets_dir = OUT / "public" / "assets"
if args.mode == "full" and OUT.exists():
shutil.rmtree(OUT)
else:
if docs_dir.exists():
shutil.rmtree(docs_dir)
if assets_dir.exists():
shutil.rmtree(assets_dir)
docs_dir.mkdir(parents=True, exist_ok=True)
report = {"copied_assets": [], "unresolved_wikilinks": [], "pages": 0, "skipped_meta": []}
top_dirs = set()
edges_path = WIKI / "graph" / "edges.json"
edges = json.loads(edges_path.read_text(encoding="utf-8")) if edges_path.is_file() else []
for relpath, path in pages:
if relpath in ("log.md", "error-book.md") and not args.include_meta:
report["skipped_meta"].append(relpath)
continue
rendered = render_page(relpath, path, title_index, today, report)
if relpath == "graph/index.md":
rendered = rendered.rstrip("\n") + "\n" + render_edges_table(edges)
dest = docs_dir / out_path_for(relpath)
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(rendered, encoding="utf-8")
report["pages"] += 1
parts = Path(relpath).parts
if len(parts) > 1:
top_dirs.add(parts[0])
site_title = args.site_title or derive_site_title()
if args.mode == "full":
(OUT / "package.json").write_text(
PACKAGE_JSON_TEMPLATE.format(name=re.sub(r"[^a-z0-9-]", "-", site_title.lower())),
encoding="utf-8",
)
(OUT / "tsconfig.json").write_text(TSCONFIG_TEMPLATE, encoding="utf-8")
(OUT / "src" / "content.config.ts").write_text(CONTENT_CONFIG_TEMPLATE, encoding="utf-8")
(OUT / "astro.config.mjs").write_text(
astro_config_template(site_title, top_dirs, args.include_meta), encoding="utf-8"
)
overview_fm, overview_body = ({}, "")
overview_path = WIKI / "overview.md"
if overview_path.is_file():
overview_fm, overview_body = parse_frontmatter(overview_path.read_text(encoding="utf-8"))
tagline = overview_fm.get("tldr", "A cascade knowledge base.")
(docs_dir / "index.mdx").write_text(
SPLASH_TEMPLATE.format(
title=yaml_quote(site_title),
description=yaml_quote(tagline),
tagline=yaml_quote(tagline),
),
encoding="utf-8",
)
else:
snippet = (
"# Sidebar snippet\n\n"
"Paste this into your own `astro.config.mjs`, inside "
"`starlight({ ... })`:\n\n"
"```js\n"
f"sidebar: {build_sidebar_js(top_dirs, args.include_meta)},\n"
"```\n\n"
"Copy `src/content/docs/**` into your project's equivalent "
"directory, and `public/assets/**` into your project's "
"`public/assets/`.\n"
)
(OUT / "SIDEBAR-SNIPPET.md").write_text(snippet, encoding="utf-8")
# Validate internal links resolve to a generated file
broken = []
for relpath, _ in pages:
if relpath in ("log.md", "error-book.md") and not args.include_meta:
continue
dest = docs_dir / out_path_for(relpath)
text = dest.read_text(encoding="utf-8")
for m in re.finditer(r"\]\((/[a-zA-Z0-9_\-./]+/)\)", text):
route = m.group(1)
candidate_dir = docs_dir / route.strip("/")
candidate_file = docs_dir / (route.strip("/") + ".md")
candidate_index = candidate_dir / "index.md"
candidate_mdx = docs_dir / (route.strip("/") + ".mdx") if route.strip("/") else docs_dir / "index.mdx"
if not (candidate_file.is_file() or candidate_index.is_file() or candidate_mdx.is_file()):
broken.append(f"{relpath} -> {route}")
print(f"mode: {args.mode}")
print(f"pages exported: {report['pages']}")
print(f"assets copied: {len(report['copied_assets'])}")
if report["skipped_meta"]:
print(f"meta pages skipped (--include-meta not set): {', '.join(report['skipped_meta'])}")
if report["unresolved_wikilinks"]:
print(f"unresolved [[wikilinks]] (stripped to plain text): {len(report['unresolved_wikilinks'])}")
for w in report["unresolved_wikilinks"]:
print(f" - {w}")
if broken:
print(f"WARNING: {len(broken)} internal link(s) do not resolve to a generated page:")
for b in broken:
print(f" - {b}")
print(f"output: {OUT}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,177 @@
---
name: project-summary
description: Generate or refresh a one-to-two-page project overview at the repo root, synthesized entirely from the current wiki/ contents — overview paragraph, project state, actions and their status, risks, and assumptions. Use when the user asks for "a project summary", "project overview", "give me the state of the project", "where do things stand", "summarize the project", or wants a quick top-level snapshot without reading the whole wiki.
---
# Project summary skill
## Purpose
Produce a short, standalone snapshot of the project at repo root - something
a busy stakeholder can read in two minutes without opening `wiki/` at all.
Everything in it must trace back to what the wiki actually says; this is a
synthesis of existing knowledge, not a place to add new claims or guesses.
This is a one-way, on-demand export - `wiki/` stays the authoritative
source; the generated overview file is always a derived, refreshable
snapshot. Re-running the skill regenerates it from whatever the wiki says
*now*; it does not append to the previous version.
## Trigger phrases
Use this skill when the user says things like:
- "give me a project summary" / "project overview"
- "where do things stand" / "what's the state of the project"
- "summarize the project" / "one-pager" / "two-pager"
- "catch me up on the project"
## How to run this skill
### Step 1 - Identify the project
Read `wiki/index.md` and `wiki/entities/index.md` first (small, cheap,
gives the lay of the land). Then find every entity page whose frontmatter
has `type: project`.
- **Exactly one** - that's the subject, proceed.
- **None found** - tell the user no `type: project` entity exists yet in
the wiki, and ask whether to point at a specific entity page instead or
run Ingest first. Do not fabricate a project summary from thin air.
- **More than one** - list them (title + `tldr`) and ask which one to
summarize, or whether they want a separate overview file for each.
### Step 2 - Gather context via the graph, not just one page
Read the chosen project entity page in full. Then read
`wiki/graph/edges.json` and pull every edge where this entity is the
`source` or `target` - these are its direct dependencies. Read each of
those pages in full too. If a dependency page itself has further edges
that look load-bearing (e.g. another concept it depends on), follow one
more hop - but stop there. This is a snapshot, not a full graph traversal;
two hops is enough to be accurate without pulling in the entire wiki.
Also check the single most recent `workload/YYYY-MM-DD_summary.md` (by
filename date) for anything very recent that may not have made it into the
wiki yet. Treat the wiki as authoritative if the two conflict - workload
files are a running log, not a maintained source of truth - but note a
real discrepancy in Project State rather than silently picking one.
### Step 3 - Extract and classify
Work through everything gathered in Step 2 and sort content into five
buckets. Do not pad any section to look complete - a short, honest section
is better than filler, and it's fine for Risks or Assumptions to be thin or
even say "none currently recorded" if that's genuinely the case.
1. **Overview** - one short paragraph: what the project is, who's involved
(client / delivery org / key roles), and what it's for. This is
scene-setting, not a list of facts.
2. **Project State** - the current concrete status: what phase things are
in, what's confirmed vs. pending, key dates. Pull directly from
"Current Shape" / "Management Summary" / status-like sections on the
entity pages gathered.
3. **Actions & Status** - every concrete to-do, next step, or open item
found across the gathered pages (commonly under headings like "Open
Points" or "Open items"). Dedupe overlapping items from different pages
into one line. Assign a status to each from this fixed vocabulary based
on the page's own wording - don't invent a finer-grained scheme:
- **Done** - completed, confirmed, signed, resolved
- **In progress** - actively being worked, partially complete
- **Blocked** - explicitly waiting on something else before it can move
- **Open** - identified but not yet started or scheduled
4. **Risks** - things that could hurt the project if they go wrong or stay
unresolved. Look for language like "risk", "concern", "gap", "blocked
on", "unresolved", "dependent on", "may not stick", "left dependent on."
For each: what the risk is, why it matters, and - if the source says so
- what would mitigate or resolve it.
5. **Assumptions** - things the current plan depends on being true but that
aren't yet confirmed. Look for language like "expected", "should",
"likely", "assuming", "TBC", "to be confirmed", "time-bound", "reconfirm
before acting." For each: what's assumed, and what would need to happen
to confirm or invalidate it.
An item can legitimately touch both Risks and Assumptions (an unconfirmed
assumption is often also a risk if it turns out false) - when that
happens, put it wherever it fits better and don't duplicate it in both.
If there's more candidate material than fits a one-to-two-pager, prioritize
by what the source pages themselves flag as higher-confidence, higher-
retention, and most recently updated - this is a snapshot of what matters
most right now, not an exhaustive appendix.
### Step 4 - Write the file
Target length: one to two pages (roughly 500-900 words total, including
bullets - err short over long). Write to `PROJECT-OVERVIEW.md` at the repo
root, overwriting it completely if it already exists (this is a refreshable
snapshot, never hand-append to a previous version).
Use this structure:
```markdown
# [Project Name] - Project Overview
*Auto-generated from `wiki/` - do not hand-edit; regenerating this skill overwrites this file. Last generated: YYYY-MM-DD.*
**At a glance:** [client] · [delivery org] · [one-line phase/status]
## Overview
[the synthesis paragraph]
## Project State
[status bullets or short paragraph]
## Actions & Status
| Action | Status |
|---|---|
| ... | Done / In progress / Blocked / Open |
## Risks
- **[risk]** - [why it matters] [mitigation if known]
## Assumptions
- **[assumption]** - [what would confirm/invalidate it]
---
*Sources: [wiki pages used, as markdown links]*
```
Use plain markdown links to the wiki pages actually used (e.g.
`[Grant Thornton FDE engagement](wiki/entities/grant-thornton-fde-engagement.md)`)
so a reader can jump to the full detail behind any line. Keep the source
list to what was actually used, not every page in the wiki.
If more than one `type: project` entity was chosen for separate summaries
(Step 1), name the files `PROJECT-OVERVIEW-<slug>.md` instead of the plain
name, using a short kebab-case slug of the project's title.
### Step 5 - Report
Tell the user the file was written (path), give a one-line gist of what it
found (e.g. "3 open actions, 2 risks, 1 assumption"), and mention it can be
regenerated any time by re-running this skill - it will always reflect
whatever the wiki says at that moment, not what it said today.
## Edge cases
- **Empty or near-empty project entity page** - write a short, honest
Overview and State, and let Actions/Risks/Assumptions be genuinely short
rather than inventing content to fill the template.
- **Conflicting information between two source pages** - prefer the page
with the more recent `last_updated`; if they're the same date, note the
conflict briefly in Project State rather than silently picking one.
- **`PROJECT-OVERVIEW.md` already exists** - overwrite it; this file is
always meant to reflect the current wiki, not accumulate history. If the
user wants history, that's what `wiki/log.md` and `workload/` are for.
- **Re-running with no wiki changes since the last run** - should produce
essentially the same content each time; don't introduce random variation
in section content or ordering between runs.

6
.claude/settings.json Normal file
View file

@ -0,0 +1,6 @@
{
"permissions": {
"allow": [
]
}
}

3
.gitignore vendored
View file

@ -1,2 +1,5 @@
libs/
tmp/
outputs/starlight
outputs/okf
.env

View file

@ -23,7 +23,9 @@ The root directory contains exactly seven top-level entries. You must maintain t
│ # On-demand workflows beyond Ingest/Lint may be defined as Claude Code Skills under
│ # `.claude/skills/` — check there before assuming a capability doesn't exist.
├── raw/ # WRITTEN BY USER ONLY. Raw files, scratchpad notes, URLs, links.txt.
│ └── inbox/ # Drop zone: unprocessed material the agent cleans on ingest.
│ ├── inbox/ # Drop zone: unprocessed material the agent cleans on ingest.
│ └── archive/ # AGENT MAINTAINED. Ingested raw material, filed by ingestion date.
│ └── <YYYY-MM-DD>/ # One folder per ingestion date; holds every raw/inbox file processed that day.
├── tmp/ # MANAGED BY AGENT. Temporary files, caches, intermediate processing artifacts (gitignored).
├── wiki/ # MANAGED BY AGENT. The local, mutable, structured markdown wiki. Overlays linked/ and libs/.
│ ├── index.md # Entry point / routing table with "Use when" triggers. Carries kb_schema_version.
@ -115,7 +117,7 @@ retention: high|medium|low # How aggressively to deprioritize when old
When the user says "Ingest", "Sync the wiki", or "Update the Wiki" (for syncing this repo's own git history with its remote, see the sync-changes skill under `.claude/skills/` instead):
1. **Process Inbox:** Scan `raw/inbox/` for new material. Move processed items to `raw/` for archiving. If `raw/inbox/` is empty, scan `raw/` directly.
1. **Process Inbox:** Scan `raw/inbox/` for new material. After ingesting, move each processed item to `raw/archive/<YYYY-MM-DD>/`, where the date is today's ingestion date (create the dated folder if it doesn't exist yet). If `raw/inbox/` is empty, scan `raw/` directly (excluding `raw/archive/`, which holds already-processed material).
2. **Consult Cascade:** Before writing anything, check if the entity already exists (local `wiki/` first, then each `linked/<name>/`, then each `libs/<name>/`). The local `wiki/` always wins. Upstream content is informative but can be overridden locally.

225
CLAUDE.md
View file

@ -1,225 +0,0 @@
# SYSTEM PROMPT: CASCADE KNOWLEDGE BASE ARCHITECT
## ROLE & PHILOSOPHY
You are an autonomous Knowledge Architect Agent for a **Cascade Knowledge Base**. The system is designed as a layered stack: read-only upstream knowledge bases (symlinked in `linked/` and git-managed copies in `libs/`) form the foundation, and the local mutable knowledge base overlays on top. This means knowledge flows downward through the cascade — upstream truths are preserved, while you only ever modify the local layer.
If an entity exists in both the local wiki and any upstream KB, the local version takes precedence and overrides the upstream one.
You view directories as storage disks, context windows as RAM, and your processing loops as CPU cycles. Your sole objective is to build, maintain, and dynamically structure a comprehensive knowledge base, respecting the cascade priority rules at all times.
You possess full autonomy over local directory structure, file naming conventions, and cross-referencing. You must strictly adhere to the operational boundaries and file management rules detailed below.
---
## 1. DIRECTORY STRUCTURE
The root directory contains exactly seven top-level entries. You must maintain this structure flawlessly:
```
├── libs/ # GIT-MANAGED COPIES ONLY. Read-only external KBs copied into the repo (gitignored — populated by the user via git).
│ └── <name>/ # Individual external knowledge base (immutable — never write here).
├── linked/ # SYMLINKS ONLY. Each entry is a symbolic link to another KB root (read-only upstream source of truth).
│ └── <name>/ # Individual upstream knowledge base (immutable — never write here).
├── outputs/ # MANAGED BY AGENT. Generated artifacts, exports, compiled files produced from the wiki.
│ # On-demand workflows beyond Ingest/Lint may be defined as Claude Code Skills under
│ # `.claude/skills/` — check there before assuming a capability doesn't exist.
├── raw/ # WRITTEN BY USER ONLY. Raw files, scratchpad notes, URLs, links.txt.
│ └── inbox/ # Drop zone: unprocessed material the agent cleans on ingest.
├── tmp/ # MANAGED BY AGENT. Temporary files, caches, intermediate processing artifacts (gitignored).
├── wiki/ # MANAGED BY AGENT. The local, mutable, structured markdown wiki. Overlays linked/ and libs/.
│ ├── index.md # Entry point / routing table with "Use when" triggers. Carries kb_schema_version.
│ ├── overview.md # High-level map of the knowledge base.
│ ├── log.md # AGENT LOG. Root rollup tracking wiki-level modifications (see Recursive Index & Log Convention).
│ ├── error-book.md # AGENT MAINTAINED. Records compilation errors and derived constraints.
│ ├── entities/ # AGENT POPULATED. Typed entity pages (people, projects, concepts, libraries). Has its own index.md.
│ └── graph/ # AGENT MAINTAINED. Edge lists and relationship data for the knowledge graph. Has its own index.md.
└── workload/ # MANAGED BY AGENT. Summaries of discussions and decisions.
└── YYYY-MM-DD_summary.md
```
### Cascade Lookup Priority
When searching for any entity, concept, or file, use the following cascade (first match wins):
1. **Local wiki/** — highest priority; agent-written content overlays everything below.
2. **linked/\<name\>/** — read-only upstream KBs mounted as symlinks, searched in alphabetical order.
3. **libs/\<name\>/** — read-only external KBs copied via git, searched in alphabetical order.
4. If no match is found anywhere, treat the entity as unknown.
You must **never** create, modify, move, or delete any file or directory inside `linked/` or `libs/`.
### Index-First Navigation
When searching for information, always start by looking for `index.md` files.
Read the index to discover what pages and subdirectories are available before
drilling into individual files. Scan `index.md` across all layers:
1. **wiki/** — scan `wiki/index.md`, then recursively check any subdirectory `wiki/<topic>/index.md`.
2. **linked/\<name\>/** — for each linked upstream KB, scan its root `index.md` and subdirectory indexes.
3. **libs/\<name\>/** — same pattern: root index first, then subdirectory indexes as needed.
This avoids blind filesystem scans and uses the index as a curated table of contents — exactly as Karpathy's original pattern intended.
### Recursive Index & Log Convention
Index-First Navigation only works if subdirectory indexes actually exist. Maintain them as follows:
- Every `wiki/` subdirectory that groups multiple pages (`entities/`, `graph/`, and any future topic folder) must contain its own `index.md`. It carries no frontmatter and is a flat bullet list of links, each with a one-line description mirroring the linked page's `tldr` — plus a link to any nested subdirectory.
- A subdirectory may also keep its own `log.md` once it has enough independent change history to warrant one (a judgment call — typically once it holds several pages or changes on its own cadence, separate from the rest of the wiki). Entries follow the same reverse-chronological format as Rule B.
- The root `wiki/log.md` stays the top-level rollup: it records changes made directly under `wiki/` (`index.md`, `overview.md`, `error-book.md`, directory-creation events) plus one pointer line whenever a subdirectory log absorbs a change, e.g. `- See wiki/entities/log.md for entity-page changes on this date.` Each change gets exactly one home log — never record the same change in both.
### Lazy-Loading with "Use When" Triggers
The `wiki/index.md` is a routing table. Each entry has a **Use when** column
that lists trigger keywords. Before loading any page:
1. Read `wiki/index.md` (stays in context — it is small).
2. Match the current task's keywords against the **Use when** entries.
3. Only load the matching page(s). Do not load every page.
4. If a page has a `tldr:` frontmatter field, read that first. If it answers the query, skip the body.
This keeps context lean: ~34 pages loaded instead of all pages.
---
## 2. PAGE FRONTMATTER SCHEMA
Every wiki page must use YAML frontmatter. `type` is required; the rest are optional:
```yaml
---
type: concept # REQUIRED. Open string for the entity/content kind (e.g. person, project, concept, library, decision, playbook). Unregistered — new values are always valid; readers must tolerate unrecognized types.
resource: https://... # Optional. Canonical URI to the authoritative external source this page describes (a linked/<name>/... or libs/<name>/... path, ticket, repo, doc, dataset). Keeps "what the wiki says about it" separate from "where the real thing lives."
tldr: One-sentence summary optimised for LLM reading
confidence: 0.01.0 # How many/corroborated sources support this
quality: 0.01.0 # Self-evaluation: well-structured, consistent, cited
supersedes: path/to/older/page.md
superseded_by: path/to/newer/page.md
last_updated: YYYY-MM-DD
freshness_window_days: 90 # Days before considered potentially stale
retention: high|medium|low # How aggressively to deprioritize when old
---
```
- **`type`** — required on every page. Set once on write and rarely changed; it's the first thing lint checks for conformance, and it's how pages in `entities/` get grouped without depending on directory naming alone.
- **`resource`** — set when the page describes something with a stable external address. Omit for pages that are pure synthesis (e.g. an overview or a decision writeup with no single external source).
- **`tldr`** — generated on write. If the TLDR alone answers a query, the body is never loaded.
- **`confidence`** — set on write based on source corroboration. Decays with time unless reinforced by new sources.
- **`quality`** — self-score on write. Below 0.7 → flag for review.
- **`supersedes` / `superseded_by`** — when new info contradicts or updates an old page, link them. Old pages are preserved but marked stale.
- **`last_updated`** — set automatically on every write or edit.
- **`freshness_window_days`** — pages older than this window are flagged stale during lint.
- **`retention`** — `low` pages may be archived or deprioritized after the freshness window expires.
### Schema Versioning
`wiki/index.md` (only) carries an additional frontmatter field, `kb_schema_version` (e.g. `"1.1"`), declaring which revision of this schema the wiki was authored against. Bump the minor version when adding an optional field (backward-compatible); bump the major version when changing or removing a required field or reserved filename convention (breaking). Individual pages do not carry this field — it is a bundle-level declaration, not a per-page one.
---
## 3. INGESTION WORKFLOW (TRIGGERED ON DEMAND)
When the user says "Ingest", "Sync the wiki", or "Update the Wiki" (for syncing this repo's own git history with its remote, see the sync-changes skill under `.claude/skills/` instead):
1. **Process Inbox:** Scan `raw/inbox/` for new material. Move processed items to `raw/` for archiving. If `raw/inbox/` is empty, scan `raw/` directly.
2. **Consult Cascade:** Before writing anything, check if the entity already exists (local `wiki/` first, then each `linked/<name>/`, then each `libs/<name>/`). The local `wiki/` always wins. Upstream content is informative but can be overridden locally.
3. **Extract Entities:** Identify typed entities in the source — people, projects, libraries, concepts, systems. Create entity pages in `wiki/entities/<entity-name>.md` if they don't exist. Record typed relationships between entities: `uses`, `depends_on`, `caused`, `contradicts`, `supersedes`. Store edge data in `wiki/graph/edges.json`.
4. **Synthesize Pages:** Convert the core knowledge into clean, modular Markdown files. Every page gets:
- A `tldr:` (one sentence, optimised for LLM reading)
- A `confidence:` score (0.01.0 based on source corroboration)
- A `quality:` self-score (0.01.0)
- A `last_updated:` timestamp
- A `freshness_window_days:` appropriate to the topic
- A `retention:` level
5. **Link & Cross-Reference:** Use **both** `[[Wikilinks]]` and standard `[markdown](path.md)` links on every cross-reference. This ensures the wiki works in Obsidian, GitHub, and CLI tools. Where useful, reference upstream files at `linked/<name>/...` or `libs/<name>/...`.
6. **Update Index & Log:** Add new pages to the routing table in `wiki/index.md` with a **Use when** description. If the page lives in a subdirectory, also add it to that subdirectory's `index.md`. Append a log entry to the most specific applicable log — the subdirectory's `log.md` if it has one, otherwise `wiki/log.md`. If this step creates a brand-new `wiki/<topic>/` subdirectory, immediately create that subdirectory's `index.md` per the Recursive Index & Log Convention.
---
## 4. QUERY WORKFLOW
When answering a question or researching a topic:
1. **Read the index**`wiki/index.md` first. Match query keywords against **Use when** triggers.
2. **Read TLDRs** — for any matched page, read its `tldr:` frontmatter first. If it answers the query, stop.
3. **Load full pages** — only if the TLDR was insufficient.
4. **Walk the graph** — if the entity has relationships in `wiki/graph/edges.json`, follow them to discover connected pages (e.g. "what depends on X?").
5. **Fall back upstream** — if the local wiki has no match, check `linked/<name>/` indexes, then `libs/<name>/` indexes. Apply cascade priority throughout.
---
## 5. MAINTENANCE WORKFLOW (LINT)
Periodically (or when asked to "Lint"), health-check the wiki:
1. **Conformance check** — verify every non-reserved `.md` file under `wiki/` (i.e. excluding `index.md` and `log.md`) has parseable YAML frontmatter with a non-empty `type` field. Flag violations first; malformed pages make every check below unreliable.
2. **Freshness check** — scan every page whose `last_updated` exceeds `freshness_window_days`. Flag as stale; suggest the user confirm or update the content.
3. **Confidence decay** — reduce `confidence` on pages not reinforced in the last window. Pages below 0.3 confidence get flagged for re-review.
4. **Retention sweep** — mark `retention: low` pages older than 2× their freshness window as archived in `wiki/archived/`. Do not delete — move with a note in the log.
5. **Supersession detection** — when two pages cover the same entity, check for contradictions. If one is newer, add `supersedes` / `superseded_by` links. Preserve the old page but mark it stale.
6. **Orphan detection** — find pages with no inbound links. Either add backlinks from relevant pages or move to `wiki/archived/` with a log note.
7. **Graph consistency** — verify every edge in `wiki/graph/edges.json` points to an existing entity page. Remove or fix broken edges.
8. **Index/log consistency** — verify every subdirectory under `wiki/` that contains pages has an `index.md` listing all of them, and that no single change is recorded in both a subdirectory `log.md` and the root `wiki/log.md`.
9. **Error Book entry** — record any systemic issue (repeated broken pattern, format mismatch) in `wiki/error-book.md` with root cause, fix applied, and the derived constraint to prevent recurrence.
Auto-fix what you can (broken links, missing backlinks, stale flags). Report what you cannot.
---
## 6. COMPLIANCE & LOGGING RULES (NON-NEGOTIABLE)
### Rule A: Immutability of linked/ and libs/
You must **never** write, modify, move, or delete any file or directory inside `linked/` or `libs/`. These are read-only upstream sources of truth managed exclusively by the User. If information in them is outdated or incorrect, you may override it by writing a corrected version in the local `wiki/`. The local version will take priority in the cascade lookup.
### Rule B: The Wiki Change Log (`wiki/log.md`)
Every single time you create, modify, move, or delete a file within the `wiki/` directory, you must immediately document it in `wiki/log.md` before proceeding.
- **Ordering:** The most recent action **must always be at the very top** of the file (chrono-reverse order).
- **Format Per Entry:**
```markdown
## [YYYY-MM-DD HH:MM] - [ACTION TYPE: e.g., CREATE/UPDATE/DELETE]
- **File Affected:** `wiki/path/to/file.md`
- **Description:** Brief summary of what knowledge was added or altered.
- **Source:** [e.g., Chat conversation, raw/notes.txt, URL]
---
```
### Rule C: Cascade-Anchored References with Dual-Linking
When cross-referencing an entity that exists in an upstream KB, write the link using the relative path from the project root (e.g., `linked/<name>/wiki/concepts/foo.md` or `libs/<name>/docs/bar.md`). This preserves the cascade structure and makes it clear which layer the reference belongs to.
For references between pages within `wiki/` itself, prefer project-root-absolute paths (e.g. `/wiki/entities/foo.md`) over relative paths (`../entities/foo.md`). Absolute paths keep resolving correctly if either page is later moved during a lint or reorganization pass; relative paths silently break.
Use **both** `[[Wikilinks]]` (Obsidian-compatible) and standard `[markdown](path.md)` links on every cross-reference. This ensures the wiki works in Obsidian graph view, GitHub rendering, and CLI tools.
### Rule D: Session Summary (`workload/`)
After every conversational turn where you take any action (read, write, search, ingest, lint, answer a question), update the summary file in `workload/`. If today's file already exists, append new notes to it; otherwise create it.
- **Naming:** `workload/YYYY-MM-DD_summary.md`
- **Content:** Brief record of what was discussed, what actions were taken, and what decisions were made during this exchange.
- **Purpose:** Provides continuity between sessions and a browsable history of how the knowledge base evolved.
### Rule E: Automation Hooks
Follow these event-driven behaviors:
- **On new source in inbox** — on the next ingest, auto-process: extract entities, update graph, update index, write to log.
- **On session start** — read `wiki/index.md` and the latest `workload/` summary to load relevant context.
- **On session end** — compress the session into observations and file insights into `workload/`.
- **On query** — if the answer has lasting value, file it back into `wiki/` as a new page or update to an existing one.
- **On memory write** — check for contradictions with existing wiki content. If found, apply supersession (link old → new) and log it.
- **On schedule** — periodic lint, consolidation, retention decay, freshness check.
### Rule F: Demand-Driven Context (DDC)
Use agent failures as the signal for what knowledge to add:
1. When you cannot answer a question or complete a task, identify the missing knowledge.
2. Propose a minimal entity or page to fill the gap.
3. The user approves or provides the source material.
4. Add it to `raw/inbox/` or describe it in chat.
5. Next ingest cycle incorporates it.
This keeps the wiki lean — you only add what is needed, not what is merely available.

1
CLAUDE.md Symbolic link
View file

@ -0,0 +1 @@
AGENTS.md

View file

@ -1,24 +0,0 @@
# Session Summary — 2026-05-30
## Actions Taken
- Transformed `instruction.md` from Karpathy-style LLM Wiki prompt to **Cascade Knowledge Base** concept with layered read-only upstream KBs
- Created `wiki/index.md`, `wiki/overview.md`, `wiki/log.md` as required core pages
- Created `AGENTS.md` and `CLAUDE.md` with full agent instructions
- Created `README.md` with user-facing documentation
- Researched 16 improvement ideas from web, wrote to `ideas.md`
- Added `libs/` directory (git-managed copies, read-only, gitignored)
- Added `outputs/` and `tmp/` directories
- Added Index-First Navigation rule (recursive index.md scanning across all layers)
- Implemented all 14 selected ideas: frontmatter schema (confidence, tldr, quality, supersedes, freshness, retention), entity extraction & graph, lazy-loading index with Use When triggers, TLDR-first query layer, self-healing lint, dual-linking, automation hooks, error book, DDC, inbox workflow
- Created `wiki/graph/`, `wiki/entities/`, `raw/inbox/` directories
- Created `wiki/error-book.md` stub
- Restructured `wiki/index.md` into a routing table
- Removed `ideas.md` and its reference from README
## Decisions Made
- Cascade priority: wiki/ → linked/ → libs/ (alphabetical within each layer)
- linked/ for symlinks, libs/ for git-cloned copies (both read-only, never touched by agent)
- Local wiki always overrides upstream content
- Frontmatter schema standardized across all wiki pages
- Dual-linking ([[wikilinks]] + markdown) required for all cross-references
- `tmp/`, `libs/` gitignored; `outputs/` tracked

View file

@ -1,41 +0,0 @@
# Session Summary — 2026-07-13
## Actions Taken
- Researched Google Cloud's Open Knowledge Format (OKF v0.1, published June 2026): a minimal markdown+YAML-frontmatter spec for portable, agent/human-consumable knowledge bundles (`type` required field, `resource` field, recursive `index.md`/`log.md`, root-absolute links, formal conformance criteria, `okf_version`).
- Compared OKF against this repo's Cascade Knowledge Base approach and identified 8 areas where OKF's design is stronger (typing, link stability, recursive progressive disclosure, hierarchical logs, resource linking, conformance checking, schema versioning, generic interop tooling), while noting this repo's cascade layering, confidence/quality/retention decay, and session continuity go beyond what OKF specifies.
- Implemented propositions 17 (OKF-inspired improvements), applied identically to `CLAUDE.md` and `AGENTS.md`:
1. Added required `type` frontmatter field + optional `resource` field to the Page Frontmatter Schema.
2. Added `resource` field (see above, same schema change).
3. Added guidance to Rule C to prefer project-root-absolute links (`/wiki/entities/foo.md`) for intra-wiki cross-references over relative paths.
4. Added a "Recursive Index & Log Convention" subsection under Index-First Navigation; created `wiki/entities/index.md` and `wiki/graph/index.md` as the first real subdirectory indexes; updated the Ingestion Workflow (step 6) to create subdirectory indexes/logs going forward.
5. Made `wiki/log.md` an explicit root-level rollup, with subdirectories permitted their own `log.md` once they have independent history — avoids one unbounded flat log file.
6. Added a "Conformance check" as lint step 1 (verify parseable frontmatter + non-empty `type` on every non-reserved page) and an "Index/log consistency" check as lint step 8.
7. Added a `kb_schema_version` field (bundle-level, on `wiki/index.md` only) with minor/major bump rules, documented in a new "Schema Versioning" subsection.
- Propagated the same documentation updates to `wiki/index.md`, `wiki/overview.md`, and `README.md` for consistency, and logged each wiki-level change in `wiki/log.md`.
## Proposition 8 — planned and implemented
- User asked for a plan for proposition 8 (OKF export) and raised the question of whether it belongs as CLAUDE.md/AGENTS.md prose or as a command/skill. Entered plan mode, delegated design validation to a Plan agent, then wrote and got approval for a plan at `/home/mkopec/.claude/plans/twinkly-wobbling-harbor.md`.
- **Decision:** implemented as a project-scoped Claude Code Skill (`.claude/skills/export-okf/SKILL.md`), not new CLAUDE.md/AGENTS.md prose. Rationale: OKF export is a deterministic, occasionally-invoked transformation with a large fixed mapping ruleset — exactly what Skills' lazy-loading is for, versus permanently taxing every session's context via the always-loaded instruction files. Project-scoped (not `~/.claude/skills/`) because the mapping is coupled to this repo's exact schema and travels with it in the same commits.
- Closed a discoverability gap for non-Claude-Code agents: added one generic sentence to `CLAUDE.md`/`AGENTS.md` §1 (`outputs/` line) pointing at `.claude/skills/` in general, without naming this specific skill, so the pointer never needs updating as skills are added/removed.
- Fixed a prerequisite gap discovered during planning: `wiki/overview.md` and `wiki/error-book.md` had zero YAML frontmatter and would have failed the conformance lint check added earlier this session. Retrofitted both with `type`/`tldr`/`last_updated`.
- Updated `README.md` with a "OKF Export" feature bullet and a note clarifying `outputs/okf/` is a fully-regenerated build artifact that stays git-tracked (consistent with the existing `outputs/` convention) but is never auto-committed by the skill itself.
- Wrote the full `export-okf` SKILL.md: 8-step procedure (read source → clear+rebuild outputs/okf/ → transform concept-doc frontmatter/links → regenerate index.md files → regenerate log.md files → handle non-reserved special pages → validate output conformance → report), plus edge cases (empty entities/graph dirs, missing `type`, future `archived/`, determinism on re-run).
- Have not yet run the skill end-to-end (wiki/entities and wiki/graph are still empty, so there's little to export yet) — first real exercise of the skill will happen on the next ingest that populates entities.
## sync-changes skill — planned and implemented
- User asked for a second local skill: on "sync changes," reconcile this repo's own git history with its `origin` remote (pull remote commits, commit local changes, resolve conflicts with the user, push).
- Clarified scope up front via AskUserQuestion: "git" means this repo's own `origin` remote (`https://codeberg.org/Valdec/llm-wiki-cascade.git`), not `libs/`; and unlike `export-okf`, auto-push is explicitly approved here — the skill commits and pushes on its own once conflicts are resolved.
- Checked real repo state (read-only): `origin` is configured, but local `main` has **zero commits** — everything was untracked at the time of this session. This made the "local has no commits, remote may already have history" first-run case a live scenario, not a hypothetical, so it's handled as an explicit hard-stop path (ask the user to choose merge / remote-wins / stop) rather than an automatic guess.
- Entered plan mode again given the risk of automated git push; delegated design validation to a Plan agent (git command sequencing, conflict-presentation mechanics, first-run handling), then resolved two remaining open questions via AskUserQuestion: (1) never offer a force-push/discard-remote option even in the first-run menu — recommended and confirmed; (2) reword the existing bare `"Sync"` Ingestion trigger to `"Sync the wiki"` to reduce ambiguity with the new skill's `"sync changes"` trigger — recommended and confirmed.
- Implemented: `.claude/skills/sync-changes/SKILL.md` (5-step procedure: pre-flight safety checks incl. secrets scan → detect first-run/unrelated-histories case → steady-state commit-then-fetch-merge-then-push flow → present each conflict block via AskUserQuestion with keep-local/keep-remote/provide-merged-text options → structured report), plus edge cases (nothing-to-sync, push-rejected-twice, no-remote-configured).
- Updated `CLAUDE.md`/`AGENTS.md` §3 trigger wording (`"Sync"``"Sync the wiki"` + pointer to the new skill) and added a "Git Sync (on demand)" bullet to `README.md`, matching the `export-okf` precedent.
- Have not yet run the skill for real — it would actually commit and push this session's changes to the live Codeberg remote, which is a real external action, so it's left for the user to explicitly trigger with "sync changes" rather than auto-run as part of this session's "verification."
## Decisions Made
- Keep `CLAUDE.md` and `AGENTS.md` byte-identical; every instruction edit is applied to both in lockstep.
- `type` is required per OKF precedent, but the rest of the existing schema (confidence/quality/retention/etc.) is retained as-is — OKF's minimalism is adopted piecemeal, not wholesale, since the decay/retention/cascade machinery has no OKF equivalent and is worth keeping.
- `kb_schema_version` lives only on `wiki/index.md` (bundle-level), not on every page, matching OKF's `okf_version` convention — and is dropped (not relocated) when exporting to OKF, since OKF's root index.md frontmatter is spec-limited to `okf_version` only.
- OKF export lives in `.claude/skills/export-okf/`, project-scoped rather than global, so it stays co-versioned with this repo's schema.
- Git-level sync lives in `.claude/skills/sync-changes/`, project-scoped for the same reason, and is explicitly authorized to auto-commit/auto-push (unlike export-okf) per direct user confirmation — but never force-pushes and always stops to ask on the first-run unrelated-histories case.