0124 — Sort docs.propflowai.co by WHEN a page's content is resolved, not by whether it has state
- Status: Proposed
- Date: 2026-08-03
- Amended 2026-09-01: the Apps section on the docs index is removed on Gera's call ("we have an apps one, can we delete this?"). It held one page. App-surface pages now list under Artifacts with everything else; the
surface: appmarker stays on the page (publish-time vs read-time is still the right distinction for the page itself — it just no longer earns a tab). - Deciders: Gera (raised it), Fede
Renumbered from 0123 on 2026-08-03 (collision: this file and ADR-0123 (scheduled-job placement) were authored in parallel under the same number, neither referencing the other —
origin/maintopped out at 0122, so both picks were correct. That ADR's PR (#4837) was older and carried an APPROVED review at its exact head, so it kept 0123 and merged first asef54d3be9). 0124 was verified free at renumber time againstorigin/mainand against all 20 open PRs, following the 0113 and 0117 precedents.This is the third recorded collision of this kind. See A note on ADR numbering at the end — it is a process finding, not an accident.
Context
docs.propflowai.co has two categories today: ADRs (rendered from markdown in
propflowai/docs/adr/) and artifacts (standalone HTML in
propflow-docs/artifacts/, published by bin/docs). A closed 15-tag vocabulary
(TAGS.md) is the only sub-classification. The corpus is ~99 artifacts and ~123 ADRs.
Both are fully version-controlled — the difference is review, not versioning.
bin/docs commits, pushes, and then proves the file is at origin by reading the blob
back (require_pushed, bin/docs:49-71), refusing to print a URL if it cannot. What an
artifact lacks is a PR and a reviewer, not a git history. This distinction matters below,
because "needs review" is one of the things that genuinely separates the surfaces and
"is versioned" is not.
Gera asked for a third category, "apps", and in doing so contradicted himself in a way that turned out to be the useful part. Verbatim, 2026-08-03:
"I do want to live in that page [
/a/god-today], but maybe we have three things: ADRs, artifacts, and then like apps. And this is gonna be like a per-developer app, so it's not something we would want on like the dev tools, because I want it to have a state right. I'm using the KV store and it's just probably not a good place for state.I do think artifacts for like those open-ended decisions is fine, like these [
/a/blocked-5cff2f82]. But then whenever you — nothing open, good — whenever you like click it, then it's gone and then it's kind of in the way right. So maybe those should be under apps. But then it's like kind of confusing because it's like it's an artifact I guess."
Three things are tangled there and they have three different answers: what separates the surfaces, where decision pages live once they're answered, and whether KV is the wrong store. This ADR separates them.
Later the same day he added four things that settle parts of it, and they are treated as input rather than as one more opinion to weigh:
- The apps section is confirmed — "natively on that docs page, just have another one which is apps." Not a hypothesis; a decision (Consequence 1).
- His axis for what makes something an app rather than a dev tool: "It's not a dev tool. PropFlow dev tools are for the system, for the website. This is more for the machine, for the agents." This is a subject axis, and §0 shows it governs a different boundary than the one this ADR was asked about — a boundary the ADR had not asked about at all, and got wrong by omission.
- The forcing case is a God app — the agent view in app form, with decisions answerable inline — plus possibly a Smith app over Temporal. Its hardest constraint, said twice: "it should NOT be the source of truth. Source of truth is always Claude / the files."
- Answered decisions must get out of the way — a requirement on the view (§3).
Designing the God app is out of scope: a separate workflow owns it. This ADR is the taxonomy; that app is the first thing to live inside it (§5).
Decision
0. Two axes at two levels — they compose, they do not compete
Placement takes two questions, and conflating them is what made this hard:
| Question | Axis | Answer |
|---|---|---|
| Which site does this belong on? | Subject — is it about the product or about the machine? | product → the PropFlow app (/admin/dev/*); machine → docs.propflowai.co |
| Which section of the docs site? | Resolution time — publish-time or read-time? | ADR / artifact / app |
The subject axis is Gera's, in his words (2026-08-03): "It's not a dev tool. PropFlow
dev tools are for the system, for the website. This is more for the machine, for the
agents." That answers a question this ADR originally failed to ask — why doesn't
god-today just live in /admin/dev/tools? — and it answers it correctly. PropFlow's
dev tools are surfaces onto the product: /admin/dev/status, /admin/dev/metrics,
/admin/dev/brain, /admin/dev/pipeline-lab. A view of the agent fleet is not one of
those, and putting it there would make the product's admin section partly about the
machine that builds the product.
The two axes are orthogonal, and all four quadrants are populated by things that already exist:
| publish-time | read-time | |
|---|---|---|
| product subject | product ADRs, turnover-architecture, collections-readiness-deck |
/admin/dev/status, /admin/dev/metrics, /admin/dev/brain (PropFlow app) |
| machine subject | agent-operating-manual, coordinator-pattern, fleet-drive-methodology |
/a/god-today, /a/decisions → the apps section |
Subject does not separate artifact from app, and the corpus says so loudly. Tested:
24 pages carry the agent-ops tag (machine subject), and 22 of them are documents —
the 72 KB operating manual, the fleet learnings, coordinator-pattern,
definition-of-done, verification-learnings, model-inventory, the three
claude-md-* audits. Using subject as the artifact/app separator would file all 22 as
apps. Misfile counts on the same corpus: subject 22, state 6, resolution-time 0
(with 4 flagged ambiguous, §2). So Gera's axis is adopted for the boundary it actually
governs — which site — and resolution time governs the one this ADR was asked about.
1. Within the docs site, the axis is publish-time vs read-time resolution
An artifact's content is resolved when it is published. An app's content is resolved when it is read.
Equivalently, the test you can apply to any page: if the thing this page is about changed right now and nobody re-published, would the page be WRONG? If yes, it is an app. If it would merely be older, it is an artifact.
Gera stated the same rule from the other direction, as the hardest constraint on the God app and said twice: "it should NOT be the source of truth. Source of truth is always Claude / the files." That is this axis in constraint form — an app is a view of a record it does not own. The definition and the constraint were arrived at independently and are the same statement, which is the main reason to trust it.
Under this rule:
| Surface | Where the record lives | When content resolves | Governance |
|---|---|---|---|
| ADR | markdown in propflowai |
publish time | PR + review |
| Artifact | the HTML file itself — the page is the record | publish time | bin/docs, versioned, no review |
| App | a store that moves on its own (a ledger, a table) | read time | a Function + a store |
An artifact is a claim about a moment. A postmortem written on 2026-07-31 is never wrong later, only older, and its date is the point. An app is a claim about the present, and a stale one actively misleads.
We rejected "an app is a page with state", which is the axis Gera reached for, because
it misclassifies the corpus at both ends. Fifteen artifacts carry data-pf-state.
Five of them — agent-operating-manual (72 KB), monday-readiness-audit-2026-08-01
(86 KB), turnover-overnight-report-2026-07-30, haiku-fast-tier-audit-2026-08-02,
propflow-cards-and-cash-2026-08-02 — are long-form documents that happen to embed a
few question boxes. Nobody would call an 86 KB audit an app. And god-today.html, the
one page that motivated the whole category, contains zero data-pf-state, zero
<script>, zero fetch(, zero setInterval — verified by grep with a positive control
on the same file. The state axis excludes the page the category exists for and pulls in
five documents. It is the wrong axis.
The reason it fails is worth stating, because it also settles §3: the shared-state
layer is what lets an artifact carry live data without becoming an app. The injected
client (build.mjs, STATE_CLIENT) fetches answers at read time and paints them onto
static bytes. So an artifact with answers is still resolved-at-publish-time for
everything that matters — the annotations are live, the document is fixed. That is a
feature, not a category error.
2. Sorting the real corpus — 8 apps, 91 artifacts, 4 flagged at-risk
Run against every file in artifacts/, the axis produces 8 apps and 91 artifacts,
and the 8 are exactly the pages generated by a script from a record that keeps moving:
Apps (page is a view of a live record kept elsewhere):
| Page | Upstream record | Generator |
|---|---|---|
god-today |
~/.claude/jobs/god-ledger.jsonl |
god-daily render |
decisions |
~/.claude/jobs/blocked/*/*.json |
decisions-dock |
blocked-441e839c, blocked-5cff2f82, blocked-616c7e1f, blocked-cb989158, blocked-cc4ad587, blocked-fa126b98 |
the same blocked ledger, per session | blocked |
Artifacts (the page is the record) — the other 91, including every audit, RCA,
postmortem, research brief, deck and prototype: deposit-escalation-postmortem,
email-death-path-audit, turnover-architecture-2026-08-01, funding-research-2026-05,
collections-readiness-deck, vendor-front-gate-recon, arsenal-salvage,
propflow-flow, and so on. Also here, deliberately, are the five state-carrying
documents named above and the standing decision pages rent-reminder-decisions,
eval-floor-decisions, ten-open-six-questions-0803, eight-open-five-questions,
built-three-one-question — their questions are fixed text; only their answers are
live, and answers arrive at read time.
Four pages sort ambiguously, and that is the most useful output of the exercise.
needs-you-2026-08-03, fleet-status, parked-decisions and
pending-decisions-2026-08-02 are hand-frozen dated snapshots of a moving record.
"Record lives elsewhere" says app; the date in the title says artifact. Both readings are
defensible, which is exactly why these are the pages that rot — a snapshot of
something live, published under a name that promises currency. The axis does not
eliminate them; it names them, and the ambiguity is the smoke detector. Their correct
treatment is to carry a visible as-of timestamp (making the artifact reading honest) or
to become views on the app (making the app reading honest). What they must not do is stay
undeclared.
So: 8 apps, 91 artifacts, 4 of the 91 flagged as at-risk. Compare with the state axis,
which misfiles six pages and cannot tell you which ones are at risk — it would file
fleet-status as a clean artifact and agent-operating-manual as an app, getting both
exactly backwards.
3. Decision pages: the category never changes, because it was wrong from the start
Gera's tension — "so maybe those should be under apps… but it's an artifact I guess" —
is real, and both halves are correct about different objects that one page was doing at
once. A blocked-<sid8> page is simultaneously:
- the question — what was asked, the options, Fable's recommendation and why. Fixed text. Publish-time. Artifact-shaped.
- the queue — "what is open right now". Read-time. App-shaped.
- the answer — read-time state.
Fuse those three into one published file and you get exactly the symptom he described.
Once answered, the publish-time bytes remain forever while the read-time part goes empty:
blocked-441e839c, blocked-5cff2f82, blocked-cb989158, blocked-cc4ad587 and
blocked-616c7e1f are each exactly 6,170 bytes of chrome around the sentence
"Nothing open. Good." They still sit in the index, still carry the decisions tag, still
appear in search. That is the "it's gone and then it's kind of in the way."
The fifth of those is direct evidence rather than argument: blocked-616c7e1f was a
10,307-byte page carrying live questions when this ADR was started, and was 6,170 bytes —
byte-identical to the other husks — a couple of hours later, because someone answered it.
The decay is not hypothetical or historical; it happened during the writing of this
document.
So: a page's category does not change when it is answered. A category that flips on runtime state is not a category, it is a status field wearing a costume, and it makes a link's meaning depend on when you clicked it. The queue was an app the whole time; the per-session pages were a queue minted as a snapshot, which is why they rot.
Gera states the lifecycle as a requirement, not a preference: an answered decision must get out of the way — "whenever you click it, then it's gone and then it's kind of in the way." Read precisely, that is a requirement on the view, not an instruction to delete the record. Answered decisions should stop competing for attention while remaining citable.
That requirement is what condemns the per-session page structurally, and this is
stronger than the tidiness argument the first draft made. A blocked-<sid8> page has only
two states — live, or husk. It has no way to be collapsed but present, because its
content is fixed at publish time and the only thing that varies at read time is whether
the questions render. A page that cannot represent "answered, still citable, out of the
way" cannot satisfy the requirement no matter how carefully it is maintained.
decisions-dock already satisfies it and shipped: /a/decisions is one page for every
open question across all sessions — open first, oldest at top, decided collapsed
underneath with the choice and when — answers keyed identically so blocked resolve
keeps working. That collapsed strip is the requirement, implemented. The per-session
pages are superseded residue.
Proposal for propflow-docs (this ADR does not change that repo — see Consequences):
stop publishing per-session blocked-<sid8> artifacts; let blocked raise into the
ledger and let /a/decisions be the only rendered surface. Delete the six existing ones.
When a decision is worth remembering rather than answering, that is a deliberate
artifact or an ADR written after the fact — not the husk of a form.
4. KV verdict: right for what it does today, wrong for what Gera is reaching toward
Gera's suspicion is half right, and not for the pages he pointed at.
The mechanism. functions/api/state/[slug].js onRequestPut does a read-modify-write
of the entire record with no compare-and-swap:
const state = await read(env, slug); // whole record
/* … mutate state.v and state.log … */
await env.DOCS_STATE.put(`state:${slug}`, JSON.stringify(state)); // whole record
Cloudflare KV offers no conditional put, and its reads are eventually consistent with
edge caching. So two writers to one key inside the propagation window produce a lost
update: B reads a copy that predates A's write, B writes the whole record back, A's
answer and A's log entry are erased. Both people saw "Saved". Neither got an error, and
bin/docs answers reads back a record that never mentions the loss. KV also limits
sustained writes to roughly one per second per key, and every debounced save on a page
hits the same key.
For today's usage that is fine, and we should not rebuild it. The precondition is two
people editing one page within the same short window; the real pattern is one person
answering a handful of distinct questions minutes apart. The handler validates input,
bounds the log, tolerates a corrupt row, aliases POST for sendBeacon, and never fails a
save on a bad token. There is no observed failure. Replacing a working store on a
theoretical race is how one moving part becomes three.
For a per-developer app it is genuinely the wrong shape, and this is the part Gera
sensed without naming. The client scopes per-person answers as <key>::<you>
(build.mjs, skey()) — every person's state packed into one KV value under one
key. So N developers share one 25 MiB blob, one ~1 write/sec budget, and one lost-update
race that now spans different people's data. The race stops being "two people edited
the same answer" (rare, and recoverable from the log) and becomes "two developers used
their own private app at the same time" (routine — and the entire point of a
per-developer app).
One consequence deserves flagging on its own: the fix for clutter made the race more
likely, not less. decisions-dock sets SLUG = "decisions", so every open question
from every session now lands on the single key state:decisions — where before, each
session's questions sat on their own state:blocked-<sid8> key and could not collide.
Consolidating the view was right (§3); it also consolidated the write contention, and
per-person mode multiplies the writers on that one key. This is not an argument against
the dock. It is the reason key-sharding (below) should land before the dock carries much
more traffic.
Note also that a lost update destroys its own evidence: the same write that drops an
answer rebuilds log from the stale copy, so the change-trail built to catch exactly
this is erased by the event it was meant to record. A lost answer will therefore present
as "I answered and it didn't save" with a clean-looking log — indistinguishable from the
documented wrangler --remote false-zero, and likely to be misdiagnosed as it.
Gera's "never the source of truth" constraint splits app state in two, and only one half needs to be durable. If an app may never own its record, then its state is either:
- Projected state — a mirror of what the files already say (which items are open, what a session is doing, whether CI is green). Losing it is a non-event: re-derive from the files and it is correct again. A lost update here costs nothing.
- Originated state — a human's answer, typed into the app, which exists nowhere else until it is written back. There is no file to re-derive it from. This is the only state in the system with a real durability requirement.
That is where the engineering belongs, and it explains a mechanism that already exists:
the decision-hook (pingDecisionHook) is the write-back path for originated state,
pushing an answer out to the machine that acts on it. Its design is already right —
fire-and-forget, because KV was written first and is the authority for the answer.
The practical consequence is that the race in §4 is dangerous in exactly one place — the answer keys — and harmless everywhere an app is merely mirroring the files. A God app that shows fleet status can lose every byte of its projected state with no consequence; the same app must not lose a decision someone typed into it.
The fix ladder, cheapest first:
- One key per writer —
state:<slug>:<email>. Each key then has exactly one writer, which removes the cross-person race completely, and it stays on KV with no new infrastructure. This alone makes per-developer state safe. Cost, named: it breaksbin/docs answers, which reads one key per slug — that command would need a list + merge, and the merge is where the ordering of a combined log has to be decided. - D1 (SQLite on Cloudflare) when you need queries across people or real transactions.
- Durable Objects when you need one serialized live record with many readers and push updates — the coordinator-ledger case. Single-threaded per object means actual serialization rather than hope, and WebSocket push comes free. Costs a paid-plan feature, a class + binding, and the loss of a KV record you can inspect with wrangler.
So the verdict is: keep KV for shared answers on artifacts; do not build per-developer app state on a single shared key. Option 1 is the migration if and when a per-developer app is built.
5. What /a/god-today needs that an artifact cannot give it — the requirements for app #1
Mechanism, verified rather than asserted:
- The record is a file on one laptop, and the edge cannot read it.
god-todayis a render of~/.claude/jobs/god-ledger.jsonl— 153 KB of append-only JSONL on the Mac mini's local disk, changing many times an hour. Cloudflare's edge has no path to that file. This is the root mechanism; everything below follows from it. - The page is therefore a static photograph. Zero
<script>, zerofetch(, zerodata-pf-state, zerosetInterval— the ledger's contents are baked into the bytes at render time. - Nothing re-runs the render. Two crontab entries (
launchd-monitor,fd-tripwire) and 27 LaunchAgents, none of which render or publish it —com.propflow.decision-hook.plistis the answer listener, which wakes a session when someone answers, not a renderer. The only occurrences ofgod-daily renderanywhere on the machine are in its own docstring. Freshness is a side effect of a god session being alive and re-rendering: the page's mtime (13:33:17) trails the ledger's (13:33:12) by five seconds, which is the signature of a session re-rendering as it works, not of a scheduler. When god dies — precisely the moment you most want to know what is open — the page freezes and goes on reading as though current. Nothing on it expresses doubt. - And a local render is not yet a published page. Reaching the site additionally
requires
bin/docs publish, which commits the regenerated file tomain, rebuilds all ~99 artifacts, and ships the entiredist/to Cloudflare Pages — against a deployment quota the README explicitly works around by hashing output to skip no-op uploads. So a live-ishgod-todayon the current pipeline means a commit of a generated file plus a full deployment per refresh, turning git history into a change feed. The staleness is structural, not laziness. - Parts of the page are wrong within minutes by construction — it bakes in "sessions right now" and per-PR CI red/green, which are the fastest-moving things on it.
What it actually needs, in order:
- (a) An ingest step that mirrors ledger events to an edge-readable store as they are written on the mini. The decision-hook listener is already this exact pattern running in the opposite direction (edge → mini), so the shape is proven here, not speculative.
- (b) Read-time resolution — one Pages Function (
GET /api/god) plus a stable HTML shell that fetches on load. An artifact structurally cannot do this: in this pipeline "artifact" means a file whose bytes are fixed at build time. Auth comes free, since Cloudflare Access already gates the site and the identity already rides the request. - (c) Write.
god-daily add|stateis a CLI on one machine. "Living in the page" means marking something done from the page. The only write endpoint artifacts have is/api/state/<slug>, shaped for a flat string map, not an append-only ledger. - (d) Per-developer. The ledger is one global JSONL. A per-developer view needs the record keyed by developer — which both the single file and the single shared KV key fail at, per §4.
The honest cost, stated rather than used as a reason to shrink the question: an app
leaves artifacts/, so it drops out of the tag index and the search box, loses the
build-time chrome injection (back-link, theme toggle, theme boot), and needs its own
route, its own store, and a small writer on the mini. That is a real rebuild — an ingest
writer, one Function, one route, client render JS — and it should be undertaken for
god-today specifically, not as a speculative platform.
The cheap alternative, so the tradeoff is explicit: schedule god-daily render && docs publish on a cron. That keeps the whole existing pipeline and costs nothing to
build, but buys only ~30-minute staleness, commits a generated file to main on every
tick, and consumes deployment quota continuously. It is the right answer if the page is
something you check; it is the wrong answer if it is somewhere you live, which is the
word Gera used.
This question is now answered, and not by this ADR. Gera has confirmed a God app
— the agent view in app form, with decisions answerable inline — and possibly a Smith
app as a window into Temporal. So god-today is not a page to decide about; it is the
first app, and the analysis above is the requirements input for building it.
Designing that app is explicitly out of scope here — a separate workflow owns its surfaces, decision loop and build order. This ADR constrains it in exactly three ways, all of which fall out of the taxonomy rather than from any opinion about what the app should look like:
- It must be a view, never the record — Gera's own constraint, and §1's axis. The files and Claude stay the source of truth; the app renders them.
- Its state must be sorted into projected vs originated (§4). Only originated state — an answer typed into the app — needs a durable write path back to the files.
- It resolves at read time, which is what makes it an app rather than another
god-today. A God app that bakes its content at publish time would be the same photograph with a new name.
It also settles the category-size question that §Consequences hinged on: with a God app and a Smith app, the apps section has members beyond the two it inherits.
6. Out of scope, and the one in-scope piece of it
"ADRs are indescriptive" is a naming and discovery complaint. We are not migrating, renumbering, restructuring or rewriting the ADR corpus, and no part of this ADR should be read as licence to.
The in-scope, cheap piece: ADRs currently carry no tags at all. build.mjs pushes
them as adrs.push({ ...meta, href }) with no tags field, and hides the filter chips
whenever the ADR tab is active (chips.hidden = view!=='artifact'). So the artifact tab
has fifteen filter chips and the ADR tab — the larger corpus — has none, leaving title
recall as the only way in. Extending the existing closed vocabulary to ADRs is a
build-time change that touches no ADR file. That is the discovery fix; nothing about the
corpus changes.
Consequences
This ADR decides the axes; it does not itself change either repo. It is written in
propflowai because that is where decisions live and where review happens.
propflow-docs is a single checkout whose main is the branch, with no PR flow, so
items 2–6 below are proposals to be executed deliberately and separately. Item 1 is a
decision already taken by the owner and is recorded, not proposed.
Apps become a third native section of the docs index. Gera confirmed this directly — "natively on that docs page, just have another one which is apps" — so it is settled, not proposed.
An earlier draft of this ADR recommended a pinned slot instead, on the grounds that a two-member category does not earn a permanent tab, and set the promotion condition: "promote to a tab when there are enough apps that a pinned list stops reading as a list." That condition has since fired. A God app and a Smith app take the section past the two members it inherits, which was the entire basis of the objection. The recommendation is recorded here rather than quietly deleted because the reasoning was sound and someone will otherwise re-derive it; what changed is the input, not the logic.
Stop publishing per-session
blocked-<sid8>artifacts;docs rmthe five husks and the one still-live page once its questions land in the dock./a/decisionsis the queue.Extend the tag vocabulary to ADRs (§6).
If a per-developer app is built, key its state per writer (§4, option 1) — and land that before
state:decisionscarries much more traffic.Give the four at-risk snapshots (§2) a visible as-of timestamp, or fold them into an app.
Until the God app exists,
god-todayshould carry a visible as-of render time — the one-line change that makes the current photograph honest about being one.
What gets easier: publishing stops requiring a judgement call, at both levels. Is it about the product or the machine? decides the site; would this be wrong if the world moved, or just older? decides the section. Both are answerable in one sentence by whoever is publishing, with no knowledge of the taxonomy's history.
What gets harder: a third top-level section is a classification every future page must pass through, and a misfile is now visible and permanent rather than a one-line tag edit. That cost is accepted rather than avoided — the apps section is confirmed — so the mitigation is the test above being cheap enough to apply every time, plus §2's four flagged pages being resolved rather than left ambiguous.
What this does not decide: what the God app is — its surfaces, its decision loop, its build order. A separate workflow owns that. §5 gives it three constraints that follow from the taxonomy (be a view, sort projected from originated state, resolve at read time) and nothing beyond them.
Alternatives considered
"An app is a page with state." Gera's own framing, and the first thing tested.
Rejected on evidence: it misfiles six of 99 pages — excluding god-today, the page the
category exists for (no state at all), and capturing five long-form documents that merely
embed question boxes. §1 has the detail. Worth recording because it is the intuitive
answer and someone will propose it again.
"An app is a page with a server-side write path." Cleaner than the state axis but
lands in the same place: every decision artifact writes to /api/state, so 15 documents
become apps and god-today — which writes nothing — stays an artifact.
"No third surface; add an apps tag." Genuinely defensible when the app category had
two members, and this ADR argued for it in draft. The corpus is ~99 pages and four
readers; decisions is already a tag; a wrong tag costs one edit while a wrong top-level
category costs an argument. Overtaken by events: a third section is confirmed, and
the God/Smith apps remove the thinness that was the whole objection. It also could never
have been a complete answer, because the three surfaces have materially different
publishing paths — ADRs get a PR, artifacts get bin/docs, an app needs a Function and a
store — and a tag cannot carry a different pipeline.
"Subject (product vs machine) is the artifact/app axis." Gera's axis, and the
strongest challenger, since it is his own mental model. Tested and rejected for this
boundary, on the corpus: 24 pages are machine-subject (agent-ops) and 22 of them are
documents, so subject would file the operating manual, the fleet learnings and
coordinator-pattern as apps — 22 misfiles against the state axis's 6. But it is not
wrong; it is answering a different question, and the right one for a question this ADR
had not asked. It is adopted in §0 as the axis that decides which site a surface
belongs to, which is where it sorts perfectly and where nothing else did.
"Decision pages move from artifacts to apps when answered." Rejected in §3: a category that changes with runtime state is a status field, and it makes a link mean different things at different times. The pages were miscategorised from the start; that is a different repair.
"Replace KV wholesale." Rejected in §4. There is no observed failure at current usage, and the concrete race has a fix (one key per writer) that costs nothing and keeps the store.
A note on ADR numbering — collisions are structural, not careless
Recorded because this ADR was renumbered mid-review and the cause is adjacent to its
subject: ADR numbers have no allocator. An author reads docs/adr/, takes the next
free number, and opens a PR. Two authors working in parallel therefore collide by
construction — neither is careless, and neither can detect the other, because the
competing number exists only on a branch. origin/main topped out at 0122 while two PRs
each correctly picked 0123.
This has now happened at least three times (0113, 0117, and this one), and the cost lands
after the work is done: a rename, an index-row edit, every internal cross-reference, and a
README.md conflict for whichever PR merges second, since both edit the same index table.
That last cost is verified, not predicted. A read-only test merge of this branch
against origin/job-scheduling-standard (git merge-tree --write-tree) exits 1 with
CONFLICT (content): Merge conflict in docs/adr/README.md — while the two ADR files
themselves now merge clean, being different files. So renumbering removes the duplicate
number but not the conflict: the index table is a single shared line-range that every
ADR PR appends to, and two PRs appending adjacent rows collide regardless of which numbers
they hold. Resolution is trivial (keep both rows, 0123 then 0124) but it is unavoidable
from either branch, which is why the third candidate fix below — generating the index
rather than hand-maintaining it — is the one that removes the whole class rather than just
the numbering half.
adr-number-uniqueness.drift.test.ts catches the collision only once both files are on
the same branch — i.e. after a merge, not before. It is the right guard for the wrong
moment.
The fix is not this ADR's to make and is deliberately left as a follow-up: issue #5343, which carries the merge-tree evidence and the candidate fixes.
The one thing worth carrying here, because it is the part that gets lost: the problem has
two halves that no single fix closes. Allocating by PR number, or teaching the guard to
read open PRs, kills the duplicate number and leaves the README conflict exactly as it
is. Generating the index from docs/adr/*.md kills the conflict surface and does nothing
about numbering. A complete fix is one of the first two plus the third — not any one of
them. Read as three equivalent options, someone closes this by picking one and the other
half survives.
Second opinion
A Fable model was briefed neutrally per ~/.claude/skills/god/fable-brief.md — all
four candidate axes steelmanned in their own best terms, no position disclosed, both
sides of the KV and lifecycle questions argued at equal length, and an explicit
instruction that either answer was acceptable. It read the primary sources itself and was
told to fact-check the brief.
It converged independently on the same axis (it called it AXIS-RECORD: "the record lives outside the HTML and a machine re-renders the page from it"), the same Q2 answer (POSITION-SPLIT), the same Q3 verdict (fine now, wrong for the per-developer direction, same read-modify-write mechanism), and the same Q4 root cause. Since there was no disagreement, no tiebreaker was spawned.
It corrected this ADR in four places, all verified before adoption and all now
incorporated: that artifacts are version-controlled and the real distinction is review
(bin/docs:49-71); that four snapshot pages sort ambiguously and an earlier draft's
claim of "no judgement calls" was an overclaim; that decisions-dock concentrates every
session onto the single key state:decisions; and that a pinned slot beats a third tab
for a two-member category. It was wrong on one count — it read the corpus as 98 files
where ls gives 99 — and its confidence was medium, resting chiefly on the thinness of
the app category. That reservation has since been resolved by the owner rather than by
argument: the section is confirmed and the God/Smith apps populate it (Consequence 1).
Recorded because the convergence is only worth anything if the brief was genuinely
neutral; the brief is at god/fable-brief.md and the reasoning above should be judged on
its evidence, not on two models agreeing.
One caveat on the strength of that convergence, stated because it cuts against this ADR: Fable was briefed before Gera's subject axis ("for the machine, for the agents") was known, and the brief did not offer subject as a candidate. So its agreement is evidence about the artifact/app boundary only, and is not independent corroboration of §0's two-level model. §0 rests on the corpus test (22 vs 6 vs 0 misfiles), which anyone can re-run, not on a second opinion that never saw the question.