Glossary
Terms this project uses in a specific way, plus a few that were used loosely and should not have been.
If a term is not here and it is not ordinary English, it should be. Add it or stop using it.
docs/LEXICON.md is the companion for somebody reading reports rather than editing the repo: fewer terms, each with why the distinction exists and a real example. This file stays the reference.
This file is for words this repository coined or bent. Terms owned by somebody else - an RFC, a vendor, an industry - live in docs/BORROWED-TERMS.md, and the rule there is different: an entry says what the term means for us and cites the real definition rather than paraphrasing it, because a glossary that restates a standard is wrong the day the standard moves and wrong in a way nobody notices. The owner asked for the split on 2026-09-15 after three email-authentication terms were used at him.
A term that already means something else
If a word is already in use outside this repository and we use it differently, the entry must say so explicitly, and the divergence has to be checked with the owner before it is adopted.
His instruction, 2026-09-17, and the instance that produced it: he looked up P0 and found that our definition is not the standard one. Everywhere else, P0 is a severity a person assigns - often "the site is down, drop everything". Here it is derived arithmetic, value + urgency, and nothing about it means drop everything. Same token, different referent, and the entry below did not say so.
That is a worse failure than undefined jargon. An undefined term makes a reader ask. A term that quietly disagrees with its common meaning makes a reader think they already know, and they carry the wrong model until something breaks. docs/BORROWED-TERMS.md exists for words we take from elsewhere and use as they are meant; this rule is for the opposite case, where we keep the word and change the meaning.
So an entry for a diverging term carries three things: what it means here, what it means elsewhere, and why we diverged. And the divergence is a decision the owner makes, not one an agent makes on his behalf while writing a checker.
Candidates worth auditing against this rule, none of them checked yet: unit, lane, quest, walk, lens, gate, frontier, epic. Each is a common word in software with an established meaning, and each is used here in a way that may or may not match it.
P0, P1, P2, P3
A ticket's priority, and it is derived, never declared. docs/TICKETS.md uses the labels and defines them nowhere, which is why the owner had to ask on 2026-09-17.
points = value + urgency high = 2, medium = 1, low = 0
| points | band | means |
|---|---|---|
| 4 | P0 | both value and urgency are high |
| 3 | P1 | one is high, the other medium |
| 2 | P2 | |
| 0-1 | P3 |
Ask the tool rather than the table: python3.12 scripts/score.py high medium prints 3 points -> P1. scripts/score.py --check fails a ticket whose declared priority disagrees with its derivation, which has caught the coordinator twice.
Which population you count decides the answer, and this entry got it wrong once. "Live" means three different things here: 242 unbuilt and dispatchable, 460 not withdrawn or merged or accepted, 484 not withdrawn. The P0 count over those is 16, 75 derived, and 67 as the declared field on the 460 pool.
This entry first said "67 live tickets were P0 - a sixth of the backlog, so the scale saturated". That was the declared field over a pool including work already built, and the figure that matters is 16 of 242 dispatchable, about seven percent - an ordinary P0 rate. OPS-143 caught it. The saturation claim is withdrawn.
What survives is narrower and still true. Both inputs are self-reported at write time and nothing corrects optimism or revises a score when the world changes, so P0 is a claim about a ticket's author as much as about the ticket. security_impact: high discriminates better because it is a claim about consequence rather than importance.
Always name the population. docs/state.json does - it carries its note and its derivation, and every count here should be read from it rather than recomputed by hand, which is how this entry went wrong.
tokens per unit
The cost of a merged unit in tokens, and the primary measure of whether a way of working is efficient. A unit is what it already means here: one PR-sized, independently verifiable step - an atomic branch.
Deliberately not given a fancier name. docs/PROCESS.md forbids adding a second name for something that already has one, and "unit" was already this repository's word for the thing being counted.
Measured on 2026-09-16, which is the first day anybody counted:
| shape | tokens per unit |
|---|---|
| flat - coordinator dispatches one agent per unit | 299,345 over 8 units |
two-tier - a team lead dispatches members, see docs/DELEGATION.md | ~279,000 per unit-equivalent |
The second figure carries a caveat that belongs with the number: the two-tier unit landed about five units of scope in one, so its per-unit figure is a division rather than a direct observation.
The mechanism that moves it is brief width, not delegation shape. Members under a team lead averaged 200,487 tokens where agents under a coordinator averaged 299,345, because a narrower brief needs less context to execute. An agent's cost is roughly orientation plus work, orientation is close to fixed, and docs/ORIENTATION.md exists to lower the fixed half for everybody.
turns per unit
The twin of tokens per unit, and the one that decides how long a session can run. Every coordinator turn resends its whole context, so turns are what grow the context that eventually ends a run.
The two move in opposite directions and that is the whole tension. Working inline is cheapest in tokens and most expensive in turns. Two-tier delegation is the reverse. Measured the same day: roughly fifteen coordinator turns per flat unit, against about eleven for five units of work under a team lead.
A shape is worth keeping when it improves one without ruining the other. Quoting either number alone is how a shape that merely moved cost gets reported as a saving.
The kit
What a product gets when it is built on this template: skeleton/ plus the components/ it selected, copied into the product's own repository. Not this repository, and not the product either - the kit is what travels between them.
Used in 60 documents under docs/ before it had an entry here, 44 of them as "the kit", which is why it got one: a word that load-bearing with no definition is a word two readers can use differently without noticing.
The distinction that matters: this repository is the factory, the kit is what it ships, and a product is what somebody built with it. docs/WHAT-TRAVELS.md is the register of which files are in the kit and docs/WHATS_COPIED.md of what a copy does. A gate that reads backlog/ or lenses/ cannot travel in the kit, because a product has neither.
A docs/kit directory to hold the documents about it is agreed but not yet built - saas-main's ruling of 2026-10-07, planned in docs/DOCS-STRUCTURE.md.
The pipeline
Unit - one piece of work that goes through the seven stages of docs/PIPELINE.md. Sized so it lands before its worktree goes stale.
Stage - one of the seven steps a unit passes through: request, contract, build, review, integrate, verify, hand back. Every stage boundary is an agent boundary.
Contract - contracts/<id>.md. What a unit must do, expressed as acceptance criteria that are executable commands. Written before the builder starts.
Criterion - one Given / When / Then with an exact command, its working directory, its environment, and a Source: line of confirmed or plausible. A criterion phrased as prose is not a criterion.
Signature - integrations/<id>.md. The integrator's record of having wired a unit into the running system and run the contract against it, with real pasted output. May be a rejection. Goes stale when a file the unit owns changes after signed_at.
Verdict - what a verifier appends to a signature. One of CONFIRMED, CONFIRMED WITH GAPS, SIGNATURE REJECTED (the work is sound, the claim was overstated - returns to stage 4), or UNIT REJECTED (returns to stage 2).
Gate - an executable check that fails the build. See docs/VERIFICATION.md. A rule with no gate is a comment.
Role - a member of the team, with its own material and a prompt about who it is. security, design, product, devops, tech-lead, fullstack and a writer. A role has a session, a branch and authority: it decides some things alone and escalates others, and roles/<name>.md states which. The owner's words, 2026-09-20: "it's like the team we're building... each should have his own material and stuff, and each could run a review over the repo."
Persona - a simulation of someone. Advises and reviews, never decides or does. The idols are personas - PROD-074 holds ten names in six stances for design. A persona that can open a pull request is a role wearing a costume, so the constraint wants to be structural rather than a convention.
Consultant - not a word here. The owner introduced it on 2026-09-20 and retired it in the same conversation: "so the personas function as consultants." One thing, one name. It is recorded only so that a reader meeting it in an old transcript knows it means persona.
Lens - lenses/<group>/<id>.md. A runnable check with a scope, with a mandatory non-empty runs list of commands it must execute. A lens that only reads cannot detect that code does not run, which is lesson 5.
This entry used to read one reviewing perspective, which made a lens an identity and collided with role. The owner ruled on 2026-09-20 that "lens was for code review from different roles" and that role is the better word, so a lens is now what a role runs rather than who is doing the reviewing. Whether the word survives at all is open - PROD-095. The 25 files, the review-policy.toml mapping and make lens-gates are unchanged either way.
Role card - and this is a collision, not a synonym. A role card is components/<name>/README.md, describing what a component is responsible for. It has nothing to do with the team sense of role above. The two words were coined eight days apart for unrelated things and the older one has 20-odd call sites. Named here rather than renamed, so that the next reader is not the third person to be caught by it.
Path - a long-lived workstream in docs/PATHS.md. A path does not finish, it advances. Units live inside paths.
Handover - an entry in HANDOVERS.md: a change an agent could not make because another agent owned the file, with the exact lines and the owner.
Role card - components/<name>/README.md. Frontmatter stating a component's role, owns, does_not_own, depends_on, provides and wires_into.
Slots, and the surfaces that have them
The owner asked what "nine slots over seven surfaces" meant on 2026-09-15, after it appeared in a message to him. It was internal shorthand and it should not have been.
A surface - a place a person meets the product without meaning to. Seven of them: the 404 page, the 405, the invite email, an empty list, an error under a form, the account-deleted page, the beta banner, and the sign-out message. Each exists in the kit as a mechanism - the code that decides when to show it - and each needs words, which the mechanism cannot supply because the words are the product's and the mechanism is the template's.
A slot - a labelled blank the founder fills. A heading in the product's own src/app/VOICE.md, with a sentence under it. Write the sentence and the page uses the founder's words; leave it and the page uses the template's default, which is deliberately usable rather than a placeholder, because a founder who has not written any words yet is the normal case.
Nine slots over seven surfaces, because the count is per slot and not per surface: the 404 and the 405 are two pages with two sentences, and an empty list needs two things - a sentence, and the name of the control that fills it. That second one is the only slot in the kit with no default at all, because no template can name a button in a feature it has never seen, and a wrong name sends a person to press something that is not on the page.
Why a slot rather than just editing the template. A slot is checkable. A flow declares the headings its output must fill, accept refuses an output that left one empty, and make surface-slots counts how many of a product's nine carry its own words. Editing a template file is a change nothing can count.
The one invariant
The one invariant - the single sentence that, if it is false, means the product is broken rather than incomplete. The owner asked what it meant on 2026-09-15 and it had been used at him repeatedly without being written down.
It is not the feature list and it is not a goal. docs/PRODUCT-PATH.md step 2 gives the test for whether you have found one: it contains a WHERE clause, or it names a permission. Usually both.
"Users can save notes" is a feature. "The WHERE is the authenticated caller's own id, and no route takes a row id" is an invariant.
Three real ones from this repository:
- Scratchpad: your scratchpad cannot be read or written by anyone but you.
- Shared phone book: what you may do to a contact depends on your role in its group, and on nothing else.
- Ati's toy shop: an order that was paid for is recorded exactly once, and is visible only to the buyer who placed it and to the shopkeeper. Its
BRIEF.mdthen says which half is which - a permission (shopkeeper) and aWHERE(the buyer's own id) - and names the file that attacks both.
Why one and not several. A product with five invariants has none, because nothing ranks them when they conflict. The single sentence is what a reviewer attacks first and what a builder may not trade away for a feature.
Why it is the first item on the minimum handoff bar. It is simultaneously the product's purpose and its security model, so a builder who has it can derive the access-control matrix, and a builder who lacks it invents one. It is also the thing a founder can answer in a sentence and an agent cannot guess.
The industry's specification documents
Seven artifacts the product-management literature recommends. The owner had not met the acronyms and they were used at him in an agent brief on 2026-09-15, so they are written down. Each entry says what it is and what, if anything, of it is worth keeping for one founder whose builder is an agent.
PRD, product requirements document - what the product does, for whom, and why. The least ceremonial of the seven and the closest to what the interview in PROD-042 produces. The useful kernel is most of it.
BRD, business requirements document - business objectives, stakeholders, expected return. An artifact of procurement: it exists so one department can justify spending money on another's behalf. There is no second department here.
MRD, market requirements document - market size, segments, competition, positioning. Written to persuade someone to fund a bet. The useful kernel is narrow and the owner already derived it independently: what similar products exist, what do you like about them, what do you dislike. The dislikes are where uniqueness comes from. That question survives. The market-sizing does not.
SRS, software requirements specification - numbered, individually testable requirements, formalised by IEEE 830 (1998, withdrawn 2011). Heavy, and its weight is the point in avionics and medical devices. The useful kernel is already in docs/PRODUCT-PATH.md step 4: write the criteria as attacks. An attack is a testable requirement that names its own failure.
TDD, technical design document - how it will be built: architecture, data model, interfaces. For this repo the template is most of the TDD, decided once and inherited. The per-product remainder is small, which is a property of having a template and not a reason to skip it.
Journey map - the steps a user takes toward a goal, with what they need and what they feel at each one. Distinct from a user story, which says what a user does. A story can be entirely satisfied while the journey is bad. The owner's own first-use findings were journey findings rather than story findings: "after sign in you get to a dead end", "page looks too long", "no logout". Nothing in the current document set would have caught them, and DOC-065 checked that story by story: six stories against three findings is eighteen cells and no hits. docs/PRODUCT-JOURNEYS.md is the proposed format, and it drops the feelings field - an adjective per step that nothing can check is prose that rots - keeping the founder's own "feels sloppy" in each step's prose instead.
Click resolution - the owner's phrase, 2026-09-15: "well-defines user stories I think to the CLICK resolution". A story at feature resolution ("a child spends points") is satisfiable by an implementation he would reject on sight. At click resolution a step names its surface, one string that must be on the screen, the control that is pressed by the words on it, and which step that press lands on. The last one is what makes the resolution checkable rather than merely finer: a dead end is a step with no successor, which is arithmetic. docs/PRODUCT-JOURNEYS.md.
RACI - a matrix of who is responsible, accountable, consulted and informed for each task. An org-chart instrument. One founder, so every cell is his name.
The product lifecycle
אפיון - Hebrew, the Israeli software industry's term for the requirements-definition phase: the work of deciding what a thing must do before anyone builds it. The owner uses it for the state between scaffold and built. Spelled spec in code and frontmatter, because this repo is ASCII throughout, and both spellings mean the same state in docs/PRODUCT-LIFECYCLE.md.
The interview - the conversation at spec that turns a founder's intent into committed documents a coding agent can read cold. For the builder, factory-side, never ships. Not the invite.
The invite - the mechanism by which somebody who is not the founder gets into a built product. For a reviewer or a client, kit-side, ships. Runs at review and later. Not the interview.
The minimum handoff bar - the eight things that must exist before the interview hands off to a builder, each one something an agent in this repo has previously had to guess. Listed in docs/PRODUCT-LIFECYCLE.md.
A ticket's end, and its last review
He asked on 2026-09-17 for "status should move to done once the ticket is completed", and the answer is that the word already exists twice and is not done. Recorded here rather than only in chat, per CLAUDE.md.
Done - not a status in this repository, and deliberately. backlog/schema.toml retired the spelling because every historical done in this backlog meant "merged" while stage 7 means "a human decided it was what they wanted", and one word for two states is the defect closed already was. The two live spellings are below.
merged - stage 6. On main, with CI green. Declared by the gate, not by a person. Twenty-four tickets sit here.
accepted - stage 7, and the real end. A human decided the work is what they wanted. Nothing in stages 1 to 6 may declare it, which is the point of it existing separately from merged.
withdrawn - the other terminal answer, and not a synonym for the first two: the work will not happen, because it was superseded or the finding was wrong. It needs a reason. A withdrawn ticket is not a deleted one - the file and the id stay, so a commit message saying Ref: CORE-040 keeps resolving to the ticket its author meant.
Reviewable - a ticket the review sweep can ask about: an unbuilt one. merged, accepted and withdrawn are filtered out by gen_state.py's stage_of, which is the mechanism behind the owner's "then it can easily filter them out" and needs no new word either.
last_reviewed_on - the date somebody last re-decided a ticket still matters as written. Not the date the file was last touched, and the distinction is the whole field: a typo fix or a regenerated blocking: line is not somebody re-deciding anything, and a date inferred from one would silence the sweep for a full threshold. Written by hand or by scripts/check_ticket_lifecycle.py --reviewed <ID>, absent when nobody has, and seeded on the 510 tickets that predate it with their filing date - because filing is the first review.
The sweep - the list of reviewable tickets whose last review is older than the threshold, ordered oldest first and capped by a limit, each row naming which of keep, re-score or retire it is asking for. docs/REVIEW-SWEEP.md. The threshold decides who is eligible and the limit decides how many are asked for, which are two different parameters: on a backlog written in one week the first alone produces 247 rows on one morning.
Measurement
The walk - the path from an empty directory to a serving application. Currently 15 steps. Asserted by tests/skeleton/test_scaffold_contents.py and the scaffold benchmark, not typed into a document.
Mutation - a deliberate, named break in the code, made to find out whether a test would notice. You change one thing that ought to matter, run the suite, and require something to go red. If nothing does, the test does not check what its name says it checks - it passes whether the code is right or wrong, and it will go on passing after somebody breaks that behaviour for real.
The word is used here in the sense of mutation testing, and it is a property of the test, not of the code. "Watched fail under a named mutation" in a report means: here is the break I made, and here is the test that died. A report that lists tests without that is listing things that ran, not things that would have caught anything.
Named is load-bearing. "I mutated the code" is unfalsifiable; "I removed the WHERE owner_id = :user clause from the signups lookup and test_signups_are_scoped_to_the_owner failed" is a claim a reader can re-run.
A surviving mutant - a break no test noticed. It is a finding, not a failure, and it gets reported rather than quietly fixed: it names the property that is asserted and unguarded. Some are equivalent mutants, where the change genuinely cannot alter behaviour, and those are reported as such rather than counted as coverage.
Three things learned about this the hard way, all on 2026-09-13:
- Mutate the code, not the documentation. Two mutations of the phone book reported green because the pattern matched a copy of the code inside a module docstring. A green mutation is worth distrusting.
- The test must call the thing.
scripts/reserve_ticket_id.py's first self-test assertedmax(taken) + 1 == 10, which is arithmetic, and stayed green under the exact mutation it was written to reject. The allocator became a function the case calls, and the mutation then killed two cases. - Never mutate the shared tree. Several agents work in it at once. Copy the tree, mutate the copy, and prove the copy is what ran.
Step versus workaround - a step is work a founder must do. A workaround is a defect wearing work's clothing. Tracked separately, because workarounds trending to zero is the real signal. 32 steps with nine workarounds became 15 with none.
Layer - one of the four things needed for a first dollar: a URL, a signup surface, email that arrives, money. Any claim to advance revenue must name its layer.
Confirmed versus plausible - confirmed means a command produced output demonstrating it, and that output was quoted. Plausible means it was reasoned about. Reading finds real things and may not claim confirmation.
Pinned - a reproduction recorded with its exact command, working directory, environment and commit. An unpinned reproduction is not one: two reviews reported mypy at 20 errors and at 28, both honestly, because neither had pinned the invocation.
Spending the quota
Point - one percent of the weekly token quota. scripts/budget.py says "this half may spend 12.3 points" rather than "12.3%" to keep two different numbers apart: the percentage already used, which is a fact, and the allowance still available to spend, which is an arithmetic answer from the owner's own rule. They are both percentages of the same thing and confusing them is easy.
The coordinator coined it and used it for a day before the owner asked. It is this repository's word, not an industry one, and plain "percent of the week" would have done.
Deep agent - one given a long window and told to reproduce, break and understand before fixing. Two to three hours, two or three at a time.
Wide dispatch - the opposite shape: many short agents, each on a narrow task, optimised for wall-clock throughput. Five or six at once, thirty to sixty minutes each.
The distinction is about what the work needs, not about generosity. On 2026-09-13 the wide shape was right while the owner was away and produced sixteen merged units in an afternoon. The same day proved where it is wrong: the findings that mattered most came from agents that constructed a failure rather than reading for one - four concurrency overlaps the repository had recorded as unreproducible, a guard emptied of the audit table it was built to protect, a health check that answered 200 with every table dropped. None of those is found by an agent working to a forty-minute rhythm.
Cost follows the same split, and it is per agent-run rather than per hour: three agents working three hours cost roughly what three working forty minutes cost, so depth is nearly free and width is not.
Under load
Three different questions, often said as one. None of them has been asked of this template: nothing here has served two requests at once except by accident, and every timing in the repository is a single request on an idle laptop.
Load testing - does it work at the volume you expect. Fixed rate, realistic mix, and the answer is a latency distribution rather than a yes.
Stress testing - where does it break, and how. Push past the expected volume until something gives, and the finding is which thing gives first and whether it fails closed. A connection pool exhausting is a different answer from a worker queue growing without bound.
Soak testing - does it still work in six hours. Same modest load, held. This is where leaks, unbounded tables and log volume show up, and it is the one most often skipped because it is the only one you cannot run in a minute.
Related and separate: contention, two agents or requests wanting the same thing at once, which is what FOR UPDATE SKIP LOCKED and the advisory lock in alembic/release.sh are for. A property under contention is not tested by running the same test faster.
Failure directions
Fail closed - when a control cannot do its job, deny. A door that locks when the power dies. An authorization check that refuses when it cannot reach the identity provider. Safe, and it can take a working system offline.
Fail open - when a control cannot do its job, allow. A door that unlocks when the power dies. An entitlement check that grants access when the payment provider is unreachable. Available, and it is a security hole the moment the control breaks.
Neither is universally right, and choosing is a real decision rather than a default. A fire exit fails open on purpose, because being trapped is worse than being burgled. A vault fails closed for the opposite reason.
This project has made the choice explicitly in two places: billing entitlement fails open with a staleness window, because a payment provider outage must not lock out paying customers, and RATE_LIMIT_REDIS_URL fails closed with a named error rather than starting an app whose rate limiting silently does nothing.
Fail loud - the property that is not optional either way. Whichever direction a control fails in, the failure is reported somewhere that stops work. A control that fails open and silently is the dangerous combination, because its presence is the reason nobody looks. See docs/PROCESS.md.
Shapes of defect
False green - a check that passes for a reason unrelated to the thing it names. Not a bug in the code under test, a bug in the test: it would pass whether the code were right or wrong, so it reports health it never measured. grep -q 'get_logger' health.py satisfied by an unused import. A deploy step called "confirm the schema is at head" passing because head=none and current=none really are equal. A self-test asserting max(taken) + 1 == 10, which is arithmetic and true of every implementation including the one it was written to reject. The standard way to find one is a mutation; the standard way to ship one is to write the check before the thing it checks exists.
Declared but unwired - configuration or metadata that exists, parses correctly, and reaches nothing. rate_limit_auth read by nobody while routes hardcoded the value. A config field looks identical whether or not anything reads it.
Green on unverified state - a check reporting success over something it did not examine. saas check passing on a project that could not import.
Two conventions for one thing - parallel work diverging on a name and nobody choosing. Four instances, including a registry reached by eight names for four operations under a false "for compatibility" comment.
Seam - the interface where two pieces of work meet. Two agents on one seam is how this repo got two registries. Fan out on files, never on a seam.
Carve-out - a deliberate exception to a rule, stated in the rule itself. A CLAUDE.md carrying no frontmatter is a carve-out from the schema.
Gap marker - GAP(TICKET-ID): one sentence in a comment, at the place a reader meets the problem rather than only in backlog/. Listed by scripts/list_gaps.py, which fails if the ticket id does not resolve. A founder hits a gap while running a command, not while reading a backlog item, so the marker puts the explanation where the surprise happens.
Dispatch ledger - DISPATCH.md. What agent work is in flight, on which branch, cut from what, holding which files, and told not to do what - plus the queue behind it. It exists because on 2026-09-12 the coordinator stopped while it still had work it wanted to do, and on 2026-09-13 it found a three-deep branch stack by accident: both times the state of the work lived in one session's context and nowhere else. A file makes the session disposable. scripts/gen_roadmap_status.py reads its Files column, so the roadmap stops offering work somebody already holds.
It is the name the owner asked about, and it turns out to name two things, so the pair is worth stating together:
The ledger - docs/ledger.jsonl, written by scripts/ledger.py. One append-only line per event that has already happened: a request in his own words, with whether it is open, partly or answered, or a dispatch with its subject, the tickets it touched, how many agents, how many tokens and what came of it.
The distinction from DISPATCH.md is tense, and it is the whole reason there are two files. DISPATCH.md is present - what is in flight right now - so it is rewritten whenever that changes and it is stale within a day by design. The ledger is past, so no line in it is ever edited, which is what makes it safe for five agents to append to at once and what makes git log on it a history.
Requests are in it because the owner asked for them on 2026-09-17: "if we're building a product, we need to keep a very good log of, like, what did we ask, what did we do, like, in the pretty high level". Before that the only record of what he asked was a chat transcript, and recovering 70 requests from it took an agent a whole session - reviews/owner-request-trace.md, which is that pass and which nothing updates. A line written when the request arrives costs seconds; an archaeologist costs a session and loses his exact words on the way.
scripts/gen_state.py reads it into docs/state.json, so anything that wants to show what is open, what was answered and what it cost reads one document.
Watchdog - a timer that resets on every sign of life and fires if nothing arrives before it expires, on the assumption the thing it watches is hung. Ordinary computing, not ours. The agent harness runs a stream watchdog over each subagent's output: 600 seconds with no progress and it declares the agent stalled and kills it. That happened once on 2026-09-13 and cost nothing, because the agent had committed nine times and left a clean tree - which is why CLAUDE.md says to commit the skeleton before building on it.
Capability URL
Capability URL - a URL that is itself the credential. Holding it is the authorisation: there is nothing to sign in to, and the secret is a path segment or a query parameter.
The owner asked on 2026-09-14 whether this is an anti-pattern and whether it is a common thing to do. It is common, it has a name, and it is not an anti-pattern. The W3C TAG published guidance on it in 2014 rather than a warning against it, and it is what these all are:
- a password-reset link in an email
- an address-confirmation link
- Google Calendar's "secret address in iCal format"
- an unlisted video, a Doodle poll, a Dropbox share link
It exists because it solves a real problem: the person who needs the link often cannot sign in, by definition. Somebody resetting a password has lost the only credential they had. That is the case this template's own components/auth/services/email_token.py makes, and it is right.
The three hazards, which are the reason it has guidance
A URL is not treated as a secret by anything that handles URLs.
- Logs. Web servers, proxies and application middleware record paths as a matter of course.
- Referrers. A browser sends the current URL to any third party the page loads, unless a referrer policy stops it.
- Sharing. A URL is the unit people paste into messages, and it looks like an address rather than a password.
Where it went wrong here, which is the useful half
Ati's Toy Shop used one as a child's standing identity rather than for one short-lived action. All three hazards then arrived, two of them in one night:
CORE-038: the structured log recordedrequest.url.path, so every page view wrote a child's token into the log stream.COMP-068:COMP-058's new account bar copied the request path into?next=, publishing the token into a query string on/login.- It could not be rotated. The database stores only
sha256(token), five were minted once, and changing one meant telling a child why.
So the pattern was not the mistake. Using it outside the range where it is correct was, and the range is: short-lived, single-purpose, and for somebody who cannot sign in yet. A durable identity is none of those three. PROD-040 replaces it with a password the shopkeeper sets.
See also declared but unwired for the other defect class this repository names, and docs/LESSONS.md lesson 22.
Conventions inherited from elsewhere
.devcontainer - a directory holding a file that declares the machine a project needs: the operating system, the language version, the command line tools, and any services like a database. A host that understands it builds that environment from the declaration rather than from somebody's memory. VS Code, GitHub Codespaces and cloud development environments all read it.
This repository does not have one, which is why the owner could not tell which of two offered cloud environments to choose on 2026-09-17 - the question has no correct answer while nothing states the requirements. OPS-189 is that gap. What a declaration here would have to name, from what today's work actually needed: python3.12 as a bare interpreter, because scripts/ runs without a virtualenv on a clean clone; uv for make setup; make, git and ruff; and a Postgres, which is the one that decides whether a host can verify anything, since make test-postgres and the mutation probe both need a database and make already prints the docker run line when it is missing.
It is the same idea as this project's declared-beside-the-code pattern, applied one level out: personal_data.py declares which columns are personal, egress.py declares what leaves, and a devcontainer declares what the build needs. All three replace a fact somebody has to remember with a fact a machine can read. The asked-for term entered the glossary on 2026-09-17 under the rule in CLAUDE.md.
The debt - how far ahead of an even pace the week's token spend is, in percentage points, and the clock time it clears at if nothing is spent. The word is the owner's: on 2026-09-18 at 05:52 UTC he wrote that "debt is the only thing that's growing", in the main session's transcript. Making it the term, on 2026-09-19, was the coordinator's call and is pending his confirmation. An earlier draft of this entry quoted him saying something he never wrote, which is why the quote now carries a time and a place.
Elsewhere, debt is money owed to somebody, and it clears only when it is paid back. Here nobody is owed anything. The word was kept anyway because it is the one the owner reached for, and a term he already uses costs him nothing to read. The difference is the paragraph below on how it clears.
Read it off scripts/budget.py:
even pace says 66% by now
actually at 67% - OVER by 1
the debt 1.5h - clears today 20:33, spending nothingOne point is 1.68 hours, because pace is a straight line across the seven days ending at the reset and there are 168 of them. So the conversion needs no modelling and the two halves are the same fact twice.
It clears by waiting, not by repaying, which is the way it differs from ordinary debt and the reason a reading of OVER by 10 is information rather than an alarm. The line rises at 100 points a week while spending nothing holds used still, so the gap closes on its own. The question it answers is how long would we have to be quiet, and that is usually the wrong thing to do - the useful version is what the spend bought.
When it cannot clear, budget.py says so rather than printing an hour that is past the reset: silence cannot bring the week even, and only spending less from there changes anything.
The trap worth knowing: a small debt late in the week is worse than a large one early. Ten points over on a Monday is ten points the week can still absorb. One point over on the Sunday before a Tuesday reset is a week with almost nothing left, and the number looks better.
Control plane - borrowed from networking, where a router's control plane decides where traffic should go and its data plane actually moves the packets. Two jobs, deliberately separated, because deciding and doing fail differently and should not fail together.
Here, adr-014's control plane is the thing that decides and configures: which products exist, who owns each one, which components each has, what a product's design decisions were, and when it deploys. It runs the interview and it holds the founder profile.
The data plane is each deployed product, serving its own users. The toy shop taking an order is data plane. Deciding that the toy shop should exist and have billing installed is control plane.
The property that makes the distinction load-bearing rather than vocabulary: the control plane must be able to be down, or deleted, without any product noticing. A product whose sign-in stops working because the factory's website is offline has not separated the two. This is adr-013's rule - shared infrastructure carries its own exit - applied to the control plane itself, and it is the reason a product keeps its own saas.toml, its own deployment credentials and its own database rather than being served them at run time. The control plane writes into a product's repository. It never becomes something the product calls.
The owner asked for this definition on 2026-09-17 and the same conversation produced adr-014, so the entry and the decision are the same age. The test to apply when anything is proposed for the control plane: if the factory's site were switched off tomorrow, would a running product care? If yes, it belongs in the product.
/healthz and /readyz, the trailing z - a Google-ism, inherited through Kubernetes. Google's internal services exposed /statusz, /varz and /healthz, and the z existed to namespace operational endpoints away from product URLs on the reasoning that no real page would ever be called that. Kubernetes adopted /healthz and it became the de facto probe convention, so an operator recognises it on sight.
It is not about infrastructure-as-code, which is a reasonable guess and not the history. The two endpoints differ in what they promise: /healthz answers without a database and says the process is up, /readyz reaches the database and says the process can serve. A load balancer wants the second.
Renaming them now would touch the contracts, the tests, the gates and the OpenTofu health checks, so the cost is real rather than cosmetic.
Words used loosely, and what they meant
Prong - one branch of a multi-part argument. Borrowed from a sub-agent's report and then repeated without defining it, which is the thing this glossary exists to stop. Ritchie's review argued the adapter registry was wrong on two counts, types and size: the typing work answered the first and left the second untouched. Say "the size half of the argument" instead.
Wince - to flinch. Used to describe a habit of prefacing corrections with self-criticism. The corrections are useful, the flinching is not. Plain English would have been "apologising".
Blind spot - used for a review system that cannot see absence. Accurate but vague. The precise statement is lesson 9: a correct check over an input that omits the subject.
Fan out - launch several agents in parallel. Ordinary usage, kept, because the rule attached to it matters: fan out on files, not on features.
Accrete - to accumulate by gradual addition, with no single decision responsible. Used for what happens to a template that adds a capability because it might be wanted: components/core/ accreted models, schemas, routes, config, middleware, logging and adapters, and no one commit did it. The plain word is "pile up", and the reason it needs naming at all is that accretion has no author to argue with.
Theatre - process performed for its appearance rather than its effect. Seven pipeline stages for a typo is theatre, and theatre gets skipped, and then the pipeline protects nothing.