Three Sessions
Why KVICK.BIKE drops from thirteen AI sessions to three, and exactly where the software stands at v0.6.954.
Written 2026-07-23 by the Codex (Command) session, which this decision retires. Every figure below was verified against the repository and the running Worker at the time of writing, not taken from a session's report. Where something is reported rather than verified, it says so. Adapted for the Bible with Codex's explicit ask that it belong here.
Part 1 — Why
What went wrong, and what did not
Thirteen sessions each owned one module. On paper that matches the code: the application really is separate files with API boundaries between them, and the seats really did stay in their own lanes. Almost no failure this week was two sessions editing the same module.
What failed was the layer between them. On 2026-07-20 the traffic between sessions consumed more of the day than the code did. THE-PROTOCOL.md was written to fix that, and by 2026-07-23 it had grown to thirteen rules. Its own closing line says: if a rule generates more traffic than it prevents, that is a defect in the rule. By that test the protocol had become the defect.
The evidence that settled it, all from a single roster sweep on 2026-07-23:
- Every one of the thirteen seats was holding a dead session id. The Codex session had been replaced; messages to the old one reported "sent" and were never delivered. Nothing bounced.
- Silence was defined as receipt (Rule 5), so a dead channel looked exactly like a healthy one — for four days.
- A direct instruction from Tom sat undelivered for three days. The auto-promote-a-passing-scenario directive, given to the Audit seat on 2026-07-20, went into the dead session and was never seen by anyone.
- Work was blocked on answers that already existed. Migration had investigated and answered two of Audit's questions a day earlier, into the dead session. Audit sat blocked the whole time.
- A bug was fixed, rolled back, and independently rediscovered by two seats who did not know a fix had ever shipped — and Command then published a protocol rule about it.
The sharpest observation came from the Audit seat: a roster that is 5-of-7 right is more dangerous than one that is 0-of-7 wrong. Only two ids had actually rotated. The roster worked often enough to be trusted, so the two dead entries stayed invisible until something load-bearing failed silently.
The code came through all of this clean. The working tree has no uncommitted work, nothing unpushed, no stashes, and the pre-rollback state is preserved on a branch. The individual seats' engineering was good — one traced a bucket bug through five steps to a delayed customer pickup; another proved a hazard was real by noticing a SQLite sequence counter sitting at 3. The coordination was the mess. The software was not.
Why these three
| Session | Owns | Why it survives |
|---|---|---|
| Coding | every application module | With one coding session there are no lanes to police, no numbers to allocate, and no notes to route |
| Migration | the converter, the wipe/reload, the database | Works in src/modules/migration/* and in the data itself; the cutover is a distinct discipline from application code |
| Bible | the documentation site | Its own repo and its own deployable — it cannot collide with the app tree at all |
Bible is the only session whose parallelism was always structurally safe: it shares no working tree, cannot collide on src/index.ts, and cannot ship someone else's uncommitted work in a deploy. It is also the only artefact a non-programmer can read.
One change to how Bible works: it reads the repository, not messages. Both of its known errors came from documenting second-hand — a slice claiming a feature was "live and shipping" when the save handler was a stub, and a version re-baseline applied on a message from the failed session that Tom never confirmed. When it read source with grep and cat, it was right. Its input should be commits, code and the changelog: sources that cannot be undelivered.
What this deletes
Most of THE-PROTOCOL existed to manage concurrency and stops being necessary:
- Rule 0 (hub-and-spoke routing) — nothing to route between.
- Version-number allocation — one writer, no collisions.
- The designation freeze and change-note fan-out.
- The Command seat itself.
Rule 12 survives and matters more than ever — do not remove what you did not build; moving counts as removing; unreachable counts as removed; retiring anything is Tom's call. That rule is about respecting the user's decisions, not about coordinating agents.
The monolith remains reference-only (Tom, 2026-07-23). It is consulted to learn what a feature was for; it is not copied, and a divergence from it is not a defect.
What this does not fix
Two of the three architectural changes Command proposed were workflow dressed as architecture, and they evaporate here rather than getting solved:
- Deriving
VERSIONinstead of hand-editing it mattered only because thirteen seats had to touch one line. - Deploying from a clean
HEADmattered only because seats shipped each other's uncommitted work.
One genuine software defect remains, and it is not about sessions at all — see the dedup predicate below. It would exist if one person had written all of this alone.
Part 2 — Where the software sits at v0.6.954
Verified state
HEAD | d674a33 |
Disk VERSION | 0.6.954 |
Served /api/build-info | 0.6.954 — agrees with disk |
| Working tree | clean; nothing uncommitted, nothing unpushed, no stashes |
| Migrations | 57, latest 057_layaway_tile_flag.sql |
| Core | 34 files in src/core/ |
| Worker / DB | kvick-bike / kvick-bike-db |
The rollback — the single most important thing to understand
The tree was rolled back to v0.6.929, undoing twenty-two versions. The commits from that range are still ancestors of HEAD, so git log reports every one of them as landed while the files say otherwise.
Commit present, effect absent. Any tool a session would think to use to ask "did my work ship?" answers yes. Three seats have work they verified in a browser that is not in the running code, and had no way to know.
Nothing is lost. The branch pre-rollback-v0.6.950 exists locally and on origin, pointing at d3d5804 (v0.6.951, the last commit before the rollback). Verified contents against HEAD:
| on the branch | at HEAD | |
|---|---|---|
Sales kbPrint refs | 9 | 0 |
Inventory kbPrint refs | 2 | 0 |
Purchases kbPrint refs | 2 | 0 |
| autoprint two-way switch | present | absent |
Re-landing any of it is a checkout of specific paths, not a rebuild. This is a decision between two known-good states, not a repair.
Module state
| Module | State |
|---|---|
| Sales | Most complete. Till, ledger, deposits, layaway, estimates, returns/exchange, gift cards, per-line special-order pill (0.6.952–954) |
| Customers | Monolith parity. Cards, links, family, YoY pulse, account credit |
| Inventory | Built. Stock cards, serialized units (mig 054), bulk price, tags |
| Purchases | Built. PO lifecycle, vendors, bucket, receiving, RMA |
| Settings | Shop info, hours, tab colours, POS tender toggles, staff + permissions matrix, discounts, audit trail. Tax settings and receipt config unbuilt |
| Service | Core workshop flow ships end to end. All 24 rows carry description = NULL — the only tab with no Store Actions, so it cannot be audited |
| Migration | Converter, wipe/reload, ZZ fixtures, post-load compliance suite |
| Trades / eComm / Rentals | Deliberately inert mock-ups. Zero INSERT statements, fixture-backed, each labelled on screen as a mock-up |
Open issues — verified directly
-
Printing is dead on four paths.
window.open+ auto-print()is back atinventory/client.ts:689,purchases/client.ts:1080,sales/client.ts:4396and:4455. A popup blocker or an embedded pane kills it silently — the operator presses a button and nothing happens. The fix exists on the pre-rollback branch. -
The autoprint one-way door is live.
kb_receipt_autoprintis written once and cleared nowhere atHEAD. Ticking "always print, don't ask again" permanently hides the receipt prompt — and the Email receipt button exists only inside that prompt. Fixed in v0.6.951; the fix was rolled back. -
Five modules write
inventory_order_queuedirectly — Sales (8 sites), Purchases (5), Inventory (5), Service (2), the converter (1). Purchases owns the table and does the second-fewest writes. The dedup predicate is the defect.purchases/api.ts:120andservice/api.ts:350match onvariant_id + supplier_id + status='pending'with nocustomer_idfilter, so a queue row already earmarked to a customer gets its quantity increased in place. Traced by the Purchases seat to its end: a mechanic's part becomes earmarked to a customer, and that customer's "your order is in" is delayed until a second unit arrives that was never theirs. The missing clause exists in the codebase — Sales'bucketBackorder(sales/api.ts:27) filterscustomer_id IS NULL.THE INSTRUCTION IS "ADD THE CLAUSE", NOT "USE THIS PREDICATE"The three predicates are not the same predicate:
matches on sales/api.ts:27variant + pending + customer_id IS NULL— no supplier clausepurchases/api.ts:120variant + supplier + pending service/api.ts:350variant + supplier + pending Lifting Sales' line into the other two drops the supplier clause, so two pending lines for one variant from different vendors merge into one row and whichever
supplier_idsurvives wins — silently mis-assigning a vendor on a real PO. A new defect, introduced by the fix, in the same table. Sales' omission is right for Sales: a backorder is staged before a vendor is assigned, so matching on supplier there would fragment the queue.Append
AND customer_id IS NULLto each existingWHERE. Leave supplier matching exactly as it is.Two consequences that fall out of the fix — expect them, they are not regressions: Service's
INSERTalready setscustomer_idfrom the ticket, so Service rows become protected too and two mechanics ordering the same part on different tickets each get their own row (correct — different jobs). And a Service row carriescustomer_idbut nospecial_order_transaction_id, soreceive-podoes not earmark it. Do not "tidy"receive-poto earmark oncustomer_id— that reintroduces the exact harm.Verify in the shape that catches this class: stage a customer special order for variant X / supplier S, then order the same variant+supplier from Service, and assert two pending rows exist with the customer's row still at its original quantity. Prove what survived unchanged, not just that a new row appeared.
⚠ Routing Service through Purchases' endpoint does not fix this — the endpoint has the same flaw. Fix the predicate first, or the conversion hides whether it was ever fixed.
-
Labour tax — the second silent money failure, and Migration can act on it. Shop labour is GST-only, and the only thing making that true is a single link: all 87
service_menurows point attax_category_id = 3(gst_only).tax_categoriessurvives a wipe — butservice_menuis wiped and rebuilt from AIM, so survival depends entirely on whether the converter restorestax_category_id. If it comes back NULL or standard, Service'srecompute()falls back toCOALESCE(tc.applies_pst, 1) = 1and charges 7% PST on every labour line — $35 on a $500 repair, on an invoice that looks completely normal. This is the Service-side twin of the subcategory PST rung. The check is one query post-reload: everyservice_menurow still resolves togst_only. It belongs on Migration's post-load compliance suite. (Raised by the Service seat during the wipe window; routing it was Command's job and the stop landed first.) -
"In transit" on the Stock Card is a fake zero, and always has been.
inventory/client.ts:93renders(v.quantity_in_transit || 0). That column does not exist — it was deliberately dropped as dead (019_inventory.sql:9). So the API never returns it, the client readsundefined, and|| 0prints a confident "In transit: 0" on every product, permanently. Not blank, not an error — it asserts a number a shop owner will believe, in the exact place they would look for on-order stock. Real on-order must be derived from open purchase-order lines, which is a cross-module read and is why it was parked. Fixing it or deleting the line are both honest; leaving it is not. -
Two edit surfaces in Inventory, and the older one is a decoy.
#prod-edit-modaland itsopenEdit/editSecchain sit alongside the real editor#prod-productcard-modal. OnlyeditSec='new'is still genuinely reached (New product / Clone); the per-section edit path is inert. This is most likely what Tom saw when he reported "an Inventory card somewhere that didn't match." Retiring it is Tom's call under Rule 12 — and the New-product/Clone path must be kept, as it is the only live consumer. -
The changelog's version numbers are POSITIONAL, and a history rewrite silently re-labels every release.
gen-changelog.mjsassigns labels by index:e.v = 0.6.${471 + (n - 1 - i)}. Appending a release is safe —nandirise together. Any rewrite of history is not: a rollback, revert, force-push or change in collision-merging altersnwithout shifting older indices, so every older label moves by the delta.changelog-curated.jsonkeys each plain-language line to that positional label and the generator attaches them after the renumber — so every plain line re-attaches to a different release, silently, and the card starts telling the shop that an internal refactor is a new feature. A force-push has already happened once (0.6.933/934 → 0.6.952). Whoever rebuilds the plain layer must key it to something immutable — the commit SHA or a stable slug — never the rendered number.
Open issues — reported, not verified here
- PII exposure. Three seats independently report that
customers/api.tshas no server-side gate:/listreturns phone and email for up to 200 customers per query, plus address fields, with only client-side hiding. All three had code-layer evidence; none could make an authenticated request to prove the response. Staff-facing, not internet-facing. - Tax — a single point of failure with no redundancy. 122 LIGHTS products resolve PST-exempt through 11 rows in
subcategories, carried across each wipe by one converter step (subcategory_tax_reapply). The 976 per-product rows that used to be a redundant second copy were deleted in v0.6.928. If that step is ever removed, those products silently charge PST and nobody at the till can tell. Under Rule 12 it is not retirable by any session without Tom. Migration and Inventory measured this independently and their figures agree. - The till drawer cannot be counted off. Opening float, end-of-day cash reconcile / Z-report and cash drop to safe are entirely unbuilt. Not a regression — never started, because the scenario library was worked as 37 items when it defines 42.
taxable_amount_centsrecords the pre-discount base while the tax itself is correctly computed on the net. Money charged is right every time; a GST/PST return reconciled against the stated base will not balance. A question for an accountant, not for a session.
Known debris
data/zz_test_fixtures.sql— a dead recovery path that looks live. It references parent ids that no longer exist, so it now fails on its first statement.rebuildZzFixtures()is what actually restores the fixtures.- ~119 direct and ~87 wrapped calls to
window.kbToast, which was deliberately retired — the audit-trail footer replaced it. Success paths are intended no-ops. Failure paths are the real issue: a refusal routed through a dead channel shows the operator nothing, which is indistinguishable from a broken button. Route failures tokbConfirm; do not resurrect the toast. - An unfinished
prod-/pur-modal id split inpurchases/view.ts.
Traps worth carrying forward
- ZZ protection is not the prefix — it is
rebuildZzFixtures(). Azzserial typed onto a real catalogue product dies silently in every wipe. Two fixtures were lost this way. The set already declaresis_serialized = 1and 18 explicit serials (ZZSER-0001–0018), and a unit moved tosoldstays sold across a rebuild — so serialized flows are testable durably inside the protected set. - A successful
wrangler deploydoes not mean your code is running. Always check the served version against disk. Clearing.wrangler/tmpand.wrangler/cacheis sometimes not enough; removing the whole.wranglerdirectory is. sales/client.tsis a backtick template — no backticks, no${}, and no apostrophes inside its JavaScript strings.inventory_locationsholds one row at id 1, andlocation_id = 1is hardcoded in 49 places. The reload writes that id explicitly because the SQLite sequence is already poisoned (high-water mark 3). This has failed once before: the location re-seeded at id 3, every product vanished from the grid with no error, and it was debugged in the converter before anyone looked at the primary key. Do not tidy that literal1out of the INSERT.- An API guard constrains the API, not the table. The converter writes 26 tables by direct SQL, bypassing every guard the application code adds.
- A rollback silently falsifies the record. Any note written against a screen is invalidated when that screen is reverted, and nothing re-checks it.
Part 3 — What to settle first
- The rollback. Re-land the twenty-two versions from
pre-rollback-v0.6.950, accept the rollback as intended, or pick per item. Until this is settled, "is this live?" has no reliable answer for any feature in that range, and every other decision gets made twice. - The dedup predicate. One clause, two call sites, with a working reference already in the tree. It reaches a real customer.
- The full stop. All thirteen sessions are currently halted and reporting clean trees. The three that remain need releasing; the rest can be closed.
Bible-side verification note
Per the "read the repository, not messages" rule this doc records, four of Codex's Part 2 claims were independently re-verified from source before publishing here:
grep -c "kbPrint" src/modules/sales/*.ts= 0 across all four files (claim: 0 at HEAD ✓)sed -n '685,695p' src/modules/inventory/client.tsshowswindow.open+setTimeout(w.print, 250)at :689 (claim: window.open + auto-print ✓)sed -n '115,130p' src/modules/purchases/api.tsshows the dedupSELECT id FROM inventory_order_queue WHERE variant_id = ? AND COALESCE(supplier_id, 0) = COALESCE(?, 0) AND status = 'pending'— nocustomer_idfilter (claim: no customer_id filter ✓)sed -n '90,96p' src/modules/inventory/client.tsrenders'<div><span class="k">In transit:</span> ' + (v.quantity_in_transit || 0) + '</div>'andmigrations/019_inventory.sql:5-11explicitly says "dropped the dead quantity_in_transit" (claim: fake zero, column doesn't exist ✓)
The verification method itself is the doctrine going forward: any Bible claim that isn't from git log, git show, cat, grep, curl /api/build-info, or Tom directly in chat is provisional until it is.
See also
- The KVICK Engine — the thirteen-session model this handoff supersedes
- The Doctrine — the load-bearing principles the Engine ships with
- Three strata — Kernel · Meta · Modules — how the app is organised
- Migration module — one of the three surviving sessions