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>
323 lines
16 KiB
Markdown
323 lines
16 KiB
Markdown
---
|
|
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>.*
|