Specification
The normative definition of everything Keep the Why reads and writes: the project config file, the personal file, the machine-wide policy file, a context cache's settings file, the context/ directory, and the entry format. This file defines what is valid, with an example under each artifact; SKILL.md and setup.md define what the skill does with it; repository-structure.md says where things go — layout, routing, adoption. Where prose elsewhere and this file disagree, this file wins.
The specification is versioned with the skill: its version is the metadata.version in SKILL.md's frontmatter, a project records the version it was last checked against in context-schema, and every change that affects an existing project is listed in migrations.md. keep-the-why-lint is the reference implementation of the mechanically checkable part; its finding codes are named below where they apply.
"Must" is a requirement a conforming file meets; "may" is an option; "default" is the value assumed when a field is absent.
1. Files
| File | Where | Committed | Purpose |
|---|---|---|---|
.keep-the-why |
project root | yes | project config: identity, where the why-knowledge lives, the project-wide settings, an optional pin, optional personal defaults |
<context>/ |
the directory context names, default context/ |
yes | the why-knowledge: one topic file per theme, an index, a README and two guard files |
<context>/index.md |
inside <context>/ |
yes | one line per topic file, under a fixed heading skeleton |
<context>/README.md |
inside <context>/ |
yes | what this directory is, for a reader landing cold |
<context>/AGENTS.md, <context>/CLAUDE.md |
inside <context>/ |
yes | guard: invoke the skill before editing here |
<context>/<topic>.md |
inside <context>/, flat |
yes | entries, one file per topic |
~/.keep-the-why/<id>.md |
the developer's home | never | personal settings for one project on one machine |
~/.keep-the-why/config |
the developer's home | never | machine-wide policy, all projects |
~/.keep-the-why/projects.json |
the developer's home | never | the mapping: where each project has been seen on this machine, with a timestamp per path; written by the skill's setup check and by keep-the-why-dashboard, read by both (§5.2) |
~/.keep-the-why/cache/<id>/ |
the developer's home | never | a read-only context cache of a family member that is not checked out here: a sparse partial clone holding its .keep-the-why and its context directory, nothing else (§5.2) |
~/.keep-the-why/cache/<id>.md |
the developer's home | never | that cache's own settings and refresh timestamp, next to the directory, not inside it (§5.1) |
All of these are written and kept current by the skill — the config files by the two wizards and the setup check, <context>/ by capture and maintenance. A person answers the setup once per project and the occasional question; editing a file by hand is for changing a setting, not part of using it.
The project is the tree under the nearest .keep-the-why walking up from the working directory, minus the trees under any deeper .keep-the-why. Discovery walks up the way Git walks up to .git; the nearest file wins and cuts off everything above it, whether or not a Git boundary lies in between. A directory above several projects and below none is not a project. The four layouts this admits — one repository with one instance, one repository with several, several repositories, one — are in repository-structure.md, "Layouts".
Three boundaries hold for every path a config file names. context and pinned-path are relative to the project root and resolve inside it: no absolute path, no .. out of the tree, no symlink that leaves it (E009). root, a path-form parent and a path-form child location are relative and resolve inside the Git toplevel — the same rule one level up, since a family member may be a sibling directory in the same repository and nothing else (E009). id is a plain file name, because it becomes ~/.keep-the-why/<id>.md (E010). A value outside its boundary is not read, written or followed. canonical, a URL-form parent and a URL-form child location are URLs and never filesystem paths.
2. Config blocks
All four config files share one block syntax:
<!-- keep-the-why:<kind> -->
- <key>: <value>
- <key>: <value>
<!-- /keep-the-why:<kind> -->
- A block starts with the opening marker on a line of its own and ends with the closing marker; a block without its closing marker is an error (
E011), a second opening marker for the same kind in one file is an error (E012). - Each line inside is
- key: value. Keys are lowercase,--separated. A key recorded twice is an error (E004); an unknown key is an error (E005). Values are trimmed; a value may be wrapped in backticks (`context/`), which are not part of the value. - Text outside the markers is prose for humans. The skill neither reads nor depends on it.
- Multi-part values use
—(space, em dash, space) as the separator, e.g.every 14 days — last: 2026-07-21.
Kinds: config, personal-defaults and children in .keep-the-why; personal in ~/.keep-the-why/<id>.md; global in ~/.keep-the-why/config; cache in ~/.keep-the-why/cache/<id>.md.
3. .keep-the-why
3.1 keep-the-why:config
Required in the file: id, context, init, capture-confirmation, source-reference (E002 when missing) and context-schema (a warning, W001). The rest — canonical, parent, dashboard-state, root and the two pin fields — are optional. The "default" column is what the skill backfills — and writes into the file — when it meets a file from before the field existed; a present field with a value outside its set is an error (E003) and, for the skill, a question, never a guess.
| Key | Values | Default | Meaning |
|---|---|---|---|
id |
file-name token: [A-Za-z0-9._-]+, not all dots |
— (required, E002) |
the project's identity across clones, machines and worktrees; keys the personal file. Written once at init: <owner>---<repo> from the published remote (upstream in a fork checkout, else origin) of the nearest .git above the file, or <uuid>---<folder-name> without one; / and every other filesystem-unsafe character normalized to -. A .keep-the-why below the Git toplevel (an isolated-context mono repo) appends its root as a slug: <owner>---<repo>---<sub-path>. Never re-derived. |
canonical |
https://<host>/<path>, no trailing slash, no .git suffix, no query or fragment |
absent when the project has no remote; backfilled from the published remote otherwise (upstream in a fork checkout, else origin) |
the repository's stored locator: the normalized URL of the published remote, SSH forms rewritten to https. Written once at init and changed only deliberately, when the repository is renamed, transferred or moved; not re-derived (E003 when malformed). |
parent |
https://<host>/<path> (another repository) or a relative directory path inside the Git toplevel |
absent (no parent) | the project this one belongs to, when it is part of a larger one: the parent's canonical, or its path relative to this .keep-the-why in an isolated-context mono repo (.. for a sub-project whose parent is the repository root). At most one. The parent lists this project back in its children block (§3.3); when both sides are in the checkout the linter checks the link both ways (E015); a URL is checked for shape only (E003), a path for its boundary (E009). |
dashboard-state |
https://<site>/<path>.json, normally …/state.json |
absent (no published export) | where this project's dashboard export is published — the state.json that ktw-dashboard --export writes next to index.html and the live badges. Read by another dashboard in public mode to browse this project's family member without a checkout; E003 when malformed. |
root |
relative directory path inside the Git toplevel | absent (the file is at the toplevel) | the path of this .keep-the-why relative to the Git toplevel, for a project below it (an isolated-context mono repo). Tooling keys such a project by (canonical, root). Not set on a toplevel (E003); never .. or absolute (E009). |
context |
relative directory path inside the project | — (required) | where the why-knowledge lives; context/ is what the wizard proposes |
init |
complete |
— (required) | the project has been set up. A file carrying any other value is a leftover to fix; init: declined was retired in 0.12.0. |
context-schema |
X.Y.Z |
0.2.0 when missing (W001) |
the newest skill version this project's context/ has been checked and migrated against; see §7 |
capture-confirmation |
automatic | confirm-always | confirm-when-unsure |
confirm-when-unsure |
whether a write to <context>/ needs permission first; project-wide |
source-reference |
always | never | filtered — <criteria> (also read: filtered: <criteria>) |
never |
whether the skill asks for a related issue, ticket or post-mortem when recording; <criteria> is free text |
pinned-version |
X.Y.Z |
absent | optional; the skill version this project pins to. Present only together with pinned-path (E006). |
pinned-path |
relative path inside the project | absent | optional; the vendored SKILL.md to follow when the installed skill's version differs. Must exist (E006), must be inside the project (E009), must be a skill file with name: keep-the-why and a metadata.version equal to pinned-version. |
Example — the prose above the block is for a human who opens the file cold; the skill reads only the block:
This is machine-readable project state for the Keep the Why skill
(https://keepthewhy.com). See context/index.md, or this project's own
README, for what Keep the Why actually is.
<!-- keep-the-why:config -->
- id: acme---widget-service
- canonical: https://github.com/acme/widget-service
- context: `context/`
- init: complete
- context-schema: 0.20.3
- capture-confirmation: confirm-when-unsure
- source-reference: never
<!-- /keep-the-why:config -->
3.2 keep-the-why:personal-defaults (optional)
The values a project offers to a developer who has no personal file for it yet. The six keys below — §4 without session, migration-prompt and source, and without last: timestamps (E008). What happens when a developer meets an offered block is governed by personal-defaults-policy (§5).
| Key | Values |
|---|---|
capture-mode |
proactive | explicit-only |
confirmation-flow |
sequential | batch |
update-check |
every <N> days | no |
consistency-check |
every <N> days | no |
pending-confirmation-check |
on-start | no |
local-lint |
auto | ask | no |
3.3 keep-the-why:children (optional)
The parent of a family lists its children here, one line each — and this is the routing: where an entry about something belongs when it is not the project the agent happens to be working in. setup.md, "Family: routing and writing across projects", says what the skill does with it.
<!-- keep-the-why:children -->
- <name>: <location> — <scope>
<!-- /keep-the-why:children -->
<name>: a token in the block grammar's key alphabet, for prose and findings (unicorn-binance-rest-api,widget); listed once (E004).<location>: the child'scanonical(another repository), or a relative directory path inside the Git toplevel (an isolated-context mono repo:packages/widget). A path must exist and carry a.keep-the-why(E016) whoseparentpoints back here (E015); a URL is checked for shape only. Any other form isE014; a path leaving the repository isE009.<scope>: required (E014) — one line saying what belongs in that project. It is written once, here; the child does not repeat it, its ownindex.mdalready lists what it holds.
The parent's own scope is not written: it holds what is family-wide, or clearly its own. The block is not a dependency graph and not a list of every related repository — a family member is a project whose context/ is organized together with this one, one parent, any number of children. A project with a children block may itself carry a parent: a family can nest (a cluster with its dashboard as a child, the cluster itself a child of a suite), and the family is then the whole tree — its root, the one ancestor without a parent, and everything below it. Each level's block routes among its own children only; routing and writing follow the parent chain (setup.md, "Family: routing and writing across projects").
Example, the parent of a suite:
<!-- keep-the-why:children -->
- unicorn-binance-rest-api: https://github.com/oliver-zehentleitner/unicorn-binance-rest-api — REST client, endpoint coverage, rate limits
- unicorn-binance-websocket-api: https://github.com/oliver-zehentleitner/unicorn-binance-websocket-api — stream client, websocket libraries, reconnect behaviour
<!-- /keep-the-why:children -->
and a child's config line: - parent: https://github.com/oliver-zehentleitner/unicorn-binance-suite.
4. ~/.keep-the-why/<id>.md — keep-the-why:personal
One file per project per developer per machine, <id> being the project's id. Never inside a repository.
| Key | Values | Default | Meaning |
|---|---|---|---|
capture-mode |
proactive | explicit-only |
asked by the wizard, which proposes proactive |
whether the skill looks for capture opportunities on its own. proactive: it records rationale as it surfaces in normal work, nobody has to ask; explicit-only: it waits for a request |
confirmation-flow |
sequential | batch |
asked once; the wizard proposes batch |
how several pending questions or confirmations are presented |
update-check |
every <N> days — last: <YYYY-MM-DD>[ — on-failure: retry-quietly \| disabled] | no |
asked by the wizard | the release check and when it last completed |
consistency-check |
every <N> days — last: <YYYY-MM-DD> | no |
asked by the wizard | the Revisit when sweep and when it last ran |
pending-confirmation-check |
on-start | no |
no |
list entries with Status: pending-confirmation at session start; with none, at most one short line |
local-lint |
auto | ask | no |
auto from the wizard; ask when the line is absent |
run keep-the-why-lint after every write to the context location and after a settings change (--setup); the linter's version must be at least the skill's — auto installs or updates it from PyPI unasked, ask asks first |
session |
attended | unattended |
inherits §5, else attended |
for this project, whether someone is present to answer; overrides the machine-wide value |
migration-prompt |
<X.Y.Z> declined |
absent | this developer declined the migration prompt for exactly that target version; one line per version |
source |
project defaults (confirmed <YYYY-MM-DD>) | project defaults (accepted automatically) |
absent | the values came from the project's personal-defaults block |
last: advances only on a check that actually ran. on-failure is set the first time an update check cannot run and the developer answers how to proceed; it is cleared once a check succeeds again.
Example:
<!-- keep-the-why:personal -->
- capture-mode: proactive
- confirmation-flow: sequential
- update-check: every 14 days — last: 2026-07-21
- consistency-check: every 30 days — last: 2026-07-21
<!-- /keep-the-why:personal -->
5. ~/.keep-the-why/config — keep-the-why:global
One file per machine, all projects.
| Key | Values | Default | Meaning |
|---|---|---|---|
personal-defaults-policy |
always-ask | auto-accept |
asked the first time it matters | what to do when a project offers personal-defaults and this developer has no personal file for it yet |
session |
attended | unattended |
attended |
whether sessions on this machine have someone present to answer; a personal file's own session line wins for its project |
cache-refresh |
every <N> days | no |
every 1 day, asked once when the first cache is created |
how often a context cache is pulled before it is read; a cache file's own refresh line wins for that cache (W002 when malformed) |
cache-offline |
ask | read-stale | stop |
ask |
what to do when a cache cannot be pulled: ask, read it and say its date, or stop; a cache file's own offline line wins (E003 when malformed) |
Example:
<!-- keep-the-why:global -->
- personal-defaults-policy: always-ask
- session: unattended
<!-- /keep-the-why:global -->
5.1 ~/.keep-the-why/cache/<id>.md — keep-the-why:cache
One file per cache, next to the cache directory ~/.keep-the-why/cache/<id>/, never inside it (the clone stays untouched and its git status clean). Written when the cache is created.
| Key | Values | Default | Meaning |
|---|---|---|---|
canonical |
the cached project's canonical |
written at creation | which repository this cache holds |
refresh |
every <N> days — last: <YYYY-MM-DD> | no |
the machine-wide cache-refresh |
this cache's own interval and when it was last pulled; overrides §5 |
offline |
ask | read-stale | stop |
the machine-wide cache-offline |
this cache's own answer for a failed pull; overrides §5 |
<!-- keep-the-why:cache -->
- canonical: https://github.com/oliver-zehentleitner/unicorn-binance-suite
- refresh: every 1 day — last: 2026-09-27
- offline: read-stale
<!-- /keep-the-why:cache -->
5.2 ~/.keep-the-why/projects.json and the cache directory
projects.json is JSON, not a Markdown block, because it is cache data rather than a setting: many rows, timestamps, written by tools and read by tools.
{
"projects-json": 1,
"projects": [
{
"id": "oliver-zehentleitner---unicorn-binance-suite",
"canonical": "https://github.com/oliver-zehentleitner/unicorn-binance-suite",
"root": "",
"paths": [{"path": "/home/me/projects/unicorn-binance-suite", "last_seen": "2026-09-27T10:12:03"}],
"cache": "/home/me/.keep-the-why/cache/oliver-zehentleitner---unicorn-binance-suite"
}
]
}
- One row per project (
id;canonicalandrootwhen the project has them),pathsmost recently seen first withlast_seen, andcachewhen a context cache exists. Rows and paths are ordered most recent first; a reader may rely on the order and on the timestamps, and must tolerate unknown keys. - The skill's setup check records the current project's path on every session; the dashboard records a project when it is opened. Both write the whole file; neither writes anything else there.
~/.keep-the-why/cache/<id>/is a sparse partial clone (git clone --filter=blob:none --sparse) checked out to the member's.keep-the-whyand its context directory only — no agent instruction files, nothing else of the repository. It is read-only for every tool: the skill reads from it and never writes into it, the dashboard reads it; deleting it loses nothing, because nothing lives only there.
6. Resolution order
For every setting that exists at more than one level:
instruction in the current session → personal file → machine-wide config → project file → default
A session instruction is scoped to that session and changes no file. capture-confirmation and source-reference exist at the project level only.
7. Versioning: context-schema
context-schema is the skill version a project's context/ was last checked and migrated against, compared every session with the installed skill's metadata.version:
- equal → nothing to do;
- behind →
migrations.mdlists what changed in between; informational entries advance the value silently, entries that require action are offered as a migration (now, later, or declined per developer viamigration-prompt); - ahead → an older skill on a newer project: say so, do not write to existing entries until the skill is updated.
Every convention below that says "since context-schema on, so an unmigrated project never fails on structure its version did not define. A context-schema newer than the linter knows is a warning (W003), not an error.
| Since | Convention |
|---|---|
| 0.3.0 | Status and Evidence mandatory per entry; Verification values |
| 0.7.0 | Type field |
| 0.8.0 | undefined — <reason> as a Type value, exclusive |
| 0.9.0 | more than one Type line per entry |
| 0.10.0 | dedicated .keep-the-why with id; index.md sorted; guard files |
| 0.13.0 | Status: pending-confirmation; index.md heading skeleton; pending-confirmation-check |
| 0.18.0 | Id per entry, mandatory and unique; See and Superseded by (E114–E121) |
8. The context directory
- Flat. Topic files sit directly in
<context>/; no subdirectories. A large project namespaces filenames (auth-tokens.md,auth-oauth.md) instead of nesting. - Topic files are
<name>.md, one per recurring theme, named for the theme. Lowercase kebab-case is the convention; the first character decides the index heading (§8.1). A topic file carries noId: its file name is its address, in the index and inSeelocators. A file that grows unwieldy is split into narrower topics (SKILL.md, rule 6). README.mdexplains, for a reader landing cold, what the directory is and how to read an entry. Not a topic file; not listed in the index.AGENTS.mdcontains the guard instruction;CLAUDE.mdcontains@AGENTS.md. Neither carries a config block. Both are warnings when missing (W201), since an equivalent doing the same job is fine. The two files in full:
Before creating or editing anything in this directory, invoke the keep-the-why skill. Don't write to the schema by hand.
@AGENTS.md
README.md's template — what it says about reading an entry — is written by the project init wizard; the text is insetup.md, "Project init wizard", step 4.- Non-topic files in
<context>/:README.md,AGENTS.md,CLAUDE.md,index.md.
8.1 index.md
# Context index
<optional intro line>
## 0
## 1
…
## 9
## A
- [auth.md](auth.md) — one line: what the file covers
## B
…
## Z
- One
#title, optionally one intro paragraph, then exactly the thirty-six level-2 headings## 0…## 9,## A…## Z, in that order, all present, empty ones included (E205). Since 0.13.0. - Those thirty-six headings together are the index skeleton — the name this specification uses for them; "deterministic write areas" names what they do for concurrent branches (two topics added on two branches land under their own headings, not in one conflicting hunk), not the skeleton itself.
- One entry per topic file (
E203), of the form- [<file>](<file>) — <one line>; the link target is the bare filename (E202if it does not exist). The one line describes what the file covers, not what was last added to it. - An entry sits under the heading of its filename's first character, uppercased; a name starting with neither a digit nor a letter goes under
## 0(E206). Within a heading, entries are sorted by filename, so the whole list reads in ascending order (E204, since 0.10.0). - Nothing else: the index is for deciding what to load, not for holding content.
9. Entries
A topic file is a # Title, then entries. An entry is a level-2 heading followed by its header fields, then its body:
# Sync
## Snapshot-before-buffer ordering
**Id:** 2f1c5b7e-8a3d-4c6e-9b0f-1d2e3f4a5b6c
**Type:** decision
**Status:** active
**Evidence:** confirmed
**Source:** maintainer interview, 2026-03-14; incident postmortem 2025-11, `incidents.md`
**Revisit when:** the sync protocol or snapshot mechanism changes
**See:** incidents.md#duplicate-state-after-cold-start-2025-11 — 9b2d4f60-7c1e-4a8b-b3d5-6e7f8a9b0c1d — as of 2026-03-14
The sync step always waits for a full snapshot before applying any
buffered events, even though this adds latency on cold start.
**Reason:** applying buffered events before the snapshot landed caused
duplicate-then-overwritten state during a 2025-11 incident (see
`incidents.md`). The ordering constraint isn't visible in the code —
it looks like it could safely be parallelized, and someone tried
exactly that once.
**Rejected alternative:** run snapshot and buffer replay in parallel,
then reconcile. Rejected because reconciliation logic was hard to get
right and the incident showed it wasn't actually needed if ordering
was enforced instead.
9.1 Header fields
Header fields are lines of the form **<Field>:** <value> directly after the heading (blank lines allowed). Order: Id first, then Type, Status, Evidence, then the optional ones in the order of the table. Type placed after Status is a warning (W103).
| Field | Required | Values |
|---|---|---|
Id |
yes, exactly one line, since 0.18.0 (E114, E112) |
a UUID version 4, lowercase, 8-4-4-4-12 hex (E115 checks the shape); unique in the project (E116). Assigned when the entry is written, by an OS command — uuidgen, cat /proc/sys/kernel/random/uuid, PowerShell's [guid]::NewGuid() — never composed by hand or from memory; never changed afterwards. Lowercase whatever the command prints: macOS's uuidgen prints capitals, its output goes through tr 'A-F' 'a-f'. An Id found in capitals is lowercased in place, together with the See and Superseded by lines citing it — a spelling correction, the same Id (E115 and E117 name the case) |
Type |
no — fill in when a value fits, at the latest when the entry is next touched (W101) |
decision | workaround | incident | constraint — one line per value that applies (since 0.9.0), no value twice (E109); or a single undefined — <short reason> line (since 0.8.0), which combines with nothing (E107, E108) |
Status |
yes, exactly one line (E101, E112) |
active | superseded | open | needs-review | pending-confirmation (since 0.13.0, E113 below it) |
Evidence |
yes, exactly one line (E102, E112) |
confirmed | inferred | unknown |
Source |
no | free text: where the rationale came from (interview, issue, commit, post-mortem, "none — no tracked issue") — a kind of source, never a person's name, handle or e-mail address (rule 7) |
Verification |
no | corroborated | uncorroborated | contradicted, optionally followed by an explanation after any separator; contradicted must carry one (E111) |
Revisit when |
no | free text, non-empty (W105): a concrete trigger that makes the entry worth re-checking |
See |
no; one line per cited entry, since 0.18.0 | <locator> — <uuid> — as of <YYYY-MM-DD> (E117). Inside the project the locator is <file>.md or <file>.md#<anchor>, the anchor being the target heading as the host renders it (lowercase, punctuation dropped, spaces to -); the <uuid> must name an entry here (E118) and the locator must still point at it (E119). In another project the locator is that project's canonical and nothing more — no path, no anchor — and is checked for shape only: the form of a canonical value (§3.1), no query, no fragment (E117) |
Superseded by |
yes when Status is superseded (E120), forbidden otherwise (E121); exactly one line (E112); since 0.18.0 |
the successor: an <uuid> of an entry here (E118), a <canonical> — <uuid> — as of <YYYY-MM-DD> reference to one in another project (the locator checked as for See), or none — <why nothing replaced it> (E117) |
Meanings:
Statusis where the entry is in its life.active: current.superseded: no longer current, kept because it explains how things got here — never deleted.open: the entry's central question has no answer yet.needs-review: previously considered current, but aRevisit whentrigger has fired and the entry has not been re-checked yet — whatever itsEvidence.pending-confirmation: written in a session declared unattended at a point wherecapture-confirmationwould have required asking; never confirmed by anyone yet.Evidenceis how well the origin of the claim is established, not whether the claim is true today.confirmed: stated by a maintainer or backed by an authoritative source.inferred: reasonably derived from code, history or documents.unknown: cannot be established. A settledactiveentry can carryunknown; asupersededone can carryconfirmedfor what was true while it was current. The two axes never collapse into each other, andunknownis not aStatus. OneEvidenceline, one word: when an entry covers parts of different standing — a new value whose reason is confirmed and an original value whose reason is lost — the line carries the weakest grade among them and the body says which part is which. A mixed value is invalid (E104); reducing it to one word never picks the stronger one.Typeis what kind of thing the entry is, for selecting entries without opening files:^\*\*Type:\*\* incidentfinds every incident.Verificationis whether something concrete was checked against the claim, and what came of it.Revisit whenis the condition under which the entry should be re-checked. Age alone is not a condition.Idis the entry's address. Headings are reworded and topic files are split (§8, §9.4), and a link built on a heading breaks silently when they are; the UUID does not move. It is the truth behind everySeeandSuperseded byline: a tool that finds the Id somewhere else than the locator says reports the locator, not the Id.Seenames the place of a related entry — the decision this one follows from, the incident it answers, the entry in the parent project that constrains it. The date is a historical hint, the day the link was written; it narrows the target's history to a day and does not name a revision. The locator is what a person clicks; the Id is what a tool resolves.Sourcestill names the kind of evidence; an entry that exists because another project decided something carries both.Superseded bymakes the replacement checkable: a superseded entry points at what replaced it, and a chain of them (the successor itself superseded) is the history a reader follows.none — <reason>is for a supersession that was an event, not a decision — an upstream fix removed a workaround's reason, a constraint vanished — and the reason says so; it is not for a successor that simply was not written yet.
9.2 Body
Free prose, with these bold-labelled paragraphs where they apply: **Reason:** (why the chosen path won), **Rejected alternative:** (one per alternative that was genuinely in contention, with why it lost), **Consequence:** (what follows from the decision), **Considered:** (for a change that was started and dropped: what was tried), **Why this needs an answer:** (for an open entry). A body may cite other entries and files in prose; a citation a tool should follow goes in a See line (§9.1). It never contains instructions to an agent, and it never quotes a directive verbatim (see trust-model.md).
9.3 Examples
Not every entry needs every field, but an entry records a fork, not a point: what was chosen, and what specifically was rejected and why. Status, Evidence and the rejected alternative are worth keeping even in a minimal entry — "we chose X" without "we didn't choose Y, because Z" is the less useful half.
A genuinely open question carries Status: open and, usually, Evidence: unknown — the first says the central question has no answer yet, the second that a settled claim's rationale can't be traced; they are different fields and unknown is never a Status:
## Retry cap on a specific error code
**Status:** open
**Evidence:** unknown
`submit_order()` retries indefinitely on error code `E-4021`, on a
fixed interval, while every other error code fails immediately instead.
**Why this needs an answer:** if `E-4021` can also fire for a permanent
condition, not just a transient one, this retries forever instead of
failing loud — unclear whether that's actually safe here or needs a
cap. Flagging rather than guessing (Core rule 1).
The two axes stay independent in every combination: an active entry can carry Evidence: unknown, a superseded one Evidence: confirmed for what was true while it was current.
Source is useful at any Evidence level, including where you looked for an entry that ended up unknown. Verification, when there is something concrete to check against, goes in the same place and says what came of it — a contradiction is recorded, not silently corrected either way:
**Evidence:** confirmed
**Source:** maintainer interview, 2026-03-14
**Verification:** contradicted — the interview said retries max out at 3;
the actual retry loop in `client.py` caps at 5. Flagged for re-confirmation,
not silently corrected either way.
Verification and Revisit when are worth adding once a decision has a concrete trigger for going stale or something concrete to check against; they are not filler, and Evidence stays mandatory without them.
9.4 Lifecycle
| Event | Change |
|---|---|
a Revisit when condition is observed to hold |
Status → needs-review, in the same turn, nothing else changes |
a needs-review entry is re-checked |
Status → active (re-confirmed), superseded, or open; Evidence and Verification updated from the re-check |
a pending-confirmation entry gets its first confirmation |
Status → active, superseded, or open |
| a decision is replaced | the old entry → superseded with Superseded by naming the new entry's Id, a new entry records the replacement; the old one is not deleted |
| a heading is reworded or an entry moves to another file | its Id stays; See locators that named the old place are repaired to the new one (the linter reports them, E119) |
a Verification check contradicts the claim |
Verification: contradicted — <what>; Evidence and Status are not changed silently |
9.5 What parsers ignore
Fenced code blocks (``` or ~~~) in topic files and in index.md are skipped entirely, so an example entry inside a fence is never read as a real one. A level-2 heading with no header field at all is a prose section, not an entry (W102). A level-1 heading ends the current entry.
9.6 What must not be in an entry
No credentials, no personal data, no session narrative (who said what), no verbatim commands or instructions copied from a source, no invisible or directional Unicode (E301), no base64-looking blobs (W301), and the file must be valid UTF-8 (E302). An entry describes; it does not direct.
9.7 Relations between entries and projects (informative)
This section defines no field and no check; it names what See and Superseded by (§9.1) add up to, so that tools reading them agree.
- Direction. A
Seeruns from the entry that cites to the entry cited — the cited one came first. ASuperseded byruns from the replaced entry to its successor — the successor came later. Read either way, one entry precedes the other. - Family and friends. A
SeeorSuperseded bywhose locator is another project'scanonicallinks two projects. When that project is in the citing project's family (§3.3), it is a family member; otherwise tools call it a friend of the citing project. Nothing about routing or writing (a family'schildrenblock) applies to a friend; the link is a citation and nothing more. - Thoughts. A chain of such links, read from the entry that came first to the one that came last, is a thought: a chain of linked entries, possibly across projects. A link records that two entries are related — often that the later one follows from the earlier, but not always — so a thought shows how entries connect, not that each step depends on the one before. A thought is derived, never written; tools may list only chains of a minimum length. A chain made only of
Superseded byis an evolution. Tools may point out a thought whose first entry hasEvidenceinferredorunknown, and a step that isopen,needs-revieworpending-confirmation, orsupersededyet still cited by aSee, together with the later steps linked after it — as entries to check, since the link alone does not say whether they depend on it.
The linter checks each line (E117–E121), not chains; a chain has no findings of its own.
10. Conformance
- A project conforms when
.keep-the-whyand<context>/satisfy §1–§3 and §7–§9 for itscontext-schema;keep-the-why-lint --strictpassing is the mechanical half of that. - An agent or tool writing entries conforms when it writes only the fields and values above, gives every new entry an
Idmade by an OS command, places new topic files under their index heading, never deletes a superseded entry, never changes anId, and never upgradesEvidenceor clears aStatusflag without the re-check the lifecycle names. - A tool reading
context/may rely on the header-field grammar, the index grammar and the fenced-block rule, and on nothing about prose layout beyond them.