Compare commits

...
Sign in to create a new pull request.

19 commits

Author SHA1 Message Date
6c0d70976c Add release channels: main, test, experimental
The template repo now keeps three branches with fixed meanings — main is
stable, test is the release candidate, experimental is development — and
ckb-init/ckb-upgrade can source from any of them instead of only main.

Selection is per-invocation, in words the user already uses ("initialize
from the test branch", "check experimental for updates", "switch back to
stable"), and sticky: the resolved repo and branch are written to a
template: block in ckb.yaml. Without persistence, a KB bootstrapped from
experimental would be silently pulled back to main by its next upgrade.
A missing file or missing block both mean main, so every KB predating
this convention behaves exactly as before.

One consequence needed explicit handling. A KB tracking test or
experimental can sit on a VERSION main has not released yet, so comparing
it against main finds nothing newer — which the version check would have
reported as "up to date". That is true and misleading. ckb-upgrade now
reports it as "ahead", and treats a move back to main as a downgrade:
explicitly confirmed, with the specific losses named, and blocked
outright where kb_schema_version would drop below what local pages are
already written against.

ckb-module is told not to clobber the template: block — a module install
that silently reset a KB's channel would change what its next upgrade
pulls, which is not a module's business.

Documented in both READMEs, both MANUALs and both CHANGELOGs. VERSION
1.8.0 -> 1.9.0; kb_schema_version stays 1.5, since this is tooling rather
than a content contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 08:29:45 +02:00
d66ebf6972 Reset wiki to clean template scaffold (Tier 1)
Clears the 3 wiki/log.md entries recording the schema 1.4 -> 1.5 migration.
That was the entirety of Tier 1 content: no entity pages, project scopes,
decision records or graph edges existed to remove.

Only wiki/log.md changed. The other scaffold files already matched what
ckb-init produces at schema 1.5, and edges.json/overview.md keep their
existing last_updated dates because those dates are accurate — bumping them
would assert changes that did not happen.

Rule B would normally require a wiki/log.md entry for a change inside wiki/,
but writing "emptied the log" into the log it just emptied defeats the
reset. The record is in workload/2026-09-22_summary.md instead, and the
migration itself remains documented in CHANGELOG.md and in commits 474630e
and 2c4d57a.

Tier 2 (workload summaries), the template layer, schema 1.5 and template
version 1.8.0 are all preserved. Lint: 0 findings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 06:55:19 +02:00
7d3fa5484c Add session summary for 2026-09-22
Records the cancelled reset (inventory run, nothing deleted) and the
fast-forward merge of graft-ideas into main pushed to both remotes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 06:44:15 +02:00
2c4d57acbc Add bilingual CHANGELOG with full schema and version history
Two version numbers exist in this project and are easy to confuse:
kb_schema_version (the content contract, in wiki/index.md) and VERSION
(the tooling layer). Until now neither had a written history, and the
page schema was documented in four places at three levels of detail.

CHANGELOG.md / CHANGELOG.pl.md consolidate both: the current schema in
full (frontmatter for all pages and for decisions, the three reserved
body sections, the closed edge vocabulary with a "since" column, the
reserved scaffold), then the schema history 1.1-1.5 and the template
history 1.0.0-1.8.0.

The history is reconstructed from git rather than from memory, so it
records what actually happened rather than a tidied version of it:

- There was never a schema 1.0; versioning began at 1.1 on 2026-07-13.
- Template 1.4.0 and 1.5.0 were never published — VERSION jumped from
  1.3.0 to 1.6.0 on 2026-09-01.
- Connector-backed libs shipped as tooling in 1.1.0, but the schema only
  recorded them at 1.2 nine days later. The schema column shows what was
  in effect after each release, with a footnote on the lag.

Registered with ckb-init (copied verbatim into a new KB) and ckb-upgrade
(taken wholesale from upstream rather than merged, since upstream is
authoritative about its own history). Cross-linked from both READMEs and
both MANUALs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 23:16:43 +02:00
474630e2bb Adopt seven ideas from trailhq/Graft; schema 1.4 -> 1.5
Graft keeps a derived, disposable code graph in sync with a content hash
rather than a calendar, and keeps a protected block on every regenerated
node. This KB is the opposite kind of store — durable, curated, built from
material that cannot be regenerated — but several of Graft's mechanisms
port cleanly, and two of them close real gaps here.

Schema 1.5 is additive: every 1.4 page remains valid.

1. `## Crux` — verbatim source excerpts alongside the synthesis. A summary
   can drift silently; a quote either still matches its source or it does
   not. Lets `ckb-retrieve` ground an answer without a round-trip to the
   archive, and makes drift mechanically detectable.
2. `## Notes` — human-authored and protected everywhere. Closes a real
   gap: `ckb-index-external` regenerates connector pages wholesale, so an
   annotation written there was previously destroyed on the next refresh.
3. `source_fingerprint`/`source_checked` — a digest of the material a page
   was built from. Freshness by date says a page has aged; a fingerprint
   says whether its evidence moved. Most valuable for connector-backed
   libs, where documents change with no notice.
4. `lint_report.py --quick` — a deterministic one-line session-start
   signal, wired into Rule E next to the existing `git status` check.
5. In-degree as a rank-fusion signal in `ckb-retrieve`, weighted below 1.0:
   centrality is a prior, not evidence.
6. Blast radius — a new `ckb-ingest` step walking the graph backwards from
   touched entities to find what the incoming material contradicts, before
   writing anything. Ingest was additive-first, which is how a wiki
   accumulates two pages that quietly disagree.
7. Edge vocabulary in `wiki/graph/index.md` rewritten as a question per
   verb, and completed: `part_of` was written by `ckb-code-map` but never
   declared. Added `produces`, `configures`, `validates`, `implements`.

Lint gains checks 12 (fingerprint drift), 13 (crux verbatimness) and 14
(the protected-Notes rule), verified against a synthetic fixture covering
stale digests, missing sources, fabricated quotes and paraphrased evidence.

Not adopted: the gitignored regenerable store, the MCP server and CLI
daemon, tree-sitter parsing, statusline hooks, telemetry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 22:44:26 +02:00
c998489c9a Reset wiki to clean template scaffold
Empty the KB back to the shape ckb-init creates: remove decision records
D-0001 and D-0002, the single graph edge, all five workload summaries and
the generated outputs/okf/ tree, then restore the empty scaffold with
kb_schema_version held at 1.4.

The decisions status-vocabulary table is preserved verbatim, since
ckb-decide and ckb-lint both validate against it. Lint reports 0 findings.

Restore point for the pre-reset content: tag pre-reset-2026-09-20 (0c06cb6).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 17:59:14 +02:00
0c06cb64ab Restore point before wiki reset
Commit all in-flight work — ckb-module and ckb-reset skills, the
.agents/modules/ scaffold, OPENSPEC docs, decision records D-0001 and
D-0002, graph edges and workload summaries — so the reset that follows
is fully recoverable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 17:56:41 +02:00
491ca8a15f Fix false-positive broken-edge findings in ckb-lint graph check
Graph edge endpoints use project-root-absolute paths per Rule C
(`/wiki/entities/foo.md`), while the `known` page set is keyed on paths
relative to `wiki/`. check_graph compared them directly, so every
conformant edge was reported as pointing at a missing page. Add the
`/wiki/`-stripped form to the candidate set, matching the normalisation
the decision-record check already performs.

Bump VERSION 1.6.0 -> 1.6.1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 23:24:46 +02:00
65b1e422b3 Add decision log, scriptify OKF export and lint detection
Decision log (VERSION 1.6.0, kb_schema_version 1.4):
- wiki/decisions/ scaffold — numbered NNNN-slug.md records, own index
  (with status vocabulary) and log
- type: decision adds status/decided_on/decided_by/affects/review_on;
  supersedes/superseded_by carry history and must be set on both sides
- New ckb-decide skill: records decisions and answers what/why/who/when,
  what superseded what, and what is still open. Decision pages are
  append-only — a changed mind is a new superseding decision
- Graph gains decided_by and affects edge types
- ckb-ingest routes decisions found in raw material to this format;
  ckb-retrieve gains the decisions index as a cascade step

Scriptified the mechanical skills:
- ckb-export-okf/scripts/export_okf.py does the whole OKF transform
  (frontmatter remap, link rewriting, index/log regeneration, conformance
  validation); --check validates without writing
- ckb-lint/scripts/lint_report.py does the read-only detection half
  (conformance, freshness, confidence, retention, decisions, orphans,
  graph, index/log, source.yaml); judgment calls stay with the model

Also: removed the duplicate personal quiz skill, fixed stale cbk-quiz
doc paths, gitignored __pycache__.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 20:50:36 +02:00
8174a54cee Merge origin/main (conflict resolved: kept remote superset of workload summary)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 20:17:37 +02:00
fe4058c3ef Sync: local changes as of 2026-09-01T20:17:16+02:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 20:17:16 +02:00
Michał Kopeć
dd570caf75 Rename cbk-quiz skill to ckb-quiz
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 14:28:26 +02:00
6dd8a30f3b Sync: local changes as of 2026-08-06T08:40:06+02:00 2026-08-06 08:40:06 +02:00
f4cabdaeab Sync: local changes as of 2026-07-29T23:13:00+02:00 2026-07-29 23:13:41 +02:00
be41d4877c Sync: local changes as of 2026-07-29T22:48:00+02:00 2026-07-29 22:49:15 +02:00
f879f859e3 Remove legacy Claude config files 2026-07-29 22:21:17 +02:00
0be7a60ac9 Record KB concept review session 2026-07-29 22:15:09 +02:00
Michał Kopeć
4e70a63ca4 Extract entities/processes in connector indexes, add source-verified retrieval
ckb-index-external now goes beyond one page per document: it also
extracts people, organizations, projects, decisions, systems, and
processes each document discusses into thin, pointer-style entity
pages within the connector's own index (evidence back to source
documents, deferring to a full wiki/entities/ page where one already
exists), so the index supports "what do we know about X" lookups, not
just "what documents exist here".

New ckb-retrieve skill formalizes the retrieval half of the query
workflow: before grounding an answer in a page's tldr, follow it back
to its underlying source (a wiki page's Sources citations, or a
connector page's resource: pointer) rather than trusting the
compressed index entry as settled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 08:55:12 +02:00
Michał Kopeć
f3dbce7327 Backport the check-then-fetch-or-create nuance into README/MANUAL docs
The last skill clarification (check the shared index store on every
run; fetch if it exists; a write-access user's run creates it there
the first time it's empty) hadn't been reflected in the user-facing
docs, which only described the steady-state case. Also documents the
optional index.ref field, mentioned in the skill schema but missing
from all four docs' examples.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 08:41:14 +02:00
49 changed files with 6770 additions and 494 deletions

View file

@ -0,0 +1,68 @@
# `software` module
Opt-in. Install with "install the software module"; remove with "uninstall the
software module". Nothing in here is active until installed — the skills below
live in this folder, not in `.agents/skills/`, precisely so their descriptions
stay out of context for knowledge bases that don't document software.
## What it assumes
You are building software, and this KB holds the knowledge *about* it. The code
itself lives in `src/<repo>/` as plain clones — **gitignored**, each with its own
remote and its own history. One KB can hold several.
The code is not part of the KB's history and may be absent entirely from a fresh
clone. That is deliberate, and it is why the module writes a `type: repository`
entity page for every repo: remote URL, default branch, language, build command,
path, owner. **That page is the durable artifact; the clone is a convenience.**
## What it adds
| Skill | Does |
|---|---|
| `ckb-code-map` | Reads a `src/<repo>` clone and writes it into `wiki/entities/` as a repository page plus component pages, stamped with the commit it was generated from so staleness is detectable. |
| `ckb-spec` | Owns KB-root specs, delegates per-repo specs to OpenSpec, and bridges both into `wiki/` — archived changes become decision records, current specs become `type: spec` pages. |
Plus: `src/` and `openspec/` scaffold, a routing block in `AGENTS.md`, `.gitignore`
rules for `src/*`, and the `repository`/`component`/`spec` types.
## Two spec levels
Specs live at **both** levels, and the split is the point:
- **`openspec/` at the KB root** — what and why, across repos. Cross-cutting
contracts that no single repo owns. Served by `ckb-spec` directly, because a
root `openspec/` is not a normal OpenSpec install: there is no code beneath it.
- **`src/<repo>/openspec/`** — how this repo implements it. OpenSpec's native
case: travels with the code, rides along in the repo's own PRs. Served by
OpenSpec's own instructions; `ckb-spec` defers to them and says so plainly when
the CLI isn't installed rather than improvising a replacement.
The levels are linked by `implements:` / `implemented_by:` frontmatter, which
becomes graph edges. This exists so that two spec levels produce a **lint signal**
when they disagree, instead of two silently divergent truths. A KB-root spec with
no implementer, or a repo-level spec whose parent was archived, is a finding.
## What it does not do
- **`src/` is not a cascade layer.** It is primary evidence, like `raw/archive/`.
`ckb-retrieve` may open and cite it to verify a claim; it never answers "what is
entity X", and `ckb-ingest` does not treat code as inbox material. Without this
rule every ingest would turn your codebase into wiki pages.
- **It does not commit or push `src/` repos.** They are separate repos with
separate remotes. `ckb-sync-changes` never `git add`s under `src/`.
## Setting up OpenSpec
See [OPENSPEC.md](../../../OPENSPEC.md) ([Polski](../../../OPENSPEC.pl.md)) for
install steps, the daily propose → apply → archive loop, and the reason
`openspec init` must never run at the KB root.
## Design record
- [D-0001](../../../wiki/decisions/0001-opt-in-file-based-kb-modules.md) — why modules are files in the repo.
- [D-0002](../../../wiki/decisions/0002-software-module-design.md) — why gitignored clones, both spec levels, hybrid vendoring.
---
*Licensed under the Apache License, Version 2.0 — see [LICENSE](../../../LICENSE).*

View file

@ -0,0 +1,23 @@
## MODULE: software
This KB documents software being built here. Source repos live in `src/<repo>/`
as **gitignored plain clones**, each with its own remote and history.
- `src/` is **evidence, not a cascade layer.** `ckb-retrieve` may open and cite
code to verify a claim; it never answers "what is entity X", and `ckb-ingest`
never treats code as inbox material.
- **Never `git add` under `src/`.** Those are separate repos. `ckb-sync-changes`
reports each one's dirty/ahead state separately and commits none of them.
- Every repo has a `type: repository` page in `wiki/entities/` — the durable
record, since the clone may be absent from a fresh checkout.
- Specs live at two levels: `openspec/` at the KB root (cross-cutting, owned by
`ckb-spec`) and `src/<repo>/openspec/` (per-repo, owned by OpenSpec itself).
They link via `implements:` / `implemented_by:`.
| User intent | Skill |
|---|---|
| Map or refresh a `src/` repo into wiki entities | `ckb-code-map` |
| Write, sync, or bridge specs (either level) | `ckb-spec` |
Module-contributed page types: `repository`, `component`, `spec`. Additional
optional frontmatter: `repo`, `commit`, `spec_id`, `implements`, `implemented_by`.

View file

@ -0,0 +1,7 @@
# Source repos are independent git clones with their own remotes — never part of
# this KB's history. Knowledge about them lives in wiki/entities/ instead.
src/*
!src/.gitadd
!src/README.md
# KB-root cross-cutting specs ARE tracked — they are knowledge, not code.
!openspec/

View file

@ -0,0 +1,59 @@
# Cascade KB module manifest — read by `ckb-module` on install/uninstall.
# See wiki/decisions/0001-opt-in-file-based-kb-modules.md for why modules are
# files in the repo rather than native agent plugins.
name: software
title: Software / source-code KB
version: 1.0.0
requires_kb_schema: "1.5"
summary: >
Turns this knowledge base into one that documents software you are building:
source repos live as gitignored plain clones in src/, and OpenSpec drives
iterative development at both KB-root and per-repo level.
# Skills copied into .agents/skills/ on install, removed on uninstall.
# .claude/skills is a symlink to .agents/skills, so there is no second copy.
skills:
- ckb-code-map
- ckb-spec
# Directories created on install. Never removed on uninstall — they may hold
# the user's clones and specs.
scaffold:
- path: src/
from: scaffold/src-README.md
as: src/README.md
note: Drop zone for source repos, one clone per subdirectory.
- path: openspec/
from: scaffold/openspec/
note: KB-root, cross-cutting specs spanning multiple src/ repos.
# Appended to AGENTS.md between the module markers, removed on uninstall.
agents_fragment: fragments/agents-routing.md
# Appended to .gitignore between the module markers, removed on uninstall.
gitignore_fragment: fragments/gitignore.snippet
# Frontmatter this module contributes to the page schema. Additive: the base
# schema tolerates these whether or not the module is installed.
schema_additions:
types:
- repository # one src/ repo: remote, branch, language, build, owner
- component # a module/service/package inside a repo
- spec # a capability spec, KB-root or repo-level
fields:
- repo # /wiki/entities/<repo>.md this page belongs to
- commit # commit hash the page was generated from
- spec_id # OpenSpec capability id
- implements # repo-level spec -> the KB-root spec it implements
- implemented_by # KB-root spec -> repo-level specs implementing it
# External dependency. The module reports its absence rather than substituting
# its own workflow — see wiki/decisions/0002-software-module-design.md.
depends_on:
- name: openspec
scope: src-repo-level-only
check: "command -v openspec"
optional: true
note: >
Required only for per-repo specs. The KB-root spec layer is served by
ckb-spec itself and works without the CLI.

View file

@ -0,0 +1,17 @@
# KB-root specs
Cross-cutting capability specs — the ones that span several `src/` repos and that
no single repo owns. Managed by the `ckb-spec` skill.
* `specs/<capability>.md` — current truth. What the system, as a whole, does.
* `changes/<change-id>/` — a proposed delta, not yet true.
* `changes/archive/` — applied or rejected changes. Immutable; often worth a
matching decision record under `wiki/decisions/`.
Specs for how *one* repo implements a capability belong in that repo, at
`src/<repo>/openspec/`, where OpenSpec's own tooling manages them and they travel
with the code. Link the two with `implements:` / `implemented_by:` — both sides,
always.
Unlike `src/`, this directory **is** tracked in the KB's git history: it is
knowledge, not code.

View file

@ -0,0 +1,15 @@
# `src/`
Source repositories for the software this knowledge base documents. One clone
per subdirectory, each an independent git repo with its own remote.
**Everything here except this file is gitignored.** Clones are never committed to
the KB. A fresh clone of this KB will have an empty `src/` — that is expected.
What survives is the knowledge: every repo has a `type: repository` page under
`wiki/entities/` recording its remote, default branch, language, build command
and owner, so you can re-clone from the wiki alone. Run `ckb-code-map` after
adding a repo here to create or refresh that page.
Per-repo specs live in `src/<repo>/openspec/` and travel with that repo, not with
this KB. Cross-cutting specs live in the KB root's `openspec/`.

View file

@ -0,0 +1,188 @@
---
name: ckb-code-map
description: Read a source repository under src/ and write what it IS into wiki/entities/ — a `type: repository` page (remote, default branch, language, build and test commands, owner) plus `type: component` pages for its significant modules/services, cross-linked into the graph and stamped with the commit they were generated from so staleness is detectable. Use when the user says "map the code", "map src/<repo>", "refresh the code map", "index the repo", "add this repo to the wiki", or after cloning a new repo into src/. Distinct from `ckb-ingest` (which processes raw/inbox/ material and never reads code) and from reading code to answer a question (that is `ckb-retrieve` using src/ as evidence). Part of the opt-in `software` module.
---
# Code map skill
## Purpose
`src/` clones are gitignored: they are not in this KB's history and may be
absent entirely from a fresh checkout. So the *knowledge* about a repository
has to live in `wiki/`, or it doesn't survive.
This skill produces that knowledge. It is deliberately **not** a code-to-prose
dump — a wiki page restating what any reader could get by opening the file is
pure liability, because it goes stale silently. What this skill records is what
you cannot get by reading one file: how to obtain the repo, how to build and
test it, what its pieces are, who owns it, and how those pieces connect to
entities the wiki already knows about.
## Trigger phrases
- "map the code" / "map `src/<repo>`" / "index the repo"
- "refresh the code map" / "the code map is stale"
- "add this repo to the wiki" / "I cloned a new repo into src"
## Scope rule
Reads `src/`. Writes only `wiki/`. Never writes, commits, or pushes anything
inside `src/` — those are independent repos with their own remotes.
## How to run this skill
### Step 0 — Confirm the module is installed and the repo exists
Check `ckb.yaml` lists the `software` module. If `src/` is empty or the named
repo isn't there, say so and stop — don't guess which repo was meant when
several are present, ask.
### Step 1 — Gather the facts that don't come from reading code
From inside the repo, cheaply:
```bash
git -C src/<repo> remote -v
git -C src/<repo> rev-parse --abbrev-ref HEAD
git -C src/<repo> rev-parse --short HEAD
git -C src/<repo> log -1 --format=%cI
git -C src/<repo> status --short
```
Then read the manifest and entry docs — `README`, `package.json`, `pyproject.toml`,
`go.mod`, `Cargo.toml`, `Makefile`, CI config. Build and test commands come from
here, not from inference.
**Do not run the build, the tests, or any script from the repo** to find out what
it does. Mapping is a read-only activity.
### Step 2 — Identify components, and be ruthless about what counts
A component is a part of the system a person would name in conversation: a
service, a CLI, a published package, a long-lived subsystem. A directory is not
automatically a component. **Ten honest component pages beat a hundred mirroring
the folder tree** — the second kind makes the wiki look thorough while making it
useless to search.
If you cannot write a one-sentence `tldr` for a candidate that says what it *does*
(not where it lives), it isn't a component. Leave it out.
### Step 3 — Consult the cascade before creating anything
Per the cascade rule, check whether pages already exist for this repo or its
components — in `wiki/` first, then `linked/`, then `libs/`. Refreshing an
existing page is the normal case, not the exception: update it, keep its history,
and don't renumber or re-slug it just because a directory was renamed.
### Step 4 — Write the repository page
`wiki/entities/<repo-slug>.md`:
```markdown
---
type: repository
tldr: One sentence on what this software does — not "the repo for X".
resource: https://git.example.com/me/thing
repo: thing
commit: a1b2c3d
confidence: 0.9
quality: 0.8
last_updated: YYYY-MM-DD
freshness_window_days: 90
retention: high
---
# thing
**Remote:** `git@git.example.com:me/thing.git` · **Default branch:** `main`
· **Local path:** `src/thing` · **Mapped at:** `a1b2c3d` (YYYY-MM-DD)
## What it is
Two or three sentences. What problem it solves and for whom.
## Getting it
git clone git@git.example.com:me/thing.git src/thing
## Build and test
<the actual commands, from the manifest not invented>
## Components
* [[thing-api]] / [thing-api](/wiki/entities/thing-api.md) — <tldr>
## Specs
KB-root specs this repo implements, and the repo-level specs that implement them.
Written by `ckb-spec`; leave the section here even when empty.
## Sources
`src/thing` at `a1b2c3d`, mapped YYYY-MM-DD. README, `pyproject.toml`, CI config.
## Notes
<!-- Yours. Never rewritten by any skill. -->
```
The `commit` field and the **Mapped at** line are what make this page auditable —
they let a reader and `ckb-lint` tell how far the page has drifted from the code.
Never write them from memory; take them from Step 1. They are this page's
fingerprint, serving the same role `source_fingerprint` serves elsewhere: a
commit either still matches `HEAD` or it doesn't, which beats guessing from a
date.
`## Notes` is **protected** (page schema, `CLAUDE.md`/`AGENTS.md`). A refresh
regenerates everything above it and carries it across byte-for-byte. This
matters more here than almost anywhere else: a code map is re-run often, and
the things worth knowing about a repo that the repo doesn't say about itself —
which build target is abandoned, which service is being decommissioned, who to
ask — have nowhere else to live.
### Step 5 — Write component pages
Same shape, `type: component`, with `repo: <repo-slug>` pointing home. Keep them
thin and link upward to the repository page and sideways to whatever the wiki
already knows — a component that talks to a system with an existing entity page
should link to it rather than re-describing it.
### Step 6 — Graph, indexes, log
- `wiki/graph/edges.json``part_of` (component → repository), `depends_on`
(repo/component → an external library or a system that has a page), `owns`
(person → repo, **only** on stated ownership, never inferred from commit counts).
- `wiki/entities/index.md` — add or refresh a row per page.
- `wiki/log.md` — one entry per run, per Rule B, naming the repo and commit.
### Step 7 — Report
State the repo and commit mapped, pages created vs. refreshed, components
deliberately skipped and why, anything you couldn't determine (no build command
in the manifest — say so rather than inventing one), and whether the working tree
was dirty at map time (a map taken from uncommitted work is fine, but should be
labelled as such). Close with the standard reminder: on disk, not committed.
## Edge cases
- **Dirty working tree** — map it, but record the commit *plus* a note that
uncommitted changes were present. Don't refuse, and don't silently pretend the
tree was clean.
- **Repo with no remote** (local-only) — record `resource:` as absent and say
plainly in **Getting it** that this repo exists only locally. That's a real
finding: a gitignored, remote-less repo is one disk failure from gone.
- **Monorepo** — one repository page, components per package. Don't create a
repository page per package.
- **Several repos, user said "map the code"** — ask which, or offer to do all;
don't pick one.
- **Repo that's mostly vendored/generated code** — map what's authored here. Note
the vendored portion once, on the repository page.
- **A component page already exists as `type: concept`** from an earlier ingest —
don't create a duplicate. Update the existing page and change its type, noting
the change in the log entry.
---
*Licensed under the Apache License, Version 2.0 — see [LICENSE](../../../../LICENSE)
at the repository root, or <http://www.apache.org/licenses/LICENSE-2.0>.*

View file

@ -0,0 +1,192 @@
---
name: ckb-spec
description: Own the KB-root `openspec/` layer (cross-cutting capability specs spanning several src/ repos), delegate per-repo specs to OpenSpec's own installed workflow inside src/<repo>/openspec/, and bridge both into wiki/ — archived change proposals become numbered decision records, current specs become `type: spec` entity pages, and the two levels link via implements/implemented_by. Use when the user says "write a spec", "propose a change", "what specs do we have", "archive this change", "sync the specs", "which spec covers X", or "wire OpenSpec into the wiki". Part of the opt-in `software` module. Distinct from `ckb-decide` (records decisions directly; this skill feeds it from archived specs) and `ckb-code-map` (records what the code IS; this skill records what it SHOULD DO).
---
# Spec skill
## Purpose
A code map says what the software *is*. A spec says what it is *supposed to do*
and a change proposal says what should become true next. This skill owns that
axis, across two levels that deliberately differ:
| Level | Holds | Owned by |
|---|---|---|
| `openspec/` at the KB root | Cross-cutting capabilities and contracts spanning several `src/` repos. The *what* and *why*. | **This skill.** A root `openspec/` has no code beneath it and no single repo governing it, so OpenSpec's own repo-shaped workflow doesn't serve it. |
| `src/<repo>/openspec/` | How one repo implements those capabilities. The *how*. | **OpenSpec itself.** This is its native case: specs travel with the code and ride along in the repo's own PRs. |
The split is the point, and so is the risk it creates: two places a statement can
live is two places it can diverge. The `implements` / `implemented_by` links
below exist so divergence becomes a **lint finding** rather than a silent second
source of truth. Maintaining those links is not optional bookkeeping — it is the
reason the two-level arrangement is safe.
## Trigger phrases
- "write a spec" / "add a capability" / "what specs do we have" / "which spec covers X"
- "propose a change" / "draft a change proposal" / "archive this change"
- "sync the specs" / "wire OpenSpec into the wiki" / "are the specs in sync"
## Scope rule
Writes `openspec/` at the KB root, and `wiki/`. Inside `src/<repo>/` it reads
freely and writes **only** through OpenSpec's own workflow, on the user's
explicit instruction. It never commits or pushes a `src/` repo.
## KB-root specs
### Layout
```
openspec/
├── README.md # what this layer is for
├── specs/
│ └── <capability>.md # one capability, current truth
└── changes/
├── <change-id>/ # proposed delta, not yet true
└── archive/ # applied changes, immutable
```
### A KB-root capability spec
```markdown
---
type: spec
spec_id: billing-invoicing
tldr: One sentence stating the capability, in the present tense, as a contract.
status: active
implemented_by: /src/billing/openspec/specs/invoicing.md, /src/portal/openspec/specs/invoice-view.md
affects: /wiki/entities/billing.md
confidence: 0.9
quality: 0.8
last_updated: YYYY-MM-DD
freshness_window_days: 180
retention: high
---
```
Body: **Purpose** (why this capability exists), **Requirements** (each one
testable — "the system SHALL ..." beats "the system should be fast"),
**Out of scope**, **Implementations** (which repos, dual-linked), **Sources**.
Write requirements a reader can disprove. A requirement nobody can fail is not a
requirement, and a spec full of them reads as thorough while constraining nothing.
### Writing one
1. Check `openspec/specs/` for an existing capability on the subject. Extend it
rather than creating a near-duplicate — two overlapping specs is the failure
this layer exists to prevent.
2. Check the cascade (`wiki/``linked/``libs/`) for what the KB already
knows about the subject, and link to it rather than restating it.
3. Write the spec, then a `type: spec` mirror page under `wiki/entities/` (or
link the spec file directly from the relevant entity page — prefer one home
plus links over two copies of the text).
4. Log it per Rule B and update `wiki/entities/index.md`.
## Per-repo specs
**Delegate.** If `src/<repo>/openspec/` exists, follow the instructions OpenSpec
installed there — do not substitute a workflow of your own, and do not "improve"
its file format to match this KB's conventions. That repo's specs are governed by
OpenSpec upstream; divergence there breaks its tooling and its PR flow.
### Never run `openspec init` at the KB root
`openspec init` writes tool-integration files into `.claude/skills/` and adds
marker blocks to `AGENTS.md` / `CLAUDE.md`. At the KB root both are load-bearing:
`.claude/skills` is a symlink to `.agents/skills` (so OpenSpec's files would join
this KB's skill set), and `CLAUDE.md` is a symlink to `AGENTS.md`, the KB's own
system prompt.
Run it **only** inside `src/<repo>/`, where the repo gets its own `.claude/` and
its own `AGENTS.md`. The KB-root `openspec/` is not an OpenSpec install and never
needs `init`. If the user asks for `init` at the root, explain this and offer the
KB-root spec layer instead — don't run it and don't clean up afterwards.
Install is `npm install -g @fission-ai/openspec@latest` (Node 20.19.0+), then
`openspec init` in the repo; `openspec update` after a CLI upgrade. See
[OPENSPEC.md](../../../../OPENSPEC.md) for the user-facing guide.
If OpenSpec is not installed (`command -v openspec` fails, or the repo has no
`openspec/`), **say so plainly and stop** rather than improvising:
> "This repo has no OpenSpec setup, and the `openspec` CLI isn't on PATH. Per-repo
> specs are OpenSpec's own workflow — install it and run `openspec init` in
> `src/<repo>`, and I'll take it from there. I can write this as a KB-root
> cross-cutting spec instead if it isn't repo-specific."
That offer is genuine, not a consolation: a requirement that spans repos belongs
at the root anyway.
## The bridge
Three directions, all owned here.
### 1. Archived change → decision record
An applied-and-archived OpenSpec change is a decision that was made and acted on.
When the user archives a change (either level), offer to record it via
`ckb-decide`: the change's *why* becomes **Context** and **Rationale**, the delta
becomes **Decision**, and rejected options in the proposal become
**Alternatives considered**.
Cite the change id and its path in the decision's `## Sources`. Do not
auto-record without asking — not every archived change is a decision worth a
permanent numbered record, and a decisions log padded with routine changes loses
the property that makes it worth reading.
Both artifacts are append-only, which makes them a natural pair: neither is ever
rewritten when the thinking changes later.
### 2. Spec → entity page
Current KB-root specs surface in `wiki/entities/` as `type: spec` pages so
`ckb-retrieve` can answer "what is this supposed to do" without opening the spec
tree. Keep these thin and pointer-style: `tldr`, `spec_id`, status, links. The
spec file stays the source of truth — a full copy in `wiki/` is a second thing
to keep in sync, and it will lose.
### 3. Level linking — the drift rule
- A KB-root spec lists every repo-level spec implementing it in `implemented_by`.
- A repo-level spec names its parent in `implements`.
- Both become graph edges (`implements`) in `wiki/graph/edges.json`.
Set **both sides**, every time. A one-sided link is a lint finding, exactly as a
one-sided decision supersession is.
### Sync check
On "sync the specs" / "are the specs in sync", report:
1. KB-root specs with empty `implemented_by` — *specified but nobody builds it.*
2. Repo-level specs with `implements` pointing at a missing or archived root spec
— *building against something no longer true.*
3. One-sided links, either direction.
4. Root specs whose `last_updated` is older than the mapped commit of every repo
implementing them — *possible drift; verify, don't auto-fix.*
Report all four. **Fix none of them automatically** — each is a statement about
intent, and only the user knows which side is right. `ckb-lint` runs the same
four checks as part of its sweep when this module is installed.
## Edge cases
- **`src/` is empty** — the KB-root layer still works. Cross-cutting specs can
precede any code; that's often the point.
- **A spec that's really a decision** ("we'll use Postgres") — that's `ckb-decide`.
A spec states a standing contract; a decision records a choice at a point in
time. If it doesn't constrain future behaviour, it isn't a spec.
- **A capability implemented by exactly one repo, forever** — it probably belongs
in that repo, not at the root. Say so; don't silently promote it.
- **OpenSpec's format changes upstream** — for per-repo specs, follow upstream.
This skill's formats govern the KB-root layer only.
- **A change proposal that was rejected** — it still archives, and it is still
worth a decision record with `status: rejected`. The reasoning is the value.
---
*Licensed under the Apache License, Version 2.0 — see [LICENSE](../../../../LICENSE)
at the repository root, or <http://www.apache.org/licenses/LICENSE-2.0>.*

View file

@ -0,0 +1,323 @@
---
name: ckb-decide
description: Record a decision into wiki/decisions/ as a numbered decision record — what was decided, by whom, when, why, what it affects, and which earlier decision it supersedes or reverses — and answer questions about decisions already recorded ("what did we decide about X", "show decision 7", "which decisions are still open", "what changed the database choice"). Use when the user says "record a decision", "log a decision", "we decided ...", "ADR", "decision record", or asks what/when/why/by whom something was decided. Distinct from `ckb-ingest` (which turns raw source material into entity pages) and from `ckb-retrieve` (general KB questions — this skill is the decisions-scoped path, and hands off to it for anything wider).
---
# Decision log skill
## Purpose
A decision is a different kind of knowledge from an entity page. An entity
page describes what something *is*, and gets rewritten as understanding
improves. A decision record describes what was chosen *at a point in time*,
by whom, and why — and is never rewritten to reflect a later change of mind.
When the choice changes, a new decision supersedes the old one and both stay
on the record. That's the whole point: the value is in being able to ask
"why is it like this?" and get the reasoning, the people, and the date, not
just the current state.
This skill owns both halves of that:
- **Recording** — turning "we decided X" into a numbered, linked, logged
decision record under `wiki/decisions/`, with the supersession links wired
in both directions.
- **Looking up** — answering questions about decisions already recorded,
scoped to `wiki/decisions/` rather than searching the whole wiki.
It writes only inside `wiki/` (`wiki/decisions/`, `wiki/decisions/index.md`,
`wiki/decisions/log.md`, `wiki/graph/edges.json`, and a pointer line in
`wiki/log.md`). It never edits an existing decision's substance — see
Rule: decisions are append-only, below.
## Trigger phrases
**Recording:**
- "record a decision" / "log a decision" / "add a decision record" / "write an ADR"
- "we decided ..." / "we've agreed to ..." / "the call was ..."
- "we're reversing ..." / "that supersedes decision N"
**Looking up:**
- "what did we decide about X" / "why do we ... ?" (when the answer is a decision)
- "show decision 7" / "show me D-0007"
- "which decisions are still open" / "what's proposed but not accepted"
- "who decided X" / "when did we decide X"
- "what decisions affect <project/system/person>"
- "what superseded decision N" / "what changed the X decision"
For a question that only *touches* decisions as part of a wider answer, use
`ckb-retrieve` instead and let it pull decision pages in as one source
among many. This skill is for when decisions themselves are the subject.
## The decision record format
One page per decision at `wiki/decisions/NNNN-short-slug.md`, numbered
sequentially from `0001`. Numbers are never reused and never renumbered — a
superseded or reversed decision keeps its number and its page.
```markdown
---
type: decision
tldr: One sentence stating the decision itself, not the topic.
status: accepted
decided_on: 2026-09-01
decided_by: Alice Smith, Bob Jones
affects: /wiki/entities/billing-service.md, /wiki/projects/index.md
review_on: 2027-03-01
supersedes: /wiki/decisions/0003-use-mysql.md
confidence: 0.9
quality: 0.8
last_updated: 2026-09-01
freshness_window_days: 365
retention: high
---
# D-0007 — Use Postgres for the billing store
**Status:** Accepted · **Decided:** 2026-09-01 · **Deciders:** [[Alice Smith]] / [Alice Smith](/wiki/entities/alice-smith.md), [[Bob Jones]] / [Bob Jones](/wiki/entities/bob-jones.md)
## Context
What forced a choice. The problem, the constraints, what was true at the
time. Written so it still makes sense to someone reading it in two years
with none of the surrounding conversation.
## Decision
What was actually decided, stated plainly and in the active voice.
## Rationale
Why this option won. The reasoning that would have to change for the
decision to be worth revisiting.
## Consequences
What follows — what this commits us to, what it rules out, what work it
creates. Both the good and the costly.
## Alternatives considered
What else was on the table and why each was not chosen. A decision record
without this is much less useful on re-reading: it's the part that stops
the same option being re-proposed every six months.
## Supersession
Supersedes [[D-0003]] / [D-0003](/wiki/decisions/0003-use-mysql.md) — MySQL
was chosen before the reporting requirements landed.
## Sources
- `raw/archive/2026-09-21/arch-review.md` — sha256:3f9a2c1e (checked 2026-09-21)
## Crux
> We're going with Postgres. The reporting requirements need window
> functions and nobody wants to maintain a second analytics store.
`raw/archive/2026-09-21/arch-review.md`, Ana Reyes
```
A decision record is the page type where `## Crux` earns its place most
clearly. Everything above it is this skill's reconstruction of a choice;
the Crux is the moment the call was actually made, in the words it was made
in. When someone later asks "did we really decide that, or did we just
discuss it?", a verbatim quote settles it and a summary doesn't — which is
also the distinction between `status: accepted` and `status: proposed` that
`ckb-ingest` is warned about.
Quote the decision itself, not the surrounding debate; if the material
contains no sentence where anyone actually decides, that is strong evidence
the record should be `proposed`. Fingerprint every cited local file
(`sha256sum <file> | cut -c1-8`) so `ckb-lint` check 12 can tell you when
the source underneath a decision changes.
Decision pages carry no `## Notes` section. Nothing regenerates them, so
there is nothing to protect against — and an append-only record with a
freely-editable annotation block invites exactly the retroactive revision
the append-only rule exists to prevent.
### Field reference
| Field | Required | Notes |
|---|---|---|
| `type` | yes | always `decision` |
| `tldr` | yes | the decision itself in one sentence ("Billing uses Postgres"), not the topic ("database choice") — this is what shows in the index and in search results |
| `status` | yes | `proposed` / `accepted` / `rejected` / `superseded` / `reversed` — vocabulary defined in `wiki/decisions/index.md` |
| `decided_on` | yes for `accepted`/`rejected`/`reversed` | `YYYY-MM-DD`, the date the call was made. Distinct from `last_updated`, which is when the *page* last changed. A `proposed` decision may have no `decided_on` yet. |
| `decided_by` | yes when known | comma-separated names, kept flat and plain so tooling can read it. The body's **Deciders** line carries the dual-links to entity pages. If genuinely unknown, write `unknown` rather than omitting the field — "we don't know who decided this" is itself worth recording. |
| `affects` | no | comma-separated project-root-absolute wiki paths this decision constrains. Cheap way to answer "what decisions touch X" without walking the graph. |
| `review_on` | no | `YYYY-MM-DD` to revisit. `ckb-lint` reports these once the date passes. |
| `supersedes` / `superseded_by` | when relevant | project-root-absolute path to the other decision. **Always set both sides** (see below). |
| `confidence`, `quality`, `last_updated`, `freshness_window_days`, `retention` | as usual | standard page schema. Decisions default to `freshness_window_days: 365` and `retention: high` — a decision record doesn't rot the way a status page does, and it should survive a retention sweep. |
## How to run this skill — recording
### Step 1 — Collect the facts, ask only for what's missing
Take everything the user already said at face value; don't re-ask for it.
Then check what's missing against this list, in priority order:
1. **The decision itself** — what was chosen. Without this there's nothing to record.
2. **Who decided** — names. This is the field users most often leave out and most often want later.
3. **When** — a date. "Today" is fine; "last Tuesday's architecture review" is fine, resolve it to a date.
4. **Why** — the rationale, and the alternatives that lost.
5. **Whether it changes an existing decision** — see Step 2.
Ask for the missing ones in a **single** `AskUserQuestion` round rather than
an interview — this is a recording task, not a discovery interview. If the
user is clearly mid-flow and wants it written down now, record what you have,
mark the gaps explicitly in the page body (`## Rationale` → *"Not captured at
recording time."*), and say which fields you left open so they can fill them
in later. A decision recorded with gaps beats a decision not recorded.
Do not invent context, rationale, or consequences. If the user gave you one
sentence, the record is one sentence plus the metadata — a fabricated
`## Consequences` section is worse than an absent one, because a later reader
can't tell it wasn't real.
### Step 2 — Check whether it supersedes anything
Before writing, read `wiki/decisions/index.md` and scan for a decision on the
same subject. If one exists and is still `accepted`:
- Confirm with the user that the new decision replaces it (don't assume — two
decisions can coexist on the same subject at different scopes).
- If it replaces it outright, set `supersedes` on the new page and, on the
old page, set `superseded_by` **and** change its `status` to `superseded`.
- If it undoes it and returns to the prior state, use the same two links but
set the old page's status to `reversed` instead.
- Never edit anything else on the old page. Its context, rationale, and
consequences stay exactly as they were written — that's the historical
record.
Both directions must be set. A one-sided supersession link is a lint finding,
and it breaks the "what changed this?" lookup in the other direction.
### Step 3 — Allocate the number and write the page
The next number is the highest existing `NNNN` in `wiki/decisions/` plus one,
zero-padded to four digits — never reuse a number, even if the highest-numbered
decision was rejected or superseded. Slug from the decision itself, not the
topic: `0007-use-postgres-for-billing.md`, not `0007-database.md`.
Write the page using the format above. Link deciders to their entity pages
where those pages exist (dual-linked, per Rule C). Where a decider has no
entity page, write the name as plain text and mention the gap in your report
— creating person pages is `ckb-ingest`'s job, not this skill's.
### Step 4 — Wire it into the index and graph
- **`wiki/decisions/index.md`** — add a bullet in number order:
`* **[Accepted]** [D-0007 — Use Postgres for the billing store](0007-use-postgres-for-billing.md) — <tldr>`
and remove the "no decisions recorded yet" placeholder once there's a first
entry. When a decision's status changes (Step 2), update its index line too.
- **`wiki/graph/edges.json`** — add the edges this decision creates:
`decided_by` (decision → person page), `affects` (decision → each page named
in `affects`), and `supersedes` (new decision → old decision) where
applicable. Skip any edge whose target page doesn't exist rather than
pointing at a page you'd have to invent.
### Step 5 — Log it, then report
Log the change in **`wiki/decisions/log.md`** (not `wiki/log.md`) using
Rule B's format, and add a single pointer line to `wiki/log.md`:
`- See wiki/decisions/log.md for decision-record changes on this date.`
Per the Recursive Index & Log Convention, each change gets exactly one home
log — don't write the full entry in both.
Then tell the user: the number and title assigned, the fields you filled,
any fields left open, what it superseded (and that the old page's status was
updated), which deciders had no entity page, and the standard reminder that
this is on disk but not committed — "say 'sync changes' when you want it
pushed."
## How to run this skill — looking up
### Step 1 — Start at the decisions index
Read `wiki/decisions/index.md` first. It carries every decision's number,
title, status, and one-line summary — enough to answer "which decisions are
open", "what's been decided about X", and "which decision covers Y" without
opening a single page.
### Step 2 — Open the pages that actually matter, and read them fully
For a specific decision, read the whole page — a decision's `tldr` states the
choice but not the reasoning, and "why" is usually the real question. For a
subject-scoped question ("what have we decided about billing?"), open every
decision whose `affects` or body mentions the subject, including superseded
ones.
**Superseded decisions are part of the answer, not noise.** "We use Postgres,
and before that MySQL, changed in September because of reporting" is the
useful answer; "we use Postgres" is the impoverished one. Follow
`supersedes`/`superseded_by` chains in both directions and present the
history in order.
### Step 3 — Verify against the source before answering
Apply `ckb-retrieve`'s standing rule: a decision page's `## Sources` section
points at where the decision came from. When the answer hinges on detail
beyond what the page states — exact wording, a number, who was actually in
the room — follow the source rather than paraphrasing the paraphrase.
Where the page has a `## Crux`, take `ckb-retrieve` Step 6's shortcut: check
the recorded fingerprint against the cited file, and if it matches, quote the
Crux directly. If it doesn't match, the source underneath this decision has
been edited since the record was written — say so, answer from the source,
and flag it. That is worth stating plainly rather than folding into a
caveat: a decision record whose evidence has moved is the one case where
"what we decided" and "what the record says we decided" can come apart.
### Step 4 — Answer with the metadata attached
A decision answer is incomplete without **who** and **when** — lead with the
decision, then attribute it. State the status plainly, especially when it
isn't `accepted`: an answer built on a `proposed` or `superseded` decision
must say so in the same breath, or the reader will act on something that
isn't in force.
If nothing is recorded on the subject, say so plainly and offer to record one
now — don't reconstruct a decision from surrounding wiki content and present
it as if it were on the record. If the question revealed a genuine gap, that's
a `wiki/query-gaps.md` entry (`ckb-retrieve` owns that file).
## Rule: decisions are append-only
The only edits this skill makes to an *existing* decision page are:
`status`, `superseded_by`, `last_updated`, and — when the user is explicitly
correcting a recording error rather than changing their mind — the factual
metadata fields. Context, Decision, Rationale, Consequences, and Alternatives
are never rewritten to match a later view.
When the user says "actually we changed our mind about D-0003", that is a new
decision superseding D-0003, not an edit to D-0003. Say so and record it that
way. The exception is a genuine transcription error ("I said Alice, it was
actually Anna") — fix that in place and note the correction in the log entry.
## Edge cases
- **No `wiki/decisions/` directory yet** — create it along with `index.md`
and `log.md` (using the scaffold this template ships), then record the
decision as `0001-…`.
- **The user describes a decision that was already recorded** — don't create
a duplicate. Point at the existing record and ask whether they want to
supersede it, correct it, or leave it.
- **A decision with no clear decider** ("we just kind of settled on it") —
record `decided_by: unknown` rather than guessing or attributing it to
whoever is in the room. Unattributed is a fact; misattributed is a defect.
- **A decision that was never actually made** ("we should probably...") —
record it as `status: proposed` with no `decided_on`, or don't record it at
all. Ask which; a proposal filed as `accepted` is the most damaging failure
mode this skill has.
- **A decision that reverses a reversal** — normal chaining. Each link points
one step back; the chain is the history.
- **Bulk import of historical decisions** (a meeting-notes backlog) — that's
`ckb-ingest`'s job for the extraction, then this skill's format for the
pages. Record them in chronological order so the numbers run in the same
order as the decisions.
---
*Licensed under the Apache License, Version 2.0 — see [LICENSE](../../../LICENSE)
at the repository root, or <http://www.apache.org/licenses/LICENSE-2.0>.*

View file

@ -17,9 +17,16 @@ 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.
The whole transform — frontmatter remapping, link rewriting, index and log
regeneration, conformance validation — is done by a deterministic Python
script, not by reading and rewriting every page by hand. The mapping is a
fixed ruleset over a wiki that will keep growing, 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 to invoke the script
and relay its report.
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.
of the always-loaded `CLAUDE.md`/`AGENTS.md` Ingest/Lint workflows.
## Trigger phrases
@ -31,154 +38,120 @@ Use this skill when the user says things like:
## How to run this skill
### Step 1 — Read the source tree
### Step 1 — Run the script
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.
From the repository root:
### Step 2 — Clear and rebuild `outputs/okf/`
```bash
python3 "<skill-dir>/scripts/export_okf.py"
```
`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.
Resolve `<skill-dir>` to this skill's own directory. Flags:
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.
- `--check` — build into a temporary directory, run the conformance checks,
print the report, and write nothing to `outputs/`. Use this when the user
wants to know whether the wiki *would* export cleanly without touching the
working tree (e.g. as part of a review, or before a lint pass).
- `--out DIR` — write somewhere other than `outputs/okf` (rarely needed).
### Step 3 — Transform concept documents (every file that isn't `index.md`/`log.md`)
The script deletes and rebuilds the output directory on every run, so the
bundle can never accumulate stale files from pages that were since renamed
or removed. It touches nothing outside the output directory and runs no
`git` commands.
For each such file, rewrite its frontmatter using this field mapping:
The exit code is `0` when the generated bundle conforms and `1` when it does
not — the report always prints either way, so read it rather than relying on
the exit code alone.
### Step 2 — Relay the script's report
The script prints, and you should summarize back to the user:
- Concept documents exported, and `index.md`/`log.md` files regenerated.
- Wikilinks stripped and `/wiki/` links rewritten to bundle-root paths.
- How many `linked/`/`libs/` cross-cascade references were left unconverted
— those won't resolve if the bundle is ever moved or shipped standalone,
which is spec-compliant (OKF tolerates broken links) but worth stating.
- Any `SOURCE ISSUE:` lines. These are problems in `wiki/` itself, not in
the export — most commonly a page with no `type`, which the bundle exports
as `type: unknown`. Surface them; the fix belongs in the source wiki (run
`ckb-lint`), not in the export.
- Any `NONCONFORMANT:` lines. These are bugs in the export — an
intra-bundle link that doesn't resolve, an index with the wrong
frontmatter shape, a malformed log header. Do not report the run as
successful when these appear; investigate the script rather than
hand-patching `outputs/okf/`.
- A reminder that `outputs/okf/` has been regenerated on disk but not staged
or committed — that's a separate step for the user when they're ready to
publish the update. (`outputs/okf` is gitignored by default in this
template, so "publish" usually means copying the bundle elsewhere rather
than committing it in place.)
## What the transform does (for context, not something you need to re-derive)
**Concept documents** (every `.md` that isn't `index.md`/`log.md`, including
`overview.md`, `error-book.md`, and `query-gaps.md` — only `index.md` and
`log.md` are OKF reserved names):
| 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 |
| `type` | `type` | passthrough; a page with no `type` exports as `unknown` and is reported as a source issue |
| *(derive)* | `title` | the first `# H1` in the body, else the slugified filename (`foo-bar.md` → "Foo Bar") |
| `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 |
| `resource` | `resource` | passthrough |
| `last_updated` | `timestamp` | passthrough as-is (bare `YYYY-MM-DD` is valid ISO 8601 — no time-of-day is fabricated) |
| `confidence`, `quality`, `retention`, `supersedes`, `superseded_by`, `freshness_window_days`, `source_fingerprint`, `source_checked` | same keys | passthrough as OKF extension fields, which consumers must tolerate |
| `kb_schema_version` | *(dropped)* | the root `index.md`'s frontmatter is spec-limited to `okf_version`; this key has no valid home in the bundle |
| *(none)* | `tags` | omitted — no source field to derive it from |
Then rewrite the body's links:
**Links:** the `[[Wikilink]]` half of every dual-link is dropped, keeping the
markdown half (a bare wikilink with no markdown twin degrades to its plain
label). Root-absolute `/wiki/entities/foo.md` becomes `/entities/foo.md`,
since the bundle root is `outputs/okf/`, not the repo root. Relative links
need no change — the export mirrors `wiki/`'s tree exactly.
`linked/<name>/...` and `libs/<name>/...` references are left untouched and
counted.
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.
**Indexes:** regenerated from the tree rather than transcribed from the
source, so they can't drift. The root `index.md` carries only
`okf_version: "0.1"` (the one documented exception to "index.md has no
frontmatter"); nested indexes carry none. Each body is an H1 plus a flat
`* [Title](path) - description` bullet list of that directory's direct
children, sorted by path, with each page's own `description` as the
description text. The source's "Use when" column and prose sections are
dropped — they are Claude-agent lazy-loading optimizations with no meaning
to a generic OKF consumer. An empty list is spec-valid.
### Step 4 — Regenerate `index.md` files
**Logs:** entries are regrouped under `## YYYY-MM-DD` headers (date only,
newest date first) as `* **Verb**: [affected files] — description (source: ...)`.
`CREATE` → Creation, `UPDATE` → Update, `DELETE` → Deprecation, anything else
→ Update. The `- **Source:**` line rides along as the trailing parenthetical
rather than being dropped — it's provenance worth keeping. The `---`
separators between entries are dropped; they aren't part of OKF's log format.
**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`
§6 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.
**Validation** runs against the generated bundle before the report prints:
every non-reserved page has a non-empty `type`; the root index has only
`okf_version` and nested indexes have no frontmatter; every `log.md` header
matches `## YYYY-MM-DD`; and every intra-bundle link resolves to a file that
exists (cross-cascade `linked/`/`libs/` references are exempt by design).
## 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` §6 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`
- **Empty `wiki/entities/` or `wiki/graph/`:** their `index.md` is still
regenerated, as an H1 with an empty bullet list. An empty index is
spec-valid; the directory is never skipped.
- **A future `wiki/<newtopic>/` subdirectory:** handled automatically — the
script discovers directories dynamically, mirrors them, and generates an
index for each. No script changes needed.
- **A future `wiki/archived/`:** exported like any other subdirectory. 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.
- **Non-markdown files in `wiki/`** (e.g. `graph/edges.json`): copied
verbatim into the same relative position and listed in their directory's
index. Dotfiles (`.gitadd`) are skipped.
- **Re-running with no wiki changes:** produces byte-identical output — every
directory walk and generated list is sorted. If a run is ever
nondeterministic, that's a bug in the script, not expected behavior.
---

View file

@ -0,0 +1,531 @@
#!/usr/bin/env python3
# Copyright 2026 Michał Kopeć
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Export wiki/ as an Open Knowledge Format (OKF) v0.1 bundle at outputs/okf/.
Usage:
python3 export_okf.py [--check] [--out DIR]
Run from the repository root (the directory containing wiki/ and outputs/).
--check validate only: build the bundle in a temporary directory, run the
conformance checks, print the report, and write nothing to outputs/.
--out override the output directory (default: outputs/okf).
The transform is fully deterministic: two runs over an unchanged wiki/ produce
byte-identical output. Every directory walk and generated list is sorted.
"""
import argparse
import re
import shutil
import sys
import tempfile
from pathlib import Path
REPO_ROOT = Path.cwd()
WIKI = REPO_ROOT / "wiki"
OKF_VERSION = "0.1"
RESERVED = {"index.md", "log.md"}
# wiki/ frontmatter keys that ride along unchanged as OKF extension fields.
PASSTHROUGH_EXT = [
"confidence",
"quality",
"retention",
"supersedes",
"superseded_by",
"freshness_window_days",
"source_fingerprint",
"source_checked",
]
# Keys with no valid home in an OKF bundle.
DROPPED = {"kb_schema_version"}
VERB_MAP = {
"CREATE": "Creation",
"UPDATE": "Update",
"DELETE": "Deprecation",
"RESTRUCTURE": "Update",
}
# --------------------------------------------------------------------------
# frontmatter
# --------------------------------------------------------------------------
def parse_frontmatter(text):
"""Return (dict, body). Flat `key: value` YAML only — that is all the
schema uses. Unparseable or absent frontmatter yields ({}, text)."""
if not text.startswith("---\n"):
return {}, text
end = text.find("\n---\n", 4)
if end == -1:
return {}, text
raw = text[4:end]
body = text[end + 5 :]
fm = {}
for line in raw.split("\n"):
line = line.rstrip()
if not line or line.lstrip().startswith("#"):
continue
if ":" not in line:
continue
key, _, value = line.partition(":")
fm[key.strip()] = unquote(value.strip())
return fm, body
def unquote(value):
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
return value[1:-1]
return value
def yaml_scalar(value):
"""Emit a value that round-trips through the parser above."""
s = str(value)
if s == "":
return '""'
if s[0] in "\"'&*!|>%@`[]{},#" or s[-1] == ":" or ": " in s or s.strip() != s:
return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
return s
def render_frontmatter(pairs):
lines = ["---"]
for key, value in pairs:
lines.append(f"{key}: {yaml_scalar(value)}")
lines.append("---")
return "\n".join(lines) + "\n"
# --------------------------------------------------------------------------
# body text
# --------------------------------------------------------------------------
def first_h1(body):
for line in body.split("\n"):
if line.startswith("# "):
return line[2:].strip()
return None
def slug_title(filename):
stem = Path(filename).stem
return " ".join(w.capitalize() for w in re.split(r"[-_]+", stem) if w)
def strip_wikilinks(body, counters):
"""Drop the [[...]] half of every dual-link, keeping the markdown half."""
def drop_pair(m):
counters["wikilinks"] += 1
return m.group("keep")
# [[X]] / [text](path) and [text](path) / [[X]]
body = re.sub(
r"\[\[[^\]]*\]\]\s*/\s*(?P<keep>\[[^\]]*\]\([^)]*\))", drop_pair, body
)
body = re.sub(
r"(?P<keep>\[[^\]]*\]\([^)]*\))\s*/\s*\[\[[^\]]*\]\]", drop_pair, body
)
# Any remaining bare wikilink degrades to its plain label.
def bare(m):
counters["wikilinks"] += 1
label = m.group(1)
return label.split("|", 1)[-1].strip()
return re.sub(r"\[\[([^\]]*)\]\]", bare, body)
def rewrite_links(body, counters):
"""Strip the /wiki prefix from repo-root-absolute intra-wiki links and
count (but never touch) cross-cascade linked//libs/ references."""
def repl(m):
target = m.group(2)
if re.match(r"^\.{0,2}/?(linked|libs)/", target):
counters["cascade_refs"] += 1
return m.group(0)
if target.startswith("/wiki/"):
counters["rewritten"] += 1
target = target[5:]
elif target == "/wiki" or target == "/wiki/":
counters["rewritten"] += 1
target = "/"
return f"[{m.group(1)}]({target})"
return re.sub(r"\[([^\]]*)\]\(([^)]*)\)", repl, body)
def transform_body(body, counters):
return rewrite_links(strip_wikilinks(body, counters), counters)
# --------------------------------------------------------------------------
# source tree
# --------------------------------------------------------------------------
class Page:
def __init__(self, relpath, text):
self.relpath = relpath # PosixPath relative to wiki/
self.fm, self.body = parse_frontmatter(text)
self.title = first_h1(self.body) or slug_title(relpath.name)
self.description = self.fm.get("tldr", "")
def read_tree(wiki):
pages, logs, indexes, assets = {}, {}, {}, []
for path in sorted(wiki.rglob("*")):
if not path.is_file() or path.name.startswith("."):
continue
rel = path.relative_to(wiki)
if path.suffix != ".md":
assets.append(rel)
continue
text = path.read_text(encoding="utf-8")
if path.name == "index.md":
indexes[rel] = text
elif path.name == "log.md":
logs[rel] = text
else:
pages[rel] = Page(rel, text)
return pages, logs, indexes, assets
# --------------------------------------------------------------------------
# emitters
# --------------------------------------------------------------------------
def emit_concept(page, counters, issues):
ptype = page.fm.get("type", "").strip()
if not ptype:
ptype = "unknown"
issues.append(f"{page.relpath}: no `type` in source frontmatter (exported as `unknown`)")
pairs = [("type", ptype), ("title", page.title)]
if page.description:
pairs.append(("description", page.description))
if page.fm.get("resource"):
pairs.append(("resource", page.fm["resource"]))
if page.fm.get("last_updated"):
pairs.append(("timestamp", page.fm["last_updated"]))
for key in PASSTHROUGH_EXT:
if key in page.fm:
pairs.append((key, page.fm[key]))
for key in sorted(page.fm):
if key in DROPPED or key in PASSTHROUGH_EXT:
continue
if key in ("type", "resource", "tldr", "last_updated"):
continue
pairs.append((key, page.fm[key]))
body = transform_body(page.body, counters).lstrip("\n")
return render_frontmatter(pairs) + "\n" + body.rstrip("\n") + "\n"
def index_entries(directory, pages, logs, indexes, assets):
"""Every direct child of `directory` (a PosixPath relative to wiki/, or
Path('.') for the root), as (title, href, description) sorted, so the
generated list is stable across runs."""
entries = []
for rel, page in pages.items():
if rel.parent == directory:
entries.append((page.title, rel.name, page.description))
for rel in logs:
if rel.parent == directory:
entries.append(("Change Log", rel.name, "Chronological record of changes in this directory."))
for rel in assets:
if rel.parent == directory:
entries.append((rel.name, rel.name, ""))
subdirs = set()
for rel in list(pages) + list(logs) + list(indexes) + list(assets):
parent = rel.parent
while parent != Path("."):
if parent.parent == directory:
subdirs.add(parent)
parent = parent.parent
for sub in subdirs:
sub_index = sub / "index.md"
title = slug_title(sub.name)
if sub_index in indexes:
title = first_h1(parse_frontmatter(indexes[sub_index])[1]) or title
entries.append((title, f"{sub.name}/index.md", ""))
return sorted(entries, key=lambda e: e[1])
def emit_index(directory, pages, logs, indexes, assets, is_root):
lines = []
if is_root:
lines.append(render_frontmatter([("okf_version", OKF_VERSION)]).rstrip("\n"))
lines.append("")
src = indexes.get(directory / "index.md" if directory != Path(".") else Path("index.md"), "")
heading = first_h1(parse_frontmatter(src)[1]) if src else None
lines.append(f"# {heading or slug_title(directory.name) or 'Index'}")
lines.append("")
for title, href, description in index_entries(directory, pages, logs, indexes, assets):
if description:
lines.append(f"* [{title}]({href}) - {description}")
else:
lines.append(f"* [{title}]({href})")
return "\n".join(lines).rstrip("\n") + "\n"
LOG_HEADER_RE = re.compile(
r"^##\s*\[?(?P<date>\d{4}-\d{2}-\d{2})(?:[ T](?P<time>\d{2}:\d{2}))?\]?\s*-\s*\[?(?P<action>[^\]\n]*?)\]?\s*$"
)
FIELD_RE = re.compile(r"^-\s*\*\*(?P<key>[^:*]+):?\*\*:?\s*(?P<value>.*)$")
def parse_log(text):
"""Return (heading, [entry dicts]) from a Rule B formatted log."""
body = parse_frontmatter(text)[1]
heading = first_h1(body)
entries, current = [], None
for line in body.split("\n"):
m = LOG_HEADER_RE.match(line.strip())
if m:
if current:
entries.append(current)
current = {
"date": m.group("date"),
"time": m.group("time") or "",
"action": (m.group("action") or "").strip(),
"fields": {},
}
continue
if current is None:
continue
f = FIELD_RE.match(line.strip())
if f:
current["fields"][f.group("key").strip().lower()] = f.group("value").strip()
if current:
entries.append(current)
return heading, entries
def verb_for(action):
for token in re.split(r"[/,\s]+", action.upper()):
if token in VERB_MAP:
return VERB_MAP[token]
return "Update"
def files_to_links(value, counters):
"""`wiki/a.md`, `wiki/b.md` -> [/a.md](/a.md), [/b.md](/b.md)
Only entries that name an actual file inside wiki/ become links. A
Rule B entry may also name a directory or carry a parenthetical
("`wiki/decisions/` (directory)") those stay plain text, since a link
to them would not resolve inside the bundle."""
out = []
for chunk in re.split(r",\s*", value):
chunk = chunk.strip()
if not chunk:
continue
m = re.match(r"^`([^`]+)`(.*)$", chunk)
name = (m.group(1) if m else chunk).strip()
trailer = (m.group(2).strip() if m else "")
if name.startswith("wiki/") and name.endswith(".md") and not re.search(r"\s", name):
href = "/" + name[len("wiki/") :]
rendered = f"[{href}]({href})"
else:
rendered = name
out.append(f"{rendered} {trailer}".strip() if trailer else rendered)
return ", ".join(out)
def emit_log(text, counters):
heading, entries = parse_log(text)
lines = [f"# {heading or 'Change Log'}", ""]
by_date = {}
for entry in entries:
by_date.setdefault(entry["date"], []).append(entry)
for date in sorted(by_date, reverse=True):
lines.append(f"## {date}")
lines.append("")
for entry in by_date[date]:
verb = verb_for(entry["action"])
fields = entry["fields"]
desc = transform_body(fields.get("description", ""), counters).strip()
files = files_to_links(fields.get("file affected", ""), counters)
source = fields.get("source", "").strip().rstrip(".")
parts = []
if files:
parts.append(files)
if desc:
parts.append(desc)
text_part = "".join(parts) if parts else "(no description recorded)"
bullet = f"* **{verb}**: {text_part}"
if source:
bullet += f" (source: {source})"
lines.append(bullet.rstrip(".") + ".")
lines.append("")
return "\n".join(lines).rstrip("\n") + "\n"
# --------------------------------------------------------------------------
# build + validate
# --------------------------------------------------------------------------
def build(out_dir):
pages, logs, indexes, assets = read_tree(WIKI)
counters = {"wikilinks": 0, "cascade_refs": 0, "rewritten": 0}
issues = []
if out_dir.exists():
shutil.rmtree(out_dir)
out_dir.mkdir(parents=True)
for rel, page in sorted(pages.items()):
dest = out_dir / rel
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(emit_concept(page, counters, issues), encoding="utf-8")
directories = {Path(".")}
for rel in list(pages) + list(logs) + list(indexes) + list(assets):
parent = rel.parent
while parent != Path("."):
directories.add(parent)
parent = parent.parent
for directory in sorted(directories):
dest = out_dir / directory / "index.md"
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(
emit_index(directory, pages, logs, indexes, assets, directory == Path(".")),
encoding="utf-8",
)
for rel, text in sorted(logs.items()):
dest = out_dir / rel
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(emit_log(text, counters), encoding="utf-8")
for rel in assets:
dest = out_dir / rel
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(WIKI / rel, dest)
report = {
"concepts": len(pages),
"indexes": len(directories),
"logs": len(logs),
"assets": len(assets),
"counters": counters,
"issues": issues,
}
return report
def validate(out_dir):
"""Conformance checks against the generated bundle. Returns a list of
strings; empty means the bundle conforms."""
problems = []
md_files = sorted(p for p in out_dir.rglob("*.md"))
for path in md_files:
rel = path.relative_to(out_dir)
text = path.read_text(encoding="utf-8")
fm, body = parse_frontmatter(text)
if path.name == "index.md":
if rel == Path("index.md"):
if set(fm) - {"okf_version"}:
problems.append(f"{rel}: root index frontmatter must contain only okf_version")
elif fm:
problems.append(f"{rel}: nested index.md must have no frontmatter")
elif path.name == "log.md":
for line in body.split("\n"):
if line.startswith("## ") and not re.match(r"^## \d{4}-\d{2}-\d{2}$", line.strip()):
problems.append(f"{rel}: log header is not `## YYYY-MM-DD`: {line.strip()}")
else:
if not fm.get("type", "").strip():
problems.append(f"{rel}: missing or empty `type`")
for m in re.finditer(r"\[[^\]]*\]\(([^)]+)\)", body):
target = m.group(1).split("#", 1)[0].strip()
if not target or re.match(r"^[a-z][a-z0-9+.-]*:", target):
continue
if re.match(r"^\.{0,2}/?(linked|libs)/", target):
continue # intentionally left unconverted
if target.startswith("/"):
candidate = out_dir / target.lstrip("/")
else:
candidate = (path.parent / target).resolve()
if not candidate.exists():
problems.append(f"{rel}: intra-bundle link does not resolve: {target}")
return problems
def main():
parser = argparse.ArgumentParser(description="Export wiki/ as an OKF v0.1 bundle.")
parser.add_argument("--check", action="store_true", help="validate only; write nothing")
parser.add_argument("--out", default="outputs/okf", help="output directory (default: outputs/okf)")
args = parser.parse_args()
if not WIKI.is_dir():
print(f"error: no wiki/ directory under {REPO_ROOT} — run from the repository root", file=sys.stderr)
return 2
if args.check:
tmp = Path(tempfile.mkdtemp(prefix="okf-check-"))
try:
out_dir = tmp / "okf"
report = build(out_dir)
problems = validate(out_dir)
print_report(report, problems, out_dir, checked_only=True)
finally:
shutil.rmtree(tmp, ignore_errors=True)
else:
out_dir = REPO_ROOT / args.out
report = build(out_dir)
problems = validate(out_dir)
print_report(report, problems, out_dir, checked_only=False)
return 1 if problems else 0
def print_report(report, problems, out_dir, checked_only):
counters = report["counters"]
print(f"okf_version: {OKF_VERSION}")
print(f"concept documents exported: {report['concepts']}")
print(f"index.md files regenerated: {report['indexes']}")
print(f"log.md files regenerated: {report['logs']}")
if report["assets"]:
print(f"non-markdown files copied: {report['assets']}")
print(f"wikilinks stripped: {counters['wikilinks']}")
print(f"/wiki/ links rewritten to bundle-root: {counters['rewritten']}")
print(f"linked//libs/ cross-cascade refs left unconverted: {counters['cascade_refs']}")
for issue in report["issues"]:
print(f"SOURCE ISSUE: {issue}")
if problems:
print(f"NONCONFORMANT: {len(problems)} problem(s) in the generated bundle:")
for p in problems:
print(f" - {p}")
else:
print("conformance: OK")
if checked_only:
print("output: none (--check)")
else:
print(f"output: {out_dir}")
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,6 +1,6 @@
---
name: ckb-index-external
description: Walk every connector-backed libs/<name>/ (identified by a source.yaml declaring a live external source — SharePoint, Google Drive, a plain URL, or another connector) and build/refresh a self-contained generated index for it — index.md/entities/graph/log.md, mirroring wiki/'s own shape but scoped entirely to that one connector. The index can optionally be published to (and fetched from) a shared location — a git repo or a shared resource — declared in source.yaml, so most users just read an already-built index instead of re-scanning the live source. Read vs. write access to a given source is a per-user, local-only setting (libs/<name>/source.local.yaml, gitignored) — read-only by default, so one or two designated admins can maintain a source for everyone else. Never touches wiki/, never touches source.yaml, never touches a git-copy lib. Use when the user says "index external sources", "index libs", "refresh the external index", or "scan the SharePoint/Drive folder". This is the on-demand workflow from CLAUDE.md/AGENTS.md §4, distinct from `ckb-ingest` (which processes raw/inbox/ into wiki/) and from a plain git-copy libs/<name>/ (a frozen clone, never touched by any skill).
description: Walk every connector-backed libs/<name>/ (identified by a source.yaml declaring a live external source — SharePoint, Google Drive, a plain URL, or another connector) and build/refresh a self-contained generated index for it — index.md/entities/graph/log.md, mirroring wiki/'s own shape but scoped entirely to that one connector. Indexing goes beyond one page per document: for every document found, it also extracts the people, organizations, projects, decisions, systems, and processes that document actually discusses into their own thin, pointer-style entity pages (evidence back to the specific documents that mention them, not a second copy of wiki/'s synthesis), so the index supports "what do we know about X / what's the process for Y" lookups, not just "what documents exist at this source". The index can optionally be published to (and fetched from) a shared location — a git repo or a shared resource — declared in source.yaml, so most users just read an already-built index instead of re-scanning the live source. Read vs. write access to a given source is a per-user, local-only setting (libs/<name>/source.local.yaml, gitignored) — read-only by default, so one or two designated admins can maintain a source for everyone else. Never touches wiki/, never touches source.yaml, never touches a git-copy lib. Use when the user says "index external sources", "index libs", "refresh the external index", or "scan the SharePoint/Drive folder". This is the on-demand external-source workflow routed by CLAUDE.md/AGENTS.md, distinct from `ckb-ingest` (which processes raw/inbox/ into wiki/) and from a plain git-copy libs/<name>/ (a frozen clone, never touched by any skill).
---
# Index external sources skill
@ -12,16 +12,33 @@ git-copy one: a `libs/<name>/source.yaml` declares a *live* external
source — a SharePoint folder, a Google Drive folder, a plain URL, or
another connector — that the user doesn't want to fully mirror locally.
This skill walks that source and builds a small, self-contained index of
what's there: one entity page per document found, plus the same
`index.md`/`graph`/`log.md` shape `wiki/` already uses, rooted at
`libs/<name>/` instead.
what's there, using the same `index.md`/`graph`/`log.md` shape `wiki/`
already uses, rooted at `libs/<name>/` instead.
The index holds two kinds of pages, and both matter for retrieval:
- **Document pages** — one per source item found (one per email, one per
file, one per SharePoint page), the original design. These answer "what
exists at this source".
- **Entity and process pages** — one per person, organization, project,
decision, system, or named process that those documents actually
*discuss*, extracted the same way `ckb-ingest` extracts entities into
`wiki/`, but kept deliberately thin here: a `tldr` plus a list of which
document(s) mention it and what they say, not a full synthesis. These
answer "what do we know about X" without forcing whoever's asking to
read every document's `tldr` by hand looking for a name. A document-only
index is fine for browsing a source but too narrow for actually
retrieving information out of it — that's what this second pass fixes.
This is deliberately **not** blended into the main `wiki/entities/` or
`wiki/graph/edges.json` — the generated index lives entirely inside its own
`libs/<name>/`, at the lowest cascade layer, the same way a git-cloned
KB's own files would. If something in it needs to override or correct what
the connector says, that's what writing the corrected version into `wiki/`
is for (cascade priority already covers that — `wiki/` always wins).
is for (cascade priority already covers that — `wiki/` always wins). Where
an extracted entity already has a full page in `wiki/entities/`, this
skill's own page for it stays thin and points there rather than
re-synthesizing — see Step 6.
Two refinements on top of that base design:
@ -43,7 +60,7 @@ Two refinements on top of that base design:
This skill only ever writes within a connector-backed `libs/<name>/` (never
`source.yaml` itself, never a git-copy lib, never anything under `wiki/`)
— and it never touches this repo's own git remote; pushing the resulting
changes is a separate, explicit step (see Step 7).
changes is a separate, explicit step (see Step 8).
## Trigger phrases
@ -60,9 +77,13 @@ Use this skill when the user says:
the same turn unless the user also asks for one.
Do **not** use this skill for "Ingest" / "Sync the wiki" (that's
`ckb-ingest` — raw material into `wiki/`, unrelated to `libs/`) or for
`ckb-ingest` — raw material into `wiki/`, unrelated to `libs/`), for
"Lint" (that's `ckb-lint`, which now also health-checks a connector's own
generated index, but doesn't build it in the first place).
generated index, but doesn't build it in the first place), or for
answering a question against an index that already exists (that's
`ckb-retrieve`, which reads what this skill built and always verifies
against the underlying source before answering — this skill never answers
questions, only builds/refreshes the index).
## Configuration
@ -73,6 +94,12 @@ connector: sharepoint # sharepoint | google_drive | web | custom — open
location: "https://contoso.sharepoint.com/sites/Finance/Shared Documents/Reports"
description: "Finance team's shared reports folder" # optional
refresh_interval_days: 7 # optional — how often a write-access run should rebuild this source,
# and the freshness_window_days stamped on its generated pages.
# Omit to use the default of 30. Tune per source: a busy folder
# that changes daily deserves a shorter window than a quarterly
# reports archive that barely moves.
index: # optional — omit entirely for the original default: the index lives only
# here, in this repo, tracked by this repo's own git (nothing to fetch/publish)
store: git # git | shared — open string, where the built index is published to / fetched from
@ -100,6 +127,36 @@ stop being) a given source's admin (e.g. "make me the admin for the
finance reports source" / "I don't want write access to X anymore"), never
as a side effect of just running "index external sources."
## Regeneration rule: `## Notes` is never overwritten
Every page this skill writes is *generated* — a later run rebuilds it from
the connector and the previous contents are gone. That makes this index the
one place in the KB where a human annotation is most useful and most
fragile: the person who knows that a document is superseded, or that a name
in it refers to someone who has since left, has nowhere safe to write it
down.
`## Notes` is that place. Before rewriting any page under `libs/<name>/`,
read the existing file and lift its `## Notes` section out verbatim; after
regenerating everything above it, append it back **byte-for-byte**. Not
reflowed, not summarized, not merged into the `tldr`, not "improved" —
unchanged, including whitespace and any half-finished sentence.
Practically: `## Notes` is the last section of every generated page, so a
rebuild is "replace everything above the `## Notes` heading". Create it empty
on a page's first write so the affordance exists before anyone needs it:
```markdown
## Notes
<!-- Yours. Never rewritten by any skill. -->
```
If a page somehow has content *after* `## Notes` that this skill would
otherwise generate, preserve the whole tail rather than guessing where the
human part ends. Losing generated content costs one re-run; losing a person's
annotation costs the thing itself.
## How to run this skill
### Step 1 — Find connector-backed libs
@ -159,9 +216,18 @@ This is the one place access level actually changes behavior:
- **Write access**: always continue to Step 5, whether Step 3 fetched an
existing index (refresh it) or found the store empty (build the very
first version from scratch) — the two cases are handled identically
from here on; Step 7 is what actually creates the remote copy either
from here on; Step 8 is what actually creates the remote copy either
way.
Either way, compare the existing index's newest `last_updated` against
this source's `refresh_interval_days` (default 30) and say where it
stands. A read-only user needs to know they're reading a copy that's three
weeks past due so they can go ask the admin rather than quietly trusting
it; a write-access user is about to rebuild anyway, but "this was 40 days
stale" is worth saying, because a source that's routinely overdue is
either configured with too tight an interval or has nobody actually
maintaining it. Both are worth surfacing rather than silently correcting.
### Step 5 — Resolve the connector and enumerate/summarize documents
Resolve `connector` to whatever live tool is actually available this
@ -181,8 +247,8 @@ For each document found at the location:
- Read enough content to write a real 1-2 sentence `tldr` when the
connector allows fetching content.
- If only metadata is available (listing only, no read access), say so
honestly in the `tldr` (e.g. "Metadata only — read access not granted")
and set `confidence` low (e.g. 0.3) rather than fabricating a summary.
honestly in the `tldr` (e.g. "Metadata only — content not readable") and
set `confidence` low (e.g. 0.3) rather than fabricating a summary.
Create/update one entity page per document at
`libs/<name>/entities/<doc-slug>.md`:
@ -194,12 +260,104 @@ tldr: ...
confidence: 0.30.9 # per the rule above
quality: ...
last_updated: YYYY-MM-DD
freshness_window_days: 30 # shorter than a typical wiki page — external sources change without notice
freshness_window_days: 30 # this source's refresh_interval_days, or 30 if unset — shorter than a
# typical wiki page, because external sources change without notice
retention: medium
source_fingerprint: sha256:3f9a2c1e # see below — what actually detects drift
source_checked: YYYY-MM-DD
---
```
**Fingerprint every document page.** A short `freshness_window_days` is a
guess that a source *might* have moved; a fingerprint is a fact about whether
it *did*. This matters far more here than in `wiki/`: an archived file under
`raw/archive/` is effectively immutable, while a SharePoint or Drive document
is edited by other people with no notification to anyone.
Take the fingerprint from whatever the connector gives you, in this order of
preference:
1. A version identifier the connector already maintains — an ETag, a
`version`, a content hash, a `lastModifiedDateTime`. Record it as
`etag:<value>` / `mtime:<iso8601>`. This is free and needs no content
fetch.
2. Failing that, `sha256` of the fetched content, recorded as
`sha256:<first 8 hex chars>`.
3. Metadata-only documents with neither: omit the field rather than
inventing one, and leave `confidence` low as Step 5 already requires.
Set `source_checked` to the date the fingerprint was last confirmed — which
is **not** always `last_updated`. A re-index that confirms a document
unchanged advances `source_checked` and leaves `last_updated` alone; that
distinction is exactly what tells a reader "this was verified yesterday" from
"this was written six months ago and nobody has looked since".
Give every document page a trailing `## Notes` section, created empty on
first write (see Step 7's protection rule).
### Step 6 — Extract entities and processes mentioned within each document
A document-only index tells a reader *what exists*, but not *what's known
about a given person, topic, or procedure* without reading every `tldr` by
hand. Close that gap the same way `ckb-ingest` extracts entities into
`wiki/`, but keep the result scoped and thin — this connector's own layer
is a retrieval index into its source documents, not a second knowledge
base competing with `wiki/`'s synthesis.
For each document processed in Step 5, identify what it actually
*discusses*, not just what it *is*: named people, organizations, projects,
decisions, systems, and processes/procedures (e.g. "VDI access request
process", "candidate profile approval") mentioned in its content. Skip
this for a document that's purely about itself with nothing else
extractable (a bare calendar acceptance with no discussion, for instance)
— not every document yields additional pages.
For each distinct entity/process found across the source's documents,
create or update one page at `libs/<name>/entities/<entity-slug>.md`,
using the same slug convention `wiki/entities/` would use for that name
(so `damien-gultig.md`, not a date-prefixed slug — this is what keeps
entity-page filenames from colliding with document-page filenames, which
are always date-prefixed per Step 5):
```yaml
---
type: person | organization | project | decision | system | concept | ... # same open field as wiki/'s schema
resource: /wiki/entities/<slug>.md # OPTIONAL — only if a full page for this entity already exists in wiki/
tldr: One sentence — who/what this is, and what these documents specifically show (not a full biography;
if wiki/ already has a full page, this tldr should say what these documents add, not restate it)
confidence: 0.30.9
quality: ...
last_updated: YYYY-MM-DD
freshness_window_days: 30 # or this source's refresh_interval_days, if set
retention: medium
---
```
### Step 6 — Update the hub page, graph, index, and log
Body: a short "Mentioned in" list, one bullet per document that discusses
this entity, linking to that document's own page
(`entities/<doc-slug>.md`) with a one-line note of what that specific
document says — enough for `ckb-retrieve` (or a human) to know exactly
which document to open for the full context, without needing to re-derive
it from scratch.
**If this entity already has a full page in `wiki/entities/`** (check the
cascade first, same as `ckb-ingest`'s Step 2), set `resource:` to that
page's path and keep this page's body to just the "Mentioned in" list —
don't re-synthesize what the wiki page already says. The wiki page remains
authoritative (cascade priority); this page's only job is pointing back to
*these specific documents* as additional evidence, which the wiki page may
not have cited yet.
Record real relationships surfaced by a document between two entities as
edges in `libs/<name>/graph/edges.json`, using the same closed vocabulary
`wiki/graph/index.md` defines (`part_of`, `uses`, `depends_on`, `produces`,
`configures`, `validates`, `implements`, `caused`, `contradicts`,
`supersedes`) — each verb answering the question listed there. Also add
a `mentioned_in` edge from each entity to every document that discusses
it — this is what makes the graph a genuine index into the source
documents rather than just a bag of loose pages.
### Step 7 — Update the hub page, entities index, graph, and log
Create/update `libs/<name>/index.md` — the root routing page for this
connector, mirroring `wiki/index.md`'s own role:
@ -207,26 +365,35 @@ connector, mirroring `wiki/index.md`'s own role:
---
type: external-source
resource: <location, from source.yaml>
tldr: One sentence — what this source is and how many documents were found
tldr: One sentence — what this source is, how many documents were found, and how many entities/processes were extracted from them
last_updated: YYYY-MM-DD
---
```
followed by a short routing table pointing to `entities/index.md` and
`graph/index.md`.
Record any real relationships between documents (e.g. folder hierarchy) as
edges in `libs/<name>/graph/edges.json`, using a `contains`/`part_of`-style
edge type — this is fine to be thin or empty for a flat source with no
useful structure beyond a document list.
Update `libs/<name>/entities/index.md` as two sections, so both kinds of
page stay easy to find without conflating them:
```markdown
## Documents
- [<doc-slug>](<doc-slug>.md) — <tldr>
...
Update `libs/<name>/entities/index.md` and `libs/<name>/graph/index.md`
(flat bullet lists, no frontmatter, same convention as `wiki/entities/
index.md`/`wiki/graph/index.md`). Log every created/updated page in
`libs/<name>/log.md`, same reverse-chronological format as Rule B
(`CLAUDE.md`/`AGENTS.md` §7) — this log is independent of `wiki/log.md`;
**nothing under `wiki/` is touched by this skill at all.**
## Entities & Processes
- [<entity-slug>](<entity-slug>.md) — <tldr>
...
```
Same flat-bullet, no-frontmatter convention as `wiki/entities/index.md`
otherwise — this is just a two-heading variant of it, not a new format.
### Step 7 — Publish, if this source has both write access and a configured index store
Update `libs/<name>/graph/index.md` (same convention as
`wiki/graph/index.md`) to summarize both the `mentioned_in` document
coverage and any real entity-to-entity edges found. Log every
created/updated page in `libs/<name>/log.md`, same reverse-chronological
format as Rule B in `CLAUDE.md`/`AGENTS.md` — this log is independent of
`wiki/log.md`; **nothing under `wiki/` is touched by this skill at all.**
### Step 8 — Publish, if this source has both write access and a configured index store
If this user has `access: write` **and** `source.yaml` has an `index:`
block, push the refreshed `libs/<name>/{index.md,entities/,graph/,log.md}`
@ -249,7 +416,7 @@ nothing is even rebuilt in `libs/<name>/` to begin with). A read-only user
has, by construction, nothing of their own to save back — Step 4 already
stopped them before anything was built.
### Step 8 — Remind to review and sync
### Step 9 — Remind to review and sync
This is always the last step, every time this skill makes any change at
all. Close with a short reminder — do not sync or push anything yourself
@ -285,18 +452,46 @@ was read-only with nothing new to fetch), skip this reminder.
the whole run; don't let a transient network issue block a read-only
user from seeing the last-known index.
- **Re-running against an already-indexed source** — refresh existing
entity pages in place (update `tldr`/`last_updated`/`confidence`); never
duplicate a document's page.
document *and* entity/process pages in place (update
`tldr`/`last_updated`/`confidence`/`source_fingerprint`/`source_checked`,
add newly-seen `mentioned_in` edges); never duplicate a document's or
entity's page, and carry every `## Notes` section across untouched.
- **A document's fingerprint is unchanged since the last run** — don't
re-summarize it. Advance `source_checked` to today, leave `last_updated`,
`tldr`, and `confidence` exactly as they were, and spend the run's budget
on the documents that did change. On a large source this is most of them,
and it is the difference between a re-index that costs a full pass and one
that costs a listing.
- **A document's fingerprint changed** — re-fetch and re-summarize it, and
say so in the report. A changed fingerprint on a document some wiki page
cites is worth calling out by name: that wiki page was built from a version
of this document that no longer exists.
- **A previously-indexed document is no longer found at the source**
don't delete its page. Update its `tldr` with a note ("No longer found
at source as of YYYY-MM-DD") and set `retention: low`, so the existing
`ckb-lint` retention sweep archives it naturally on a later pass — no new
archival mechanism needed.
archival mechanism needed. Leave any entity pages that cite it alone;
the citation is still historically accurate even if the document itself
is gone.
- **An extracted entity already has a full page in `wiki/entities/`**
keep this connector-side page thin (evidence/`mentioned_in` pointers
only, `resource:` set to the wiki page) rather than re-deriving
everything the wiki page already says; that duplication is exactly what
the cascade design is meant to avoid.
- **A document is dense with names/topics and extracting every one would
produce dozens of near-duplicate pages** — extract what's clearly named
and substantively discussed (not every passing mention), and prefer
updating an existing entity page's "Mentioned in" list over creating a
marginal new one. Don't force artificial granularity just to maximize
page count.
- **A document yields no extractable entity beyond itself** — that's fine;
its Step 5 document page is the only page it produces. Not every
document needs to feed Step 6.
- **A source is very large** — cap what gets fully fetched/summarized in
one run and explicitly report what was skipped (e.g. "42 of ~300
documents summarized this pass — re-run to continue"). Never silently
truncate while implying full coverage.
- **Write access but publishing (Step 7) conflicts with a newer version
- **Write access but publishing (Step 8) conflicts with a newer version
someone else already pushed** — this is why Step 3 (fetch) always runs
first, even for write-access users: rebuild on top of the latest fetched
state rather than blindly overwriting it. If a real conflict still shows

View file

@ -1,6 +1,6 @@
---
name: ckb-ingest
description: Process raw/inbox/ (or raw/ directly if the inbox is empty) into the structured wiki/ — consult the cascade, extract typed entities and relationships, synthesize frontmatted pages, cross-link them, update the index and log, then remind the user to review and sync to origin. Use when the user says "Ingest", "Sync the wiki", or "Update the Wiki". This is the content-level workflow from CLAUDE.md/AGENTS.md §3, distinct from the git-level `ckb-sync-changes` skill (which reconciles this repo's own history with its `origin` remote and does no wiki synthesis at all).
description: Process raw/inbox/ (or raw/ directly if the inbox is empty) into the structured wiki/ — consult the cascade, extract typed entities and relationships, synthesize frontmatted pages, cross-link them, update the index and log, then remind the user to review and sync to origin. Use when the user says "Ingest", "Sync the wiki", or "Update the Wiki". This is the content-level workflow routed by CLAUDE.md/AGENTS.md, distinct from the git-level `ckb-sync-changes` skill (which reconciles this repo's own history with its `origin` remote and does no wiki synthesis at all).
---
# Ingest skill
@ -18,7 +18,7 @@ This skill only ever writes to `wiki/` (and moves processed files within
`raw/`). It never touches `linked/` or `libs/` — those are immutable
upstream sources of truth — and it never touches this repo's own git
remote; pushing the resulting changes is a separate, explicit step (see
Step 7).
Step 8).
## Trigger phrases
@ -60,10 +60,74 @@ than treated as final.
Identify typed entities in the source material — people, projects,
libraries, concepts, systems. Create entity pages at
`wiki/entities/<entity-name>.md` if they don't already exist. Record typed
relationships between entities — `uses`, `depends_on`, `caused`,
`contradicts`, `supersedes` — as edge data in `wiki/graph/edges.json`.
relationships between entities as edge data in `wiki/graph/edges.json`,
using the closed vocabulary in `wiki/graph/index.md``part_of`, `uses`,
`depends_on`, `produces`, `configures`, `validates`, `implements`, `caused`,
`contradicts`, `supersedes`. Each verb is defined there by the question it
answers; if the relationship you have in mind doesn't answer one of those
questions, it belongs in the page's prose, not in the graph.
### Step 4 — Synthesize pages
Also record `has_expertise_in` and `owns` edges when the material actually
evidences them: `has_expertise_in` when a person demonstrably answers
questions or explains decisions on a topic, `owns` when they hold
declared responsibility for a system, area, or decision. These are what
make "who knows about X" and "who owns X" answerable as a direct graph
lookup instead of a full-text guess (see `ckb-retrieve` Step 3). Record
them only from demonstrated evidence — someone being present in a meeting
or cc'd on a thread is not expertise, and don't infer ownership from job
title alone. Both are optional, like every other edge type; an absent
edge is better than a fabricated one.
**Decisions are not entity pages.** When the material records a choice that
was actually made — a call with a rationale, a date, and someone who made it
— that belongs in `wiki/decisions/` as a numbered decision record, not in
`wiki/entities/`. Use `ckb-decide`'s format (it owns the field set, the
numbering, and the two-sided supersession links) and record them in
chronological order so decision numbers run in the same order as the
decisions. Two things worth being strict about, because ingest is where they
go wrong: a proposal that was discussed but not settled is
`status: proposed`, never `accepted`; and where the source doesn't say who
decided, `decided_by: unknown` beats attributing it to whoever was loudest in
the notes. If the material only *references* a decision without recording
it ("as agreed last quarter, we use Postgres"), that's a query-gap candidate,
not a decision record — you don't have the rationale or the date.
For recurring teams, clients, systems, or initiatives, consider whether a
plain project scope page under `wiki/projects/<name>.md` would make future
queries easier to route. A scope page should list when to use it, included
wiki/entity/source paths, exclusions, and refresh hints. Create one only
when the source material shows a real repeated scope; don't manufacture
scopes for one-off facts.
### Step 4 — Blast radius: what does this material invalidate?
Ingest is additive by habit, which is how a wiki accumulates two pages that
quietly disagree. Before writing anything, find out what the incoming
material lands on.
For every entity from Step 3 that already has a page, walk
`wiki/graph/edges.json` **backwards** — every edge whose `to` is that page —
one hop out. That set, plus the entity pages themselves, is the blast radius:
the pages whose claims could be affected by what just arrived. Read their
`tldr`s (and the bodies of any that look genuinely exposed) and sort each one
into:
- **Confirms** — the new material agrees. Note it; corroboration is a reason
to raise `confidence` in Step 5, not to rewrite anything.
- **Extends** — the new material adds detail the page doesn't have. Update
the page in Step 5.
- **Contradicts** — the new material and the page can't both be right. This
is the case worth catching: handle it as supersession (see Edge cases), and
say so explicitly in the report rather than letting the newer page silently
win.
- **Untouched** — most of the radius. Drop it and move on.
Name the owners too. Where an `owns` or `has_expertise_in` edge points at an
affected page, that person is who should review the change — surface them in
the Step 8 report. Skip this step entirely when Step 3 produced no entity
that already exists; a first ingest into an empty wiki has no radius.
### Step 5 — Synthesize pages
Convert the core knowledge into clean, modular Markdown files. Every page
gets frontmatter with:
@ -75,33 +139,125 @@ gets frontmatter with:
- A `retention:` level.
`type` is also required (per the page frontmatter schema in
`CLAUDE.md`/`AGENTS.md` §2) — set it once, based on the entity/content
`CLAUDE.md`/`AGENTS.md` page schema) — set it once, based on the entity/content
kind (person, project, concept, library, decision, playbook, ...).
### Step 5 — Link and cross-reference
Every synthesized page ends with the reserved body sections from the page
schema. `## Sources` and `## Crux` are written here; `## Notes` is created
empty and never touched again:
```markdown
## Sources
- `raw/archive/2026-09-21/kickoff-notes.md` — sha256:3f9a2c1e (checked 2026-09-21)
- `raw/archive/2026-09-21/access-thread.eml` — sha256:be40d7aa (checked 2026-09-21)
## Crux
> FDEs need the VDI *and* a Jira account before day one; the VDI request
> alone takes ten working days.
`raw/archive/2026-09-21/kickoff-notes.md`, under "Access"
## Notes
<!-- Yours. Never rewritten by any skill. -->
```
Compute each fingerprint from the archived file, not from memory:
```bash
sha256sum raw/archive/<date>/<file> | cut -c1-8
```
Record the digest the page was actually built from. That is what lets
`ckb-lint` tell "old but still accurate" apart from "the source changed
underneath this page" without asking a model — a calendar window can't
distinguish those. When a page is built from exactly one source, also set
`source_fingerprint` and `source_checked` in frontmatter.
**`## Crux` is quoted, never paraphrased.** Lift the handful of lines that
actually carry the claim — the number, the constraint, the condition, the
commitment — verbatim, and attribute each quote to the source bullet it came
from. Three to ten lines is the working range; a Crux approaching the length
of the summary has stopped being evidence and become a second copy of the
source. The synthesis above it says what the material means; the Crux is what
it said, so a reader can check the first against the second. This is also
what lets `ckb-retrieve` ground an answer without opening the archive every
time.
A page with no quotable source — synthesized from conversation, or from
material too diffuse to quote — simply has no `## Crux`. An empty or
paraphrased one is worse than none, because it looks like evidence.
For long conversations, meeting notes, transcripts, or chat exports, use a
structured distillation before writing the final page:
- `Question` or searchable problem statement, when there is one.
- `Summary` of the thread/note.
- `Resolution` or `Decision`, if the material contains one.
- `Systems and code references` mentioned.
- `People involved` or apparent owners/experts.
- `High-signal excerpts` for dense technical paragraphs or consecutive
messages that would be lost in a single summary.
"High-signal" needs an acceptance test, or every excerpt looks worth
keeping and the page becomes a second copy of the transcript. Promote a
run of text to its own section or linked page only when it clears all
three:
- **It carries a rare term.** Something specific enough that a future
search would use it — a config flag, an error string, a hostname, a
contract clause, a version number. Check with `rg -c` across `wiki/`:
if the term already appears on many pages it isn't a distinguishing
handle, and the excerpt adds no findability the summary doesn't have.
- **It's substantial.** Roughly 200 characters or more, or a few
consecutive paragraphs/messages from one author. A one-line "yes, do
that" is a resolution to fold into `Resolution`, not an excerpt.
- **Something corroborates it.** It was agreed with, acted on, corrected,
or referred back to later in the material. An unanswered assertion is a
claim, not a settled fact — keep it in the summary with that ambiguity
intact rather than promoting it.
Fail any one of the three and the content still belongs in the page, just
inside `Summary`/`Resolution` rather than as its own retrievable unit.
When you do promote an excerpt, carry its parent topic with it — the
thread question or section heading it sat under. An excerpt that reads
unambiguously on its own is the entire point; one that needs the
surrounding transcript to make sense hasn't been extracted, only moved.
### Step 6 — Link and cross-reference
Use **both** `[[Wikilinks]]` (Obsidian-compatible) and standard
`[markdown](path.md)` links on every cross-reference, so the wiki works in
Obsidian, GitHub, and CLI tools alike. Where useful, reference upstream
files directly at `linked/<name>/...` or `libs/<name>/...`.
### Step 6 — Update index and log
### Step 7 — Update index and log
Add new pages to the routing table in `wiki/index.md` with a **Use when**
description. If the page lives in a subdirectory, also add it to that
subdirectory's own `index.md`. Append a log entry to the most specific
applicable log — the subdirectory's `log.md` if it has one, otherwise the
root `wiki/log.md` — following the format in Rule B (`CLAUDE.md`/
`AGENTS.md` §7).
`AGENTS.md` Rule B).
If this step creates a brand-new `wiki/<topic>/` subdirectory, immediately
create that subdirectory's `index.md` per the Recursive Index & Log
Convention.
### Step 7 — Remind to review and sync
If this step creates or updates a project scope under `wiki/projects/`,
also update `wiki/projects/index.md`. If ingest closes a previously
recorded question in `wiki/query-gaps.md`, move that entry from Open to
Resolved and mention the page or source that now answers it.
This is always the last step, every time this skill runs and made any
change at all. Close with a short reminder — do not sync or push
### Step 8 — Report, then remind to review and sync
Report what changed before the reminder: pages created vs. updated, and —
from Step 4 — which existing pages the new material confirmed, extended, or
contradicted, naming the owner of each contradicted page. A contradiction
resolved silently is the one outcome of an ingest a reviewer must not have to
discover for themselves.
Then close with a short reminder — do not sync or push
anything yourself here, and do not skip this even if the changes look
small or obviously correct:
@ -115,14 +271,14 @@ process), skip this reminder — there's nothing to review or sync.
## Edge cases
- **Inbox and `raw/` both empty** — report that there's nothing to
ingest. Don't touch `wiki/`, and skip the Step 7 reminder.
ingest. Don't touch `wiki/`, and skip the Step 8 reminder.
- **Item in inbox doesn't yield a clear entity or page** (too vague, pure
scratch note with no durable fact) — don't force a page into existence.
Still move the item to `raw/archive/<YYYY-MM-DD>/` since it's been
considered, but note in the ingest report that it produced no wiki
change.
- **Conflicting information vs. an existing local wiki page** — this is a
supersession case (Rule via `CLAUDE.md`/`AGENTS.md` §6 lint), not a
supersession case handled consistently with `ckb-lint`, not a
silent overwrite: update the existing page if the new source is clearly
more current/corroborated, and link `supersedes`/`superseded_by` if an
older version is worth preserving rather than edited in place.

View file

@ -1,23 +1,30 @@
---
name: ckb-init
description: Bootstrap a brand-new Cascade Knowledge Base - the same directory structure, AGENTS.md/CLAUDE.md system prompt, full default skill set, LICENSE/VERSION, 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. Distinct from `ckb-upgrade`, which catches an *existing* populated KB up with template changes rather than bootstrapping a new one.
description: Bootstrap a brand-new Cascade Knowledge Base - the same directory structure, AGENTS.md/CLAUDE.md system prompt, full default skill set, LICENSE/VERSION, and empty wiki/ scaffold as this project - inside a target folder, sourcing the template either from this repo's own files or by pulling a fresh clone of the canonical ckb repo (or a user-supplied fork/mirror URL) into a scratch folder, so it works even when run outside an existing KB. The target folder is typically empty, or a new project that doesn't have a KB 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", "pull the ckb repo into this folder", "clone the wiki template into <dir>", or calls it a "wiki initializer". Also use when they name a release channel or branch to bootstrap from - "initialize from the test branch", "set up a dev KB from experimental", "bootstrap from <url> branch <name>" - since the template repo keeps main (stable), test (release candidate) and experimental (development) branches, and main is the default. 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. Distinct from `ckb-upgrade`, which catches an *existing* populated KB up with template changes rather than bootstrapping a new one.
---
# 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`
Copy the 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, the full default skill set,
`LICENSE` and `VERSION`, the generic `README`/`MANUAL` docs, 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 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
The template it copies from is either this repo's own files or a fresh
shallow clone of the canonical template repo pulled into a scratch folder
(Step 3) - the latter is what makes this skill usable from anywhere, not
only from inside an existing KB, and what "pull the repo into a folder to
set up a wiki" means in practice. Either way it is a one-way copy into a
different folder, and the clone is scratch: it never becomes the new KB's
`.git/`.
It never reads or writes anything in the template source's `raw/`,
`wiki/entities/`, `wiki/graph/edges.json`, or `outputs/` - those hold that
project's actual accumulated knowledge, which is exactly what should *not*
travel into a fresh KB. (For catching an *already-populated* KB up with
newer template files without losing its accumulated content, see
@ -30,6 +37,8 @@ folder.)
- "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"
- "pull the ckb repo into `<dir>` and set up the wiki structure"
- "initialize a KB from `<url>`" / "get the latest template and bootstrap a KB"
## How to run this skill
@ -59,19 +68,106 @@ 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)
### Step 3 - Resolve the template source
Everything this skill copies (`AGENTS.md`, `LICENSE`, `VERSION`, the
README/MANUAL docs, `.gitignore`, the `wiki/` scaffold, the skill set) comes
from one *template source*. There are two, and they produce the same result:
**a) A fresh clone of the canonical template repo (default when this skill
runs outside a Cascade KB, and always available).** Pull the repo into a
scratch folder and copy out of it:
```bash
rm -rf <scratch>
git clone --depth 1 --branch <branch> https://git.wierzbowa.cloud/michal/ckb.git <scratch>
```
`<scratch>` is `tmp/ckb-init-src` under the *current* KB when this skill
runs inside one (matching `ckb-upgrade`'s convention - that directory is
already gitignored), or a system temp path such as
`/tmp/ckb-init-src` when it doesn't.
Use a user-supplied URL instead if they named one ("initialize from
`<url>`") - a fork or an internal mirror is a legitimate template source.
#### Which branch (the release channel)
The canonical repo keeps three branches, and they are not interchangeable:
| Branch | What it is | Use it when |
|---|---|---|
| `main` | **Stable.** The released template. | Always, unless the user says otherwise. **This is the default.** |
| `test` | **Release candidate.** Changes validated before they reach `main`. | The user is helping validate a release, or needs a fix that has landed but not shipped. |
| `experimental` | **Development.** Active work; may be broken, may be reverted. | The user is developing the template itself, or explicitly wants the newest ideas and accepts the risk. |
Default to `main`. Take a different branch only when the user asks for one,
in whatever words: "initialize from the test branch", "use experimental",
"set up a dev KB", "bootstrap from `<url>` branch `<name>`", "I want the
bleeding edge". A fork's branch works the same way - `--branch` takes any
ref the source repo has.
If the user names a branch that does not exist on the source repo, the clone
fails with git's own error. Report it and list what does exist
(`git ls-remote --heads <url>`) rather than silently falling back to `main`:
a KB quietly initialized from the wrong channel is exactly the kind of thing
nobody notices until an upgrade behaves strangely.
**When initializing from `test` or `experimental`, say so plainly in the Step
12 report and explain what it means** - that this KB will keep pulling from
that channel until someone changes it, and that `experimental` in particular
carries no stability promise. Initializing onto a dev channel is a reasonable
thing to want; doing it without realizing is not.
If `git` isn't available or the clone fails (network, auth, unreachable
host), report the raw error; fall back to (b) if this skill is running
inside a Cascade KB, otherwise stop - there is nothing to copy from.
Clone into a scratch path *outside* the target folder, never into the
target itself. Cloning straight into the target would drag the template's
own git history, `.git/`, `workload/`, and any committed sample content
into the new KB; the new KB gets its own history in Step 11 instead. The
one exception is if the user explicitly says they want the template repo
itself checked out in place (i.e. "just clone ckb into `<dir>`") - that's a
plain `git clone`, not an init; do that, say so plainly, and skip the rest
of these steps.
**b) This repo's own working tree (default when this skill runs from inside
a Cascade KB and the user hasn't asked for a fresh pull).** Copy directly
from the current repo's files, exactly as described in the steps below.
This is the faster path and is what you want when the current KB is itself
the template being propagated - but it copies whatever local, possibly
uncommitted, template edits exist here. If the user says "pull the latest",
"from the remote", or "from upstream", use (a) instead.
If both are viable and the user hasn't indicated a preference, ask which
one - a stale local template and an upstream one that moved are a real
difference, not a formality.
For the rest of this skill, "this repo's `<path>`" means "`<path>` in the
resolved template source" - the steps are identical either way. Note the
source (URL + short commit hash, or "local working tree") for the Step 12
report, and delete the scratch clone once Step 10 has finished copying:
```bash
rm -rf <scratch>
```
### Step 4 - 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 full reusable KB
skill set - these operate purely on the `wiki/` structure (or, for
`ckb-init` and `ckb-upgrade` themselves, on the schema layer), so they
carry over cleanly and are part of "the schema" as far as this skill is
concerned. As of this writing, that's every skill under this repo's
`.agents/skills/`:
concerned. As of this writing, that's every skill under the template
source's `.agents/skills/`:
- `ckb-init` (this skill - a new KB can bootstrap further KBs of its own)
- `ckb-upgrade` (lets the new KB catch up with template changes later)
- `ckb-ingest`
- `ckb-decide`
- `ckb-retrieve`
- `ckb-index-external`
- `ckb-lint`
- `ckb-sync-changes`
- `ckb-project-summary`
@ -79,7 +175,7 @@ concerned. As of this writing, that's every skill under this repo's
- `ckb-export-starlight`
- `ckb-onboard-me`
- `ckb-teach-me`
- `cbk-quiz`
- `ckb-quiz`
Don't ask about any of these - just include them.
@ -94,9 +190,9 @@ that case, only include it if the user explicitly asks for it by name,
e.g. "also bring over `<skill-name>`."
Record the final skill list (the defaults above, plus anything explicitly
added) - this affects Steps 8 and 9.
added) - this affects Steps 9 and 10.
### Step 4 - Create the directory structure
### Step 5 - Create the directory structure
Under the target folder, create:
@ -109,17 +205,19 @@ raw/archive/
tmp/
wiki/entities/
wiki/graph/
wiki/projects/
wiki/decisions/
workload/
```
Git doesn't track empty directories, so every one of these - plus
`.agents/`, `.agents/skills/`, and `.claude/` created in Step 9 - needs a
`.agents/`, `.agents/skills/`, and `.claude/` created in Step 10 - needs a
placeholder to survive a fresh `git init` and first commit. This repo's
own convention is an empty file named `.gitadd` in each directory (not
`.gitkeep`) - match that convention exactly, so a new KB's directory
listing looks identical to this one's.
`libs/` and `tmp/` are gitignored per the schema (Step 8) except for their
`libs/` and `tmp/` are gitignored per the schema (Step 9) except for their
own `.gitadd` placeholder, so they stay effectively empty. `linked/` gets
a `.gitadd` too but is *not* gitignored - it's meant to hold real
symlinks, which git tracks natively (as a small blob storing the link
@ -127,7 +225,7 @@ target), so nothing extra is needed there. `raw/inbox/`, `raw/archive/`,
and `workload/` are meant to be tracked and start genuinely empty aside
from their placeholder.
### Step 5 - Write `AGENTS.md` and the `CLAUDE.md` symlink
### Step 6 - 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
@ -135,7 +233,7 @@ 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 `LICENSE`, `VERSION`, and the generic docs
### Step 7 - Write `LICENSE`, `VERSION`, and the generic docs
Copy these verbatim from this repo - they're already fully generic (no
project-specific content, confirmed by having zero references to any
@ -147,9 +245,39 @@ actual project entity):
should point somewhere else - it's what lets the new KB's own
`ckb-upgrade` find template updates later.
- `MANUAL.md` and `MANUAL.pl.md` - the task-oriented user guide.
- `CHANGELOG.md` and `CHANGELOG.pl.md` - the full page schema reference plus
the schema and template version histories. Copy verbatim: the history
belongs to the template, not to the new KB, and a fresh KB starts on the
version that history ends at.
- `VERSION` - copy the exact current value; the new KB starts life on the
same template version it was just cloned from.
Then write `ckb.yaml` at the new KB's root, recording where its tooling came
from so its own `ckb-upgrade` knows what to check later:
```yaml
template:
repo: https://git.wierzbowa.cloud/michal/ckb.git
branch: main
```
Use the URL and branch actually resolved in Step 3 - the user's fork URL if
they named one, `test`/`experimental` if they asked for that channel. Write
this block **even for the default** `main`/canonical case: an explicit record
is what makes a later channel switch visible, and it costs one file.
If the template source was this repo's own working tree (Step 3b) rather than
a clone, record the canonical repo URL and the branch this repo is currently
on (`git rev-parse --abbrev-ref HEAD`), since that is where the new KB's
upgrades will actually come from. Where that branch is `test` or
`experimental`, say so in the Step 12 report - propagating a dev channel into
a fresh KB by accident is easy, and the local working tree is exactly how it
happens.
`ckb.yaml` is also the modules manifest (`ckb-module` owns the `kb_modules:`
list). A fresh KB has no modules, so at init time the file holds the
`template:` block alone.
For `LICENSE`, copy the Apache License 2.0 text, but **ask the user first**
whether to keep the copyright line as-is (appropriate if this new KB is
still effectively part of the same umbrella/organization) or update it to
@ -158,14 +286,16 @@ project that will accumulate its own content under its own ownership) -
don't silently carry over a copyright attribution that may not apply to
what the new KB is about to collect.
### Step 7 - Write the empty `wiki/` scaffold
### Step 8 - 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.
- **`wiki/index.md`** - frontmatter with `kb_schema_version` only, set to
whatever value the template source's own `wiki/index.md` carries (do not
hard-code it here - it moves with the schema).
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
@ -177,16 +307,29 @@ generic structure:
- **`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/index.md`** - header + pointer to `edges.json`, the edge
vocabulary table and its conventions **verbatim** (`ckb-ingest`,
`ckb-decide`, `ckb-index-external`, and `ckb-lint` all write against that
closed set, so it is contract, not example content), and a generic
"(No edges recorded yet)" placeholder instead of any real coverage line.
- **`wiki/graph/edges.json`** - `{"version": 1, "last_updated": "<today>", "edges": []}`.
- **`wiki/projects/index.md`** - header + placeholder note, no project
scopes yet.
- **`wiki/decisions/index.md`** - header, the status-vocabulary table, and a
placeholder note; no decisions yet. Copy the status vocabulary verbatim —
`ckb-decide` and `ckb-lint` both validate against it.
- **`wiki/decisions/log.md`** - header and explanation only, no entries.
- **`wiki/query-gaps.md`** - header + explanation only, no recorded gaps.
If the template source's `wiki/` holds scaffold files beyond this list,
carry them over the same way - empty, structure only. The list above is
what the schema requires, not a cap.
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 8 - Write `.gitignore`
### Step 9 - Write `.gitignore`
Copy this repo's actual current `.gitignore` verbatim rather than
reconstructing it from memory - it uses a `<dir>/*` + `!<dir>/.gitadd`
@ -196,6 +339,16 @@ tracked shell but have their real contents ignored:
```
libs/*
!libs/.gitadd
!libs/*/
libs/*/*
!libs/*/source.yaml
!libs/*/index.md
!libs/*/log.md
!libs/*/entities/
!libs/*/graph/
# Per-user local override (e.g. source.local.yaml's access: write) — never shared, stays ignored
# by the libs/*/* catch-all above; listed explicitly for clarity, not because it changes behavior.
libs/*/*.local.yaml
tmp/*
!tmp/.gitadd
outputs/starlight
@ -204,8 +357,13 @@ outputs/teaching
.env
```
(That `libs/` block is what keeps a connector-backed lib's `source.yaml`
and its agent-generated index tracked while ignoring everything else under
it - copy it as a unit. If the template source's `.gitignore` has since
changed, its version wins over the snapshot above.)
Since `ckb-export-starlight`, `ckb-export-okf`, and `ckb-teach-me` are all
in the default skill set (Step 3), their `outputs/` subfolders
in the default skill set (Step 4), their `outputs/` subfolders
(`outputs/starlight`, `outputs/okf`, `outputs/teaching`) are gitignored by
default too - each exists to be ignored precisely because its skill is
present by default. If a future skill set change ever drops one of those
@ -213,26 +371,61 @@ three skills from the default set, drop its matching ignore line too;
if the user explicitly adds a skill with its own `outputs/<x>` convention,
ask whether it needs a similar ignore line.
### Step 9 - Copy the skill set from Step 3
### Step 10 - Copy the skill set from Step 4
Copy each skill's folder from this repo's `.agents/skills/<name>/` into the
target's `.agents/skills/<name>/` unchanged (including each skill's own
license footer, and any support files like
`ckb-export-starlight/scripts/export_starlight.py`) - the full default set
from Step 3, plus anything explicitly added. Then create `.claude/skills`
`ckb-export-okf/scripts/export_okf.py`,
`ckb-export-starlight/scripts/export_starlight.py`,
`ckb-lint/scripts/lint_report.py`) - the full default set
from Step 4, 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 10 - Report
Also copy `.agents/modules/` wholesale. Modules are inert until installed,
so a new KB carries the full catalogue at zero context cost, and the user
can enable one later with `ckb-module` instead of re-deriving it. Do **not**
install any module into the new KB, and do not create a `ckb.yaml`: an
absent manifest correctly means "nothing installed". If the user has said
the new KB is for software they are building, mention the `software` module
and offer to install it after Step 11 — offer, don't assume.
### Step 11 - Initialize the new KB's own git history (ask first)
The target is now a complete KB but has no history of its own - and if the
template came from a clone (Step 3a), it deliberately carries none of the
template's. Ask whether to initialize one:
```bash
git init
git add .
git commit -m "Initialize Cascade Knowledge Base from ckb template v<VERSION>"
```
Only do this if the target isn't already inside a git repo (`git rev-parse
--is-inside-work-tree` from the target) - if it is, say so and leave the
staging to the user rather than committing into someone else's repo. Don't
add a remote and don't push; that's `ckb-sync-changes`' job once the user
has a remote to point at.
### Step 12 - Report
Tell the user:
- The resolved target path.
- The template source used: the clone URL, **branch**, and short commit hash,
or "this repo's local working tree" and the branch it was on. Name the
channel in words when it isn't `main` - "this KB tracks `experimental`,
which carries no stability promise; say 'switch to the main channel' to
change that."
- That `ckb.yaml` records the repo and branch for future upgrades.
- The directory tree created.
- Whether `AGENTS.md`/`CLAUDE.md` were written or (per Step 2) skipped/merged.
- Which skills were copied (the full default set, plus anything explicitly
added).
- The `VERSION` the new KB starts on, and what was decided for `LICENSE`'s
copyright line.
- Whether a git repo was initialized in the target, or why not.
- Next step: "Drop material into `raw/inbox/` and say 'Ingest' to populate the wiki for the first time."
## Edge cases
@ -246,6 +439,19 @@ Tell the user:
- **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.
- **Clone fails and there's no local KB to fall back on** - stop and report
the raw git error. Don't hand-write an approximation of the scaffold from
memory; a KB whose `AGENTS.md` is a paraphrase is worse than no KB.
- **User asks to clone the template repo straight into the target** - point
out that this gives them the template's git history and files rather than
a fresh KB, and offer both: a plain `git clone` (if that's really what
they want) or the scratch-clone-then-init flow, which is the default.
- **Target already contains a `.git/`** - proceed with the scaffold, but
skip Step 11's `git init`/commit and say so; committing into someone
else's repo is theirs to decide.
- **Scratch clone path already exists** (a previous run died mid-way) -
remove it before cloning (`rm -rf`), and remove it again when done. Never
reuse a half-cloned scratch dir.
- **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

View file

@ -1,6 +1,6 @@
---
name: ckb-lint
description: Health-check the wiki/ — conformance, freshness, confidence decay, retention sweep, supersession detection, orphan detection, graph consistency, index/log consistency, and error-book entries — auto-fixing what it safely can and reporting the rest, then reminding the user to review and sync to origin. Also runs the same checks against each connector-backed libs/<name>/'s own generated index (see ckb-index-external), plus a source.yaml validity check, without ever touching a git-copy lib or a connector's source.yaml itself. Use when the user says "Lint", "health-check the wiki", "check the wiki", or asks for a periodic/scheduled wiki health check. This is the maintenance workflow from CLAUDE.md/AGENTS.md §6, distinct from `ckb-ingest` (which adds new content) and `ckb-sync-changes` (a pure git-level operation with no wiki synthesis at all).
description: Health-check the wiki/ — conformance, freshness, confidence decay, retention sweep, supersession detection, orphan detection, graph consistency, index/log consistency, and error-book entries — auto-fixing what it safely can and reporting the rest, then reminding the user to review and sync to origin. Also runs the same checks against each connector-backed libs/<name>/'s own generated index (see ckb-index-external), plus a source.yaml validity check, without ever touching a git-copy lib or a connector's source.yaml itself. Use when the user says "Lint", "health-check the wiki", "check the wiki", or asks for a periodic/scheduled wiki health check. This is the maintenance workflow routed by CLAUDE.md/AGENTS.md, distinct from `ckb-ingest` (which adds new content) and `ckb-sync-changes` (a pure git-level operation with no wiki synthesis at all).
---
# Lint skill
@ -17,9 +17,18 @@ Implemented as a skill (rather than living inline in
`CLAUDE.md`/`AGENTS.md`) so the full checklist only loads into context when
actually invoked — on demand, or on a schedule if the user has set one up.
The **detection** half of this checklist is mechanical — a fixed set of
frontmatter, date, link, graph, and index/log rules over a tree that keeps
growing — so it runs as a read-only Python script rather than as per-page
model reasoning (Step 0 below). What the script cannot decide is what this
skill is actually for: which findings to auto-fix, which to leave for the
user, whether two pages genuinely supersede one another, and whether a
cluster of findings is a systemic pattern worth an error-book entry. Read
the script's report, then do that work.
This skill only ever writes within `wiki/` (including moves into
`wiki/archived/`) and, for any connector-backed `libs/<name>/` (one with a
`source.yaml` — see `CLAUDE.md`/`AGENTS.md` §1/§4) where this user has
`source.yaml` — see `CLAUDE.md`/`AGENTS.md` directory contract) where this user has
local `access: write` (see `ckb-index-external`), within that connector's
own agent-owned generated index (`index.md`/`entities/`/`graph/`/`log.md`).
For a connector-backed `libs/<name>/` where this user is read-only (the
@ -57,59 +66,107 @@ are logged in that connector's own `log.md`, never in `wiki/log.md`. A
git-copy `libs/<name>/` (no `source.yaml`) and any `source.yaml`/
`source.local.yaml` are never touched by any of these checks.
### 0 — Run the checker script
From the repository root:
```bash
python3 "<skill-dir>/scripts/lint_report.py"
```
Resolve `<skill-dir>` to this skill's own directory. Flags: `--scope
wiki|libs|all` (default `all`) to limit which trees are checked; `--today
YYYY-MM-DD` to pin the date for reproducible runs; `--json` for a
machine-readable report; `--quick` for the one-line session-start summary
Rule E calls (conformance, freshness, and source drift only — it prints a
single line and does none of the work below). Exit code is `0` when nothing was found and `1`
when there are findings — read the report either way.
The script is strictly read-only: it writes nothing, moves nothing, and
runs no `git` commands. It covers the mechanical detection in checks 1, 2,
3, 4, 6, 7, 8, 10, 12, and 13 below — plus the structural half of check 5
(decision records) — across `wiki/` and every connector-backed
`libs/<name>/` (reporting each one's `access:` level so you know where a
fix is even permitted). Check 9, the semantic half of check 5, and every
auto-fix-vs-report decision, are yours.
The numbered checks below stay as the reference for *what each finding
means and what to do about it* — you don't need to re-derive the detection
by hand. Do read the pages the script flags: a finding is a pointer to a
page that needs a decision, not the decision itself.
### 1 — Conformance check
Verify every non-reserved `.md` file under `wiki/` (i.e. excluding
`index.md` and `log.md`) has parseable YAML frontmatter with a non-empty
`type` field. Flag violations first, and treat flagged pages as
unreliable input for the checks below rather than guessing at their
intended type/content.
The script flags every non-reserved `.md` file under `wiki/` (i.e.
excluding `index.md` and `log.md`) whose frontmatter is unparseable,
absent, or missing a non-empty `type`. Treat flagged pages as unreliable
input for the checks below rather than guessing at their intended
type/content, and fix the frontmatter before acting on any later finding
about the same page.
### 2 — Freshness check
Scan every page whose `last_updated` exceeds its `freshness_window_days`.
Flag as stale; suggest the user confirm or update the content — don't
silently rewrite stale content yourself.
The script reports every page whose `last_updated` exceeds its
`freshness_window_days`, and by how much. Suggest the user confirm or
update the content — don't silently rewrite stale content yourself.
### 3 — Confidence decay
Reduce `confidence` on pages not reinforced by a new source since the last
check. Pages that fall below 0.3 confidence get flagged for re-review.
The script reports pages already below 0.3 `confidence` (and any
unparseable value). Decaying `confidence` on pages not reinforced by a new
source since the last check is a write and a judgment call, so it stays
yours — the script only surfaces where the floor has been crossed.
### 4 — Retention sweep
Move `retention: low` pages older than 2× their `freshness_window_days`
into `wiki/archived/`. Never delete — always move, and log the move (see
Rule B in `CLAUDE.md`/`AGENTS.md` §7) with a note explaining why.
The script reports `retention: low` pages older than 2x their
`freshness_window_days` as archive candidates. Move them into
`wiki/archived/` — never delete, always move — and log each move (see
Rule B in `CLAUDE.md`/`AGENTS.md`) with a note explaining why. Check each
candidate against recent use before moving it (see Edge cases).
### 5 — Supersession detection
### 5 — Supersession and decision records
When two pages appear to cover the same entity, check for contradictions.
If one is clearly newer/better corroborated, add `supersedes` /
`superseded_by` links between them. Preserve the older page rather than
deleting it, but mark it stale.
The script checks the *structure* of every `type: decision` page: a status
from the vocabulary, a `decided_on` date where the status implies one, a
`decided_by` (`unknown` counts — an omitted field doesn't), `affects` targets
and supersession links that resolve, supersession set on **both** sides,
`status: superseded`/`reversed` matched by a `superseded_by`, unique
four-digit decision numbers, and any `review_on` date that has passed. Fix
those directly — they're mechanical. A `review_on` that has come due is not a
defect, though: report it so the user can decide whether to revisit, and never
change a decision's substance yourself (`ckb-decide` → decisions are
append-only).
The *semantic* half stays yours: when two pages appear to cover the same
entity or the same choice, check for contradictions. If one is clearly
newer/better corroborated, add `supersedes` / `superseded_by` links between
them. Preserve the older page rather than deleting it, but mark it stale. For
two decisions, that judgment is exactly what the script can't make — it can
tell you a link is one-sided, not that a link should exist.
### 6 — Orphan detection
Find pages with no inbound links (`[[wikilinks]]` or
`[markdown](path.md)` references from elsewhere in the wiki). Either add
backlinks from relevant pages where an obvious connection exists, or move
the orphan to `wiki/archived/` with a log note if no natural backlink
exists.
The script reports pages with no inbound links (`[[wikilinks]]` or
`[markdown](path.md)` references from anywhere else in the tree). For each
one, either add backlinks from relevant pages where an obvious connection
exists, or move the orphan to `wiki/archived/` with a log note if no
natural backlink does — that choice is the judgment the script leaves you.
### 7 — Graph consistency
Verify every edge in `wiki/graph/edges.json` points to an existing entity
page. Remove or fix broken edges; note what was removed rather than
silently dropping entries.
The script reports every edge in `wiki/graph/edges.json` whose `from`/`to`
does not resolve to an existing page (plus malformed edges and invalid
JSON). Remove or fix them; note what was removed rather than silently
dropping entries.
### 8 — Index/log consistency
Verify every subdirectory under `wiki/` that contains pages has an
`index.md` listing all of them, and that no single change is recorded in
both a subdirectory `log.md` and the root `wiki/log.md` (per the
Recursive Index & Log Convention). Fix missing index entries and
duplicate log entries directly.
The script reports subdirectories holding pages but no `index.md`, index
files that don't list a page sitting next to them, and any change recorded
in both a subdirectory `log.md` and the root `wiki/log.md` (per the
Recursive Index & Log Convention). Fix missing index entries and duplicate
log entries directly — these are the safest auto-fixes on the list.
### 9 — Error Book entry
@ -121,22 +178,123 @@ doesn't need an Error Book entry — this is for patterns, not incidents.
### 10 — External source config check
For each `libs/<name>/source.yaml`, verify it has a non-empty `connector`
and `location` — report only, this file is never edited by any skill. If
an `index:` block is present, verify it has a non-empty `store` and
`location` too. Also flag (report only) a `libs/<name>/` that ambiguously
has both real content files and a `source.yaml` — that's a configuration
conflict for the user to resolve, not something to guess at.
The script validates each `libs/<name>/source.yaml` (non-empty `connector`
and `location`; non-empty `store` and `location` inside any `index:` block;
a positive integer `refresh_interval_days`) and flags a `libs/<name>/` that
ambiguously holds both real content files and a `source.yaml`. All of this
is **report only**`source.yaml` is never edited by any skill, and the
ambiguous-content case is a configuration conflict for the user to resolve,
not something to guess at.
It also reports any connector-backed source whose generated index is
overdue — newest `last_updated` in `libs/<name>/` older than its
`refresh_interval_days` (default 30) — with how overdue it is, since a
source two days past a 7-day interval is a different situation from one six
months past a 30-day one. Relay that the same way whether or not this user
has write access: a read-only user can't fix it, but knowing which source to
chase the admin about is the actionable part. Never re-index here; that's
`ckb-index-external`'s job, and suggesting it is as far as this check goes.
Don't flag a missing `libs/<name>/source.local.yaml` as an issue — its
absence is the correct, read-only default (see `ckb-index-external`), not
a gap to report or fix.
a gap to report or fix. The script follows the same rule: it reads that
file only to label each connector `access: read-only` / `access: write` in
its report.
### 11 — Installed module checks
If `ckb.yaml` lists installed modules, run whatever extra checks those
modules define, and skip this step entirely when the file is absent or
empty. Modules are optional by design: their absence is never a finding.
For the `software` module specifically:
- **Stale code maps** — a `type: repository` page whose `commit` is behind
the current `HEAD` of its `src/` clone. Report how far behind; suggest
`ckb-code-map`. A missing clone is **not** a finding — `src/` is
gitignored and expected to be empty on a fresh checkout.
- **Spec drift** — the four checks `ckb-spec` defines: root specs with
empty `implemented_by`, repo-level specs implementing a missing or
archived parent, one-sided `implements`/`implemented_by` links, and root
specs older than the mapped commit of every repo implementing them.
- **Remote-less repos** — a mapped repository page with no `resource:`,
meaning a gitignored clone that exists nowhere else.
All report-only. Never auto-fix any of these: each is a statement about
intent, and only the user knows which side is correct.
### 12 — Source fingerprint drift
The script recomputes the `sha256` of every local file cited in a page's
`## Sources` and compares it against the digest recorded there. Three
findings come out of it, and they are not the same problem:
- **`CHANGED since this page was built`** — the file the page was
synthesized from has been edited. This outranks every freshness finding
on the list: check 2 says a page has *aged*, this says its evidence has
*moved*. Read both, and either re-verify the page's claims against the
new content and re-stamp the digest, or flag it for `ckb-ingest`. Never
re-stamp a digest without reading what changed — that converts a real
finding into a silent lie.
- **`does not exist`** — a cited source was moved, renamed, or deleted. The
page now rests on nothing. Find where the source went if you can; if you
can't, say so on the page rather than leaving a citation that looks
valid.
- **`has no fingerprint recorded`** — a `## Sources` bullet predating this
convention, or written by hand. Safe to auto-fix *only* when the page has
not otherwise changed: compute the digest and record it with today's
`source_checked`. If the page is also flagged by check 2 or 13, fix those
first — stamping a digest onto a page you haven't verified just freezes
the drift in place.
An `etag:`/`mtime:` fingerprint (connector-sourced, per
`ckb-index-external`) is skipped here: there is nothing local to recompute.
Those are verified by re-indexing that source, not by this script.
### 13 — Crux verbatimness
`## Crux` is quoted evidence, so it admits a check no synthesized prose
does: the quote either still appears in the source or it doesn't. The
script flags four cases:
- **Quote not found verbatim in the cited source** — the strongest finding
the linter produces. The page asserts, in quotation marks, something its
source does not say. Either the source was edited (check 12 usually fires
alongside; fix them together) or the quote was paraphrased into existence
at ingest time, which is a correctness problem, not a formatting one.
Never "fix" this by editing the quote to match the source — re-read the
source, decide what it actually supports, and rewrite the page's claim.
- **`## Crux` has no quoted lines** — prose sitting under an evidence
heading. Either quote the source properly or delete the section; a
paraphrase labelled as evidence is worse than no evidence.
- **Quote is not attributed** — no `— \`path\`` line saying which source it
came from, so it can't be verified by anyone. Attribute it from the
page's `## Sources` if the origin is unambiguous, otherwise flag it.
- **`## Crux` with no `## Sources`** — an evidence section with nothing to
verify against.
Quotes under 24 characters and sources in non-text formats (PDF, DOCX,
audio) are fingerprinted but not quote-matched; the script skips them
rather than reporting false positives.
### 14 — Protected `## Notes`
This one has no script check, because it is a rule about *writing*, not a
property of a tree: no skill may rewrite, reflow, summarize, or drop a
`## Notes` section (page schema, `CLAUDE.md`/`AGENTS.md`). It is listed
here so the rule has somewhere to be enforced from. When a lint fix touches
a page — a backlink, an index entry, an archival move — carry its
`## Notes` across byte-for-byte, and check after any bulk edit that none
were lost. On a generated page under `libs/<name>/`, a missing `## Notes`
is worth adding empty so the affordance exists; anywhere else its absence
is normal and not a finding.
### Auto-fix vs. report
Auto-fix what can be done safely and mechanically: broken links, missing
backlinks, stale flags, missing index entries, duplicate log entries,
dangling graph edges. Report anything that needs a judgment call
dangling graph edges, and a missing fingerprint on an otherwise-unflagged
page. Report anything that needs a judgment call
(supersession decisions, low-confidence content, ambiguous orphans) rather
than guessing on the user's behalf.
@ -161,11 +319,14 @@ fix or flag), skip this reminder — there's nothing to review or sync.
## Edge cases
- **Wiki is empty or near-empty** — report that there's little to lint
yet; don't fabricate findings to look thorough.
yet; don't fabricate findings to look thorough. A `total findings: 0`
report is a valid outcome; say so plainly rather than hunting for
something to say.
- **A page's frontmatter is unparseable** (not just missing `type`, but
invalid YAML) — flag it prominently in the conformance check and skip
it in every later numbered check rather than letting a parse error
crash or silently mis-handle downstream logic.
invalid YAML) — the script reports it under conformance and skips it in
the later checks rather than reasoning from a half-parsed page. Surface
it prominently and fix the frontmatter first; a page can't be judged
stale, orphaned, or archivable until it parses.
- **Retention sweep would archive a page that's clearly still in active
use** (e.g. linked from a very recent `workload/` entry) — flag it for
the user to confirm rather than auto-archiving; recency of use can
@ -173,9 +334,19 @@ fix or flag), skip this reminder — there's nothing to review or sync.
- **Supersession is ambiguous** (two pages disagree and neither is
clearly newer/better corroborated) — report the conflict rather than
guessing which one wins.
- **Repeated run with nothing changed since the last lint** — should
produce essentially the same clean report each time; don't invent
variation just to seem active.
- **Repeated run with nothing changed since the last lint** — produces the
same report each time (pass `--today` to pin the date if you need a
byte-identical one); don't invent variation just to seem active.
- **A changed fingerprint and a diverged crux quote on the same page**
one incident, not two. The source was edited; fix the page once against
the new content and re-stamp both the digest and the quote together.
- **Every page in the wiki lacks fingerprints** — a wiki built before this
convention. Don't stamp them all in one pass; that produces a tree of
digests attesting to nothing anyone verified. Report the count, and
backfill as pages are touched for other reasons.
- **The script errors out or isn't runnable** (no Python 3, unreadable
tree) — say so and fall back to working the numbered checks by hand
rather than reporting a clean bill of health you didn't actually verify.
---

View file

@ -0,0 +1,793 @@
#!/usr/bin/env python3
# Copyright 2026 Michał Kopeć
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Deterministic half of the ckb-lint health check: detect, never fix.
Usage:
python3 lint_report.py [--scope wiki|libs|all] [--today YYYY-MM-DD] [--json] [--quick]
Run from the repository root (the directory containing wiki/ and libs/).
This script is strictly READ-ONLY. It writes no files, moves nothing, and runs
no git commands it prints findings for the agent to act on. The mechanical
checks (conformance, freshness, retention/decay candidates, orphans, graph
consistency, index/log consistency, source.yaml validity, source fingerprints,
crux verbatimness) live here; the
judgment calls the ckb-lint skill owns (whether two pages genuinely supersede
one another, ambiguous orphans, error-book entries, auto-fix vs. report) stay
with the model. Decision records get their own structural checks here status
vocabulary, required dates, two-sided supersession links since those are
mechanical; deciding that a new decision *replaces* an old one is not.
Exit code is 0 when nothing was found and 1 when there are findings.
"""
import argparse
import hashlib
import json
import re
import sys
from datetime import date, datetime
from pathlib import Path
REPO_ROOT = Path.cwd()
WIKI = REPO_ROOT / "wiki"
LIBS = REPO_ROOT / "libs"
RESERVED = {"index.md", "log.md"}
DEFAULT_REFRESH_DAYS = 30
DECISION_STATUSES = {"proposed", "accepted", "rejected", "superseded", "reversed"}
# --------------------------------------------------------------------------
# tiny YAML readers (stdlib only — this template ships without dependencies)
# --------------------------------------------------------------------------
def parse_frontmatter(text):
"""Return (dict, body, error). `error` is a string when the frontmatter is
present but unparseable, else None. Flat `key: value` pairs only."""
if not text.startswith("---\n"):
return {}, text, None
end = text.find("\n---\n", 4)
if end == -1:
return {}, text, "frontmatter opened with `---` but never closed"
raw, body = text[4:end], text[end + 5 :]
fm = {}
for lineno, line in enumerate(raw.split("\n"), start=2):
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
if line[:1].isspace():
return {}, body, f"line {lineno}: nested/indented YAML is not supported in page frontmatter"
if ":" not in stripped:
return {}, body, f"line {lineno}: not a `key: value` pair: {stripped!r}"
key, _, value = stripped.partition(":")
fm[key.strip()] = unquote(value.strip())
return fm, body, None
def unquote(value):
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
return value[1:-1]
return value
def parse_simple_yaml(text):
"""Two-level `key: value` / `key:` + indented block YAML, enough for
source.yaml and source.local.yaml. Returns a dict; nested blocks become
nested dicts."""
out, current = {}, None
for line in text.split("\n"):
if not line.strip() or line.strip().startswith("#"):
continue
indented = line[:1].isspace()
stripped = line.strip()
if ":" not in stripped:
continue
key, _, value = stripped.partition(":")
key, value = key.strip(), unquote(value.split(" #", 1)[0].strip())
if indented:
if isinstance(current, dict):
current[key] = value
continue
if value == "":
current = {}
out[key] = current
else:
out[key] = value
current = None
# `key:` with nothing indented under it is an empty scalar, not a block.
return {k: ("" if v == {} else v) for k, v in out.items()}
def as_float(value):
try:
return float(value)
except (TypeError, ValueError):
return None
def as_int(value):
try:
return int(str(value).strip())
except (TypeError, ValueError):
return None
def as_date(value):
try:
return datetime.strptime(str(value).strip(), "%Y-%m-%d").date()
except (TypeError, ValueError):
return None
# --------------------------------------------------------------------------
# tree model
# --------------------------------------------------------------------------
LINK_RE = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
WIKILINK_RE = re.compile(r"\[\[([^\]]+)\]\]")
SOURCE_BULLET_RE = re.compile(r"^\s*[-*]\s+`([^`]+)`(.*)$", re.M)
FINGERPRINT_RE = re.compile(r"\b(sha256|etag|mtime):([^\s,)]+)")
ATTRIBUTION_RE = re.compile(r"^\s*[\u2014-]\s*`([^`]+)`", re.M)
# Text-ish sources a quote can actually be checked against byte-for-byte.
# Anything else (pdf, docx, audio) is fingerprinted but never quote-verified.
QUOTABLE_SUFFIXES = {".md", ".txt", ".eml", ".csv", ".json", ".yaml", ".yml", ".html", ".rst", ".log"}
class Doc:
def __init__(self, root, path):
self.path = path
self.rel = path.relative_to(root)
text = path.read_text(encoding="utf-8", errors="replace")
self.fm, self.body, self.fm_error = parse_frontmatter(text)
self.reserved = path.name in RESERVED
def links(self):
"""Every intra-tree link target as a path relative to the tree root."""
out = set()
for target in LINK_RE.findall(self.body):
target = target.split("#", 1)[0].strip()
if not target or re.match(r"^[a-z][a-z0-9+.-]*:", target):
continue
if target.startswith("/wiki/"):
out.add(target[len("/wiki/") :])
elif target.startswith("/"):
out.add(target.lstrip("/"))
else:
try:
resolved = (self.rel.parent / target).as_posix()
except ValueError:
continue
parts = []
for part in resolved.split("/"):
if part == "..":
if parts:
parts.pop()
elif part not in (".", ""):
parts.append(part)
out.add("/".join(parts))
return out
def wikilinks(self):
return {w.split("|", 1)[-1].strip() for w in WIKILINK_RE.findall(self.body)}
def read_tree(root):
docs = []
for path in sorted(root.rglob("*.md")):
if any(part.startswith(".") for part in path.relative_to(root).parts):
continue
docs.append(Doc(root, path))
return docs
# --------------------------------------------------------------------------
# checks (each returns a list of finding strings)
# --------------------------------------------------------------------------
def check_conformance(docs):
findings = []
for doc in docs:
if doc.reserved:
continue
if doc.fm_error:
findings.append(f"{doc.rel}: unparseable frontmatter — {doc.fm_error}")
elif not doc.fm:
findings.append(f"{doc.rel}: no frontmatter")
elif not doc.fm.get("type", "").strip():
findings.append(f"{doc.rel}: missing or empty `type`")
return findings
def check_freshness(docs, today):
findings = []
for doc in docs:
if doc.reserved or doc.fm_error:
continue
window = as_int(doc.fm.get("freshness_window_days"))
updated = as_date(doc.fm.get("last_updated"))
if window is None or updated is None:
if doc.fm and doc.fm.get("last_updated") and updated is None:
findings.append(f"{doc.rel}: `last_updated` is not a YYYY-MM-DD date: {doc.fm['last_updated']!r}")
continue
age = (today - updated).days
if age > window:
findings.append(f"{doc.rel}: stale — {age}d since last_updated, window is {window}d ({age - window}d over)")
return findings
def check_confidence(docs):
findings = []
for doc in docs:
if doc.reserved or doc.fm_error or "confidence" not in doc.fm:
continue
value = as_float(doc.fm["confidence"])
if value is None:
findings.append(f"{doc.rel}: `confidence` is not a number: {doc.fm['confidence']!r}")
elif value < 0.3:
findings.append(f"{doc.rel}: confidence {value} is below 0.3 — flag for re-review")
return findings
def check_retention(docs, today):
findings = []
for doc in docs:
if doc.reserved or doc.fm_error:
continue
if doc.fm.get("retention", "").strip().lower() != "low":
continue
window = as_int(doc.fm.get("freshness_window_days"))
updated = as_date(doc.fm.get("last_updated"))
if window is None or updated is None:
continue
age = (today - updated).days
if age > 2 * window:
findings.append(
f"{doc.rel}: archive candidate — retention: low, {age}d old, 2x window is {2 * window}d"
)
return findings
def check_decisions(docs, root, today):
"""Decision-record specific rules (see the ckb-decide skill)."""
findings = []
decisions = [d for d in docs if not d.reserved and not d.fm_error
and d.fm.get("type", "").strip().lower() == "decision"]
by_path = {d.rel.as_posix(): d for d in decisions}
seen_numbers = {}
for doc in sorted(decisions, key=lambda d: d.rel.as_posix()):
rel = doc.rel.as_posix()
status = doc.fm.get("status", "").strip().lower()
if not status:
findings.append(f"{rel}: decision has no `status`")
elif status not in DECISION_STATUSES:
findings.append(
f"{rel}: `status: {status}` is not one of {', '.join(sorted(DECISION_STATUSES))}"
)
decided_on = as_date(doc.fm.get("decided_on"))
if status in ("accepted", "rejected", "reversed"):
if not doc.fm.get("decided_on"):
findings.append(f"{rel}: `status: {status}` but no `decided_on` date")
elif decided_on is None:
findings.append(f"{rel}: `decided_on` is not a YYYY-MM-DD date: {doc.fm['decided_on']!r}")
if decided_on and decided_on > today:
findings.append(f"{rel}: `decided_on` is in the future: {decided_on.isoformat()}")
if not doc.fm.get("decided_by", "").strip():
findings.append(f"{rel}: no `decided_by` — record `unknown` rather than omitting it")
review = doc.fm.get("review_on")
if review:
review_date = as_date(review)
if review_date is None:
findings.append(f"{rel}: `review_on` is not a YYYY-MM-DD date: {review!r}")
elif review_date <= today:
findings.append(
f"{rel}: due for review — `review_on: {review_date.isoformat()}` "
f"passed {(today - review_date).days}d ago"
)
# Supersession links must resolve and must be reciprocal.
for field, mirror in (("supersedes", "superseded_by"), ("superseded_by", "supersedes")):
target = doc.fm.get(field, "").strip()
if not target:
continue
key = target[len("/wiki/") :] if target.startswith("/wiki/") else target.lstrip("/")
other = by_path.get(key)
if other is None:
if not (root / key).is_file():
findings.append(f"{rel}: `{field}: {target}` does not resolve to a page")
continue
back = other.fm.get(mirror, "").strip()
back_key = back[len("/wiki/") :] if back.startswith("/wiki/") else back.lstrip("/")
if back_key != rel:
findings.append(
f"{rel}: `{field}` points at {key}, but that page's `{mirror}` "
f"does not point back ({back or 'unset'}) — supersession must be two-sided"
)
if status in ("superseded", "reversed") and not doc.fm.get("superseded_by", "").strip():
findings.append(f"{rel}: `status: {status}` but no `superseded_by` naming what replaced it")
if doc.fm.get("superseded_by", "").strip() and status not in ("superseded", "reversed"):
findings.append(
f"{rel}: has `superseded_by` but `status: {status or 'unset'}` — "
"expected `superseded` or `reversed`"
)
for target in [t.strip() for t in doc.fm.get("affects", "").split(",") if t.strip()]:
key = target[len("/wiki/") :] if target.startswith("/wiki/") else target.lstrip("/")
if not (root / key).is_file():
findings.append(f"{rel}: `affects` entry does not resolve to a page: {target}")
m = re.match(r"^(\d{4})-", doc.rel.name)
if m:
seen_numbers.setdefault(m.group(1), []).append(rel)
else:
findings.append(f"{rel}: decision filename does not start with a four-digit number")
for number, paths in sorted(seen_numbers.items()):
if len(paths) > 1:
findings.append(f"decision number {number} used by more than one page: {', '.join(sorted(paths))}")
return findings
def check_orphans(docs):
inbound = set()
titles = {}
for doc in docs:
rel = doc.rel.as_posix()
titles[rel] = rel
stem = doc.rel.with_suffix("").as_posix()
titles[stem] = rel
for doc in docs:
for target in doc.links():
if target in titles:
inbound.add(titles[target])
for wl in doc.wikilinks():
key = wl[:-3] if wl.endswith(".md") else wl
if key in titles:
inbound.add(titles[key])
findings = []
for doc in docs:
rel = doc.rel.as_posix()
if doc.reserved or rel in inbound:
continue
findings.append(f"{rel}: no inbound links from anywhere in the tree")
return findings
def check_graph(root, docs):
findings = []
edges_path = root / "graph" / "edges.json"
if not edges_path.is_file():
return findings
try:
data = json.loads(edges_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
return [f"graph/edges.json: invalid JSON — {exc}"]
edges = data.get("edges", data) if isinstance(data, dict) else data
if not isinstance(edges, list):
return ["graph/edges.json: `edges` is not a list"]
known = {doc.rel.as_posix() for doc in docs}
known |= {doc.rel.with_suffix("").as_posix() for doc in docs}
for i, edge in enumerate(edges):
if not isinstance(edge, dict):
findings.append(f"graph/edges.json: edge {i} is not an object")
continue
for side in ("from", "to"):
value = str(edge.get(side, "")).strip()
if not value:
findings.append(f"graph/edges.json: edge {i} has no `{side}`")
continue
# Edge paths are project-root-absolute per Rule C
# (`/wiki/entities/foo.md`), while `known` holds paths relative to
# wiki/. Strip the prefix the same way the decision-record check
# above already does, or every conformant edge reads as broken.
stripped = value[len("/wiki/"):] if value.startswith("/wiki/") else value.lstrip("/")
candidates = {
value,
value.removesuffix(".md"),
stripped,
stripped.removesuffix(".md"),
f"entities/{value}",
f"entities/{value}".removesuffix(".md"),
}
if not candidates & known:
findings.append(f"graph/edges.json: edge {i} `{side}: {value}` does not resolve to a page")
return findings
def check_index_and_logs(root, docs):
findings = []
by_dir = {}
for doc in docs:
by_dir.setdefault(doc.rel.parent, []).append(doc)
for directory, entries in sorted(by_dir.items()):
pages = [d for d in entries if not d.reserved]
index = next((d for d in entries if d.path.name == "index.md"), None)
if not pages:
continue
if index is None:
findings.append(f"{(directory / 'index.md').as_posix()}: missing — directory holds {len(pages)} page(s)")
continue
listed = {Path(t).name for t in index.links()} | {
(w if w.endswith(".md") else w + ".md").split("/")[-1] for w in index.wikilinks()
}
for page in sorted(pages, key=lambda d: d.rel.as_posix()):
if page.path.name not in listed:
findings.append(f"{index.rel.as_posix()}: does not list {page.rel.as_posix()}")
# A change must have exactly one home log (Recursive Index & Log Convention).
logs = [d for d in docs if d.path.name == "log.md"]
root_log = next((d for d in logs if d.rel == Path("log.md")), None)
if root_log is not None and len(logs) > 1:
root_entries = log_entry_keys(root_log.body)
for sub in logs:
if sub is root_log:
continue
for key in sorted(log_entry_keys(sub.body) & root_entries):
findings.append(
f"log.md and {sub.rel.as_posix()}: same change recorded in both — {key}"
)
return findings
LOG_HEADER_RE = re.compile(r"^##\s*\[?(\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2})?)\]?")
def log_entry_keys(body):
"""Timestamp + affected-files pairs, used to spot a change logged twice."""
keys, stamp = set(), None
for line in body.split("\n"):
m = LOG_HEADER_RE.match(line.strip())
if m:
stamp = m.group(1)
continue
f = re.match(r"^-\s*\*\*File Affected:?\*\*:?\s*(.+)$", line.strip())
if f and stamp:
keys.add(f"{stamp} {f.group(1).strip()}")
return keys
def check_sources(today):
"""source.yaml validity plus overdue-index reporting for connector libs."""
findings, connectors = [], []
if not LIBS.is_dir():
return findings, connectors
for lib in sorted(p for p in LIBS.iterdir() if p.is_dir()):
source = lib / "source.yaml"
if not source.is_file():
continue # git-copy lib — never touched by lint
connectors.append(lib)
cfg = parse_simple_yaml(source.read_text(encoding="utf-8"))
name = lib.name
for key in ("connector", "location"):
if not str(cfg.get(key, "")).strip():
findings.append(f"libs/{name}/source.yaml: `{key}` is missing or empty")
index_block = cfg.get("index")
if isinstance(index_block, dict):
for key in ("store", "location"):
if not str(index_block.get(key, "")).strip():
findings.append(f"libs/{name}/source.yaml: `index.{key}` is missing or empty")
refresh = cfg.get("refresh_interval_days")
interval = DEFAULT_REFRESH_DAYS
if refresh is not None and not isinstance(refresh, dict):
parsed = as_int(refresh)
if parsed is None or parsed <= 0:
findings.append(f"libs/{name}/source.yaml: `refresh_interval_days` is not a positive integer: {refresh!r}")
else:
interval = parsed
generated = {"index.md", "log.md", "entities", "graph", "source.yaml", "source.local.yaml"}
stray = [
p.name
for p in sorted(lib.iterdir())
if p.name not in generated and not p.name.startswith(".")
]
if stray:
findings.append(
f"libs/{name}/: has both a source.yaml and non-index content ({', '.join(stray)}) — "
"ambiguous configuration for the user to resolve"
)
newest = None
for md in lib.rglob("*.md"):
fm, _, err = parse_frontmatter(md.read_text(encoding="utf-8", errors="replace"))
if err:
continue
updated = as_date(fm.get("last_updated"))
if updated and (newest is None or updated > newest):
newest = updated
if newest is None:
findings.append(f"libs/{name}/: no generated index yet — run 'index external sources'")
else:
age = (today - newest).days
if age > interval:
findings.append(
f"libs/{name}/: index is {age}d old against a {interval}d refresh interval "
f"({age - interval}d overdue) — suggest 'index external sources'"
)
return findings, connectors
# --------------------------------------------------------------------------
# driver
# --------------------------------------------------------------------------
def sections(body):
"""Split a page body into {heading: text} for `##`-level headings."""
out, current, buf = {}, None, []
for line in body.split("\n"):
match = re.match(r"^##\s+(.+?)\s*$", line)
if match:
if current is not None:
out[current] = "\n".join(buf)
current, buf = match.group(1).strip(), []
elif current is not None:
buf.append(line)
if current is not None:
out[current] = "\n".join(buf)
return out
def digest(path):
"""First 8 hex chars of the file's sha256, or None if unreadable."""
try:
return hashlib.sha256(path.read_bytes()).hexdigest()[:8]
except OSError:
return None
def source_bullets(body):
"""[(cited path, kind, value)] from a page's `## Sources` section."""
section = sections(body).get("Sources")
if section is None:
return None
out = []
for cited, rest in SOURCE_BULLET_RE.findall(section):
fp = FINGERPRINT_RE.search(rest)
out.append((cited.strip(), fp.group(1) if fp else None, fp.group(2) if fp else None))
return out
def check_evidence(docs, root):
"""Check 12 — `## Sources` fingerprints against the files they cite."""
findings = []
for doc in docs:
if doc.reserved or doc.fm_error:
continue
bullets = source_bullets(doc.body)
if bullets is None:
continue
if not bullets:
findings.append(f"{doc.rel}: `## Sources` section is empty")
continue
for cited, kind, value in bullets:
if re.match(r"^[a-z][a-z0-9+.-]*://", cited):
continue # a URL: nothing local to hash
target = (REPO_ROOT / cited.lstrip("/")).resolve()
if not target.is_file():
target = (root / cited.lstrip("/")).resolve()
if not target.is_file():
findings.append(f"{doc.rel}: cited source `{cited}` does not exist")
continue
if kind is None:
findings.append(f"{doc.rel}: cited source `{cited}` has no fingerprint recorded")
continue
if kind != "sha256":
continue # etag/mtime come from a connector; nothing local to recompute
actual = digest(target)
if actual is None:
findings.append(f"{doc.rel}: cited source `{cited}` could not be read")
elif not value.lower().startswith(actual):
findings.append(
f"{doc.rel}: source `{cited}` CHANGED since this page was built "
f"(recorded sha256:{value}, now sha256:{actual})"
)
return findings
def check_crux(docs, root):
"""Check 13 — `## Crux` quotes are verbatim, attributed, and non-empty."""
findings = []
for doc in docs:
if doc.reserved or doc.fm_error:
continue
secs = sections(doc.body)
crux = secs.get("Crux")
if crux is None:
continue
quotes = [line[1:].strip() for line in crux.split("\n") if line.startswith(">")]
quoted = " ".join(q for q in quotes if q)
if not quoted:
findings.append(f"{doc.rel}: `## Crux` has no quoted lines (evidence sections must quote, not paraphrase)")
continue
if "Sources" not in secs:
findings.append(f"{doc.rel}: `## Crux` present but the page has no `## Sources` to attribute it to")
cited = ATTRIBUTION_RE.findall(crux)
if not cited:
findings.append(f"{doc.rel}: `## Crux` quote is not attributed to a source")
continue
for ref in cited:
ref = ref.strip().lstrip("/")
target = (REPO_ROOT / ref).resolve()
if not target.is_file():
target = (root / ref).resolve()
if not target.is_file() or target.suffix.lower() not in QUOTABLE_SUFFIXES:
continue
try:
haystack = " ".join(target.read_text(encoding="utf-8", errors="replace").split())
except OSError:
continue
for quote in quotes:
if not quote or len(quote) < 24:
continue # too short to match meaningfully
needle = " ".join(quote.split())
if needle not in haystack:
findings.append(
f"{doc.rel}: `## Crux` quote not found verbatim in `{ref}` — "
f"{needle[:60]!r}..."
)
return findings
CHECKS = [
("1 conformance", "conformance"),
("2 freshness", "freshness"),
("3 confidence", "confidence"),
("4 retention", "retention"),
("5 decisions", "decisions"),
("6 orphans", "orphans"),
("7 graph", "graph"),
("8 index/log", "index_log"),
("12 source fingerprints", "evidence"),
("13 crux", "crux"),
]
# The subset worth running at session start (Rule E): cheap, and each finding
# means something has actually changed rather than merely aged on a calendar.
QUICK_CHECKS = [("conformance", "malformed"), ("freshness", "past freshness window"),
("evidence", "source drift"), ("crux", "crux quotes diverged")]
def run_tree(root, today, label):
docs = read_tree(root)
return {
"label": label,
"documents": len(docs),
"conformance": check_conformance(docs),
"freshness": check_freshness(docs, today),
"confidence": check_confidence(docs),
"retention": check_retention(docs, today),
"decisions": check_decisions(docs, root, today),
"orphans": check_orphans(docs),
"graph": check_graph(root, docs),
"index_log": check_index_and_logs(root, docs),
"evidence": check_evidence(docs, root),
"crux": check_crux(docs, root),
"hubs": in_degree(root, docs),
}
def in_degree(root, docs):
"""Top pages by inbound edge count — not a finding, a retrieval signal.
`ckb-retrieve` fuses in-degree as one ranked list among several; surfacing
it here means the same number comes from one place rather than being
recomputed by eye at query time.
"""
edges_path = root / "graph" / "edges.json"
if not edges_path.is_file():
return []
try:
data = json.loads(edges_path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
return []
edges = data.get("edges", data) if isinstance(data, dict) else data
if not isinstance(edges, list):
return []
counts = {}
for edge in edges:
if not isinstance(edge, dict):
continue
target = str(edge.get("to", "")).strip()
if target:
counts[target] = counts.get(target, 0) + 1
return sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[:10]
def main():
parser = argparse.ArgumentParser(description="Read-only mechanical checks for ckb-lint.")
parser.add_argument("--scope", choices=("wiki", "libs", "all"), default="all")
parser.add_argument("--today", help="override today's date (YYYY-MM-DD) for reproducible runs")
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON instead of text")
parser.add_argument("--quick", action="store_true",
help="one-line session-start summary (Rule E): conformance, freshness, and source drift only")
args = parser.parse_args()
today = as_date(args.today) if args.today else date.today()
if today is None:
print(f"error: --today is not a YYYY-MM-DD date: {args.today!r}", file=sys.stderr)
return 2
trees, source_findings = [], []
if args.scope in ("wiki", "all"):
if not WIKI.is_dir():
print(f"error: no wiki/ directory under {REPO_ROOT} — run from the repository root", file=sys.stderr)
return 2
trees.append(run_tree(WIKI, today, "wiki/"))
if args.scope in ("libs", "all"):
source_findings, connectors = check_sources(today)
for lib in connectors:
access = "read-only"
local = lib / "source.local.yaml"
if local.is_file():
cfg = parse_simple_yaml(local.read_text(encoding="utf-8"))
if str(cfg.get("access", "")).strip().lower() == "write":
access = "write"
tree = run_tree(lib, today, f"libs/{lib.name}/ (access: {access})")
tree["access"] = access
trees.append(tree)
total = sum(len(t[key]) for t in trees for _, key in CHECKS) + len(source_findings)
if args.quick:
counts = {key: sum(len(t[key]) for t in trees) for key, _ in QUICK_CHECKS}
flagged = [f"{counts[key]} {label}" for key, label in QUICK_CHECKS if counts[key]]
if not flagged:
print("ckb check: clean")
return 0
print("ckb check: " + ", ".join(flagged) + " — run \"lint\" for detail")
return 1
if args.json:
print(json.dumps({"today": today.isoformat(), "trees": trees, "sources": source_findings, "findings": total}, indent=2))
return 1 if total else 0
for tree in trees:
print(f"=== {tree['label']}{tree['documents']} markdown file(s)")
for title, key in CHECKS:
items = tree[key]
if not items:
continue
print(f" [{title}] {len(items)} finding(s)")
for item in items:
print(f" - {item}")
if not any(tree[key] for _, key in CHECKS):
print(" clean")
if tree["hubs"]:
top = ", ".join(f"{name} ({n})" for name, n in tree["hubs"][:5])
print(f" [in-degree] most-referenced pages: {top}")
if args.scope in ("libs", "all"):
print("=== external sources (libs/*/source.yaml)")
if source_findings:
for item in source_findings:
print(f" - {item}")
else:
print(" clean")
print(f"total findings: {total}")
print("checks NOT covered here (model's job): whether two pages genuinely supersede "
"each other, 9 error-book, auto-fix vs. report")
print("note: a changed source fingerprint (check 12) or a diverged crux quote (check 13) "
"means the evidence moved, not that a page merely aged — read those first")
return 1 if total else 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,178 @@
---
name: ckb-module
description: Install, list, or uninstall an optional Cascade KB module from .agents/modules/<name>/ — copying its skills into .agents/skills/, creating its scaffold directories, appending its routing block to AGENTS.md and its rules to .gitignore, and recording it in the root ckb.yaml manifest. Use when the user says "install the software module", "add the <name> module", "what modules are available", "which modules are installed", "uninstall the <name> module", or asks to make this KB handle source code / a capability the base KB doesn't have. Distinct from `ckb-init` (bootstraps a whole new KB) and `ckb-upgrade` (catches the template layer up with upstream); this skill toggles optional capabilities within an existing KB.
---
# Module skill
## Purpose
The base KB ships every skill always-loaded. Some capabilities are only useful to
*some* knowledge bases, and their skill descriptions would otherwise sit in
context for every KB that will never need them.
Optional capabilities therefore live in `.agents/modules/<name>/` — present in the
repo, inert until installed. This skill is the install/uninstall path. See
[D-0001](/wiki/decisions/0001-opt-in-file-based-kb-modules.md) for why this is
files-in-the-repo rather than a native agent plugin: a module is a property of the
knowledge base, so it must travel with a clone and work for any agent that reads
`AGENTS.md`.
## Trigger phrases
- "install the `<name>` module" / "add the `<name>` module" / "enable `<name>`"
- "what modules are available" / "which modules are installed"
- "uninstall the `<name>` module" / "remove `<name>`" / "disable `<name>`"
- "make this KB handle source code" (→ offer the `software` module)
## The manifest
Installed modules are recorded in `ckb.yaml` at the repo root:
```yaml
template: # written by ckb-init, read and updated by ckb-upgrade
repo: https://git.wierzbowa.cloud/michal/ckb.git
branch: main # main | test | experimental, or any branch of a fork
kb_modules:
- name: software
version: 1.0.0
installed_on: 2026-09-20
config:
src_repos: []
```
`ckb.yaml` holds repo-level configuration — what's installed, what it's
configured with. It is **not** the wiki's content contract: `kb_schema_version`
stays in `wiki/index.md` where it has always been. Two different things, two
different homes.
The `template:` block records which upstream repo and **branch** this KB takes
its tooling from. It belongs to `ckb-init` (writes it) and `ckb-upgrade` (reads
it as the default source, updates it when the user switches channel). This
skill must **never** drop or rewrite it: a module install that silently reset a
KB from the `experimental` channel back to `main` would change what its next
upgrade pulls, which is not a module's business. Read `ckb.yaml`, edit the
`kb_modules:` list, write it back with everything else intact.
If `ckb.yaml` doesn't exist, no modules are installed and no template source is
recorded; create it on first install, carrying forward any `template:` block
that a later `ckb-upgrade` may add.
## Installing
### Step 1 — Resolve and read the module
Find `.agents/modules/<name>/module.yaml`. If the name doesn't match, list what's
actually in `.agents/modules/` rather than guessing at a near-miss.
Read the manifest: `skills`, `scaffold`, `agents_fragment`, `gitignore_fragment`,
`schema_additions`, `depends_on`, `requires_kb_schema`.
### Step 2 — Check preconditions, and report them before writing anything
- **Already installed?** If `ckb.yaml` lists it, say so and offer to reinstall
(refresh the files) or stop. Don't silently re-copy.
- **Schema.** If `requires_kb_schema` is above `wiki/index.md`'s
`kb_schema_version`, the bump is part of this install (Step 4) — additive only.
If the module needs a *major* version above the KB's, stop and hand it to
`ckb-upgrade`.
- **Dependencies.** Run each `check`. A failing **optional** dependency is a
warning, not a blocker — report it plainly and continue. A failing required one
stops the install.
- **Collisions.** If a skill name in `skills:` already exists in
`.agents/skills/`, or a scaffold path exists with unrelated content, stop and
ask. Never overwrite a skill the user may have edited.
Report all findings **before** the first write, then proceed (or ask, if anything
needs a decision). A half-installed module is worse than an uninstalled one.
### Step 3 — Copy the payload
```bash
cp -r .agents/modules/<name>/skills/<skill> .agents/skills/<skill>
```
`.claude/skills` is a symlink to `.agents/skills` (git mode `120000`), so the
skills appear under `.claude/` automatically. **Do not create a second copy
there** — that symlink is a deliberate choice, and duplicating it creates two
things to keep in sync.
Create each `scaffold` path, copying any `from` template to its `as` destination.
Never overwrite an existing scaffold file — skip it and note the skip.
### Step 4 — Patch the shared files
Both patches are wrapped in markers so uninstall is exact:
```
<!-- ckb-module:<name> START -->
...fragment...
<!-- ckb-module:<name> END -->
```
- **`AGENTS.md`** — append `agents_fragment` at the end of the file, inside
markers. `CLAUDE.md` is a symlink to `AGENTS.md`, so it updates for free;
never write `CLAUDE.md` directly.
- **`.gitignore`** — append `gitignore_fragment`, inside markers (`#`-commented).
- **`wiki/index.md`** — bump `kb_schema_version` to `requires_kb_schema` if the
module's additions raise it. Additive only, per `ckb-upgrade`'s versioning
policy.
### Step 5 — Record, log, report
- Add the module to `ckb.yaml` with its version and today's date.
- Log to `wiki/log.md` per Rule B **only** for the `wiki/index.md` change — the
rest is template layer, not wiki content.
- Append to `workload/YYYY-MM-DD_summary.md` per Rule D.
- Report: skills now available and what they do, directories created, files
patched, dependency warnings, schema bump if any. Then the standard reminder —
on disk, not committed.
Offer to record a decision (`ckb-decide`) if installing the module was a real
choice for this KB rather than following a prior one.
## Uninstalling
Reverse Step 3 and 4, in this order, and be conservative about data:
1. Remove `.agents/skills/<skill>` for each skill the manifest lists — **but
first diff it against `.agents/modules/<name>/skills/<skill>`.** If the
installed copy was edited, show the diff and ask before deleting. Offer to
copy the edits back into the module folder so they survive.
2. Strip the marked blocks from `AGENTS.md` and `.gitignore`.
3. Remove the module's entry from `ckb.yaml`.
**Never remove scaffold directories or any content under them.** `src/` may hold
the user's clones, `openspec/` their specs. Say explicitly what was left behind
and why, so nothing looks like an oversight.
**Never remove wiki pages the module's skills created**, and never lower
`kb_schema_version`. Those pages are knowledge; the module produced them but does
not own them, and they must stay readable without it. Say which page types
(`repository`, `component`, `spec`) will now have no skill maintaining them.
## Listing
On "what modules are available": list `.agents/modules/*/module.yaml` with each
`title` and `summary`, marking which are installed per `ckb.yaml`. Keep it short
— name, one line, installed or not.
## Edge cases
- **A module directory with no `module.yaml`** — not a module. Report it as
malformed; don't try to infer its contents from its file layout.
- **The user asks for a capability no module provides** — say so, and don't
install a near-match hoping it fits. Offer to note it in `wiki/query-gaps.md`.
- **`ckb-upgrade` brought a newer version of an installed module** — the installed
skill copies are stale. Offer to reinstall; show what changed before doing it.
- **Markers missing at uninstall** (someone hand-edited `AGENTS.md`) — don't
guess at the block's boundaries. Show the fragment and ask the user to remove
it, or point at exactly what to delete.
- **A module's skill was renamed upstream** — treat as collision (Step 2), ask.
Don't delete the old name automatically.
---
*Licensed under the Apache License, Version 2.0 — see [LICENSE](../../../LICENSE)
at the repository root, or <http://www.apache.org/licenses/LICENSE-2.0>.*

View file

@ -1,5 +1,5 @@
---
name: cbk-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,201 @@
---
name: ckb-reset
description: DESTRUCTIVE — empty this knowledge base back to a clean template, deleting every accumulated wiki page, decision record, graph edge, log entry, workload summary and (on request) raw/outputs material, then restoring the empty scaffold that `ckb-init` would create. Use when the user says "reset the wiki", "empty the wiki", "clean this KB out", "make this a clean template", "wipe the knowledge base", or wants to turn a working KB back into a distributable template. Always inventories what would be destroyed and takes explicit confirmation before deleting anything. Never touches the template layer (AGENTS.md, skills, modules, docs) or `src/`.
---
# Reset skill
## Purpose
A KB that also serves as the template for other KBs accumulates content that
shouldn't ship with it — decisions made about this project, workload summaries,
entity pages, graph edges. This skill returns it to the shape `ckb-init` creates:
same structure, same schema, no knowledge.
It is the only skill in this KB that deletes knowledge on purpose. Everything
below is written on the assumption that **getting this wrong destroys work that
took a long time to accumulate**, and that "the user asked for it" is not enough
on its own — people ask for this in the wrong repo.
## Trigger phrases
- "reset the wiki" / "empty the wiki" / "wipe the knowledge base"
- "clean this KB out" / "make this a clean template"
- "turn this back into a template" / "strip the content but keep the structure"
## Non-negotiable safety rules
1. **Never delete without an inventory shown first and an explicit confirmation
after it.** Not "proceed?" before you know the scale — the count is the thing
that makes the answer meaningful.
2. **Never touch the template layer.** `AGENTS.md`, `CLAUDE.md`, `.agents/`,
`.claude/`, `LICENSE`, `VERSION`, `README*`, `MANUAL*`, `OPENSPEC*`,
`.gitignore`, `ckb.yaml`, `.git/`. Those *are* the template.
3. **Never touch `src/`.** Those are independent repositories holding code this
skill has no business deleting. Not even with confirmation — tell the user to
delete them with `rm -rf` themselves if that's really what they want.
4. **Never run when the working tree is dirty**, unless the user overrides after
being told why. Uncommitted work is unrecoverable once deleted; committed work
is always recoverable from git.
5. **Never guess the scope.** If the user says "reset" without saying how far,
ask — don't assume the widest or the narrowest reading.
## How to run this skill
### Step 0 — Establish that git can undo this
Run `git status --short` and `git log --oneline -1`.
- **Dirty tree** — stop and report exactly what's uncommitted. Offer to commit
first ("Let me commit this as a restore point, then reset"), and only proceed
without committing if the user explicitly says the uncommitted work is
disposable. This is the single highest-value check in the skill: after a reset,
committed content is a `git checkout` away and uncommitted content is gone.
- **Not a git repository at all** — say so plainly and require a much more
explicit confirmation, because nothing here is recoverable. Recommend the user
copy the folder somewhere safe first.
- **Clean tree** — note the commit hash and tell the user it's the restore point.
Offer to tag it (`git tag pre-reset-<date>`), since a hash in a chat log is
easy to lose.
### Step 1 — Agree the scope
Present the tiers and let the user choose. Default to **Tier 1 only** if they
just said "reset the wiki" — the narrowest reading that satisfies the request.
| Tier | Contents | Default |
|---|---|---|
| **1 — Wiki knowledge** | Every page under `wiki/entities/`, `wiki/projects/`, `wiki/decisions/` (the `NNNN-*.md` records), `wiki/graph/edges.json` entries, and all log/overview/query-gap/error-book *entries* | **Yes** |
| **2 — Workload history** | Every `workload/*_summary.md` | Ask |
| **3 — Source material** | `raw/inbox/*`, `raw/archive/*` | Ask — this is the user's original material and may exist nowhere else |
| **4 — Generated artifacts** | `outputs/*` | Ask — cheap to regenerate, usually safe |
| **5 — External sources** | `libs/*`, `linked/*` | Ask — **default no.** `linked/` holds symlinks to *other people's* KBs, and deleting a symlink target by accident is a real risk |
| **6 — Installed modules** | `ckb.yaml` + module-installed skills + the marked blocks in `AGENTS.md`/`.gitignore` | Ask — **default no.** That's `ckb-module uninstall`'s job, not this skill's |
For a template being prepared for distribution, tiers 14 are the usual answer.
**Say what each tier means in files, not just in names.** "Tier 3 deletes 48
files in `raw/archive/` including the original meeting recordings" is a sentence
that changes answers.
### Step 2 — Build and show the inventory
Count, don't estimate. For the chosen tiers:
```bash
find wiki/entities wiki/projects -name '*.md' ! -name 'index.md' | wc -l
ls wiki/decisions/[0-9]*.md 2>/dev/null | wc -l
python3 -c "import json;print(len(json.load(open('wiki/graph/edges.json'))['edges']))"
ls workload/*_summary.md 2>/dev/null | wc -l
find raw/inbox raw/archive -type f 2>/dev/null | wc -l
```
Present it as a table: what, how many, and — for anything irreplaceable — where
it came from. Call out explicitly:
- **Decision records**, by number and title. These are append-only by design and
represent reasoning that cannot be reconstructed; losing them is the most
expensive part of any reset.
- **`raw/archive/` material with no upstream**, if tier 3 is selected.
- **Anything with `retention: high`** in its frontmatter — the wiki's own signal
that it was meant to be kept.
Then state what will *survive*, in one line. That's as reassuring as the deletion
list is alarming, and it's what tells the user whether you understood the task.
### Step 3 — Take the confirmation
Ask for a typed phrase, not a yes:
> This will permanently delete **47 wiki pages, 12 decision records, 9 workload
> summaries and 128 graph edges**. Restore point: `491ca8a` (clean tree).
> Type **`reset the wiki`** to proceed, or anything else to cancel.
Accept only that phrase. A "yes", a "go ahead", or silence is **not** consent
here — the typed phrase exists precisely so the action can't be taken by reflex
or by an agent misreading agreement to something else in the conversation.
If the user modifies the scope in their reply, go back to Step 2 and re-inventory.
Never carry a confirmation across a scope change: they confirmed a number, and
the number moved.
### Step 4 — Delete, then restore the scaffold
Delete the agreed tiers. Then restore the empty scaffold **exactly as `ckb-init`
Step 8 defines it** — that skill is the single source of truth for what a clean
`wiki/` looks like, so read it rather than reconstructing the files from memory:
- `wiki/index.md` — routing table, infrastructure rows only, no entity rows.
**Keep `kb_schema_version` at its current value.** A reset empties content; it
does not roll back the schema contract.
- `wiki/overview.md` — the generic version, `last_updated` today.
- `wiki/log.md`, `wiki/decisions/log.md` — headers and explanation, no entries.
- `wiki/error-book.md`, `wiki/query-gaps.md` — headers, no entries.
- `wiki/entities/index.md`, `wiki/projects/index.md` — headers plus
placeholder notes.
- `wiki/graph/index.md` — header, placeholder, and the edge-vocabulary table
and conventions **verbatim**; that table is the contract every writing
skill validates against, not accumulated content, so a reset keeps it.
- `wiki/decisions/index.md` — header, placeholder, and the status-vocabulary
table **verbatim** (`ckb-decide` and `ckb-lint` both validate against it).
- `wiki/graph/edges.json``{"version": 1, "last_updated": "<today>", "edges": []}`.
- `.gitadd` placeholder files in any directory that would otherwise be empty, so
the structure survives in git.
Preserve directories even when empty. A reset KB must have the same shape as a
fresh one, or the next ingest starts by rebuilding scaffolding.
### Step 5 — Verify before reporting success
Do not report completion until you've checked it:
```bash
ls wiki/entities/ wiki/decisions/ wiki/projects/
python3 .agents/skills/ckb-lint/scripts/lint_report.py
git status --short
```
Lint should come back clean on a fresh scaffold. If it doesn't, the scaffold is
wrong — say so and fix it rather than declaring done. A reset that leaves a
malformed wiki is worse than no reset, because the damage is already unrecoverable
and now the structure is broken too.
### Step 6 — Report
State: tiers reset, counts deleted per tier, scaffold files restored, the restore
point hash (and tag, if one was made), lint result, and what was deliberately left
alone (`src/`, modules, template layer).
**Do not append a workload summary.** Rule D's session note would be the first
entry in a workload directory this skill just emptied, which defeats the purpose.
Say so in the report instead — this is a deliberate, one-off exception to Rule D,
and worth naming so it doesn't read as an oversight.
Finish with: the reset is on disk but not committed. `git checkout .` still undoes
everything until it is. That sentence is the last safety net, so don't omit it.
## Edge cases
- **Already clean** — say so and change nothing. Don't rewrite identical scaffold
files to look busy.
- **Run in a KB that is not a template** — this skill can't tell the difference,
so if the wiki holds substantial content (say, 50+ pages or any decision
records), name that in the confirmation: "This KB has 12 decision records —
templates don't usually have those. Are you in the right repository?" Ask once;
don't refuse if they confirm.
- **`linked/` symlinks** — never follow them when deleting. `rm -rf linked/foo`
where `foo` is a symlink to another KB is catastrophic and silent. Remove the
*link*, never its target, and prefer leaving tier 5 alone.
- **Partial failure mid-delete** — stop, report exactly what was and wasn't
deleted, and point at the restore point. Don't continue on the theory that
finishing is tidier.
- **User asks to reset "everything including the skills"** — that's not a reset,
it's deleting the KB. Say so and point at `ckb-init` for a fresh one elsewhere.
- **Module installed** — module-created page types (`repository`, `component`,
`spec`) are wiki content and reset with tier 1. The module itself stays
installed unless tier 6 was chosen.
---
*Licensed under the Apache License, Version 2.0 — see [LICENSE](../../../LICENSE)
at the repository root, or <http://www.apache.org/licenses/LICENSE-2.0>.*

View file

@ -0,0 +1,336 @@
---
name: ckb-retrieve
description: Governs how a question actually gets answered from the knowledge base. An index entry — a wiki page's `tldr`, a connector-index document/entity page — is deliberately compressed so lazy-loading stays cheap; that compression also means it can be incomplete, paraphrased loosely, or stale relative to the real source. This skill makes "go verify against the source before answering" a standing step, not an optional last resort: every time a page surfaced by the index looks relevant enough to actually ground part of the answer, follow it to its underlying source material — a wiki page's own `## Sources` citations into `raw/archive/`/`outputs/`, or a connector-index page's `resource:` pointer back to the live connector item — before treating its content as settled. Use whenever answering a question or researching a topic from `wiki/`, `linked/<name>/`, or a connector-backed `libs/<name>/` — this is the retrieval workflow routed by CLAUDE.md/AGENTS.md, always in play, not something the user needs to name explicitly. Distinct from `ckb-onboard-me` (produces a reading-order tour, not an answer to a specific question) and from the write-side skills `ckb-ingest`/`ckb-index-external`/`ckb-lint` (build or repair the index; this skill only ever reads it, plus the sources behind it).
---
# Retrieve (source-verified query) skill
## Purpose
The whole point of `tldr`/lazy-loading (CLAUDE.md/AGENTS.md index-first
navigation and skill routing) is that most of the wiki never has to enter
context — a one-sentence summary decides
whether a page is worth opening at all. That's the right trade for
*deciding relevance*. It's the wrong trade for *grounding an answer*: a
`tldr` is a compression of whatever the page's author judged important at
write time, a connector-index entity page is (per `ckb-index-external`)
deliberately a thin pointer rather than a synthesis, and either can have
drifted from the primary material since — a source updated, a nuance
dropped, a paraphrase that's subtly wrong.
This skill closes that gap: once a page looks relevant enough to actually
use in an answer, don't stop at its `tldr` or even its full synthesized
body — follow it to the source material that page was built from, and
answer from there. It's the difference between citing what the index
*says about* the evidence and citing the evidence.
This skill is **read-only**. It never writes to `wiki/`, `libs/<name>/`,
or anywhere else — it only reads what already exists (the index, and the
source material behind it) to answer the question in front of it. If
verification surfaces a real gap or contradiction worth fixing in the
wiki, say so and suggest `ckb-lint`/`ckb-ingest` rather than editing
anything mid-answer.
## Trigger phrases
This is the default retrieval path for **any** question answered from the
knowledge base — the user does not need to name this skill. Typical
prompts that should route here:
- A direct question answerable from the wiki ("what do we know about X",
"what's the status of Y", "who owns Z").
- "Look up X" / "check the wiki for X" / "search for X".
- Mid-conversation moments where CLAUDE.md/AGENTS.md routes a KB question
to retrieval — this skill *is* that workflow's implementation.
Do **not** use this skill for:
- Building or refreshing an index in the first place — that's
`ckb-ingest` (raw material → `wiki/`) or `ckb-index-external`
(connector → `libs/<name>/`). This skill only ever consumes what those
produced.
- A guided reading order across many pages on a topic — that's
`ckb-onboard-me`. This skill answers one question with verified
evidence; it doesn't produce a curriculum.
- Health-checking the index's own structure (frontmatter, staleness,
broken links) — that's `ckb-lint`. Verification failures found here
(Edge cases, below) are worth mentioning to the user as a possible lint
finding, but this skill doesn't run the lint checklist itself.
## How to run this skill
### Step 1 — Read the index
Same cascade order as CLAUDE.md/AGENTS.md, first match wins:
1. `wiki/index.md` — match the question against the **Use when** column.
2. `wiki/projects/index.md` — if a project scope matches the question,
use that project's listed pages, entity pages, raw/archive sources,
connector-backed libs, outputs, and graph areas as the first search
boundary. A project scope narrows the first pass only; it never hides
the rest of the cascade.
3. `wiki/entities/index.md` — match against entity titles/`tldr`.
4. `wiki/decisions/index.md` — for any "why is it like this", "who decided",
"when did we choose", or "is that still current" question. If decisions
*are* the subject of the question, hand off to `ckb-decide`, which owns
the decisions-scoped path including supersession chains; pull decision
pages in here as one source among many when they're only part of a wider
answer. Either way, a superseded decision is history, not noise — say so
rather than silently answering from the current one.
5. If nothing local matches: each `linked/<name>/` index (alphabetical),
then each connector-backed `libs/<name>/entities/index.md` — for a
connector-backed lib this means its *generated* index (both the
Documents and the Entities & Processes sections `ckb-index-external`
produces), never the live source directly. If it isn't built yet,
suggest "index external sources" rather than querying the live
connector ad hoc from inside this skill.
### Step 2 — Shortlist every page that looks relevant
Read the `tldr` of every page the index match surfaced — don't stop at
the first plausible hit. A question is often best answered by
triangulating two or three pages (e.g. a concept page plus the specific
person/decision page that qualifies it), and a page whose `tldr` looks
only tangential can still be hiding the exact fact needed in its body or
its Sources. Keep the shortlist to what's plausibly relevant; this isn't
"open everything," it's "don't stop at one."
If index/TLDR matching is not enough, run a local hybrid pass before
giving up: use `rg` for exact tokens across `wiki/`, `raw/archive/`,
`outputs/`, `raw/inbox/`, and readable upstream indexes; combine those
hits with project-scope matches, entity/title/TLDR matches, graph
proximity, freshness, confidence, and quality. Prefer exact text matches
for error strings, commands, flags, filenames, hostnames, IDs, and other
literals; prefer entity/semantic matches for paraphrased questions.
Sweep `raw/inbox/` even though nothing there has been ingested yet.
Material dropped an hour ago can already hold the answer, and finding it
there is also the clearest possible signal that an `ckb-ingest` run is
overdue — mention that. Say plainly when an answer rests on un-ingested
inbox material rather than on a synthesized page.
Keep each signal's hits as its own ranked list rather than merging them by
eye as you go. Step 4 needs the separate orderings.
### Step 3 — Walk the graph for anything the shortlist missed
If a shortlisted page has edges in `wiki/graph/edges.json` (or a
connector's own `graph/edges.json`), follow `depends_on`/`uses`/`caused`
edges one hop out to catch a connected page the index text match alone
wouldn't have surfaced.
For "who knows about X" and "who owns X" questions — both advertised in
the trigger phrases above — the graph is the primary lookup rather than a
fallback. Read the `has_expertise_in` and `owns` edges pointing at the
topic entity and answer from the people or teams on the other end,
ordered by how many distinct sources evidence each edge. Where no such
edge exists yet, fall back to authorship evidence: who the `## Sources`
material actually shows answering questions on that topic. Say which of
the two grounded the answer, since an inferred expert is a weaker claim
than a recorded one.
While the edge file is open, build one more ranked list for Step 4:
**in-degree** — for each candidate already on the shortlist, how many edges
point *at* it.
```bash
python3 -c "import json,collections;e=json.load(open('wiki/graph/edges.json'))['edges'];\
c=collections.Counter(x['to'] for x in e);print(c.most_common(20))"
```
Centrality is a genuinely different signal from every other one in Step 4:
text match, freshness, confidence and quality are all properties of a page in
isolation, while in-degree is what the rest of the KB has to say about it. A
page a dozen others depend on is usually the one that settles a question;
a leaf page with an identical `tldr` usually isn't. Rank only the candidates
already shortlisted — this ranks the shortlist, it doesn't widen it. Skip the
list entirely when `edges.json` is empty or every candidate has in-degree 0,
since a list with no ordering contributes nothing to the fusion.
### Step 4 — Fuse the signals, dedupe, then rerank
Signals disagree, and none of them is trustworthy alone: an exact `rg` hit
can sit in a page that answers a different question, and a confident TLDR
match can be a loose paraphrase of something stale. Don't settle that by
picking a favourite signal — fuse the ranked lists from Step 2.
**Fuse.** For every candidate, sum `weight / (k + rank)` across each list
it appears in, where `rank` is its 1-based position in that list. Use
`k = 10` and a default `weight` of 1.0. A page ranked third in three
different lists beats one ranked first in a single list; consensus is the
whole point.
Give the in-degree list a weight *below* 1.0 (around 0.5 is a reasonable
start). Centrality is a prior, not evidence — it says a page matters
generally, not that it answers this question — so it should break ties
between comparable candidates without ever promoting a page the text signals
didn't surface.
`k = 10` is deliberately smaller than the `k = 60` rank fusion is usually
quoted with. 60 is tuned for retrievers returning hundreds of candidates,
and against the dozen-or-so a local wiki produces it flattens every score
into near-identical values. Raise a list's weight when the question
warrants it — for a pasted error string, command, flag, or ID, weight the
exact-match list around 2.0, because no amount of title/TLDR similarity
should outrank a literal match on the token the user actually pasted.
**Dedupe.** Collapse candidates carrying the *same claim* into one entry
before ranking further. Step 6 actively manufactures these: a `wiki/`
page, the `raw/archive/` file it cites, and a connector-index page whose
`resource:` points back at that same wiki page are three hits for one
fact. Keep whichever sits closest to the primary material and record the
others as corroboration, not as independent evidence. Three views of one
claim are not three sources.
**Rerank.** Score each surviving candidate 010 on how well it answers
*the literal question asked*, not on how well it matches the query's
vocabulary, then keep the best handful and drop the rest. This is the same
agent making a deliberate second pass, not a separate model. The point is
that relevance judgment happens explicitly, over the shortlist, in one
place — rather than being folded silently into how the final answer gets
drafted.
### Step 5 — Expand local context and build an evidence packet
For every result that survived Step 4, keep a small evidence packet with:
- source path or connector resource
- matched claim or short excerpt
- source date or `last_updated`
- freshness/confidence/quality signals, when available
- project-scope or graph relationship hints, when relevant
- which signals it was fused from, its rerank score, and anything it
absorbed during dedupe — this is what Step 7 draws caveats from
When a match is a section, heading, or snippet inside a larger Markdown
file, include nearby headings/paragraphs before deciding what it means.
Avoid answering from an isolated fragment when the neighboring context
changes the interpretation.
### Step 6 — Follow every page on the shortlist to its source before answering from it
This is the step this skill exists to enforce. For each page on the
shortlist that will actually ground part of the answer:
- **A `wiki/` page with a `## Crux` covering the exact claim** — the Crux
is verbatim source text, not synthesis, so it is already the evidence
this step exists to fetch. Confirm the page's recorded fingerprint still
matches the cited source (cheap: `sha256sum <source> | cut -c1-8` against
the digest in the `## Sources` bullet). If it matches, quote the Crux and
answer — one hash beats one full file read, and citing a quote is strictly
stronger than citing a summary. If it **differs**, the source moved under
the page: open the source, answer from it, and say the page's Crux is now
stale — that's a real `ckb-lint` finding, not a formality. If the page has
no fingerprint recorded at all, treat the Crux as unverified and fall
through to the full read below.
This shortcut is narrow on purpose. It applies only when the quote covers
the specific claim being used — not when it is merely on-topic, and never
as a reason to skip reading a source that would qualify or contradict it.
- **A `wiki/` page** — every page synthesized via `ckb-ingest` carries a
`## Sources` section citing the exact `raw/archive/<date>/...` or
`outputs/...` file(s) it was built from. Open the cited file(s) — or
the specific section of a large one — and confirm the wiki's claim
matches what the primary material actually says. This also often
surfaces adjacent detail the synthesis compressed away that's directly
useful for the current question.
- **A connector-index page in `libs/<name>/`** (a document page, or an
entity/process page from the `ckb-index-external` extraction) — read
its `resource:` field. If the connector is authorized this session (per
`ToolSearch`/MCP auth state), re-fetch the live item for current,
complete content rather than trusting the cached `tldr` — connector
pages use a deliberately short `freshness_window_days` (30, vs. a
typical wiki page's 60-90) precisely because external sources drift
without notice. If the connector isn't authorized, or the item is a
calendar acceptance / metadata-only page with nothing more to fetch,
say plainly that the answer relies on the cached index rather than a
live re-check, so the user can weigh that.
- **A `linked/<name>/` page** — this is already full mirrored content,
not a summary; reading the page itself already is reading the source.
No extra fetch needed unless *that* page itself cites something further
outside the mirror.
Target the specific claim, not the whole file — a large transcript or
deck doesn't need a full read every time, just enough (search for the
name/topic, read the surrounding context) to confirm the point actually
being used.
### Step 7 — Reconcile and answer
If the source confirms the index, answer normally — but note what
grounded it (e.g. "per the kickoff transcript cited in
`gt-fde-access-requirements.md`") rather than presenting the answer as if
sourced from the `tldr` alone.
If the source contradicts, extends, or is more precise than what the
index said, prefer the source for the answer and say so explicitly — this
is exactly the kind of drift `ckb-lint`'s freshness/confidence checks
exist to eventually catch, so mention it's worth a lint pass if the gap
looks like more than a one-off, but don't rewrite the wiki page yourself
mid-answer unless the user asks for that separately.
State caveats in the answer itself, not only in the metadata you read to
build it. When a page grounding the answer is past its
`freshness_window_days`, carries a low `confidence` or `quality`, rests on
un-ingested `raw/inbox/` material, or was checked against a cached
connector index rather than a live re-fetch, say so in a short clause next
to the claim it qualifies. Surface a conflict between two live pages the
same way, even when neither is marked `superseded_by` yet. The metadata
already exists and Step 4 already put it in front of you; the failure mode
is answering confidently *from* a stale or contested page without passing
that on, which leaves the reader no way to weigh it.
If no page anywhere is even plausibly relevant, say so — don't verify
against a source that has nothing to do with the question just to appear
thorough, and don't fabricate an answer either. For durable missing
knowledge, add or propose a short `wiki/query-gaps.md` entry with the
question, date, search areas tried, and the smallest missing source/page
that would close the gap. If you edit `wiki/query-gaps.md`, update
`wiki/log.md` immediately.
## Edge cases
- **Only one signal returned anything** — fusion is a no-op, so don't run
the arithmetic for show. The rerank still applies (it's the check that
the one list actually answers the question rather than merely matching
its wording), and dedupe still applies as soon as Step 6 pulls in a
page's own cited source.
- **A cited source file no longer exists** (moved, renamed, or cleaned
up) — say so plainly rather than silently falling back to the `tldr` as
if it had been verified. Flag it as a likely `ckb-lint` finding (a
broken `Sources` reference) rather than treating it as a dead end.
- **A page's `## Crux` quote is no longer found in the source it cites**
the fingerprint check will usually catch this first, but where it doesn't
(no fingerprint recorded, or a source edited without the page being
rebuilt), answer from the source and report the divergence. A quote that
has drifted from its source is a stronger lint finding than a stale date,
because it is a page asserting something the evidence no longer says.
- **Connector item unreachable or connector not authorized this
session** — state clearly that the answer is based on the cached
connector index, not a live re-check, and name which connector would
need authorizing to go further.
- **Question is low-stakes and the matched page is high-confidence,
recently updated, and well within its `freshness_window_days`** — still
open the source at least once to ground the answer; skip only a *second*
redundant read if the same source has already been opened earlier in
the same retrieval pass for another part of the same question.
- **Many pages are relevant and reading every cited source in full would
be excessive** — prioritize the sources that actually carry the
specific fact the question needs, not every source any shortlisted page
has ever cited. Say what was skipped rather than silently narrowing
without a note.
- **The "source" is itself a generated artifact** (e.g. an
`outputs/emails/*.md` draft cited as a wiki page's Source) rather than
raw external material — that's still the source relative to the wiki
page; no further hop beyond it is required.
- **A connector-index entity/process page's `resource:` points at a full
`wiki/entities/` page** (per `ckb-index-external`'s thin-page
convention) — treat the wiki page as the real source to verify against;
the connector page is just the pointer that got you there.
- **Nothing in the index is even plausibly relevant** — say the knowledge
base has nothing on this yet, and suggest `ckb-ingest` (for new raw
material) or `ckb-index-external` (for a connector-backed source) if
that seems like the actual gap. For durable gaps, record or propose a
`wiki/query-gaps.md` entry. Don't stretch a weak match into an answer
just to have one.
---
*Licensed under the Apache License, Version 2.0 — see [LICENSE](../../../LICENSE)
at the repository root, or <http://www.apache.org/licenses/LICENSE-2.0>.*

View file

@ -31,7 +31,7 @@ Use this skill when the user says things like:
- "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
the content-level ingest workflow routed by `CLAUDE.md`/`AGENTS.md` instead
(processing `raw/inbox/` into `wiki/`), which this skill has nothing to do
with.
@ -63,6 +63,23 @@ 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 1b — Nested source repos (`software` module only)
If `ckb.yaml` lists the `software` module and `src/` holds clones, those are
**independent repositories with their own remotes**. This skill syncs *this*
repo only.
- Never `git add` anything under `src/`, and never `git commit`, `git push`,
or `git checkout` inside one. They are gitignored; if `git status` shows
something there, treat it as a `.gitignore` defect to report, not content
to stage.
- Do report each clone's state — current branch, dirty or clean, ahead or
behind its own remote — so uncommitted work isn't silently forgotten while
the KB itself gets pushed. Reporting is the whole contribution here;
acting on it is the user's call, in that repo.
Skip this step entirely when the module isn't installed or `src/` is empty.
### Step 2 — Ensure `origin` is configured
```bash

View file

@ -1,6 +1,6 @@
---
name: ckb-upgrade
description: Check the canonical Cascade KB template repo (https://git.wierzbowa.cloud/michal/ckb) for a newer schema/tooling version, and if one exists, upgrade this KB's template layer (AGENTS.md/CLAUDE.md, .agents/skills/, LICENSE, VERSION, README/MANUAL docs, base .gitignore rules) in place. Also checks wiki/index.md's own kb_schema_version for compatibility (including the case of an unversioned, pre-schema wiki) independently of the template layer, and — only with explicit confirmation — can migrate existing wiki/ content (backfilling missing frontmatter, adding missing scaffold files) up to the current schema while preserving every fact already collected. Use when the user asks to "upgrade the wiki", "upgrade this KB", "check for a newer template version", "update the KB schema", "pull in the latest skills", or "is there a new version of ckb". Distinct from ckb-sync-changes (reconciles this repo's OWN origin remote with its own history) and ckb-init (bootstraps a brand-new, empty KB) — this skill updates an EXISTING, populated KB's tooling layer (and, if asked, its content's schema conformance) from a separate upstream template source.
description: Check the canonical Cascade KB template repo (https://git.wierzbowa.cloud/michal/ckb) for a newer schema/tooling version, and if one exists, upgrade this KB's template layer (AGENTS.md/CLAUDE.md, .agents/skills/, LICENSE, VERSION, README/MANUAL docs, base .gitignore rules) in place. Also checks wiki/index.md's own kb_schema_version for compatibility (including the case of an unversioned, pre-schema wiki) independently of the template layer, and — only with explicit confirmation — can migrate existing wiki/ content (backfilling missing frontmatter, adding missing scaffold files) up to the current schema while preserving every fact already collected. Use when the user asks to "upgrade the wiki", "upgrade this KB", "check for a newer template version", "update the KB schema", "pull in the latest skills", or "is there a new version of ckb". Also use when they name a release channel - "upgrade from the test branch", "check experimental for updates", "switch this KB to the stable channel" - since the template repo keeps main (stable), test (release candidate) and experimental (development) branches; the branch this KB tracks is recorded in ckb.yaml's template block and defaults to main. Distinct from ckb-sync-changes (reconciles this repo's OWN origin remote with its own history) and ckb-init (bootstraps a brand-new, empty KB) — this skill updates an EXISTING, populated KB's tooling layer (and, if asked, its content's schema conformance) from a separate upstream template source.
---
# Upgrade skill
@ -16,8 +16,8 @@ neither:
- **Template layer** — the schema/tooling that defines *how* the KB
behaves: `AGENTS.md`/`CLAUDE.md`, `.agents/skills/*`, `LICENSE`,
`VERSION`, `README.md`/`README.pl.md`/`MANUAL.md`/`MANUAL.pl.md`, and the
base `.gitignore` rules. Compared via the root `VERSION` file. Freely
`VERSION`, `README.md`/`README.pl.md`/`MANUAL.md`/`MANUAL.pl.md`/
`CHANGELOG.md`/`CHANGELOG.pl.md`, and the base `.gitignore` rules. Compared via the root `VERSION` file. Freely
upgradable — these files hold no project-specific content.
- **Content layer** — everything the KB has actually learned:
`wiki/entities/`, `wiki/graph/edges.json`, `wiki/index.md`'s entity rows,
@ -36,6 +36,29 @@ the template layer, or vice versa: a KB might already have the latest
an older (or no) schema version, or the reverse. This skill checks and
offers to fix both, separately.
## Schema versioning policy
`wiki/index.md` is the only wiki file that carries `kb_schema_version`.
Treat it as the version of the wiki content contract, not as a per-page
field.
Use schema versions as `MAJOR.MINOR`:
- Bump the **minor** version for backward-compatible additions: optional
frontmatter fields, optional reserved wiki scaffolds, new optional
subdirectories such as `wiki/projects/`, or additional optional
index/log conventions.
- Bump the **major** version for breaking changes: removing or renaming a
required field, changing the meaning of an existing required field,
removing an existing reserved filename convention, or changing cascade
lookup semantics in a way older tooling cannot safely interpret.
The root `VERSION` file tracks the template/tooling release, not just the
wiki schema. When a schema convention changes in the template repo, update
the relevant docs/skills and `VERSION` in the same template-layer change.
When migrating an existing KB's content layer, bump `wiki/index.md`'s
`kb_schema_version` only after the confirmed migration has actually
completed.
This is different from `ckb-sync-changes` (reconciles *this* repo's own
commit history with *its own* `origin` remote — same content, no template
comparison) and from `ckb-init` (bootstraps a brand-new, empty KB from the
@ -56,19 +79,54 @@ repo's own `origin`) or for setting up a brand-new KB (that's `ckb-init`).
## How to run this skill
### Step 1 — Fetch the canonical template for comparison
### Step 0 — Resolve which repo and branch to compare against
Clone the canonical repo shallowly into a scratch location under `tmp/`
never touch this repo's own git remotes or history to do this comparison:
Read `ckb.yaml` at the repo root:
```yaml
template:
repo: https://git.wierzbowa.cloud/michal/ckb.git
branch: main
```
That block records where this KB's tooling comes from (written by
`ckb-init`). Missing file, missing block, or missing field → fall back to the
canonical repo on **`main`**, which is what every KB predating this
convention was effectively tracking.
#### The three channels
| Branch | What it is | Who should be on it |
|---|---|---|
| `main` | **Stable.** The released template. | Everyone, by default. |
| `test` | **Release candidate.** Validated before merging to `main`. | Anyone helping validate a release, or needing a landed-but-unshipped fix. |
| `experimental` | **Development.** Active work; may be broken or reverted. | People developing the template itself. |
A user switches channel by saying so: "upgrade from the test branch", "check
experimental for updates", "switch this KB to the stable channel", "I want
the dev version". A branch named in the request **overrides** `ckb.yaml` for
this run, and — once the upgrade is actually applied — is written back to
`ckb.yaml` so the next run stays on the channel the user chose. Do not write
it back on a run the user cancelled: they asked to *look*, not to move.
Clone shallowly into a scratch location under `tmp/` — never touch this
repo's own git remotes or history to do this comparison:
```bash
rm -rf tmp/ckb-upgrade-src
git clone --depth 1 https://git.wierzbowa.cloud/michal/ckb.git tmp/ckb-upgrade-src
git clone --depth 1 --branch <branch> <repo> tmp/ckb-upgrade-src
```
If the clone fails (network, auth, unreachable host), report the raw
error and stop — this skill does not fall back to a cached or partial
check, and does not retry silently.
check, and does not retry silently. If it fails specifically because the
**branch doesn't exist**, list what does (`git ls-remote --heads <repo>`)
and stop; never quietly substitute `main` for a branch the user named.
State the resolved repo and branch before doing anything else. On `test` or
`experimental`, say what that means in one line — an upgrade that pulls
unreleased or actively-broken tooling into a working KB should never be a
surprise.
### Step 2 — Compare template (tooling) versions
@ -90,6 +148,24 @@ Step 3 regardless — a "no" here does **not** end the skill, because the
wiki content schema (Step 3) is checked independently and may still be
behind.
**When the local version is *higher* than the remote's, say so explicitly
rather than reporting "up to date".** The two are different facts and only
one of them is reassuring. This happens routinely once channels exist: a KB
that took tooling from `experimental` sits on a version `main` has not
released yet, so comparing it against `main` finds nothing newer — which is
true, and also not what "up to date" usually means to a reader.
Report it as what it is: "local v1.9.0 is ahead of `main`'s v1.8.0 — this KB
is carrying tooling from a pre-release channel." Then ask what they want,
because both answers are legitimate: stay ahead, or **downgrade** to the
stable branch. A downgrade is a real operation with real consequences — it
can remove skills, remove schema fields that local pages already use, and
lower `kb_schema_version` below what the content is written against — so
never perform one as a side effect of a channel switch. Name the specific
losses, take an explicit confirmation, and treat a `kb_schema_version` that
would drop below the local wiki's as a **blocker**, not a warning: the
content would stop conforming to its own declared contract.
### Step 3 — Compare the wiki content's own schema version
This is a **separate** check from Step 2, and can find something to do
@ -126,10 +202,20 @@ The template layer is exactly these paths — never anything under `wiki/`,
symlink)
- `LICENSE`
- `VERSION`
- `README.md`, `README.pl.md`, `MANUAL.md`, `MANUAL.pl.md` — whichever
exist upstream (an older local KB may not have some of these yet)
- `README.md`, `README.pl.md`, `MANUAL.md`, `MANUAL.pl.md`,
`CHANGELOG.md`, `CHANGELOG.pl.md` — whichever exist upstream (an older
local KB may not have some of these yet). Take the changelogs from
upstream wholesale rather than merging: they record the template's
history, which the upstream copy is authoritative about.
- `.agents/skills/<name>/**` for every skill folder present in
`tmp/ckb-upgrade-src/.agents/skills/`
- `.agents/modules/<name>/**` for every optional module present upstream
(see `ckb-module`). Upgrade the module *payload* freely — it is inert
template content. But **never delete a skill under `.agents/skills/`
just because it is absent upstream**: it may be a module-installed copy
that this KB deliberately enabled. Check `ckb.yaml` before treating any
skill as removed upstream. When an installed module's payload changes,
say so and suggest reinstalling it — the installed copies are now stale.
- The base ignore rules in `.gitignore` (`libs/`, `linked/`, `tmp/`,
regenerated `outputs/` subfolders, `.env`, etc.) — merged, never
replaced wholesale (see Step 6)
@ -221,6 +307,13 @@ Only if `template_upgrade_available` and at least one item was accepted:
this step — `wiki/` changes, if any, happen only in Step 7.
- Update local `VERSION` to the template's new version last, only once
every other accepted change has been applied successfully.
- If the user switched channel for this run (Step 0), write the new
`branch:` into `ckb.yaml`'s `template:` block now that the switch has
actually happened. Create the file with just that block if it doesn't
exist; if it does, edit only `template:` and leave `kb_modules:` exactly
as found — that list belongs to `ckb-module`. Persisting the branch is
what stops the *next* upgrade from silently pulling the KB back to
whichever channel it was on before.
### Step 7 — Rebuild wiki content to the new schema (only if confirmed in Step 5)
@ -240,7 +333,7 @@ exists, even if it looks sparse):
its own `index.md` per the Recursive Index & Log Convention.
**b. Backfill frontmatter on every existing page**, per the schema in
`AGENTS.md` §2. For each `.md` file under `wiki/` (excluding the reserved
`AGENTS.md` page schema. For each `.md` file under `wiki/` (excluding the reserved
`index.md`/`log.md`/`error-book.md`), check its frontmatter against the
schema and fix only what's missing — never touch a field that's already
present, and never alter the page's body text:
@ -266,6 +359,35 @@ present, and never alter the page's body text:
vice versa) — add the missing form. Don't invent new cross-references
that weren't already there in some form.
**b2. Schema 1.4 → 1.5: evidence sections.** Schema 1.5 added the reserved
body sections (`## Sources`, `## Crux`, `## Notes`), the
`source_fingerprint`/`source_checked` frontmatter fields, and the completed
edge vocabulary in `wiki/graph/index.md`. Migrating a 1.4 wiki:
- **`wiki/graph/index.md`** — replace its vocabulary section with the
template's table verbatim. Existing edges stay valid; 1.5 only added
verbs (`part_of`, `produces`, `configures`, `validates`, `implements`),
it removed none.
- **`## Sources` without fingerprints** — backfill a digest for every cited
file that still exists (`sha256sum <file> | cut -c1-8`), with
`source_checked` set to today. Where the cited file is **gone**, do not
quietly drop the bullet: leave it and flag the page, because a page whose
evidence has vanished is a finding the migration surfaced, not one it
should bury.
- **`## Crux`** — do **not** manufacture one. A Crux is verbatim source
text, and inventing quotes during a migration is precisely the failure
the section exists to make impossible. Pages gain a Crux when they are
next ingested or deliberately re-grounded, and a 1.5 page with no Crux is
fully conformant.
- **`## Notes`** — add an empty one to generated pages under
`libs/<name>/` so the protected affordance exists. Elsewhere, leave it
absent.
These are additive: a 1.4 page with none of them is valid 1.5, so this step
never blocks the version bump. Backfilling fingerprints is worth doing in
the same pass anyway, since it is mechanical and it is what makes `ckb-lint`
check 12 meaningful from that point on.
**c. Bump `wiki/index.md`'s `kb_schema_version`** to the template's
current value, once every page has been checked.
@ -297,7 +419,9 @@ Report a summary covering whichever tracks actually ran:
```markdown
## Upgrade report
**Template version:** v<old> → v<new> (or "already current")
**Source:** <repo> on branch **<branch>** (channel: stable / release candidate / development)
**Channel changed:** <old><new> (or "no")
**Template version:** v<old> → v<new> (or "already current", or "local vX is ahead of this branch's vY")
**Added:** <N> file(s) — <paths, or "none">
**Updated:** <N> file(s) — <paths, or "none">
**Kept local (declined template version):** <N> file(s) — <paths, or "none">
@ -316,6 +440,27 @@ other than the template project itself.
## Edge cases
- **`ckb.yaml` exists but has no `template:` block** — a KB created before
this convention. Treat it as canonical repo on `main`, and add the block
once an upgrade is actually applied. Don't write it on a check-only run.
- **`ckb.yaml` names a branch that no longer exists upstream** (a dev branch
that was merged and deleted) — report it with the list of branches that do
exist, and ask which to move to. Don't guess: a deleted `experimental` may
mean "its work is in `main` now" or "it was abandoned", and those lead to
different answers.
- **User asks to switch channel but declines every proposed change** — the
switch didn't happen, so don't persist the new branch. They looked at
another channel and chose not to take it; recording it would make the next
run pull from a channel they rejected.
- **Local `VERSION` is ahead of the target branch** — see Step 2. Report it
as "ahead", never as "up to date", and treat any downgrade as an explicit,
separately-confirmed operation.
- **Template repo is this very repo** (the template project upgrading
itself) — the comparison is between branches of one repo rather than
between two repos. That's valid, and the usual flow is `experimental`
`test``main`; but say plainly that this is a self-upgrade, since the
"local vs upstream" framing in the report reads oddly otherwise.
- **No `VERSION` file locally** — treat local version as `0.0.0`; any real
template version counts as newer. Mention in the report that this KB
predates version tracking.

2
.gitignore vendored
View file

@ -16,3 +16,5 @@ outputs/starlight
outputs/okf
outputs/teaching
.env
__pycache__/
*.pyc

369
AGENTS.md
View file

@ -1,209 +1,256 @@
# SYSTEM PROMPT: CASCADE KNOWLEDGE BASE ARCHITECT
## ROLE & PHILOSOPHY
You are an autonomous Knowledge Architect Agent for a **Cascade Knowledge Base**. The system is designed as a layered stack: read-only upstream knowledge bases (symlinked in `linked/` and git-managed copies in `libs/`) form the foundation, and the local mutable knowledge base overlays on top. This means knowledge flows downward through the cascade — upstream truths are preserved, while you only ever modify the local layer.
## ROLE
You are an autonomous Knowledge Architect Agent for a Cascade Knowledge Base.
The local `wiki/` overlays read-only upstream knowledge in `linked/` and
`libs/`. Local knowledge wins when the same entity exists in multiple layers.
If an entity exists in both the local wiki and any upstream KB, the local version takes precedence and overrides the upstream one.
You view directories as storage disks, context windows as RAM, and your processing loops as CPU cycles. Your sole objective is to build, maintain, and dynamically structure a comprehensive knowledge base, respecting the cascade priority rules at all times.
You possess full autonomy over local directory structure, file naming conventions, and cross-referencing. You must strictly adhere to the operational boundaries and file management rules detailed below.
Keep the always-loaded rules small. Detailed workflows live in skills and
should be loaded only when their trigger applies.
---
## 1. DIRECTORY STRUCTURE
The root directory contains exactly seven top-level entries. You must maintain this structure flawlessly:
## 1. DIRECTORY CONTRACT
Maintain this root layout:
```
├── libs/ # Read-only external sources, one of two kinds per <name>/ subfolder:
│ └── <name>/ # - GIT-COPY: a git-managed clone/ZIP unpack, gitignored, fully immutable — never write here.
│ # - CONNECTOR: identified by a user-authored source.yaml (connector + location,
│ # optionally an index: block pointing at a shared/pre-built index to fetch from).
│ # The agent owns and maintains a self-contained generated index alongside it —
│ # index.md/entities/graph/log.md, mirroring wiki/'s own shape but scoped entirely
│ # to this one connector. See §4 EXTERNAL SOURCE INDEXING. source.yaml itself stays
│ # user-only, same as everything in a git-copy lib. Whether *this* user may rebuild
│ # it (vs. only read a fetched/published copy) is a local, per-user, gitignored
│ # source.local.yaml — read-only by default.
├── linked/ # SYMLINKS ONLY. Each entry is a symbolic link to another KB root (read-only upstream source of truth).
│ └── <name>/ # Individual upstream knowledge base (immutable — never write here).
├── outputs/ # MANAGED BY AGENT. Generated artifacts, exports, compiled files produced from the wiki.
│ # On-demand workflows beyond Ingest/Lint may be defined as Claude Code Skills under
│ # `.claude/skills/` — check there before assuming a capability doesn't exist.
├── raw/ # WRITTEN BY USER ONLY. Raw files, scratchpad notes, URLs, links.txt.
│ ├── inbox/ # Drop zone: unprocessed material the agent cleans on ingest.
│ └── archive/ # AGENT MAINTAINED. Ingested raw material, filed by ingestion date.
│ └── <YYYY-MM-DD>/ # One folder per ingestion date; holds every raw/inbox file processed that day.
├── tmp/ # MANAGED BY AGENT. Temporary files, caches, intermediate processing artifacts (gitignored).
├── wiki/ # MANAGED BY AGENT. The local, mutable, structured markdown wiki. Overlays linked/ and libs/.
│ ├── index.md # Entry point / routing table with "Use when" triggers. Carries kb_schema_version.
│ ├── overview.md # High-level map of the knowledge base.
│ ├── log.md # AGENT LOG. Root rollup tracking wiki-level modifications (see Recursive Index & Log Convention).
│ ├── error-book.md # AGENT MAINTAINED. Records compilation errors and derived constraints.
│ ├── entities/ # AGENT POPULATED. Typed entity pages (people, projects, concepts, libraries). Has its own index.md.
│ └── graph/ # AGENT MAINTAINED. Edge lists and relationship data for the knowledge graph. Has its own index.md.
└── workload/ # MANAGED BY AGENT. Summaries of discussions and decisions.
├── .agents/modules/ # Optional modules, inert until installed
├── libs/ # Read-only external sources:
│ └── <name>/ # - git-copy clone/ZIP: immutable, never write here
│ # - connector source: has user-authored source.yaml
│ # and an agent-owned generated index
├── linked/ # Symlinks to other KB roots, read-only
├── outputs/ # Agent-generated artifacts and exports
├── raw/ # User-provided source material
│ ├── inbox/ # Drop zone for unprocessed material
│ └── archive/ # Agent-filed processed inputs by date
├── tmp/ # Agent temporary files and caches
├── wiki/ # Local mutable structured wiki
│ ├── index.md # Routing table with kb_schema_version
│ ├── overview.md
│ ├── log.md
│ ├── error-book.md
│ ├── query-gaps.md
│ ├── projects/
│ ├── decisions/ # Numbered, append-only decision records
│ ├── entities/
│ └── graph/
└── workload/ # Session summaries and decisions
└── YYYY-MM-DD_summary.md
```
### Cascade Lookup Priority
When searching for any entity, concept, or file, use the following cascade (first match wins):
### Optional Modules
Capabilities not every KB needs ship in `.agents/modules/<name>/` and are
inert until installed. Installing copies the module's skills into
`.agents/skills/`, creates its scaffold, appends a marked block to this
file and `.gitignore`, and records it in the root `ckb.yaml`. `ckb-module`
owns that; read `ckb.yaml` to see what is installed. A module may add
directories (such as `src/`) and optional page types — always additively.
1. **Local wiki/** — highest priority; agent-written content overlays everything below.
2. **linked/\<name\>/** — read-only upstream KBs mounted as symlinks, searched in alphabetical order.
3. **libs/\<name\>/** — read-only external sources, searched in alphabetical order. For a git-copy lib this is its cloned files; for a connector-backed lib (one with a `source.yaml`) this layer's content *is* the agent-generated index (`index.md`/`entities/`/`graph/`) built by the `ckb-index-external` skill, not raw copied files — see §4.
4. If no match is found anywhere, treat the entity as unknown.
`CLAUDE.md` is a symlink to this file and `.claude/skills` a symlink to
`.agents/skills`. Write the `.agents`-side path only; never duplicate.
You must **never** create, modify, move, or delete any file or directory inside `linked/` or a git-copy `libs/<name>/`. The one exception is a connector-backed `libs/<name>/`'s own generated index, which the agent owns and maintains exactly like `wiki/` — see Rule A in §7.
### Cascade Priority
When searching for any entity, concept, or file, use first match wins:
1. `wiki/` - local mutable layer.
2. `linked/<name>/` - read-only upstream KBs, alphabetical.
3. `libs/<name>/` - read-only external sources, alphabetical. For a
connector-backed lib, this means its generated index, not the live
connector itself.
4. If no match is found, treat the entity as unknown.
Never write inside `linked/` or a git-copy `libs/<name>/`.
Connector-backed `libs/<name>/` folders are the exception: if a folder has
`source.yaml`, the agent may maintain that folder's generated
`index.md`/`entities/`/`graph/`/`log.md` through `ckb-index-external`.
The agent must never edit `source.yaml`. Rebuilding from the live connector
requires local `libs/<name>/source.local.yaml` with `access: write`;
absence means read-only.
### Index-First Navigation
When searching for information, always start by looking for `index.md` files.
Read the index to discover what pages and subdirectories are available before
drilling into individual files. Scan `index.md` across all layers:
For KB questions, start at `wiki/index.md`, then matching subdirectory
indexes. Only drill into pages that match the task. If local indexes do not
answer, continue through `linked/` and `libs/` indexes in cascade order.
1. **wiki/** — scan `wiki/index.md`, then recursively check any subdirectory `wiki/<topic>/index.md`.
2. **linked/\<name\>/** — for each linked upstream KB, scan its root `index.md` and subdirectory indexes.
3. **libs/\<name\>/** — same pattern: root index first, then subdirectory indexes as needed.
This avoids blind filesystem scans and uses the index as a curated table of contents — exactly as Karpathy's original pattern intended.
### Recursive Index & Log Convention
Index-First Navigation only works if subdirectory indexes actually exist. Maintain them as follows:
- Every `wiki/` subdirectory that groups multiple pages (`entities/`, `graph/`, and any future topic folder) must contain its own `index.md`. It carries no frontmatter and is a flat bullet list of links, each with a one-line description mirroring the linked page's `tldr` — plus a link to any nested subdirectory.
- A subdirectory may also keep its own `log.md` once it has enough independent change history to warrant one (a judgment call — typically once it holds several pages or changes on its own cadence, separate from the rest of the wiki). Entries follow the same reverse-chronological format as Rule B.
- The root `wiki/log.md` stays the top-level rollup: it records changes made directly under `wiki/` (`index.md`, `overview.md`, `error-book.md`, directory-creation events) plus one pointer line whenever a subdirectory log absorbs a change, e.g. `- See wiki/entities/log.md for entity-page changes on this date.` Each change gets exactly one home log — never record the same change in both.
- The same convention applies verbatim inside a connector-backed `libs/<name>/` (§4) — its generated `index.md`/`entities/index.md`/`graph/index.md`/`log.md` mirror this pattern exactly, scoped entirely to that one connector. Its `log.md` is independent of `wiki/log.md` — never record a connector-indexing change in both.
### Lazy-Loading with "Use When" Triggers
The `wiki/index.md` is a routing table. Each entry has a **Use when** column
that lists trigger keywords. Before loading any page:
1. Read `wiki/index.md` (stays in context — it is small).
2. Match the current task's keywords against the **Use when** entries.
3. Only load the matching page(s). Do not load every page.
4. If a page has a `tldr:` frontmatter field, read that first. If it answers the query, skip the body.
This keeps context lean: ~34 pages loaded instead of all pages.
Every `wiki/` subdirectory that groups pages, including `projects/`,
`entities/`, and `graph/`, must have its own `index.md`. Use the same
convention inside connector-backed libs for their generated index.
---
## 2. PAGE FRONTMATTER SCHEMA
Every wiki page must use YAML frontmatter. `type` is required; the rest are optional:
## 2. PAGE SCHEMA
Every non-reserved wiki page uses YAML frontmatter. `type` is required; the
other fields are optional but preferred when meaningful:
```yaml
---
type: concept # REQUIRED. Open string for the entity/content kind (e.g. person, project, concept, library, decision, playbook). Unregistered — new values are always valid; readers must tolerate unrecognized types.
resource: https://... # Optional. Canonical URI to the authoritative external source this page describes (a linked/<name>/... or libs/<name>/... path, ticket, repo, doc, dataset). Keeps "what the wiki says about it" separate from "where the real thing lives."
type: concept
resource: https://...
tldr: One-sentence summary optimised for LLM reading
confidence: 0.01.0 # How many/corroborated sources support this
quality: 0.01.0 # Self-evaluation: well-structured, consistent, cited
confidence: 0.0-1.0
quality: 0.0-1.0
supersedes: path/to/older/page.md
superseded_by: path/to/newer/page.md
last_updated: YYYY-MM-DD
freshness_window_days: 90 # Days before considered potentially stale
retention: high|medium|low # How aggressively to deprioritize when old
freshness_window_days: 90
retention: high|medium|low
source_fingerprint: sha256:3f9a2c1e # digest of the source this page was built from
source_checked: YYYY-MM-DD # when that digest was last verified
---
```
- **`type`** — required on every page. Set once on write and rarely changed; it's the first thing lint checks for conformance, and it's how pages in `entities/` get grouped without depending on directory naming alone.
- **`resource`** — set when the page describes something with a stable external address. Omit for pages that are pure synthesis (e.g. an overview or a decision writeup with no single external source).
- **`tldr`** — generated on write. If the TLDR alone answers a query, the body is never loaded.
- **`confidence`** — set on write based on source corroboration. Decays with time unless reinforced by new sources.
- **`quality`** — self-score on write. Below 0.7 → flag for review.
- **`supersedes` / `superseded_by`** — when new info contradicts or updates an old page, link them. Old pages are preserved but marked stale.
- **`last_updated`** — set automatically on every write or edit.
- **`freshness_window_days`** — pages older than this window are flagged stale during lint.
- **`retention`** — `low` pages may be archived or deprioritized after the freshness window expires.
### Page Body Sections
Four body sections are reserved across the whole KB. All are optional, but
where present they mean exactly this and nothing else:
### Schema Versioning
`wiki/index.md` (only) carries an additional frontmatter field, `kb_schema_version` (e.g. `"1.1"`), declaring which revision of this schema the wiki was authored against. Bump the minor version when adding an optional field (backward-compatible); bump the major version when changing or removing a required field or reserved filename convention (breaking). Individual pages do not carry this field — it is a bundle-level declaration, not a per-page one.
- **`## Sources`** — where this page came from. One bullet per source, each
carrying a fingerprint so drift is detectable by machine rather than by
calendar:
`` - `raw/archive/2026-09-21/kickoff.md` — sha256:3f9a2c1e (checked 2026-09-21) ``
For a page built from exactly one source, the same digest also goes in
`source_fingerprint`/`source_checked` frontmatter.
- **`## Crux`** — verbatim excerpts from those sources, never a paraphrase.
Quote the few lines that actually carry the claim, attributed to the
specific source bullet they came from. A quote is evidence: it either
still matches the source or it doesn't, which is what makes drift
visible. Never edit a quote to read better — if it no longer matches,
that is a finding, not an edit.
- **`## Notes`** — human-authored, and **protected**. No skill may
rewrite, reflow, summarize, or drop this section; regeneration
preserves it byte-for-byte. It is the only place a person can annotate
an agent-regenerated page and expect it to survive.
- **`## Evidence`** / other sections are ordinary content with no special
handling.
Pages with `type: decision` live in `wiki/decisions/` as `NNNN-slug.md` and
add `status` (`proposed`/`accepted`/`rejected`/`superseded`/`reversed`),
`decided_on`, `decided_by`, and optionally `affects` and `review_on`. They are
append-only: never rewrite a decision's substance to match a later change of
mind — record a new decision that supersedes it, and set both
`supersedes` and `superseded_by`. `ckb-decide` owns the format.
`wiki/index.md` alone also carries `kb_schema_version`, currently `"1.5"`
for the base contract. An installed module may raise it additively by
declaring `requires_kb_schema` and contributing optional types and fields;
`ckb-module` applies that bump at install time. Detailed schema migration
and version-bump policy belongs in `ckb-upgrade`.
---
## 3. INGESTION WORKFLOW (TRIGGERED ON DEMAND)
## 3. SKILL ROUTING
Use skills for procedural work instead of keeping full workflows in this
file.
When the user says "Ingest", "Sync the wiki", or "Update the Wiki" (for syncing this repo's own git history with its remote, see the ckb-sync-changes skill under `.claude/skills/` instead), run the **ckb-ingest** Claude Code Skill — see `.agents/skills/ckb-ingest/SKILL.md` — rather than following inline steps here, so the full procedure (process inbox, consult cascade, extract entities, synthesize pages, cross-link, update index/log, then remind to review and sync) only loads into context when actually invoked.
| User intent | Skill |
|---|---|
| Answer or research a KB question | `ckb-retrieve` |
| Ingest raw material into `wiki/` | `ckb-ingest` |
| Record a decision, or answer what/why/who/when was decided | `ckb-decide` |
| Index connector-backed `libs/` sources | `ckb-index-external` |
| Health-check or repair wiki/index structure | `ckb-lint` |
| Sync this repo with `origin` | `ckb-sync-changes` |
| Upgrade template or wiki schema | `ckb-upgrade` |
| Bootstrap a new empty KB (from local files or a fresh clone of the template repo) | `ckb-init` |
| Install, list, or uninstall an optional module | `ckb-module` |
| Empty the wiki back to a clean template (destructive) | `ckb-reset` |
| Export OKF or Starlight artifacts | `ckb-export-okf`, `ckb-export-starlight` |
| Generate a project overview | `ckb-project-summary` |
| Teach, quiz, or onboard from the wiki | `ckb-teach-me`, `ckb-quiz`, `ckb-onboard-me` |
Short routing rules:
- For questions, use `ckb-retrieve`; it owns project scopes, hybrid local
search, rank fusion across signals, dedupe/rerank, expertise and
ownership lookups, evidence packets, source verification, answer
caveats, and query-gap capture.
- For "Ingest", "Sync the wiki", or "Update the wiki", use `ckb-ingest`.
- For "record a decision", "we decided ...", or a question whose subject is a
decision (what/why/who/when, what superseded it, what is still open), use
`ckb-decide`. For a wider question that merely touches decisions, stay in
`ckb-retrieve`.
- For "Index external sources", "index libs", or "refresh the external
index", use `ckb-index-external`.
- For "Lint" or "health-check the wiki", use `ckb-lint`.
- For "sync changes", "sync with origin", or "push and pull my changes",
use `ckb-sync-changes`, not ingest.
- For "upgrade the wiki" or "check for a newer template version", use
`ckb-upgrade`. The template repo keeps three branches — `main` (stable),
`test` (release candidate), `experimental` (development) — and a request
naming one ("upgrade from `test`", "switch to the stable channel") routes
here too. Which branch this KB tracks lives in `ckb.yaml`'s `template:`
block and defaults to `main`; `ckb-init` names it the same way when
bootstrapping.
- For "install/uninstall the `<name>` module", "what modules are
available", or a request for a capability the base KB lacks (such as
handling source code), use `ckb-module`.
- For "reset the wiki", "empty the wiki", or "make this a clean template",
use `ckb-reset`. It deletes accumulated knowledge, so it always shows an
inventory and takes a typed confirmation first, and never touches the
template layer or `src/`.
---
## 4. EXTERNAL SOURCE INDEXING (TRIGGERED ON DEMAND)
## 4. NON-NEGOTIABLE RULES
When the user says "Index external sources" (or "index libs", "refresh the external index"), run the **ckb-index-external** Claude Code Skill — see `.agents/skills/ckb-index-external/SKILL.md` — rather than following inline steps here, so the full procedure only loads into context when actually invoked. It walks every connector-backed `libs/<name>/` (one with a `source.yaml` — see §1), fetches a shared/pre-built index if `source.yaml` declares one (`index.store`/`index.location` — git or a shared resource), and — only if this user has local `access: write` in `libs/<name>/source.local.yaml` (read-only by default) — resolves the declared connector to whatever live tool is available this session and builds/refreshes that connector's own self-contained `index.md`/`entities/`/`graph/`/`log.md`, publishing it back to the shared store if one is configured. This never touches `wiki/`, never touches `source.yaml`, and never touches a git-copy lib.
### Rule A: Immutability
Do not modify `linked/` or git-copy `libs/<name>/`. To correct upstream
knowledge, write a local override in `wiki/`.
For connector-backed `libs/<name>/`, only generated index files are
agent-owned. `source.yaml` is user-owned. `source.local.yaml` may be created
or edited only when the user explicitly asks to become or stop being that
source's admin.
### Rule B: Wiki Change Log
Every create, update, move, or delete inside `wiki/` must be logged
immediately in `wiki/log.md` before proceeding.
Use reverse chronological order and this format:
```markdown
## [YYYY-MM-DD HH:MM] - [ACTION TYPE]
- **File Affected:** `wiki/path/to/file.md`
- **Description:** Brief summary of what knowledge or structure changed.
- **Source:** Chat conversation, raw file, URL, or skill name.
---
```
## 5. QUERY WORKFLOW
### Rule C: Links
For upstream references, link with project-root-relative paths such as
`linked/<name>/...` or `libs/<name>/...`.
When answering a question or researching a topic:
For local wiki references, prefer project-root-absolute paths such as
`/wiki/entities/foo.md`. Use both `[[Wikilinks]]` and standard Markdown
links on cross-references whenever practical.
1. **Read the index**`wiki/index.md` first. Match query keywords against **Use when** triggers.
2. **Read TLDRs** — for any matched page, read its `tldr:` frontmatter first. If it answers the query, stop.
3. **Load full pages** — only if the TLDR was insufficient.
4. **Walk the graph** — if the entity has relationships in `wiki/graph/edges.json`, follow them to discover connected pages (e.g. "what depends on X?").
5. **Fall back upstream** — if the local wiki has no match, check `linked/<name>/` indexes, then `libs/<name>/` indexes (for a connector-backed lib, that means its generated `entities/`/`index.md`, not the live source directly — if it's not there yet, suggest running "index external sources" rather than fetching the live source ad hoc). Apply cascade priority throughout.
### Rule D: Session Summary
After every conversational turn where you read, write, search, ingest, lint,
or answer, append a short note to `workload/YYYY-MM-DD_summary.md`.
---
### Rule E: Session Start and End
At session start, read `wiki/index.md`, the latest `workload/` summary, and
run two cheap checks:
## 6. MAINTENANCE WORKFLOW (LINT)
```bash
git status --short --branch
python3 .agents/skills/ckb-lint/scripts/lint_report.py --quick
```
Periodically (or when asked to "Lint"), run the **ckb-lint** Claude Code Skill — see `.agents/skills/ckb-lint/SKILL.md` — rather than following inline steps here, so the full checklist (conformance, freshness, confidence decay, retention sweep, supersession detection, orphan detection, graph consistency, index/log consistency, error-book entries, auto-fix vs. report, then a reminder to review and sync) only loads into context when actually invoked.
`git status` covers unsynchronized history — if there are uncommitted
changes or the branch is ahead/behind its remote-tracking branch, say so and
suggest `ckb-sync-changes`. `--quick` covers knowledge rot: one line stating
how many pages are past their freshness window, how many cited sources no
longer match their recorded fingerprint, and how many pages are malformed.
It is deterministic and costs no model tokens. Report the line if anything
is non-zero and suggest `ckb-lint`; stay quiet when it is clean.
---
At session end, update `workload/` and repeat the unsynchronized-change
check. Do not fetch or push unless the user asks.
## 7. COMPLIANCE & LOGGING RULES (NON-NEGOTIABLE)
### Rule A: Immutability of linked/ and libs/
You must **never** write, modify, move, or delete any file or directory inside `linked/` or a git-copy `libs/<name>/`. These are read-only upstream sources of truth managed exclusively by the User. If information in them is outdated or incorrect, you may override it by writing a corrected version in the local `wiki/`. The local version will take priority in the cascade lookup.
**Exception — connector-backed `libs/<name>/`:** identified by the presence of a `source.yaml` (see §1). Its `source.yaml` is user-authored and stays just as untouchable as anything else here. But everything else in that folder — `index.md`, `entities/`, `graph/`, `log.md` — is a generated index the agent owns and maintains exactly as it would `wiki/`, built and refreshed by the `ckb-index-external` skill (§4). This exception applies only to a `libs/<name>/` that has a `source.yaml`; a plain git-copy lib has no such carve-out.
Within that exception, two things the agent may always do regardless of this user's access level: fetch a shared/pre-built index down into `libs/<name>/` if `source.yaml` declares one, and read whatever's cached there. Actually rebuilding it from the live connector — and publishing that rebuild back to a shared store — is gated by a separate, local, per-user `libs/<name>/source.local.yaml` (never committed, never synced, never read by anyone else): `access: write` opts this user in; its absence (the default) means read-only. Unlike `source.yaml`, the agent *may* create or edit `source.local.yaml` — but only when this user explicitly asks to become (or stop being) that source's admin, never on its own initiative.
### Rule B: The Wiki Change Log (`wiki/log.md`)
Every single time you create, modify, move, or delete a file within the `wiki/` directory, you must immediately document it in `wiki/log.md` before proceeding.
- **Ordering:** The most recent action **must always be at the very top** of the file (chrono-reverse order).
- **Format Per Entry:**
```markdown
## [YYYY-MM-DD HH:MM] - [ACTION TYPE: e.g., CREATE/UPDATE/DELETE]
- **File Affected:** `wiki/path/to/file.md`
- **Description:** Brief summary of what knowledge was added or altered.
- **Source:** [e.g., Chat conversation, raw/notes.txt, URL]
---
```
### Rule C: Cascade-Anchored References with Dual-Linking
When cross-referencing an entity that exists in an upstream KB, write the link using the relative path from the project root (e.g., `linked/<name>/wiki/concepts/foo.md` or `libs/<name>/docs/bar.md`). This preserves the cascade structure and makes it clear which layer the reference belongs to.
For references between pages within `wiki/` itself, prefer project-root-absolute paths (e.g. `/wiki/entities/foo.md`) over relative paths (`../entities/foo.md`). Absolute paths keep resolving correctly if either page is later moved during a lint or reorganization pass; relative paths silently break.
Use **both** `[[Wikilinks]]` (Obsidian-compatible) and standard `[markdown](path.md)` links on every cross-reference. This ensures the wiki works in Obsidian graph view, GitHub rendering, and CLI tools.
### Rule D: Session Summary (`workload/`)
After every conversational turn where you take any action (read, write, search, ingest, lint, answer a question), update the summary file in `workload/`. If today's file already exists, append new notes to it; otherwise create it.
- **Naming:** `workload/YYYY-MM-DD_summary.md`
- **Content:** Brief record of what was discussed, what actions were taken, and what decisions were made during this exchange.
- **Purpose:** Provides continuity between sessions and a browsable history of how the knowledge base evolved.
### Rule E: Automation Hooks
Follow these event-driven behaviors:
- **On new source in inbox** — on the next ingest, auto-process: extract entities, update graph, update index, write to log.
- **On new or changed `libs/<name>/source.yaml`** — on the next "index external sources" run, process it: resolve the connector, enumerate documents, build/refresh that connector's own `index.md`/`entities/`/`graph/`/`log.md`.
- **On session start** — read `wiki/index.md` and the latest `workload/` summary to load relevant context. Also check for unsynchronized changes (`git status` — uncommitted local changes, or the local branch ahead/behind its remote-tracking ref) and, if any are found, tell the user and suggest running the `ckb-sync-changes` skill before proceeding. This is a cheap, read-only check (no `git fetch`) — a heads-up, not a substitute for actually running that skill.
- **On session end** — compress the session into observations and file insights into `workload/`. Also re-run the same unsynchronized-changes check as at session start — the session's own work may have just created new local changes — and suggest `ckb-sync-changes` if anything is now pending.
- **On query** — if the answer has lasting value, file it back into `wiki/` as a new page or update to an existing one.
- **On memory write** — check for contradictions with existing wiki content. If found, apply supersession (link old → new) and log it.
- **On schedule** — periodic lint, consolidation, retention decay, freshness check.
### Rule F: Demand-Driven Context (DDC)
Use agent failures as the signal for what knowledge to add:
1. When you cannot answer a question or complete a task, identify the missing knowledge.
2. Propose a minimal entity or page to fill the gap.
3. The user approves or provides the source material.
4. Add it to `raw/inbox/` or describe it in chat.
5. Next ingest cycle incorporates it.
This keeps the wiki lean — you only add what is needed, not what is merely available.
### Rule F: Demand-Driven Context
When the KB cannot answer something, identify the missing knowledge and
propose the smallest source or page that would close the gap. `ckb-retrieve`
owns durable query-gap entries; `ckb-ingest` owns turning approved/source
material into wiki pages.

370
CHANGELOG.md Normal file
View file

@ -0,0 +1,370 @@
# Changelog & Schema Reference
*Read this in: **English** | [Polski](CHANGELOG.pl.md)*
This file tracks two things: the page schema exactly as it stands today, and
how this project arrived at its current version numbers.
There are **two independent version numbers**, and they are not the same
thing:
| Number | Lives in | Describes | Who bumps it |
|---|---|---|---|
| `kb_schema_version` | frontmatter of `wiki/index.md` | the **content contract** — what a page may contain and what those fields mean | `ckb-upgrade` (on a confirmed migration), `ckb-module` (additively, on install) |
| Template version | [`VERSION`](VERSION) | the **tooling layer**`AGENTS.md`, the skills, the scripts, the docs | `ckb-upgrade`, when it pulls a newer template |
They move independently on purpose. You can take a newer set of skills
without touching a single wiki page, and a wiki authored against an older
schema keeps working — that is what the schema version is for.
**Versioning policy.** Bump the **minor** version for an additive change: a
new optional field, a new optional body section, a new optional scaffold
file. Bump the **major** version for a breaking one: changing or removing a
required field, or changing an existing reserved filename convention. There
has never been a major bump; every schema version to date has been additive,
so any page written since 2026-07-13 is still valid today.
---
## Contents
1. [Current page schema (1.5)](#current-page-schema-kb_schema_version-15)
2. [KB schema version history](#kb-schema-version-history)
3. [Template version history](#template-version-history)
4. [Migrating between versions](#migrating-between-versions)
---
## Current page schema (`kb_schema_version: "1.5"`)
### Frontmatter — every non-reserved page
Every `.md` file under `wiki/` except the reserved ones (`index.md`,
`log.md`) carries YAML frontmatter. Only `type` is required; the rest are
optional, and preferred where meaningful. The parser is deliberately simple —
flat `key: value` pairs only, no nesting.
| Field | Required | Meaning |
|---|---|---|
| `type` | **yes** | Open string: `person`, `project`, `concept`, `library`, `decision`, `playbook`, `repository`, `component`, … New values are always valid; readers tolerate unrecognised ones. |
| `resource` | no | Canonical URI of the authoritative external source this page describes, kept separate from the wiki's own commentary. |
| `tldr` | no | One-sentence summary optimised for LLM reading. This is what the index shows and what decides whether a page gets opened at all. |
| `confidence` | no | `0.0``1.0`. Source corroboration. Set on write, decays if nothing reinforces it, raised by a new source that agrees. |
| `quality` | no | `0.0``1.0`. Self-evaluation of the page's own structure and citations. Below `0.7` is flagged for review. |
| `supersedes` | no | Path to the older page this one replaces. |
| `superseded_by` | no | Path to the newer page that replaced this one. **Always set both sides.** |
| `last_updated` | no | `YYYY-MM-DD`. When the *page* last changed. |
| `freshness_window_days` | no | Days before lint flags the page as stale. Typical: 90 for a wiki page, 365 for a decision, 30 for a connector-indexed document. |
| `retention` | no | `high` / `medium` / `low`. A `low` page is archived (never deleted) after 2× its freshness window. |
| `source_fingerprint` | no | *(1.5)* Digest of the source this page was built from — `sha256:<8 hex>` for a local file, or `etag:<value>` / `mtime:<iso8601>` for a connector item. |
| `source_checked` | no | *(1.5)* `YYYY-MM-DD` — when that digest was last verified. Distinct from `last_updated`: a re-check that finds nothing changed advances this and leaves `last_updated` alone. |
`wiki/index.md` alone carries `kb_schema_version`. It is a bundle-level
declaration, not a per-page one — individual pages never carry it.
### Frontmatter — decision pages
Pages with `type: decision` live in `wiki/decisions/` as `NNNN-slug.md` and
add:
| Field | Required | Meaning |
|---|---|---|
| `status` | **yes** | `proposed` / `accepted` / `rejected` / `superseded` / `reversed`. The vocabulary is defined in `wiki/decisions/index.md` and validated by lint. |
| `decided_on` | for `accepted`/`rejected`/`reversed` | `YYYY-MM-DD`, the date the call was made. |
| `decided_by` | when known | Comma-separated names. Where genuinely unknown, write `unknown` rather than omitting the field — "we don't know who decided this" is itself worth recording. |
| `affects` | no | Comma-separated wiki paths this decision constrains. |
| `review_on` | no | `YYYY-MM-DD` to revisit. Lint reports these once the date passes. |
Decision records are **append-only**. A decision is never rewritten to match
a later change of mind: record a new one that supersedes it, and both stay on
the record.
### Reserved body sections
*(New in 1.5.)* Four `##` headings mean the same thing on every page in every
layer of the cascade. All are optional; where present, they mean exactly this
and nothing else.
#### `## Sources`
Where the page came from. One bullet per source, each carrying a fingerprint:
```markdown
## Sources
- `raw/archive/2026-09-21/kickoff-notes.md` — sha256:3f9a2c1e (checked 2026-09-21)
```
A freshness window is a guess that a source *might* have moved. A fingerprint
is a fact about whether it *did*. Lint recomputes local digests and flags what
actually changed, which is a different and more urgent finding than a page
that has merely aged.
#### `## Crux`
Verbatim excerpts from those sources — evidence, never paraphrase — attributed
to the source bullet they came from:
```markdown
## Crux
> FDEs need the VDI *and* a Jira account before day one; the VDI request
> alone takes ten working days.
`raw/archive/2026-09-21/kickoff-notes.md`, under "Access"
```
Three to ten lines is the working range. A Crux approaching the length of the
summary above it has stopped being evidence and become a second copy of the
source. Two things follow from quoting rather than paraphrasing: a question
can often be answered from the page instead of the archive, and drift becomes
visible — a summary can wander from its source silently, a quote either still
matches or it does not.
A page with no quotable source simply has no `## Crux`. An empty or
paraphrased one is worse than none, because it looks like evidence.
#### `## Notes`
Human-authored, and **protected**. No skill may rewrite, reflow, summarise or
drop this section; regeneration preserves it byte-for-byte.
```markdown
## Notes
<!-- Yours. Never rewritten by any skill. -->
```
This matters most on pages the agent *regenerates* — connector indexes, code
maps — where everything else is discarded and rebuilt on the next run. It is
the only place an annotation survives. Decision pages deliberately have no
`## Notes`: nothing regenerates them, and an append-only record with a freely
editable annotation block invites exactly the retroactive revision the
append-only rule exists to prevent.
### Edge vocabulary
Relationships live in `wiki/graph/edges.json` (and each connector index's own
`graph/edges.json`). The vocabulary is closed, and each verb is defined by the
question it answers — if a proposed edge answers none of them, it belongs in
the page's prose instead.
| Verb | Question it answers | Since |
|---|---|---|
| `part_of` | Where does this live? What is it a piece of? | 1.5 |
| `uses` | What does this reach for at runtime? | 1.1 |
| `depends_on` | What breaks if I change this? | 1.1 |
| `produces` | Where does this output come from? | 1.5 |
| `configures` | What changes this thing's behaviour? | 1.5 |
| `validates` | What checks, tests, or judges this? | 1.5 |
| `implements` | What contract must this honour? | 1.5 |
| `caused` | Why did this happen? | 1.1 |
| `contradicts` | What disagrees with this, unresolved? | 1.1 |
| `supersedes` | What replaced this, and what did it replace? | 1.1 |
| `decided_by` | Who made this call? | 1.4 |
| `affects` | What does this decision constrain? | 1.4 |
| `has_expertise_in` | Who can answer questions on this? | 1.3 |
| `owns` | Who is responsible for this? | 1.3 |
| `mentioned_in` | Which source document discusses this? | 1.2 (lib indexes only) |
Record one direction per relationship — `part_of`, `supersedes`,
`depends_on` and `uses` are canonical, and the inverse is not stored as a
second edge. Record edges only from demonstrated evidence; an absent edge
beats a fabricated one, and a padded graph degrades retrieval rather than
improving it (in-degree is used as a ranking signal).
### Reserved scaffold
| Path | Since | Purpose |
|---|---|---|
| `wiki/index.md` | 1.1 | Routing table; the only page carrying `kb_schema_version` |
| `wiki/overview.md` | 1.1 | High-level map of the KB |
| `wiki/log.md` | 1.1 | Reverse-chronological change log for `wiki/` |
| `wiki/error-book.md` | 1.1 | Systemic issues with root cause and fix |
| `wiki/entities/` | 1.1 | Typed entity pages |
| `wiki/graph/` | 1.1 | `edges.json` plus the vocabulary in `index.md` |
| `wiki/query-gaps.md` | 1.2 | Questions the wiki could not answer, driving demand-driven ingest |
| `wiki/projects/` | 1.2 | Optional local query scopes |
| `wiki/decisions/` | 1.4 | Numbered, append-only decision records, with their own `log.md` |
Every subdirectory that groups pages carries its own `index.md`, so
navigation stays lazy.
---
## KB schema version history
### 1.5 — 2026-09-21 · evidence, fingerprints, and a completed edge vocabulary
Adopted from an analysis of [trailhq/Graft](https://github.com/trailhq/Graft),
a context layer for coding agents that keeps a derived code graph in sync via
content hashes rather than dates, and protects a user-authored block on every
regenerated node. Graft's store is disposable and regenerable; this one is not,
so most of its design does not transfer — but several mechanisms do, and two
of them closed real gaps here.
**Added:**
- **`## Crux`** — verbatim source excerpts alongside the synthesis. Lets
`ckb-retrieve` ground an answer without a round-trip to the archive (when
the fingerprint still matches), and makes drift detectable.
- **`## Notes`** — human-authored and protected everywhere. This closed a real
gap: `ckb-index-external` regenerates connector pages wholesale, so an
annotation written there was previously destroyed on the next refresh.
- **`## Sources`** — formalised as a reserved section with one fingerprinted
bullet per source. Previously an informal convention that `ckb-retrieve`
relied on but no skill actually specified.
- **`source_fingerprint` / `source_checked`** frontmatter.
- **Edge verbs** `part_of`, `produces`, `configures`, `validates`,
`implements`. `part_of` fixed a live inconsistency: `ckb-code-map` had been
writing it since template 1.7.0 while the schema never declared it.
**Changed:**
- `wiki/graph/index.md` rewritten as a question-per-verb table with
conventions on edge direction and evidence.
- `ckb-ingest` gained a blast-radius step: before writing, walk the graph
backwards from touched entities to find what the incoming material
confirms, extends or contradicts, and name the owners of affected pages.
Ingest had been additive-first, which is how a wiki accumulates two pages
that quietly disagree.
- `ckb-retrieve` fuses graph in-degree as one ranked list among several,
weighted below 1.0 — centrality is a prior, not evidence.
- Rule E (session start) now runs `lint_report.py --quick` alongside
`git status`: a deterministic one-line knowledge-rot signal costing no
model tokens.
- Lint gained checks 12 (fingerprint drift), 13 (crux verbatimness) and 14
(the protected-`## Notes` rule).
**Compatibility:** fully additive. A 1.4 page with none of the new sections or
fields is valid 1.5. A migration will never manufacture a `## Crux`
inventing quotes is precisely the failure that section exists to prevent.
### 1.4 — 2026-09-01 · decision records
**Added:** `type: decision` pages under `wiki/decisions/` as `NNNN-slug.md`,
with `status`, `decided_on`, `decided_by`, `affects`, `review_on`; the
`decided_by` and `affects` edge verbs; `wiki/decisions/index.md` and its own
`log.md`; the append-only rule. Answers "why is it like this", "who decided",
and "what changed that decision" as direct lookups instead of a full-text
guess. Owned by the `ckb-decide` skill.
### 1.3 — 2026-08-06 · people-to-topic edges
**Added:** the `has_expertise_in` and `owns` edge verbs, making "who knows
about X" and "who owns X" a graph lookup rather than a full-text search.
Recorded only from demonstrated evidence — attending a meeting is not
expertise, and a job title is not ownership.
### 1.2 — 2026-07-29 · query scopes, gaps, and connector-backed libs
**Added:** `wiki/projects/` (optional local query scopes grouping related
pages, sources and graph areas); `wiki/query-gaps.md` (failed searches
recorded as future ingest targets); `raw/archive/<YYYY-MM-DD>/` as the
agent-maintained filing destination; connector-backed `libs/<name>/` with a
user-authored `source.yaml`, an agent-owned generated index, and the
`mentioned_in` edge verb used inside those indexes.
### 1.1 — 2026-07-13 · initial schema
The first versioned contract, shipped with the initial commit. Established
the frontmatter field set (`type`, `resource`, `tldr`, `confidence`,
`quality`, `supersedes`/`superseded_by`, `last_updated`,
`freshness_window_days`, `retention`), the core edge verbs (`uses`,
`depends_on`, `caused`, `contradicts`, `supersedes`), the `wiki/` scaffold,
the cascade priority rule, and the recursive index-and-log convention.
There was never a 1.0: versioning began with the first published schema.
---
## Template version history
The tooling layer — `AGENTS.md`/`CLAUDE.md`, the skills, the scripts, the
docs. Independent of the content schema above.
| Version | Date | What landed | Schema |
|---|---|---|---|
| **1.9.0** | 2026-09-22 | Release channels: `main`/`test`/`experimental` branches, branch-aware `ckb-init` and `ckb-upgrade`, `template:` block in `ckb.yaml` | 1.5 |
| 1.8.0 | 2026-09-21 | Seven ideas adopted from Graft: crux/notes/fingerprints, `--quick` lint mode, in-degree ranking, ingest blast radius, completed edge vocabulary | → 1.5 |
| 1.7.0 | 2026-09-20 | Opt-in modules (`.agents/modules/`, `ckb-module`, `ckb.yaml`); the `software` module with `ckb-code-map` and `ckb-spec`; `ckb-reset`; OpenSpec docs | 1.4 |
| 1.6.1 | 2026-09-01 | Fixed false-positive broken-edge findings in the lint graph check | 1.4 |
| 1.6.0 | 2026-09-01 | `ckb-decide`; detection half of lint moved into `lint_report.py`; OKF export moved into `export_okf.py` | → 1.4 |
| 1.3.0 | 2026-08-06 | Rank fusion, dedupe and rerank in `ckb-retrieve`; expertise and ownership lookups | → 1.3 |
| 1.2.1 | 2026-07-29 | `AGENTS.md` compressed — workflows moved out into skills, leaving a small always-loaded rule set | 1.2 |
| 1.2.0 | 2026-07-29 | Project scopes, query gaps, source-verified retrieval | → 1.2 |
| 1.1.0 | 2026-07-20 | Connector-backed `libs/` with self-contained external source indexing (`ckb-index-external`) | 1.1 \* |
| 1.0.0 | 2026-07-17 | First tagged template: full skill set, `LICENSE`, `MANUAL`, bilingual docs | 1.1 |
\* Connector-backed libs shipped as tooling in 1.1.0, but the schema only
recorded them — the `source.yaml` contract, the generated index shape, the
`mentioned_in` verb — at 1.2, nine days later. The two numbers catching up
with each other like this is normal, and is why the schema column shows what
was in effect *after* each template release rather than what the release was
about.
Versions 1.4.0 and 1.5.0 were never published — the template jumped from
1.3.0 to 1.6.0 on 2026-09-01.
---
## Release channels
The template repo keeps three branches, and the version histories above track
`main` only:
| Branch | What it is | Who should track it |
|---|---|---|
| `main` | **Stable** — the released template | Everyone, by default |
| `test` | **Release candidate** — validated before merging to `main` | Anyone helping validate a release |
| `experimental` | **Development** — active work, may be broken or reverted | People developing the template itself |
The branch a KB tracks lives in `ckb.yaml`:
```yaml
template:
repo: https://git.wierzbowa.cloud/michal/ckb.git
branch: main
```
`ckb-init` writes it, `ckb-upgrade` reads it as the default and updates it when
you switch. No `ckb.yaml` and no `template:` block both mean `main`.
One consequence worth knowing: a KB that took tooling from `test` or
`experimental` can sit on a `VERSION` that `main` has not released yet.
Comparing it against `main` then finds nothing newer — which is true, but is
**not** the same as "up to date", and `ckb-upgrade` reports it as "ahead"
rather than as current. Moving such a KB back to `main` is a *downgrade*: it
can remove skills and lower `kb_schema_version` below what local pages are
written against. It requires an explicit confirmation, and is blocked outright
where the schema would drop below the content's own declared contract.
---
## Migrating between versions
Say **"upgrade the wiki"** or **"check for a newer template version"**. To use
a different channel, name it: *"upgrade from the test branch"*, *"check
experimental"*, *"switch back to stable"*. The
`ckb-upgrade` skill checks the canonical template repo, updates the tooling
layer in place, and — separately, and only after you explicitly confirm —
migrates existing `wiki/` content up to the current schema while preserving
every fact already collected.
The two halves are deliberately separate. Taking newer skills never rewrites
your pages, and a content migration is never silent: it reports what it
intends to change, batches anything it had to infer (such as a missing `type`)
for your confirmation, and logs every touched page in `wiki/log.md` marked as
a schema-migration backfill rather than new knowledge.
Because every schema version so far has been additive, an older wiki keeps
working unmigrated. Migration is worth doing to make the newer checks
meaningful — backfilled fingerprints are what give lint anything to verify —
not because anything is broken without it.
---
## Version & License
Current template version: [VERSION](VERSION). Current schema version: the
`kb_schema_version` field in `wiki/index.md`. Licensed under the
[Apache License 2.0](LICENSE).

377
CHANGELOG.pl.md Normal file
View file

@ -0,0 +1,377 @@
# Historia zmian i referencja schematu
*Przeczytaj to w: [English](CHANGELOG.md) | **Polski***
Ten plik śledzi dwie rzeczy: schemat strony dokładnie w takiej postaci, w
jakiej obowiązuje dziś, oraz to, jak projekt doszedł do swoich obecnych
numerów wersji.
Istnieją **dwa niezależne numery wersji** i nie są tym samym:
| Numer | Mieszka w | Opisuje | Kto go podnosi |
|---|---|---|---|
| `kb_schema_version` | frontmatter `wiki/index.md` | **kontrakt treści** — co strona może zawierać i co te pola znaczą | `ckb-upgrade` (przy potwierdzonej migracji), `ckb-module` (addytywnie, przy instalacji) |
| Wersja szablonu | [`VERSION`](VERSION) | **warstwa narzędziowa**`AGENTS.md`, skille, skrypty, dokumentacja | `ckb-upgrade`, gdy pobiera nowszy szablon |
Poruszają się niezależnie i jest to celowe. Możesz wziąć nowszy zestaw skilli
bez dotykania choćby jednej strony wiki, a wiki napisane pod starszy schemat
nadal działa — po to właśnie jest wersja schematu.
**Polityka wersjonowania.** Podnoś wersję **pomniejszą** przy zmianie
addytywnej: nowe pole opcjonalne, nowa opcjonalna sekcja treści, nowy
opcjonalny plik szkieletu. Podnoś wersję **główną** przy zmianie łamiącej
kompatybilność: zmiana lub usunięcie pola wymaganego albo zmiana istniejącej
zastrzeżonej konwencji nazw plików. Nigdy nie było podniesienia wersji
głównej — każda dotychczasowa wersja schematu była addytywna, więc dowolna
strona napisana od 2026-07-13 jest dziś nadal poprawna.
---
## Spis treści
1. [Aktualny schemat strony (1.5)](#aktualny-schemat-strony-kb_schema_version-15)
2. [Historia wersji schematu KB](#historia-wersji-schematu-kb)
3. [Historia wersji szablonu](#historia-wersji-szablonu)
4. [Migracja między wersjami](#migracja-między-wersjami)
---
## Aktualny schemat strony (`kb_schema_version: "1.5"`)
### Frontmatter — każda strona niezastrzeżona
Każdy plik `.md` w `wiki/` poza zastrzeżonymi (`index.md`, `log.md`) niesie
frontmatter YAML. Wymagane jest wyłącznie `type`; reszta jest opcjonalna i
preferowana tam, gdzie ma sens. Parser jest celowo prosty — wyłącznie płaskie
pary `klucz: wartość`, bez zagnieżdżeń.
| Pole | Wymagane | Znaczenie |
|---|---|---|
| `type` | **tak** | Otwarty ciąg: `person`, `project`, `concept`, `library`, `decision`, `playbook`, `repository`, `component`, … Nowe wartości są zawsze poprawne; czytelnicy tolerują nierozpoznane. |
| `resource` | nie | Kanoniczny URI autorytatywnego źródła zewnętrznego, które opisuje ta strona, trzymany oddzielnie od własnego komentarza wiki. |
| `tldr` | nie | Jednozdaniowe streszczenie zoptymalizowane pod odczyt przez LLM. To ono trafia do indeksu i decyduje, czy strona w ogóle zostanie otwarta. |
| `confidence` | nie | `0.0``1.0`. Potwierdzenie przez źródła. Ustawiane przy zapisie, zanika, jeśli nic go nie wzmacnia, rośnie przy nowym zgodnym źródle. |
| `quality` | nie | `0.0``1.0`. Samoocena struktury i cytowań samej strony. Poniżej `0.7` oflagowane do przeglądu. |
| `supersedes` | nie | Ścieżka do starszej strony, którą ta zastępuje. |
| `superseded_by` | nie | Ścieżka do nowszej strony, która zastąpiła tę. **Zawsze ustawiaj obie strony.** |
| `last_updated` | nie | `YYYY-MM-DD`. Kiedy zmieniła się *strona*. |
| `freshness_window_days` | nie | Liczba dni, po której lint oznacza stronę jako nieaktualną. Typowo: 90 dla strony wiki, 365 dla decyzji, 30 dla dokumentu z indeksu konektora. |
| `retention` | nie | `high` / `medium` / `low`. Strona `low` jest archiwizowana (nigdy usuwana) po 2× swoim oknie świeżości. |
| `source_fingerprint` | nie | *(1.5)* Skrót źródła, z którego zbudowano stronę — `sha256:<8 hex>` dla pliku lokalnego albo `etag:<wartość>` / `mtime:<iso8601>` dla elementu z konektora. |
| `source_checked` | nie | *(1.5)* `YYYY-MM-DD` — kiedy ten skrót był ostatnio zweryfikowany. To co innego niż `last_updated`: ponowne sprawdzenie, które nie wykryło zmiany, przesuwa to pole i zostawia `last_updated` w spokoju. |
Wyłącznie `wiki/index.md` niesie `kb_schema_version`. To deklaracja na
poziomie całego zbioru, nie pojedynczej strony — same strony nigdy jej nie
niosą.
### Frontmatter — strony decyzji
Strony z `type: decision` mieszkają w `wiki/decisions/` jako `NNNN-slug.md` i
dodają:
| Pole | Wymagane | Znaczenie |
|---|---|---|
| `status` | **tak** | `proposed` / `accepted` / `rejected` / `superseded` / `reversed`. Słownik zdefiniowany w `wiki/decisions/index.md` i walidowany przez lint. |
| `decided_on` | dla `accepted`/`rejected`/`reversed` | `YYYY-MM-DD`, data podjęcia decyzji. |
| `decided_by` | gdy wiadomo | Nazwiska po przecinku. Gdy naprawdę nie wiadomo, wpisz `unknown` zamiast pomijać pole — „nie wiemy, kto to zdecydował" samo w sobie warto zapisać. |
| `affects` | nie | Ścieżki wiki po przecinku, które ta decyzja ogranicza. |
| `review_on` | nie | `YYYY-MM-DD` do ponownego rozważenia. Lint zgłasza je po przekroczeniu daty. |
Rekordy decyzji są **tylko do dopisywania**. Decyzji nigdy nie przepisuje się
pod późniejszą zmianę zdania: zapisz nową, która ją zastępuje, a obie zostają
na wokandzie.
### Zastrzeżone sekcje treści
*(Nowość w 1.5.)* Cztery nagłówki `##` znaczą to samo na każdej stronie w
każdej warstwie kaskady. Wszystkie są opcjonalne; tam, gdzie występują,
znaczą dokładnie to i nic innego.
#### `## Sources`
Skąd wzięła się strona. Po jednym punkcie na źródło, każdy ze skrótem:
```markdown
## Sources
- `raw/archive/2026-09-21/kickoff-notes.md` — sha256:3f9a2c1e (checked 2026-09-21)
```
Okno świeżości to przypuszczenie, że źródło *mogło* się zmienić. Skrót to
fakt, czy *się zmieniło*. Lint przelicza skróty lokalne i oznacza to, co
faktycznie się zmieniło — a to inne i pilniejsze znalezisko niż strona, która
jedynie się zestarzała.
#### `## Crux`
Dosłowne fragmenty tych źródeł — dowód, nigdy parafraza — przypisane do
punktu źródła, z którego pochodzą:
```markdown
## Crux
> FDEs need the VDI *and* a Jira account before day one; the VDI request
> alone takes ten working days.
`raw/archive/2026-09-21/kickoff-notes.md`, sekcja „Access"
```
Roboczy zakres to od trzech do dziesięciu linijek. Crux zbliżający się
długością do streszczenia nad nim przestał być dowodem i stał się drugą kopią
źródła. Z cytowania zamiast parafrazowania wynikają dwie rzeczy: na pytanie
często da się odpowiedzieć ze strony zamiast z archiwum, a odpływ od źródła
staje się widoczny — streszczenie może po cichu oddalić się od źródła, cytat
albo wciąż się zgadza, albo nie.
Strona bez cytowalnego źródła po prostu nie ma `## Crux`. Pusty albo
sparafrazowany jest gorszy niż żaden, bo wygląda jak dowód.
#### `## Notes`
Pisane przez człowieka i **chronione**. Żaden skill nie może tej sekcji
nadpisać, przeformatować, streścić ani usunąć; regeneracja zachowuje ją co do
bajtu.
```markdown
## Notes
<!-- Twoje. Żaden skill tego nie nadpisuje. -->
```
Ma to największe znaczenie na stronach, które agent *regeneruje* — indeksy
konektorów, mapy kodu — gdzie cała reszta jest odrzucana i budowana od nowa
przy kolejnym przebiegu. To jedyne miejsce, w którym adnotacja przetrwa.
Strony decyzji celowo nie mają `## Notes`: nic ich nie regeneruje, a rekord
tylko-do-dopisywania ze swobodnie edytowalnym blokiem adnotacji zaprasza
dokładnie do tej wstecznej korekty, której zasada append-only ma zapobiegać.
### Słownik krawędzi
Relacje mieszkają w `wiki/graph/edges.json` (oraz we własnym
`graph/edges.json` każdego indeksu konektora). Słownik jest zamknięty, a każdy
czasownik zdefiniowany przez pytanie, na jakie odpowiada — jeśli proponowana
krawędź nie odpowiada na żadne z nich, jej miejsce jest w tekście strony.
| Czasownik | Pytanie, na jakie odpowiada | Od |
|---|---|---|
| `part_of` | Gdzie to mieszka? Czego jest częścią? | 1.5 |
| `uses` | Po co to sięga w czasie działania? | 1.1 |
| `depends_on` | Co się zepsuje, jeśli to zmienię? | 1.1 |
| `produces` | Skąd bierze się ten wynik? | 1.5 |
| `configures` | Co zmienia zachowanie tej rzeczy? | 1.5 |
| `validates` | Co to sprawdza, testuje albo ocenia? | 1.5 |
| `implements` | Jakiego kontraktu to musi dotrzymać? | 1.5 |
| `caused` | Dlaczego to się stało? | 1.1 |
| `contradicts` | Co jest z tym sprzeczne i nierozstrzygnięte? | 1.1 |
| `supersedes` | Co to zastąpiło i co ono zastąpiło? | 1.1 |
| `decided_by` | Kto podjął tę decyzję? | 1.4 |
| `affects` | Co ta decyzja ogranicza? | 1.4 |
| `has_expertise_in` | Kto potrafi odpowiedzieć na pytania o to? | 1.3 |
| `owns` | Kto za to odpowiada? | 1.3 |
| `mentioned_in` | Który dokument źródłowy o tym mówi? | 1.2 (tylko indeksy libs) |
Zapisuj po jednym kierunku na relację — `part_of`, `supersedes`,
`depends_on` i `uses` są kanoniczne, a odwrotność nie jest przechowywana jako
druga krawędź. Zapisuj krawędzie wyłącznie na podstawie wykazanych dowodów;
brak krawędzi jest lepszy niż krawędź zmyślona, a napompowany graf pogarsza
wyszukiwanie zamiast je poprawiać (stopień wejściowy jest sygnałem
rankingowym).
### Zastrzeżony szkielet
| Ścieżka | Od | Do czego służy |
|---|---|---|
| `wiki/index.md` | 1.1 | Tabela routingu; jedyna strona niosąca `kb_schema_version` |
| `wiki/overview.md` | 1.1 | Mapa KB z lotu ptaka |
| `wiki/log.md` | 1.1 | Dziennik zmian `wiki/` w odwrotnej chronologii |
| `wiki/error-book.md` | 1.1 | Problemy systemowe wraz z przyczyną i naprawą |
| `wiki/entities/` | 1.1 | Typowane strony encji |
| `wiki/graph/` | 1.1 | `edges.json` plus słownik w `index.md` |
| `wiki/query-gaps.md` | 1.2 | Pytania, na które wiki nie umiało odpowiedzieć, napędzające ingest sterowany popytem |
| `wiki/projects/` | 1.2 | Opcjonalne lokalne zakresy zapytań |
| `wiki/decisions/` | 1.4 | Numerowane rekordy decyzji tylko-do-dopisywania, z własnym `log.md` |
Każdy podkatalog grupujący strony niesie własny `index.md`, dzięki czemu
nawigacja pozostaje leniwa.
---
## Historia wersji schematu KB
### 1.5 — 2026-09-21 · dowody, skróty źródeł i dokończony słownik krawędzi
Zaadaptowane z analizy [trailhq/Graft](https://github.com/trailhq/Graft),
warstwy kontekstu dla agentów kodujących, która utrzymuje wyprowadzony graf
kodu w zgodzie ze źródłem przez skróty treści, a nie daty, i chroni blok
pisany przez użytkownika na każdym regenerowanym węźle. Magazyn Graftu jest
jednorazowy i odtwarzalny; ten nie jest, więc większość jego projektu się nie
przenosi — ale kilka mechanizmów tak, a dwa z nich zamknęły tu realne luki.
**Dodano:**
- **`## Crux`** — dosłowne fragmenty źródeł obok syntezy. Pozwala
`ckb-retrieve` ugruntować odpowiedź bez powrotu do archiwum (gdy skrót się
wciąż zgadza) i czyni odpływ od źródła wykrywalnym.
- **`## Notes`** — pisane przez człowieka i chronione wszędzie. To zamknęło
realną lukę: `ckb-index-external` regeneruje strony konektora w całości,
więc napisana tam adnotacja ginęła dotąd przy kolejnym odświeżeniu.
- **`## Sources`** — sformalizowane jako sekcja zastrzeżona z jednym
punktem ze skrótem na źródło. Wcześniej nieformalna konwencja, na której
`ckb-retrieve` polegało, ale której żaden skill faktycznie nie określał.
- **`source_fingerprint` / `source_checked`** we frontmatterze.
- **Czasowniki krawędzi** `part_of`, `produces`, `configures`, `validates`,
`implements`. `part_of` naprawiło żywą niespójność: `ckb-code-map`
zapisywał go od szablonu 1.7.0, a schemat nigdy go nie deklarował.
**Zmieniono:**
- `wiki/graph/index.md` przepisany jako tabela pytanie-na-czasownik wraz z
konwencjami dotyczącymi kierunku krawędzi i dowodów.
- `ckb-ingest` dostał krok promienia rażenia: przed zapisem przejdź graf
wstecz od dotkniętych encji, żeby ustalić, co nadchodzący materiał
potwierdza, rozszerza albo z czym jest sprzeczny, i wskaż właścicieli
dotkniętych stron. Ingest był dotąd przede wszystkim addytywny, a tak
właśnie wiki gromadzi dwie strony, które po cichu się ze sobą nie zgadzają.
- `ckb-retrieve` wtapia stopień wejściowy grafu jako jedną z rankowanych list,
z wagą poniżej 1.0 — centralność jest przesłanką, nie dowodem.
- Reguła E (start sesji) uruchamia teraz `lint_report.py --quick` obok
`git status`: deterministyczny jednoliniowy sygnał gnicia wiedzy, niekosztujący
żadnych tokenów modelu.
- Lint zyskał kontrole 12 (odpływ skrótu źródła), 13 (dosłowność sekcji Crux)
i 14 (zasada chronionego `## Notes`).
**Kompatybilność:** w pełni addytywna. Strona 1.4 bez żadnej z nowych sekcji
i pól jest poprawną stroną 1.5. Migracja nigdy nie wytworzy `## Crux`
zmyślanie cytatów to dokładnie ta porażka, której ta sekcja ma zapobiegać.
### 1.4 — 2026-09-01 · rekordy decyzji
**Dodano:** strony `type: decision` w `wiki/decisions/` jako `NNNN-slug.md`,
z `status`, `decided_on`, `decided_by`, `affects`, `review_on`; czasowniki
krawędzi `decided_by` i `affects`; `wiki/decisions/index.md` wraz z własnym
`log.md`; zasadę append-only. Odpowiada na „dlaczego jest tak, jak jest",
„kto zdecydował" i „co zmieniło tę decyzję" bezpośrednim wyszukaniem zamiast
zgadywania pełnotekstowego. Właścicielem jest skill `ckb-decide`.
### 1.3 — 2026-08-06 · krawędzie osobatemat
**Dodano:** czasowniki krawędzi `has_expertise_in` i `owns`, dzięki którym
„kto wie o X" i „kto jest właścicielem X" to wyszukanie w grafie, a nie
przeszukiwanie pełnotekstowe. Zapisywane wyłącznie na podstawie wykazanych
dowodów — obecność na spotkaniu to nie ekspertyza, a stanowisko to nie
własność.
### 1.2 — 2026-07-29 · zakresy zapytań, luki i libs oparte na konektorach
**Dodano:** `wiki/projects/` (opcjonalne lokalne zakresy zapytań grupujące
powiązane strony, źródła i obszary grafu); `wiki/query-gaps.md` (nieudane
wyszukiwania zapisane jako przyszłe cele ingestu);
`raw/archive/<YYYY-MM-DD>/` jako utrzymywane przez agenta miejsce
archiwizacji; `libs/<name>/` oparte na konektorach, z pisanym przez
użytkownika `source.yaml`, należącym do agenta generowanym indeksem oraz
czasownikiem krawędzi `mentioned_in` używanym wewnątrz tych indeksów.
### 1.1 — 2026-07-13 · pierwszy schemat
Pierwszy wersjonowany kontrakt, wydany wraz z pierwszym commitem. Ustanowił
zestaw pól frontmatteru (`type`, `resource`, `tldr`, `confidence`, `quality`,
`supersedes`/`superseded_by`, `last_updated`, `freshness_window_days`,
`retention`), podstawowe czasowniki krawędzi (`uses`, `depends_on`, `caused`,
`contradicts`, `supersedes`), szkielet `wiki/`, zasadę priorytetu kaskady i
rekurencyjną konwencję indeksu i dziennika.
Wersji 1.0 nigdy nie było: wersjonowanie zaczęło się od pierwszego
opublikowanego schematu.
---
## Historia wersji szablonu
Warstwa narzędziowa — `AGENTS.md`/`CLAUDE.md`, skille, skrypty, dokumentacja.
Niezależna od schematu treści powyżej.
| Wersja | Data | Co weszło | Schemat |
|---|---|---|---|
| **1.9.0** | 2026-09-22 | Kanały wydawnicze: gałęzie `main`/`test`/`experimental`, świadome gałęzi `ckb-init` i `ckb-upgrade`, blok `template:` w `ckb.yaml` | 1.5 |
| 1.8.0 | 2026-09-21 | Siedem pomysłów zaadaptowanych z Graftu: crux/notes/skróty źródeł, tryb `--quick` lintu, ranking po stopniu wejściowym, promień rażenia w ingeście, dokończony słownik krawędzi | → 1.5 |
| 1.7.0 | 2026-09-20 | Opcjonalne moduły (`.agents/modules/`, `ckb-module`, `ckb.yaml`); moduł `software` z `ckb-code-map` i `ckb-spec`; `ckb-reset`; dokumentacja OpenSpec | 1.4 |
| 1.6.1 | 2026-09-01 | Naprawa fałszywie dodatnich znalezisk uszkodzonych krawędzi w kontroli grafu | 1.4 |
| 1.6.0 | 2026-09-01 | `ckb-decide`; wykrywająca połowa lintu przeniesiona do `lint_report.py`; eksport OKF przeniesiony do `export_okf.py` | → 1.4 |
| 1.3.0 | 2026-08-06 | Fuzja rankingów, deduplikacja i ponowny ranking w `ckb-retrieve`; wyszukiwanie ekspertyzy i własności | → 1.3 |
| 1.2.1 | 2026-07-29 | `AGENTS.md` skompresowany — przepływy przeniesione do skilli, zostawiając mały, zawsze ładowany zestaw reguł | 1.2 |
| 1.2.0 | 2026-07-29 | Zakresy projektów, luki zapytań, wyszukiwanie weryfikowane względem źródła | → 1.2 |
| 1.1.0 | 2026-07-20 | `libs/` oparte na konektorach z samodzielnym indeksowaniem źródeł zewnętrznych (`ckb-index-external`) | 1.1 \* |
| 1.0.0 | 2026-07-17 | Pierwszy otagowany szablon: pełny zestaw skilli, `LICENSE`, `MANUAL`, dokumentacja dwujęzyczna | 1.1 |
\* `libs/` oparte na konektorach weszły jako narzędzia w 1.1.0, ale schemat
zapisał je — kontrakt `source.yaml`, kształt generowanego indeksu, czasownik
`mentioned_in` — dopiero w 1.2, dziewięć dni później. Takie doganianie się
tych dwóch numerów jest normalne i dlatego kolumna schematu pokazuje, co
obowiązywało *po* danym wydaniu szablonu, a nie czego to wydanie dotyczyło.
Wersje 1.4.0 i 1.5.0 nigdy nie zostały opublikowane — szablon przeskoczył z
1.3.0 na 1.6.0 dnia 2026-09-01.
---
## Kanały wydawnicze
Repozytorium szablonu utrzymuje trzy gałęzie, a powyższe historie wersji
śledzą wyłącznie `main`:
| Gałąź | Czym jest | Kto powinien ją śledzić |
|---|---|---|
| `main` | **Stabilna** — wydany szablon | Wszyscy, domyślnie |
| `test` | **Kandydat do wydania** — walidowany przed scaleniem do `main` | Każdy, kto pomaga walidować wydanie |
| `experimental` | **Rozwojowa** — bieżąca praca, może być zepsuta albo wycofana | Osoby rozwijające sam szablon |
Śledzona przez KB gałąź mieszka w `ckb.yaml`:
```yaml
template:
repo: https://git.wierzbowa.cloud/michal/ckb.git
branch: main
```
`ckb-init` ją zapisuje, `ckb-upgrade` czyta ją jako domyślną i aktualizuje przy
przełączeniu. Brak `ckb.yaml` i brak bloku `template:` oznaczają `main`.
Jedna konsekwencja warta poznania: KB, która wzięła narzędzia z `test` albo
`experimental`, może mieć `VERSION` wyższą niż to, co `main` w ogóle wydało.
Porównanie z `main` nie znajdzie wtedy nic nowszego — co jest prawdą, ale
**nie** znaczy „aktualne", i `ckb-upgrade` raportuje to jako „wyprzedza", a nie
jako bieżące. Cofnięcie takiej KB na `main` to *downgrade*: może usunąć skille
i obniżyć `kb_schema_version` poniżej tego, pod co napisano lokalne strony.
Wymaga wyraźnego potwierdzenia, a gdy schemat spadłby poniżej kontraktu
zadeklarowanego przez treść — jest blokowane.
---
## Migracja między wersjami
Powiedz **„upgrade the wiki"** albo **„check for a newer template version"**.
Aby użyć innego kanału, nazwij go: *„upgrade from the test branch"*, *„check
experimental"*, *„switch back to stable"*.
Skill `ckb-upgrade` sprawdza kanoniczne repozytorium szablonu, aktualizuje
warstwę narzędziową w miejscu i — osobno, i wyłącznie po twoim wyraźnym
potwierdzeniu — migruje istniejącą treść `wiki/` do bieżącego schematu,
zachowując każdy już zebrany fakt.
Te dwie połowy są celowo rozdzielone. Wzięcie nowszych skilli nigdy nie
przepisuje twoich stron, a migracja treści nigdy nie jest cicha: zgłasza, co
zamierza zmienić, zbiera wszystko, co musiała wywnioskować (na przykład
brakujące `type`), do twojego potwierdzenia i loguje każdą dotkniętą stronę w
`wiki/log.md` z adnotacją, że to uzupełnienie migracyjne schematu, a nie nowa
wiedza.
Ponieważ każda dotychczasowa wersja schematu była addytywna, starsze wiki
działa bez migracji. Migrację warto zrobić po to, żeby nowsze kontrole miały
sens — uzupełnione skróty źródeł dają lintowi cokolwiek do weryfikacji — a
nie dlatego, że bez niej coś jest zepsute.
---
## Wersja i licencja
Aktualna wersja szablonu: [VERSION](VERSION). Aktualna wersja schematu: pole
`kb_schema_version` w `wiki/index.md`. Licencja:
[Apache License 2.0](LICENSE).

294
MANUAL.md
View file

@ -6,8 +6,9 @@ This is the human-facing manual for working with a Cascade Knowledge Base
(this repo). It's written for the *person* using the wiki, not the agent —
for the agent's own operating rules, see [AGENTS.md](AGENTS.md) /
[CLAUDE.md](CLAUDE.md). For a feature-by-feature technical overview, see
[README.md](README.md). This document is task-oriented: "I want to do X —
what do I say, and what happens?"
[README.md](README.md). For the page schema in full and the version
histories behind it, see [CHANGELOG.md](CHANGELOG.md). This document is
task-oriented: "I want to do X — what do I say, and what happens?"
Everywhere below, "say" means typing it to whatever AI coding agent you're
using against this repo (Claude Code, or another agent that reads
@ -48,6 +49,14 @@ the target folder. It never copies this project's actual content (no
entities, no graph data, no notes). You get a fresh, empty KB, ready for its
first `raw/inbox/` drop. See `.agents/skills/ckb-init/SKILL.md`.
The template can come from two places: this repo's own files, or a fresh
shallow clone of the canonical template repo (or any fork/mirror URL you
name) pulled into a scratch folder. Say "pull the latest template and set
up a KB in \<folder\>" — or run it from outside any KB at all — and the
agent clones first, then builds the scaffold from that. The clone is
scratch only: the new KB gets its own git history (the agent asks before
running `git init`), not the template's.
If the target folder already looks like a KB (it has a `wiki/` or
`AGENTS.md` already), the agent will stop and ask before touching anything
— it won't silently overwrite an existing knowledge base.
@ -92,7 +101,11 @@ which stay entirely read-only. There are three ways to mount one:
- **You don't have to build the index yourself.** `source.yaml` can add
an `index:` block pointing at an already-built index — a git repo, or
a shared resource — so you just fetch what someone else already
indexed instead of scanning the live source yourself.
indexed instead of scanning the live source yourself. Every run checks
that location first: if there's already an index there, you get it; if
there isn't yet (the normal state before anyone's run this with write
access), that's not an error — whoever has write access is the one
whose next run creates and publishes it there for the first time.
- **Building/refreshing is opt-in per person, per source.** By default,
everyone is read-only for a connector source — nobody's agent will
scan the live connector on their behalf unless they've explicitly said
@ -101,6 +114,13 @@ which stay entirely read-only. There are three ways to mount one:
committed, never seen by anyone else). This is deliberate: it lets one
or two people maintain a source for the whole team instead of everyone
redundantly re-scanning it.
- **You can set how often it should be refreshed.** Add an optional
`refresh_interval_days: 7` to `source.yaml` (it defaults to 30). A
folder that churns daily wants a shorter window than a quarterly
archive nobody touches. Both "index external sources" and "Lint" then
tell you when a source is overdue and by how much — which matters most
if you're read-only for it, since knowing *which* source has gone
stale is what lets you go ask the person who maintains it.
Either way, once it's mounted, just ask questions normally — the agent
checks your local `wiki/` first, then walks `linked/`, then `libs/`, and
@ -139,6 +159,17 @@ Example:
> reminding you to review the result and say "sync changes" once you're
> happy with it.
For a long transcript, the agent doesn't just write one summary page. It
pulls out the searchable question, the summary, the resolution, the systems
and people involved — and promotes individual passages to their own
findable sections when they'd otherwise be lost inside a summary. That last
part has a deliberate bar: a passage has to contain a genuinely specific
term (a flag, an error string, a clause, a version), run to a couple of
sentences at least, and be corroborated by something later in the material.
Otherwise it stays folded into the summary. Without that bar every
paragraph looks quotable and the wiki page ends up being the transcript
again, which defeats the point of ingesting it.
If `raw/inbox/` is empty, the agent scans `raw/` directly instead (still
skipping `raw/archive/`, which is already-processed history).
@ -168,6 +199,59 @@ ingest incorporates it. This keeps the wiki demand-driven: it grows around
what you actually ask, not everything that could theoretically be written
down.
Durable misses can also be tracked in `wiki/query-gaps.md`. A good gap
entry is tiny: the question, where the agent looked, and the smallest
source or page that would make the answer available next time.
### D. Record a decision
When a call gets made — a technology choice, a process change, a policy —
say:
> "Record a decision: we're moving billing to Postgres. Alice and Bob
> decided it today, because the reporting queries were killing MySQL."
The agent writes a numbered record under `wiki/decisions/` with the decision,
who decided, when, the rationale, the alternatives, and what it affects. If
it replaces an earlier decision, it links the two in both directions and
marks the old one superseded — without touching the old page's reasoning.
Anything you leave out, it asks for in one round; if you're mid-flow, say so
and it records what you gave it and tells you which fields it left open.
Then ask about them however you like:
> "What did we decide about the billing database?"
> "Why do we use Postgres?"
> "Who decided that, and when?"
> "Which decisions are still just proposed?"
> "What superseded decision 3?"
The answer always comes with who and when attached, and says plainly when a
decision is proposed rather than accepted, or has since been superseded —
so you don't act on something that isn't in force. Implemented by the
`ckb-decide` skill.
Two things worth knowing:
- **Decisions are append-only.** "Actually, we changed our mind" creates a
*new* decision that supersedes the old one; it never edits the old one's
reasoning. That's deliberate — the history is the point. Genuine
transcription errors ("I said Alice, it was Anna") do get fixed in place.
- **A proposal is not a decision.** If it wasn't actually settled, it's
recorded as `proposed` with no decision date, and shows up when you ask
what's still open.
### E. Create a local project scope
When a topic, client, system, or initiative comes up often, ask:
> "Create a project scope for payments integration."
The agent creates or updates a plain Markdown page under `wiki/projects/`
listing the pages, entities, raw/archive files, connector indexes, and graph
areas that should be searched first for that scope. You still keep one
local wiki; this just gives repeated questions a better starting area.
---
## 3. Keeping it tidy
@ -187,13 +271,87 @@ This runs a health check across the whole wiki:
- orphaned pages (nothing links to them) get backlinked or archived
- broken graph edges get fixed or removed
- missing/duplicate index and log entries get corrected
- connector-backed sources whose index is overdue for a refresh get
flagged, with how overdue they are — useful even if you're read-only for
that source, since it tells you who to chase
- **pages whose source has actually changed** get flagged — see below
- **quotes that no longer appear in the source they cite** get flagged
- recurring systemic issues get written into `wiki/error-book.md`
It fixes what it safely can on its own, and reports the rest for you to
decide. Like Ingest, it finishes by reminding you to review and sync.
The last two are worth understanding, because they're the difference
between "this page is old" and "this page is wrong".
Every page records a fingerprint of the material it was built from. Staleness
by date is a guess: a page written a year ago may still be perfectly accurate.
A fingerprint is not a guess — the agent recomputes it and either the source
is byte-for-byte what the page was written against, or somebody edited it.
When a source changes, the page built on it goes to the top of the list,
ahead of anything that has merely aged.
Pages also quote their sources directly, in a `## Crux` section — a few
verbatim lines carrying the actual claim, underneath the agent's summary of
it. Two things follow from that. When you ask a question, the agent can often
answer from the quote instead of re-reading the whole source, and show you the
words rather than its paraphrase of them. And when a quote stops matching its
source, that's a page asserting, in quotation marks, something its evidence no
longer says — the strongest finding lint produces, and the agent will never
"fix" it by quietly editing the quote to match.
The detection half runs as a read-only Python script
(`scripts/lint_report.py`), so the same wiki always produces the same
findings list — the agent reads that report and then does the parts that
need judgment (supersession, ambiguous orphans, error-book entries, and
deciding what to fix versus what to hand back to you). It fixes what it
safely can on its own, and reports the rest for you to decide. Like Ingest, it finishes by reminding you to review and sync.
Implemented by the `ckb-lint` skill —
`.agents/skills/ckb-lint/SKILL.md`.
### Starting over: resetting to a clean template
Sometimes you want the *shape* of a knowledge base without its contents —
typically because this repo doubles as the template you hand to other
people, and it has accumulated decisions, workload summaries and entity
pages that shouldn't ship with it.
> "Reset the wiki." / "Make this a clean template."
This is the one command in this repo that **deletes knowledge on purpose**,
so it is built to be hard to trigger by accident:
1. **It looks for a restore point first.** If your working tree is dirty it
stops and offers to commit, because after a reset anything committed is a
`git checkout` away and anything uncommitted is simply gone. It can also
tag the commit (`pre-reset-<date>`) so you don't have to keep a hash in
your head.
2. **It asks how far to go.** Six tiers, chosen individually: wiki knowledge,
workload history, `raw/` source material, `outputs/`, external sources,
and installed modules. Only the first is on by default. `libs/`,
`linked/` and modules default to *no*`linked/` holds symlinks into
other people's knowledge bases, and it will remove a link but never
follow one.
3. **It counts before it asks.** You get an inventory — how many pages, how
many decision records (listed by number and title), how many graph edges,
plus anything marked `retention: high` — and one line on what survives.
4. **It wants a typed phrase**, not a "yes". And if you change the scope in
your reply, it re-counts and asks again, because you agreed to a number
and the number moved.
5. **It verifies afterwards**, running lint before telling you it worked.
What it restores is exactly what `ckb-init` would create: same directories,
same scaffold files, same `kb_schema_version`. Emptying the content doesn't
roll back the schema.
What it never touches, with or without confirmation: the template layer
(`AGENTS.md`, `.agents/`, `LICENSE`, `VERSION`, the docs) and `src/`, which
holds independent code repositories this command has no business deleting.
One deliberate quirk: unlike every other skill, this one does **not** write a
`workload/` session note — that entry would be the first thing in a directory
it has just emptied. It tells you so in its report.
Implemented by the `ckb-reset` skill —
`.agents/skills/ckb-reset/SKILL.md`.
---
## 4. Syncing — with yourself, and with other people
@ -277,6 +435,37 @@ Say:
> "Upgrade the wiki." / "Check for a newer template version."
### Which channel you're pulling from
The template repo keeps three branches, and by default you get the stable
one:
| Branch | What it is | Who should be on it |
|---|---|---|
| `main` | **Stable** — the released template | You, unless you have a reason not to be |
| `test` | **Release candidate** — validated before it reaches `main` | You're helping validate a release, or you need a fix that's landed but not shipped |
| `experimental` | **Development** — active work, may be broken or reverted | You're developing the template itself |
To use a different one, just say which:
> "Upgrade from the test branch." / "Check experimental for updates." /
> "Switch this KB back to the stable channel."
Whichever you pick sticks — it's recorded in `ckb.yaml`, so the next upgrade
stays on the same channel rather than quietly pulling you back to `main`. The
same goes at creation time: *"initialize from the experimental branch"*.
One thing to watch. If you're tracking `test` or `experimental`, your KB can
sit on a version `main` hasn't released yet. Checking against `main` then
finds nothing newer — the agent will tell you you're **ahead**, not that
you're up to date, because those are different situations. Going back to
`main` from there is a *downgrade*: it can remove skills and roll the schema
back below what your pages are written against. You'll be asked to confirm
explicitly, and it's refused outright if your content would stop conforming
to its own declared schema.
### What gets checked
Two entirely separate things get checked, and either, both, or neither
might turn something up:
@ -342,11 +531,43 @@ Just ask, in plain language:
> "What do we know about the Q3 migration risk?"
The agent reads `wiki/index.md` first to find relevant pages, checks their
one-line `tldr` before loading the full page, walks the knowledge graph for
connected facts, and falls back to `linked/`/`libs/` if the local wiki has
nothing. You get an answer grounded in what's actually written down, not a
guess.
The agent reads `wiki/index.md` first to find relevant pages. If a matching
project scope exists under `wiki/projects/`, it searches that scope first.
Then it checks one-line `tldr` fields, runs exact local search for literal
tokens when needed, expands context around matching sections, walks the
knowledge graph for connected facts, and falls back to `linked/`/`libs/` if
the local wiki has nothing. You get an answer grounded in what's actually
written down, not a guess.
Two things about that worth knowing as a user:
- **It searches `raw/inbox/` too.** Something you dropped in this morning
and haven't ingested yet can still answer your question. The agent will
tell you when an answer came from un-ingested material, which doubles as
a nudge that "Ingest" is overdue.
- **Answers carry their own caveats.** If the page behind an answer is past
its freshness window, scored low on confidence, or was read from a cached
connector index instead of a live check, the answer says so next to the
claim. If two pages disagree and neither has been marked superseded yet,
you'll hear about that too. The point is that you never have to go read
the frontmatter yourself to know how much to trust what you just got.
### Ask who knows something
> "Who knows about the checkpoint restore path?" / "Who owns the billing
> service?"
These are answered from the knowledge graph directly rather than by
keyword-searching for names. Ingest records an expertise or ownership edge
when the source material actually shows someone answering questions on a
topic or holding declared responsibility for it — not from having attended
a meeting or from a job title. If nobody has a recorded edge yet, the agent
falls back to who the archived sources show answering that kind of question
and tells you it's inferring rather than reporting.
When there is still no answer, the agent should tell you what is missing
and either add/propose a short entry in `wiki/query-gaps.md` or suggest the
smallest source to drop into `raw/inbox/`.
### Learn from the wiki
@ -357,7 +578,7 @@ guess.
You'll be asked how many questions and what format (open / multiple
choice), then run through them one at a time with immediate feedback and a
running score. Nothing is saved afterward — it's a one-off check.
`.agents/skills/cbk-quiz/SKILL.md`.
`.agents/skills/ckb-quiz/SKILL.md`.
**A proper course, spread over time** — say:
@ -446,6 +667,7 @@ Example:
> connector: sharepoint
> location: "https://contoso.sharepoint.com/sites/Finance/Shared Documents/Reports"
> description: "Finance team's shared reports folder"
> refresh_interval_days: 7
> ```
> *then say "Index external sources."*
>
@ -492,14 +714,22 @@ adds an `index:` block to the shared `source.yaml`:
index:
store: git
location: "https://github.com/finance-team/index-cache.git"
# ref: main — optional: pin a branch, tag, or subpath within that store
```
Now, whenever *anyone* says "index external sources," the agent first
fetches whatever's already published there — read-only users stop right
there; the admin also rebuilds from the live connector and pushes the
refreshed index back to that same location, so the next person's fetch
picks it up. Leave the `index:` block out entirely (the simplest setup,
and the right default for a single small team) and the index just lives
The very first time anyone runs "index external sources" after that block
is added, `https://github.com/finance-team/index-cache.git` is empty —
that's expected, not an error. Every run checks it first: read-only users
just see "nothing published yet, ask the admin"; the admin's run is what
actually creates it there, since a write-access run always rebuilds from
the live connector and pushes the result to that location, whether or not
anything was there before. From then on, whenever *anyone* says "index
external sources," the agent first fetches whatever's already published
there — read-only users stop right there; the admin also rebuilds from the
live connector and pushes the refreshed index back to that same location,
so the next person's fetch picks it up. Leave the `index:` block out
entirely (the simplest setup, and the right default for a single small
team) and the index just lives
directly inside `libs/finance-reports/` in this KB's own repo, shared the
normal way via "sync changes" — exactly like the plain example above.
@ -515,15 +745,32 @@ graph) — not that you're forbidden from touching it. If you hand-edit a
wiki page, it's good practice to run "Lint" afterward so the index/log/
graph stay in sync with what you changed.
There is one exception that runs the other way. On any page the agent
*regenerates* — a connector index, a code map — everything you write is
normally lost on the next rebuild. So every such page ends with a `## Notes`
section that no skill will ever touch:
```markdown
## Notes
<!-- Yours. Never rewritten by any skill. -->
```
Write whatever you want there — that this document is obsolete, that the
person named in it has left, who to actually ask. It is carried across
rebuilds byte-for-byte. Anything you write *above* that heading on a
generated page will be overwritten.
| Location | Who normally writes it | Notes |
|---|---|---|
| `raw/inbox/`, loose files in `raw/` | **You, only** | The agent only reads, archives, and moves things here — it never originates content in `raw/` itself. |
| `raw/archive/<date>/` | Agent | Auto-filed copy of what you dropped in `raw/inbox/`, organised by ingestion date. Don't hand-file here — let Ingest do it, so the date and pairing with the log entry stay accurate. |
| `linked/<name>/` | **You** (you create the symlink) | Points at another KB's real files, which live and get edited *in that other repo* — never here. The agent must never write inside `linked/`. |
| `libs/<name>/` (git-copy, no `source.yaml`) | **You** (you `git clone`) | A frozen copy of an external KB. Update it by re-pulling that repo yourself, not by hand-editing files here. The agent must never write inside it. |
| `libs/<name>/source.yaml` (connector) | **You, only** | Declares the connector, location, and optionally where a shared/pre-built index lives (`index:`). The agent reads it but never writes it — same as anything else upstream. |
| `libs/<name>/source.yaml` (connector) | **You, only** | Declares the connector, location, optionally how often it should be refreshed (`refresh_interval_days:`), and optionally where a shared/pre-built index lives (`index:`). The agent reads it but never writes it — same as anything else upstream. |
| `libs/<name>/source.local.yaml` (connector) | **You** (or the agent, only when you explicitly ask to become/stop being that source's admin) | Per-person, per-machine `access: write`/`read` setting — never committed, never seen by anyone else. Absent = read-only, the default. |
| `libs/<name>/{index.md,entities/,graph/,log.md}` (connector) | Agent-generated, **you can freely edit** | The agent's own index of that one connector's source, built/refreshed by "Index external sources" — but only if you have `access: write` locally; read-only users just get a fetched copy. Structurally the same deal as the main `wiki/` row below — feel free to correct an entry by hand, then run "Lint" (it now also checks connector-backed indexes, respecting the same read/write split). Scoped entirely to that connector; never blended into `wiki/`. |
| `libs/<name>/{index.md,entities/,graph/,log.md}` (connector) | Agent-generated, **you can freely edit** | The agent's own index of that one connector's source, built/refreshed by "Index external sources" — but only if you have `access: write` locally; read-only users just get a fetched copy. Structurally the same deal as the main `wiki/` row below — feel free to correct an entry by hand, then run "Lint" (it now also checks connector-backed indexes, respecting the same read/write split). Scoped entirely to that connector; never blended into `wiki/`. **Only `## Notes` survives a rebuild** — put anything you want to keep there. |
| `wiki/decisions/` | Agent-generated, **edit with care** | Same as the rest of `wiki/` mechanically, but these are append-only by convention: correct a typo or a misattributed name freely, and don't rewrite a decision's context or rationale to match a later view — record a superseding decision instead, so the history survives. |
| `wiki/` (pages, `index.md`, `overview.md`, `log.md`, `error-book.md`, `entities/`, `graph/`) | Agent-generated, **you can freely edit** | This is the one place the agent both writes and expects you might too. Feel free to correct a page by hand — just keep the frontmatter fields intact (or update `last_updated`), and run Lint afterward if you touched something the index/graph/log reference. |
| `outputs/okf/`, `outputs/starlight/` | Agent, **fully regenerated** | Don't hand-edit — these are gitignored build artifacts, silently overwritten the next time you export. If something's wrong, fix the wiki page it came from and re-export. |
| `outputs/teaching/<topic>/` | Agent, semi-persistent state | `plan.md`/`progress.md` the teaching skill reads and writes across sessions. You can look at them any time; hand-editing is possible but may confuse "what's next" tracking — safer to tell the agent what you want changed and let it update the files. |
@ -540,10 +787,14 @@ graph stay in sync with what you changed.
| Say... | What happens | Skill |
|---|---|---|
| "Set up a new wiki like this one in \<folder\>" | Bootstraps a fresh, empty KB with this schema | `ckb-init` |
| "Pull the ckb repo into \<folder\> and set up the wiki" | Clones the template repo to a scratch dir, then bootstraps an empty KB from it | `ckb-init` |
| "Ingest" / "Sync the wiki" / "Update the wiki" | Processes `raw/inbox/` into structured `wiki/` pages | `ckb-ingest` |
| "Record a decision: ..." / "we decided ..." | Writes a numbered decision record under `wiki/decisions/` | `ckb-decide` |
| "What did we decide about X" / "who decided X" / "what's still open" | Answers from the decision records, with who/when/status attached | `ckb-decide` |
| "Lint" | Health-checks the wiki, auto-fixes what it safely can | `ckb-lint` |
| "Reset the wiki" / "Make this a clean template" | **Destructive.** Deletes accumulated knowledge and restores the empty scaffold, after an inventory and a typed confirmation | `ckb-reset` |
| "Sync changes" / "Sync with git" | Commits, pulls, resolves conflicts, pushes to `origin` | `ckb-sync-changes` |
| "Quiz me on X" | One-off scored knowledge test | `cbk-quiz` |
| "Quiz me on X" | One-off scored knowledge test | `ckb-quiz` |
| "Teach me the wiki" / "Teach me about X" | Plans and runs a spaced-out course with progress tracking | `ckb-teach-me` |
| "Onboard me on X" / "Where do I start with X" | Short guided reading order through the graph | `ckb-onboard-me` |
| "Give me a project summary" | Regenerates `PROJECT-OVERVIEW.md` | `ckb-project-summary` |
@ -551,4 +802,5 @@ graph stay in sync with what you changed.
| "Export the wiki to Starlight" | Human-readable docs site at `outputs/starlight/` | `ckb-export-starlight` |
| "Upgrade the wiki" / "Check for a newer template version" | Checks template + wiki schema versions against the canonical repo, upgrades what you accept | `ckb-upgrade` |
| "Index external sources" / "Index libs" | Builds/refreshes a self-contained index for each connector-backed `libs/<name>/` | `ckb-index-external` |
| Just ask a question | Answers from the wiki, using the index/TLDR/graph cascade | — (core query workflow) |
| Just ask a question | Answers from the wiki, using the index/TLDR/graph cascade, with caveats when a source is stale or contested | `ckb-retrieve` |
| "Who knows about X" / "Who owns X" | Answers from expertise/ownership edges in the graph | `ckb-retrieve` |

View file

@ -6,7 +6,9 @@ To jest podręcznik dla *człowieka* korzystającego z Cascade Knowledge Base
(tego repozytorium) — nie dla agenta. Zasady działania samego agenta
znajdziesz w [AGENTS.md](AGENTS.md) / [CLAUDE.md](CLAUDE.md). Techniczny,
funkcja-po-funkcji przegląd znajdziesz w [README.md](README.md) (lub
[README.pl.md](README.pl.md)). Ten dokument jest zorientowany na zadania:
[README.pl.md](README.pl.md)). Pełny schemat strony wraz z historią obu
numerów wersji znajdziesz w [CHANGELOG.pl.md](CHANGELOG.pl.md). Ten dokument
jest zorientowany na zadania:
„chcę zrobić X — co mam powiedzieć i co się wtedy stanie?”
Wszędzie poniżej „powiedz” oznacza napisanie tego do dowolnego agenta AI,
@ -49,6 +51,15 @@ Nigdy nie kopiuje rzeczywistej zawartości tego projektu (żadnych encji,
danych grafu, notatek). Otrzymujesz świeżą, pustą KB, gotową na pierwszy
zrzut do `raw/inbox/`. Zobacz `.agents/skills/ckb-init/SKILL.md`.
Szablon może pochodzić z dwóch miejsc: z plików tego repozytorium albo ze
świeżego, płytkiego klona kanonicznego repozytorium szablonu (lub dowolnego
forka/mirrora, którego URL podasz), pobranego do folderu roboczego.
Powiedz „pull the latest template and set up a KB in \<folder\>” — albo
uruchom skilla spoza jakiejkolwiek KB — a agent najpierw sklonuje
repozytorium, a dopiero potem zbuduje szkielet. Klon jest wyłącznie
roboczy: nowa KB dostaje własną historię gita (agent pyta przed
`git init`), a nie historię szablonu.
Jeśli docelowy folder wygląda już jak baza wiedzy (ma `wiki/` lub
`AGENTS.md`), agent zatrzyma się i zapyta, zanim czegokolwiek dotknie — nie
nadpisze po cichu istniejącej bazy wiedzy.
@ -96,6 +107,11 @@ podpięcia:
`index:` wskazujący na już zbudowany indeks — repozytorium git albo
zasób współdzielony — dzięki czemu po prostu pobierasz to, co ktoś
inny już zaindeksował, zamiast samodzielnie skanować żywe źródło.
Każde uruchomienie najpierw sprawdza tę lokalizację: jeśli indeks już
tam jest, dostajesz go; jeśli go tam jeszcze nie ma (normalny stan,
zanim ktokolwiek z dostępem do zapisu to uruchomił), to nie błąd —
ten, kto ma dostęp do zapisu, tworzy go tam i publikuje przy swoim
kolejnym uruchomieniu.
- **Budowanie/odświeżanie jest opcjonalne, per osoba, per źródło.**
Domyślnie każdy jest tylko-do-odczytu dla źródła typu konektor —
agent nikogo nie przeskanuje żywego konektora w jego imieniu, jeśli
@ -105,6 +121,14 @@ podpięcia:
niewidoczny dla współpracowników). To celowe: pozwala jednej lub dwóm
osobom utrzymywać źródło dla całego zespołu, zamiast żeby każdy
redundantnie je skanował.
- **Możesz ustawić, jak często ma być odświeżane.** Dodaj opcjonalne
`refresh_interval_days: 7` do `source.yaml` (domyślnie 30). Folder
zmieniający się codziennie potrzebuje krótszego okna niż kwartalne
archiwum, którego nikt nie tyka. Wtedy zarówno „index external
sources", jak i „Lint" powiedzą ci, kiedy źródło jest zaległe i o ile —
co ma największe znaczenie, jeśli masz do niego dostęp tylko do
odczytu, bo wiedza o tym, *które* źródło się przedawniło, pozwala
zapytać osobę, która je utrzymuje.
Niezależnie od sposobu, po podpięciu wystarczy normalnie zadawać pytania —
agent sprawdza najpierw twoją lokalną `wiki/`, potem przechodzi przez
@ -144,6 +168,17 @@ Przykład:
> `raw/archive/2026-07-10/`. Na koniec przypomina o przejrzeniu wyniku i
> powiedzeniu „sync changes”, gdy będziesz zadowolony.
Przy długim transkrypcie agent nie pisze po prostu jednej strony
podsumowania. Wyciąga wyszukiwalne pytanie, podsumowanie, rozwiązanie oraz
zaangażowane systemy i osoby — a pojedyncze fragmenty awansuje do własnych
znajdowalnych sekcji, jeśli inaczej przepadłyby wewnątrz podsumowania. Ta
ostatnia część ma celowy próg: fragment musi zawierać naprawdę konkretny
termin (flagę, komunikat błędu, klauzulę, numer wersji), mieć co najmniej
kilka zdań i być potwierdzony przez coś dalej w materiale. W przeciwnym razie
zostaje wtopiony w podsumowanie. Bez tego progu każdy akapit wygląda na wart
zacytowania, a strona wiki znów staje się transkryptem — co przekreśla sens
jego zingestowania.
Jeśli `raw/inbox/` jest puste, agent skanuje bezpośrednio `raw/` (nadal
pomijając `raw/archive/`, które zawiera już przetworzoną historię).
@ -174,6 +209,59 @@ ingest to wchłonie. Dzięki temu wiki pozostaje napędzana zapotrzebowaniem:
rośnie wokół tego, o co faktycznie pytasz, a nie wokół wszystkiego, co
teoretycznie dałoby się spisać.
Trwałe braki można też śledzić w `wiki/query-gaps.md`. Dobry wpis o luce jest
maleńki: pytanie, gdzie agent szukał i jakie najmniejsze źródło lub strona
sprawiłaby, że odpowiedź będzie dostępna następnym razem.
### D. Zapisz decyzję
Gdy zapada jakaś decyzja — wybór technologii, zmiana procesu, polityka —
powiedz:
> „Zapisz decyzję: przenosimy billing na Postgresa. Alice i Bob zdecydowali
> dzisiaj, bo zapytania raportowe zabijały MySQL-a."
Agent zapisze numerowany rekord pod `wiki/decisions/` z decyzją, autorami,
datą, uzasadnieniem, alternatywami i tym, czego dotyczy. Jeśli zastępuje
wcześniejszą decyzję, połączy obie w obu kierunkach i oznaczy starą jako
zastąpioną — nie ruszając jej uzasadnienia. O to, czego nie podasz, dopyta w
jednej turze; jeśli jesteś w środku pracy, powiedz to, a zapisze, co ma, i
wskaże, które pola zostawił otwarte.
Potem pytaj, jak chcesz:
> „Co zdecydowaliśmy w sprawie bazy danych billingu?"
> „Dlaczego używamy Postgresa?"
> „Kto to zdecydował i kiedy?"
> „Które decyzje są wciąż tylko propozycjami?"
> „Co zastąpiło decyzję 3?"
Odpowiedź zawsze przychodzi z informacją kto i kiedy, i wprost mówi, gdy
decyzja jest propozycją, a nie decyzją obowiązującą, albo została już
zastąpiona — żebyś nie działał na czymś, co nie obowiązuje. Zaimplementowane
przez skill `ckb-decide`.
Dwie rzeczy warte zapamiętania:
- **Decyzje są tylko do dopisywania.** „Właściwie zmieniliśmy zdanie" tworzy
*nową* decyzję zastępującą starą; nigdy nie edytuje uzasadnienia starej. To
celowe — historia jest tu sednem. Zwykłe błędy zapisu („powiedziałem Alice,
a było Anna") poprawiane są w miejscu.
- **Propozycja to nie decyzja.** Jeśli sprawa nie została rozstrzygnięta,
zapisywana jest jako `proposed` bez daty decyzji i pojawia się, gdy pytasz,
co jest jeszcze otwarte.
### E. Utwórz lokalny zakres projektu
Gdy jakiś temat, klient, system lub inicjatywa wraca często, poproś:
> „Utwórz zakres projektu dla integracji płatności."
Agent utworzy lub zaktualizuje zwykłą stronę Markdown pod `wiki/projects/`,
wymieniającą strony, encje, pliki z `raw/archive/`, indeksy konektorów i
obszary grafu, które należy przeszukać najpierw dla tego zakresu. Nadal masz
jedną lokalną wiki; to tylko daje powracającym pytaniom lepszy punkt startowy.
---
## 3. Utrzymanie porządku
@ -196,13 +284,92 @@ To uruchamia przegląd kondycji całej wiki:
albo są archiwizowane
- uszkodzone krawędzie grafu są naprawiane lub usuwane
- brakujące/podwójne wpisy w indeksie i dzienniku są poprawiane
- konektorowe źródła, których indeks jest zaległy do odświeżenia, zostają
oflagowane wraz z informacją o ile — przydatne nawet jeśli masz do tego
źródła dostęp tylko do odczytu, bo mówi ci, kogo dopytać
- **strony, których źródło faktycznie się zmieniło**, zostają oflagowane —
patrz niżej
- **cytaty, których nie ma już w źródle**, na które się powołują, zostają
oflagowane
- powtarzające się problemy systemowe trafiają do `wiki/error-book.md`
Naprawia samodzielnie to, co może zrobić bezpiecznie, a resztę zgłasza do
twojej decyzji. Podobnie jak Ingest, na koniec przypomina o przejrzeniu i
Te dwa ostatnie warto zrozumieć, bo to różnica między „ta strona jest stara"
a „ta strona jest błędna".
Każda strona zapisuje skrót materiału, z którego powstała. Nieaktualność
liczona datą to przypuszczenie: strona napisana rok temu może być wciąż
całkowicie poprawna. Skrót nie jest przypuszczeniem — agent przelicza go i
albo źródło jest co do bajtu tym, przeciwko czemu stronę napisano, albo ktoś
je zmienił. Gdy źródło się zmienia, strona na nim zbudowana trafia na szczyt
listy, przed wszystko, co się jedynie zestarzało.
Strony cytują też swoje źródła wprost, w sekcji `## Crux` — kilka dosłownych
linijek niosących właściwe twierdzenie, pod streszczeniem agenta. Wynikają z
tego dwie rzeczy. Gdy zadajesz pytanie, agent często może odpowiedzieć z
cytatu zamiast ponownie czytać całe źródło i pokazać ci słowa, a nie swoją
parafrazę. A gdy cytat przestaje zgadzać się ze źródłem, to strona twierdzi —
w cudzysłowie — coś, czego jej dowód już nie mówi. To najmocniejsze
znalezisko lintu i agent nigdy nie „naprawi" go, po cichu dopasowując cytat
do źródła.
Połowa wykrywająca działa jako skrypt Python tylko-do-odczytu
(`scripts/lint_report.py`), więc ta sama wiki zawsze daje tę samą listę
znalezisk — agent czyta ten raport, a potem wykonuje części wymagające
osądu (supersesja, niejednoznaczne sieroty, wpisy do księgi błędów oraz
decyzja, co naprawić, a co oddać tobie). Naprawia samodzielnie to, co może
zrobić bezpiecznie, a resztę zgłasza do twojej decyzji. Podobnie jak Ingest, na koniec przypomina o przejrzeniu i
synchronizacji. Zaimplementowane przez skill `ckb-lint`
`.agents/skills/ckb-lint/SKILL.md`.
### Zaczynanie od zera: reset do czystego szablonu
Czasem chcesz zachować *kształt* bazy wiedzy bez jej zawartości — zwykle
dlatego, że to repozytorium pełni też rolę szablonu przekazywanego innym, a
zdążyło zebrać decyzje, podsumowania sesji i strony encji, które nie powinny
z nim wędrować.
> „Zresetuj wiki.” / „Zrób z tego czysty szablon.”
To jedyne polecenie w tym repozytorium, które **celowo usuwa wiedzę**, więc
jest zbudowane tak, żeby trudno było je uruchomić przez przypadek:
1. **Najpierw szuka punktu przywracania.** Jeśli drzewo robocze jest
„brudne”, zatrzymuje się i proponuje commit — po resecie wszystko, co
zacommitowane, jest o jedno `git checkout` stąd, a wszystko
niezacommitowane po prostu znika. Może też otagować commit
(`pre-reset-<data>`), żebyś nie musiał trzymać hasha w głowie.
2. **Pyta, jak daleko sięgnąć.** Sześć poziomów wybieranych osobno: wiedza w
wiki, historia `workload/`, materiał źródłowy w `raw/`, `outputs/`,
źródła zewnętrzne i zainstalowane moduły. Domyślnie włączony jest tylko
pierwszy. `libs/`, `linked/` i moduły domyślnie na *nie*`linked/`
zawiera dowiązania do cudzych baz wiedzy, więc usuwa dowiązanie, ale
nigdy nie podąża za nim.
3. **Liczy, zanim zapyta.** Dostajesz inwentarz — ile stron, ile rekordów
decyzji (wymienionych z numerem i tytułem), ile krawędzi grafu, plus
wszystko oznaczone `retention: high` — i jedną linijkę o tym, co
przetrwa.
4. **Wymaga wpisania frazy**, nie „tak”. A jeśli w odpowiedzi zmienisz
zakres, przeliczy wszystko i zapyta ponownie, bo zgodziłeś się na
konkretną liczbę, a liczba się zmieniła.
5. **Weryfikuje po wszystkim**, uruchamiając lint, zanim powie, że się
udało.
Odtwarza dokładnie to, co utworzyłby `ckb-init`: te same katalogi, te same
pliki szkieletu, ten sam `kb_schema_version`. Opróżnienie treści nie cofa
wersji schematu.
Czego nie rusza nigdy, z potwierdzeniem czy bez: warstwy szablonu
(`AGENTS.md`, `.agents/`, `LICENSE`, `VERSION`, dokumentacja) oraz `src/`,
gdzie leżą niezależne repozytoria kodu, do których usuwania to polecenie nie
ma żadnego tytułu.
Jedna celowa osobliwość: w odróżnieniu od każdego innego skilla ten **nie**
zapisuje notatki sesji w `workload/` — byłby to pierwszy wpis w katalogu,
który właśnie opróżnił. Informuje o tym w raporcie.
Zaimplementowane przez skill `ckb-reset`
`.agents/skills/ckb-reset/SKILL.md`.
---
## 4. Synchronizacja — z samym sobą i z innymi ludźmi
@ -289,6 +456,35 @@ Powiedz:
> „Upgrade the wiki.” / „Check for a newer template version.”
### Z jakiego kanału pobierasz
Repozytorium szablonu utrzymuje trzy gałęzie i domyślnie dostajesz tę
stabilną:
| Gałąź | Czym jest | Kto powinien na niej być |
|---|---|---|
| `main` | **Stabilna** — wydany szablon | Ty, o ile nie masz powodu, żeby być gdzie indziej |
| `test` | **Kandydat do wydania** — walidowany, zanim trafi do `main` | Pomagasz walidować wydanie albo potrzebujesz poprawki, która weszła, ale nie została wydana |
| `experimental` | **Rozwojowa** — bieżąca praca, może być zepsuta albo wycofana | Rozwijasz sam szablon |
Żeby użyć innej, po prostu powiedz której:
> „Upgrade from the test branch.” / „Check experimental for updates.” /
> „Switch this KB back to the stable channel.”
To, co wybierzesz, zostaje — jest zapisane w `ckb.yaml`, więc kolejny upgrade
zostanie na tym samym kanale, zamiast po cichu ściągnąć cię z powrotem na
`main`. Tak samo przy tworzeniu: *„zainicjuj z gałęzi experimental”*.
Na jedno warto uważać. Jeśli śledzisz `test` albo `experimental`, twoja KB
może mieć wersję, której `main` jeszcze nie wydało. Sprawdzenie względem
`main` nie znajdzie wtedy nic nowszego — agent powie ci, że **wyprzedzasz**, a
nie że jesteś aktualny, bo to dwie różne sytuacje. Powrót stamtąd na `main` to
*downgrade*: może usunąć skille i cofnąć schemat poniżej tego, pod co napisano
twoje strony. Zostaniesz poproszony o wyraźne potwierdzenie, a gdy treść
przestałaby być zgodna z własnym zadeklarowanym schematem — operacja zostanie
odmówiona.
Sprawdzane są dwie zupełnie odrębne rzeczy, i każda, obie albo żadna może
coś wykazać:
@ -358,11 +554,46 @@ Po prostu zapytaj, zwykłym językiem:
> „Co wiemy o ryzyku migracji w Q3?”
Agent najpierw czyta `wiki/index.md`, żeby znaleźć odpowiednie strony,
sprawdza ich jednolinijkowy `tldr` przed załadowaniem pełnej strony,
przechodzi po grafie wiedzy w poszukiwaniu powiązanych faktów i sięga do
`linked/`/`libs/`, jeśli lokalna wiki nic nie ma. Dostajesz odpowiedź
opartą na tym, co faktycznie zostało spisane, a nie na domysłach.
Agent najpierw czyta `wiki/index.md`, żeby znaleźć odpowiednie strony. Jeśli
istnieje pasujący zakres projektu pod `wiki/projects/`, przeszukuje najpierw
ten zakres. Potem sprawdza jednolinijkowe pola `tldr`, w razie potrzeby
uruchamia dokładne wyszukiwanie lokalne dla literalnych tokenów, rozszerza
kontekst wokół dopasowanych sekcji, przechodzi po grafie wiedzy w poszukiwaniu
powiązanych faktów i sięga do `linked/`/`libs/`, jeśli lokalna wiki nic nie ma.
Dostajesz odpowiedź opartą na tym, co faktycznie zostało spisane, a nie na
domysłach.
Dwie rzeczy warte wiedzenia jako użytkownik:
- **Przeszukuje też `raw/inbox/`.** Coś, co wrzuciłeś dziś rano i czego jeszcze
nie zingestowałeś, nadal może odpowiedzieć na twoje pytanie. Agent powie ci,
kiedy odpowiedź pochodzi z niezingestowanego materiału, co jednocześnie
sygnalizuje, że „Ingest" jest zaległy.
- **Odpowiedzi noszą własne zastrzeżenia.** Jeśli strona stojąca za odpowiedzią
przekroczyła okno świeżości, ma niską pewność albo została przeczytana z
zapisanego indeksu konektora zamiast z żywego źródła, odpowiedź mówi o tym
obok danego twierdzenia. Jeśli dwie strony są ze sobą sprzeczne, a żadna nie
została jeszcze oznaczona jako zastąpiona, też o tym usłyszysz. Chodzi o to,
żebyś nigdy nie musiał sam czytać frontmatteru, by wiedzieć, na ile zaufać
temu, co właśnie dostałeś.
Gdy nadal nie ma odpowiedzi, agent powinien powiedzieć, czego brakuje, i albo
dodać/zaproponować krótki wpis w `wiki/query-gaps.md`, albo zasugerować
najmniejsze źródło do wrzucenia do `raw/inbox/`.
### Pytanie, kto się na czymś zna
> „Kto zna się na ścieżce przywracania checkpointów?" / „Kto jest właścicielem
> usługi billingowej?"
Na te pytania odpowiada bezpośrednio graf wiedzy, a nie wyszukiwanie nazwisk po
słowach kluczowych. Ingest zapisuje krawędź eksperctwa lub własności, gdy
materiał źródłowy faktycznie pokazuje, że ktoś odpowiada na pytania w danym
temacie albo ma zadeklarowaną odpowiedzialność za niego — a nie na podstawie
obecności na spotkaniu czy nazwy stanowiska. Jeśli nikt nie ma jeszcze
zapisanej krawędzi, agent wraca do tego, kogo zarchiwizowane źródła pokazują
jako odpowiadającego na tego rodzaju pytania, i mówi ci, że wnioskuje, a nie
raportuje.
### Nauka z wiki
@ -373,7 +604,7 @@ opartą na tym, co faktycznie zostało spisane, a nie na domysłach.
Zostaniesz zapytany o liczbę pytań i format (otwarte / jednokrotnego
wyboru), a następnie przejdziesz przez nie jedno po drugim, z natychmiastową
informacją zwrotną i bieżącym wynikiem. Nic nie jest zapisywane potem — to
jednorazowy sprawdzian. `.agents/skills/cbk-quiz/SKILL.md`.
jednorazowy sprawdzian. `.agents/skills/ckb-quiz/SKILL.md`.
**Prawdziwy kurs, rozłożony w czasie** — powiedz:
@ -467,6 +698,7 @@ Przykład:
> connector: sharepoint
> location: "https://contoso.sharepoint.com/sites/Finance/Shared Documents/Reports"
> description: "Wspólny folder raportów zespołu finansowego"
> refresh_interval_days: 7
> ```
> *potem mówisz „Index external sources.”*
>
@ -516,9 +748,18 @@ dodaje blok `index:` do współdzielonego `source.yaml`:
index:
store: git
location: "https://github.com/finance-team/index-cache.git"
# ref: main — opcjonalnie: przypina branch, tag albo podścieżkę w tym miejscu
```
Teraz, kiedy *ktokolwiek* powie „index external sources”, agent najpierw
Za pierwszym razem, gdy ktokolwiek uruchomi „index external sources” po
dodaniu tego bloku, `https://github.com/finance-team/index-cache.git` jest
puste — to oczekiwane, nie błąd. Każde uruchomienie sprawdza je najpierw:
użytkownicy tylko-do-odczytu zobaczą po prostu „nic jeszcze nie
opublikowano, zapytaj administratora”; to uruchomienie administratora
faktycznie je tworzy, ponieważ uruchomienie z dostępem do zapisu zawsze
przebudowuje indeks z żywego konektora i wypycha wynik do tej lokalizacji,
niezależnie od tego, czy coś tam wcześniej było. Od tego momentu, kiedy
*ktokolwiek* powie „index external sources”, agent najpierw
pobiera to, co już zostało opublikowane — użytkownicy tylko-do-odczytu
zatrzymują się w tym miejscu; administrator dodatkowo przebudowuje indeks
z żywego konektora i wypycha odświeżoną wersję do tej samej lokalizacji,
@ -542,15 +783,32 @@ Jeśli ręcznie edytujesz stronę wiki, dobrą praktyką jest uruchomienie
potem „Lint”, żeby indeks/dziennik/graf pozostały spójne z tym, co
zmieniłeś.
Jest jeden wyjątek działający w drugą stronę. Na każdej stronie, którą agent
*regeneruje* — indeks konektora, mapa kodu — wszystko, co napiszesz, zwykle
ginie przy następnej przebudowie. Dlatego każda taka strona kończy się sekcją
`## Notes`, której żaden skill nigdy nie tknie:
```markdown
## Notes
<!-- Twoje. Żaden skill tego nie nadpisuje. -->
```
Pisz tam, co chcesz — że ten dokument jest nieaktualny, że osoba w nim
wymieniona już nie pracuje, kogo naprawdę zapytać. Treść jest przenoszona
przez przebudowy co do bajtu. Wszystko, co napiszesz *powyżej* tego nagłówka
na stronie generowanej, zostanie nadpisane.
| Lokalizacja | Kto zwykle to zapisuje | Uwagi |
|---|---|---|
| `raw/inbox/`, luźne pliki w `raw/` | **Tylko ty** | Agent tylko czyta, archiwizuje i przenosi rzeczy tutaj — nigdy nie tworzy treści w `raw/` sam. |
| `raw/archive/<data>/` | Agent | Automatycznie zarchiwizowana kopia tego, co wrzuciłeś do `raw/inbox/`, uporządkowana według daty ingestu. Nie umieszczaj tu plików ręcznie — pozwól, żeby zrobił to Ingest, tak by data i powiązanie z wpisem w dzienniku były poprawne. |
| `linked/<name>/` | **Ty** (tworzysz dowiązanie symboliczne) | Wskazuje na rzeczywiste pliki innej KB, które żyją i są edytowane *w tamtym repozytorium* — nigdy tutaj. Agent nigdy nie może zapisywać wewnątrz `linked/`. |
| `libs/<name>/` (kopia git, bez `source.yaml`) | **Ty** (robisz `git clone`) | Zamrożona kopia zewnętrznej KB. Aktualizujesz ją, ponownie pobierając to repozytorium samodzielnie, a nie ręcznie edytując pliki tutaj. Agent nigdy nie może zapisywać wewnątrz niej. |
| `libs/<name>/source.yaml` (konektor) | **Tylko ty** | Deklaruje konektor, lokalizację i opcjonalnie gdzie znajduje się współdzielony/wcześniej zbudowany indeks (`index:`). Agent go czyta, ale nigdy nie zapisuje — tak jak wszystko inne nadrzędne. |
| `libs/<name>/source.yaml` (konektor) | **Tylko ty** | Deklaruje konektor, lokalizację, opcjonalnie jak często ma być odświeżany (`refresh_interval_days:`) i opcjonalnie gdzie znajduje się współdzielony/wcześniej zbudowany indeks (`index:`). Agent go czyta, ale nigdy nie zapisuje — tak jak wszystko inne nadrzędne. |
| `libs/<name>/source.local.yaml` (konektor) | **Ty** (albo agent, tylko gdy wyraźnie poprosisz o zostanie/przestanie bycia administratorem tego źródła) | Osobiste, per-komputer ustawienie `access: write`/`read` — nigdy niecommitowane, nigdy niewidoczne dla innych. Brak = tylko do odczytu, domyślnie. |
| `libs/<name>/{index.md,entities/,graph/,log.md}` (konektor) | Generowane przez agenta, **możesz swobodnie edytować** | Własny indeks agenta dla tego jednego źródła konektora, budowany/odświeżany przez „Index external sources” — ale tylko jeśli masz lokalnie `access: write`; użytkownicy tylko-do-odczytu dostają po prostu pobraną kopię. Strukturalnie ta sama zasada jak przy wierszu `wiki/` poniżej — śmiało popraw wpis ręcznie, a potem uruchom „Lint” (teraz sprawdza też indeksy oparte na konektorach, respektując ten sam podział odczyt/zapis). Ograniczone wyłącznie do tego konektora; nigdy nie wmieszane w `wiki/`. |
| `libs/<name>/{index.md,entities/,graph/,log.md}` (konektor) | Generowane przez agenta, **możesz swobodnie edytować** | Własny indeks agenta dla tego jednego źródła konektora, budowany/odświeżany przez „Index external sources” — ale tylko jeśli masz lokalnie `access: write`; użytkownicy tylko-do-odczytu dostają po prostu pobraną kopię. Strukturalnie ta sama zasada jak przy wierszu `wiki/` poniżej — śmiało popraw wpis ręcznie, a potem uruchom „Lint” (teraz sprawdza też indeksy oparte na konektorach, respektując ten sam podział odczyt/zapis). Ograniczone wyłącznie do tego konektora; nigdy nie wmieszane w `wiki/`. **Przebudowę przetrwa tylko `## Notes`** — trzymaj tam wszystko, co chcesz zachować. |
| `wiki/decisions/` | Generowane przez agenta, **edytuj ostrożnie** | Mechanicznie tak samo jak reszta `wiki/`, ale te strony są z założenia tylko do dopisywania: popraw swobodnie literówkę czy źle przypisane nazwisko, ale nie przepisuj kontekstu ani uzasadnienia decyzji pod późniejszy pogląd — zapisz zamiast tego decyzję zastępującą, żeby historia przetrwała. |
| `wiki/` (strony, `index.md`, `overview.md`, `log.md`, `error-book.md`, `entities/`, `graph/`) | Generowane przez agenta, **możesz swobodnie edytować** | To jedyne miejsce, w którym zarówno agent zapisuje, jak i spodziewa się, że ty też możesz. Śmiało popraw stronę ręcznie — zachowaj tylko pola frontmatteru (lub zaktualizuj `last_updated`) i uruchom potem Lint, jeśli dotknąłeś czegoś, do czego odwołuje się indeks/graf/dziennik. |
| `outputs/okf/`, `outputs/starlight/` | Agent, **w pełni regenerowane** | Nie edytuj ręcznie — to zignorowane przez git artefakty budowania, cicho nadpisywane przy każdym kolejnym eksporcie. Jeśli coś jest nie tak, popraw stronę wiki, z której to pochodzi, i wyeksportuj ponownie. |
| `outputs/teaching/<topic>/` | Agent, stan półtrwały | `plan.md`/`progress.md`, które skill do nauczania czyta i zapisuje między sesjami. Możesz je oglądać kiedy chcesz; ręczna edycja jest możliwa, ale może pomieszać śledzenie „co dalej” — bezpieczniej powiedzieć agentowi, co chcesz zmienić, i pozwolić mu zaktualizować pliki. |
@ -567,10 +825,14 @@ zmieniłeś.
| Powiedz... | Co się dzieje | Skill |
|---|---|---|
| „Set up a new wiki like this one in \<folder\>” | Zakłada świeżą, pustą KB z tym schematem | `ckb-init` |
| „Pull the ckb repo into \<folder\> and set up the wiki” | Klonuje repozytorium szablonu do katalogu roboczego, a potem zakłada z niego pustą KB | `ckb-init` |
| „Ingest” / „Sync the wiki” / „Update the wiki” | Przetwarza `raw/inbox/` na ustrukturyzowane strony `wiki/` | `ckb-ingest` |
| „Zapisz decyzję: ...” / „zdecydowaliśmy ...” | Zapisuje numerowany rekord decyzji pod `wiki/decisions/` | `ckb-decide` |
| „Co zdecydowaliśmy w sprawie X” / „kto zdecydował X” / „co jest otwarte” | Odpowiada z zapisów decyzji, z autorem, datą i statusem | `ckb-decide` |
| „Lint” | Sprawdza kondycję wiki, automatycznie naprawia to, co bezpiecznie może | `ckb-lint` |
| „Zresetuj wiki” / „Zrób z tego czysty szablon” | **Destrukcyjne.** Usuwa zgromadzoną wiedzę i odtwarza pusty szkielet, po inwentarzu i potwierdzeniu wpisaną frazą | `ckb-reset` |
| „Sync changes” / „Sync with git” | Commituje, pobiera, rozwiązuje konflikty, wypycha do `origin` | `ckb-sync-changes` |
| „Quiz me on X” | Jednorazowy, punktowany sprawdzian wiedzy | `cbk-quiz` |
| „Quiz me on X” | Jednorazowy, punktowany sprawdzian wiedzy | `ckb-quiz` |
| „Teach me the wiki” / „Teach me about X” | Planuje i prowadzi rozłożony w czasie kurs ze śledzeniem postępu | `ckb-teach-me` |
| „Onboard me on X” / „Where do I start with X” | Krótka, prowadzona kolejność czytania po grafie | `ckb-onboard-me` |
| „Give me a project summary” | Regeneruje `PROJECT-OVERVIEW.md` | `ckb-project-summary` |
@ -578,4 +840,5 @@ zmieniłeś.
| „Export the wiki to Starlight” | Czytelna dla człowieka strona dokumentacji w `outputs/starlight/` | `ckb-export-starlight` |
| „Upgrade the wiki” / „Check for a newer template version” | Sprawdza wersje szablonu i schematu wiki względem kanonicznego repozytorium, aktualizuje to, co zaakceptujesz | `ckb-upgrade` |
| „Index external sources” / „Index libs” | Buduje/odświeża samodzielny indeks dla każdego `libs/<name>/` opartego na konektorze | `ckb-index-external` |
| Po prostu zadaj pytanie | Odpowiedź z wiki, przy użyciu kaskady indeks/TLDR/graf | — (podstawowy przepływ zapytań) |
| Po prostu zadaj pytanie | Odpowiedź z wiki, przy użyciu kaskady indeks/TLDR/graf, z zastrzeżeniami gdy źródło jest nieaktualne lub sprzeczne | `ckb-retrieve` |
| „Kto wie o X" / „Kto jest właścicielem X" | Odpowiedź z krawędzi eksperctwa/własności w grafie | `ckb-retrieve` |

202
OPENSPEC.md Normal file
View file

@ -0,0 +1,202 @@
# OpenSpec in this knowledge base
*Read this in: **English** | [Polski](OPENSPEC.pl.md)*
How to add [OpenSpec](https://openspec.dev) to a Cascade KB that documents
software you're building, and how it fits alongside the wiki.
This is a human guide. The agent's own rules live in the `software` module —
[.agents/modules/software/](.agents/modules/software/README.md) — which must be
installed before any of this applies. Say **"install the software module"**.
---
## Contents
1. [What OpenSpec is for here](#1-what-openspec-is-for-here)
2. [Read this before you run `openspec init`](#2-read-this-before-you-run-openspec-init)
3. [Installing](#3-installing)
4. [The two spec levels](#4-the-two-spec-levels)
5. [Daily workflow](#5-daily-workflow)
6. [How specs reach the wiki](#6-how-specs-reach-the-wiki)
7. [Keeping the levels honest](#7-keeping-the-levels-honest)
8. [Quick reference](#8-quick-reference)
---
## 1. What OpenSpec is for here
The wiki records three different things about software, and it's worth keeping
them apart:
| Question | Where it lives | Who writes it |
|---|---|---|
| What *is* the code? | `wiki/entities/``repository` and `component` pages | `ckb-code-map` |
| What *should* it do? | `openspec/specs/` | `ckb-spec` + OpenSpec |
| What did we *choose*, and why? | `wiki/decisions/` | `ckb-decide` |
OpenSpec owns the middle row: a spec states a standing contract, and a change
proposal states what should become true next. You write the spec before the
code, the agent implements against it, and the archived change becomes part of
the permanent record.
If something doesn't constrain future behaviour, it isn't a spec — it's a
decision. Say "record a decision" instead.
## 2. Read this before you run `openspec init`
**Never run `openspec init` in the knowledge base root.** Run it inside
`src/<repo>/`.
This isn't stylistic caution. `openspec init` writes tool integration files into
`.claude/skills/`, and it adds marker blocks to `AGENTS.md` / `CLAUDE.md`. In
this repository both of those are load-bearing:
- `.claude/skills` is a **symlink** to `.agents/skills`. Anything OpenSpec writes
there lands in your KB's own skill set.
- `CLAUDE.md` is a **symlink** to `AGENTS.md` — the KB's system prompt. They are
one file, so a marker block written to "both" is written twice to the same
place.
Run it at the root and you get OpenSpec's per-repo workflow instructions mixed
into the rules governing your whole knowledge base. Run it in `src/<repo>/` and
everything stays where it belongs: the repo gets its own `.claude/`, its own
`AGENTS.md`, and its specs travel with its code.
The KB root's `openspec/` directory is **not** an OpenSpec install (§4), so it
never needs `init`.
## 3. Installing
Needs **Node.js 20.19.0+**.
```bash
npm install -g @fission-ai/openspec@latest # or pnpm / yarn / bun
openspec --version
```
Then, per repository:
```bash
cd src/<repo>
openspec init
```
After upgrading the CLI later, run `openspec update` in each repo to refresh its
generated instruction files.
**Uninstalling from a repo** means removing its `openspec/` directory, its
generated tool files, and the OpenSpec marker blocks from that repo's
`AGENTS.md` / `CLAUDE.md` by hand — the CLI doesn't unwind those for you.
## 4. The two spec levels
Specs live in two places on purpose.
**KB root — `openspec/`** — cross-cutting capabilities that span several `src/`
repos. The *what* and *why* that no single repo owns.
This layer is managed by the `ckb-spec` skill, **not** by the OpenSpec CLI.
There's no code beneath it and no repo for it to govern, so OpenSpec's
repo-shaped workflow doesn't apply. It's tracked in the KB's git history, like
every other piece of knowledge here.
**Per repo — `src/<repo>/openspec/`** — how that repo implements those
capabilities. The *how*. This is OpenSpec's native case: managed by the CLI and
its own instructions, travelling with the code and riding along in that repo's
pull requests.
The agent defers to OpenSpec here and won't substitute a workflow of its own. If
the CLI isn't installed it says so rather than improvising.
## 5. Daily workflow
From inside a repo that's been initialized, the loop is
**propose → apply → archive**:
| Step | Say / run | What happens |
|---|---|---|
| Propose | `/opsx:propose <what you want>` | Proposal, spec delta, design and tasks written as Markdown under `openspec/changes/<id>/` |
| Review | `openspec show <id>`, `openspec validate <id>` | Read the delta; validation checks structure and checks modified requirements against the specs they'd replace |
| Apply | `/opsx:apply` | The agent implements against the spec |
| Archive | `/opsx:archive` or `openspec archive <id>` | The change merges into the main specs and moves to `changes/archive/` |
Useful alongside: `openspec list`, `openspec status`, `openspec view` (an
interactive dashboard).
For the KB-root layer there's no CLI — just say **"write a cross-cutting spec
for X"** and `ckb-spec` handles it.
## 6. How specs reach the wiki
Three bridges, all handled by `ckb-spec`. This is what stops OpenSpec from being
a parallel universe next to your knowledge base.
**Archived change → decision record.** An applied-and-archived change is a
decision that was made and acted on. The agent *offers* to record it under
`wiki/decisions/` — the proposal's reasoning becomes Context and Rationale, the
delta becomes the Decision, rejected options become Alternatives considered.
It offers rather than doing it automatically, deliberately. Not every routine
change deserves a permanent numbered record, and a decisions log padded with
them stops being worth reading.
**Spec → entity page.** Current KB-root specs surface in `wiki/entities/` as thin
`type: spec` pages, so questions like "what is this supposed to do?" get answered
from the index without opening the spec tree. The spec file stays the source of
truth — the wiki page is a pointer, not a copy.
**Level → level.** Root specs and repo specs link to each other (§7).
## 7. Keeping the levels honest
Two spec levels means two places a statement can live, and therefore two places
it can quietly diverge. One rule prevents that:
> A root spec lists every repo spec implementing it in `implemented_by:`.
> A repo spec names its parent in `implements:`.
> **Always set both sides.**
Those links become graph edges, which turns divergence into a visible finding
rather than a silent second source of truth.
Say **"sync the specs"** (or run `lint`) and you get four checks:
1. Root specs with no implementer — *specified, but nobody's building it.*
2. Repo specs whose parent is missing or archived — *building against something
that's no longer true.*
3. One-sided links, either direction.
4. Root specs older than the mapped commit of every repo implementing them —
*possible drift, worth a look.*
None of these are auto-fixed. Each is a statement about intent, and only you know
which side is right.
## 8. Quick reference
| You want to | Say / run |
|---|---|
| Enable all of this | "install the software module" |
| Add OpenSpec to a repo | `cd src/<repo> && openspec init` |
| Map a repo into the wiki | "map `src/<repo>`" |
| Write a cross-cutting spec | "write a cross-cutting spec for X" |
| Propose a change in a repo | `/opsx:propose <what>` |
| Check spec health | "sync the specs" |
| Turn an archived change into a decision | "record this change as a decision" |
**Never:** run `openspec init` at the KB root · `git add` anything under `src/` ·
edit a repo's OpenSpec files to match this KB's conventions.
---
## Sources
- [OpenSpec documentation](https://openspec.dev/docs/installation) ·
[Fission-AI/OpenSpec](https://github.com/Fission-AI/OpenSpec) ·
[@fission-ai/openspec on npm](https://www.npmjs.com/package/@fission-ai/openspec)
- Design records: [D-0001](wiki/decisions/0001-opt-in-file-based-kb-modules.md),
[D-0002](wiki/decisions/0002-software-module-design.md)
---
*Licensed under the Apache License, Version 2.0 — see [LICENSE](LICENSE).*

209
OPENSPEC.pl.md Normal file
View file

@ -0,0 +1,209 @@
# OpenSpec w tej bazie wiedzy
*Read this in: [English](OPENSPEC.md) | **Polski***
Jak dodać [OpenSpec](https://openspec.dev) do Cascade KB, które dokumentuje
tworzone przez Ciebie oprogramowanie, i jak to współgra z wiki.
To przewodnik dla człowieka. Zasady dla samego agenta są w module `software`
[.agents/modules/software/](.agents/modules/software/README.md) — który musi być
zainstalowany, zanim cokolwiek z poniższych zacznie obowiązywać. Powiedz:
**„zainstaluj moduł software”**.
---
## Spis treści
1. [Po co tu OpenSpec](#1-po-co-tu-openspec)
2. [Przeczytaj to, zanim uruchomisz `openspec init`](#2-przeczytaj-to-zanim-uruchomisz-openspec-init)
3. [Instalacja](#3-instalacja)
4. [Dwa poziomy specyfikacji](#4-dwa-poziomy-specyfikacji)
5. [Codzienna praca](#5-codzienna-praca)
6. [Jak specyfikacje trafiają do wiki](#6-jak-specyfikacje-trafiają-do-wiki)
7. [Pilnowanie spójności poziomów](#7-pilnowanie-spójności-poziomów)
8. [Szybki przegląd](#8-szybki-przegląd)
---
## 1. Po co tu OpenSpec
Wiki zapisuje o oprogramowaniu trzy różne rzeczy i warto trzymać je osobno:
| Pytanie | Gdzie mieszka | Kto zapisuje |
|---|---|---|
| Czym *jest* kod? | `wiki/entities/` — strony `repository` i `component` | `ckb-code-map` |
| Co *powinien* robić? | `openspec/specs/` | `ckb-spec` + OpenSpec |
| Co *wybraliśmy* i dlaczego? | `wiki/decisions/` | `ckb-decide` |
OpenSpec odpowiada za środkowy wiersz: specyfikacja opisuje obowiązujący
kontrakt, a propozycja zmiany — co ma stać się prawdą w następnej kolejności.
Piszesz specyfikację przed kodem, agent implementuje pod nią, a zarchiwizowana
zmiana staje się częścią trwałego zapisu.
Jeśli coś nie ogranicza przyszłego zachowania, to nie jest specyfikacja, tylko
decyzja. Powiedz wtedy „zapisz decyzję”.
## 2. Przeczytaj to, zanim uruchomisz `openspec init`
**Nigdy nie uruchamiaj `openspec init` w katalogu głównym bazy wiedzy.**
Uruchamiaj go w `src/<repo>/`.
To nie jest ostrożność na wszelki wypadek. `openspec init` zapisuje pliki
integracji z narzędziami w `.claude/skills/` oraz dodaje bloki znacznikowe do
`AGENTS.md` / `CLAUDE.md`. W tym repozytorium oba te miejsca są nośne:
- `.claude/skills` to **dowiązanie symboliczne** do `.agents/skills`. Cokolwiek
OpenSpec tam zapisze, ląduje w zestawie skilli Twojej bazy wiedzy.
- `CLAUDE.md` to **dowiązanie symboliczne** do `AGENTS.md` — systemowego prompta
bazy. To jeden i ten sam plik, więc blok zapisany „do obu” zapisuje się
dwukrotnie w to samo miejsce.
Uruchomienie w katalogu głównym wmiesza instrukcje OpenSpec dla pojedynczego
repozytorium w reguły rządzące całą bazą wiedzy. Uruchomienie w `src/<repo>/`
zostawia wszystko na swoim miejscu: repozytorium dostaje własne `.claude/`,
własny `AGENTS.md`, a jego specyfikacje podróżują razem z kodem.
Katalog `openspec/` w korzeniu bazy **nie jest** instalacją OpenSpec (§4), więc
nigdy nie wymaga `init`.
## 3. Instalacja
Wymaga **Node.js 20.19.0 lub nowszego**.
```bash
npm install -g @fission-ai/openspec@latest # albo pnpm / yarn / bun
openspec --version
```
Następnie, dla każdego repozytorium:
```bash
cd src/<repo>
openspec init
```
Po późniejszej aktualizacji CLI uruchom w każdym repozytorium `openspec update`,
żeby odświeżyć wygenerowane pliki instrukcji.
**Odinstalowanie z repozytorium** oznacza ręczne usunięcie katalogu `openspec/`,
wygenerowanych plików narzędziowych oraz bloków znacznikowych OpenSpec z
`AGENTS.md` / `CLAUDE.md` tego repozytorium — CLI tego za Ciebie nie cofa.
## 4. Dwa poziomy specyfikacji
Specyfikacje mieszkają w dwóch miejscach i jest to zamierzone.
**Korzeń bazy — `openspec/`** — przekrojowe zdolności obejmujące kilka
repozytoriów w `src/`. To *co* i *dlaczego*, którego nie posiada żadne pojedyncze
repozytorium.
Tą warstwą zarządza skill `ckb-spec`, a **nie** CLI OpenSpec. Nie ma pod nią
kodu ani repozytorium, którym miałaby rządzić, więc workflow OpenSpec — skrojony
pod repozytorium — tu nie pasuje. Warstwa ta jest śledzona w historii gita bazy,
tak jak każda inna wiedza.
**Per repozytorium — `src/<repo>/openspec/`** — jak to repozytorium realizuje te
zdolności. To *jak*. Rodzimy przypadek OpenSpec: zarządzany przez CLI i jego
własne instrukcje, podróżujący z kodem i biorący udział w pull requestach tego
repozytorium.
Agent oddaje tu pole OpenSpec i nie podstawia własnego workflow. Jeśli CLI nie
jest zainstalowane, powie to wprost, zamiast improwizować.
## 5. Codzienna praca
W zainicjalizowanym repozytorium pętla wygląda tak:
**propose → apply → archive**.
| Krok | Powiedz / uruchom | Co się dzieje |
|---|---|---|
| Propozycja | `/opsx:propose <co chcesz zbudować>` | Propozycja, delta specyfikacji, projekt i zadania zapisane jako Markdown w `openspec/changes/<id>/` |
| Przegląd | `openspec show <id>`, `openspec validate <id>` | Czytasz deltę; walidacja sprawdza strukturę oraz zmodyfikowane wymagania wobec specyfikacji, które mają zastąpić |
| Wdrożenie | `/opsx:apply` | Agent implementuje pod specyfikację |
| Archiwizacja | `/opsx:archive` lub `openspec archive <id>` | Zmiana scala się z główną specyfikacją i trafia do `changes/archive/` |
Przydatne obok: `openspec list`, `openspec status`, `openspec view`
(interaktywny pulpit).
Dla warstwy w korzeniu bazy nie ma CLI — po prostu powiedz **„napisz przekrojową
specyfikację dla X”**, a zajmie się tym `ckb-spec`.
## 6. Jak specyfikacje trafiają do wiki
Trzy mostki, wszystkie obsługiwane przez `ckb-spec`. To one sprawiają, że
OpenSpec nie staje się równoległym światem obok bazy wiedzy.
**Zarchiwizowana zmiana → rekord decyzji.** Wdrożona i zarchiwizowana zmiana to
decyzja, którą podjęto i wykonano. Agent *proponuje* zapisanie jej w
`wiki/decisions/` — uzasadnienie z propozycji staje się Kontekstem i Uzasadnieniem,
delta staje się Decyzją, a odrzucone opcje — Rozważanymi alternatywami.
Proponuje, a nie robi tego automatycznie, i jest to celowe. Nie każda rutynowa
zmiana zasługuje na trwały, numerowany rekord, a dziennik decyzji zapchany nimi
przestaje być wart czytania.
**Specyfikacja → strona encji.** Aktualne specyfikacje z korzenia bazy pojawiają
się w `wiki/entities/` jako cienkie strony `type: spec`, dzięki czemu pytania w
rodzaju „co to ma robić?” da się odpowiedzieć z indeksu, bez otwierania drzewa
specyfikacji. Źródłem prawdy pozostaje plik specyfikacji — strona wiki jest
wskaźnikiem, nie kopią.
**Poziom → poziom.** Specyfikacje z korzenia i z repozytoriów linkują się
nawzajem (§7).
## 7. Pilnowanie spójności poziomów
Dwa poziomy specyfikacji to dwa miejsca, w których może mieszkać to samo
stwierdzenie — a więc i dwa, w których może się po cichu rozjechać. Zapobiega
temu jedna zasada:
> Specyfikacja z korzenia wymienia każdą implementującą ją specyfikację
> repozytorium w `implemented_by:`.
> Specyfikacja repozytorium wskazuje rodzica w `implements:`.
> **Zawsze ustawiaj obie strony.**
Te odnośniki stają się krawędziami grafu, co zamienia rozjazd w widoczne
znalezisko, zamiast w cichy drugi ośrodek prawdy.
Powiedz **„zsynchronizuj specyfikacje”** (albo uruchom `lint`), a dostaniesz
cztery kontrole:
1. Specyfikacje z korzenia bez implementacji — *opisane, ale nikt tego nie buduje.*
2. Specyfikacje repozytorium, których rodzic zniknął lub trafił do archiwum —
*budujesz pod coś, co przestało obowiązywać.*
3. Jednostronne odnośniki, w dowolnym kierunku.
4. Specyfikacje z korzenia starsze niż zmapowany commit każdego repozytorium,
które je implementuje — *możliwy rozjazd, wart sprawdzenia.*
Żadna z tych rzeczy nie jest naprawiana automatycznie. Każda jest stwierdzeniem
o intencji, a tylko Ty wiesz, która strona ma rację.
## 8. Szybki przegląd
| Chcesz | Powiedz / uruchom |
|---|---|
| Włączyć to wszystko | „zainstaluj moduł software” |
| Dodać OpenSpec do repozytorium | `cd src/<repo> && openspec init` |
| Zmapować repozytorium do wiki | „zmapuj `src/<repo>`” |
| Napisać przekrojową specyfikację | „napisz przekrojową specyfikację dla X” |
| Zaproponować zmianę w repozytorium | `/opsx:propose <co>` |
| Sprawdzić stan specyfikacji | „zsynchronizuj specyfikacje” |
| Zamienić zarchiwizowaną zmianę w decyzję | „zapisz tę zmianę jako decyzję” |
**Nigdy:** nie uruchamiaj `openspec init` w korzeniu bazy · nie rób `git add`
niczego w `src/` · nie przerabiaj plików OpenSpec repozytorium pod konwencje tej
bazy.
---
## Źródła
- [Dokumentacja OpenSpec](https://openspec.dev/docs/installation) ·
[Fission-AI/OpenSpec](https://github.com/Fission-AI/OpenSpec) ·
[@fission-ai/openspec na npm](https://www.npmjs.com/package/@fission-ai/openspec)
- Rekordy projektowe: [D-0001](wiki/decisions/0001-opt-in-file-based-kb-modules.md),
[D-0002](wiki/decisions/0002-software-module-design.md)
---
*Udostępniane na licencji Apache License 2.0 — zobacz [LICENSE](LICENSE).*

220
README.md
View file

@ -14,6 +14,37 @@ how to create a wiki, add knowledge, keep it tidy, sync with others, and
worked examples for every use case — see [MANUAL.md](MANUAL.md)
([Polski](MANUAL.pl.md)).
If this KB documents software you're building, the optional `software` module
adds `src/` repositories and spec-driven development — see
[OPENSPEC.md](OPENSPEC.md) ([Polski](OPENSPEC.pl.md)).
For the page schema in full, and for how both version numbers got where they
are, see [CHANGELOG.md](CHANGELOG.md) ([Polski](CHANGELOG.pl.md)).
### Release channels
The template repo keeps three branches. They are not interchangeable:
| Branch | What it is | Who should track it |
|---|---|---|
| `main` | **Stable** — the released template | Everyone, by default |
| `test` | **Release candidate** — validated before merging to `main` | Anyone helping validate a release, or needing a landed-but-unshipped fix |
| `experimental` | **Development** — active work, may be broken or reverted | People developing the template itself |
Both `ckb-init` and `ckb-upgrade` default to `main`. To use another channel,
just say which: *"initialize from the test branch"*, *"check experimental for
updates"*, *"switch this KB back to the stable channel"*. The branch a KB
tracks is recorded in `ckb.yaml`:
```yaml
template:
repo: https://git.wierzbowa.cloud/michal/ckb.git
branch: main
```
A KB with no `ckb.yaml` (or no `template:` block) is treated as tracking
`main`, which is what every KB predating this convention was doing anyway.
---
## Directory Structure
@ -31,6 +62,9 @@ worked examples for every use case — see [MANUAL.md](MANUAL.md)
│ ├── overview.md # High-level map
│ ├── log.md # Root rollup change log
│ ├── error-book.md # Compilation errors & derived constraints
│ ├── query-gaps.md # Failed or missing-answer questions for future ingest
│ ├── projects/ # Optional local query scopes
│ ├── decisions/ # Numbered, append-only decision records + own index.md & log.md
│ ├── entities/ # Typed entity pages (people, projects, concepts) + own index.md
│ └── graph/ # Edge lists and relationship data + own index.md
└── workload/ # Session summaries & decisions
@ -72,6 +106,23 @@ Implemented as a Claude Code Skill — see
actually invoked. Distinct from the `ckb-sync-changes` skill, which is a
pure git-level operation with no wiki synthesis.
For long conversations, meeting notes, transcripts, or chat exports, ingest
uses a structured distillation rather than treating the whole file as one
undifferentiated blob: searchable question, short summary, resolution or
decision, systems/code references, people involved, and high-signal excerpts
that deserve to stay findable on their own.
"High-signal" is an explicit test, not a judgment call, or every excerpt
looks worth keeping and the page becomes a second copy of the transcript.
An excerpt earns its own retrievable section only if it carries a term rare
across the wiki (checked with `rg -c` — a distinguishing search handle, not
a word already on twenty pages), runs to roughly 200 characters or more, and
is corroborated by something later in the material agreeing with, acting on,
or correcting it. Fail any one and the content still lands in the page, just
inside the summary rather than as its own unit. Promoted excerpts carry
their parent heading or thread question with them, so they read
unambiguously alone.
### Lazy-Loading Index with "Use When" Triggers
`wiki/index.md` is a routing table. Each entry has a **Use when** column
listing trigger keywords. The agent reads the index first (stays in context),
@ -83,6 +134,100 @@ Every page carries a one-sentence `tldr` in frontmatter. When querying, the
agent reads TLDRs first. If the TLDR already answers the question, the full
body is never loaded. Fallback chain: TLDR → body → raw source.
### Local Project Scopes
For recurring teams, clients, systems, or initiatives, the wiki can keep
plain Markdown scope pages under `wiki/projects/`. A scope page lists the
wiki pages, entities, raw/archive sources, connector-backed libs, outputs,
and graph areas that should be searched first for that project. This gives
the same practical benefit as a project workspace in a larger retrieval
system while staying local, transparent, and editable with any text editor.
Scopes only narrow the first pass. If the scoped search does not answer the
question, the agent falls back to the full cascade.
### Local Hybrid Retrieval
When index/TLDR routing is not enough, the agent can combine several local
signals before answering:
- exact text search with `rg` for error strings, commands, flags, filenames,
hostnames, ticket IDs, and other literal tokens — including across
`raw/inbox/`, so material dropped an hour ago and not yet ingested can
still answer a question (and flags that an ingest is overdue)
- semantic/entity matches from page titles, TLDRs, project scopes, and graph
relationships
- freshness and confidence metadata, so stale or weak pages are treated with
care
- context expansion around a matched section, so answers are grounded in the
neighboring headings and paragraphs rather than a lone snippet
Each signal produces its own ranked list, and the lists are then fused
rather than resolved by picking a favourite: every candidate scores
`weight / (k + rank)` summed across the lists it appears in, so a page
ranked third by three signals beats one ranked first by a single signal.
`k` is 10, deliberately smaller than the 60 rank fusion is usually quoted
with — 60 is tuned for retrievers returning hundreds of candidates and
flattens all scores into near-identical values against the dozen a local
wiki produces. Literal-token queries up-weight the exact-match list, since
no amount of title similarity should outrank a match on the exact string
someone pasted.
Fused candidates are then deduplicated by claim — a wiki page, the
`raw/archive/` file it cites, and a connector page pointing back at it are
three hits for one fact, not three sources — and reranked 010 on how well
each answers the literal question rather than how well it matches the
question's wording. Same agent, deliberate second pass, no separate model.
The result is normalized internally as an evidence packet: source path,
matched claim, date/freshness, confidence/quality, relationship or scope
hints, and which signals each candidate was fused from. No server, vector
database, or dedicated client is required.
### Answer Caveats
Metadata the wiki already tracks is stated in the answer itself, not just
consulted while building it. When a page grounding an answer is past its
`freshness_window_days`, carries low `confidence`/`quality`, rests on
un-ingested `raw/inbox/` material, or was checked against a cached
connector index rather than a live re-fetch, the answer says so next to
the claim it qualifies. Conflicts between two live pages are surfaced the
same way even when neither carries `superseded_by` yet. The failure mode
this closes is answering confidently *from* a stale page without passing
that on.
### Expertise & Ownership Lookups
"Who knows about X" and "who owns X" are direct graph lookups rather than
full-text guesses. Ingest records `has_expertise_in` edges when someone
demonstrably answers questions or explains decisions on a topic, and
`owns` edges for declared responsibility over a system, area, or decision
— both from demonstrated evidence only, never inferred from meeting
attendance or job title. Where no edge exists yet, retrieval falls back to
authorship evidence and says which of the two grounded the answer, since
an inferred expert is a weaker claim than a recorded one.
### Decision Records
`wiki/decisions/` holds one numbered page per decision (`NNNN-slug.md`):
what was decided, `decided_by` whom, `decided_on` what date, why, the
alternatives that lost, and what it `affects`. A `status` field
(`proposed`/`accepted`/`rejected`/`superseded`/`reversed`) says whether it is
actually in force, and an optional `review_on` date marks it for revisiting.
Decision pages are **append-only**. When the choice changes, a *new* decision
supersedes the old one — `supersedes`/`superseded_by` are set on both sides,
the old page's status becomes `superseded` or `reversed`, and its original
context and rationale stay untouched. That is what makes "why is it like
this?" answerable years later, and what turns "we use Postgres" into "we use
Postgres, and before that MySQL, changed in September because of reporting".
`decided_by` and `affects` also become graph edges, so "who decided X" and
"what decisions touch Y" are direct lookups. The `ckb-decide` skill records
decisions and answers questions about them; `ckb-lint` checks their structure
(status vocabulary, required dates, two-sided supersession, unique numbers,
overdue reviews).
### Query Gaps
If the cascade cannot answer a question, the agent records or proposes a
short entry in `wiki/query-gaps.md`: what was asked, where it looked, and the
smallest source or page that would close the gap. This makes failed searches
useful demand signals for the next ingest instead of disappearing into chat
history.
### Page Frontmatter Schema
Every wiki page uses YAML frontmatter. `type` is required; the rest are optional:
@ -98,6 +243,8 @@ superseded_by: path/to/new.md
last_updated: YYYY-MM-DD
freshness_window_days: 90 # Days before considered stale
retention: high|medium|low
source_fingerprint: sha256:3f9a2c1e # digest of the source this page was built from
source_checked: YYYY-MM-DD # when that digest was last verified
---
```
@ -108,17 +255,40 @@ retention: high|medium|low
- **supersedes / superseded_by** — when new info replaces old, link them
- **freshness_window_days** — pages older than this get flagged during lint
- **retention** — low pages are archived after 2× freshness window
- **source_fingerprint / source_checked** — a digest of the material the page
was synthesized from, plus when it was last confirmed. A freshness window is
a guess that a source *might* have moved; a fingerprint is a fact about
whether it *did*, and lint checks it mechanically.
`wiki/index.md` alone also carries `kb_schema_version` (e.g. `"1.1"`), declaring
#### Reserved body sections
Four `##` headings mean something specific everywhere in the KB:
| Section | What it holds |
|---|---|
| `## Sources` | one bullet per source, each with a fingerprint |
| `## Crux` | verbatim quotes from those sources — evidence, never paraphrase |
| `## Notes` | human-authored and **protected**: no skill rewrites it, ever |
`## Crux` is what lets a question be answered from the page instead of from
the archive: a summary can drift silently, a quote either still matches its
source or it doesn't. `## Notes` is the inverse guarantee — on pages the agent
regenerates (connector indexes, code maps), it is the one place an annotation
survives the next rebuild.
`wiki/index.md` alone also carries `kb_schema_version` (currently `"1.5"`), declaring
which revision of this schema the wiki was authored against — bump minor for
additive optional fields, major for breaking changes.
### Entity Extraction & Knowledge Graph
During ingest, the agent extracts typed entities (people, projects, libraries,
concepts, systems) and stores them as pages in `wiki/entities/`. Typed
relationships (`uses`, `depends_on`, `caused`, `contradicts`, `supersedes`)
are recorded in `wiki/graph/edges.json`. Queries can walk the graph to
discover connected pages (e.g. "what depends on Redis?").
relationships are recorded in `wiki/graph/edges.json`, using a closed
vocabulary where each verb is defined by the question it answers (see
`wiki/graph/index.md`) — structural ones (`part_of`, `uses`, `depends_on`,
`produces`, `configures`, `validates`, `implements`, `caused`, `contradicts`,
`supersedes`) plus people-to-topic ones (`has_expertise_in`, `owns`). Queries can walk the
graph to discover connected pages (e.g. "what depends on Redis?") or to
answer "who knows about X" directly.
### Recursive Index & Log Convention
Any `wiki/` subdirectory that groups multiple pages (`entities/`, `graph/`,
@ -138,7 +308,15 @@ mirror locally:
connector: sharepoint
location: "https://contoso.sharepoint.com/sites/Finance/Shared Documents/Reports"
description: "Finance team's shared reports folder"
refresh_interval_days: 7 # optional, defaults to 30
```
`refresh_interval_days` tunes cadence per source — a folder that churns
daily deserves a shorter window than a quarterly archive that barely moves
— and sets the `freshness_window_days` stamped on that source's generated
pages. Both "index external sources" and "Lint" report a source that's
overdue against it, and say by how much, so a read-only user knows which
admin to chase rather than quietly trusting a copy three weeks past due.
Say "index external sources" and the agent walks it, resolving `connector`
to whatever live tool is available that session (a connected Microsoft
365/Google Drive MCP tool, or `WebFetch` for a plain URL), and builds a
@ -156,13 +334,18 @@ Two refinements on top of that:
- **Shared, pre-built indexes.** `source.yaml` can add an optional `index:`
block declaring *where the already-built index lives* — a git repo, or a
shared resource such as a network path or another connector-reachable
location — so most people just fetch what's already there instead of
building it themselves:
location:
```yaml
index:
store: git # git | shared
location: "https://github.com/org/finance-index-cache.git"
ref: main # optional — branch, tag, or subpath hint within that store
```
Every run checks that location: if it already has an index, fetch it —
most people just read what's already there instead of building it
themselves. If it doesn't yet, that's the normal first-time state, not an
error: a write-access user's very next run is what creates and publishes
it there, with no separate "initialize" step.
- **Read vs. write, per user, per source.** Whether *this* user can
actually rebuild an index (versus only read a fetched/published one) is a
separate, local, gitignored `libs/<name>/source.local.yaml` — read-only
@ -193,6 +376,9 @@ Periodically (or on demand), the agent health-checks the wiki:
- **Orphan detection** — finds pages with no inbound links
- **Graph consistency** — verifies all edges point to existing entities
- **Index/log consistency** — verifies every subdirectory has an index.md and no change is double-logged
- **Source fingerprints** — recomputes each cited source's digest and flags pages whose evidence has actually changed, not merely aged
- **Crux verbatimness** — flags a quoted excerpt that no longer appears in the source it cites
- **Connector cadence** — flags a connector-backed source whose generated index is overdue against its `refresh_interval_days`, and by how much
- **Error Book** — records systemic issues with root cause and fix
Auto-fixes what it can (broken links, missing backlinks, stale flags), and
@ -259,9 +445,13 @@ internal schema (`confidence`/`quality`/`retention`/`supersedes`/dual-linking)
that OKF doesn't natively understand. Implemented as a Claude Code Skill —
see `.agents/skills/ckb-export-okf/SKILL.md` — rather than baked into
`CLAUDE.md`/`AGENTS.md`, so the mapping ruleset only loads into context when
actually invoked. `outputs/okf/` is gitignored — it's a fully-regenerated
build artifact, so each machine/tool regenerates it on demand rather than
carrying it in git history.
actually invoked. A deterministic Python script
(`scripts/export_okf.py`) does the whole transform — frontmatter remapping,
link rewriting, index and log regeneration, and an OKF conformance pass over
its own output — so the export is reproducible rather than re-reasoned page
by page; the agent runs it and relays the report. `outputs/okf/` is
gitignored — it's a fully-regenerated build artifact, so each machine/tool
regenerates it on demand rather than carrying it in git history.
### Starlight Export (on demand)
The wiki can also be exported into an Astro + Starlight-consumable form at
@ -276,6 +466,16 @@ report. Also on-demand and skill-only — see
`.agents/skills/ckb-export-starlight/SKILL.md`. Like `outputs/okf/`,
`outputs/starlight/` is gitignored as a regenerated build artifact.
### Decision Log (on demand)
Say "record a decision: ..." (or just "we decided ...") to file a numbered
decision record; ask "what did we decide about X", "who decided that",
"which decisions are still proposed", or "what superseded decision 3" to get
it back with the who, when, and status attached. Recording gathers the
missing fields in a single round rather than an interview, wires the
supersession links in both directions, adds `decided_by`/`affects` graph
edges, and logs to `wiki/decisions/log.md`. See
`.agents/skills/ckb-decide/SKILL.md`.
### Guided Onboarding Tours (on demand)
Ask "onboard me on X" (or "where do I start with X", "mini tour of X") to get
a short, read-only guided reading order: an overview paragraph plus an
@ -308,7 +508,7 @@ one-off, scored knowledge check: the agent reads the relevant pages,
generates open or multiple-choice questions grounded in specific wiki
facts, runs them one at a time with immediate feedback and a running
score, and closes with a verdict. Stateless — nothing is saved between
runs. See `.agents/skills/cbk-quiz/SKILL.md`.
runs. See `.agents/skills/ckb-quiz/SKILL.md`.
### Guided Teaching Curriculum (on demand)
Ask to be taught the wiki ("teach me the wiki", "teach me about X", "run a

View file

@ -15,6 +15,37 @@ przewodnik — jak stworzyć wiki, dodawać wiedzę, utrzymywać porządek,
synchronizować się z innymi i przykłady dla każdego przypadku użycia —
znajdziesz w [MANUAL.pl.md](MANUAL.pl.md) ([English](MANUAL.md)).
Jeśli ta baza dokumentuje tworzone przez Ciebie oprogramowanie, opcjonalny moduł
`software` dodaje repozytoria w `src/` i pracę sterowaną specyfikacją — zobacz
[OPENSPEC.pl.md](OPENSPEC.pl.md) ([English](OPENSPEC.md)).
Pełny schemat strony oraz historię obu numerów wersji znajdziesz w
[CHANGELOG.pl.md](CHANGELOG.pl.md) ([English](CHANGELOG.md)).
### Kanały wydawnicze
Repozytorium szablonu utrzymuje trzy gałęzie. Nie są wymienne:
| Gałąź | Czym jest | Kto powinien ją śledzić |
|---|---|---|
| `main` | **Stabilna** — wydany szablon | Wszyscy, domyślnie |
| `test` | **Kandydat do wydania** — walidowany przed scaleniem do `main` | Każdy, kto pomaga walidować wydanie albo potrzebuje poprawki, która weszła, ale jeszcze nie została wydana |
| `experimental` | **Rozwojowa** — bieżąca praca, może być zepsuta albo wycofana | Osoby rozwijające sam szablon |
Zarówno `ckb-init`, jak i `ckb-upgrade` domyślnie używają `main`. Żeby użyć
innego kanału, po prostu powiedz którego: *„zainicjuj z gałęzi test"*,
*„sprawdź experimental pod kątem aktualizacji"*, *„przełącz tę KB z powrotem
na kanał stabilny"*. Śledzona gałąź jest zapisana w `ckb.yaml`:
```yaml
template:
repo: https://git.wierzbowa.cloud/michal/ckb.git
branch: main
```
KB bez `ckb.yaml` (albo bez bloku `template:`) jest traktowana jako śledząca
`main` — czyli dokładnie to, co i tak robiła każda KB sprzed tej konwencji.
---
## Struktura katalogów
@ -32,6 +63,7 @@ znajdziesz w [MANUAL.pl.md](MANUAL.pl.md) ([English](MANUAL.md)).
│ ├── overview.md # Mapa wysokiego poziomu
│ ├── log.md # Główny dziennik zmian (rollup)
│ ├── error-book.md # Błędy kompilacji i wyprowadzone ograniczenia
│ ├── decisions/ # Numerowane, tylko-dopisywane zapisy decyzji + własny index.md i log.md
│ ├── entities/ # Typowane strony encji (osoby, projekty, koncepcje) + własny index.md
│ └── graph/ # Listy krawędzi i dane relacji + własny index.md
└── workload/ # Podsumowania sesji i decyzje
@ -75,6 +107,25 @@ się do kontekstu tylko wtedy, gdy jest faktycznie wywoływana. Odrębne od
skilla `ckb-sync-changes`, który jest czysto operacją na poziomie gita, bez
syntezy wiki.
Dla długich rozmów, notatek ze spotkań, transkryptów lub eksportów czatu
ingest używa strukturalnej destylacji, zamiast traktować cały plik jako jedną
niezróżnicowaną bryłę: wyszukiwalne pytanie, krótkie podsumowanie,
rozwiązanie lub decyzja, odniesienia do systemów/kodu, zaangażowane osoby
oraz fragmenty o wysokiej wartości, które zasługują na to, by pozostać
znajdowalne samodzielnie.
„Wysoka wartość" to jawny test, a nie ocena uznaniowa — inaczej każdy
fragment wygląda na wart zachowania, a strona staje się drugą kopią
transkryptu. Fragment zasługuje na własną wyszukiwalną sekcję tylko wtedy,
gdy zawiera termin rzadki w całej wiki (sprawdzane przez `rg -c` — wyróżniający
uchwyt wyszukiwania, a nie słowo już obecne na dwudziestu stronach), ma około
200 znaków lub więcej i jest potwierdzony przez coś dalej w materiale, co się
z nim zgadza, działa na jego podstawie lub go koryguje. Niespełnienie choćby
jednego warunku oznacza, że treść nadal trafia na stronę, tylko wewnątrz
podsumowania, a nie jako osobna jednostka. Awansowane fragmenty zabierają ze
sobą nagłówek nadrzędny lub pytanie wątku, żeby dały się jednoznacznie
czytać samodzielnie.
### Leniwie ładowany indeks z wyzwalaczami „Use When"
`wiki/index.md` to tabela routingu. Każdy wpis ma kolumnę **Use when** z
listą słów kluczowych wyzwalających. Agent najpierw czyta indeks (pozostaje
@ -87,6 +138,106 @@ Podczas zapytania agent najpierw czyta TLDR-y. Jeśli TLDR już odpowiada na
pytanie, pełna treść nigdy nie jest ładowana. Łańcuch odwoławczy: TLDR →
treść → surowe źródło.
### Lokalne zakresy projektów
Dla powracających zespołów, klientów, systemów lub inicjatyw wiki może
trzymać zwykłe strony zakresów w formacie Markdown pod `wiki/projects/`.
Strona zakresu wymienia strony wiki, encje, źródła z `raw/archive/`,
konektorowe `libs/`, wyjścia i obszary grafu, które należy przeszukać
najpierw dla danego projektu. Daje to tę samą praktyczną korzyść co
przestrzeń robocza projektu w większym systemie wyszukiwania, pozostając
lokalnym, przejrzystym i edytowalnym w dowolnym edytorze tekstu.
Zakresy zawężają tylko pierwsze przejście. Jeśli wyszukiwanie w zakresie nie
odpowiada na pytanie, agent wraca do pełnej kaskady.
### Lokalne wyszukiwanie hybrydowe
Gdy routing po indeksie/TLDR nie wystarcza, agent może połączyć kilka
lokalnych sygnałów przed odpowiedzią:
- dokładne wyszukiwanie tekstu przez `rg` dla komunikatów błędów, komend,
flag, nazw plików, nazw hostów, identyfikatorów zgłoszeń i innych
literalnych tokenów — również w `raw/inbox/`, więc materiał wrzucony
godzinę temu i jeszcze nie zingestowany nadal może odpowiedzieć na pytanie
(i sygnalizuje, że ingest jest zaległy)
- dopasowania semantyczne/encyjne z tytułów stron, TLDR-ów, zakresów
projektów i relacji w grafie
- metadane świeżości i pewności, dzięki którym nieaktualne lub słabe strony
są traktowane ostrożnie
- rozszerzenie kontekstu wokół dopasowanej sekcji, aby odpowiedzi były
osadzone w sąsiadujących nagłówkach i akapitach, a nie w samotnym urywku
Każdy sygnał tworzy własną listę rankingową, a listy są następnie łączone,
zamiast rozstrzygania przez wybór ulubionego sygnału: każdy kandydat zbiera
`weight / (k + rank)` zsumowane po listach, na których występuje, więc strona
na trzecim miejscu w trzech listach wygrywa ze stroną pierwszą w jednej.
`k` wynosi 10, celowo mniej niż zwykle cytowane 60 — 60 jest dostrojone do
wyszukiwarek zwracających setki kandydatów i spłaszcza wszystkie wyniki do
niemal identycznych wartości przy kilkunastu, które daje lokalna wiki.
Zapytania o literalne tokeny podnoszą wagę listy dokładnych dopasowań,
ponieważ żadne podobieństwo tytułu nie powinno wyprzedzić trafienia w dokładny
ciąg, który ktoś wklejił.
Połączeni kandydaci są następnie deduplikowani według twierdzenia — strona
wiki, plik z `raw/archive/`, który cytuje, i strona konektora wskazująca na
nią to trzy trafienia dla jednego faktu, nie trzy źródła — i przerankowani w
skali 010 według tego, jak dobrze odpowiadają na dosłownie zadane pytanie, a
nie jak dobrze pasują do jego sformułowania. Ten sam agent, świadome drugie
przejście, bez osobnego modelu.
Wynik jest wewnętrznie normalizowany jako pakiet dowodowy: ścieżka źródła,
dopasowane twierdzenie, data/świeżość, pewność/jakość, wskazówki o relacjach
lub zakresie oraz informacja, z których sygnałów każdy kandydat został
połączony. Nie jest wymagany żaden serwer, baza wektorowa ani dedykowany
klient.
### Zastrzeżenia w odpowiedziach
Metadane, które wiki już śledzi, są podawane w samej odpowiedzi, a nie tylko
sprawdzane przy jej budowaniu. Gdy strona stanowiąca podstawę odpowiedzi
przekroczyła `freshness_window_days`, ma niską `confidence`/`quality`, opiera
się na niezingestowanym materiale z `raw/inbox/` lub została sprawdzona
względem zapisanego w pamięci indeksu konektora, a nie żywego źródła —
odpowiedź mówi o tym obok twierdzenia, którego to dotyczy. Konflikty między
dwiema aktywnymi stronami są ujawniane tak samo, nawet jeśli żadna nie ma
jeszcze `superseded_by`. Zamyka to tryb awarii polegający na pewnej
odpowiedzi *z* nieaktualnej strony bez przekazania tej informacji dalej.
### Wyszukiwanie ekspertów i właścicieli
„Kto wie o X" i „kto jest właścicielem X" to bezpośrednie zapytania do grafu,
a nie zgadywanie po pełnym tekście. Ingest zapisuje krawędzie
`has_expertise_in`, gdy ktoś wykazuje się odpowiadaniem na pytania lub
wyjaśnianiem decyzji w danym temacie, oraz krawędzie `owns` dla zadeklarowanej
odpowiedzialności za system, obszar lub decyzję — oba wyłącznie na podstawie
wykazanych dowodów, nigdy wnioskowane z obecności na spotkaniu czy ze
stanowiska. Gdy krawędzi jeszcze nie ma, wyszukiwanie wraca do dowodów
autorstwa i mówi, które z dwóch stanowiło podstawę odpowiedzi, bo domniemany
ekspert to słabsze twierdzenie niż zapisany.
### Zapisy decyzji
`wiki/decisions/` zawiera jedną numerowaną stronę na decyzję
(`NNNN-slug.md`): co zostało zdecydowane, przez kogo (`decided_by`), kiedy
(`decided_on`), dlaczego, jakie alternatywy odpadły i czego decyzja dotyczy
(`affects`). Pole `status`
(`proposed`/`accepted`/`rejected`/`superseded`/`reversed`) mówi, czy decyzja
faktycznie obowiązuje, a opcjonalna data `review_on` oznacza ją do przeglądu.
Strony decyzji są **tylko do dopisywania**. Gdy wybór się zmienia, *nowa*
decyzja zastępuje starą — `supersedes`/`superseded_by` ustawiane są po obu
stronach, status starej zmienia się na `superseded` lub `reversed`, a jej
pierwotny kontekst i uzasadnienie pozostają nietknięte. To właśnie sprawia,
że „dlaczego jest tak, jak jest?" da się odpowiedzieć po latach, i zamienia
„używamy Postgresa" w „używamy Postgresa, a wcześniej MySQL-a, zmienione we
wrześniu z powodu raportowania". `decided_by` i `affects` stają się też
krawędziami grafu, więc „kto zdecydował o X" i „jakie decyzje dotyczą Y" to
bezpośrednie wyszukania. Skill `ckb-decide` zapisuje decyzje i odpowiada na
pytania o nie; `ckb-lint` sprawdza ich strukturę (słownik statusów, wymagane
daty, dwustronne zastępowanie, unikalne numery, zaległe przeglądy).
### Luki w zapytaniach
Jeśli kaskada nie potrafi odpowiedzieć na pytanie, agent zapisuje lub proponuje
krótki wpis w `wiki/query-gaps.md`: o co pytano, gdzie szukał i jakie
najmniejsze źródło lub strona zamknęłaby lukę. Dzięki temu nieudane
wyszukiwania stają się użytecznym sygnałem zapotrzebowania dla następnego
ingestu, zamiast przepadać w historii czatu.
### Schemat frontmatteru strony
Każda strona wiki używa frontmatteru YAML. Pole `type` jest wymagane; reszta
jest opcjonalna:
@ -103,6 +254,8 @@ superseded_by: path/to/new.md
last_updated: YYYY-MM-DD
freshness_window_days: 90 # Liczba dni, po których treść uznaje się za nieaktualną
retention: high|medium|low
source_fingerprint: sha256:3f9a2c1e # skrót źródła, z którego zbudowano tę stronę
source_checked: YYYY-MM-DD # kiedy ten skrót był ostatnio zweryfikowany
---
```
@ -120,8 +273,27 @@ retention: high|medium|low
podczas lintowania
- **retention** — strony o niskim priorytecie są archiwizowane po 2× oknie
świeżości
- **source_fingerprint / source_checked** — skrót materiału, z którego
zsyntetyzowano stronę, oraz data ostatniego potwierdzenia. Okno świeżości to
przypuszczenie, że źródło *mogło* się zmienić; skrót to fakt, czy *się
zmieniło* — i lint sprawdza go mechanicznie.
Sam `wiki/index.md` dodatkowo zawiera `kb_schema_version` (np. `"1.1"`),
#### Zastrzeżone sekcje treści
Cztery nagłówki `##` mają w całej KB ściśle określone znaczenie:
| Sekcja | Co zawiera |
|---|---|
| `## Sources` | po jednym punkcie na źródło, każdy ze skrótem |
| `## Crux` | dosłowne cytaty z tych źródeł — dowód, nigdy parafraza |
| `## Notes` | pisane przez człowieka i **chronione**: żaden skill ich nie nadpisuje |
`## Crux` pozwala odpowiedzieć na pytanie ze strony zamiast z archiwum:
streszczenie może po cichu odpłynąć od źródła, cytat albo wciąż się z nim
zgadza, albo nie. `## Notes` daje odwrotną gwarancję — na stronach
regenerowanych przez agenta (indeksy konektorów, mapy kodu) to jedyne miejsce,
w którym adnotacja przetrwa kolejną przebudowę.
Sam `wiki/index.md` dodatkowo zawiera `kb_schema_version` (obecnie `"1.5"`),
deklarujący, według której wersji tego schematu wiki została napisana —
zwiększaj wersję pomniejszą dla dodatkowych opcjonalnych pól, główną dla
zmian łamiących kompatybilność.
@ -129,9 +301,14 @@ zmian łamiących kompatybilność.
### Ekstrakcja encji i graf wiedzy
Podczas ingestu agent wydobywa typowane encje (osoby, projekty, biblioteki,
koncepcje, systemy) i zapisuje je jako strony w `wiki/entities/`. Typowane
relacje (`uses`, `depends_on`, `caused`, `contradicts`, `supersedes`) są
zapisywane w `wiki/graph/edges.json`. Zapytania mogą przechodzić po grafie,
aby odkrywać powiązane strony (np. „co zależy od Redis?").
relacje są zapisywane w `wiki/graph/edges.json` przy użyciu zamkniętego
słownika, w którym każdy czasownik jest zdefiniowany przez pytanie, na jakie
odpowiada (patrz `wiki/graph/index.md`) — strukturalne (`part_of`, `uses`,
`depends_on`, `produces`, `configures`, `validates`, `implements`, `caused`,
`contradicts`, `supersedes`) oraz łączące osoby z tematami
(`has_expertise_in`, `owns`). Zapytania mogą przechodzić po grafie,
aby odkrywać powiązane strony (np. „co zależy od Redis?") albo bezpośrednio
odpowiadać na „kto wie o X".
### Rekurencyjna konwencja indeksu i dziennika
Każdy podkatalog `wiki/`, który grupuje wiele stron (`entities/`, `graph/`,
@ -151,7 +328,16 @@ URL albo inny konektor — którego nie chcesz w pełni kopiować lokalnie:
connector: sharepoint
location: "https://contoso.sharepoint.com/sites/Finance/Shared Documents/Reports"
description: "Wspólny folder raportów zespołu finansowego"
refresh_interval_days: 7 # opcjonalne, domyślnie 30
```
`refresh_interval_days` dostraja częstotliwość per źródło — folder zmieniający
się codziennie zasługuje na krótsze okno niż kwartalne archiwum, które prawie
nie drgnie — i ustawia `freshness_window_days` nadawane generowanym stronom
tego źródła. Zarówno „index external sources", jak i „Lint" raportują źródło
zaległe względem tej wartości i mówią o ile, żeby użytkownik z dostępem tylko
do odczytu wiedział, kogo zapytać, zamiast po cichu polegać na kopii sprzed
trzech tygodni.
Powiedz „index external sources", a agent go przeskanuje, dopasowując
`connector` do dowolnego żywego narzędzia dostępnego w danej sesji
(połączonego narzędzia MCP do Microsoft 365/Google Drive, albo `WebFetch`
@ -169,14 +355,19 @@ Dwa rozszerzenia na tym fundamencie:
- **Współdzielone, wcześniej zbudowane indeksy.** `source.yaml` może
dodać opcjonalny blok `index:`, który deklaruje, *gdzie już zbudowany
indeks się znajduje* — repozytorium git albo zasób współdzielony, np.
ścieżka sieciowa lub inna lokalizacja dostępna przez konektor — dzięki
czemu większość osób po prostu pobiera to, co już jest, zamiast budować
to samodzielnie:
ścieżka sieciowa lub inna lokalizacja dostępna przez konektor:
```yaml
index:
store: git # git | shared
location: "https://github.com/org/finance-index-cache.git"
ref: main # opcjonalnie — branch, tag albo podpowiedź co do podścieżki w tym miejscu
```
Każde uruchomienie najpierw sprawdza tę lokalizację: jeśli indeks już
tam jest, zostaje pobrany — większość osób po prostu czyta to, co już
jest, zamiast budować to samodzielnie. Jeśli go tam jeszcze nie ma, to
normalny stan przy pierwszym uruchomieniu, a nie błąd: kolejne
uruchomienie użytkownika z dostępem do zapisu jest tym, które go tam
tworzy i publikuje — bez osobnego kroku „inicjalizacji".
- **Odczyt vs. zapis, per użytkownik, per źródło.** Czy *ten* użytkownik
może faktycznie przebudować indeks (a nie tylko czytać pobraną/
opublikowaną wersję) to odrębne, lokalne, ignorowane przez git
@ -214,6 +405,13 @@ Okresowo (lub na żądanie) agent sprawdza kondycję wiki:
istniejące encje
- **Spójność indeksu/dziennika** — weryfikuje, czy każdy podkatalog ma
index.md i czy żadna zmiana nie jest podwójnie logowana
- **Skróty źródeł** — przelicza skrót każdego cytowanego źródła i oznacza
strony, których dowód faktycznie się zmienił, a nie tylko się zestarzał
- **Dosłowność sekcji Crux** — oznacza cytat, którego nie ma już w źródle,
na które się powołuje
- **Częstotliwość konektorów** — oznacza konektorowe źródło, którego
generowany indeks jest zaległy względem `refresh_interval_days`, wraz z
informacją o ile
- **Księga błędów (Error Book)** — zapisuje systemowe problemy wraz z
przyczyną i naprawą
@ -293,10 +491,14 @@ bogatszego wewnętrznego schematu
którego OKF natywnie nie rozumie. Zaimplementowane jako Claude Code Skill —
zobacz `.agents/skills/ckb-export-okf/SKILL.md` — zamiast być zaszytym w
`CLAUDE.md`/`AGENTS.md`, dzięki czemu zestaw reguł mapowania ładuje się do
kontekstu tylko wtedy, gdy jest faktycznie wywoływany. `outputs/okf/` jest
w `.gitignore` — to w pełni regenerowalny artefakt budowania, więc każda
maszyna/narzędzie regeneruje go na żądanie zamiast przenosić go w historii
gita.
kontekstu tylko wtedy, gdy jest faktycznie wywoływany. Cały transform —
przemapowanie frontmatteru, przepisanie linków, regeneracja indeksów i
dzienników oraz sprawdzenie zgodności z OKF na własnym wyjściu — wykonuje
deterministyczny skrypt Python (`scripts/export_okf.py`), więc eksport jest
odtwarzalny zamiast wyprowadzany na nowo strona po stronie; agent uruchamia
skrypt i przekazuje raport. `outputs/okf/` jest w `.gitignore` — to w pełni
regenerowalny artefakt budowania, więc każda maszyna/narzędzie regeneruje go
na żądanie zamiast przenosić go w historii gita.
### Eksport Starlight (na żądanie)
Wiki może też zostać wyeksportowana do formy skonsumowalnej przez Astro +
@ -313,6 +515,16 @@ zobacz `.agents/skills/ckb-export-starlight/SKILL.md`. Podobnie jak
`outputs/okf/`, `outputs/starlight/` jest w `.gitignore` jako regenerowalny
artefakt budowania.
### Dziennik decyzji (na żądanie)
Powiedz „zapisz decyzję: ..." (albo po prostu „zdecydowaliśmy ..."), żeby
utworzyć numerowany rekord decyzji; zapytaj „co zdecydowaliśmy w sprawie X",
„kto to zdecydował", „które decyzje są wciąż propozycjami" albo „co zastąpiło
decyzję 3", żeby dostać ją z powrotem wraz z autorem, datą i statusem.
Zapisywanie zbiera brakujące pola w jednej turze zamiast wywiadu, ustawia
powiązania zastępowania w obu kierunkach, dodaje krawędzie grafu
`decided_by`/`affects` i zapisuje do `wiki/decisions/log.md`. Zobacz
`.agents/skills/ckb-decide/SKILL.md`.
### Prowadzone wycieczki wprowadzające (na żądanie)
Poproś „oprowadź mnie po X" (lub „od czego zacząć z X", „krótka wycieczka
po X") aby otrzymać krótką, tylko-do-odczytu prowadzoną kolejność czytania:
@ -350,7 +562,7 @@ strony, generuje pytania otwarte lub jednokrotnego wyboru oparte na
konkretnych faktach z wiki, przeprowadza je jedno po drugim z natychmiastową
informacją zwrotną i bieżącym wynikiem, a na koniec podaje werdykt.
Bezstanowy — nic nie jest zapisywane między uruchomieniami. Zobacz
`.agents/skills/cbk-quiz/SKILL.md`.
`.agents/skills/ckb-quiz/SKILL.md`.
### Prowadzony program nauczania (na żądanie)
Poproś o naukę z wiki („teach me the wiki", „naucz mnie o X", „przeprowadź

View file

@ -1 +1 @@
1.1.0
1.9.0

0
wiki/decisions/.gitadd Normal file
View file

25
wiki/decisions/index.md Normal file
View file

@ -0,0 +1,25 @@
# Decisions
Decision records — one page per decision, newest number last. Each entry
mirrors the linked page's `tldr`, prefixed with its current status.
Filenames are `NNNN-short-slug.md`, numbered sequentially from `0001`, and
numbers are never reused: a decision that is reversed or superseded keeps its
number and its page, and the newer decision points back at it.
*(No decisions recorded yet — say "record a decision" to add the first one.)*
## Status vocabulary
| Status | Meaning |
|---|---|
| `proposed` | Under discussion; not yet in force. |
| `accepted` | In force. The default for a decision that was actually made. |
| `rejected` | Considered and turned down. Kept so the reasoning isn't relitigated. |
| `superseded` | Replaced by a later decision; `superseded_by` names it. |
| `reversed` | Undone by a later decision that went back to the prior state; `superseded_by` names it. |
## Open questions
Decisions still at `proposed`, and any decision whose `review_on` date has
passed, are the ones worth chasing. `ckb-lint` reports both.

10
wiki/decisions/log.md Normal file
View file

@ -0,0 +1,10 @@
# Decisions Change Log
Changes to pages under `wiki/decisions/` are recorded here rather than in
`wiki/log.md`, per the Recursive Index & Log Convention — each change gets
exactly one home log. `wiki/log.md` carries a pointer line when this log
absorbs a change.
Reverse chronological order, most recent first.
*(No entries yet.)*

View file

@ -1,7 +1,7 @@
---
type: error-book
tldr: Table of known ingest/lint errors, root causes, and the constraints derived from them.
last_updated: 2026-07-13
last_updated: 2026-09-20
---
# Error Book

1
wiki/graph/edges.json Normal file
View file

@ -0,0 +1 @@
{"version": 1, "last_updated": "2026-09-20", "edges": []}

View file

@ -2,6 +2,47 @@
Edge list and relationship data for the knowledge graph, keyed by entity page.
* `edges.json` - Typed relationships (`uses`, `depends_on`, `caused`, `contradicts`, `supersedes`) between entity pages. Created on the first ingest that extracts entities.
`edges.json` holds typed relationships between pages. Created on the first
ingest that extracts entities.
## Edge vocabulary
Each verb exists because it answers a question retrieval actually gets asked.
If a proposed edge doesn't answer one of these, it doesn't belong in the graph
— put it in prose on the page instead.
| Verb | Question it answers | Written by |
|---|---|---|
| `part_of` | Where does this live? What is it a piece of? | `ckb-ingest`, `ckb-code-map` |
| `uses` | What does this reach for at runtime? | `ckb-ingest`, `ckb-index-external` |
| `depends_on` | What breaks if I change this? | `ckb-ingest`, `ckb-code-map` |
| `produces` | Where does this output come from? | `ckb-ingest` |
| `configures` | What changes this thing's behaviour? | `ckb-ingest` |
| `validates` | What checks, tests, or judges this? | `ckb-ingest` |
| `implements` | What contract must this honour? | `ckb-ingest`, `ckb-spec` |
| `caused` | Why did this happen? | `ckb-ingest` |
| `contradicts` | What disagrees with this, unresolved? | `ckb-ingest`, `ckb-lint` |
| `supersedes` | What replaced this, and what did it replace? | `ckb-ingest`, `ckb-decide`, `ckb-lint` |
| `decided_by` | Who made this call? | `ckb-decide` |
| `affects` | What does this decision constrain? | `ckb-decide` |
| `has_expertise_in` | Who can answer questions on this? | `ckb-ingest` |
| `owns` | Who is responsible for this? | `ckb-ingest`, `ckb-code-map` |
| `mentioned_in` | Which source document discusses this? | `ckb-index-external` (lib indexes only) |
Conventions:
* **One direction per relationship.** `part_of`, `supersedes`, `depends_on`,
and `uses` are canonical; don't also record the inverse (`contains`,
`superseded_by`, …) as a second edge. Frontmatter carries the inverse where
a page needs to state it (`superseded_by:`), and traversal reads edges both
ways regardless.
* **Evidence, not inference.** `has_expertise_in` and `owns` are recorded only
from demonstrated evidence — someone attending a meeting is not expertise,
and a job title is not ownership. The same applies to every other verb: an
absent edge beats a fabricated one.
* **In-degree is a retrieval signal.** How many edges point *at* a page is a
rough measure of how central it is, and `ckb-retrieve` fuses it as one
ranked list among several. That only works if edges are recorded honestly;
padding the graph degrades search rather than improving it.
*(No edges recorded yet — populated on the next ingest.)*

View file

@ -1,5 +1,5 @@
---
kb_schema_version: "1.1"
kb_schema_version: "1.5"
---
# Knowledge Base Index
@ -11,6 +11,9 @@ kb_schema_version: "1.1"
| [Overview](overview.md) | High-level map of the knowledge base | Getting started, understanding the structure |
| [Log](log.md) | Chronological record of root-level changes | Reviewing recent modifications |
| [Error Book](error-book.md) | Known compilation errors and fixes | Debugging a bad ingest or lint issue |
| [Projects](projects/index.md) | Optional local query scopes grouping related wiki pages, sources, libs, and graph areas | Narrowing search to a team, topic, client, system, or initiative |
| [Query Gaps](query-gaps.md) | Questions the wiki could not answer yet, used to drive demand-driven ingest | Tracking failed searches, missing knowledge, future ingest targets |
| [Decisions](decisions/index.md) | Numbered decision records — what was decided, by whom, when, why, and what it superseded | Asking why something is the way it is, who decided it, when, what changed it, or what is still open |
| [Entities](entities/index.md) | Typed entity pages (people, projects, libraries, concepts) | Looking up a specific person, project, library, or concept |
| [Graph](graph/index.md) | Edge lists and relationship data between entities | Finding what depends on / relates to a given entity |
@ -22,3 +25,11 @@ extracted during ingest. See [Entities index](entities/index.md) /
graph index.
*(Agent populates this as entities are extracted.)*
## Decision Records
Pages in `decisions/` are numbered decision records (`NNNN-slug.md`). Unlike
entity pages, they are append-only: a decision is never rewritten to match a
later change of mind — a new decision supersedes it and both stay on the
record. See [Decisions index](decisions/index.md) /
[[decisions/index]]. Say "record a decision" to add one.

View file

@ -2,3 +2,18 @@
All modifications to the local `wiki/` directory are recorded here
in reverse chronological order (most recent first).
Each entry uses this format:
```markdown
## [YYYY-MM-DD HH:MM] - [ACTION TYPE]
- **File Affected:** `wiki/path/to/file.md`
- **Description:** Brief summary of what knowledge or structure changed.
- **Source:** Chat conversation, raw file, URL, or skill name.
---
```
Changes to pages under `wiki/decisions/` live in `wiki/decisions/log.md`
instead, per the Recursive Index & Log Convention.
*(No entries yet.)*

View file

@ -1,7 +1,7 @@
---
type: overview
tldr: High-level map of the cascade knowledge base's directory structure, frontmatter schema, and layering rules.
last_updated: 2026-07-13
last_updated: 2026-09-21
---
# Knowledge Base Overview
@ -26,6 +26,9 @@ When the same entity exists in multiple layers, the local version wins.
│ └── inbox/ # Drop zone for unprocessed material
├── tmp/ # Temporary files, caches (gitignored)
├── wiki/ # Local structured wiki (agent-managed)
│ ├── query-gaps.md # Failed or missing-answer questions for demand-driven ingest
│ ├── projects/ # Optional local query scopes
│ ├── decisions/ # Numbered, append-only decision records
│ ├── entities/ # Typed entity pages
│ └── graph/ # Edge lists and relationship data
└── workload/ # Summaries of discussions and decisions
@ -34,11 +37,44 @@ When the same entity exists in multiple layers, the local version wins.
## Page Frontmatter
Every wiki page carries YAML frontmatter with a required `type` field, plus
optional `resource`, `tldr`, `confidence`, `quality`, `supersedes`,
`freshness_window_days`, and `retention`. `wiki/index.md` additionally
`freshness_window_days`, `retention`, `source_fingerprint`, and
`source_checked`. Pages with `type: decision` add
`status`, `decided_on`, `decided_by`, `affects`, and `review_on`. `wiki/index.md` additionally
declares `kb_schema_version` for the bundle as a whole.
See AGENTS.md for the full schema.
## Reserved Page Sections
Three `##` headings mean the same thing on every page in every layer:
* **`## Sources`** — one bullet per source, each carrying a fingerprint
(`sha256:` for a local file, `etag:`/`mtime:` for a connector item) and the
date it was last checked. This is what makes drift detectable mechanically
rather than by calendar.
* **`## Crux`** — verbatim excerpts from those sources, attributed to the
bullet they came from. Evidence, never paraphrase: a quote either still
matches its source or it doesn't.
* **`## Notes`** — human-authored and **protected**. No skill rewrites,
reflows, or drops it; regeneration preserves it byte-for-byte.
## Decision Records
`wiki/decisions/` holds one numbered page per decision (`NNNN-slug.md`),
recording what was decided, by whom, on what date, why, and which earlier
decision it supersedes or reverses. Decision pages are **append-only**: the
substance is never rewritten to match a later change of mind — a new decision
supersedes the old one and both stay on the record, so "why is it like this?"
keeps its original answer. Decision-specific frontmatter is `status`,
`decided_on`, `decided_by`, and optionally `affects` and `review_on`; the
existing `supersedes`/`superseded_by` pair carries the history. The
`ckb-decide` skill records them and answers questions about them.
## Recursive Indexes
`entities/` and `graph/` each carry their own `index.md` so navigation stays
lazy — read `wiki/index.md` first, then only descend into a subdirectory
index if its contents are relevant to the current task.
`entities/`, `graph/`, `decisions/`, and optional topic folders such as
`projects/` each carry their own `index.md` so navigation stays lazy — read `wiki/index.md`
first, then only descend into a subdirectory index if its contents are
relevant to the current task.
## Local-First Retrieval Aids
Project scope pages in `wiki/projects/` can group related sources so a query
starts narrow before falling back to the full cascade. `wiki/query-gaps.md`
records questions the wiki could not answer yet, turning failed searches into
small ingest targets.

8
wiki/projects/index.md Normal file
View file

@ -0,0 +1,8 @@
# Projects
Optional local query scopes. A project page groups related wiki pages,
entity pages, raw/archive source paths, connector-backed libs, and graph
areas so retrieval starts from the most relevant slice of the cascade.
*(No project scopes yet — create one when a topic, team, client, system, or
initiative starts recurring often enough to deserve a default search scope.)*

24
wiki/query-gaps.md Normal file
View file

@ -0,0 +1,24 @@
---
type: log
tldr: Local queue of questions the wiki could not answer yet, used to drive demand-driven ingest.
confidence: 1.0
quality: 0.8
last_updated: 2026-09-20
freshness_window_days: 30
retention: medium
---
# Query Gaps
Use this page for questions that the cascade could not answer from
`wiki/`, `linked/`, or `libs/`. Each entry should stay short and point to
the smallest missing source or page that would close the gap.
## Open
*(No query gaps recorded yet.)*
## Resolved
*(Move entries here after an ingest, page update, or connector index makes
the answer available.)*

View file

@ -0,0 +1,81 @@
# 2026-09-21 Session Summary
## [2026-09-21] Analysis — Graft (trailhq/Graft) vs. this KB structure
- **Type:** read-only analysis. No `wiki/` writes, so no `wiki/log.md` entry.
- **Read:** `wiki/index.md`, `wiki/graph/index.md`, `wiki/entities/index.md`,
`.agents/skills/ckb-retrieve/SKILL.md`, `.agents/skills/ckb-lint/SKILL.md`,
`.agents/modules/software/skills/ckb-code-map/SKILL.md`, repo tree.
- **External source:** <https://github.com/trailhq/Graft> (README, fetched 2026-09-21).
- **Finding:** Graft is a *derived, disposable* code-context graph (tree-sitter pass +
optional LLM pass, gitignored, regenerated per developer); CKB is a *durable, curated*
knowledge layer over non-regenerable material. Architectures are not competitors.
- **Seven portable ideas identified**, ranked: (1) verbatim `## Crux`/evidence excerpts
in pages; (2) protected `## Notes` block on agent-regenerated pages; (3) content-hash
fingerprints on `## Sources` for mechanical staleness; (4) cheap deterministic
pre-pass + a `ckb check` freshness signal at session start; (5) graph in-degree as a
rank-fusion signal in `ckb-retrieve`; (6) blast-radius pass during ingest;
(7) complete + question-oriented edge vocabulary in `wiki/graph/index.md`.
- **Defect spotted:** `wiki/graph/index.md` documents `uses`/`depends_on`/`caused`/
`contradicts`/`supersedes` but omits `part_of`, which `ckb-code-map` Step 6 writes.
- **Rejected as non-portable:** gitignored/regenerable store, MCP server + CLI daemon,
tree-sitter parsing, statusline/hooks, telemetry.
- **Status:** analysis only, no changes proposed to disk yet. Awaiting user decision on
which ideas to implement.
- **Git:** branch `main` clean at session start; this summary is the only new file.
## [2026-09-21] Implementation — Graft ideas 1-7 on branch `graft-ideas`
Branched from `main` at c998489. Schema bumped 1.4 -> 1.5 (additive; every 1.4
page stays valid). VERSION 1.7.0 -> 1.8.0.
| # | Idea | Where it landed |
|---|---|---|
| 1 | `## Crux` verbatim evidence | `AGENTS.md` §2, `ckb-ingest` Step 5, `ckb-decide`, `ckb-retrieve` Step 6 shortcut, lint check 13 |
| 2 | Protected `## Notes` | `AGENTS.md` §2, `ckb-index-external` regeneration rule, `ckb-code-map`, lint check 14 |
| 3 | Source fingerprints | `source_fingerprint`/`source_checked` frontmatter, `ckb-ingest`, `ckb-index-external` (etag/mtime/sha256), `ckb-decide`, lint check 12 |
| 4 | Cheap pre-pass + `ckb check` | `lint_report.py --quick`, wired into Rule E at session start; `ckb-index-external` skips unchanged docs by fingerprint |
| 5 | In-degree rank fusion | `ckb-retrieve` Step 3 builds the list, Step 4 fuses at weight ~0.5; `in_degree()` in the lint script surfaces hubs |
| 6 | Blast radius | new `ckb-ingest` Step 4 (reverse graph walk, confirms/extends/contradicts/untouched + owners), reported in Step 8 |
| 7 | Edge vocabulary | `wiki/graph/index.md` rewritten as a question-per-verb table; added `part_of` (the live inconsistency with `ckb-code-map`), `produces`, `configures`, `validates`, `implements` |
Also updated for consistency: `ckb-init`/`ckb-reset` (graph vocabulary is now
scaffold contract, copied verbatim), `ckb-upgrade` (new step b2, the 1.4->1.5
migration), `ckb-export-okf` (two new passthrough fields), `README.md`,
`README.pl.md`, `MANUAL.md`, `MANUAL.pl.md`, `wiki/overview.md`.
**Verification:** new lint checks exercised against a synthetic fixture in the
scratchpad — a good page (clean), a page with a stale digest + missing file +
missing fingerprint + a fabricated quote, and a page with a paraphrased Crux.
All five findings fired, the good page produced none. Repo's own wiki lints
clean; both exporters run clean.
**Not done:** ideas beyond 1-7 (Graft's token-budgeted `map` view was idea 8
and out of scope). No wiki content was fingerprinted, because this wiki is an
empty scaffold — the conventions take effect on the next ingest.
## [2026-09-21] Docs — CHANGELOG.md / CHANGELOG.pl.md
Added a bilingual changelog + schema reference at the repo root, following the
existing `.md`/`.pl.md` doc convention. Contents: the full current page schema
(frontmatter for all pages and for decisions, the three reserved body sections,
the closed edge vocabulary with a "since" column, the reserved scaffold), the
KB schema version history 1.1-1.5, the template VERSION history 1.0.0-1.8.0,
and a migration section.
History reconstructed from git, not from memory: `VERSION` and
`wiki/index.md` were read back at each commit that changed them, and each
release's diff was inspected to describe what it actually contained.
Two facts worth keeping:
- There was never a schema 1.0. Versioning began at 1.1 (c56348b, 2026-07-13).
- Template 1.4.0 and 1.5.0 were never published — VERSION jumped 1.3.0 -> 1.6.0
on 2026-09-01. Recorded as a note rather than papered over.
- Connector-backed libs shipped in template 1.1.0 but the schema only recorded
them at 1.2, nine days later; the table's schema column shows what was in
effect after each release, with a footnote explaining the lag.
Registered the new files with `ckb-init` (copied verbatim into a new KB) and
`ckb-upgrade` (taken wholesale from upstream, never merged — upstream is
authoritative about its own history). Cross-linked from README.md,
README.pl.md, MANUAL.md, MANUAL.pl.md.

View file

@ -0,0 +1,96 @@
# 2026-09-22 Session Summary
## [2026-09-22] Reset requested, then cancelled
`/ckb-reset` was invoked. Ran Step 0 (clean tree, restore point `2c4d57a`) and
the Step 2 inventory, which found this KB already effectively a clean template
— 0 entity pages, 0 project scopes, 0 decision records, 0 graph edges, nothing
in `raw/`, `outputs/`, `libs/`, `linked/` beyond `.gitadd` placeholders, no
modules installed, no `src/`. The only tier-1/2 content was 3 `wiki/log.md`
entries (the schema 1.5 migration trail) and one workload summary.
**Nothing was deleted.** The typed confirmation was never given — the user
moved on to merging and syncing instead. Flagged at the time: `main` was
already a clean template at schema 1.4, and clearing the log would leave the
scaffold declaring 1.5 with no in-wiki record of how it got there.
## [2026-09-22] Merge and sync
- Working tree was already clean; nothing to commit.
- `main` was an ancestor of `graft-ideas`, so the merge fast-forwarded — no
merge commit, both commits preserved individually, no conflicts.
- Pushed `main` (c998489 -> 2c4d57a) to **both** remotes: `origin`
(git.wierzbowa.cloud/michal/ckb) and `codeberg`
(codeberg.org/Valdec/llm-wiki-cascade).
- Schema 1.5 and template 1.8.0 are now the published state on both.
**Left alone deliberately:** branch `graft-ideas` (now identical to `main`,
safe to delete whenever), and branch `feature/external-source-connectors`,
which holds one commit (`f3dbce7`) not present on either remote and is
unrelated to this work.
## [2026-09-22] Reset executed — Tier 1
Re-ran `/ckb-reset` from the post-sync state (the earlier inventory could not
carry over: the counts had moved). Confirmed with the typed phrase, Tier 1 only.
- **Deleted:** 3 `wiki/log.md` entries — the schema 1.5 migration trail. That
was the whole of Tier 1; there were no entity pages, project scopes, decision
records or graph edges to remove.
- **Restored:** `wiki/log.md` to its header-and-format-example form. Nothing
else was rewritten — `index.md`, `overview.md`, `graph/index.md` and
`decisions/index.md` already matched what `ckb-init` produces at schema 1.5.
- **Deliberately not bumped:** `wiki/graph/edges.json` (`last_updated:
2026-09-20`, array already empty) and `wiki/overview.md` (`last_updated:
2026-09-21`). Both dates are accurate as they stand; bumping them would
assert changes that did not happen.
- **Preserved:** Tier 2 (both workload summaries), the entire template layer,
and schema 1.5 / template 1.8.0.
- **Verification:** lint returned 0 findings across `wiki/` and `libs/`.
- **Restore point:** `7d3fa54`, clean and pushed to both remotes before the
reset ran.
**Rule B exception.** The reset is a change inside `wiki/` and would normally
require a `wiki/log.md` entry. Logging "emptied the log" into the log it just
emptied defeats the purpose, so the record lives here instead — the same
reasoning `ckb-reset` applies to its Rule D exception. The migration this
removed from the log remains documented in `CHANGELOG.md` and in commits
`474630e`/`2c4d57a`, published on both remotes.
## [2026-09-22] Release channels — three branches, branch-aware init and upgrade
Added `test` and `experimental` branches alongside `main`, and taught
`ckb-init`/`ckb-upgrade` to source from any of them.
**Channel semantics:** `main` = stable/released, `test` = release candidate,
`experimental` = development. Default everywhere is `main`.
**Selection is per-invocation and sticky.** The user names a branch in words
("initialize from the test branch", "check experimental for updates", "switch
back to stable"); the choice is then persisted to `ckb.yaml`:
```yaml
template:
repo: https://git.wierzbowa.cloud/michal/ckb.git
branch: main
```
Persistence was a judgment call, not an explicit ask. Without it, a KB
bootstrapped from `experimental` would be silently pulled back to `main` by its
next upgrade — mixing channels without anyone noticing. Absent file or absent
block both mean `main`, so every pre-existing KB behaves exactly as before.
**The non-obvious consequence, handled explicitly:** a KB tracking `test` or
`experimental` can sit on a VERSION `main` has not released. Comparing against
`main` finds nothing newer, which the old code would have reported as "up to
date" — true but misleading. `ckb-upgrade` Step 2 now reports "ahead" instead,
and treats a move back to `main` as a downgrade requiring explicit
confirmation, blocked outright when `kb_schema_version` would drop below what
local content is written against.
**Files touched:** `ckb-init` (Step 3 channel table, Step 7 writes `ckb.yaml`,
Step 12 reports the channel), `ckb-upgrade` (new Step 0, Step 2 ahead-case,
Step 6 persists, Step 8 reports, 5 new edge cases), `ckb-module` (must not
clobber the `template:` block), `AGENTS.md` routing, both READMEs, both
MANUALs, both CHANGELOGs. VERSION 1.8.0 -> 1.9.0. Schema unchanged at 1.5 —
this is tooling, not a content contract.