Restructure plugins into doc-help, common, cascade-knowledge-base bundles

Consolidates ghost-writer/resolve-todos/tldr-this into doc-help,
hr-manager/interview into common, and the ckb-*/meeting-notes/meeting-prep/quiz
skills into cascade-knowledge-base (renaming the latter three to ckb-meeting-notes,
ckb-meeting-prep, ckb-quiz for naming consistency). Updates marketplace.json and
README.md to match the new plugin set.
This commit is contained in:
Michał Kopeć 2026-07-16 16:44:54 +02:00
parent da4f334a0c
commit c9f6186b4b
31 changed files with 1691 additions and 88 deletions

View file

@ -6,9 +6,21 @@
"description": "A personal marketplace of Claude Code skills for interviews, meetings, and writing.",
"plugins": [
{
"name": "ghost-writer",
"source": "./plugins/ghost-writer",
"description": "Rewrite and humanize text to remove AI writing patterns and match your own voice.",
"name": "cascade-knowledge-base",
"source": "./plugins/cascade-knowledge-base",
"description": "Bootstrap, sync, export, and query a Cascade Knowledge Base wiki - init, ingest sync, OKF/Starlight export, guided onboarding tours, project summaries, meeting notes/prep, and quizzes.",
"version": "1.0.0"
},
{
"name": "common",
"source": "./plugins/common",
"description": "Run mock job interviews with a scored assessment and learning plan, and run structured interviews that capture knowledge to a markdown file.",
"version": "1.0.0"
},
{
"name": "doc-help",
"source": "./plugins/doc-help",
"description": "Rewrite and humanize text to remove AI writing patterns, produce short TL;DR summaries, and find and resolve TODO items in a document.",
"version": "1.0.0"
},
{
@ -17,42 +29,6 @@
"description": "Check KSeF invoices against contractors/white-list and audit team timesheet completion via Tempo.",
"version": "1.0.0"
},
{
"name": "hr-manager",
"source": "./plugins/hr-manager",
"description": "Run mock job interviews and produce a scored assessment with a prioritised learning plan.",
"version": "1.0.0"
},
{
"name": "interview",
"source": "./plugins/interview",
"description": "Run a structured interview and save the captured knowledge to a markdown file.",
"version": "1.0.0"
},
{
"name": "meeting-notes",
"source": "./plugins/meeting-notes",
"description": "Interview a participant about a meeting and produce structured meeting notes.",
"version": "1.0.0"
},
{
"name": "meeting-prep",
"source": "./plugins/meeting-prep",
"description": "Generate a prioritised list of questions for an upcoming meeting based on wiki knowledge gaps.",
"version": "1.0.0"
},
{
"name": "quiz",
"source": "./plugins/quiz",
"description": "Quiz the user on knowledge captured in the wiki, with scoring and immediate feedback.",
"version": "1.0.0"
},
{
"name": "resolve-todos",
"source": "./plugins/resolve-todos",
"description": "Find and resolve TODO items in a document, asking for input when instructions are unclear.",
"version": "1.0.0"
},
{
"name": "timesheet",
"source": "./plugins/timesheet",

View file

@ -7,14 +7,10 @@ writing, and invoice/timesheet checks. Each skill is packaged as its own install
| Plugin | Description |
| --- | --- |
| `ghost-writer` | Rewrite and humanize text to remove AI writing patterns and match your own voice. |
| `hr-manager` | Run mock job interviews and produce a scored assessment with a prioritised learning plan. |
| `interview` | Run a structured interview and save the captured knowledge to a markdown file. |
| `cascade-knowledge-base` | Bootstrap, sync, export, and query a Cascade Knowledge Base wiki - init, ingest sync, OKF/Starlight export, guided onboarding tours, project summaries, meeting notes/prep, and quizzes. |
| `common` | Run mock job interviews with a scored assessment and learning plan, and run structured interviews that capture knowledge to a markdown file. |
| `doc-help` | Rewrite and humanize text to remove AI writing patterns, produce short TL;DR summaries, and find and resolve TODO items in a document. |
| `ivona` | Check KSeF invoices against contractors/white-list and audit team timesheet completion via Tempo. |
| `meeting-notes` | Interview a participant about a meeting and produce structured meeting notes. |
| `meeting-prep` | Generate a prioritised list of questions for an upcoming meeting based on wiki knowledge gaps. |
| `quiz` | Quiz the user on knowledge captured in the wiki, with scoring and immediate feedback. |
| `resolve-todos` | Find and resolve TODO items in a document, asking for input when instructions are unclear. |
| `timesheet` | Check and log your own Tempo timesheet, and see what to put on your invoice for a given month. |
## Installing
@ -23,7 +19,7 @@ Add this marketplace locally and install a plugin:
```bash
/plugin marketplace add "/Users/michalkopec/Documents/Projects/Skills Marketplace"
/plugin install ghost-writer@skills-marketplace
/plugin install doc-help@skills-marketplace
```
This marketplace is published at `git.wierzbowa.cloud`. Since that's not GitHub, the
@ -43,7 +39,6 @@ across all plugins.
## Licensing
All plugins (`ghost-writer`, `hr-manager`, `interview`, `ivona`, `meeting-notes`,
`meeting-prep`, `quiz`, `resolve-todos`, `timesheet`) are licensed under the MIT
License — see [LICENSE](LICENSE).
All plugins (`cascade-knowledge-base`, `common`, `doc-help`, `ivona`, `timesheet`)
are licensed under the MIT License — see [LICENSE](LICENSE).

View file

@ -0,0 +1,5 @@
{
"name": "cascade-knowledge-base",
"version": "1.0.1",
"description": "Bootstrap, sync, export, and query a Cascade Knowledge Base wiki - init, ingest sync, OKF/Starlight export, guided onboarding tours, project summaries, meeting notes/prep, and quizzes."
}

View file

@ -0,0 +1,181 @@
---
name: ckb-export-okf
description: Export the local wiki/ knowledge base as an Open Knowledge Format (OKF) v0.1-conformant bundle at outputs/okf/, so any generic OKF tool can consume it. Use when the user asks to "export the wiki as OKF", "generate the OKF bundle", "export to OKF", "publish the OKF export", or "sync outputs/okf".
---
# Export to OKF skill
## Purpose
Regenerate `outputs/okf/` from the current `wiki/` tree as a bundle that
conforms to Google's Open Knowledge Format (OKF) v0.1 spec
(https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md),
so any generic OKF consumer (e.g. Google's reference graph visualizer) can
read this knowledge base without needing to understand this repo's richer,
more opinionated schema (`confidence`, `quality`, `retention`, `supersedes`,
dual-linking, the cascade layers). This is a one-way, on-demand export —
`wiki/` stays the authoritative source; `outputs/okf/` 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, so its
mapping ruleset doesn't tax every session's context.
## Trigger phrases
Use this skill when the user says things like:
- "export the wiki as OKF" / "export to OKF"
- "generate the OKF bundle" / "regenerate outputs/okf"
- "publish the OKF export"
- "sync outputs/okf with the wiki"
## How to run this skill
### Step 1 — Read the source tree
Read every file under `wiki/` (the whole tree, including `entities/`,
`graph/`, and any other subdirectories present). Note which filenames are
`index.md` or `log.md` at any depth (these are OKF's two reserved names) —
everything else is a concept document.
### Step 2 — Clear and rebuild `outputs/okf/`
`outputs/okf/` is a pure, fully-regenerated build artifact. Delete its
current contents (if any) and rebuild it from scratch by mirroring `wiki/`'s
directory structure 1:1 under `outputs/okf/`. Never hand-patch an existing
export — always regenerate the whole bundle, so it can't accumulate stale
files left over from wiki pages that were since renamed or removed.
Do not touch anything outside `outputs/okf/`. Do not run any `git`
commands — regenerating files is this skill's job; staging and committing
the result is a separate, explicit action left to the user.
### Step 3 — Transform concept documents (every file that isn't `index.md`/`log.md`)
For each such file, rewrite its frontmatter using this field mapping:
| wiki/ field | OKF output field | Rule |
|---|---|---|
| `type` | `type` | passthrough (required on both sides — every wiki page should already have one; if a page is somehow missing it, use `type: unknown` and flag it in the final report rather than skipping the file) |
| `resource` | `resource` | passthrough |
| `tldr` | `description` | rename |
| `last_updated` | `timestamp` | passthrough as-is (bare `YYYY-MM-DD` is a valid ISO 8601 date — do not fabricate a time-of-day that was never recorded) |
| *(none — derive)* | `title` | the first `# H1` heading in the body; if there is none, slugify the filename (e.g. `foo-bar.md` → "Foo Bar") |
| `confidence`, `quality`, `retention`, `supersedes`, `superseded_by`, `freshness_window_days` | same key names | passthrough unchanged, as OKF extension fields — OKF requires consumers to tolerate unrecognized keys, so these ride along as-is |
| `kb_schema_version` (only ever present on `wiki/index.md`) | *(omit)* | OKF's root `index.md` frontmatter is spec-limited to `okf_version` only; this key has no valid home in the bundle and is dropped, not relocated |
| *(none)* | `tags` | omit — there is no source field to derive it from; do not fabricate |
Then rewrite the body's links:
1. **Strip wikilinks.** Every dual-link in the source looks like
`[[Page Name]] / [Page Name](path.md)`. Delete the `[[...]]` half
(and the ` / ` separator if present), keep only the
`[text](path.md)` half. OKF has no wikilink concept.
2. **Rewrite repo-root-absolute intra-wiki links.** A link like
`/wiki/entities/foo.md` becomes `/entities/foo.md` — the OKF bundle
root is `outputs/okf/`, not the repo root, so the leading `/wiki`
segment must be stripped. Plain relative links (e.g.
`../entities/foo.md`) need no change, since the export mirrors
`wiki/`'s tree exactly.
3. **Leave `linked/<name>/...` and `libs/<name>/...` references
untouched.** These point outside `wiki/` and outside any
self-contained bundle — converting or inlining them is out of scope.
OKF explicitly tolerates broken links, so leaving these as
unresolvable-outside-the-repo references is spec-compliant. Count
how many of these you leave untouched, for the final report.
### Step 4 — Regenerate `index.md` files
**Root `outputs/okf/index.md`:** frontmatter contains *only*
`okf_version: "0.1"` — this is the one documented exception to "index.md
has no frontmatter" in the OKF spec. Body is a flat bullet list, one line
per linked page or subdirectory, in the form
`* [Title](path) - one-line description` (reuse each page's `description`,
i.e. the renamed `tldr`, as the description text). Drop the source
`wiki/index.md`'s "Use when" column entirely — it's a Claude-agent-only
lazy-loading optimization with no meaning to a generic OKF consumer — and
drop any prose sections (like the source's "## Entity Pages" paragraph).
**Every nested `index.md`** (`entities/index.md`, `graph/index.md`, and
any future topic-folder index): no frontmatter, ever. Strip the source's
explanatory prose and italic placeholder notes (e.g.
"*(No entities yet — populated on the next ingest.)*"), keeping only the
bullet list — an empty list is spec-valid if the directory has no pages
yet.
### Step 5 — Regenerate `log.md` files
For each `log.md` in the source tree (root and any subdirectory), reformat
its entries into OKF's convention: group entries by date under
`## YYYY-MM-DD` headers (date only, no time, newest date first), each
entry as a bullet:
```
* **Verb**: description with [links](/path/to.md) (source: original source text).
```
Map the source's `ACTION TYPE` to an OKF verb: `CREATE` → Creation,
`UPDATE` → Update, `DELETE` → Deprecation, `RESTRUCTURE` → Update (fallback
for anything else). Fold the source entry's `- **Source:**` line in as the
trailing parenthetical shown above rather than dropping it — it's useful
provenance information and OKF's format has room for free text after the
verb. Drop the `---` horizontal-rule separators between entries; they are
not part of OKF's log format.
### Step 6 — Handle non-reserved special pages
`wiki/error-book.md` and `wiki/overview.md` are not OKF reserved
filenames (only `index.md` and `log.md` are) — export them as ordinary
concept documents using the Step 3 rules like any other page. They should
already carry `type`/`tldr`/`last_updated` frontmatter; if you ever find
one that doesn't, that's a lint problem in the source wiki — flag it in
the report rather than silently patching the export.
### Step 7 — Validate the output bundle
Before reporting done, re-check the *generated* `outputs/okf/` bundle
against OKF's own conformance criteria (the same shape of check `CLAUDE.md`
§5 step 1 runs against the source wiki):
- Every non-reserved `.md` file has frontmatter with a non-empty `type`.
- The root `index.md`'s frontmatter contains only `okf_version` (or is
empty); every nested `index.md` has no frontmatter at all.
- Every `log.md` entry matches the `## YYYY-MM-DD` header pattern.
- Every intra-bundle link (i.e. every link you did NOT leave untouched in
Step 3.3) resolves to a file that actually exists in `outputs/okf/`. If
the rewrite in Step 3.2 produced a link that doesn't resolve, that's a
bug in this export, not an acceptable "broken link" — fix it before
reporting done, don't just note it as a warning.
### Step 8 — Report
Tell the user:
- How many concept documents were exported.
- How many `index.md`/`log.md` files were regenerated.
- How many `linked/`/`libs/` cross-cascade references were left
unconverted (Step 3.3), since those won't resolve if the bundle is ever
moved or shipped standalone.
- Any conformance issues found in Step 7 and whether they were fixed.
- A reminder that `outputs/okf/` has been regenerated on disk but not
staged or committed — that's a separate step for the user to take when
ready to publish the update.
## Edge cases
- **Empty `wiki/entities/` or `wiki/graph/`** (as of writing, both are
empty): still regenerate their `index.md` as an empty bullet list under
`outputs/okf/entities/` and `outputs/okf/graph/` — an empty index is
spec-valid, don't skip the directory entirely.
- **A wiki page missing `type`:** per Step 3, use `type: unknown` and flag
it in the report — this indicates the source wiki itself failed lint's
conformance check (see `CLAUDE.md` §5 step 1), which is worth surfacing
to the user rather than quietly masking it in the export.
- **A future `wiki/archived/` directory:** export it like any other
subdirectory (mirror the structure, apply the same per-file rules) —
OKF has no notion of archival status; `retention`/`freshness_window_days`
already ride along as extension fields for any consumer that cares.
- **Re-running the skill with no wiki changes since the last run** should
produce byte-identical output — if you notice non-determinism (e.g. from
arbitrary ordering when listing directory entries), sort filenames
alphabetically wherever you're generating a bullet list or walking a
directory, so re-runs are stable.

View file

@ -0,0 +1,147 @@
---
name: ckb-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
`ckb-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 `ckb-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,170 @@
---
name: ckb-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:
`ckb-export-okf`, `ckb-export-starlight`, `ckb-sync-changes`, `extract-transcript`,
`ckb-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
`ckb-export-starlight` and `ckb-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

@ -1,5 +1,5 @@
---
name: meeting-notes
name: ckb-meeting-notes
description: Run an interview with the participant to collect the information from the meeting and prepare meeting notes in a dedicated form
---

View file

@ -1,5 +1,5 @@
---
name: meeting-prep
name: ckb-meeting-prep
description: Prepare questions for an upcoming meeting where the user will interview or question someone. Scans the wiki to identify knowledge gaps, then generates a prioritised question list aimed at filling those gaps. Use when asked to "prepare for a meeting with X", "what should I ask X", "help me prep for my meeting", or "generate questions for my interview with X".
---

View file

@ -0,0 +1,143 @@
---
name: ckb-onboard-me
description: Given a topic, walk the wiki knowledge graph and print a guided reading order — a mini tour. Produces a short overview of the topic plus the best sequence of wiki pages to read and what you'll find in each, ordered so prerequisites come before the pages that build on them. Use when the user says "onboard me on X", "give me a reading order for X", "where do I start with X", "walk me through X", "mini tour of X", "guided tour of X", or "what should I read to understand X".
---
# Onboard me (guided wiki tour) skill
## Purpose
Turn a topic into a **reading order**: a short scene-setting overview followed
by an ordered list of wiki pages to read, each with a one-line note on what
you'll find there and why it comes at that point. It's a mini onboarding tour
built by walking the knowledge graph outward from the topic, so a newcomer
learns things in a sensible order — foundations first, then the topic itself,
then what builds on it.
This skill is **read-only**. It never writes to `wiki/`, so it does not touch
`wiki/log.md`. It synthesizes a route through existing knowledge; it does not
add new claims. If the wiki has nothing on the topic, say so — don't invent a
tour.
## Trigger phrases
Use this skill when the user says things like:
- "onboard me on X" / "get me up to speed on X"
- "where do I start with X" / "what should I read to understand X"
- "give me a reading order for X" / "reading list for X"
- "walk me through X" / "mini tour of X" / "guided tour of X"
## How to run this skill
### Step 1 — Resolve the topic to a starting page
Read the cheap indexes first, in cascade order (first match wins):
1. `wiki/index.md` — match the topic against the **Use when** column.
2. `wiki/entities/index.md` — match against entity titles and their `tldr`.
3. If nothing local matches, check each `linked/<name>/` index, then each
`libs/<name>/` index (alphabetical). The local `wiki/` always wins if both
have a candidate.
Pick the single best-matching page as the **anchor** of the tour.
- **No match anywhere** — tell the user the wiki has nothing on that topic
yet. Offer the closest 23 pages by title/`tldr` if any are vaguely related,
and suggest running Ingest to add the material. Do not fabricate a tour.
- **Several equally good anchors** — list them (title + `tldr`) and ask which
one to start from, or offer to build a short tour that covers all of them.
### Step 2 — Walk the graph outward from the anchor
Read `wiki/graph/edges.json`. Edges are typed relationships between entity
pages (`uses`, `depends_on`, `caused`, `contradicts`, `supersedes`) with a
`source` and a `target`. Starting from the anchor, do a breadth-first walk:
- Collect every page directly connected to the anchor (anchor is `source` or
`target` of the edge).
- Follow one more hop out from those pages. **Stop at 2 hops** — a tour is a
route, not the whole map. Keep a visited set so cycles don't loop.
- Cap the tour at roughly **8 pages**. If the 2-hop neighbourhood is larger,
keep the pages closest to the anchor and highest `confidence` /
`retention: high`, and tell the user you trimmed it (say how many were left
out) rather than silently dropping them.
**If `edges.json` is missing or empty** (no ingest has extracted edges yet),
fall back to link-following: read the anchor page and collect the pages it
links to via `[[wikilinks]]` and `[markdown](path.md)` links, then one hop out
from those. Same 2-hop, ~8-page, visited-set rules apply. Note in the output
that ordering came from page links rather than the typed graph.
### Step 3 — Order the pages pedagogically
The reading order is the whole point — don't just dump the neighbourhood.
Order by dependency direction, not by hop distance alone:
1. **Foundations first.** A page the anchor `depends_on` or `uses` is a
prerequisite — it comes *before* the anchor. Recurse: a prerequisite's own
prerequisites come before it.
2. **The anchor** — the topic itself, once its groundwork is laid.
3. **Builds-on / related** — pages that `depend_on` or `use` the anchor, or
are related without a clear dependency, come after. These are "where to go
next."
4. Put `contradicts` / `superseded_by` links near the page they qualify, with
a note that they offer a competing or newer view.
Within a tier, order by `confidence` then `last_updated` (higher/newer first)
so the most solid reading comes first. Read each page's `tldr` (frontmatter)
to write its one-line "what you'll find" — only open the body if the `tldr` is
missing or too thin to describe the page.
### Step 4 — Print the tour
Print to chat (this skill produces a message, not a file). Use this shape:
```markdown
## Guided tour: [Topic]
[23 sentence overview: what the topic is and what this tour will teach you,
synthesized from the anchor page's tldr and its immediate context.]
**Reading order:**
1. **[Page title](wiki/entities/foo.md)** — *foundation.* [what you'll find,
and why it's first — e.g. "the concept everything here builds on."]
2. **[Page title](wiki/entities/bar.md)** — *the core.* [what you'll find.]
3. **[Page title](wiki/entities/baz.md)** — *builds on it.* [what you'll find.]
...
**After this you'll understand:** [one line on the payoff — what the reader
can do or reason about once they've read the sequence.]
```
Rules for the output:
- Use clickable markdown links to the actual pages (project-root-relative,
e.g. `wiki/entities/foo.md`), so the reader can jump straight in.
- Label each step with its role in the tour (*foundation* / *core* /
*builds on it* / *alternative view*) so the ordering logic is visible.
- Keep each "what you'll find" to one line, drawn from the page's `tldr`.
- If pages came from `linked/` or `libs/`, link them with their full cascade
path and mark them as upstream.
### Step 5 — Offer follow-ups
Close with a short offer: to go deeper on any single step, to widen the tour
(3+ hops), or to start a different topic. Don't act on these unless asked.
## Edge cases
- **Anchor has no edges and no outgoing links** — the tour is just the anchor.
Print its overview and say it's currently a standalone page with no mapped
connections; suggest an Ingest may add links later.
- **Topic spans several disconnected clusters** — if the graph walk finds two
unrelated groups, present them as two short mini-tours under one topic rather
than forcing a single misleading sequence.
- **Broken edge (points to a missing page)** — skip it silently in the tour,
but mention at the end that the graph has a dangling edge so the user can run
Lint. Do not fabricate the missing page.
- **Very fresh material in `workload/` not yet in the wiki** — this skill tours
`wiki/` only. If the user expects a topic that was just discussed but not yet
ingested, note that it isn't in the wiki yet and point them at Ingest.
- **Upstream-only topic** — if the anchor lives in `linked/`/`libs/` with no
local overlay, build the tour from the upstream KB's own index/links and mark
every step as upstream/read-only.

View file

@ -0,0 +1,177 @@
---
name: ckb-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.

View file

@ -1,5 +1,5 @@
---
name: quiz
name: ckb-quiz
description: Quiz the user on knowledge captured in the wiki. Reads wiki pages, generates questions in the user's chosen format (open questions or multiple choice), runs the quiz one question at a time with score tracking and immediate feedback. Use when the user asks for a quiz, wants to test their knowledge, or says "quiz me on X".
---

View file

@ -0,0 +1,199 @@
---
name: ckb-sync-changes
description: Reconcile this repo's git history with its origin remote — pull down remote commits, commit any local working-tree changes, resolve any conflicts with the user, then push. Use when the user asks to "sync changes", "sync with git", "sync with the remote", "sync with origin", "push and pull my changes", "reconcile git", or "sync the repo". This is a git-level operation, distinct from the content-level "Sync the wiki" / "Ingest" workflow in CLAUDE.md, which processes raw/inbox/ into structured wiki/ pages.
---
# Sync changes skill
## Purpose
Keep this repo's local working tree and its `origin` remote in sync in both
directions: pull remote commits down, commit local file changes, resolve any
conflicts with the user, then push. `wiki/`, `outputs/`, `workload/`, and
every other tracked path are treated as opaque files at this layer — this
skill only does git mechanics (stage/commit/fetch/merge/push). It never
invokes the Ingestion Workflow, never re-derives entities/graph/index/log
content, and never tries to semantically reconcile markdown or frontmatter —
a conflicted file is just text with conflict markers until the user says
otherwise.
Unlike the `ckb-export-okf` skill, this skill **does** commit and push on its
own once conflicts (if any) are resolved — that automation was explicitly
requested for this skill.
## Trigger phrases
Use this skill when the user says things like:
- "sync changes"
- "sync with git" / "sync with the remote" / "sync with origin"
- "push and pull my changes"
- "reconcile git"
- "sync the repo"
Do **not** use this skill for bare "Sync" or "Sync the wiki" — those trigger
the content-level Ingestion Workflow in `CLAUDE.md`/`AGENTS.md` §3 instead
(processing `raw/inbox/` into `wiki/`), which this skill has nothing to do
with.
## How to run this skill
### Step 1 — Pre-flight safety checks
Run:
```bash
git rev-parse --is-inside-work-tree
git status --porcelain=v1
```
Check for an in-progress merge/rebase/cherry-pick:
```bash
test -f .git/MERGE_HEAD && echo "MERGE IN PROGRESS"
test -f .git/REBASE_HEAD -o -d .git/rebase-merge -o -d .git/rebase-apply && echo "REBASE IN PROGRESS"
test -f .git/CHERRY_PICK_HEAD && echo "CHERRY-PICK IN PROGRESS"
```
If any of these report something in progress, **stop immediately**. Tell
the user a git operation is already underway and must be resolved or
aborted manually (`git merge --abort` / `git rebase --abort`) before running
this skill again. Do not try to clean it up yourself.
Scan the `git status --porcelain` output for paths that look like secrets:
`.env`, `.env.*`, anything containing `credentials`, `*.pem`, `*.key`,
`id_rsa*`, anything containing `secret` (case-insensitive). If any match,
ask the user via `AskUserQuestion` whether to include or skip each one
*before* staging anything in Step 3 — never silently commit or silently
drop a flagged file.
### Step 2 — Detect the first-run / unrelated-histories case
```bash
git fetch origin
git rev-parse HEAD # fails with "unknown revision" if local has no commits yet
git rev-parse origin/main # fails if the remote branch doesn't exist/is empty
```
- **Local `HEAD` exists** → go to Step 3, regardless of remote state.
- **Local `HEAD` doesn't exist, and `origin/main` doesn't exist or is empty**
→ this is a plain first publish, not a reconciliation. Proceed
automatically to Step 3 (it will just commit and push with nothing to
merge).
- **Local `HEAD` doesn't exist, but `origin/main` already has commits**
**hard stop, every time this is detected.** Show the user:
```bash
git log origin/main --oneline | head -20
git status --porcelain # everything currently on local disk, untracked
```
Then ask via `AskUserQuestion` with exactly these three options:
- "Merge — bring in origin/main's history (`git merge --allow-unrelated-histories origin/main` after committing local content), then resolve any conflicts"
- "Remote wins — check out origin/main first (`git reset --hard` is not needed since there's no local commit yet; instead branch from origin/main), then reapply my local changes on top of it"
- "Stop — I'll sort this out myself"
Do **not** offer a fourth "discard remote, force local to become main"
option — that requires a force-push and is out of scope for this skill;
if the user wants that, tell them it needs to be done manually. Execute
only the option chosen, then continue to Step 3 with whichever git state
results.
### Step 3 — Steady-state flow
Order matters: **commit local changes first, then fetch/merge, then push.**
Committing first turns any overlap into an ordinary merge conflict (which
Step 4 already knows how to present), rather than a stash-pop conflict with
no commit boundary to fall back on.
```bash
# Commit local changes, if any (skip entirely if git status --porcelain is clean)
git add <path1> <path2> ... # explicit paths only — never `git add -A` or `git add .`
git commit -m "Sync: local changes as of <ISO timestamp>"
# Reconcile with remote
git fetch origin
git merge origin/main # merge, never rebase — rebase would rewrite local commits
# and require a force-push, which is out of scope here
```
Outcomes of the merge:
- `Already up to date.` or a clean auto-merge → continue below.
- `CONFLICT` → go to Step 4, then come back here once every conflict is
resolved and committed.
```bash
# Push
git push origin main
```
If the push is rejected as non-fast-forward (the remote moved again between
fetch and push), retry the fetch → merge → push cycle **exactly once**. If
it fails again, stop and report the raw error to the user — never force,
never retry more than once.
### Step 4 — Present each conflict to the user
```bash
git diff --name-only --diff-filter=U
```
For each path returned, read the full file. It may contain more than one
conflict block — handle each independently, leaving all surrounding
unconflicted content untouched. Each block looks like:
```
<<<<<<< HEAD
(local text)
=======
(remote text)
>>>>>>> origin/main
```
For each block:
- Call `AskUserQuestion` with the file path (and block number if the file
has more than one) as context, showing both the local and remote text in
full. Options: **"Keep local version"**, **"Keep remote version"**,
**"Provide merged text"**.
- If "Provide merged text" is chosen, ask the user to supply the
replacement text directly.
- Rewrite the block with the chosen/provided content and strip the three
marker lines for that block.
- If the conflict is on a binary file or the markers can't be cleanly
parsed, skip the "Provide merged text" option and instead resolve with
`git checkout --ours <path>` or `git checkout --theirs <path>` per the
user's local/remote choice.
Once every block in a file is resolved, `git add` that file immediately —
don't wait for every file to be done before staging the first one, so
progress is always visible in `git status`. Once every conflicted file is
staged, commit:
```bash
git commit -m "Merge origin/main (conflicts resolved with user input)"
```
Then return to Step 3's push.
### Step 5 — Report
End with a structured summary:
```
## Sync report
**Pulled from origin/main:** <N> commit(s) — <oneline log, or "none, already up to date">
**Committed locally:** <N> file(s) — <paths>, commit <short-hash> (or "no local changes to commit")
**Conflicts encountered:** <N> (or "none")
- <path> (block N of M) — resolved: kept local / kept remote / custom merged text
**Secrets flagged:** <files and the user's choice, or "none">
**Push result:** origin/main now at <short-hash> (or the retry/failure detail if it didn't succeed)
```
If anything was committed under `wiki/` without a corresponding
`wiki/log.md` (or subdirectory `log.md`) update in the same diff, add a
one-line nudge to run Ingest/Lint afterward — this is a suggestion, not a
blocker.
## Edge cases
- **Nothing to do on either side** — report "already in sync," make no
commits, run no push.
- **Local changes only, remote unchanged** — commit, fast-forward push, no
merge/conflict step needed.
- **Remote changes only, local clean** — fast-forward merge, nothing to
commit, nothing to push; report "pulled N commits, nothing local to
push."
- **Push rejected twice in a row** — stop, report the raw git error, do not
force and do not retry a third time.
- **No remote configured, or fetch/push fails on auth/network** — report
the error clearly. This skill does not manage git credentials.

View file

@ -0,0 +1,5 @@
{
"name": "common",
"version": "1.0.1",
"description": "Run mock job interviews with a scored assessment and learning plan, and run structured interviews that capture knowledge to a markdown file."
}

View file

@ -0,0 +1,5 @@
{
"name": "doc-help",
"version": "1.0.1",
"description": "Rewrite and humanize text to remove AI writing patterns, produce short TL;DR summaries, and find and resolve TODO items in a document."
}

View file

@ -0,0 +1,62 @@
---
name: tldr-this
description: Produce a short TL;DR summary of a document or block of text. Use this whenever the user asks to "tldr this", "summarize this", "give me the gist", "what does this say", "boil this down", or pastes a long block of text, article, or points at a file and wants the short version instead of the whole thing. Make sure to use this any time the deliverable is a compact summary rather than analysis, critique, opinions, or a rewrite — even if the user doesn't say the word "summary."
---
# TLDR This skill
## Purpose
Turn a document or block of text into a short, faithful summary — compression,
not commentary. The output should let someone skip reading the source
entirely and still walk away with the point.
## How to run this skill
### Step 1 — Get the source
- **Pasted text** — use it as-is.
- **A file path** — read it in full. For binary formats (`.docx`, `.pdf`,
`.pptx`, etc.) use the matching skill or tool to extract the text first;
don't guess at content from a filename.
- **A URL** — fetch it.
- **Multiple documents at once** — summarize each separately (one TLDR block
per document) unless the user asked for a single combined takeaway.
### Step 2 — Write the summary
- Lead with one bolded sentence: the single most important takeaway — the
thing someone would need to know if they read nothing else.
- Follow with a few short bullets for supporting detail — key facts, numbers,
decisions, or action items — but only if the source is substantial enough
that the one-liner alone would lose something worth keeping. A short input
(an email, a paragraph) often needs just the one-liner and nothing else.
- Match length to source length. Don't stretch a two-paragraph memo into a
six-bullet report, and don't compress a 40-page document into one line
without at least noting what's being left out.
- Stick to what the source actually says. Don't add analysis, opinions, or
inferences it doesn't support — if the user wants that, it's a different
request than a TLDR.
- If the source is ambiguous or missing context needed to summarize it
accurately (e.g., a code diff with no description of intent), say what's
unclear rather than guessing at the missing part.
## Output format
```markdown
**[One-sentence takeaway.]**
- [supporting point]
- [supporting point]
```
Omit the bullets entirely for inputs short enough that the one-liner already
says everything.
## Edge cases
- **Empty or near-empty input** — say so; don't fabricate a summary.
- **Very long or multi-section input** — group bullets by section if that's
clearer, but keep the total short. A "TLDR" as long as the source has
failed at its one job.
- **Source is already short/is itself a summary** — say that directly rather
than padding out a summary-of-a-summary.

View file

@ -1,5 +0,0 @@
{
"name": "ghost-writer",
"version": "1.0.0",
"description": "Rewrite and humanize text to remove AI writing patterns and match your own voice."
}

View file

@ -1,5 +0,0 @@
{
"name": "hr-manager",
"version": "1.0.0",
"description": "Run mock job interviews and produce a scored assessment with a prioritised learning plan."
}

View file

@ -1,5 +0,0 @@
{
"name": "interview",
"version": "1.0.0",
"description": "Run a structured interview and save the captured knowledge to a markdown file."
}

View file

@ -1,5 +0,0 @@
{
"name": "meeting-notes",
"version": "1.0.0",
"description": "Interview a participant about a meeting and produce structured meeting notes."
}

View file

@ -1,5 +0,0 @@
{
"name": "meeting-prep",
"version": "1.0.0",
"description": "Generate a prioritised list of questions for an upcoming meeting based on wiki knowledge gaps."
}

View file

@ -1,5 +0,0 @@
{
"name": "quiz",
"version": "1.0.0",
"description": "Quiz the user on knowledge captured in the wiki, with scoring and immediate feedback."
}

View file

@ -1,5 +0,0 @@
{
"name": "resolve-todos",
"version": "1.0.0",
"description": "Find and resolve TODO items in a document, asking for input when instructions are unclear."
}