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:
parent
da4f334a0c
commit
c9f6186b4b
31 changed files with 1691 additions and 88 deletions
|
|
@ -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",
|
||||
|
|
|
|||
17
README.md
17
README.md
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
181
plugins/cascade-knowledge-base/skills/ckb-export-okf/SKILL.md
Normal file
181
plugins/cascade-knowledge-base/skills/ckb-export-okf/SKILL.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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""
|
||||
|
||||
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()
|
||||
170
plugins/cascade-knowledge-base/skills/ckb-init/SKILL.md
Normal file
170
plugins/cascade-knowledge-base/skills/ckb-init/SKILL.md
Normal 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.
|
||||
|
|
@ -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
|
||||
---
|
||||
|
||||
|
|
@ -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".
|
||||
---
|
||||
|
||||
143
plugins/cascade-knowledge-base/skills/ckb-onboard-me/SKILL.md
Normal file
143
plugins/cascade-knowledge-base/skills/ckb-onboard-me/SKILL.md
Normal 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 2–3 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]
|
||||
|
||||
[2–3 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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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".
|
||||
---
|
||||
|
||||
199
plugins/cascade-knowledge-base/skills/ckb-sync-changes/SKILL.md
Normal file
199
plugins/cascade-knowledge-base/skills/ckb-sync-changes/SKILL.md
Normal 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.
|
||||
5
plugins/common/.claude-plugin/plugin.json
Normal file
5
plugins/common/.claude-plugin/plugin.json
Normal 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."
|
||||
}
|
||||
5
plugins/doc-help/.claude-plugin/plugin.json
Normal file
5
plugins/doc-help/.claude-plugin/plugin.json
Normal 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."
|
||||
}
|
||||
62
plugins/doc-help/skills/tldr-this/SKILL.md
Normal file
62
plugins/doc-help/skills/tldr-this/SKILL.md
Normal 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.
|
||||
|
|
@ -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."
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"name": "interview",
|
||||
"version": "1.0.0",
|
||||
"description": "Run a structured interview and save the captured knowledge to a markdown file."
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"name": "meeting-notes",
|
||||
"version": "1.0.0",
|
||||
"description": "Interview a participant about a meeting and produce structured meeting notes."
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue