Your project's collective memory, captured once and drawn on every time Spektacular (Spek) plans a feature.
What it is
The knowledge base is the accumulated know-how a project draws on when
planning: the rules it commits to, the shared vocabulary it speaks, how the
system is built, the traps to avoid, what past work taught, and why decisions
were made. Spektacular (Spek) keeps it alongside your code, under
.spektacular/knowledge/, so it lives and versions with the project.
It is a planning-time input. When you ask Spek to plan a feature,
the planning agent reads the relevant knowledge and writes the parts that
matter straight into the plan. The implement workflow then works only from the
plan documents (it never re-reads the knowledge base), so the plan is the
contract.
This is what it solves: without a knowledge base, every plan starts from a
blank slate. The agent rediscovers your conventions, walks into gotchas you
already mapped, and re-derives decisions you already made. Captured once and
reused on every plan, the knowledge base is what lets Spek produce
plans that fit your project instead of generic ones. It is also why you can
trust the output enough to build on it.
The six categories
Every entry belongs to exactly one of six categories, fixed by the first
segment of its path: an entry at gotchas/timeouts.md is a gotchas entry.
The categories are the single model the whole subsystem is built on.
Each category carries a retrieval tier that decides when its entries reach
the planning agent. There are two:
Always-applied: conventions and glossary. Every entry is loaded in
full on every planning task, and these categories are deliberately excluded
from search so the same content is never surfaced twice. Keep them small:
their entire contents are paid for on every task.
Looked-up: architecture, gotchas, learnings, and decisions.
This is the larger reference body. Entries are retrieved only when a query
matches them, so the category can grow without weighing down every task.
The six categories, in the order the registry declares them:
Category
Tier
What goes in it
What belongs elsewhere
conventions
always-applied
The rules the team always wants honoured: coding standards, naming, formatting, required patterns, house style. Stated as an imperative rule to follow.
The reasoning behind a rule (a decision); a one-off lesson (a learning).
glossary
always-applied
The shared vocabulary: domain and project terms, each a term plus a one- or two-sentence gloss.
How a thing works (architecture); why a choice was made (a decision).
architecture
looked-up
How the system is built and how new work must be built into it: components, responsibilities, boundaries, and data/control flow. Binding on the work being planned, not a survey of today’s code.
Why a structure was chosen (a decision); a defined term (glossary).
gotchas
looked-up
Sharp edges and non-obvious traps: surprising behaviours and easy mistakes, with what to do instead.
A standing rule (a convention); the system’s structure (architecture).
learnings
looked-up
Empirical findings from doing the work: what was tried, what worked, what didn’t.
A decision with its rationale (a decision); a rule to follow (a convention).
decisions
looked-up
The reasoning behind choices (ADR-style): options considered, trade-offs weighed, why one path won.
The resulting structure (architecture); a rule to follow (a convention).
The boundaries matter as much as the purposes: they are what let you, and the
assistant, tell two similar-looking categories apart at the edges, so a
rule lands in conventions, the reasoning behind it lands in decisions, and
the structure it shapes lands in architecture.
Every category is binding, whatever its tier. The tiers describe when
knowledge reaches the planning agent, not how much authority it carries. An
entry states how the project works and how work in it must be done, so where
an entry and the current code disagree, the entry states the target and the
code is what has yet to meet it. A codebase that does something else is work
to do, not evidence that the entry has gone stale.
The lifecycle of an entry
Entries are plain markdown files, and you manage them through the
spektacular knowledge commands: create one, search and read it back,
rewrite it when things change, and remove it when it has had its day.
Creating. A new entry is written with knowledge write, naming the tier
and the store to put it in and the path within that store. The content comes
from a file or standard input:
You rarely run this by hand. The spek-knowledge skill proposes the tier, the
store, the path, the tags and the body together, and waits for you to confirm
all of it before anything is written.
The first segment of the path picks the category (gotchas/db-timeouts.md
lands in gotchas) and the entry is stored as raw markdown at
<location>/<category>/<name>.md, with any missing parent directories created
for you. There is no required frontmatter; an entry’s title is taken from its
first heading, falling back to its path. An entry may open with an optional
frontmatter block declaring its tags, covered under Tagging an entry below.
Searching and retrieving.knowledge search runs a keyword query across
the stores the request covers and returns ranked, one-per-document hits, each
with a few short excerpts so an agent can scan many results cheaply. An entry
does not have to contain every word of your query: it is returned if it carries
evidence for any of them, and ranked on how good a match it is. How that
ranking works is set out in full below. Every hit reports the tier and store
it came from, which is exactly what knowledge read needs, so a result can be
pulled back in full without a second lookup and with no risk of fetching a
same-named entry from somewhere else. Add --tier and --filter to narrow
what a search covers.
Terminal window
spektacularknowledgesearch"database timeout"
The two always-applied categories, conventions and glossary, are
deliberately left out of search results. They are already loaded in full on
every planning task (via knowledge always-applied), so surfacing them in
search too would only repeat the same content.
Keeping it up to date. There is no separate update command: you write the
entry again to the same tier, store name, and path, and the new content replaces
the old.
Because entries are identified by the exact bytes of their content, rewriting
the same text changes nothing, while genuinely edited text supersedes the
previous version.
Removing. An entry is removed with knowledge delete, addressed exactly as
a read or a write is: the tier, the store, and the path within that store.
Naming a store the project has not declared is refused, and the refusal lists
the stores available in that tier. Naming a path that simply holds nothing is
not an error: the removal reports success and changes nothing, so a maintenance
pass that retries is safe. The result says which of the two happened, so you can
tell a removal from a no-op without reading the store back.
The one entry that cannot be removed is a category’s own README.md. It is
generated from the project’s definition of that category rather than written by
anyone, so removing it would leave a gap nothing fills; the refusal points at
spektacular init, which regenerates it. Removal is immediate and there is no
undo, which is no worse than it sounds: every store in a normal setup is a
directory under version control, so recovery is whatever git already gives you.
How a search is ranked
To enable the effective retrieval of knowledge base entries, Spek
uses a search algorithm that is based on the content of the knowledge base
entry and any tags that are associated with it.
During the planning process the agent prompts within Spek
instruct the agent to search the knowledge base for entries that provide
information to the task in hand.
The search query
How a query becomes terms. Your query is lower-cased and split on
whitespace into terms. There is no stemming, no synonyms and no plural
handling, so router and routers are different terms.
What counts as evidence. For each term, an entry offers at most two kinds:
a tag match: full strength when the term equals one of the entry’s tags,
and reduced when the two are prefix-related, described under partial credit
below.
body occurrences: the number of case-insensitive substring matches in the
entry’s text. The frontmatter block is not part of the text, so a tag is
never also counted as a mention of itself.
The formula.
damp(n) = 0 if n = 0
= 1 + log2(n) otherwise
tagAffinity(t) = 1 if t equals one of the tags
= len(shorter)/len(longer) if t and a tag are prefix-related
coverage = (terms with any evidence) / (total terms)
score = sum(termScore(t) for t in terms) * coverage ^ COVERAGE_EXPONENT
with TAG_WEIGHT = 8, COVERAGE_EXPONENT = 2 and MIN_PREFIX = 4. Where a
term is prefix-related to more than one tag, the strongest match wins;
affinities do not add up.
These values live in internal/knowledge/ranking.go as tagWeight,
coverageExponent, minPrefixLen and cutoffFraction, which is the single
source of truth for all of them. They are quoted here and in the project’s own
maintainer documentation, so changing one means changing the code and every
document that repeats it.
A worked example. Searching http routing against two entries:
Entry A, tagged http, routing
Entry B, untagged, mentions “http” 8 times
http
tag match, 0 in body: 8 + 0 = 8
no tag, 8 in body: 0 + (1 + log2 8) = 4
routing
tag match, 2 in body: 8 + 2 = 10
no evidence: 0
coverage
2 of 2 = 1.0
1 of 2 = 0.5
score
(8 + 10) * 1.0² = 18
4 * 0.5² = 1
Entry B mentions the word far more often and still ranks well below Entry A,
because Entry A is about both terms and Entry B is about neither.
The cutoff. Once every store has been searched and the results merged, any
hit scoring below CUTOFF_FRACTION of the best hit is dropped, with
CUTOFF_FRACTION = 0.25. The threshold is relative rather than fixed, so a
loosely related entry surfaces when nothing better exists and falls away once
something genuinely relevant is present, and it keeps its meaning as a
knowledge base grows. In the example above, Entry B is dropped: 1 is below
0.25 * 18.
Ordering, and what never appears. Ties break by configured store order and
then by path, so repeated searches return the same order. The always-applied
categories, conventions and glossary, never appear in search results,
because they are already loaded in full.
Tagging an entry
Tags say what an entry is about, independently of the words it happens to
use. They are proposed for you when an entry is captured, drawn from the tags
already in use, so the vocabulary converges without anyone maintaining a
taxonomy.
The frontmatter block. An entry may open with a YAML frontmatter block. It
must be the very first thing in the file, opened and closed by a line
containing only ---:
---
tags: [go, http, routing]
---
# HTTP routing standard
All Go services route HTTP endpoints through chi.
The block form of a YAML list works too:
---
tags:
- go
- http
- routing
---
The rules.
Tags are optional. An entry without a block is perfectly valid and fully
searchable. There is nothing to migrate and no command to run.
Tags are lower-cased and de-duplicated when read, so HTTP and http
cannot both exist as separate tags.
tags is the only key read. Any other key in the block is ignored, so
the block is safe to use for your own metadata.
A malformed or unclosed block is treated as no frontmatter, not as an
error. An entry that happens to open with a horizontal rule keeps working,
and one bad entry can never fail a search across a whole store.
Partial credit. A tag does not have to be typed exactly to be found. When a
search term and a tag are prefix-related, the tag still counts, at a strength
equal to the shorter string divided by the longer:
search
tag
counts as
https
https
100%
http
https
80%
apples
apple
83%
https
https-security
36%
go
golang
nothing
This is why plurals and word forms mostly look after themselves: apple and
apples find each other without either being written down twice. It works in
both directions, so it does not matter which form the tag uses.
Two limits are deliberate. The match must start at the beginning of the word,
so test does not find latest. And the shorter of the two must be at least
four characters, so very short terms like go or id match only exactly,
rather than matching every tag that happens to start with them.
What it does not do is understand words. route does not find routing,
because route is not a prefix of routing. Where a form is genuinely
different rather than merely longer, carry both tags.
You rarely write this block yourself. The spek-knowledge skill proposes
tags when it captures an entry, and shows them alongside the destination for
you to confirm or edit before anything is written.
Auditing the tags you already have. Entries written before tags existed, or
tagged carelessly, can be reviewed the same way. There is no separate command:
invoke the spek-knowledge skill and ask it to audit the knowledge base tags.
The skill works out the intent from the wording of the request, the same way it
tells a lookup from a contribution.
Nothing is read-write until you approve it. The audit lists the entries in
scope, loads the vocabulary in use, reads each entry, and reports two things per
entry:
Unsupported tags, a tag the entry’s content does not bear out, reported
for removal.
Missing tags, a subject the entry is clearly about but carries no tag for,
proposed from the vocabulary already in use where something fits.
Changes are proposed and confirmed one entry at a time. Accepting one
entry’s changes never applies another’s, and declining one does not carry to the
next, so a suggestion you disagree with costs a “no” rather than a cleanup.
Narrow what gets reviewed with --tier and --filter.
The audit is deliberately careful in two opposite directions. It will not tell
an entry tagged https to use http instead, because those are different
subjects and prefix matching already relates them at reduced strength. It will
report apples sitting beside apple as removable, because prefix matching
already reaches it and a redundant tag is how a vocabulary silently doubles.
Configuration
Which knowledge you mean: the two tiers a store belongs to, how it gets its
name, and how a request says which stores it covers.
Knowledge lives in one of two tiers. Every registered repository contributes
exactly one store, holding what is true of that repository’s own code. The
project declares any number of shared stores for knowledge that belongs to no
single repository, a team handbook say, or a personal one you carry between
projects. Every request states which tier it means, and names a store within
it.
One word does double duty, so it is worth separating now. A category has a
retrieval tier, which says when its entries are loaded: always-applied
for conventions and the glossary, looked-up for everything else. A store
has a tier in the sense used on this page, which says which knowledge it
holds. The two are different axes and never appear on the same thing.
A repository declares one store. It lives in the repository’s repo.yaml,
so it travels with the repository into every project that registers it.
Relative locations resolve from the folder holding that file:
repo.yaml
knowledge:
provider: file
config:
location: knowledge
There is no name to set here. The store is addressed by the name the project
registered the repository under, so the repository does not choose it, and
there is no list because a repository has exactly one store.
The project declares its shared stores. These are the ones no single
repository owns, and they live under the knowledge key of
.spektacular/config.yaml, each with its own name. Relative locations resolve
from the folder holding config.yaml, the same base a repos entry uses, so
.. is the project’s own root and ../team-knowledge a folder beside it.
Absolute paths and ~ work too:
.spektacular/config.yaml
knowledge:
sources:
- name: team
provider: file
config:
location: ~/work/team-knowledge
Names must be unique within the project tier. A shared store may reuse a
repository’s name, because a store is identified by its tier and its name
together, not by the name alone.
Addressing a store. Reading and writing name exactly one store, so they
take a tier and a name alongside the path:
Both the tier and the name are required. A write that leaves either out is
refused rather than landing somewhere plausible, and the refusal lists the
names available in that tier so you can reissue it straight away. Run
spektacular knowledge sources to see every configured store by tier and
name.
Narrowing what a request covers. Searching, listing, and the
always-applied load take a --tier and a repeatable --filter:
Search takes one more, a repeatable --tag, which narrows to entries carrying
a given tag:
Terminal window
spektacularknowledgesearch"routing"--taghttp
An entry without that tag is never returned, however well it would otherwise
score, and it is excluded before ranking rather than filtered out of the
results. Repeating --tag narrows further: an entry must carry every tag
listed. Note the asymmetry with --filter, which names alternative stores and
so widens. Tag matching here is exact, unlike the partial credit used for
ranking, because a filter should give you what you asked for and nothing
adjacent to it. spektacular knowledge tags lists the vocabulary in use, and a
--tag naming a tag no entry carries is refused rather than silently returning
nothing.
Omitting the narrowing covers every store the tier reaches, and naming stores
covers exactly those. No store is ever included or excluded implicitly, which
is what lets planning work on one repository skip the standing rules of every
repository it is not touching. Because every store is just a directory of
markdown files under its category folders, the whole knowledge base stays
something you can read, edit, and version directly.
Why it works this way
A few deliberate design choices are worth understanding, because they are
what make the knowledge base trustworthy rather than just convenient.
Knowledge is a planning-time input, not a runtime one. The planning agent
reads the knowledge base while producing a plan and writes the relevant parts
into the plan itself; the implement workflow then works only from the plan. So
the knowledge base shapes what gets planned, and the plan, not a live lookup,
is the contract the implementation is held to. This keeps implementation
reproducible: the same plan builds the same thing, regardless of how the
knowledge base changes afterwards.
One registry is the single source of truth for the category model. The six
categories and their tiers are declared once, in code. That single declaration
scaffolds a new project’s directories and READMEs, decides each category’s
retrieval behaviour, and tells the assistant where a new entry belongs. Adding
a category, or moving one between tiers, is a change in exactly one place.
Two tiers, split by how knowledge is retrieved. Always-applied knowledge
(conventions, glossary) is the small set every plan must reflect and the
vocabulary needed to read everything else, so it is loaded in full each time.
Looked-up knowledge (architecture, gotchas, learnings, decisions) is the
larger reference body, fetched only when a query matches it. Because tier
membership lives in that one registry, re-tiering a category is a single,
self-consistent action: the full-load reader starts including it and search
stops returning it at the same moment, so the two behaviours cannot drift
apart.
De-duplication is exact, not fuzzy. This is a different question from
ranking, and it gets a different tool. De-duplication asks whether two entries
are the same, which is a yes-or-no fact. Ranking asks which entry is a better
answer, which is a matter of degree, so it is scored rather than decided. The
argument that follows is about the first question only, and is not an argument
against inexact ranking.
When the same knowledge is written more
than once, the mechanical check for “this is already here” matches on exact
byte-identity rather than a similarity score. That is deliberate, because
lexical similarity does not track meaning: “always retry” and “never retry”
are nearly identical text but opposite instructions, while “add jitter to
retries” and “randomize backoff intervals” share almost no words yet say the
same thing. Exact matching claims the one thing it can prove, that two entries
are byte-for-byte the same, and leaves every genuinely semantic judgement to a
human or the assistant, which is where that judgement belongs.
The two tiers answer different questions. A repository’s own store answers
what is true of that repository’s code; the project’s shared stores answer what
spans repositories. Neither overrides the other, so nothing is dropped because
of where it lives. When two stores hold related knowledge, a refinement is kept
alongside what it refines and noted rather than hidden, while a genuine
disagreement is surfaced as a conflict naming both stores instead of being
silently resolved.
See it in the workflow
The knowledge base feeds the planning step. See where it fits in the
spec-driven pipeline.