Session-history entries from
agents/hermes/housekeeping-log.
Moved here in two passes — pre-2026-08-25 entries (2026-08-25) and rounds 1-5 plus all
dated session handovers 2026-09-02 → 2026-09-15 (2026-09-21), rounds 6–49 on 2026-10-13, and
rounds 50–54 on 2026-10-14, and round 55 on 2026-10-15, round 56 on 2026-10-15 (session 83), and rounds 57–61 on
2026-10-16 (session 87).
Newest first. Archive-not-delete — this is the audit trail and stays searchable.
Further pass 2026-10-13 (session 76): rounds 6–49 moved off the live log verbatim and
newest-first — the log had regrown to 248 KB with the current round buried under 44 earlier ones. The live
page keeps the current round, Round 50, the phase tracker, the open items and the conventions. Archive-not-
delete, as always.
No phase advanced. Matthew closed the first pass to go to OpenCode and returned with "may as well do some more
housekeeping now that we have no open notes left", so the second pass was the handback verification — the half
of the loop that is mine — followed by closing the queue and an archive pass on both inboxes.
1. What OpenCode's session answered (they worked the six queue notes in the order given). C closed (C1 was
already in scope; C2's generate_pooler_config step existed but would have failed in-container — reordered into
recreate-then-Django, mirrored into rollback; C3 split between us; C4 mine). A2(a)(b)(c) fixed — the GP
fleet-view key, the dental dashboard gate, the dental mirror's source column — and A4 answered (Business
Manager only; copy the helpers verbatim). B1–B4 fixed and B5 stated in the runbook. D1–D4 fixed, with
three guides corrected. E1–E9 and E11 fixed (E10 left as Matthew's call). Untouched: A1 (the pharmacy
client-maintenance port), A3, A5, and all of F. Nothing is committed or deployed — that is
Matthew's decision, and the queue now carries it as the top note.
2. My verification, and it was independent rather than a reading of their summary. Every item was confirmed by
reading the diff on the working tree — is_fleet_view = is_superuser and not is_client_scope, the dental
PlatformOrClientPracticeRequiredMixin on InfrastructureDashboardView, the mirror's source CharField,
Coalesce('endorsement__dispensed_at','updated_at') rendered as row.dispensed, OptOutView.get() with the new
optout.html, submit_sales_invoice, the installer's leading bare OnCalendar= reset and its printf '%q'
writer with a source-and-compare check — and then by running things myself: GP targeted 13 OK
(clinical_data.tests.test_snomed_search reporting.tests integrations.tests.ERPNextSubmitSalesInvoiceTests),
dental infrastructure 22 OK, pharmacy full suite 693 OK in 284 s, and the installer's own dry-run,
which prints patch 04:07 / reboot 04:37 / inventory 05:07 for test-client — exactly the drop-in I had applied
by hand, and no shared minute. The two rotation scripts were read: apply-rotation.sh recreates
redis app celery_worker celery_beat before the Django steps and rollback-rotation.sh now regenerates the
pooler, and scan-literal.sh carries an explicit NOTE (2026-10-16) naming the pooler file. Not re-run by me:
their sandbox round-trip of a $ token (B2) and B4's three image_refresh paths.
3. The pass. from-hermes 7 → 3 notes (24.6 KB → 14.1 KB): B, C, D, E and the stale-resume pointer closed
and moved verbatim to the archive with their verification evidence, (A) narrowed by a dated progress line
(A2/A4 done; A1/A3/A5 open) and (F) unchanged, with a new (awaiting-deploy) note added at the top — one
decision plus the four things that follow it. from-opencode 20 → 8 notes: the five 2026-10-16 handbacks each
now carry a VERIFIED BY HERMES line, 11 closed notes (2026-10-06 → 2026-10-15) archived verbatim, and the
2026-10-13 rotation execution pack stays live because the window is still Sunday. Both pages and both archives
were rebuilt from raw bodies, guarded by a re-fetch-and-diff before the upsert, and verified three ways (Page ID +
Render OK; stored body byte-identical; rendered 200 with the right note counts).
4. A hazard worth recording, and it bit once. A naive ^Status: **HANDED match moved a note whose status reads
"HANDED BACK — … your verification is invited": the word starts the marker but the note is not closed. It was
restored to the live page with an annotation and the archive's pass line corrected to 11, and the skill's move
predicate now says to match the whole marker. The general rule: an archive pass must never absorb a note whose
status invites action.
5. What the deploy would set off, and who holds it. (1) My live re-verification of each fixed item on the
deployed estate — working-tree tests are not live evidence; (2) figure re-shoots — health_clinic/recording- symptoms-search.png (E1's ranking changed) plus the medical-notes and blood-tests figures the 2026-10-08 note
asked to re-shoot once E2/E3/E4 landed, embeds unchanged; (3) a no-op installer re-run on the dental canary so
the code, not my hand-fix, produces the drop-ins; (4) snomed_descendants needs rebuilding before heart
attack can surface the MI concepts — a corpus-import decision, not a query fix (E1 is fixed on the query side).
7. The deploy (same session, at Matthew's word). Both were authorised together — "commit and deploy" — so
the four working trees were committed and pushed (GP 165dc9e, dental 9990fa1, pharmacy 6aed942,
veripath-ops 483c399) and then deployed by a targeted file copy: 48 files across the three apps, each
hash-verified byte-identical on the engine, the replaced files backed up first at
/root/work/deploy-backups-20261016-124628-wo-queue-fixes/, then the app and both celery containers restarted per
app. Before the copy I proved the engine's copies equalled the pre-change committed version (or the
already-deployed one) and diffed the four files that differed only because the engine carried earlier deployed
work — all four were strict subsets of the new content, which is the check that makes a file-copy deploy safe on
production. After: manage.py check clean but for the pre-existing W342 warning, all three apps healthy,
/infrastructure/health/ 200 on the platform hosts, the client hosts and publicly through nginx, the client
surfaces still gating (anon 403, pharmacy staff 302 to OIDC), and the fixed code grep-confirmed on the engine. The
installer is not installed by this deploy — it takes effect at the next build visit. 8. Follow-up 1 — the live item-by-item verification — DONE the same session (and it found one thing). Every
fix was exercised on the deployed estate, inside the containers, with the tenants' own users and sessions written
directly (no user row touched), GETs only. Passed live: A2(a) the GP client-superuser fleet leak (as the client's
own admin: the client page, no platform heading, no other client named); E1 the SNOMED rewrite (tummy ache →
Stomach ache (finding), the exact synonym found for the first time; rash → 20 rash concepts with no trash and no
Rashkind and no repeats; chest pain and nausea → the plain term first; heart attack → still only the
fear-of rows, which is the corpus finding below, not a query defect); E2 the debug gate (DEBUG=False, no panel,
debugDiv is null); E3 the copy buttons (rendered patient pages carry "bi bi-check", no raw-escaped
form, no stray script text); E4 the wrapped note (appointments 115/117/119 render white-space: pre-wrap with the
note text present); E6 the receptionist search (baseline 14 links, ?q=Art → 3, nonsense → 0 result rows —
the filter applies); E7 (no "placeholder" left in reporting/signals.py, resolves clinic → Practice, logs at
debug); E8 wired in three files (not exercised against the live books — that is a business action); D1 the
receipt's print sheet, D3 the Counter Assistant's opt-out (dashboard links → /collection/31/opt-out/ 200,
no contact details, form present, while the Manage-only preference page still 403s the CA) and D4 the charge
picker (0 offered of 8 dispensed — everything withheld on that tenant, so the positive case rests on the unit test).
Two things measured rather than assumed, worth keeping: E1's latency through the view is 279–286 ms for
rash but 364–389 ms for tummy ache — over the 300 ms budget, where OpenCode's 210–279 ms was the query
alone; and D2 cannot be demonstrated end to end on test-client-pharmacy because its tokens carry status
ENDORSED with no endorsement row, so the Coalesce fallback is what runs (the code is verified in the deployed
files). And the finding: A2(c) is half-fixed — source landed, but infrastructure_maintenancerecord.notes is
text NOT NULL with no default and is still absent from the dental model, so a dental-side create now fails on
notes instead of source (measured: the create raises, and information_schema confirms the column). Raised as
note (G) with the fix and a "close the whole gap, this bit twice in a day" note. Still to do on this thread:
follow-up 3 (the canary installer re-run), 2 (the figure re-shoots) and 4 (snomed_descendants). The session closed here, deliberately, on context length — Matthew called it so the remaining three start in a fresh context; nothing is in flight, and the deploy toolkit (/root/work/session85-deploy/) and the live-verification scripts (/root/work/session85-live-*.py) are on disk for it.
6. Still open, in one place. (A1) the pharmacy client-maintenance port — the last step of Matthew's
sequence; (A3) the client-box backend items (no cadence rows, next_run never computed, the platform host
filter/status strip); (A5) the estate ruling's page edits and the credential inventory; (F1–F5), including
Matthew's MinIO-image decision and the ledger-vocabulary question. Owed: C4 — my post-window DSPT evidence note
after Sunday 2026-10-18; the client pharmacy database password rotation (mine); the pooler's own client
credential and the dental canary's agent credentials (OpenCode's, as split today). Phase 4 still waits on
Matthew naming it.
One more disclosure-adjacent note, for the record: to reproduce the installer dry-run I passed placeholder
token and secret values (TOK_tokenVERIFYonly, SEC_secretVERIFYonly) as command arguments on the local
host — they are not credentials and nothing on any box was touched, but the habit file records that values belong in
files or stdin, and this is exactly the shape of thing that turns into a real leak if repeated with real ones.
No phase advanced. Matthew's instruction was to clear the open notes — "do them in date order" — to archive
what is no longer needed, and to get the rest "done before we continue with any further work". That made the
session an audit of the whole inbox: every one of the 30 notes on from-hermes was re-verified against the
deployed estate and the repositories rather than against its own status line, then either closed with evidence
or carried into one current list.
1. What the audit measured — nothing taken on trust. On the deployed trees: the pharmacy app still has
no @media print rule anywhere, and still dates its Dispensed column by updated_at while the uncollected
list uses endorsement.dispensed_at; the SNOMED search still filters type_id = 900000000000003001 (FSN
only); the clinical-note form's dlog() still sets display:'block' and runs at load; the appointment record
still prints the note in a bare <pre> with no pre rule; the escaped-quote copy buttons are still in
lab_documents/ templates; the GP app's integrations/erpnext_client.py still has no submit_sales_invoice();
the receptionist patient search still filters encrypted columns with icontains (so it matches every row);
the GP dashboard still sets is_fleet_view = self.request.user.is_superuser before the client branch
(infrastructure/views.py:104); the dental dashboard still carries no gate; and the dental MaintenanceRecord
mirror still lacks the shared table's NOT NULL source. On the repos: veripath-ops is still at cf2462f on
the agent branch — the installer's slot_dropin() still writes a bare OnCalendar= (systemd appends, so
every installed box carries two schedules and the base 02:15 next elapse), and the env writer still uses an
unquoted heredoc. What had landed was confirmed in the deployed templates, not in a note: the insurance
panel's panel_appointment_pk, the appointments period filter, RadioSelect excluded from the widget-class loop
and the NHS Notify comment block; the scaling auth branch is an ancestor of main (11e3911); the frozen MinIO
image is still on the host (14cea493d9a3, 241 MB); and check-backup-freshness.sh was repaired on
2026-09-27, so the 2026-09-26 finding is closed rather than carried.
2. The result: 12 closed, 18 carried, and the page rebuilt as the queue. Closed (each with a dated line
saying what was verified, then moved verbatim to the archive): the 2026-09-26 backup-gzip pair; the 2026-10-06
CV9 counter-views brief (its status line was stale — CV9 was verified live on 2026-10-06); the 2026-10-07
insurance-panel 500 and the two UI changes; the 2026-10-08 unbootstrapped ERPNext site; the 2026-10-14
WO-A…WO-D rulings and the WO-D park; the MinIO→SeaweedFS instruction; the 2026-10-15 WO-D un-park, the two
correction notes and note (19) (already handled). Carried into six queue notes: (A) the client-VPS
patching surface (the pharmacy port, three residuals, the §5 backend items, the spec questions, the estate
ruling's consequences for OpenCode's pages), (B) the agent and installer (the two one-line defects, the
shared slot minute, the box with no docker compose, the WO-M4 gate), (C) the rotation window — time-boxed:
its scan-scope fix must land before Sunday 2026-10-18 02:00–04:00 — and the three credentials we owe, (D)
pharmacy counter/notifications, (E) the GP app's clinical, record and integration defects, (F) scaling,
the front door and the pooler hardening, plus the two decisions that are Matthew's.
3. The mechanics, so the next pass repeats them. Both pages were rebuilt from the raw extracted bodies
(one-pass regex off --get; no wrapper ever fed back into an upsert), the inbox was re-fetched and diffed
immediately before the upsert (the two-writer rule), and both writes were verified three ways: Page ID +
Render: OK from the tool, the stored body byte-identical to the file written, and the rendered pages curled
at 200 with the new anchors present and the old notes absent. Sizes: the inbox 137,116 → 23,647 chars
(30 → 6 notes); the archive 89,536 → 238,965 chars, 30 notes added verbatim, newest on top. Nothing was
deleted.
4. A convention, widened deliberately. A note now also leaves the inbox when it is CARRIED: its live
items are restated in a queue note with their state re-measured, and its own text is archived verbatim and linked.
Without that rule 18 genuinely live notes — each a separate ask, several overlapping in one defect list — stay on
the page for ever and every session pays for all of them; with it the inbox is the current list and the archive is
the trail. The move predicate is therefore HANDLED or CARRIED, never "drop".
5. Open items updated for it (section below): the queue is now the whole of the outstanding work; Matthew's
two pending decisions are the frozen MinIO image and the live-mail-domain account in the gp tenant; the
rotations owed are the pooler's own client credential, the client pharmacy database password and the dental
canary's agent credentials; and the dated DSPT rotation evidence record follows Sunday's window.
6. Phase 4 still waits on Matthew naming it. Nothing else moved.
No phase advanced. Matthew opened by asking me to get ready, then authorised the build visit, so the session
was the install on the GP test-estate box — the first real one on a client box — followed by its verification,
its records, the credential close, and this pass.
1. The install, done and verified (the estate's first client-box agent was already live on the dental canary).
Credentials minted in the app (client_patch_credentials test-client, output to a 0600 file so no value reached a
transcript), the tree staged from the agent branch, a dry run first, then the real run in the box's own
04:07Z quiet-window slot. Verified live, not assumed: the host-local config is 0600 root:root and sources
cleanly (token and secret both 48 chars after sourcing — lengths compared, values never printed); four timers
live (30-minute signed-job poll, monthly patch, nightly reboot window, daily inventory, plus the boot health
unit); the first inventory posted and landed as record 34 (Ubuntu 26.04 LTS, kernel 7.0.0-27-generic,
44 upgradable, 0 security, reboot pending); client-patch-poll.sh answered 204 / no job; the box's container
kept its uptime (started 2026-07-16) and the 44 upgrades plus the pending reboot were deliberately not
triggered; and the run renders on the client's own surface — re-checked as the tenant's own manager inside
the deployed container (/root/work/session84-client-card-check.py, 19/19 PASS): their dashboard and
/my-maintenance/ show their box only, the run is listed as Client inventory on 2026-10-15, the runs export
carries it with the measured summary, the package export is header-only, and the manager is still 403 on
/maintenance/, /vps/ and the platform host. Recorded in the app as the visit's own manual record (35)
and a temporary_credential row (36), both client-scoped — the runbook's step, done in the app and not only
on the wiki.
2. Two installer defects, both measured on the boxes. (a) The per-box slot drop-in does not replace the
unit's own OnCalendar — systemd appends. systemctl cat client-patch.timer on the freshly installed box
showed two OnCalendar= lines and a next elapse of the base 02:15, not the slot (04:07); the dental
canary was the same (base 02:15, slot 02:23). So every installed box carried a duplicate schedule and the
quiet-window slot did not mean what §10.6.4 says. Fixed in place on both boxes with a bare OnCalendar= reset
line before the slot value (test-client → next elapse 2026-11-01 04:07Z, reboot/inventory → nightly 04:07Z; canary
→ 02:23, verified by merged-unit line count and NextElapseUSecRealtime). (b) The generated env file still
expands a $ when sourced — this box's token carried none (!#%&^ was its symbol set), so the install is safe,
but the installer keeps the hazard. Both are OpenCode's to land in veripath-ops; note (22) carries the
one-line fixes. Also found: all three timed units share one slot minute, so on the 1st the monthly patch
run and the deferred-reboot check start together — the reboot check can fire mid-patch.
3. The credential closed, in the safe order. The estate key (/root/.ssh/vps_client) is now authorised
for root on this box and key-only login verified first; the temporary root password was then rotated
(chpasswd over stdin, never on a command line) with both directions proven — the new value logs in and
the exposed one is refused. Nothing else referenced that password (the box's only consumer on our side is the
15-minute TCP-22 uptime probe in /etc/cron.d/client-uptime), so no job needed changing.
4. One finding for Matthew (flagged, not changed). That box's SSH is open to the internet with
PermitRootLogin yes and PasswordAuthentication yes (UFW 22/tcp from anywhere, no fail2ban) and its
auth log holds ~62,000 failed password attempts in seven days — a continuous brute-force against root. The
exposed password is now dead and the estate key works, so key-only SSH, or restricting 22/tcp to our own
addresses, is the obvious next hardening step; and Matthew decided it within the hour: key-only SSH is now in force on both test-estate Client VPSes
(a host-local 00-veripath-key-only.conf drop-in — named 00- because sshd takes the first obtained
value per keyword and the image ships 50-cloud-init.conf with password auth on — sshd -t before the
reload, then both directions verified: the key signs in and a password attempt is refused with
Permission denied (publickey), i.e. the method is not offered). Each box also carries a client-scoped
app record of the change, so the client's own page shows it.
5. This pass. from-opencode: no new notes from OpenCode this session; their note (18) (the gate deploy,
whose §6 checks session 83 ran) was marked HANDLED 2026-10-15 in place with the verification line, so the
inbox reads clean rather than "every note closed but one unmarked". from-hermes: nothing movable — it now
carries 29 notes, 26 of them open, and the newest (22) is this session's build-visit report; the size remains
backlog, not clutter. hermes-resume: session 81 moved verbatim to the resume archive (blocks 84/83/82 kept).
This log: round 57 moved to the log archive, so the live page keeps 59 + 58.
6. A product question answered by measurement, and it opens the next build. Matthew asked whether the
pharmacy app needs a users section built before the patching work can be finished there. Measured on the
deployed app, inside the container, as the client tenant's own Business Manager (tc.bm.one,
PHARMACY_BUSINESS_MANAGER, org test-client-pharmacy): no — the users half exists (/pharmacy/staff/
200, and pharmacy identities are central with role + org_id, so the client's own admin is identifiable
without any new module), while the maintenance half is absent entirely (both maintenance routes 404,
and the GP/dental-shaped Manage Users / Audit Log nav items are 403 for every pharmacy role). The
maintenance tables are already in the database that app uses and the fork already reads the shared
ClientVPS table, so it is a port, not a project. Matthew decided it is the next build; the spec is
Pharmacy App — Client Maintenance Surface,
briefed to OpenCode as note (23), with the three pharmacy-specific differences written down: central
users not a per-tenant table, a ClientVPS model behind its own table, and the shared box. Shared-box
rule confirmed by him: the pharmacy tenant shares the dental box, so both tenants see the same box history
with one line saying the server is shared (design §6) — and production stays one box per client. He then
said he is arranging a separate Client VPS for the pharmacy tenant, so the shared shape is explicitly
interim: the migration plan for when it lands (move the pharmacy database and its ERPNext site database,
re-point the tunnel and the tenant row, restart the app, then the build visit) is on Pharmacy App — Client
VPS.
7. A disclosure of mine, and a rotation owed. Looking for the client tenant's host prefix I dumped the
whole Tenant row rather than naming the fields I wanted, and the client pharmacy database password
(test-client-pharmacy, the tenant's own app-user credential) appeared in my transcript. Nothing was written
to a page and no value is repeated here; the credential nonetheless needs rotating — into the
2026-10-18 window or sooner. The lesson is the one Round 58 already recorded, and I broke it again: name the
fields you print; never dump a row.
Carried forward (nothing changed hands this session):
OnCalendar reset line in slot_dropin(), andnext_run (nowsource).No phase advanced. Matthew opened pointing at the maintenance/patching thread and told me to read OpenCode's
notes before doing anything — they had finished the work the previous session found missing, so the session's
subject became the handback verification (note 19 §6), not new work.
1. The client patching surface is VERIFIED — the ruling holds, measured on the deployed apps. Script
/root/work/session83-s6-checks.py, run inside the deployed containers on the app engine (real views, mixins,
templates, tenant routing; the tenants' own users and roles; sessions opened by writing the session row so no user
row is touched; GETs plus deliberately-refused POSTs). CHECK 1 pass, both sectors: the client's own
manager/admin reach /infrastructure/ and /infrastructure/my-maintenance/ on
test-client.gp.veripath.co.uk and test-client-dental.dental.veripath.co.uk, see only their own box's rows,
and the dental page renders the canary's real run (record 26). CHECK 3 pass: both are 403 on
/maintenance/, /vps/, /fasthosts/ and on the platform host, while staff still reach the platform views (the
security check-in redirect is the app's own flow, not a refusal). CHECK 4 pass: a RECEPTIONIST /
DENTAL_CLINICIAN is 403 on the client surface and on the bring-forward POST, an anonymous request is 403, and a
synthetic row written against another client's box (dated 2001-01-01 so the check cannot pass vacuously) was
invisible on every surface and then deleted — 0 left, the estate back to 23 records / 1 client row / 0 patch
jobs. The client's own Request a run now was not pressed: that first press hands the canary its first real
patch run and stays Matthew's.
2. Three residuals, recorded (note 20 on from-hermes, and on the patching
design page). (a) GP: a client tenant whose own admin is is_superuser still gets the fleet/platform
dashboard, because is_fleet_view = user.is_superuser is evaluated before the client branch — measured: no client
card, and other clients' boxes named on the page; /my-maintenance/ is correct for the same user. (b) The
dental dashboard has no gate at all (InfrastructureDashboardView drops the mixin GP has), so a
DENTAL_CLINICIAN gets it 200 — no cross-client leak seen, but the gate is not applied. (c) The dental app's
MaintenanceRecord mirror is missing the real table's NOT NULL source, so a create from the dental app
fails (measured: same insert works from the GP app). §5 items re-measured unchanged: 0 cadence rows on any
client box, GP's client box has no runs at all, the host filter and status strip still cover platform hosts only.
3. A disclosure of mine. Introspecting ClientVPS rows I printed model fields through a denylist
(password, token, secret) instead of an allowlist, so the canary box's maintenance_token and job_secret
appear in my transcript. Nothing written to a page; a re-mint (client_patch_credentials) is available. Lesson:
allowlist the fields you print, never denylist.
4. This pass. from-hermes: nothing was movable, and that is the finding — the page holds 27 notes,
not the 23 its own header claimed (the claim was rewritten from the real statuses), and every one of the 27
carries live work: 25 open, one partly superseded, one parked. Note 19 itself stays, because its status still
carries §5's untouched items. The ~119 KB trigger is therefore met but the size is content, not clutter — what
this inbox wants is a processing pass, one note at a time (as Round 56 named), not an archive pass.
from-opencode: 18 HANDLED notes archived (the 2026-10-15 client-gate exchange, the SeaweedFS handbacks, the
rotation-rehearsal findings, the 2026-10-06 counter-views and socat notes) — 95.6 KB → 50.1 KB; every note
left behind is HANDED-not-HANDLED, OPEN or PARKED, including CV7/CV8, which keeps a live charge-picker
refinement. hermes-resume: session 80 moved to the resume archive (2 blocks left). This log: round 56
moved to the log archive, keeping 58 + 57.
5. An unplanned find, and it is cleaned: three pages carried the tool's own wrapper baked into their stored
content. While verifying the pass I checked the two inboxes for the --get wrapper artefact and found it:
from-hermes 3 artefact lines, from-opencode 9, and (whole-wiki scan) security/security-check-in 6 —
all trailing ==== rulers with repeated Page: https://… lines, which render as junk at the foot of the page.
Pre-existing, not mine: the pre-write captures show from-hermes already had one baked trailer and
from-opencode three (they accumulate one per bad write and survive every later pass, because the parser strips
only the outermost). Whole-wiki detector: 641 pages enumerated, 3 carrying artefacts, 0 prose-risk lines.
Cleaned with the round-41 tooling (wiki-sweep-wrappers.py --apply), every page captured to
/root/work/wrapper-sweep-backup/ first: 127 artefact lines removed, 0 prose lines lost, 0 mid-body clusters.
Re-run: 0 of 641 carrying artefacts; wiki-sweep-verify.py reports 127 lines removed, 0 non-artefact; both
inboxes re-checked (27 and 16 notes intact) and all three render 200 with no stray Page: line. One Page: string
remains on from-hermes — it is prose in a note that describes this very defect, so it stays.
6. The docs half of WO-D is DONE (2026-10-15), awaiting review. The client guide
is rewritten around the record and retitled Understanding Your Maintenance & Updates View. Scope was measured
rather than assumed: rendering the client dashboard as each client's own manager shows only the
Maintenance & updates card — VPS health, database, backup, tunnel, trend charts and the status badge are all
absent for a client practice — so the guide documents the card, the run list, and the two downloads (with their
real column names), states the schedule the client receives, and says plainly that the estate cards are
VeriPath's own monitoring surface. The withdrawn control is gone from the page. The card explanations the old
page carried for the estate cards are kept verbatim at
/root/work/client-vps-estate-cards-removed-20261015.md for re-homing on a platform-facing page.
7. Access to the GP box, corrected and opened (2026-10-15). Our record said the GP box's SSH key was
"revoked 2026-08-11"; Matthew says he never revoked it and he is right — re-tested with the three keys we
hold (as clientadmin and as root), all refused while the box answered on 22 offering password auth, so the
recorded "revocation" was an inference from one failed login. Corrected on the task page
(tasks/client-vps-test-client-monitoring-restore) and in the client-vps-ops skill. Matthew then supplied root
credentials; they live at /root/.secrets/test-client-root.pass (0600), used via sshpass -f, never on a
command line and never in a page, and the value arrived in a chat transcript, so it is a rotation item after
the install (or replace it with the estate key). Verified once: Ubuntu 26.04, up 13 weeks, client_db only, no
client-* timers, no /etc/veripath, 43 packages behind — so the box is ready for the agent install that opens
the next session.
Carried forward (nothing changed hands this session):
patient-images bucket, the shared-store reachability question, §7.6 and the SLA wording); and the surplussource.No phase advanced. The session ran the three start-of-session jobs, then reviewed OpenCode's four notes on the
MinIO → SeaweedFS swap, corrected one of my own claims from the day before, finished the WO-M7/WO-D documentation,
recorded a ruling from Matthew about the test estate, installed the client patch agent on the canary and — chasing a
question about where the client's card can be seen — found it gated to the wrong people entirely.
1. Housekeeping, three passes. Inbox: one HANDLED note archived, and the live page's 2026-10-06
reading-order index retired — the newest-on-top prepends had pushed it into the middle of the page and it still
named a closed set — replaced by a short preamble at the top; 100,308 → 93,451 chars, 23 → 22 notes, 22 of
which are open, so the size is a backlog of open notes rather than a stock of closed ones (recorded, because it is
the reason the trigger keeps firing with nothing to move). Resume: session 78 archived verbatim;
25,384 → 19,187 chars, keeping 81/80/79. Log: three resolved items trimmed out of the open-items block, and
my own claim of the day before corrected by measurement — the log's size is its two rounds (26.9 KB of 40 KB),
not the open-items block (6.1 KB) as last session recorded.
2. The pooler: my "live defect" was wrong, and OpenCode was right. They asked me to re-test over TCP from a
non-local host before recording it as broken. Measured two ways: a pooled SELECT 1 succeeds for both
databases (so the pooler's own server login works, whatever the test host does), and over TCP from the app
engine a deliberately wrong password is refused while the stored value is accepted. The two
user=postgres entries are hash-identical to the app's own current credential — i.e. the literal — and the
lengths are 8 / 20 / 32, not the 10 / 22 / 34 I recorded: the +2 was the ini's single quotes, which is
exactly what broke my probe (I tested the value with its quotes). So the pooler is a carrier, not a defect,
and their regeneration step is the right one. Corrected in place on the rotation runbook with the method error
kept visible, retracted in my note (16), and the lesson folded into the client-vps-ops skill. Also reproduced
their 85-hit baseline by running their own scanner from the primary (45 + 26 + 8 + container environments + the
three role-auth probes), and confirmed the session-dump carrier is in their purge list.
3. WO-M7 completed, and three stragglers closed. The three patching pages now name SeaweedFS (design,
how-it-works, promises), the design's stale "MinIO today / MinIO until the WO-M4 build visit" statements and its
WO-D/WO-E rows are corrected, and three pages that still described the old world are fixed: the Phase 8 DICOM/PACS
page (retitled SeaweedFS + Orthanc, current state on top), the clinician-facing imaging guide (no longer tells
dentists their scans live in MinIO) and the restore-drill page ("the store's risk is closed; the agent half is
what is open").
4. WO-D's docs half done — written from the deployed UI, not the work order. The dashboard guide gained the
Maintenance & updates card section (updates applied, the three restart states, next run, failed-runs badge,
Download/View all, and Request a run now with the honest note that it brings the run forward rather than gating
it) plus two FAQs; the onboarding runbook gained Phase 4b (tenant link, cadence rows, the real
client_patch_credentials + install-client-patch.sh commands, and what staff and the client each see).
5. Matthew's test-estate ruling, recorded in five places. The three boxes are a permanent test estate —
the estate the apps are developed against and the canary for patching — with access switching between SSH
(development) and tunnel-only (in service) as a standing mode. Recorded on the patching design's estate section,
both plain-English pages (the "we cannot log in" claim is now scoped to a real client's box, and the canary wave
says its first box is always a test-estate one), a dated ruling block on OpenCode's access-mode design, and my own
skill — which had been teaching "the model is no-SSH" as if it were estate-wide.
6. The canary install — the agent is live, and the loop works. On the dental test-estate box, in SSH/dev mode:
credentials minted, agent + four timers + health unit installed with the box's own slot (02:23), and the first
inventory posted outbound and landed as a record (client_inventory, success, Ubuntu 26.04, 40 upgradable,
0 security, reboot pending) — so agent → token → ingest → record → client-scoped view is proven end to end. The
box's containers kept their uptime. That also closes WO-M4's step 6: the box's image_refresh list now names
the client-tier PostgreSQL, SeaweedFS and Orthanc. Two gaps went to OpenCode in note (18): the installer writes
its env file so that a generated token containing $ (~half of them, by chance) is expanded when the file is
sourced — the agent would then fail silently — and that box has no docker compose, so
image_refresh --apply can pull and report but cannot recreate anything.
7. The find that matters most: the client surface is gated to the wrong people. Chasing where Matthew (logged
in as the tenant's own manager) could see the new card, we established that every client-scoped view inherits
PlatformAdminRequiredMixin — SYSTEM_ADMIN/ADMIN, i.e. VeriPath staff — while the mixin's own docstring says
a client practice admin may see their own box's rows on the tenant-scoped card. So the client variant renders
only when staff browse a client tenant, and the client's own manager is refused; the WO-C tests authenticate as
ADMIN, which is why it was never caught. Two further facts: the dental app has none of these views at all
(verified in its deployed container — no routes, no card), so a dental tenant has no route to their card whatever
the gate says; and no client box has cadence rows, so "next scheduled run" is unknowable for a box (only the
platform hosts are seeded) even though the box's own timers are real. Matthew's ruling: VeriPath does not log
in as staff/admin to any client VPS, test estate included — the surface is for the client's own manager/admin, who
have to produce the evidence. Recorded as an OPEN DEFECT on the design page and as work order note (19),
with the role inventories measured from the tenants' own databases (test-client: ADMIN,
PARTNER_REGISTERED_MANAGER; test-client-dental: DENTAL_ADMIN, DENTAL_PRACTICE_MANAGER;
test-client-pharmacy: no users_customuser table, so its users need finding first).
8. Also reviewed: OpenCode's notes (13)–(16) verified against the estate and marked HANDLED, and the swap
assessed as complete at WO-M1–M6 with four residuals — the dental box's MinIO decommission (its own
backup-cycle gate), deleting the surplus local MinIO image (awaiting Matthew's explicit confirmation), WO-M4's last
step, now done, and the orphan patient-images bucket still undecided.
Carried forward (Matthew's order, given 2026-10-15): the backend /infrastructure/maintenance/ first — a
box's cadence rows (nothing in the app knows a box's schedule), then the host filter and status strip
covering client boxes — then test-client, then test-client-dental, then test-client-pharmacy; after
that, documentation and DSPT policies. Still owed separately: the DSPT evidence record after Sunday's
2026-10-18 02:00–04:00 rotation window, the pooler's own client-credential rotation, and the estate items
(VPS1's pending reboot, the client-tier migration cadence gap, weekly image scanning covering the primary only).
No phase advanced. Matthew opened pointing back at the maintenance/patching thread and asked me to say when I was
ready. The session became one discovery leading to the next: a reachability test that corrected an access
conclusion I had recorded the day before, and that correction then reached four pages plus a work request.
1. WO-M4's blocker is a credential, not a site visit — my own conclusion of 2026-10-14 was wrong. I had read the
box's Permission denied (publickey,password) as "cannot be done remotely / needs a build visit". Checked properly:
the box answers on port 22 from the app engine, on both its public and its tunnel address, and the failure is at
authentication. The cause was already in our own records —
Restore Test-Client VPS — SSH Access & Monitoring carries the
SIAAS no-SSH decision of 2026-08-11: key access to the client VPS was revoked (the primary's
/root/clientadmin-key is stale), the box is monitored by a TCP port-22 probe, and the documented way back in is
"the client, or a Fasthosts control-panel reinstall". Its registry row also has no hosting account and no
provisioning record, so it was built outside the pipeline and no credential was ever captured. The lesson worth
keeping: a failed login is evidence about authorisation, not about reachability — separate the two before concluding
anything about access, and check the estate's own records for a revocation decision before asking an owner for a
credential he may never have held. (He held none; the ask was wrong twice over, and he was right to push back.)
2. The correction went to four pages in one pass. The runbook — retitled Client Box Radiograph Storage Swap —
Access Runbook (WO-M4), with a new Access section (reachability vs authentication, the one-way
forwarding-restricted key model, the registry/hosting-panel checks, the ban-risk caution) and a dated correction
record; the plan page (both WO-M4 rows and the open-decision line, with OpenCode's Phase 0, build record and erratum
verified intact afterwards); OpenCode's note (10) status line (marked PARTLY HANDLED in place, keeping their text
and the audit trail); and a new handoff note (12). Also removed an internal address that had been sitting in the
runbook's tenant-row line — the page is now clean of internal addresses.
3. The rotation window's premise was stale and its evidence scope blind. Pre-checking Sunday's 2026-10-18
02:00–04:00 window found the runbook's stated shell "there is no connection pooler in the estate (no pgBouncer /
Pgpool container on any host)" — false: gp_booking_pgbouncer (edoburu/pgbouncer) runs on the app engine with
its own config directory /etc/veripath/pgbouncer/. Verified rather than assumed: no app container's DB_HOST
points at the pooler (it serves the scalability rig), its upstream credentials are distinct literals and not
the 8-character value the window rotates (hash-compared; values never printed), so the window will not break it
— but the execution pack's scan tooling (scan-literal.sh, mapping.md, the verifier) carries zero references
to that path, so the closing "0 hits" evidence is blind to the pooler's directory. Both corrections are recorded
on the runbook, and the scan request went to OpenCode in note (12). The pooler's own credential rotation stays a
separate owed item (my 2026-10-14 disclosure) and was deliberately not folded into the window.
4. One patching gap fixed while there. The pooler was missing from the app-engine image-refresh list, unlike
the other tag-only third-party images: added to image-refresh/vps2.conf, to the deployed
/etc/veripath/image-refresh.conf (backup …bak-20261015, and the service name validated against the compose file
before adding it) and committed as 0b4ab48 on feature/seaweedfs-wo-m1-m5. The four tag-only :latest images
(pgbouncer, orthanc, ollama, mailhog) are a question, not a defect — the sweep handles tag-only images by
design — asked of OpenCode in note (12).
5. Measured, not estimated — and it corrected me again. I opened by saying the log wanted its prune pass.
Measured: the live log is ~29.7 KB holding a single round, against a trigger of ~35 KB or more than three rounds
— not met, so no prune. (Re-measured after the session's later rounds: the page is now ~41 KB holding two
rounds (55, 56), so the size trigger is met but there is nothing to move on rounds — the size is the
open-items block. Next session: review that block for resolved items rather than expecting rounds to shift.)
The from-hermes inbox is now ~100 KB against a ~100 KB trigger, so it takes its
archive pass (HANDLED notes only) next session; and the resume note will then hold four blocks
(sessions 81/80/79/78), so 78 comes off — never the block just written.
6. The restore drill — run, and it is the other half of the WO-M6 gate. The gate asked for one full backup
cycle and one restore drill; the cycle cleared at 03:01, so I ran the drill — and it is a store drill, not
the database one: the gate is about the radiographs, so proving the wiki dump restores would have answered the
wrong question. Newest volume tar (seaweedfs_data_20261015-013001.tar.gz, 17,151,649 bytes) restored into a
scratch tree: gzip intact, extract clean, 86 files / 18,467,929 bytes — identical to the live directory with zero
size differences. Then the part that makes it a real drill: the restored copy was served on a throwaway
container (loopback only, the live path never touched) and compared with production over S3 — same bucket, 67
objects, 18,389,458 bytes, three sample PNGs md5-identical, the 0600 credential restored from its own tar, and
the offsite vault confirmed to hold the same tar. Tear-down proved production untouched: same StartedAt, 0
restarts, healthy, still serving 67/18,389,458. Written up as Drill B on
the drill page with its procedure, so it is repeatable rather
than a one-off. WO-M6's remaining blockers are now the rollback-window decision (his) and the client box.
7. Housekeeping: one pass done, and one trigger measured false. The from-hermes inbox crossed 100 KB with
note (13), so it took its archive pass — 4 HANDLED notes moved verbatim (the WO-D split, the front door's
Keycloak half, the gated-hostname request, the pooler-verification ACK), 101,117 → 83,088 chars, 25 → 21 notes,
with every remaining note verified byte-identical afterwards. The log's own prune trigger was checked and is not
met on rounds — it carries two (55, 56) — and the size is the open-items and conventions sections, which the
convention keeps. What the inbox actually holds is a backlog: 19 genuinely-open notes, the oldest from
2026-09-26, several reporting completed work that has never been worked through. That wants a processing pass, one
note at a time, not a prune — named here so the next session starts it deliberately.
8. OpenCode's handback — and two more corrections of mine. Their notes (13)/(14) reported WO-M4 done, the pooler
as a rotation carrier, and the access-mode design for review. Checked rather than accepted, and two of my earlier
findings did not survive:
Exited (0) with its volume retained, SeaweedFS pinned and bound at port parity on 9000, the tenant rowmigrate_tenant_radiographs --verify reporting 6/7 through the tunnel. Recorded on the planscan-literal.sh scans/etc on both hosts). Their carrier claim is wrong too, but for a more interesting reason: the pooler holds nopassword= value hashed — lengths 10/22/34, none matching) and instead holds stale credentialsdsp_clinic and veripath_dental (tested directly; the role's real verifiergenerate_pooler_config is needed before the window, not only after — and the pooler's serverDB_HOST points at it (it listens on 4321).--status must workold-value, the credentialCarried forward:
image_refresh list) and the patching service's firstpatient-images bucket, the shared-storepatching-how-it-works, patching-promises, design §7.1). Standing: the secrets-sweep rotations, theNo phase advanced. Matthew opened by pointing back at yesterday's maintenance/patching thread. The session's
job was the gate that has held that work since 2026-10-13: the rulings on
Client VPS Patching — Managed Service, which WO-A…WO-D cannot be
raised without.
1. How the rulings were taken — worth keeping as the method. He opened with no view and asked me to say
when I was ready; I re-read the cold-start note, OpenCode's inbox, this log and the patching page, and verified
the one thing that had to have run since — the app engine's 2026-10-14 03:00 bootstrap, which did: 44
applied, 1 left (phased), reboot performed at 03:03, no reboot-required flag, all 16 containers back and
healthy, stack guard active. Then I put one ruling at a time: §7.1 (cadence), which he approved on
condition that the shape actually achieves the objective under tunnel-only access — so before recording it I
checked the condition and it holds: every recurring step runs on the box, so none needs an inbound path or a
standing login, and the build visit is the only access-dependent step. He then ruled §7.2 (the restart policy),
chose the hybrid, and said plainly that he did not follow my explanation — which is why the plain-English
pages below exist. After that he delegated the remaining engineering shape to my recommendation ("press ahead …
I can read up later"), so §7.3–§7.5, §9.6.1–§9.6.5 and §10.6.1–§10.6.4 were recorded as delegated rulings
with the reasoning attached.
2. What is now ruled (page v0.4, 12 items ruled, 1 left open): cadence (continuous security updates +
monthly full run per box + quarterly image refresh, per-box minute-level slots, outside clinic hours, clear
of the box's 02:30 backup); restart policy (never silent — 7 days' notice for a security-critical restart,
a 30-day ceiling with a second notice for anything else, notice and outcome recorded, the client's press
being bring-forward only); method (the agent baseline installed at the build visit, method C a fallback for
a client who refuses an agent, central orchestration rejected — no inbound management path, no centrally
held per-box credentials, no single point of failure; permitted actions a fixed published list verified against
a signed job); client evidence (CSV and an auto-generated quarterly pack held with their backups;
retention defaults 24 months kept / 12 shown); detail level (a sentence on the card, the package list in the
page and the exports); SLA (7 days / monthly / quarterly); waves (canary 1 box → 10% → remainder, halt on a
failed check or a missed reporting window, no automatic re-run, halt and re-run recorded as their own events);
patch-or-replace (in-place for the database host, rebuild-from-image for everything else); recovery (agent
self-heal first, then a plain-English client-side runbook, then an on-demand temporary credential logged as its
own event — a stored credential under dual control rejected); and the build-visit runbook as a first-class
deliverable. Still open: §7.6, the commercial item — and it does not gate the build.
3. Two reconciliations I had to make while ruling, both recorded on the page rather than smoothed over.
§10.3 recommended method B (the client pressing "Install now") for disruptive work, which contradicts the
hybrid as just ruled (the ceiling means a run does not depend on them pressing anything) — resolved by making
the button the bring-forward path, not the gate, with §10.3's row to be read as "available to them, not
required from them". And §9.6.1 asks to "accept the WireGuard overlay": the direction is accepted but the
mechanism is now an evidence question — the overlay attempt has been run and failed (inbound UDP never
arrives; the interfaces report 635 KiB sent and 0 bytes received), so it stays open on the scaling work and is
explicitly not a prerequisite for the first client boxes, because the agent reports outbound over the path
the backup heartbeat already proves.
4. Three plain-English pages written, because the owner has to be able to read this without an engineer.
infrastructure/SIAAS/client_vps/: patching-how-it-works (the access constraint, the three kinds of update,
what a restart means to a practice, the restart rule, what the client sees, the honest limits),
patching-evidence (the card, the run list, the CSV and the quarterly pack, retention, and why the record is
read-only — evidence rather than a claim), patching-promises (the 7-day/monthly/quarterly promise, waves and
the halt rule, patch-or-rebuild, the layered recovery path, and the honest caveat that the agent is delegated
rather than keyless). The design page now links to all three as companions, and all 16 inbound links across
the four pages resolve 200 (checked). The convention this establishes, worth reusing: the engineering page
carries the reasoning, a plain-English companion carries the decision — one page per ruling cluster, not one
per ruling.
5. WO-A…WO-D raised to from-hermes (note 2026-10-14 (8)) with every ruled
parameter written into the note so the build brief needs no re-reading of this page, the two reconciliations
above, and one question I could not answer myself: who can produce the read-only inventory of the two
test-estate boxes? My own §1 finding is that the primary has no management path to a client box, so WO-B's
"first, a read-only inventory" cannot run from here at all — either somebody still holding a credential runs it,
or it becomes the agent's first outbound job. Recommended building for the second regardless, since a real
client's box will only ever be inventoried that way.
6. OpenCode's handback landed the same day and I verified it rather than accepting it (WO-A, WO-B, WO-C).
Their three notes (2026-10-14 (4)/(5)/(6)) claim the client dimension on the maintenance record, the client-box
agent with its signed-job poll and build-visit runbook, and the client-facing card/detail/export including the
§2 leak fix. Checked independently, not taken on trust: migrations 0012–0014 applied on the central
database; the new routes deployed and gating exactly as the pre-existing platform view does (403
unauthenticated rather than 404, probed with the real Host header and over the public interface — the first,
bare probe on 127.0.0.1:8000 returned 404 for a pre-existing route, which is the tell that the probe was
wrong, not the app); the agent scripts and docs/client-build-visit.md present in veripath-ops at cf2462f,
with the reboot state machine reading the ruled defaults (7 / 30) from the environment; the leak fix read in the
code — a client-tier request scoped through client_maintenance_for(tenant) while the fleet view is untouched;
and the application suite re-run by hand on VPS1: 53 tests … OK against a disposable Postgres, matching
their claim exactly. The two reconciliations were accepted as written, and they chose option (b) for the
inventory (the agent's first outbound job) — the only method a real client box will ever have.
7. One correction the verification forced, and it is the reason to do this rather than skim. My §10.6.3
ruling asked for agent self-heal "with an automatic revert where the platform allows it", and the
plain-English page then described that as the box putting the previous kernel back. The build does not do
that — it ships a post-run/post-boot health report and holds a restart that has not succeeded; automatic
package rollback is not built. So the promise page was overstating the service in the direction that matters
most (what happens when a patch breaks a box). Both plain pages were corrected the same evening, the design
page now carries an explicit build status under the ruling separating what is built from what is ruled, and
the outstanding halves — the client-side recovery runbook and the break-glass credential — are named as work
rather than implied as done. Lesson worth keeping: when a ruling is written as an aspiration ("where the
platform allows"), the handback must be read against the ruling line by line, or the aspiration becomes a
claim in the client-facing text.
8. The MinIO replacement became a plan, and WO-D was parked behind it — by his ruling, not mine. He chose
SeaweedFS himself (so the decision gate on that page is closed) and asked for the rest of the day spent
turning the scoping note into something OpenCode can be instructed from. The
scope page is now PLAN v1.0 — retitled, with the
decision and what was weighed, the pinned image (chrislusf/seaweedfs:4.48, amd64 digest recorded, and the
two traps: the repo is chrislusf/ not seaweedfs/, and there is no :latest tag), the seven calls the app
actually makes mapped to SeaweedFS's own S3 support table, WO-M1…WO-M7 with acceptance criteria each, the
side-by-side rollback rule, and five decisions left with him (encryption at rest, retention, the rollback window,
the client-box timing, the orphan bucket). Two things the plan turned up that the scoping note did not have: the
app's endpoint is a single-label service name on port 9000 whose resolution is not defined in the dental
compose on the apps host, so it has to be found before cutover rather than assumed; and the MinIO service lives
in /opt/infra, not in the dental app's compose, with the data on a bind mount. He also ruled the order —
MinIO first, WO-D after — accepting the coupling I flagged: the dental client box's own MinIO is one of the two
images the client-tier quarterly refresh covers, so the swap settles that list. WO-D is marked PARKED on the
design page and in note (9) of from-hermes, so OpenCode cannot start it by
mistake.
9. The SeaweedFS swap: the plan, the ruling, the build, and my verification of it — one session, one thread.
He chose SeaweedFS himself and had me spend the rest of the day turning the scoping note into an instruction
OpenCode could work from; the scope page became
PLAN v1.0 with WO-M1…WO-M7, acceptance criteria each, a WO-M1 appendix, and the side-by-side rollback rule
(item 8 above). Then it all happened inside the same day, and the interesting part was not the build — it was
being wrong twice and having the estate say so.
sha256:eb1b8173… as amd64; it is linux/arm/v7. Their4e61d15f…, amd64 aba492e2…, arm eb1b8173…). A pin is only as good as the platform it names.test-client-dental"; their Phase 0demo_*MINIO_ENDPOINT=minio:9000 does not resolve on the apps host at all. So I ruled (a) keepmigrate_tenant_radtiographs.py documents shared-store → tenant's own VPS) and what the client bought. The.dcm plus two PNGs md5-identical; estate stateminio untouched throughout.image_refresh list must not be switched before its own swap.10. The one thing I got wrong about process, and the fix. I overwrote OpenCode's record on the plan page:
they wrote their Phase 0 and build record onto it, and I upserted from a copy fetched before their write. The
wiki has no merge and no conflict detection, so the last write won silently and their additions vanished. Nothing
was lost permanently — their inbox notes carried the substance — and I reconstructed the record labelled clearly
as my reconstruction, asked them to replace it with the authoritative version (they did), and disclosed it to
them in the handoff note. My own method changed: re-fetch a page immediately before every write and diff it;
if it carries sections I did not author, stop and merge by hand. Written into the wiki-housekeeping skill, where
it belongs, rather than remembered.
Carried forward (tomorrow):
patient-images bucket; and whether theNo phase advanced. Matthew opened with no view on the ramp's anchor rate, so rather than guess it we cleared
the one thing the front door was waiting on, and he then ruled that the patching/maintenance work resumes in a
new session.
1. The front door opened. The scaling vhost was staged and blocked on OpenCode registering the hostname with
the site's auth service, which their queue had not reached. Checked the running service rather than the repo
(it runs from the same checkout, and its map held only the two manuals hosts), reproduced the login loop it would
cause, and applied my half in Keycloak — the cms client now allows the scaling callback. Matthew's ruling:
if no site-code work was queued for OpenCode, do the edit myself — so the host-map entry was applied here, the
service restarted, and the vhost enabled: /scaling/ answers 302 to the login and never the tool, undefined
paths 404, and his own login then proved the allow path (session minted, then the tool's 200s).
2. The finding that mattered: the realm brokers partners. Testing his stated requirement ("anyone in the
veripath realm is fine, outside it would be a risk") showed the veripath realm brokers the partners realm —
enabled, linkOnly: false, three users, and its button renders on this host's login page — so a
partner-organisation identity would have been accepted by the gate as a veripath session. The gate asked only
"is there a session?", never "who issued the identity". Closed at our gate: a host-map entry can set
nativeOnly: true, which refuses a login whose ID token carries Keycloak's identity_provider claim before
a session exists; the decision lives in server/auth-policy.mjs with a shipped unit test (11 assertions)
rather than in a session script, and both manuals hosts are unchanged. The refusal path is unit-tested but not
exercised with a real partner credential — the honest limit, offered as a fixture test.
3. One thing fixed in passing, and one lesson. The auth service — the gate for every gated host — was an
unsupervised nohup orphan, the only site-stack piece without a unit (veripath-cms-proxy.service and
veripath-webhook.service both had one); it now runs under veripath-auth-server.service, which is also why
a host-map change has a restart path at all. The lesson: page bodies are server-rendered, so curl plus a grep still verifies content — but the
sidebar is not in the HTML at all (no nav markup; the only cross-page links present are the pages' own
body links). A curl-based check of navigation is therefore meaningless, and my first attempt at it silently
measured body links instead. Verify the tree through the tool: --folder <parent> for the folder flag,
--list for enumerability, --dedupe --check for duplicate rows.
4. The rig is now documented. The scalability rig — how to use it:
the three panels, the client ladder, the four pressures, "binds first" and the knee, model-versus-measurement,
the arm shapes and the honest limits. Every figure was re-derived from the rig's own model.py rather than
recalled (0.02 req/s per client binds the hub at ~498 clients; 0.2 binds the app tier at 68 direct against 263
pooled), and the guide says plainly where the model is conservative. The scaling topic page carries an inbound
link; the repo README points at it; the infrastructure folder node was confirmed still a folder and the wiki
has no duplicate-path rows.
5. Close-out. from-hermes notes (5) and (6) marked HANDLED with a
closure record and (7) opened for the merge (hermes/scaling-auth-host-map) plus the broker-button decision; the
resume note trimmed to 79/78/77 (76+75 verbatim into the archive, 20,352 → 15,847 chars); this round.
Carried forward:
hermes/scaling-auth-host-map; the broker-button decision (a dedicated client/api/auth/login?redirect= (every gated host) and thecode landing in the nginx access log.No phase advanced. Matthew opened with "let's do some housekeeping first", which is exactly what was owed
after session 77: the from-hermes inbox was 100,807 chars (past the ~100 KB trigger), the resume page carried
five session blocks (77, 76, 75, 73, 72), and the log had no Round 52 — the previous session ended without
writing its round.
1. from-hermes inbox archive pass. The page's own rule is that only OPEN notes stay on it, so every note that
is not OPEN and dated on or before 2026-10-13 was moved verbatim (newest on top) to
archive/2026-10/from-hermes: 13 notes, the whole
WO-1 → WO-9 patch-automation day of 2026-10-13 (three work orders, their ACKs, the auxiliary-host extension,
the window-plan correction, the DSPT rotation-window note) and the four 2026-10-11 notes (@8043 identified and
retired, the socat-retirement authorisation and its handbacks). Live inbox 100,807 → 56,794 chars, 27 → 14
notes; archive page 21,201 → 66,146 chars, 6 → 19 notes. What stays is deliberate: the two 2026-10-14 notes
(the current handback pair) and all twelve OPEN notes — the open backlog from 2026-09-26 → 2026-10-08 is now
the bulk of the live page, which is what an inbox is for. Verified by re-read: every moved note present in the
archive, none left on the live page, and both pages confirmed through their rendered URLs.
2. Resume trim. Sessions 73 → 72 moved verbatim and newest-first to
hermes-resume-archive; the live page is 18,387 → 14,154 chars and
keeps 77, 76, 75 plus the cold-start rule. The archive's own intro was updated so it still answers "which
sessions are where".
3. Round 52 back-filled for session 77 rather than left as a gap in the audit trail — the log is the round-by-round
record and a session that ends without one leaves the next reader guessing. Written from the resume note's own
record, not from memory.
4. Two small things worth keeping. The page description field is a 255-character varchar: the first write of
the inbox pass failed with StringDataRightTruncation on the live page while the archive half landed, so a failed
tools write can leave a pass half-applied — the script was made idempotent (it now detects an archive that
already carries the moves and writes only the outstanding half) and the description was shortened. And the archive
intro's dated line cannot be grepped out of the rendered HTML when the date is wrapped in ** — verify against
the stored body, not the page source.
5. OpenCode's pooler handback verified independently (the last work order of session 77), and one credential-handling
item raised by my own mistake. The verification is written up at
apparatus §9a: the connection-setup tax measured 49.0 ms → 1.47 ms by running
the same instrument against the direct path and the live pooler in one process, throughput 51.7 req/s at c10 and
78.3 req/s at c40 with zero failures, central connections peaking at 11 over 1,000 requests, 7/7 of the web
app's database aliases resolving through the pooler, and the three isolation refusals reproduced (wrong password, a tenant
DB username, an unlisted database). Three residual observations are recorded there: the pooler user is a single key to all
five listed databases; admin_users/stats_users are the same credential the app uses; and log_connections is off, so
the pooler keeps no per-connection record.
The mistake: while checking the web container's environment I printed the pooler credential into this session's
terminal output (a printenv grep that should have been masked, as the other three checks in that batch were). It is a
host-local service credential, it is not on any wiki page, and the value never appeared in this log — but it did leave
the host in a tool transcript, so rotation is owed: the credential is generated (generate_pooler_config writes the
md5 line into userlist.txt from POOLER_PASSWORD), so rotating it is a fresh value in the app's environment plus a
regenerate-and-RELOAD. That is OpenCode's territory and Matthew's decision; recorded here rather than quietly dropped.
Lesson for the sweep habit: mask first, then look — the rule is not "don't print passwords", it is "don't run a
command whose output you have not constrained".
6. The rest of the session: the pooler verified, the rig built, and a tool Matthew can drive (the same day,
after the housekeeping above). Verification: OpenCode's per-tenant pooler handback re-measured independently — setup
tax 49.0 ms → 1.47 ms, throughput 51.7 req/s at c10 / 78.3 at c40 with zero failures, central connections
peak 11 across 1,000 requests, 7/7 aliases through the pooler, the three isolation refusals reproduced; written
up as apparatus §9a, with three residuals and one disclosure I have to own: I printed the pooler credential into a
session transcript (an unmasked printenv grep). Rotation is owed and sits with Matthew. The rig: built to stage 1
and verifiable end to end (rig/scaling-rig, verifier 74/74, ad0b788 → fa22718) — its own network and reserved
port range behind a restricted key, the matched production image, both tunnel legs per simulated client, register rows
the app builds aliases from, synthetic volume through the real models, and a tenant page serving 200/302 over both legs;
two arm shapes (fidelity / multiplex) so the rig host never becomes the ceiling. Three findings, none about scale:
the app cannot migrate an empty database into its own schema (cold-standby relevance); a session minted for one
simulated tenant authenticates on another tenant's host (unresolved, OpenCode's); and my own new model had inherited the
falsified app-tier premises, so rig/model.py now labels every constant and treats the central cliff as a parameter.
The tool: rig/rig-ui.py — enter a client count and a per-client rate, see what binds first, the curve and the knee,
and start/stop a run. The front door: Matthew chose scaling.veripath.co.uk, added the A record, and the
certificate is issued (bootstrap vhost removed afterwards, estate unchanged); the vhost is staged and gated, blocked
only on the site auth service registering the new hostname (WO to OpenCode, 2026-10-14 (5)).
Carried forward: unchanged from Round 52 — the UDP transport mechanism decision, retention + rollup
before the metrics tables reach 5M rows, the pooler as GP-only, and the rig, which is mine to build.
Phase 4 remains the next named phase; nothing in these passes touched it.
Back-filled 2026-10-14 (session 78) — session 77 ended without a round entry, so this is written from the
resume note's own record. No phase advanced.
1. The apparatus and the maths. Matthew opened by pointing at the captured Gemini chat on
infrastructure/scale_siaas; my review of it (the proposed rig would not have measured the app tier and could not
have run on this kit) and the corrected rig are published as
Scalability Testing Ground. Then the measurement: two instruments in
veripath-ops/scripts — scale-measure.py (connect / roundtrip --compare / cpu) and scale-model.py (constants
max_connections 100. Written up at2. The gate failed. Inbound UDP never reaches the primary: two unrelated networks, two ports (one already
permitted by the firewall), a capture on the hub — zero datagrams, while TCP from the same hosts answers in
10–25 ms and their DNS-over-UDP works. Corroborated from inside: the overlay interfaces report 635 KiB sent,
0 bytes received, no handshake ever. So a WireGuard-over-UDP overlay cannot be deployed as drawn; the first
question is whether the filter is the provider's cloud firewall (controllable) or their network.
Separate finding: a half-built overlay that has never passed a byte, with no alert.
3. The staggering fix deployed. The per-client capacity cron (one cron line and one credentials file per
client, all firing in the same minute — a hard stop at ~91 clients against max_connections = 100) is replaced by
/opt/collect-capacity-sweep.py + /etc/veripath/clients.json: one cron line for any number of clients, each
client's slot derived from its slug. Verified end to end (a real snapshot row landed through the new path) and the
cron daemon is confirmed invoking it. Old crontab kept at /root/capacity-monitoring.cron.bak-2026-10-14.
4. The app-tier work order came back and corrected the model (OpenCode's 2026-10-14 handback): gunicorn +
2 uvicorn workers shipped, CONN_MAX_AGE withdrawn (under ASGI every request gets its own thread and therefore
its own connection — persistence cannot reuse and in fact leaks; 60 s of max-age exhausted the central connection
limit at ~150 requests), and two fresh connections per request (central + tenant, ~50 ms each) with throughput
flat at 25–30 req/s. That promoted the pooler from sixth to first; the follow-up work order is posted and
was handed back the same day (pgbouncer, c40 74 req/s) — see
from-opencode and apparatus §8/§9.
5. Decisions: the app-tier fixes are agreed and briefed; the collection host is deferred by my own decision
(triggers recorded), superseding the capacity plan's "cheapest big reliability win"; the gate was run and failed;
and the rig is mine to build (generator, seeded databases, a reserved port range, its own tunnels and its own
app container, switchable with and without the pooler in front).
Carried forward: the UDP transport mechanism decision; retention + rollup before the metrics tables reach
5M rows; the pooler is GP-only (dental and pharmacy unchanged — extend on his word); the rig; and, as
housekeeping debt, the from-hermes inbox past 100 KB and a resume page carrying five session blocks.
No phase advanced. Matthew opened the session asking for a clear-up of the outstanding items. All three are
closed and verified, and the sweep turned up a defect in my own tooling that had already begun to destroy page
content.
1. The whole-wiki wrapper sweep is done — 628 pages enumerated, 0 carrying artefacts. Round 50 found the
Phase-8 DICOM/PACS page carrying a baked --get dump and flagged that a whole-wiki sweep was owed; the blocker
was that --list returns at most 50 rows when called without a search term, so nobody had ever enumerated the
wiki. Fixed by calling it with a wildcard term (--list "%"), which returns all 628 pages through the sanctioned
tool. New /root/work/wiki-sweep-wrappers.py (report-only by default, --apply to clean, eight parallel readers,
full report to /root/work/wiki-wrapper-sweep-report.txt). 33 pages carried real artefacts — 32 with
header/trailer clusters and one with three dumps baked mid-body
(agents/handoff/archive/2026-08/from-opencode). 126 artefact lines removed in total, 0 non-artefact lines,
audited per page by /root/work/wiki-sweep-verify.py against pre-change captures in
/root/work/wrapper-sweep-backup/. Worst offenders: eViewPlus/enhanced-lab-order-flow,
security/security-check-in and veripath-accounting (five stale trailers each),
dental_app_developer_plan and SIAAS/medical_imaging (baked header dumps).
2. A defect in my own tooling, found by the audit — and it had eaten a real section. wiki-clean-artefacts.py
deleted from the start of a baked trailer run to the end of the body, so any legitimate prose sitting after a
stale trailer went with it: eViewPlus/enhanced-lab-order-flow lost a whole QA-testing checklist (3,177 chars)
and guides/client-admin-guide/users-and-roles lost its Related Pages section. Both were restored from the
pre-change capture and re-cleaned with a corrected algorithm — every deletion is now confined to artefact-shaped
lines inside a run that a Content (N chars): or Page: <url> line proves is the tool's own wrapper. A second
bug is worth remembering: the trailer regex used \S+, which cannot match a URL containing a space, so every
page whose path contains a space reported the tool's own trailer as a baked artefact (the eight
guides/AES Guide/* pages — written back unchanged, harmless, but they would have inflated every future sweep).
The corrected parser takes the Content (N chars): line as ground truth. The tooling is now three files:
wiki-wrapper-lib.py (parse, detect, strip — read-only), wiki-clean-artefacts.py (CLI over it, --deep for
mid-body clusters) and wiki-sweep-wrappers.py (the sweep). Round 41's defect class is now cleared wiki-wide,
not just where a session happened to touch it.
3. Both overdue housekeeping passes are cleared. The from-hermes inbox was 111 KB: 6 HANDLED/ANSWERED
notes (2026-10-02 → 2026-10-06) moved verbatim into a new October page
agents/handoff/archive/2026-10/from-hermes — the September
page is 360 KB and its own header asks for the month to be split. The live inbox is now 91 KB / 25 notes, and
only open notes stay: the four still open from that period are the two 2026-10-06 collection-guide findings, the
receipt print-stylesheet refinement, the counter-views brief (verification done, one refinement finding) and the
2026-09-26 backup-gzip items for Matthew. The resume note was 26 KB with sessions 65–71 verbatim: sessions
71 → 67 (with the page's own condensed 66 and 65 closure blocks and the 2026-10-06 trim note) moved verbatim
and newest-first to hermes-resume-archive; the live page is now 6.5 KB and keeps sessions 75, 73 and 72
plus the cold-start rules.
4. This log pruned. Rounds 6–49 moved verbatim and newest-first to
agents/hermes/housekeeping-log-archive; the live page keeps
Round 51 (current), Round 50, the phase tracker, the open items and the conventions. The log had regrown to
248 KB with the current round buried under 44 earlier ones — the same regression Round 7's prune fixed, and worth
watching: the cause is that each round is prepended above the tracker, not that the prune fails.
Flagged for the next pass: the resume archive now carries two write-ups of sessions 66 and 65 (the fuller
2026-10-11 pair and this trim's condensed copies) — left verbatim rather than silently dropped.
5. A scope decision the same session: client-VPS patching is now IN SCOPE. Matthew ruled that patching
the SIAAS client VPSs forms part of the managed service and that each client must be able to show their own
patch record to an inspector. Spec written:
Client VPS Patching — Managed Service — the run performed on
the client box and reported outbound (the pattern the backup heartbeat already proves, and the only method
that needs no inbound path to a box whose design point is that it has none), a "Maintenance & updates" card on
the client dashboard that already exists, and the data-model change: MaintenanceRecord.host FKs to
PlatformVPS, which is why no client box can appear in the platform view. Found while writing it — a leak:
the client dashboard's existing maintenance card queries the platform-wide table with no tenant filter, so
any client practice admin sees the platform hosts and their patch state. Platform metadata, not patient data,
but not theirs; the card must become client-scoped as part of this work. Assessment gap 5 and the patching
log's limitation 4 are updated (no longer "outside scope"), and the client-side MinIO copy on the dental client
box is added to the replacement scope. WO-A…WO-D await Matthew's rulings (§7 of the spec). §9 answers the
scale question he then raised — a few hundred clients: the transport breaks before the patching does (per-client
reverse-SSH forwards put the primary in the data path of every client's database), the control plane is what
scales (agent in the provisioning pipeline, poll-for-work, wave rollout behind a health gate), the metrics are
the data problem (~10.5M rows a year at 300 clients), and the platform tier's own capacity is the axis that
grows with the whole estate (central Postgres, the shared app tier, the tunnel hub, collection). Matthew then
set the position that turns this into a plan rather than a worry — assume it will not hold until measured, and
pre-plan the signs that tell us when to act — written up as
Platform Capacity & Growth Plan: a measured baseline, the trend
data we currently lack, a scale test to find the real ceiling, and the trigger/purchase-order table. At
Matthew's request the scaling topic now has its own discussion page —
Scaling the Platform, separate from patching and maintenance: the seven levers
and their state, the decision log (what was considered, kept, rejected), what is measured vs what is genuinely
unknown, what to do first, and the agenda for 2026-10-14.
Carried forward:
performed reboot through the fixed script), then the 2026-10-18 rotation window and my dated DSPT evidencetrigger=reported with stale/up_to_date, or akind=image_audit — their call) and the remote-scanning work order with its cost read.No phase advanced. Both remaining build items landed inside the hour, and verifying them turned up a
finding that is bigger than either work order.
1. The quarterly image refresh has a trigger at last (WO-7). refresh-image.{service,timer} enabled on
all three hosts, OnCalendar = the 1st Saturday of Jan/Apr/Jul/Oct 03:30, next fire 2027-01-02, running a
report-only sweep against per-host stack lists, one row per stale service. Proof run: 12 services checked,
9 stale, timer-driven status 0, then a full apply-then-rollback on netdata — applied, healthy, rolled
back. Both directions proven with production back on its original pin.
2. Per-run package detail is live (WO-8). detail_json accepts the run's apt Upgrade: list with
from → to versions and a security flag; the view expands the row and badges security updates; a second
export at /infrastructure/maintenance/export/packages/ carries the detail as its own CSV; 17 tests green.
Two things I would not have thought to specify but are right: the ingest is object-only and 512 KB-capped,
so uploading a whole log is impossible rather than discouraged, and refresh-image.sh now records success as
well as failure.
3. THE FINDING: minio is unpullable, everywhere. docker manifest inspect fails for
minio/minio:latest, Docker Hub's repository API returns 404, and quay will not serve it from here either
— MinIO has withdrawn the community image. The local copy is the only one. Because the compose pins it by
digest, the data is on bind mounts and the container is unless-stopped, this is not an availability
problem today — it is a recoverability one: lose that local image (a prune, a host rebuild, or a restore
onto the cold standby, whose compose contains no minio service) and it can never be re-obtained, with no
further upstream security fixes at all. The workload is the dental tenancy's radiograph storage
(dental-radiographs). Written up in the patching log with three options and a recommendation — preserve
the artifact now (WO-9 on approval), plan the migration — and the decision sits with Matthew.
4. Judgement call I got wrong, corrected by their choice. I asked for the refresh proof on minio; they
used netdata for the smallest blast radius. Had I got my way the proof run would have failed on an
unpullable image and told us nothing about the mechanism. Worth remembering: when asking for a proof, ask for
it somewhere expendable.
5. One vocabulary question passed back, not a defect. The report-only sweep writes outcome=success for
a check that found something stale (19 rows on vps1 today), which reads backwards to an auditor and will
accumulate every quarter. Suggested trigger=reported with stale/up_to_date, or a separate
kind=image_audit; theirs to choose, and I will document the convention once they do.
Carried forward:
dental-radiographs,dental-radiographs-test, plus an orphan patient-images that nothing references; one access module,dental/services/minio_client.py; no GP/pharmacy/portal/LiveKit dependency), so the swap is contained.--get dump baked into itswiki-repair-baked-wrapper.py); the detection tool (wiki-diagnose-wrappers.py) catches bothwiki-strip-trailers.py does not. A whole-wiki sweep is owed —--list cap, so it is real work, not a re-run.performed through the fixed script); the 2026-10-18 rotation window; then myNo phase advanced. WO-6 was executed within the hour and this round closed the Standard 9 thread
entirely: every gap is closed, every run records itself, and the remaining items are deliberate, documented
limitations rather than unknowns.
1. The run, recorded. Auxiliary host, one-off bootstrap started 14:32:50 UTC, 53 packages applied
(~179 MB, including docker-ce / containerd.io and grub), upgradable 55 → 2, reboot scheduled with
shutdown -r +1, host back at 14:36:49 UTC. The two left behind are python3-software-properties /
software-properties-common 0.120.1, held by apt phasing at 0% — 53 + 2 = 55, so nothing was skipped, and
the log says so explicitly so a later reader does not read the 2 as a failure.
2. Verified, not just reported. I checked the record directly — id 2 | vps3 | os_patch | success | applied=53 | pending=2 | reboot=performed — and corroborated the restart check from the primary: the
tunnel to that host active and both test-client ERPNext hostnames returning HTTP 200. The app-engine
side's own check (5/5 containers healthy, reboot-required cleared) agrees. Two ends of one tunnel, so their
tun-vps1-erpnext and my tun-aux-vps3 are the same link, not a discrepancy — worth noting because it looked
like one for a moment.
3. A defect fixed in passing, and the right way to fix it. The patch scripts reported the reboot field
only as required|none, captured before the reboot fired, so a run that did reboot read as outstanding and
the view could never show performed (veripath-ops 7f92d1d). The already-written vps3 row was corrected
by hand — and the correction is disclosed in the row's own notes field, which is precisely what an auditor
wants to see when a figure changes after the fact. A silently edited record would have been worse than the
defect.
4. Standard 9, final state. Gaps 1–4 all closed (app-engine run; image-refresh cadence; auxiliary run;
the auxiliary host's own assessment). Record is automatic for four kinds of work; the rotation's finalize
will report the first credential_rotation row on Sunday. What remains is documented limitation, not
outstanding work: client-tier migrate_client has no cadence, weekly image scanning has no remote-host step,
and the client-VPS estate is out of scope — each recorded so it is found deliberately rather than discovered.
One cosmetic observation passed back (not a defect): finished_at is empty on both rows, so the view cannot
show a duration.
Carried forward:
credential_rotation row landed.minio (the 13-month-old image) before January.history.log with old → newrefresh-image.sh deployed == committed, and patch-host.shagents/handoff/from-hermes is now 108 KB, past the ~100 KBagents/handoff/archive/2026-09/from-hermes, keeping the four live work-order notes on the page.No phase advanced. OpenCode delivered WO-4 and WO-5 within hours of approval; this round verified the
delivery, moved the record, and closed the Standard 9 thread.
1. Verified before documenting (read-only). The first MaintenanceRecord row exactly as reported
(vps2 | os_patch | success | applied=0 | pending=44 | reboot=required | source=patch-host.sh); the four
seeded cadence rows (vps1 02:00 / vps2 03:00 / vps3 04:30 / image refresh next 2027-01-02); the per-host
token set on all three hosts; /etc/veripath/maintenance.env present at 0600; the route live
(403 unauthenticated vs 404 for a bogus path — the cheap way to prove a route exists without a login);
and the four veripath-ops commits. Not verified: the rendered layout, which needs authentication — and the
spec page now says so explicitly rather than implying otherwise.
2. The record moved, exactly as designed. tasks/dspt-patching-log is now the cadence declaration +
pre-2026-10-13 history + policy summary, pointing at /infrastructure/maintenance/ as the live record, with
a table of which reporter feeds which kind. The assessment's evidence map leads with the view and its CSV
export; policies/standard_9b states the staggered cadence; the spec page carries an as-built section.
Standard 9 is now one story told in four places with the app as the source.
3. Gaps closed. Assessment gap 1 (app-engine run), gap 2 (image cadence) and gap 4 (auxiliary run) are
all closed with what closed them; the auxiliary close carries the caveat that actually mattered —
restart-safety was checked before the reboot step was enabled (all five ERPNext containers
restart=unless-stopped), and that host has no stack guard, unlike the app engine.
4. One known limitation added, one to watch. scan_trivy.sh / scan_trivy_weekly.sh have no
remote-host step, so the app-engine and auxiliary images are in the quarterly refresh scope but outside the
weekly scan scope — recorded in the patching log as an observation rather than promoted to a work order.
And the rotation's finalize.sh now reports a credential_rotation row, which means the 2026-10-18 window's
DSPT evidence is generated rather than remembered.
Carried forward:
vps3 | os_patch.credential_rotation row landed.migrate_client has no cadence (documented limitation); the dedicatedNo phase advanced. Matthew approved both open items, so this round is two raised work orders and the
documentation that keeps them honest.
1. WO-5 — in-app maintenance view (approved). His idea, written up as a spec and now settled:
MaintenanceRecord in the existing infrastructure app, an ingest endpoint on the already-working
backup-heartbeat pattern, reporters in patch_monthly.sh / patch-host.sh / refresh-image.sh / the
rotation's finalize, a /infrastructure/maintenance/ view with filters, a status strip and CSV export,
plus staff-only manual entry for ad-hoc work. Three decisions taken with him and recorded on the page: the
app becomes the live record (the wiki page drops to policy summary + pre-2026-10-13 history, so the two
cannot drift), manual entry is allowed, and v1 covers all six kinds so the 2026-10-18 rotation is a
credential_rotation row from day one. Design points I insisted on: failed runs post too (a log of
successes is not evidence), idempotency on (host, kind, started_at), no patient data or credentials ever
stored — evidence_path points at a log rather than inlining it.
2. WO-4 — auxiliary-host monthly patch run (approved). The same patch-host.sh (already
parameterised), its own timer on that host, staggered again since the app engine owns 03:00. I attached the
one caveat that matters more than the schedule: confirm the ERPNext containers actually come back before
the reboot step is enabled — the app engine had restart: unless-stopped plus the stack guard; this host's
policy is unchecked, and a host that does not come back is worse than an unpatched kernel.
3. An accuracy guard, repeated deliberately. Both new artefacts (the spec and the build brief) tell the
app-engine side not to put DSPT assertion numbers in the UI or the CSV export until they are verified
against the current assertion list. The retired research page quoted 5.1.1 / 8.3.5 / 10.2.1 for this
requirement; plausible, unverified, and a wrong reference in an evidence pack costs more than none.
4. Housekeeping on the research pages, completed. maintenance_records was the fourth page of that
class and is now bannered and archived with its falsified specifics spelled out (non-existent query columns,
three non-existent scripts, and a "13/10/2026 executed" window that never ran). All four are retired; none
was deleted, so the record of what was researched survives.
Carried forward:
credential_rotation row.tasks/dspt-patching-log to summary + history, point it at the1. maintenance_records (wiki root) verified false and retired. The fourth page of the same class
(after rotating_password, database_patching, and the SIAAS improvements page in Round 43). Checked
against the live system: its CSV query selects timestamp, status and details from
infrastructure_clientbackuplog — the real columns are ran_at, success, error_message,
manifest_path, backup_size_bytes, file_count, gpg_encrypted, tenant_id, so the query fails; and
its "change register" records a client-VPS patching window "executed 13/10/2026" driven by
ai-schema-auditor.py, patch_siaas_estate.sh and client_patch_template.sql — none of which exists on
any host (find across /opt, /root/work, /usr/local). Bannered with those specifics and archived to
archive/maintenance_records.
2. The pattern is now worth naming. These research pages describe a plausible but absent
architecture, then dress it as executed work. The response that works: verify each specific claim against
the live system, banner with the exact falsified specifics, archive, and salvage the genuine requirement
into a real page. Three of the four were wrong in the same way — invented scripts, invented columns,
invented "executed" dates.
3. His idea is real and is now a proposal:
Maintenance Records — In-App View
(new page, PROPOSAL status, nothing built). It proposes a MaintenanceRecord model in the existing
infrastructure app, an ingest endpoint on the already-working heartbeat pattern
(POST /infrastructure/api/backup-heartbeat/ is written by the client backup script today), host-side
reporting from patch_monthly.sh / patch-host.sh / refresh-image.sh and the rotation's finalize,
a /infrastructure/maintenance/ view with filters, a status strip and CSV export for the evidence
folder, and staff-only manual entry for ad-hoc work. Deliberate design points: failed runs must post too
(a log of successes only is not evidence); idempotency on (host, kind, started_at); no patient data and no
credentials — evidence_path points at a log, never inlines it. If approved, the app becomes the live
record and the wiki patching log becomes the policy-facing summary plus the pre-2026-10-13 history, so
the two cannot drift.
4. An accuracy guard added. The proposal explicitly says not to quote DSPT assertion numbers for
this requirement until they are checked against the current assertion list — the retired page cited 5.1.1,
8.3.5 and 10.2.1, which is plausible but unverified, and a wrong assertion reference in an evidence pack
costs more than none.
Carried forward:
No phase advanced. The session closed out the patching thread: OpenCode had implemented the three work
orders, and my job was to verify the deployment, reconcile the documentation, and record the one gap their
own data exposed.
1. Verified before documenting. veripath-ops 779b808 + 3634e6a on master carry
scripts/patch-host.sh, scripts/refresh-image.sh and systemd/patch-monthly.{service,timer}; on the app
engine patch-monthly.timer is enabled (OnCalendar=*-*-01 03:00:00, Persistent=true, next 2026-11-01)
and patch-bootstrap.timer fires 2026-10-14 03:00; /var/log/patching/summary.log exists with the dry-run
line. Read-only checks only — the timers and units are theirs to own, but the wiki now asserts nothing I
have not seen.
2. Gaps closed in the assessment. Gap 1 (no app-engine monthly run) and gap 2 (no image-refresh
cadence) are struck through with what closed them. The host table gained the auxiliary host's real figures.
The policy page (policies/standard_9b, rewritten earlier in the day) now states the agreed cadences
rather than "no cadence set", so the policy, the assessment and the log all describe the same estate —
which is the whole point, and was not true this morning.
3. One new gap, from the auxiliary host's own data. WO-3 was a read-only assessment and it came back
with 55 packages upgradable, a reboot outstanding, no root crontab and no /var/log/patching — the same
shape the app engine had. Recorded as assessment gap 4 with patch-host.sh (already parameterised)
recommended as the fix; awaiting Matthew's decision, and briefed to OpenCode as a possible WO-4.
4. Two details worth keeping. (a) OpenCode's shutdown -r +1 fix is recorded verbatim: a nohup-based
reboot dies with the systemd oneshot cgroup, so the reboot would have silently never happened — my own
runbook's rollback advice needed no change, but the note is the kind of trap the next person would re-learn.
(b) The stack guard gets opposite treatment for the two events four days apart: it stays running for
the 2026-10-14 patch reboot (it is the recovery path) and is paused for the 2026-10-18 rotation window (so
a credential failure cannot be masked). Both statements are now in the runbook, since a future operator
reading only one of them would get it wrong.
Carried forward:
migrate_client has no cadence — documented limitation, not a build item, unlessNo phase advanced. The session continued the DSPT-preparation task: OpenCode had been through my
2026-10-13 note, prepared the window's execution pack, answered my three questions and left one
correction to my work. Everything they reported was verified read-only before it went into the wiki.
1. My window plan was wrong, and correcting it mattered. I had named /opt/restart-services.sh (a
per-minute cron) as the thing that could mask a wrong-credential failure. OpenCode said it only restarts
two website Node services. Verified: the script nohups only ports 3052/3053 and never touches DB-backed
services — my claim was simply wrong. The real self-healer is vps2-stack-guard.timer on the app engine
(OnUnitActiveSec=2min, confirmed on the host), which re-runs docker compose up -d --no-build, plus its
container-watchdog cron. Both are now quiesce items and the runbook row says the earlier claim was wrong.
Had this stood, the window would have run with the masking risk unpaused and a harmless job paused.
2. Counts reconciled — two different metrics, both now published. My "49 files" was a file count;
OpenCode's figures are occurrence/line counts. The runbook now carries both: primary 33 files / 45
occurrences, app engine 13 / 23, auxiliary 8 + 1. My own scan also had a root gap — it never
walked /usr/local, which is exactly why OpenCode found two scripts on the primary
(preflight-backup.sh, post-restart-verify.sh, both carrying PGPASSWORD=<literal>) that my pass had
missed. Both scanners now include /usr/local, /srv and /home. The 36→33 fall is accounted for:
−6 profile skill scripts fixed earlier in the day, +2 for those scripts, +1 for the execution pack's own
old-value — which is now an explicit purge step, since the window is not clean while a rollback copy of
the retired credential sits on disk.
3. Verified independently before publishing. The two /usr/local/bin scripts carry the literal; the
central fh_app_user owns 0 objects, is not a superuser and has no memberships (safe to alter, and not
a literal consumer — the client-tier fh_app_user rejects the literal over TCP and stays out of the
rotation); the app engine holds 13 carrier files; tenancy_tenant reads gp/dental at
localhost:5432 with the client tenants on per-tenant ports. All folded into the runbook's verified facts,
inventory tables and step 3.
4. Answers folded into the patching half. Auxiliary host is redis-only in scope (live requirepass
on its local redis + the redis URLs in common_site_config.json; its Postgres is clean). The app-engine
patching gap is a confirmed oversight, not a decision, with the action "add a monthly patch/reboot run"
now on both Standard 9 pages. Client-tier migrate_client has no scheduled cadence — it runs only from
the pharmacy entrypoint at container start — and nothing refreshes pinned images; both are now recorded
gaps, and the log carries a "Cadence questions — answered 2026-10-13" table.
5. Runbook structure. Added an Execution pack section (what /root/rotation-20261018/ holds and that
nothing has run), restated the evidence checklist against the new counts, and put quiesce/restore into step 1
and step 8 (purges). An ack note went back to OpenCode; their note stays OPEN because the execution is
still ahead.
Carried forward:
policies/standard_9b — the page both Standard 9watchtower as the container update mechanism ("runs as adocker ps → Watchtower container active as its evidence. Watchtower was retired deliberately, so the control as written did notHe answered "let's proceed — you lead / your decision" to the three open questions, so the calls were
made and the work done in this order: close the secret-footprint items, hand the rotation to OpenCode
with the decided shape, then the patching documentation.
Decisions taken (mine, as instructed):
archive/rotating_password, archive/database_patching. Their superseded banners stay on the pageswiki-insert-asset.py / wiki_insert_asset.py under/root/.config/veripath-db.conf (section wikijs) at run time,/opt/wiki-manage.py does; the hardcoded literal and a stale docker-bridge host address areSKILL.md line in all three profiles that said "load from /opt/wiki-manage.py (it hardcodes the.bakRotation thread. Handoff note revised in place (wiki-replace-note.py) rather than duplicated:
the decided shape, the five-step execution sequence, the confirmation that no git-history rewrite is
needed, and the two read-only asks (auxiliary-host inventory; whether a client-tier patching runner
already exists). One adjacent item was added to the note: the apps authenticate as the postgres
superuser, which is a least-privilege improvement (DSPT 4.1.1/8.1.4) worth taking in the same window.
Patching thread (done, from live evidence rather than assumption). The May 2026 assessment was not
merely stale, it was wrong in its conclusions, so both pages were rebuilt on verified facts:
tasks/dspt-patching-assessment re-verified 2026-10-13: the monthly patch_monthly.sh --apply --reboot job is real and has run five consecutive months (Jun→Oct: 3 / 17 / 15 / 101 / 43 packagesunattended-upgrades on the primary and app engine, weeklytasks/dspt-patching-log created — the evidence page the May assessment proposed and nobody built.unattended-upgrades alone, with a reboot outstanding sinceminio is over 12 months old, wikijs /Tooling added: wiki-replace-text.py — replaces one exact unique block inside a page, trailer-safe
and refusing on ambiguity (used for the resume revision; wiki-replace-note.py only handles ### -headed
notes). Reusable for any in-place page correction.
Carried forward (new):
/opt/restart-services.sh, which would otherwise silently restart a service failing on a wrongNo tidy-up phase advanced, and Phase 4 was not started. The session was a named task of Matthew's:
DSPT accreditation preparation. He had created two pages from AI research —
/en/rotating_password and /en/database_patching (raw chat dumps at the wiki root, no description) —
and asked for a look, then for a plan.
1. Orientation found that both topics were already covered. infrastructure/central-db-password-rotation
(a prepared, authorised but never-executed runbook whose 2026-09-20 window had lapsed) and
tasks/dspt-patching-assessment + policies/standard_9b. So the raw pages were duplication, not new ground
— and each carried at least one instruction that would fail or mislead. Both now carry a dated SUPERSEDED
banner naming the specific defect and pointing at the verified page; their bodies are untouched, so
Matthew can still delete or keep them at his discretion.
2. The rotation runbook was re-verified and rewritten (5069 → 8775 chars). The re-verification, all
read-only and with no value ever printed:
pg_authid.rolpassword), and there is no connection pooler anywhere inALTER ROLE) andPOSTGRES_PASSWORD, WIKI_DB_PASSWORD, REDIS_PASSWORD, tenant db_password) and isapp-engine ssh alias), including.bak-* copies, a pre-git tree, a July backup tree, two internal documents that quote it, agentconfig/secrets/ is gitignored and git log --all -S findspostgres superuser — a least3. The patching page's genuine gap, separated from its noise. Its SQL cannot run against this schema
(the infrastructure_clientbackuplog insert names columns that do not exist), it puts
CREATE INDEX CONCURRENTLY inside a transaction, calls 5438 — the client's MinIO reverse tunnel — a
database port, and embeds a credential. The real mechanism is the app's own
manage.py migrate_client --slug … --app-label …. What is genuinely missing is a documented cadence and
evidence log for client-tier patching (and image refresh, with watchtower retired) — carried forward below.
4. Tooling added (both read-only, both reusable, in /root/work/):
dspt-cred-blast-radius.py (VPS1 credential-spread scanner: confirms hits and prints redacted context) and
dspt-cred-scan-remote.py (the same inventory piped over ssh to another host; prints key names, paths and
a 12-char hash, never a value — safe to re-run). Use these for any future "where is this secret written"
question rather than an ad-hoc grep, which produces false positives on short values.
5. Handoff. A note went to OpenCode in from-hermes (Status: OPEN, two
read-only asks): confirm the VPS3 side of the inventory (no shell path from here), and tell me whether a
client-tier patching runner/cron already exists before I document one. The windowed rotation execution is
theirs once Matthew decides the shape.
Carried forward (new, from this round):
/en/rotating_password and /en/database_patchingtasks/dspt-patching-assessment (dated 14 May 2026 — still/tasks/dspt-patching-log evidence page; then the DSPT evidence records for both threads.Carried forward, unchanged: the Round-43 marketing/API-key outreach items, the nine marketing drafts
whose stored content still holds the wiki tool's output trailer, and the resume-archive split
(hermes-resume-archive past 300 KB) noted in Round 43.
No tidy-up phase advanced. The session was the Gemini research exchange on SIAAS infrastructure improvements
(development/infrastructure_improvements), which Matthew flagged for its tunnel-and-middleware section.
1. The page now carries a dated assessment block at the top. It is a verbatim Gemini exchange with no
description; the block gives a verdict per proposal (OpenZiti — reject for now, revisit above ~15–20 client boxes;
Traefik/Envoy as a database router — reject, wrong protocol layer; WireGuard over TCP — defer with a trigger),
states what is actually deployed (two reverse tunnels carrying four forwards, six tenant-gateway@ socats, seven
tenant rows, the router loading one alias per tenant row), and names the one genuine performance item the exchange
misses: the client aliases run with CONN_MAX_AGE = 0. Description set.
2. A live finding, verified read-only. The six tenant-gateway@ socat units on the primary publish the client
database ports (5435 / 5436 / 5438 / 5439) on a Docker bridge gateway address that now belongs to the sandbox
bridge; a probe from a throwaway container on the hub network connected on all four. The units look dead on the
database path — the tunnel sshd connects to the reverse-tunnel listener on loopback, the socat forward target — and
they are not in veripath-ops. Briefed to OpenCode in agents/handoff/from-hermes (Status: OPEN —
authorised by Matthew the same day, not yet applied), together with the documentation follow-ups (two ops-repo unit
comments, the 2026-08-01 incident page's chain diagram, committing the template if any instance is kept).
3. Tooling. /root/work/wiki-prepend.py added: captures a page with --get, cuts the output at the trailing
ruler (the Round-41 defect), inserts a block after the page's first H1 and upserts. Companion /root/work/wiki-replace-note.py replaces one handoff note in place,
so an inbox note can be revised (e.g. from awaiting a decision to authorised) without a second copy. Both pages written this session
were checked afterwards for duplicated trailers and are clean. Use it for wiki→wiki prepends rather than
hand-capturing content.
Outcome, same day — applied and independently verified. OpenCode re-verified the finding, asked four queries
(all answered: skip the firewall rule; repo/host artefacts its side, wiki mine; @8000 is a live path so it stays;
the corrected tunnel port map), and then retired tenant-gateway@5435/@5436/@5438/@5439 on the primary. My own
re-probe from a hub-network container afterwards: 5435/5436/5438/5439 connection-refused, 8000/8043 still open
by design; the four loopback listeners untouched; the three client tenants' tunnel-health rows written healthy at
10:50 and all four tenant hostnames still answering (302 to the identity provider). veripath-ops d10c139 on
master carries the retired-template commit, the corrected chain comments in tun-vps1-dental-minio.service and
pharmacy-tunnel.service, and the AGENTS.md topology corrections (including the stale 5437 forward). Dated notes
added to the 2026-08-01 tunnel incident page (its chain diagram) and to
infrastructure/siaas/hermes-infrastructure-preparation (its Phase 3 line); the GP Booking App overview (development/projects/gp_booking_app) was refreshed the same day — a dated changelog section, the corrected connectivity/port map, and two new Known Issues rows (the retired gateways, and CONN_MAX_AGE = 0 on the client database aliases). @8043 closed the same day. Fingerprinting the service through the tunnel (the Explorer UI and /system
answer) identified it as the client box's Orthanc — the dental pipeline's client-side PACS, documented as such
on infrastructure/SIAAS/medical_imaging. No container, vhost, script or app repo referenced it, so the
tenant-gateway@8043 socat was stopped and disabled too: the port is now refused from a hub-network container and
reachable on the primary's loopback only, while the client's Orthanc still answers through the tunnel (307 to its
Explorer UI). Five units retired in total; only @8000 — the patient portal's /api/ path — remains, and all five
retired units were reset-failed so monitoring sees no failed unit. Left deliberately: the dead 5437 forward on
the app engine (removing it means restarting the unit that carries the tenant database forwards, for a cosmetic
line; AGENTS.md documents it as stale).
Housekeeping pass, same session (Round 43 continued). Three open items from Rounds 41–42 were closed, and the
resume note was trimmed:
marketing/web-content,agents/handoff and agents/hermes (/root/work/wiki-strip-trailers.py --scan, matching the tool's ruler +Page: https://… line inside stored content) found one page still carrying it —agents/hermes/demo_scenes/viewer-qa, which had a leading ruler and a trailing block — now strippedwiki-prepend.py / wiki-replace-note.py for wiki→wiki edits, which cut the capturemarketing/web-content/health_clinics noagents/handoff/hermes-resume-archive (243 KB → 301 KB) under a new range heading, per the Round-32 convention:agents/handoff/from-hermes holds 21 notes / 78 KB (oldest 2026-09-26, four of them from today and stillfrom-opencode 10 notes / 21 KB. The last archive pass was 2026-10-06 (Round 32),agents/handoff/archive/2026-10/* — is due at roughly 24+ notes or 100 KB on either page, not before. BothCarried forward, unchanged: nine marketing drafts still hold the wiki tool's output trailer in their stored
content (Round 41, item 4); the Round-42 item on drafts that still say nothing here is live.
No tidy-up phase advanced. This session was the Health Clinics web set, the publishing of it, and the website
work around it (see the resume note). Housekeeping it did touch:
1. Set index (marketing/web-content/health_clinics) brought current twice. The Insurance row moved from scope
to confirm to drafted 2026-10-08 with its evidence, and the running-order paragraph with it; the VAT row then
became Parked 2026-10-08 (Matthew) — nothing has been built, with the reason (no VAT return work anywhere, no
guide in /guides/; the clinic's site carries only stock ERPNext reports and one tax template).
2. A wiki→website flow now exists, and it is not yet in a repo. /tmp/mirror_health_clinics.py reads each
draft with --get, cuts the drafting note, rewrites wiki links and /health_clinic/ image paths to website
ones, adds CMS front matter and downloads the figures into public/images/uploads/health-clinic/. Two lessons
worth keeping: (a) a mirrored page must also have its body H1 stripped (the site renders the title from front
matter, so a second heading is a duplicate) and an image must not be glued to the heading that follows it in
the wiki source; (b) front-matter descriptions must be quoted — an unquoted : broke the whole site build
with a YAML BLOCK_AS_IMPLICIT_KEY error. It must be moved into the site repo before it is used again, with
the drafts' new LIVE banner stripped too, or a later run publishes the banner.
3. The six drafts now carry a LIVE on the website banner pointing at their published URL, in the same shape
as the dental pages' banner — so the published page is the canonical copy from here on.
4. Still carrying the Round-41 defect: nine marketing drafts keep the wiki tool's output trailer baked into
their content (the pharmacy and siaas pages named in Round 41). Strip a page's trailer when next editing it.
5. Open item carried forward: the drafts of this set say nothing here is live in places where that is now
false (the set index's own header, and any page still describing the set as unpublished). Worth a pass next time
the index is touched.
Running log for the staged Wiki.js tidy-up (started 2026-08-11). Workflow per
phase: audit → propose → confirm → apply → verify (see the
veripath-wiki-housekeepingskill). Archive-not-delete: everything moved to
archive/stays searchable and recoverable. Hermes reads this page at the
start of every housekeeping session to pick up where it left off.
No tidy-up phase advanced. The session was the Health Clinics web-content set (see the resume note); this round
records the one piece of wiki maintenance it did, and the defect behind it.
1. The output-trailer junk — cause found, not just cleaned. Twelve marketing drafts and both handoff inboxes
carried the wiki tool's own output trailer inside their stored content, rendering as ==== rulers and repeated
Page: https://… lines at the foot of the page. The cause is a helper that captures a page with --get and feeds the
captured text straight back into an upsert. Session 65's insert_records.py captures with
out.split("Content (", 1)[1].split(RULE, 1)[1], which keeps the closing ruler and the Page: line — so every
edit made by that helper appended one more block: the set's index carried five, from-hermes twelve. Anything built
the same way will keep growing the junk, so the rule for this tree is: cut captured content at the first trailing
ruler; never feed a whole --get output into an upsert.
2. Repaired where the session touched the page: the Health Clinics index (5 blocks), from-hermes (12), the
Medical notes draft and the Accounting app draft (1 each). The repair is partial by design — the scan
found nine marketing drafts still carrying it (pharmacy_app/compliance_admin/{accounting,counter_checkout,rp_log},
pharmacy_app/prescriptions/dispensing, pharmacy_app/stock and stock/{data_feeds,ordering,supplier_portals},
siaas/siaas_explained), plus one block each on the dental drafts. Strip a page's trailer when next editing it.
3. State left behind. No phase work mid-flight, nothing archived or deleted. The set's worklist page carries the
running order and the accounting blocker (see the resume note); from-hermes carries the four app defects and that
blocker. Next session in this tree: nothing is owed to the housekeeping queue by this round.
No tidy-up phase advanced. Matthew cleared both questions Round 39 left open — "you have my clearance" — and
asked for the session to be closed, so this round is the two clearances carried out and the state left clean.
1. The SNOMED finding is now a spec with numbers, not an impression. Round 39 recorded it from one probe; this
round measured it and committed the means to re-measure: scripts/measure_snomed_search.py (commit
3260d65, pushed) is read-only, takes a tenant name, and prints both what the endpoint returns today and what a
word-boundary, exact-first, de-duplicated query would return. The measured shape on the test tenant: 1,291,958
active descriptions, of which the endpoint's type_id = FSN filter reaches 504,042 and leaves 787,916
synonyms (61%) unreachable; tummy ache — a real active term — returns nothing at all today; heart attack
returns three rows that are all fear of one, though the exact synonym exists; raw ILIKE '%rash%' matches trash
(row 10 of the current top ten is a trash fire accident); alphabetical order puts "Nausea and vomiting caused by
drug" above "Nausea and vomiting (disorder)" and "Rash absent"/"Rashkind umbrella device" above anything useful;
and 21,213 FSN terms sit under more than one concept, so de-duplicating on the concept id is not enough —
several distinct concepts share the display term ("Rash" five times, "Sore throat" six). One thing ranking cannot
fix was found too: the corpus's "Heart attack" synonyms point at legacy parents (155304006 → "Myocardial
infarction (& [acute]) or coronary thrombosis (disorder)"), while the clean 22298006 has no such synonym — the
store looks like a derived subset, not a full UK release, which is a provenance question for OpenCode. The note
carries six acceptance criteria and warns that the published recording-symptoms-search.png shows today's
ordering, so a change means I re-shoot it rather than leave stale copy. Filed as the top note in from-hermes.
2. The stray insurance claim row is gone — Matthew's clearance, one row, explicitly named. The row his own click
created for #121 (claim #1, created 17:53:54) was deleted, and only it — the tool refuses to delete without
an explicit pk and re-checks the row belongs to the appointment named on the command line. Verified after deletion:
0 claim rows on #121, the record back to "No insurance invoice yet" with the working "Create one" link, and the
whole panel verification re-run at 0 failed (200 where it used to be 500, prefills intact, nothing written). So
he can walk the create path from scratch, which was the point of the clearance.
3. State left behind. feature/wda-register-import at 3260d65, pushed to Forgejo; this session's commits
are 9fdc56d, aca8ac5, f162b62 and 3260d65 (plus 81a4cd1 from Round 37). The apps host still carries
hand-copied files and its checkout has no git remote — the deploy-path question in from-hermes is now three
sessions old and is still the one thing most worth an answer. hermes verify --json green, 35/35. No phase
work is mid-flight and nothing is half-published.
Read first next time: this round, then the resume note's session-65 block, then from-opencode and
from-hermes (whose top note is the SNOMED spec). Next: Matthew's read of the Recording Symptoms figures and the
SNOMED spec, then E-Prescriptions (five clinician guides behind it), Medical notes, and the three
Accounting pages whose scope is still to confirm.
No tidy-up phase advanced. The round's value is a blocking fault on the appointment record found in his own testing, traced to one line, fixed and verified live, plus the figures the symptoms page needed.
1. How it surfaced. Matthew created an appointment himself — #121, Colin Whitaker — to try the coded reason, and the code appeared, which also settles the earlier "blank" figure: the figure was faithful and the impression came from how the bar reads. In doing so he found that /appointments/121/ said "No insurance invoice yet" although he had chosen an insurer at booking, and that the "Create one" link returned a 500.
2. The cause, one line. The panel's form action and its "Create one" link reversed
insurance_billing:insurance_panel with appointment_pk=view.kwargs.pk. That is correct where the panel is
included on the appointment page (whose URL kwarg is pk), but the panel is also served by its own view,
whose kwarg is appointment_pk — so the reverse failed with NoReverseMatch and Django answered 500. It was
reproduced before anything was changed, by calling the view for #121 and capturing the traceback. The reason the
insurance details appeared to vanish: InsuranceDetailPanelView.get_object() creates the invoice
(get_or_create) before the template renders, so the click wrote the row and then failed — the record was there
all along and the page could not be reached again to show it. Checked while diagnosing: the invoice row for #121
was created at 17:53:54, before the first diagnostic script existed (18:01) — so his click created it, not a
tool of mine; nothing I ran wrote to that tenant.
3. The fix and its verification. Both views now put the appointment's pk in panel_appointment_pk and the
template uses that name, so it resolves in both contexts (the twin view.kwargs.pk defect cannot recur by naming
either kwarg). Commit aca8ac5, pushed, the three files copied to the apps host, container restarted. Verified
on the deployed code, in a transaction that is rolled back: the panel returns 200 where it returned 500, the
form posts back to the right URL, the insurer, policy, pre-authorisation code and total are prefilled from the
appointment, and no row is written by the check. The appointment page renders 200 and points at the right panel.
Live: /appointments/121/ and the panel 302 to OIDC (no 500 from the edge), health 200. Noted: for an
appointment with no insurer (self-pay, e.g. #103) the same checks pass with only the total prefilled.
4. Three figures now carry the Recording Symptoms page, all shot from the test clinic and all real renders,
not mock-ups: the search with its matches (recording-symptoms-search.png — "nausea" typed, the four matches the
endpoint actually returns), the coded reason on the booking form (recording-symptoms-coded.png, re-shot for the
same symptom so the pair reads as one story), and the coded reason on the appointment record
(recording-symptoms-record.png — the record's own panel, code and term, no patient identifiers in the crop). Each
was checked by OCR of the served file and by byte comparison, and the search/record ones were cropped so their width
matches the others.
5. A finding for OpenCode, from building those figures. The SNOMED search matches only FSN-type descriptions
(type_id = 900000000000003001), so it returns what the terminology calls things rather than what a patient says,
and the store carries duplicate rows — "Chest pain (finding)" twice (139228007 and 29857009), "Sore throat
(finding)" twice, "Back pain (finding)" three times. Because the ordering is prefix-first and then alphabetical,
a receptionist typing rash is offered "Rash absent", "Rash due to dribbling from mouth", "Rashkind umbrella
device" before anything useful. 1,295,958 active descriptions are in the tenant store. Not a copy problem — the
draft claims only that matching terms appear — but worth a note to OpenCode.
6. hermes verify is green again — the recipe now uses a disposable PostgreSQL (commit f162b62). Every app
change this session came with the same caveat: the project's verification recipe built its test database on the
central cluster, where the application's role has no CREATEDB, so it failed before collecting a test — and
granting that privilege would let test runs create and drop databases on the cluster the platform runs on. Matthew
was offered both options twice and did not rule, so the safer half was taken, touching no production database:
scripts/test_with_disposable_db.sh starts a throwaway postgres:15-alpine on a free loopback port, points the
suite at it and removes it on exit, and .hermes/environment.json calls that instead of the central loopback
forward (with the reason recorded in its own evidence list). hermes verify --json now reports ok: true,
35/35 on this host. If he would rather the role had CREATEDB, the recipe is one line to revert — but this way no
test run can touch production.
Read first next time: this round, then the resume note's session-65 block, then from-opencode and
from-hermes. Next: Matthew's read of the three figures, then E-Prescriptions (five clinician guides behind
it), Medical notes, and the three Accounting pages whose scope is still to confirm.
No tidy-up phase advanced (Phase 4 still IN PROGRESS). The round's value is the first two pages of the
third and last website set, drafted to the sales register Matthew asked for and reviewed one page at a time, plus
the asset and app-side work that fell out of illustrating them.
1. The set. Drafting area marketing/web-content/health_clinics, three sections and nine pages agreed
2026-10-07. Patient Management page 1 Appointments and page 2 Recording Symptoms are drafted (2,700 and
2,650 characters) and their rows on the set's landing page carry Drafted 2026-10-07 — awaiting review; the third,
the Personal Web App, stays parked until its rework. Recording Symptoms was retitled from SNOMED & Symptoms
at Matthew's request, and the wiki path was renamed with it (snomed_and_symptoms → recording_symptoms), so the
old path is gone rather than left as a duplicate; the Appointments draft's cross-link and the worklist row were both
pointed at the new name.
2. Matthew's rulings, now settled for this set. These clinics are private, so NHS Notify is not mentioned
at all; no NHS-number lookup — patients are found by name, as the app does (and it could not be otherwise:
those columns are encrypted, so a search on them matches everything); the personal web app is parked and referred
to without figures; SNOMED is not explained — it appears only as "the standard clinical vocabulary used across
the NHS", with the detail left to its own page. The register is the pharmacy/dental sales voice: benefit-led
headings, feature → what it does for the clinic, no task steps.
3. The figure, and how it was made. The page needed one shot of the booking form's SNOMED area with the coded
reason in place. Path taken: render the real /appointments/new/ as the test-client receptionist through
Django; cut the "Reason for the appointment" card verbatim from that output; apply only the changes the page's
own JavaScript makes after a term is picked (search box keeps the typed symptom, dropdown closes, the italic
instruction line hides, the blue panel carries the term); screenshot with headless Chrome; trim to the card. The
symptom shown is real — "Chest pain (finding)" (29857009) — the actual top match for chest pain in the
tenant's 1,295,958-description SNOMED store, not a made-up example. Uploaded to the wiki asset folder
health_clinic as recording-symptoms-coded.png (id 280, embedded in the draft) — re-shot tighter the same session at a 620px card width, because the first cut left a long empty blue tail after the coded line that could be read as an unfilled field (the coded line now fills 89% of the bar, up from 73%). Verified three ways:
tesseract OCR reads back exactly the strings that should be on it (and confirms the italic line is correctly
absent), the served bytes are identical to the uploaded file, and the figure's markup is byte-identical to the
deployed app's markup — so the figure is evidence of the app, not an illustration of it.
4. A fifth app change, found by shooting the figure. The receptionist's booking card showed a "Reason" label
and its help text over a hidden field — the same defect as the clinic label, in the same view, for a role where the
field posts without being seen. Fixed the same way (the block renders only when the field is visible; the value still
posts), commit 9fdc56d, pushed, copied to the apps host and deployed — health 200, /appointments/new/
302 to OIDC. Recorded in the note to OpenCode.
Verification for these commits. (Superseded later the same session — see Round 39: the recipe now uses a disposable PostgreSQL and hermes verify is green. At the time of writing, hermes verify --json could not pass on VPS1 — the app's Postgres role has no
CREATEDB, so the recipe died creating its test database before a single test was collected, which no template
edit could cause and which said nothing about the code.) The evidence instead: scripts/verify_appointment_form_markup.py
(new, committed 5e32c19) asserts every change made to /appointments/new/ — 38 checks, 38 pass, across both
tenants and both roles; the manifest's own two modules run 35/35 against a throwaway Postgres the role may create;
and the live checks stand — health 200, /appointments/new/ 302, and the deployed markup byte-identical
to the published figure's.
Tooling note for the next figure: the Chrome binary path in the veripath-wiki-assets skill is stale — that
skill is pinned, so this belongs here instead: use
/root/.agent-browser/browsers/chrome-152.0.7977.42/chrome with --headless --no-sandbox --disable-gpu --force-device-scale-factor=2 --window-size=W,H --screenshot=…. There is no vision tool on this host, so a
fresh figure is checked with tesseract plus pixel checks (the blue panel bar, the dark text pixels) rather than by
looking at it.
Read first next time: this round, then the resume note's session-65 block, then from-opencode and
from-hermes. Next: E-Prescriptions (five clinician guides behind it; expect a split, or lead with issuing
and signing), then Medical notes, then the three Accounting pages — whose scope is still to confirm — and the
Personal Web App only once its rework lands.
/appointments/new/ booking form reworked at Matthew's request and deployedNo tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from
Round 18; sections C/D, the home/ strays, the DSPT merge read and the password rotation all still
stand). The round's value is one app view brought back up to the house style, verified before and
after deploying, for a view whose screenshots the guides depend on.
1. Four changes, exactly as asked. The Patient field is now a type-ahead lookup; the Clinic
label no longer renders where it says nothing; the Payment and insurance section is tidy; and the
Other clinics in your PCN card is hidden. Nothing else in the view was touched.
2. The "mashed and partially cut-off" payment section was our own defect, introduced in session 64's
restyle. That session gave every widget its Bootstrap class, and for a radio group Django copies the
widget's attrs onto the wrapper <div> as well as each <input>
(django/forms/widgets/multiple_input.html) — so .form-check-input, a 1em-tall bordered box, landed on
the payment-method wrapper and clipped the three radios. Fixed at the source (AppointmentForm skips
RadioSelect in that loop) and the group is now hand-built as .form-check form-check-inline. The
insurance fields moved into a panel shown only for Private Insurance; they were previously always on
under a Self Pay default.
3. A server-side patient search is not possible in this app today — measured, not assumed.
first_name, last_name and nhs_number are EncryptedCharField, so first_name__icontains matches
every row: 9/9 on gp and 12/12 on test-client. The lookup therefore filters the options already
in the page, and the <select> stays in the DOM (hidden) as the posted value, so what can be booked is
unchanged. It is hand-rendered without required: a display:none required control cannot show
the browser's validation bubble and Chrome refuses to submit rather than flag it — the page's own script
and the server both enforce the choice, and a pre-set patient (?patient=, or the patient role) still
renders as plain text.
4. The PCN card and its handlers are hidden together. Commenting out the card alone would throw on
getElementById('search_pcn_clinics').addEventListener and kill the SNOMED wiring that follows in the
same <script> block — so the card, the toggle, the results panel and both handlers are one comment
block, with the dated note that they restore together.
5. Verified, then deployed, then verified again. 200 in both tenants as receptionist and
clinician, and 200 for the pre-set-patient variant; both <script> blocks pass node --check;
the form still validates (patient from the lookup → valid; nothing chosen → still "This field is
required"; insurance rules and the corporate-sponsor path unchanged). Commit 81a4cd1 on
feature/wda-register-import, pushed to Forgejo; both files copied to the apps host (its checkout still
has no git remote, at 634c139) and docker restart gp_booking_app — health 200,
/appointments/new/ 302 to OIDC, and the deployed page's markup is byte-identical to the locally
verified render.
Tooling note for later rounds: /opt/wiki-manage.py refuses a page description longer than 255
characters (varchar(255)) — the upsert fails after printing the "Content: N chars" line, so a write
must be judged by the tool's stderr/exit status, not by the tail of its stdout. Related (fixed in Round 39): hermes verify could not pass on VPS1 — the app's Postgres role has no CREATEDB, so the recipe died creating the test database; the appointments module also fails 8 of 11 identically before and after this commit (test_settings disables the router), so it is no signal either way, while the recipe's own modules pass 35/35 when given a database the role may create.
Read first next time: this round, then the resume note's session-65 block, then
from-opencode (non-blocking refinements) and from-hermes (the appointment-form note now on top).
Next: any further changes Matthew names on the booking form, then the left-overs he put behind it —
his two open calls on the SIAAS page, his review of the live pharmacy pages (the three unreviewed
ones first), group F (premises and registration) and the conversion pass.
siaas_intro page replaced from its wiki draftNo tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round's value
is two website changes carried through to live and verified, plus the wiki-side bookkeeping that follows a
publish.
1. The symptom and its cause. Matthew: "I cannot see any pharmacy section in the navbar so I cannot see any
menu options." Cause confirmed before acting, not assumed: the live homepage still listed only SIAAS / Health
Clinics / Dental Surgeries / Staff and https://veripath.co.uk/pharmacies returned 404, while the local tree
held content/pharmacies/ (nine files), the pharmacies collection block in public/admin/config.yml, 24 uploads
under public/images/uploads/pharmacy/ and the navbar entry in build.mjs. Session 63's commit a2f6416 was
never pushed, so nothing had ever reached the docroot. No fault on either side — an undeployed change.
2. Deploy. git push origin main (8a9c2dd..a2f6416), then /opt/deploy-veripath-site.sh — pull,
npm install, node build.mjs ("Build complete! Target=full"), dist/ to /var/www/veripath, nginx -t
successful, nginx reloaded, site responding, Deployment complete. The working tree was clean beforehand, so
the skill's pre-publish git stash push -u step was unnecessary. Nginx emitted only its pre-existing warnings
(duplicate TLS protocol options on patient.veripath.co.uk and pharmacy.veripath.co.uk, a duplicate TLSv1.2
value on the test-client vhost) — the same notices previous deploys logged.
3. Verified live. /pharmacies/ 200 (bare /pharmacies 301 → trailing slash, the normal
directory-route behaviour); the navbar carries the item and links all eight pages; the landing renders the four
headings in Matthew's order — Prescriptions / Stock / Accounting & Counter / Compliance/Admin; all eight pages
200; 24/24 images 200.
4. Three pages published unreviewed — by his choice. data_feeds, accounting and counter_checkout were
flagged before the deploy; he chose to put the section up whole and review it live. Recorded because it changes the
routine: any correction he names is now an edit to the live CMS file (content/pharmacies/<slug>.md), not a
re-mirror — and per the marketing skill's site is canonical rule the draft must be back-ported from the live page,
never used to overwrite it.
5. The eight pharmacy drafts are marked LIVE. Each opens with the standard marker — "LIVE on the website —
published 2026-10-07. …" plus its https://veripath.co.uk/pharmacies/<slug>/ URL — the wording the published
dental drafts carry. Applied by /tmp/mark_live_pharmacy.py, which reads each draft with --get, refuses one
already marked, and re-upserts preserving title and description. The notifications draft stays unmarked and
out (parked: no live NHS Notify). This marker set is the worklist for the conversion pass. A second pass
later the same session marked the SIAAS draft with the same marker (/tmp/mark_live_one.py).
6. The SIAAS siaas_intro page replaced from the wiki draft. Matthew: "we need to replace the content therein
with the content we have in" marketing/web-content/siaas/siaas_explained. The draft's five island
illustrations (wiki /siaas/pexels-*.jpg) were fetched, resized to 1600 px / q82 and added to
public/images/uploads/siaas/; the embeds rewired to /images/uploads/siaas/; the body H1 dropped (the builder
renders its own from title); the file kept its name and its slug: siaas_intro, so the live URL is unchanged.
Typos corrected on the way in ("Sovererign … as as service", "storing there assets", "build a bridge in which
you can use", "draw bridge", "Saas" → SaaS) and the description rewritten — the live one read "What is
'SiaaS and how does it work?", an unmatched quote. Body 2,767 → 4,758 characters. Committed f2d5e04, pushed,
deployed; verified live 200 with the H1 and five H2s and 5/5 images 200. Two items left undecided and
therefore NOT published: the live page's closing "What happens if the agreement terminated?" was not in the
draft, so a faithful replace takes it off the page (no other SIAAS page covers that ground), and the cost section
still carries the placeholder "Our pricing is given on [....]" the live page already had. Alt text was written
from the draft's own captions — no vision tool on this host — and he was told to correct any that misdescribe the
photo. Housekeeping note for later: the file name content/siaas/siass-intro.md transposes the letters
(hence the misspelling in the live URL's page title in earlier rounds); routing is unaffected because the slug is
explicit, so it is a Phase-8-style casing/underscore item, not a fix now.
7. The health-clinics test tenant was seeded with synthetic demo data — and the first pass's real addresses corrected. Matthew reported the test model looked empty. Confirmed before acting: tenant test-client (sector client, tenant DB dsp_clinic_dev on the client box) held 5 staff users and zero patients, appointments, notes, prescriptions or lab documents. Wrote scripts/seed_health_clinic_demo.py in the gp_booking_app repo — synthetic only, test-client only, idempotent, dry-run by default. Two lessons the script now carries: a rollback does not check deferred foreign keys, so the dry run forces them with SET CONSTRAINTS ALL IMMEDIATE before rolling back (that is how a PatientProfile.partner_org pointing at the tenant's empty partner table was caught), and the prescription token_id must be unique per row. Committed on the apps host, the script copied in with docker cp. He then caught a real fault in the first pass: the seeded addresses were plausible real ones — Leeds streets with genuine postcodes — so all twelve were replaced with placeholder streets in a fictional town (Sampletown) and ZZ99 postcodes (the NHS convention for an address that cannot be resolved), the pharmacy names were fictionalised with them, and the seeder now repairs drifted contact details on every run, so re-running is also the fix. Checked while in there: the pre-existing staff rows carry no address data at all. Verified by counts and through the app's own tenant-scoped queryset: 12 patients (with 12 health and 12 registration profiles), 24 appointments (11 scheduled; one no-show, one cancelled, two insurance cases with claim statuses, one child under 12), 10 clinical notes, 10 prescriptions (dispensed and issued across pharmacies, a Schedule 2 CD with words and figures, a cancelled Schedule 4), 2 lab orders (one filed with a TDL-style report and 5 results, one outstanding), 5 invoices with 7 line items, 3 appointment types and 1 insurance provider. A second run adds nothing. The synthetic-data conventions are recorded on the set's landing page so every page we draft matches them.
8. One UI change for the screenshots — and a deployment gap found while applying it. At Matthew's request the NHS Notify button on the receptionist patient view is hidden, not removed: the anchor in templates/dashboards/receptionist_patient_detail.html now sits inside a {% comment %} block with a dated note, so deleting the two tags restores it. Committed 694b35f; verified by rendering the page through Django's test client as a real receptionist on test-client — 200, the button and its send-message link absent, and In-App Chat / Edit Details / Reset Password / Back to Search all present. Found while applying it: the app container bind-mounts /root/work/gp_booking_app on the apps host, and that checkout has no git remote — it sits on feature/wda-register-import while VPS1 and Forgejo carry main at 694b35f — so it cannot git pull, which is a drift risk. Raised with OpenCode in agents/handoff/from-hermes, with the question of what the intended deploy path is; the template was applied by copy and the container restarted (the step scripts/deploy.sh documents).
9. The /appointments/ view tidied — the period filters made to work and the top navigation trimmed — and Matthew's "505" explained. He reported the filter buttons erroring, "Today" showing every appointment, and asked for the Invoices / Appointment Types / Lab / Manuals links to go. The filter fault was real; the error was not. AppointmentListView.get_queryset ignored the ?period= parameter entirely, so every button returned the whole list — it now filters (weeks Monday–Sunday, past = before today, all = unfiltered), defaults to today and orders the day by time. The four navbar links are gone (their pages remain), with the dead pending-lab-badge fetch in base.html. The "505" was a 502 from my own container restart: nginx logged three upstream prematurely closed connection 502s at 14:29:19–14:29:24 and the container's StartedAt is 14:29:19; RestartCount is 0 and the app's logs hold no 5xx at all, so those buttons were interrupted, not broken. Verified on test-client across four roles — today 0 (correctly empty), this_week 2, next_week 5, last_week 4, past 13, all 24, all 200 — with none of the four removed links in the rendered nav. Committed 6609b19, beside 694b35f (the Notify hide). Both are on feature/wda-register-import, not main — a correction: the handoff note had recorded the Notify commit as being on main, and this working copy is checked out on the feature branch. They are pushed to Forgejo, and the changed files were copied to the apps host, whose checkout cannot git pull.
10. The appointment booking form restyled to match the app. Matthew: the /appointments/new/ form "looks unfinished and quite raw". It was: {{ form.as_p }} inside hand-rolled inline-styled divs, a visible debug panel ("Selected Code: None | Selected Term: None"), and leftover [5] citation markers in the labels. Rebuilt to the app's house style — the Bootstrap 5 card pattern the dashboards use (section cards with text-primary headings, a two-column grid, form-label small fw-bold, per-field errors and help text, primary/secondary buttons, a "back" link in the header) — and AppointmentForm.__init__ now gives every widget its Bootstrap class (form-control / form-select / form-check-input, preserving the insurance-field classes the page's JavaScript uses). The SNOMED search, PCN toggle, price display, delivery-method sync and validation script are untouched, and every id they depend on is intact. Verified by rendering the template for both role branches against a real form on test-client: receptionist 17 inputs, all classed, 4 cards, 11 styled labels, coded-reason block and suggestion box rendered; clinician 16 inputs, all classed, 4 cards, coded-reason block correctly absent; and on the live page as a receptionist, 200 with no as_p, no debug panel and no [5] anywhere. Committed e915938 on feature/wda-register-import, pushed, and copied to the apps host. Noted, not changed: validateSNOMEDSelection() is defined in the template's JavaScript but never wired to the form's submit event, so the receptionist coded-reason check does not actually run — left as is because it is behaviour, not presentation, and wiring it would begin blocking submissions.
Read first next time: this round, then the resume note's session-64 block, then from-opencode (two
collection refinements and the print stylesheet open; nothing blocking) and from-hermes. Next: Matthew's two
open calls on the SIAAS page, then his review of the live pharmacy pages — the three unreviewed ones first — then
group F (premises and registration) one page at a time, and the conversion pass once each set is signed off.
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round's value is
one marketing draft brought into line with the build, and three figures replaced at the source with no write to
the tenant.
1. Why this page. Matthew reached the counter page while walking the guides and thought it "basic" at the time; the
CV1–CV9 build has since changed that screen materially. He approved one page, one pass: three text touches plus the
figures re-shot from the delivered screens, review before anything is deployed.
2. The figures were the real fault. All three embedded images were 2026-10-05 mockups of the pre-CV counter:
the basket mock carried per-line Net / VAT amount / Gross columns with "Estimated net 16.30 · VAT 1.28 · total
17.58" and a separate Post the sale step; the till mock showed a session "Open since Sept. 14, 2026, 5:54 p.m. by
." — no holder — with Expected cash 115.90 and a cash-up that recorded only the count; the takings mock had no
cash banked and no float carried forward. Those are exactly the behaviours CV1 (inclusive prices), CV2 (the
till is the operator's), CV5 (the VAT receipt) and CV6 (the cash routine) replaced.
3. The new figures are the verified live captures, trimmed to marketing shape. Built by
/tmp/mkt/build_marketing_figures.py from the group-D captures already taken off the delivered app —
d2-charge-added (basket 18: the £3.20 line incl. VAT with net/VAT inside it, the zero-rated £9.90
NHS prescription charge [token:10], estimate 12.57 / 0.53 / 13.10) for the basket; d6-till (holder Cass
Tillworth, opening float 100.00, takings so far by tender and band, expected cash 113.10, and the cash-up with
Counted cash / Cash banked) for the till; d6-takings (Sales 2, cash 13.10 / card 2.50, standard-rated
4.75 / 0.95, zero-rated 9.90 / 0.00, expected cash 113.10) for the takings. The basket figure stops at the running
estimate and the takings figure stops above Recent refunds and voids, whose reasons are internal test notes.
Uploaded to pharmacy-app-guides under the existing names (assets 191–193), byte-compared after upload
(HTTP 200, identical) and OCR-checked (no vision tool on this host). Nothing was written to the tenant this
round — the till stays closed and basket 17 open, so the D6 walk-through still begins by opening the till.
4. The copy — three touches, paid for elsewhere. The price shown is the price the customer pays (a £2.50 shelf
price is a £2.50 sale, not £3.00 at the till); the receipt stands as a VAT document (identity, time of supply, a total
for each rate, the NHS charge shown zero-rated and never exempt); the till belongs to the person signed in and hands
over with the counted cash as the next float, and the close records what was banked and what was carried forward, the
variance reported rather than smoothed. Paid for by tightening the section prose, the image alts and the link captions:
3,465 characters against the previous 3,355 — +110 for three corrected claims. Deliberately not claimed:
clean printing (the print stylesheet finding is still open with OpenCode) and the charge-picker caveat.
5. A false line on the set landing page, corrected. marketing/web-content/pharmacy_app said of the row
"(No screenshots — group D's guides are not written yet.)"; it now records the 2026-10-06 revision, the three
figures and their source, and the status line matches.
6. Verified. Page 690 / tree 603 renders 200 with all three images referenced; each image is
byte-identical to the local file and 200; the page sits beside its siblings (accounting, rp_log) in the tree.
Nothing deployed — the wiki is the drafting surface and the CMS/deploy is a separate, confirmed step.
7. The website gained its Pharmacies section — navbar, four groups, and the mirrored pages. Matthew: "we need
to add 'Pharmacies' to the navbar — in the menu we need these sections with headings: 'Prescriptions', 'Stock',
'Accounting & Counter' and 'Compliance/Admin'". The live site had no pharmacy content at all (/pharmacies 404,
no CMS collection), so the item needed the section built, not just a link: a new pharmacies collection in
public/admin/config.yml (with the group select and sort), PHARMACIES_GROUPS plus the grouped navbar entry and the
section/page build in build.mjs, content/pharmacies/_intro.md (no pricing line — the dental intro's £125/seat has
no pharmacy equivalent on record), and the eight drafted pages mirrored from the wiki drafts by
/tmp/pharm/mirror_pharmacy.py — body H1 stripped (the builder renders its own), /en/marketing/... cross-links
retargeted to /pharmacies/<slug>, remaining wiki guide links made absolute, and 24 assets fetched into
public/images/uploads/pharmacy/ (heroes resized to 1600 px, quality 82). His grouping is his own and re-groups
the wiki's Compliance & Administration: rp_log → Compliance/Admin, accounting + counter_checkout →
Accounting & Counter. The parked notifications page stays out (holding note; no live NHS Notify), so
Prescriptions carries Dispensing alone. Build verified: the navbar renders the item with all four headings and
the pages beneath them, the landing groups them in order, and 324 internal links and 24 images resolve (the only
dead link found anywhere is the pre-existing footer /onboarding, a status: draft page — live 404 before this
change too). Committed locally as a2f6416, NOT pushed — the deploy waits on Matthew, and three of the
eight pages (data_feeds, accounting, counter_checkout) are still awaiting review. The mirror script and the
section verifier are kept with the marketing skill (scripts/mirror_pharmacy.py, scripts/verify_section.py).
Read first next time: this round, then the resume note's session-63 block, then from-opencode (nothing
open) and from-hermes. Next: Matthew's review of the counter_checkout page, his go-ahead for the Pharmacies
deploy (with the three unreviewed pages flagged), then group F (premises and registration) as before.
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round's value is
one guide published against the running app, the token census corrected on the coverage plan, and two
tooling fixes (one in the capture recipe, one in the render helper) that the next three counter guides inherit.
1. D2 published. Counter — Add the NHS Prescription Charge
(Wiki page 704, tree 684, ~7.9K chars) — the charge card and its Token picker, the zero-rated £9.90 line
carrying the opaque token reference and no patient identity, the running estimate, and both refusals. Guides index
and coverage plan carry the D2 row; D3–D6 are next.
2. Four figures from the live counter as tc.ca.one (basket 18): d2-charge-picker, d2-charge-added (the
charge line NHS prescription charge [token:10], £9.90, Zero-rated, £0.00 VAT; estimate 2.67/0.53/3.20 →
12.57/0.53/13.10 — the VAT does not move, because there is none in the charge), d2-charge-refused-exempt and
d2-charge-refused-collected. Uploaded to pharmacy-app-guides, byte-compared after upload, all HTTP 200; the
rendered page returns 200 with all four referenced. Each figure was read back with tesseract (the only way to
check a render on a host with no vision tool).
3. The token census, corrected on the coverage plan. The live picker offers ten tokens (DISPENSED or
ENDORSED, most recently updated first, labelled by EPS reference), of which three are collectable —
demo-token-dispensed-1, demo-collection-ready-1, demo-collection-optedout-1; the rest are exempt declarations
or already collected. The coverage plan's group D note claimed "every dispensing token the charge picker offers has
its charge collected" — false, and now corrected in place with the census and the fixtures D2's figures used.
4. The refusal strings, exact (verified live): the declaration exempt → "The declaration says no charge is due
(exempt)."; already collected → "The charge for this token has already been collected."; nothing chosen →
"Choose a dispensing token." No double collection is possible — the service refuses it. The CV9 refinement stands
(the picker offers tokens the app will then refuse) and remains with OpenCode.
5. The recipe extended. capture_counter.py gains the charge scenes (charge-card, charge-add,
charge-exempt, charge-collected), token fixtures named by EPS reference rather than pk, and
capture_post() — a refusal banner is consumed by the redirect's own render, so the POST's response is captured,
never a fresh GET. --check re-proves the recipe before every capture run.
6. A figure-pipeline bug fixed at the source. shoot_trim.py built file://<relative name> whenever it was
handed a relative path, and Chrome rendered ERR_INVALID_URL — a small centred error box — instead of the page;
the trim then reported height == window, which reads as "clipped" and hides the real fault. It now resolves the
source with os.path.abspath(). Three figures were repaired by re-shooting on absolute paths.
7. The reviewer's state left standing. The till is open (session 20, float 100.00) and basket 18 holds an
over-the-counter line (Ibuprofen £3.20) plus the NHS charge line for demo-token-dispensed-1; basket 17 (D1's
two lines) is untouched, and that token's charge is still collectable because basket 18 is open, not completed —
nothing posts until the sale is completed.
8. D3 published, and the sale it needed is left standing. Counter — Take Payment and Close the Sale
(Wiki page 705, tree 685, ~7.5K chars): posting the invoice ("Sale posted as ACC-SINV-2026-00029. Take the
tender below to complete it."), the tender card with ERPNext's own net / VAT / total and the two bands side by
side, the below-total refusal, the change, and the Awaiting tender list that holds a posted sale whose money has
not been taken. Four figures (d3-sale-posted, d3-awaiting-tender, d3-tender-refused, d3-sale-complete),
byte-verified after upload. To make them, basket 18 was completed — ACC-SINV-2026-00029 submitted, the payment
entry posted against the counter till account, counter stock consumed for the goods (never for the charge), tender
cash 20.00, change 6.90 — and it is left standing as the completed sale D4's receipt and D5's refund figures will
use. The recipe gained the posting scenes (post, awaiting-tender, tender-refused, complete), and they refuse
to run without --allow-posting: they write a real invoice, a payment and stock.
9. D4 published. Counter — Give the Customer a Receipt
(Wiki page 706, tree 686, ~6.3K chars): what the receipt carries (the pharmacy's identity, time of supply,
sale reference, a line per item, net / VAT / total, the tender and the change, and the VAT rate table), how to print
it, emailing a copy only where the customer asks ("Receipt emailed to …" and the receipt's A copy was
emailed.), and finding an earlier sale's receipt again from Recent sales. Three figures (d4-receipt,
d4-recent-sales, d4-receipt-emailed). One finding, filed to OpenCode: there is no print stylesheet — the
Print button hands the whole page (app header, menu, buttons, the email form) to the browser's print dialogue, so
a customer's slip prints with the app's chrome around it. The guide says so plainly; the fix is OpenCode's.
10. D5 published. Counter — Refund or Void a Sale (Wiki page
707, tree 687, ~6.9K chars): who may reverse a sale, the form (Kind refund/void, Reason, Return
stock), what a reversal actually does, what the sale reads afterwards, and the guard. Four figures
(d5-refund-form, d5-refund-refused, d5-refund-done, d5-reversed-basket). The gate was verified live:
tc.ca.one and tc.tech.one get 403 on the reversal screen and no button on the sale page; tc.rp.one and
tc.bm.one get both. The reversal was verified in the client's own books: basket 19's invoice
ACC-SINV-2026-00030 and payment ACC-PAY-00016 both Cancelled (docstatus 2), the stock returned as an
ADJUSTMENT +1.00 with reason refund into the same batch it was sold from (DEMO-OTC-PARA-B1), and the audit
rows COUNTER_REFUND and STOCK_ADJUSTMENT recorded against the RP. Both kinds set the sale to Voided — the
kind is on the reversal row — and the guard reads "Only a completed sale can be refunded or voided."
11. D6 published — the group is complete. Counter — The Till and the Day's Takings
(Wiki page 708, tree 688, ~8.2K chars): opening the till and its float, the session's Takings so far, the
handover to the next operator, the takings view by tender method and VAT band (VAT read from the submitted
ERPNext invoices), and the cash-up close — counted, banked, the float carried forward and the variance, reported
never smoothed. Five figures (d6-till, d6-till-handover, d6-takings, d6-till-closed, d6-takings-closed).
Verified live: the day closed with expected 113.10, counted 112.10, variance −1.00, 50.00 banked, 62.10 carried
forward, and the refusals "The till is held by Synthetta Chemsworth. They must close it (or hand it over) before
you can open a session.", "You already have the open till session.", "There is no open till session.". A reversed
sale drops out of the session's takings (that is why the closed figure reads one sale, not two) and is listed
under Recent refunds and voids; that list shows the counter's most recent reversals, not just the session's.
12. Group E opened and closed the same session — six guides on collection and patient contact. All six
published off live captures on the demo tenant, one at a time: E1 Hand a Prescription Over
(page 709), E2 Which Are Ready but Uncollected (710),
E3 Send or Resend a Collection Notification (711),
E4 The Delivery Trail (712),
E5 Record a Contact Preference (713) and
E6 Record a Patient Opt-out (714), with 14 live
figures (e1-* … e6-*), each byte-verified after upload. The group had no build spec — the screens are as
delivered — so every claim was read off the running app: the handover recorded two ways (Collected on
demo-accounting-claim-1, Returned to the shelf on demo-accounting-claim-2), the failed collection-ready on
demo-collection-failed-1 resent ("Notification resent.", attempts 2), a contact preference recorded on
demo-token-dispensed-1, and an opt-out recorded and reversed on demo-collection-uncollected-1 so the fixtures
stand.
13. The role gates were checked live, and they turned up two findings — both filed. The Counter Assistant
reaches the collection dashboard, the handover form and the handover history (200) and is refused the uncollected
list, the delivery trail and the contact-preference screen (403); the pharmacist, technician and business manager
reach all six. (i) The Collection screen's Dispensed column is updated_at while the uncollected list uses the
true dispense time — demo-collection-uncollected-1 reads 2026-09-15 on one screen and 2026-09-05 on the
other. (ii) The Counter Assistant's opt-out is accepted by the view but unreachable in the UI (the opt-out lives
on the contact-preference screen the CA is refused). Both are in from-hermes; the guides state the behaviour plainly
and tell the counter to pass the opt-out on. The E-group sequence script
(scripts/capture_collection_sequence.py) is the durable recipe for the group.
Read first next time: this round, the resume note's session-62 block, then from-opencode, then
from-hermes (the two collection findings are the newest note). Next: the walk-throughs with Matthew — twelve
new guides (counter D1–D6, collection E1–E6) await his review — then group F (premises and registration), one
page at a time.
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round closed
the counter work end to end: built → deployed → verified live → recorded, with the guides as the only thing left.
1. CV1–CV8 verified live and ACCEPTED. OpenCode's five commits (2d3bfa1, e8e9440, 528f2a8, 1bf5628,
9c5cf3a) delivered inclusive counter prices, the operator's till and handover, park/resume, scan-to-line, the VAT
receipt, the cash routine, the audit evidence and the synthetic coverage. Hermes verified them against the client's
own ERPNext with synthetic data, driving test-client-pharmacy through the real OIDC login and reading the client
database, the ERPNext books and the central database independently of the app — 28/28 checks, through a new
committed verifier, veripath-ops fa46eb0 (scripts/verify-pharmacy-counter-views-live.py).
2. The evidence that matters. Basket 9 / ACC-SINV-2026-00021: a £2.50 shelf item and the £9.90 NHS charge
invoice at £12.40, the OTC line carrying net_amount 2.08 and amount 2.50 under tax head
VAT @ 20% (included in rate), tax 0.42, item_wise_tax_detail
{"DEMO-OTC-PARA-500":[20.0,0.416…],"NHS-PRESCRIPTION-CHARGE":[0.0,0.0]} — so the overcharge that opened this whole
thread (a £2.50 shelf price charged as £3.00) is closed, and the receipt reads Standard @ 20.00% 2.50 / Zero-rated
@ 0% 9.90 with the NHS charge zero-rated and never exempt. The till refusal names the holder and the handover
carries the counted figure as the incoming float; the close records counted 112.40, banked 50.00 and float carried
62.40; the central database holds zero pharmacy% tables; pharmacy_counter.0002–0004 and
pharmacy.0020/0021 are applied on the client tenant.
3. One finding, and it is a refinement. The NHS charge picker lists every DISPENSED/ENDORSED token, including
ones whose charge has already been collected, so an operator can choose one and get the (correct) refusal "The
charge for this token has already been collected." Filtering the queryset by the rule services.token_collectable
already applies would remove the dead choice. No double collection is possible — the service refuses it. Left
with OpenCode, in from-hermes.
4. Two data notes for the guides. The tenant's CounterSettings identity is seeded (Synthetic Test Pharmacy (DEMO)) and its VAT registration number is blank — set to an obviously synthetic value to prove the receipt
renders it, then cleared. And every token the charge picker offers now has its charge collected, so a figure of
that picker wants a spare token seeded first.
5. Records touched. The spec page gained its "As delivered — CV1–CV8 built, deployed and verified live"
section with the criterion-by-criterion evidence; from-hermes carries the CV9 result and the finding; the coverage
plan's group D now reads verified, with the six guides named as the next work.
6. The group D guides opened — D1 published (2026-10-06).
Counter — Sell an Over-the-Counter Item — the
basket, scanning versus searching, quantities, the price the customer pays and the running estimate, parking a
basket, and the failure paths — with four figures captured from the live counter as tc.ca.one (byte-verified
after upload), the guides index and the coverage plan updated, and the reviewer's state left ready (till open,
basket 17 holding two lines). D2–D6 follow, one page at a time for review, per the series rhythm.
Read first next time: the resume note's session-60 block, then from-opencode (nothing open), from-hermes
(the CV9 result), then this log (Rounds 32–33). Next: D2 (add the NHS prescription charge), then D3–D6, one page
at a time for Matthew's review.
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round's value is
one workstream re-opened and properly scoped, one build spec written, and the handoff directory reset to a
working state before the brief goes into it.
1. The counter/till work re-opened, and the test that orders it. Matthew resumed development on the pharmacy
app's counter views, handing over a Gemini design exchange (kept at
Counter Til) with the rule that scope is set by what an
operating pharmacy must do to trade. Read against the code, the sketch mostly describes what already exists —
pharmacy_counter C1–C13, accepted 2026-09-14 (dashboard, till, basket, receipt, refund, takings), with the Business
Manager's reconciliation in pharmacy_accounting and the contact preference/opt-out in pharmacy_notifications. The
test reordered the list: the top gap is not the queue strip but that a counter price is net, so a £2.50
shelf-priced standard-rated item is charged £3.00 (services.price_for_item takes ERPNext's price_list_rate as
net; estimate_totals adds the rate; ERPNext adds it again on the document). Written up as
The Counter and the Till — Scope by the Operating-Pharmacy Test,
gaps ranked blocking / friction / optional, split tender left out on D4.3, vocabulary left as ratified (takings,
never "Z-report").
2. Decisions settled, and one resolved from the source rather than from memory. Matthew settled inclusive
counter prices (closes Phase 4's last open finding) and the till session per operator — the person signed in,
sequential: a handover is close-then-open with the counted figure carried as the next float, and the single
open-session index (one_open_till_session) is untouched. Split tender stays out. For the receipt, HMRC's VAT
Notice 700 §16.6.2 was read rather than recalled: a retailer must be able to give a VAT invoice when a taxable
person asks — name, address and VAT registration number, the time of supply, a description identifying the goods, and
per VAT rate the total payable including VAT and that rate (£250 threshold) — and §7.3.1 gives the VAT fraction
(rate ÷ (100 + rate), one-sixth at 20%) as the arithmetic for an inclusive price. No source was found requiring
the GPhC premises number on a receipt, so nothing is asserted; it is display-only config.
3. The build spec written — not yet handed over. The Counter Views — Inclusive Prices, the Operator's Till, and
the Counter at Work: work items CV1–CV9, gates
G1–G4, acceptance criteria and risks. It goes to OpenCode on Matthew's word.
4. The handoff directory tidied (his ask, first). A 25-note archive pass: 15 off from-hermes (every one
handled, closed or FYI), all 10 off from-opencode; and the resume note trimmed 43 KB → 17 KB (sessions 52–56
moved to the archive). Two corrections rode with it: the 2026-09-24 ERPNext desk 500s note still read "OPEN —
mitigated for today" although OpenCode's session-55 handback fixed it and Hermes verified it on 2026-10-02 — filed
HANDLED 2026-10-02; and the 2026-10-01 "ORDER TO WORK IN" note, which only ordered the notes moved with it,
is marked superseded, its ordering now the live inbox header. The Linux-launcher note is filed as a second copy
of a pair (the 2026-10-01 pass moved the original) — kept, and labelled, never dropped.
5. Two findings kept rather than buried. Sessions 46–51 appear on neither the resume note nor its archive —
their rounds live here; recorded on the archive page and in this round. And the live inboxes had drifted into
carrying long tails of handled notes, which is exactly the cost the handoff token rules exist to prevent.
6. Two exceptions recorded, neither actioned. (i) The dental seed migration
tenancy/migrations/0002_seed_sectors_and_tenants.py — its two db_password='***REMOVED***' redaction literals now
read os.environ.get('TENANT_DB_PASSWORD', '') (dental 03012da, already deployed; OpenCode re-posted the query
after its acknowledgement note was overwritten). Hermes answered it: keep the edit — Django keys applied
migrations on the app label and name, so a deployed database cannot re-run it; reverting would keep the artefact in
source and make a fresh build seed a bogus credential; and nothing in the repo reads Tenant.db_password, so the
seeded value is inert. Two follow-ups attached: TENANT_DB_PASSWORD needs documenting where the deploy and dev
environments are described (it appears nowhere else in the repository), and the migration comment's claim that
"provisioning overwrites it" is not demonstrated — provisioning_service.py writes only
ClientVPS.app_db_password / client_db_password. (ii) The ERPNext image and deploy trap (OpenCode's
d894dc2): the upstream frappe/erpnext:v15 base carries frappe at gid 0 with no frappe group, so
chown frappe:frappe fails the build — and the deploy script's piped | tail masked the non-zero exit, so a failed
build reported success. Both want a line in the ops docs before the next image rebuild.
Read first next time: the resume note's session-60 block, then from-opencode (nothing open), from-hermes
(five notes — the counter brief newest), then this log (Round 32).
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round's value is
six published guides amended or finished, every one against the running app rather than the log, seven
figures re-shot and byte-verified, two claims retired that the app had quietly made false, and one app defect
authorised and specified for OpenCode.
1. The parked item, recovered — and it was more than the one sentence recorded. Matthew parked it on 2026-09-30
("the expiry guide still says the position refresh never removes a stale position, which 508aea3 made untrue"), and
the note required the tenant's cache to be read before any wording was written. Doing that showed the false claim was
only half of it: the cache holds 5 rows, all synced 2026-10-02 04:32, and nothing inside the 90-day band — the two
Amoxicillin rows the guide's worked example was built on (DEMO-AMOX-B3 expired, DEMO-AMOX-B2 critical) are gone,
removed by a prune-enabled refresh, and the report is empty. The dead claim and the stale example were the same
event seen from two ends.
2. A second drift, found while verifying: the report carries price columns now. f05fc3f (the ordering views,
2026-10-01) put the price_view columns on the expiry and low-stock reports for the three stock-managing roles. The
guide's column table stopped at Quantity. Fixed on the page, with the five columns named and the Counter
Assistant's page stated plainly to be without them. The coverage plan's own C3 entry carried the opposite claim
("byte-identical … all four roles see the same list"); it now carries the dated correction.
3. Evidence: the live tenant, not the code. Nothing was concluded from reading services.py alone. manage.py shell through the tenant alias read the cache row by row (batch, expiry, quantity, synced_at); the deployed container
was grepped to confirm the prune is in the running image; and the app was driven through the real OIDC login as
tc.rp.one and tc.ca.one — the expiry report renders "Nothing is expiring within the warning band." at HTTP 200 for
both. An empty report cannot show the price columns, so they were proved on the same show_prices path on the
low-stock report: 8 columns for the RP, 3 for the Counter Assistant. One claim I could not settle and did not
make: no schedule refreshes the positions (CELERY_BEAT_SCHEDULE carries no stock task and no host cron on either box
does it), so the 04:32 refresh was a manual press — which also made the empty state the only honest starting point
before the illustration was rebuilt (item 4).
4. The illustration rebuilt — an empty report is a poor guide, and Matthew agreed. With his word, two short-dated
batches were put on the demo tenant through the app's own goods-in form as tc.tech.one: DEMO-AMOX-B4 (24 packs,
expiry 20 Oct 2026 — inside the 30-day band, so critical) and DEMO-SALB-B2 (12 packs, expiry 1 Dec 2026 — inside
the 90-day band only, so no pill). Receipt GIN-20261002-6C6D66 posted to the ledger as ERPNext
MAT-STE-2026-00033, the refresh then reported 7, and the overview reads Expired 0 / Short-dated (90d) 2 —
which is the two rows. The guide's example table is that live page, and the figure was re-shot from the app's own HTML
as tc.ca.one — CSRF tokens, secrets and internal addresses checked for and absent — rendered in headless Chrome,
trimmed, then downloaded and byte-compared (HTTP 200, MATCH). There is no vision tool on this host: the render is
verified by its <title> and its colour profile (3,799 distinct colours), not by eye, and the caption describes the
state rather than implying an eyeball check.
5. A finding recorded, not worked around: an expired batch cannot be created at all. Staging the third case — an
expired row — was refused at both ends, and the refusals are the finding: the goods-in form rejects a past expiry
date outright, and ERPNext refused the adjustment with BatchExpiredError: … DEMO-AMOX-B3 has already expired
(HTTP 417). Nothing moved: the batch document already existed and holds no stock, and no movement, cache row or
position was written. Two things follow. The guide now says plainly that an expired row is found, not created,
rather than leaving a reader wondering why the example has none; and the adjustment's failure banner — "could not be
posted to ERPNext. Please try again." — invites a retry that can never succeed while the batch is expired, which is
noted for C4 / OpenCode, not fixed here.
6. C5 — the other half of the parked pair, corrected in full. The same false claim sat in four places: the
"Refreshed 5 stock position(s)" row, the paragraph headed "One thing the refresh does not do: it never removes a
position", the "neither counted nor removed" troubleshooting row, and "It does not remove stale positions, as
above". All four now say what the code and the live tenant do: the refresh adds, updates and deletes — a cached
position in the scope it has just read that ERPNext did not report is removed — so the old 5-versus-7 gap cannot recur;
and the deletion happens only after that scope read without error, which turns "the cache can be stale" into the
guide's explicit reassurance that a failed refresh cannot make it wrong. The figure was re-shot from a real
refresh as tc.tech.one ("Refreshed 7 stock position(s).", Last position refresh 2:55 p.m. → 3:01 p.m.; bytes
verified).
7. Its knock-on closed in the same pass: C2. Two drifts on the low-stock page — it sent the reader to Low stock in
the top menu (it is a button under the overview's figures; a live read of the nav settled it) and it did not
mention the five price columns the same three roles see. Both fixed, and both C2 figures re-shot live as tc.ca.one
(c2-stock-overview.png, c2-low-stock.png, bytes verified).
8. B5 finished, and B4 with it — a "pending" that had quietly stopped being true. The coverage plan carried "B5 —
two corrections pending": the endorsement reference box to be removed, and the declaration to move to the coded
exemption values. Both landed on 2026-09-19 (0a55eea, 5b4e135) and are deployed; B5's text already carried the
older-records note, so the pending entry had become a false statement about the app rather than a to-do. Checked
against the running app: the endorse step renders one button and no reference input, and the dispense screen
renders the coded EPS list, whose first entry is the blank value labelled "— no exemption (charge due) —" — B4 gained
that line, because a reader choosing from a dropdown is not "leaving it blank". Three figures re-shot as
tc.rp.one (assets 147 / 148 / 150, bytes verified) as they predated the 19 September changes;
b5-endorsed-token.png was left deliberately — the endorsements it shows were made before the field went, and the
guide explains that.
9. A sweep, so the same claim is not left anywhere else. All 21 pharmacy guide pages were grepped for the false
phrasing and for the price-column gap: no other page claims the refresh never prunes, and every other "in the top
menu" reference is correct (Stock and Dispensary are real menu items; the sub-screens are buttons). The last
copy of the old claim was the coverage plan's own C5 note, which now carries a dated correction.
10. C4 amended — and the app-side fix authorised, specified and handed over. Staging C3's expired example turned up
a second, related finding: the adjustment form reports every ERPNext failure with "please try again", which is
wrong when the ledger has refused the entry — an increase naming an expired batch (HTTP 417,
BatchExpiredError). The boundary was settled from the deployed source rather than by experiment: ERPNext
15.121.6's StockController.validate_serialized_batch() skips the expiry check when
purpose in ("Material Issue", "Material Transfer"), so an increase cannot record expired stock, but a decrease can
write it off — the guide's "expired — disposal" advice stands, and it was the refusal that needed stating, not
the advice changing. C4 gained a troubleshooting row, a retry passage split into refusal vs post-then-fail, and a
Direction note that an increase cannot name an expired batch. Matthew authorised the app fix in the same session,
so the finding became an OPEN build brief in from-hermes (tell a 4xx refusal from a post-then-fail, carry ERPNext's
own reason, leave the ambiguous wording exactly as it is, one test per branch) — now the second ask in OpenCode's
inbox, after the dental Notify brief. No app code was touched from this side: building and testing the pharmacy app
is OpenCode's.
11. Not invented, not over-reached. Every claim added is from the live tenant or the deployed code; nothing still
true was removed; the expired-batch refusal and the adjustment banner that invites a hopeless retry are recorded
for C4 / OpenCode, not fixed; and the endorsed-record figure was left alone rather than re-staged by consuming a
fixture.
Owed after this round — nothing in the pharmacy series; two app asks with OpenCode. Every page in groups A, B and C
is now free of a caveat that is no longer true, and every figure touched this session is a live capture. What is owed
is OpenCode's: the adjustment-refusal message (authorised by Matthew this session) and the dental Notify
brief. Unchanged and elsewhere: the parked Phase 4 tidy-up.
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round's
housekeeping value is one page amended where the finding it carried has now been closed, one build record
amended, and one verification note on my own outbox. OpenCode returned with three handbacks (sessions 53–55;
nothing was open before them) and all three are accepted.
1. The accounting nav entry (session 53, bb8b5d3) — my own finding — verified live, 11/11, my own run. The
A8 handover pack could not say "click Accounting in the menu" because no such entry existed. Driving the deployed
test-client-pharmacy through the real OIDC login: as tc.bm.one the header carries href="/accounting/" on
/pharmacy/ and on /stock/, /accounting/ is 200 and its button row carries /accounting/unmatched/; as
tc.rp.one, tc.tech.one and tc.ca.one there is no accounting link and a direct /accounting/ is still
403 — so criterion 7 is re-proved. The commit is the branch tip here and there is no migration.
2. The dental Redis URLs (session 54, 87e1b25) — accepted, with one residual finding. This host's working copy is
at 87e1b25; dsp_clinic/settings.py holds no ***REMOVED*** and the channel layer takes
CHANNEL_LAYER_URL from the environment with a credential-less fallback on dental_redis; live, the dental health
endpoint is 200 and / is 302. The residual — the same redaction class at appointments/views.py (two
NHSNotifyService(api_key=…) literals) and in the 0002_seed_sectors_and_tenants migration — is recorded as a
candidate for Matthew, since it wants a Notify key name decided and the key added host-locally.
3. The ERPNext desk 500s (session 55, 840dd37) — accepted at code level, and the limit stated. 840dd37 is the
tip of erpnext-deploy and its diff carries all three fixes (the restored separator that ends DESClimit,
_patch_db_error_rollback(), _patch_boot_degradation()), applied at import; the client ERPNext sites answer
(/login 200). I did not drive a desk to watch get_bootinfo build — that needs desk credentials this host does
not hold — so the live proof there remains OpenCode's, recorded plainly rather than implied.
4. One page amended, so nothing stale is left behind. The
A8 pack's "how to get there" line no longer tells the
reader to bookmark /accounting/; it now says the Accounting entry is in the Business Manager's header, and the
unmatched-receipts step points at the dashboard's button row. The pack carries no caveat that is no longer true.
The Phase 6 record gained the same: its A8
block now says the finding was fixed and verified, with the 11/11 evidence.
5. Not redacted, not invented. No credential entered any page (the dental findings are described, never quoted);
the verification of session 55 is recorded as far as it went rather than being written up as a live desk drive;
and the one item I re-proved for myself — criterion 7 — is stated as re-proved.
6. Candidates recorded, not acted on: the parked C3 item — now identified (item 10), and still his to name
— and Phase 4. (Three candidates left this list in the same session: the dental Notify-key literals + migration
seed is now briefed to OpenCode at his direction; and the two pages quoting the old header row and the
asset pass were done — items 7 and 8.)
7. The pages quoting the old order-file header — the drift was wider than recorded, and is now closed. The
candidate said "two pages"; there were five live pages quoting ProductCode,Quantity as the export's content.
The truth is in the code and the file contract:
order_profiles.py says in its own words that "the standard two-column order file is headerless", and the contract of
2026-09-29 settled it. Fixed: guide C6 (its quoted file block is now headerless), the
coverage plan (its record of C6's quote), plus dated corrections on
three specs that had drifted — Supply Orders — Build Spec
(whose GENERIC line read "headers ProductCode,Quantity" and now names the labels as the portal's columns),
Phase 4 — Order Value and the
satellite's Supplier Portals scoping note.
Left alone deliberately: research/supply_orders, which already carries its own status block saying those layouts
are unevidenced, and the archived transcripts. Verified afterwards: no guide states the export carries a header row.
8. The asset pass — C6's figure re-shot, and the pack illustrated. C6's first figure (c6-order-build.png) had
been captured 2026-09-21, ten days before the ordering view existed, so the guide's text described the new screen
beside a picture of the old one — a real defect in a published guide, and the reason this candidate existed. It is now
re-captured live as tc.tech.one and uploaded over the same path (asset id 164), byte-verified after
download, so the published guide shows the ordering view. The A8 pack gained two figures — the accounting
dashboard and the September period-end pack, both captured live as tc.bm.one (ids 182/183, byte-verified) and placed
in the guides' existing asset folder, which is the one that belongs to the section the pack sits in. How: the app's
pages carry their CSS inline and reference nothing external, so the authenticated HTML renders faithfully from
file:// in headless Chrome; each render was checked for the right screen (<title>) and for non-blankness
(3.3k–7.8k distinct colours). Stated plainly: I did not view the images — there is no vision tool on this host — so
they are verified structurally, not by eye, and that limit is recorded rather than glossed.
9. A process finding worth keeping. The candidate that sent me looking — "two pages still quoting the old header
row" — was recorded once (Round 24) and then carried forward six rounds without its two pages ever being named.
It cost nothing this time only because the string was searchable. The lesson is the one this log has already learned
twice: a candidate must be recorded with the evidence that would let it be actioned, or it becomes a phrase nobody
can pick up.
10. The parked C3 item is not orphaned after all — it is identified, and Matthew's question is what found it. The
reference had been carried since Round 24 as "the parked C3 item" with no definition, and my first pass this round
concluded its content was lost. It was not lost — it was recorded on
hermes-resume, in session 45's block: "the parked C3 item he set aside at the
start of this session (the expiry guide still says the position refresh never removes a stale position, which
508aea3 made untrue — C3 and C5 both carry it)". His closing question — "anything to do with this:
/guides/pharmacy_app_guides/run-the-expiry-check?" — is what recovered it, and the answer is yes: C3 is the guide
itself, Stock — Run the Expiry Check, parked by him on
2026-09-30 rather than corrected. Verified at both ends, nothing corrected yet: commit 508aea3 (2026-09-21,
"pharmacy_stock: prune the position cache on refresh") deletes cache rows in the synced (item, warehouse) scope
that ERPNext did not report, only after the positions were read without error — so a failed refresh still leaves
the cache intact — and the deployed container carries that code, so it is live rather than merely committed. The
false claim sits in five places across two published guides: C3's one troubleshooting row ("A batch that has left
the shelf still shows a quantity") and four in
C5, including the whole paragraph headed "One thing the refresh
does not do: it never removes a position". C3's other line — "does not remove or move stock" — is about the report
page having no button on it and stays true. Two lessons, and the second is new: a candidate carried without its
evidence is unfindable by whoever picks it up (item 9) — and the resume note is a record too, so a look there
belongs in the first pass, before concluding anything is lost.
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round's
housekeeping value is one new client-facing page placed and linked, one build record amended where it asserted
a deliverable did not exist, and one inbox note opened; the session's substance — the pack itself and its live
verification — is recorded on
phase-6-accounting.
1. One new page — the A8 deliverable that was outstanding. The Bookkeeper Handover Pack — Running the
Pharmacy's Books (page 680, tree 657) is the last
outstanding Phase 6 acceptance criterion: the written documentation a client's bookkeeper needs to run the
pharmacy's own books. Written to the D6.5 shape — a one-page owner summary on top, then the bookkeeper's half —
and built from the delivered code (pharmacy/accounting.py's DOCUMENT_ACCOUNTS, pharmacy_accounting/reports.py
and services.py at f05fc3f), so nothing was invented. It carries the accounts A1 creates, the eight
/accounting/ screens, how to prove a figure, the six PERIOD_END_MANUAL items verbatim, and a worked period on the
test client's own synthetic figures.
2. Placed with the client-facing guides, and linked so it is findable. It sits under guides/pharmacy_app_guides/
— the set that is handed over — with a new ## For the pharmacy's bookkeeper section on the
guides index (that page's description updated in the same pass). It deliberately
does not join the coverage plan's A–H groups, which are staff tasks; the pack is a document handed to a third
party.
3. One build record amended rather than left asserting the opposite. phase-6-accounting
read "criterion 6 … OUTSTANDING — A8, Hermes's, not yet written". It now carries a dated A8 block: where the
pack is, what it carries, and what was read live as tc.bm.one on the client tenant — all eight screens 200;
the September band figures 5.00 / 1.00 standard and 9.90 / 0.00 zero-rated with the report's own tie line
"Linked figures tie."; the −1.00 variance shown, not absorbed; stock 353.85 over 252.00 units; and
403 on all eight for the other three roles. Criterion 6 is marked written and verified live, acceptance with
Matthew — not "PROVEN", because the acceptance is his.
4. One inbox note opened, in the right place. from-hermes is now 13 notes, the newest OPEN: the
accounting section has no menu entry — found by driving the app while writing the pack. The eight reports all work
for the Business Manager, but base.html carries no nav entry for them and nothing links to /accounting/, so the
section is reachable only by its address. Recorded rather than worked around: the pack states the gap as a finding
and the two-line fix is OpenCode's. The owed list was renumbered to four items.
5. Nothing was redacted this round, and the boundary held. The new page carries no credential and no
internal address beyond the app's own paths; the live walk was read-only — the only writes are the app's own
REPORT_VIEWED audit rows (A10), and nothing was posted to ERPNext and no receipt was recorded. Illustrations:
the pack has none yet. The guides are illustrated and this one wants at least the period-end pack and the accounting
dashboard as figures — recorded as an asset candidate, not silently skipped.
6. Candidates recorded, not acted on (still his to name, one at a time): the two pages quoting the old header
row; the parked C3 item; guide C6's first figure (stale since the ordering view shipped) and the
demo-seed limitation the verification found; and the pack's figures (item 5). (The four research pages that
headed this list are gone — see item 8.)
7. The resume-note prune was done, in this session, with its own block. agents/handoff/hermes-resume had reached
~198 KB and 30 session blocks, and it is the file every session reads first — so the tax was paid on every cold
start. Sessions 45 and earlier moved verbatim to
hermes-resume-archive (which already held session 19 and earlier), and the
page now keeps the current cold start, sessions 46–50, the read-first order (refreshed to 2026-10-02) and
the Do not re-discover list. Verified by reading both pages back: every session 20–49 is present exactly once,
none is duplicated, and no moved block was rewritten. The live page is 198 KB → 39 KB; the archive is 210 KB —
the price of keeping history rather than deleting it. This is the prune Round 27 flagged and deliberately left for a
later session.
8. The research transcripts were archived at his direction — with the inbound links repaired in the same pass.
Asked whether the four banner-owing research pages were worth keeping, Matthew ruled: "they were just to get us
started in the areas concerned — they've served their purpose now so you can archive them unless you think they could
be useful in the future." Five pages went to archive/: the four (research/csv_format,
research/supplier_csv_files, research/selenium_gadget_supplier_portals, research/discounts_test_portals) plus the
root stray pharmacy_app_selenium_gadget (~69K, description-less, no en/ prefix, and the original transcript of
the same selenium research — archived with its near-duplicate rather than left behind as an orphan). Nothing was
deleted — archive-not-delete, still searchable and recoverable. I read them before moving rather than assuming:
their substance lives on in the file contract, the satellite stages 1–2, the supplier-portal scoping notes
and the test-rig spec, and where one held mutually exclusive positions (the discounts page's two incompatible
readings of a short-liner's file) the discrepancy is already recorded on the rig spec — so nothing authoritative was
parked. Six live pages referenced them, and every link was repaired to the archive path in the same pass (the
file contract — whose "three research pages owe a dated assessment banner" item is now marked CLOSED, archived
instead — both satellite stages, the test-rig spec, the portal-interaction note and the price-download note), plus
the standing note in from-hermes that carried the old csv_format exclusion. Verified afterwards by re-scanning: no
live page now points at a research transcript at its old path.
9. The satellite got its own sub-directory — because this is the area with the most still to come. His steer:
"we'll need a sub-directory for the selenium/satellite page because there will a lot more to come in this area — we
have only scratched the surface on this topic, [with] a lot of testing yet to be done and possibly further development
work." The topic had been scattered across three places — four specs in build-records, three pages in
testing/, two scoping notes loose at the project root — with nowhere obvious for the next round to go. New:
development/projects/pharmacy_app/satellite/, a folder with a hub page,
The Order Satellite — the Programme: what is live today (the
setup screen, the package with both launchers, stages 1–4, the inbound half, the hosted test rig), the two honest
limits (a real configuration is refused by design; no real wholesaler has been tried), and what comes next —
the testing and the development. Five pages moved in, keeping their content and their leaf slugs (a move, not a
rewrite): the three testing/ pages (the test environment, the Linux launcher and Exercise 1) and the two scoping
notes (outbound and inbound). The four build specs stay in build-records, the estate's spec archive — the hub
links to them. Every inbound link was repaired in the same pass: 14 live pages, 39 links repointed (both inboxes,
the resume note, five specs, the project index, the app overview, guide C8, and the moved pages' own cross-links).
Archive-class pages were left verbatim — the two handoff month archives and hermes-resume-archive, 4 links — and
that is the deliberate exception. Verified by re-scanning 114 live pages: the only old-path references left are in
those three archived pages.
The empty testing/ node was removed, on his word. With all three pages moved out,
development/projects/pharmacy_app/testing/ was left an empty nav node; his ruling was "may as well remove it", so it
was pruned — 1 nav node, 0 page rows, confirmed by the tool's own report and by the page count being 85 before
and 85 after, so nothing else was touched. Checked first that it was genuinely empty (--list under the path: no
pages; no page at the node itself), and afterwards that the satellite folder and its six children are intact and
the hub still resolves (200).
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round's
housekeeping value is one new page placed correctly, one research page cleared of its banner debt, and one
inbox note opened in the right place; the session's substance is on hermes-resume.
1. One new page, under the build-records node it belongs to.
The Ordering Views — the Business Manager's Decision Screen
(page 679, tree 656; the parent is already isFolder, so it shows in the sidebar and no nav change was needed) —
the collective ordering view for the pharmacy, approved by Matthew on the day with the supplier offers capped at
four. It carries the spec and the rendered mockup, which is the point: asked for the view rather than the prose, it
was built as a picture on dummy data and amended from there. It documents what is already live (pricing stages 1–4,
the derived pricing, the comparison) and names the honest limits — prices not stock, no "short supply" claim anywhere,
no cell ever blank or £0.00. No phase number, deliberately: the pricing and reference-data families have taken
1–4 and 7–8, so a bare 9 would be ambiguous.
2. The first of the four banner-owing research pages is cleared.
Stock Order Views — the page that asked for this
design — now opens with a dated assessment banner: what was kept, what was ruled out and why, and a pointer to the
spec as the live document. Its own text is kept verbatim (checked, not assumed: the original body is present
character-for-character below the banner, and the H1 is untouched); the page also gained the description it never
had. The other three research pages owing banners are untouched and still stand as candidates.
3. The handoff inbox gained the brief, at the top, where OpenCode will read it.
from-hermes is now 12 notes, the newest being the ordering-views build brief with Status: OPEN; the header
was re-counted, the owed list rewritten as six items with the brief first, and the remaining items renumbered. The
pass was verified by reading the page back: the new note, its status line and the two open questions are present, the
previous newest note is intact, and the only lines that changed are the header block and the renumbered list —
no note body was lost.
4. One asset run, and nothing else touched. Two renders of the same view were uploaded into a new asset folder
pharmacy-ordering-views (v1 superseded, v2 embedded in the spec); both were uploaded with the shipped
upsert-and-clear-cache script and byte-compared after download (HTTP 200, identical). The mockup carries dummy
data only — no real wholesaler figures, no credential, no internal address beyond the VPS designations the wiki
already uses — and is marked on its face as a design mockup that is not built.
5. Candidates recorded, not acted on (still his to name, one at a time): guide C6's stale first figure and the
demo-seed limitation the build's verification found (both in item 7 below); the three
research pages still owing dated banners; the two pages quoting the old header row; the parked C3 item; and the
resume-note prune (agents/handoff/hermes-resume, sessions ≤ 45) — which this session has made slightly larger.
6. The build came back the same session, and it was verified live by me — 39/39 — before it was accepted.
OpenCode's session-52 handback delivered the ordering view (f05fc3f) and the two small items (44d8595). I verified
it on the deployed app, not from the note: a new committed verifier (veripath-ops 7e2be34) drove the real
tenant through the real OIDC login and passed 39 checks with none failed — the four-offer header and the cap (four
cells a row, none blank), the offers naming their own supplier, both money columns with a loss shown rather than
hidden, no "short supply" and no £0.00 anywhere, the basket bar, the strip, the search, the dashboard pointer,
Technician 200 and the Counter Assistant 403 — while the on-order figures were cross-checked against the app's own
orders list, per item, and all five matched. Suite on my own run: 645 tests OK (230.6 s). Deploy confirmed from
the running container (MAX_OFFERS = 4, main.wide, migrations ending at 0014, so no migration), and no
regression on the screens that kept the parked column set. The handback note is marked HANDLED with the record;
the spec page now carries the as-built block.
7. One real documentation defect the change created, found by checking the guides against the screen — and it is
mine to fix, not OpenCode's. Guide C6's first figure is /pharmacy-app-guides/c6-order-build.png, uploaded
2026-09-21, ten days before the ordering view existed: the guide's text now describes the new screen while the
picture beside it still shows the old one. Recorded as a candidate for the asset work (the figure wants
re-capturing against the live screen), together with the demo-seed limitation the same pass found — the tenant holds
7 suppliers, 3 without a price file, so the live +N more offers not shown count cannot be exercised until two
more suppliers are priced.
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round's
housekeeping value is one inbox note closed in place and one page amended so it stops stating a rule the build
does not follow; the session's substance is on hermes-resume.
1. One note closed in place — no archive pass was needed this round. OpenCode's single OPEN handback (the
register's daily check) was verified live and marked HANDLED 2026-10-01 with its full verification record, and the
inbox header was rewritten to read "Nothing is open either way" (its description updated to match). from-hermes
gained the session 48 note on top and its header was re-counted to 8 notes held — my first count was 9 and wrong,
because the carried dental item has no heading of its own; the header now says so explicitly instead of asserting a
number it cannot support. Nothing went to the archives this round: with one note handled in place, an archive pass
would have been churn, and the September/October month pages were already split last round.
2. The daily-check page was amended rather than left contradicting the build — the failure this class of
housekeeping exists to catch. The spec's §5 ("no notification … not on failure") and §W4 (refuse a file whose
shape has changed) are not what was built: the task calls health.notify() on a problem attempt, and the only
refusals are "not an xlsx / no licence rows", per Matthew's later "we just update in accordance with what the
register says". Rather than quietly rewriting his ruling, the page now carries a dated As built and verified block:
what was proved live, the two deviations with the evidence for each, and the plain statement that both are his to
settle — with the measured consequence of the relaxed gate spelled out (a re-ordered export imports with the columns
swapped). His wording stays his wording until he changes it. The status line now flags the two deviations rather
than a clean DRAFT.
3. Nothing was redacted this round, and no credential came near a page. The new text carries no credential, no
internal address beyond the VPS1/VPS2 designations the wiki already uses, and no tenant or supplier data. The demo
supplier created last round (id 17) is unchanged, and the C7 figure-recapture item still stands.
4. Candidates recorded, not acted on (unchanged from Round 26, and still his to name, one at a time): the four
research pages owing dated assessment banners; the two pages still quoting the old header row; the parked C3
item; and — re-flagged this round, and now the biggest single file I maintain — agents/handoff/hermes-resume is
~185K with 28 session blocks and wants the prune the housekeeping log had: sessions ≤ 45 to
hermes-resume-archive, keeping the cold-start rule, the "do not re-discover" list and the "read first" order intact.
Flagged rather than done, because the session that writes the block is the wrong one to cut it.
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round's
housekeeping value is one archive pass and one new page placed correctly; the session's substance is on
hermes-resume.
1. One new page, under the existing build-records node — no new nav node needed.
The Supplier Register — MHRA WDA Licence Data
(page 674, tree 650) specs bringing the MHRA wholesale dealer's authorisation register into the platform: the
parse contract measured from the real file (six header rows, 1,757 records, 2,902 sites, 1,754 distinct licence
numbers, the three letter-O typos), the registry tables, the holder-then-site lookup, one supplier record per
site with the trading name as the label, and warn-never-block for a wholesaler not on the register. His
Q2 ruling is recorded on it with the source's own clauses: publish for the purpose the register serves, labelled;
closed, and not to be reopened. The scheduled update is parked at his instruction, and scope is now the
import alone — so the lookup, the supplier fields and the schedule are documented as deferred rather than
half-built. The pharmacy index's pick-up paragraph points at it; the stdlib xlsx reader written against the file is
committed as veripath-ops 7a1b8f3 (scripts/wda-xlsx-dump.py) for the build.
2. The handoff archive pass — the page had become congested in four distinct ways, all fixed.
from-hermes had grown to 67.8K: a note duplicated verbatim (the Linux-launcher note, two copies, eight
identical paragraphs each), the same note's tail carrying ~5KB of stale header/open-list matter, three
separate stale count/list blocks, and — because a previous pass had appended an inline archive to the live page —
13 handled notes sitting on the inbox instead of in an archive page, three of which were open notes below an
"Archive" heading where nobody would look. from-opencode had 11 notes, every one HANDLED or ANSWERED, and no
archive page of its own in this month. Applied: 13 notes moved off from-hermes (12 handled — the rig
informational note, the launcher/allow-list/rig verification, the §11 answer, the rig GO-AHEAD and plan of record,
the launcher addendum, the satellite-view verification and build brief, the stage-2 findings, the stage-2 GO-AHEAD,
the two small asks — plus the verbatim duplicate, kept once on the live page and recorded as the duplicate it
was) and all 11 off from-opencode, into the new month pages
archive/2026-10/from-hermes (13 notes) and
archive/2026-10/from-opencode (11 notes), each with a dated
pass line. The live inboxes are now 5 notes (4 OPEN, 1 partly closed) and nothing open, with the standing
rule and pointers kept. The month split was itself overdue (the September pages had reached ~364K / ~260K — the
flag carried since Round 22), and both October pages were created before the inboxes were rewritten, so every
link resolves.
3. One real content problem found while moving, and rescued rather than lost. The stale open list (dropped in
the pass) carried item 9, the dental CHANNEL_LAYER_URL defect (2026-09-24) — and the note it summarised is on
neither the inbox nor either archive, so the item would have vanished with the stale list. It is now carried
explicitly as item 6 on the live inbox, with its state re-checked 2026-10-01: the dental app's
dsp_clinic/settings.py still defaults CHANNEL_LAYER_URL to a redacted placeholder (line 262), so the deployment
must override it. Its companion tooth-click item is likewise not on this inbox, and the note says so.
4. Verified in the rendered pages, not the rows. from-hermes 5 headings, from-opencode 0,
archive/2026-10/from-hermes 13, archive/2026-10/from-opencode 11; moved note text is absent from both
live inboxes and present in the archives; no stray ==== rules or second Page: lines were carried in; and the new
month node is already a folder (--folder reports id 651, so its children show). Nothing was deleted at any
point — the whole pass is archive-and-rewrite.
5. The register's build came back the same session, and both halves were verified live by me and accepted.
OpenCode imported the register into the platform store (one release, wda-2026-09-30-f33878ba, live: sha256
f33878ba…, 149,703 bytes, 1,757 licence rows / 1,754 distinct numbers / 2,902 site rows, the register's own
Total Records line and its typos verbatim, Great Bear's four sites) and — at Matthew's direct request, so a
scope change on the day — added the supplier-screen lookup with the licence number, trading address and postcode
stored on Supplier. My pass: the store read back to every acceptance number, a re-run of the same file printed
already held — no-op with the store unchanged, the lookup's service layer answered live (search by holder / site
name / postcode / licence number, the 25-hit cap, too_short, warn-never-block), and the screen was driven over
HTTPS as the Business Manager: panel, source line, search, prefill and save all behaved, with the three fields
reading back on the supplier's own page. One supplier was created deliberately at his instruction — id 17,
Stirling Anglian Pharmaceuticals Limited (UK WDA(H) 42582, Glasgow) — and left ACTIVE so the supplier screen and the
register panel can be used for guide figures; that is recorded as a figure-recapture item for C7. The handoff
inbox is now clear on both sides (nothing open either way), and the register's daily check was written up as
its own page (the Daily Check)
with no notifications or change reports in it, by his ruling that the register's "purpose is for looking up
licensed suppliers and nothing else".
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round's
housekeeping value is the records this pass wrote and the two pages it added; the session's substance is on
hermes-resume.
1. Two new pages, both placed in the tree that owns them, both checked in the sidebar. Under
development/projects/pharmacy_app/testing/ (the folder node is isFolder, so both children show):
Exercise 1 — Portal 1's Price File Into the App
(page 672, tree 648) and
The Satellite on a Linux PC — the Launcher
(page 673, tree 649). The exercise is a working document — amended eight times in one session as Matthew read it
and as findings landed — and the launcher page carries the verified script and the two traps that cost him a run. Both
are linked from the portals page and from each other; the exercise also appears in the section's Related lists.
2. The rig, its spec and its page carry the codes fix. The Test Rig (Build Spec)
gained §12.1 — the finding (the rig's files matched none of the app's items, so they read and mapped and landed
nothing), the change (the catalogue now uses the app's own item codes; veripath-ops c226b73), and the verification
(11 rig tests, 88 / 22 / 16 live checks, 16 of 16 rows matched read-only). The practical page gained the matching
paragraph, and all six of its pictures were re-captured and replaced, byte-compared after upload, because they show
the portals' tables.
3. C8 was amended, dated, in the guide's own voice. C8
now names both launchers and which PC uses which, states that the exported file must be in that PC's own Downloads
(OneDrive/redirect caveat included), adds the Linux/Wine and stuck-terminal rows to If it doesn't work, and replaces its
"a first real run on Windows is still outstanding" paragraph with the run that happened (2026-09-30, Linux, Ready).
The unbuilt automatic price-file route keeps its dated NOT YET AVAILABLE block, which is still true.
4. The handoff inbox took two notes and a repair. from-hermes gained the packaging note (the package ships only
a Windows launcher; the PC is Linux; three items) and the GO-AHEAD brief for the stages 3–4 / I1–I3 build, and its
header was re-counted to 18 notes held — 13 OPEN, 1 partly closed, 4 HANDLED. Repair: my own earlier edit had left
the count paragraph and open list below the newest note; they are now back under the title, and the open list runs
0–12 with the new item as 0 and the rest shifted. from-opencode still holds nothing open.
5. Nothing was redacted this round, and one credential rule was exercised. The rig's sign-in pages print their own
demo account and the wiki says so without copying the value (Round 24's redaction stands). The walk's app-side logins were
read from the host-local demo credential file; no credential, address or internal hostname entered any page: the two
new pages name only the public portal addresses the test-environment page already carried.
6. Candidates recorded, not acted on. (a) Three research pages still owe dated assessment banners (research/csv_format,
research/supplier_csv_files, research/selenium_gadget_supplier_portals) plus the fourth added in Round 24
(research/discounts_test_portals) — his to name, one at a time. (b) Two pages still quote the old header row.
(c) The supplier record for Synthetic Wholesaler (DEMO) carries an evidence sentence that is no longer true
("this portal does not exist and is never contacted") — an app-side edit, offered to Matthew, not a wiki task.
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round's
housekeeping value is the records this pass wrote; the session's substance is on
hermes-resume.
1. One new page, placed in the tree it belongs to, at Matthew's request.
The Synthetic Wholesaler Portals — the Test Rig (Build Spec)
(page 670) was written after his go-ahead for a synthetic portal of our own: the four basic portals mapped to the
four existing demo suppliers, four databases, the two shapes a discount file can take plus churn, and §10's four
questions for him — so the answer no longer lives only inside the two open scoping notes. It went up as a leaf under
the existing build-records tree; no new nav node was needed, and it appears beside its siblings in the rendered
sidebar.
2. Three records updated in place, so nothing contradicts. The pharmacy index's pick-up paragraph now reads his
synthetic-portal decision as answered — a published plan with four questions outstanding, not a decision he still
owed. from-hermes: the plan note went on top, the header was re-counted (12 notes held — 7 OPEN, 1 partly closed,
4 HANDLED), and the open list was renumbered so the launcher note is 1 and the new note is 0. hermes-resume
carries the session block.
3. One new housekeeping candidate, recorded not acted on.
Discounts & Test Portals is a fourth research
transcript owing a dated assessment banner, alongside the three already owed (research/csv_format,
research/supplier_csv_files, research/selenium_gadget_supplier_portals). It is ~66K, carries no description, and
holds two mutually exclusive positions — that short-liner files give baseline prices (so a discount matrix should be
layered on top) and, later, that short-liner files are already pre-discounted net files — plus a stack it is not written
for (Frappe/ERPNext doctypes, a satellite that scrapes rows and holds them in memory). The spec's §5 records the
substance; the page itself is Matthew's to name, one at a time, as always. He added a further section to it on
2026-09-30 — No CSV download facility — and the spec now carries both halves of that: P4's three no-download
states, and the section's scrape-the-table-into-PostgreSQL answer ruled out as a break of three standing rules
(§4.1 and §7).
4. The round closed with the build authorised, so no record says otherwise. Matthew answered the spec's §10 the same
session — anyone in the VeriPath network may reach the rig, and all four portals are built in the first pass — so
the go-ahead brief went to the top of from-hermes (the plan note marked AMENDED, its read-don't-build
instruction lifted) and the index's pick-up paragraph and hermes-resume were brought into line. A no-portal scenario
was ruled out by him; §4.1 carries the ruling rather than dropping it silently. OpenCode read the go-ahead and asked
two questions before building anything (rig host and names; allow-list mechanism) — both answered by me the same
session as the spec's §11, since neither is Matthew's to answer: VPS1 /opt/synthetic-portal-rig in its own
network, dedicated Postgres, and the domain-suffix allow-list. from-hermes gained the reply (14 held — 9
OPEN) and OpenCode's note is marked ANSWERED.
5. The rig's build came back and was verified live by me the same session (Round 24 continued). The record is the
spec's §12; the round's housekeeping value is three things. (a) A credential was found on a wiki page — the
handback note carried the rig's dummy login — and was redacted in place with a marker saying where it belongs (the
rig's own notes). A synthetic credential is still a credential; nothing else in that note was touched. (b) The inbox was
repaired where my own earlier edit had left two items numbered 0. on from-hermes; the open list now runs 0–9
(15 notes held — 10 OPEN, 1 partly closed, 4 HANDLED). (c) The rig's source was found to be in no version control
while its delete.sh removes its directory — irreversibly, for a test asset the next two stages must both use — so it is
now committed as veripath-ops/rig/synthetic-portal-rig/ with a RESTORE.md, and the delete-and-rebuild was proved
end to end.
6. The round ended with the rig published (session 45, same round). Matthew asked why the portals were not simply
hosted and added four subdomains — portal1 to portal4.veripath.co.uk — so the rig now has an nginx front door on
443 with one certificate for the four names, while its own port stays internal. Records brought into line rather than
left contradicting: the spec's §7, §10 Q1, §11.1 and §12, the pharmacy index's pick-up paragraph, the rig's
README (hosted addresses), and the versioned copy in veripath-ops (9cb0818). The hosting decision is
Matthew's, and the spec now says so with his reasoning — the page no longer carries a rule the rig does not follow. 7. A new testing/ sub-directory and its practical page (session 45, same round, at his request):
The Synthetic Wholesaler Portals — the Test Environment (page 671) went up under development/projects/pharmacy_app/testing/ — a genuinely new nav node, checked as
isFolder (id 646) so its children show — carrying the addresses, what each portal does, the state and churn commands,
the three verifiers with their counts, and what the environment is not. The spec stays the record; the index, the
spec's Related list and hermes-resume all point at it, so it is findable from the section front page and not only from
the sidebar.
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round's
housekeeping value is the four records this pass wrote; the session's substance is on
hermes-resume.
1. The spec carries its acceptance record now. satellite-view-setup: the status line flipped from "Nothing is
built" to built, deployed and verified, and a new §9 — Verification (live, Hermes, 2026-09-29) (~6K) records the
revision (d11cbd0, the deployed revision confirmed on the app host), migration 0011, the 96 live + 9 host-side
checks, the switch both ways, the download taken by a real browser, the launcher, the roles, W9's honest caveat, the
footprint, and the one correction to the build note (the tenant holds two DEMO configurations, not three). Only the
status line and §9 were touched — the work items, the never-do list, the tests and the questions are as they were.
2. Both handoff inboxes updated in place — no archive pass this round. from-hermes gained the pass's note (top) and
its header was re-counted (10 notes held — 5 OPEN, 1 partly closed, 4 HANDLED); the satellite-view brief below it is
now HANDLED. from-opencode's newest note is marked HANDLED with the verification summary. from-opencode is
now ~41.6K and from-hermes ~37K — both still comfortably readable; the from-hermes archive page (~360K) is still
the one that must be split at the next September pass, the flag carried forward from Round 22 rather than acted on.
3. The pharmacy index was corrected where it would have contradicted itself. Its pick-up paragraph now leads with the
satellite view (d11cbd0, 592 tests green) and names the Windows pass, the synthetic-portal decision and the inbound
scoping note as what is left; the PHASE 8 block below it is now introduced as "before it, the last verified build".
4. New rig scripts in veripath-ops (not wiki work, recorded because future passes reuse them):
verify-satellite-view-live.py (96 checks — the live logins, the view in a real headless Chrome, the browser download,
the switch, the launcher), verify-satellite-view-admin-host.py (the platform administrator), recon-satellite-view.py
(the expectations read read-only out of the tenant database over ssh), satellite-launcher-linux.sh (the Linux
equivalent of run-satellite.bat) and wiki-page-body.py, which extracts a page's markdown from a --get — use it
for every future page edit instead of hand-trimming the wrapper.
5. One new guide page, published at Matthew's request after the verification: C8.
Stock — Set Up and Run the Order Satellite went up
as a leaf under the existing folder (no new nav node; the folder node guides/pharmacy_app_guides was already
isFolder, id 452, so the sidebar needed nothing), with two illustrations built from the app's own template CSS and
synthetic data (c8-satellite-view.png, c8-satellite-report.png, asset ids 171–172 — the console picture deliberately
omits the report's first line, which names the pharmacy's own web address). It is listed on the section index and marked
Published (2026-09-29) on the coverage plan, whose description was updated from "complete to C7" to "complete to
C8". The guide carries a dated NOT YET AVAILABLE (checked 2026-09-29) holding note for the unbuilt automatic price
download, per the house convention, and writing it surfaced one finding — the launcher runs the program before the user
can log in and open the order, so the first run always ends in the no-page sentence — which went to OpenCode as the new
top note on from-hermes rather than into the guide as a defect.
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). The round's
housekeeping value is one archive pass and one new page placed correctly in the tree; the session's substance
was the satellite work, which is on hermes-resume.
1. The from-hermes inbox pass (maintenance, 2026-09-29) — 16 notes → 7. 9 HANDLED notes moved verbatim, newest
on top, to agents/handoff/archive/2026-09/from-hermes with a dated pass line: the file contract and its two ruled
decisions, the 2026-09-28 live-pass trio and its two build briefs, the stock item page's raw cell dict, the phase-8
build/closure pair, and the 2026-09-22 stage-4 verification. The inbox now holds 6 OPEN + 1 partly closed — the
stage-2 go-ahead, the two small pharmacy asks and the four non-pharmacy items. Verified against the rendered pages,
not the DB rows: the inbox shows 7 <h3> (was 16) and the archive 104 (was 95), the moved notes are present in
the archive, and the stale "(latest, still)" on the archive's top-note heading was removed in passing.
A baked-in wrapper footer (a stray rule line and a second Page: line) sat on the oldest moved note; it was
stripped as the note moved rather than copied into the archive — the artefact hazard again, now recorded in the pass
line itself. The archive page is now ~360K characters: the next September pass should start a second page for the
month; the 2026-09-27 pass flagged the same threshold and this pass carried the flag forward rather than acting on
it, so it is worth doing for real next time.
2. One new page, correctly placed — and nothing displaced to make room for it.
Supplier Price Downloads — the Automated Route (Scoping Note)
(DRAFT, awaiting Matthew's review) is the inbound mirror of the supplier-portal scoping note: bringing a
wholesaler's price file into the pharmacy's own database automatically, in stages, with the manual upload kept beside it
for good. It was written in the section's established shape and placed as a sibling leaf under
development/projects/pharmacy_app/ beside supplier-portal-interaction, so no new nav node was created and no
sidebar work was needed. The module map, the pharmacy index and the neighbouring specs were read but not edited —
the index's "next session" paragraph and the module map's deferred wholesale and procurement ordering row will want
updating only if Matthew authorises the route, and the same rule applies to the three research pages that owe dated
assessment banners (research/csv_format — the page he first pointed at — research/supplier_csv_files and
research/selenium_gadget_supplier_portals).
3. A root-level stray noticed, deliberately not touched. pharmacy_app_selenium_gadget (~69K, no description)
is the original research transcript for the whole satellite programme and overlaps
research/selenium_gadget_supplier_portals heavily. It is a Phase 3/4-class root stray (it has no en/ prefix and
no home in the tree), so it belongs to a pass that is named for it — recorded here rather than acted on during a
handoff pass.
4. The stage-2 verification (same session, the round's substance). Stage 2 was built by OpenCode (4e947c8,
origin/dev = deployed, checked on the app host) and verified live by me on the Linux rig: my own suite run
(562 tests, OK, no skips) and every state the spec fixes exercised against the real page and the real export
(exit 2/3/0/0/6/7, the six-row report, the state file's four facts, the read-only CDP surface, no Django import either
way). Two findings went back as the new top note on from-hermes: the stale rule compares the file against the
page load while its sentence claims the export (so a page reload makes a fresh export read as stale — a stage-3
blocker, harmless now), and a doubled full stop in the waiting sentence. The mapping pre-select is verified live
on all four supplier files (and the tenant held 9 price files before and after, so the confirm step records nothing);
the seed minimum is in the code but not on the live tenant, deliberately — a re-seed would re-import Two's example
file dated today, which shares a price date with Matthew's own Two file, so it would move the live/superseded pair the
guides quote. Record: §12 of the stage-2 spec. One write attempted and refused (the supplier screen, twice,
nothing written) — the list was read back unchanged, and the failed attempt is recorded rather than hidden.
5. The contract-2 pass, verified the same session. OpenCode's second pass (87cc874: e9f9ff8 + 87cc874) fixed
all four items and I verified them live: signal contract 2 carrying the order's export time (so the stale sentence
is the check actually made — the reload case is accepted now, a back-dated file still refused), one full stop in the
waiting sentence, and the demo order-value states pinned in the seed (Three £150, every demo item's cached position
pinned, DEMO-FOUR-001 for the no-minimum control, plus a test). My own runs: verify-pharmacy-order-value.py
50/50, verify-pharmacy-price-ordering.py 26/26, 565-test suite OK. The contract's version 2 is recorded in
§6 of the stage-1 spec (the contract's home) and the verification in §13 of the stage-2 spec. Two of my own
records corrected rather than left standing: the price-ordering verifier repoint was never needed (it keys on the
price's label), and my "a waiting order carries no export time" assertion was wrong (a cancelled order that was
exported keeps one). Flagged for Matthew: the price seed now resets every file for the synthetic suppliers, which
removed his four imported files from the demo tenant — correct for a demo re-seed, but the help text should say so.
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D, the home/ strays, the DSPT merge read and the password rotation all still stand). This round was
the live import test of the four per-supplier price files: Matthew ran it in the browser as the Business
Manager, with me guiding and reading the tenant read-only either side.
1. The test ran, and the comparison works. Counts exactly as predicted (A 8/6/1/1, Two 5/5, Three 5/5,
Four 2/2); price files 5 → 9, price rows 11 → 28, audit 337 → 342, position cache untouched at 6. The
best-price column moves between suppliers (Two on amoxicillin £2.10, ibuprofen 400 £0.79, aciclovir £3.40,
methylphenidate £24.50; Three on salbutamol £3.58; A alone on typhoid £14.60) and now sits beside the NHSBSA
figure on the Low / zero stock report and on each item page. Full record on
hermes-resume.
2. A real defect, found by guiding rather than by testing. The mapping screen pre-selects nothing:
suggest_columns() keys its dict code/price/description/pack while the form's fields are
code_column/…, so the form-level initial never reaches them. No test covers the wiring — tests.py only
exercises the pure function — which is why my own earlier note claimed the hints pre-select. Handed to OpenCode
as an OPEN ask with its acceptance case, and my record corrected on the resume note.
3. The date trap, and two deviations accepted. The upload step's date box defaults to today, and the date
entered decides the supersede and the freshness state — so Three and Four went in dated 29/09, their seeded
rows were not superseded, and the stale / one-working-day freshness states are absent from this run. Matthew
accepted the run and moved on. The reset is the only undo (no per-file delete in the app); it was run twice
this session, verified back to five files / eleven rows between runs. Lesson recorded: when handing a file to
an operator, state the price date to type as well as the file to upload.
4. Next session — the Selenium satellite gadget, at Matthew's request: the order-file/portal half, still
blocked on the test portals he intends to build. Nothing is running; the only uncommitted file anywhere remains
the pre-existing assign_roles.py edit, which is not mine.
No tidy-up phase advanced (Phase 4 remains IN PROGRESS — section B is the content question from Round 18;
sections C/D and the home/ strays, the DSPT merge read and the password rotation all still stand). The session was
import/export work ordered ahead of the satellite, as Matthew set it, and it touched four wiki records.
1. The file contract, settled by Matthew (2026-09-29) and recorded. An order file is a flat two-column grid —
code, then outer packs — comma separated, no header row, no supplier id (the portal knows both parties from the
login), one pack size per row, quantity always whole packs; the default is to be the "Standard UK Wholesale Export".
He also confirmed the general price-file anatomy from the CSV Format
research page. Three research pages in this class now owe a dated assessment banner — that one (2026-09-29), plus
research/supplier_csv_files and research/selenium_gadget_supplier_portals, both of which lost their banners to a
re-upload on 2026-09-28 and now overlap heavily (~82k / ~70k). Reinstating all three is the next content job on my side.
2. Written and measured — the first synthetic price-file set. Four files plus a README.md in
pharmacy_stock/sample_price_files/ (a new folder, so the seed's own example_price_files/ list is untouched), read
through the real importer read-only on test-client-pharmacy: five-column portal export (read 7 — 3/1/3),
renamed-header .tsv with a decoy price column and a leading-zero-lost code (read 4 — 2/2), a headerless file
(read 2), and a semicolon file with decimal commas (read 3, every price ×100).
3. Two of my own records corrected, because they were wrong and both were load-bearing.
(a) I had written that the price import refuses a headerless file. It does not: read_file takes the first
non-blank line as the header, so a headerless file's first data line is eaten and silently lost (a three-line file
reads as headers=['0123456','1.20'], rows=2, clean pass), and the "No header row was found in the file" error
fires only for a file with no non-blank line at all. Corrected on
hermes-resume, in the OpenCode note, and here.
(b) My rule "no headerless order sample may ever be created as an app output" is reversed: headerless is now the
standard default output. Recorded so it is not later "fixed" back.
4. Handed to OpenCode as the top note on from-hermes — the headerless export
profile (label, headers: [], a conditional header write, three tests) and the reversal; the decimal-comma fix (a comma
is a thousands separator only in threes; any other shape refused with a named reason); the mapping-hint normalisation
(PIP_CODE / TRADE_PRICE pre-select nothing today because the match is exact). The two decisions he left open were
delegated to me at the end of the session and are now RULED (a later note the same day): the headerless import
route is built as an operator-declared mode — the mapping screen gains the "this file has no header row" choice
with the code and price columns chosen by position, a suspicion prompt fires when the first row carries no
alphabetic cell, the mapping is recorded positionally so the shape needs one confirmation next time, and the app never
decides and never drops a line silently; synthetic-no-header-positional.csv is the acceptance fixture (must read 3
lines, not 2). Reasoning: the mapping is stored as column names, so a positional mapping is a model question rather
than a parse tweak, and making the operator the detector converts today's silence into a named consequence.
5. The round closed the same day — built, deployed and verified. OpenCode built the brief
(feature/pharmacy-session-20260929 @ 085f7ba, migration pharmacy_stock.0010) and I verified it live:
the export read off the wire (headerless, one line per order line, filename unchanged), the parse rules and the hints
replayed read-only, the headerless import mode driven to the confirm step with nothing written, the deploy's
own tenant migration confirmed (migrate_client --plan → No planned migration operations), the partial state
measured live and the superseded wording read. My own run of the full pharmacy suite: 530 tests, OK. Record:
The File Contract;
verdict: from-opencode.
6. Two things I fixed or found while verifying, and one hazard closed.
verify-pharmacy-price-headerless-live.py (12/12) covers the headerless mode, the listveripath-ops (3e67a9b).from-hermes): theorigin/dev sat 23 commits behinddev would have silently reverted the whole contract. I fast-forwarded dev to085f7ba — dev = deployed. (Worth checking after every handback: a deployed feature branch with dev7. The live import test set written and handed over (same day, continuation). Matthew asked for real test files
rather than more fixtures: one typical wholesaler price file per demo supplier, so that importing them shows the
suppliers' prices compared — and he deferred the order-file/portal half until he has built test portals himself.
So: pharmacy_stock/supplier_price_files/ (new folder, sibling of the edge-case sample_price_files/) with four
files in four different header styles plus a README.md carrying the price matrix, the counts to expect and every
consequence of importing; committed fa0de11 to dev and the feature branch, both hosts level, app host clean. A
delivery tarball (/root/supplier-price-files-20260929.tgz, sha256 recorded on the resume) went with the reply. The
files are designed to compare and to preserve the demo: Two cheapest on most lines, Three on salbutamol, A alone on
the typhoid vaccine; a blank price (rejected) and an unstocked line (unmatched) are deliberate; Two's £0.79 and
Three's £1.05 were left unchanged because the live ordering verifier keys on them, and A deliberately gains a
second live file (the app's own latest-live-file-wins rule), which is the one thing its stale-price check will
need repointing for. The handover's first task is therefore: read what his imports produced, then re-check that
verifier. Nothing is running; the only uncommitted file anywhere is the pre-existing assign_roles.py edit in the
pharmacy working copy, which is not mine.
No tidy-up phase was completed this session. The work was the supplier-portal stage (see the cold-start block on
the resume note); the housekeeping value is that two pages already in the
development/ tree were kept honest and one new page took its place in it.
1. Published — Stage 2: the Satellite Skeleton
(build spec, 19.5k), into the existing build-records/ folder beside stage 1 — the folder already carried 17 pages, so
no new nav node was created and no sidebar work was needed. It is written in the section's established shape: what the
stage is and is not, decisions taken, what exists today, the measured findings, work items W1–W7, the fixed report
sentences, what it must never do, tests, and the live verification plan.
2. Corrected — the scoping note. Its
open-questions table still said question 2 (the pilot platform) blocked stage 2, which stopped being true the
moment Matthew settled the rigs. Row 2 now records what is settled (the Linux host first, then his Windows PC; the
fleet expected to be Windows) and what is not (the pilot's browser; the pilot IT's position on the debugging port,
which is a stage 6 item). The stage-2 spec was added to its Related list, so the scoping note is once again the
place a reader can see the whole stage from.
3. Recorded for a future pass — a stale path in a pinned skill. The veripath-wiki-assets skill documents
/root/.cache/puppeteer/chrome/linux-148.0.7778.97/chrome-linux64/chrome for headless rendering; that path does not
exist on this host, so the documented screenshot command fails as written. The working browser is Chrome for
Testing 152 at /root/.agent-browser/browsers/chrome-152.0.7977.42/chrome. The skill is pinned, so this is a flag
for Matthew rather than a patch — but any future asset pass will hit it.
5. Guide C7 published — group C (stock) is complete. Stock — Manage the Supplier List
went up the same session, written against the supplier record as it now stands (it carries the portal fields verified
that morning), with the coverage plan's C7 row flipped to Published, the group-C closing line updated and the row
added to the guides index. Three figures were built from the app's own CSS block and
uploaded to the pharmacy-app-guides asset folder. As the series requires, C7 now stops and waits for the reviewer to
walk it; the next item is his to name, not mine.
Unchanged: the phase tracker above (Phase 4 IN PROGRESS, section B a content question; Phases 5–9 pending) and
every open item on it. Section B, the home/ strays, the DSPT merge read and the password rotation all still stand.
No tidy-up phase completed, but section B's shape changed on measurement and two of its items closed: the two
redundant archived copies are marked superseded rather than deleted, and section B is now correctly scoped (below). This
round also carries the session's deliverable: the deferred walkthrough.
1. The walkthrough exists — Pricing & the Reimbursement Comparison
(five stops, one sitting): the price-file import, the prices on the working screens, building an order from them, the
minimum-order advisory, and the finish — what the pharmacy paid beside what the NHS reimburses, with the rule named
on screen (£1.30 less 20% = £1.04, bought at £2.10). Deferred by Matthew 2026-09-22 ("once we are done with
the development work"); written now because the development work has stopped moving. Every figure was read off the live
test client the same day, and the pricing plan's "How this ships" paragraph now points at it instead of at the deferral.
One defect fell out of writing it, now with OpenCode: the stock item page prints a raw cell dict
({'key': 'reimbursement', …}) under Your price — one template field ({{ info.cells|last }} → .text).
2. Section B, measured — the finding that matters. The policies/ pages are not merely badly named. Three of them
carry pasted model reasoning instead of policy: policies/standard_5 is a meta-answer ("The provided context does
not contain any information about Standard 5…"), standard_7a ("Based on the context, Standard 7: Continuity
Planning is referenced [1] but detailed evidence requirements are not provided in the snippets…") and standard_8 open
the same way — an AI's scratch answer saved as a compliance page. standard_1a's content is a Staff Confidentiality
Agreement, which is not what its title says. Meanwhile the published versions of the same policies exist under the
website's content/policies/*.md (BCP, IT protection, encryption, patching, unsupported systems, accountable
suppliers, incident response…), which is what a reader is actually shown. So section B is a content question first and
a naming question second: recasting the page titles before deciding which text is authoritative would dress up drafts.
3. Decisions taken (Matthew delegated this on 2026-09-27).
Naming convention: hyphenated paths with human titles (standard-4-jml-process-and-access-reviews) — the newest
pages already do this (standard-1-access-control, standard-2-data-security, standard-10), and the underscore set
is the older import with bare titles. Applied to titles only, in the same pass as the content cleanup, because
renaming paths means a full referrer sweep and there is nothing to gain from moving a page whose text is not yet
authoritative — the standard number is the stable identifier either way.
The two redundant archived copies: marked, not deleted. archive/security/hetzner_storage_box and
archive/en/policies/standard_6 now open with a dated "superseded by " line. Archive-not-delete holds; the
copies stay searchable and nobody is misled about which is live.
Standard 3 and Standard 7 pages: not to be invented. They must be written from the DSPT's own standard text (or
the matching published policy), which is a content-authoring job with Matthew's compliance content — not a wiki task.
The two DSPT set families (the compliance/governance-compliance/dspt-standards/* pages and the smaller
infrastructure/compliance/dspt-standards-* set) get their own session: they are not duplicates (similarity
0.36–0.42), so merging them means reading ~100K characters and deciding which text is authoritative — the same
question as item 2, and it should be answered once, for both families.
The home/ strays are one move each and were not moved this round: home/partner-user-management-plan-phase3 →
development/projects/gp_booking_app/partner_client/ (where its two sibling pages already live) and
home/wireguard-vpn-setup → infrastructure/. They wait for the referrer sweep that every move in this log is
required to have, and they are cheap to do together with the next structural pass.
One slip of my own, recorded because the log warns about exactly this: the first write of the pricing plan's
new paragraph went out with the reader's wrapper baked in (I redirected --get without stripping it through
wiki-strip.py). /root/work/wiki-clean-artefacts.py caught it on the report-mode pass and removed the header and
footer blocks (602 chars); the stored body is verified clean and the page renders with no stray Page: line. The
habit the log already states — strip before writing — is the fix; the tool is the safety net.
4. Phase tracker: row 4 stays IN PROGRESS — section A done; section B now scoped to the content question above
(titles + authoritative text, one pass) with the archived-copy marks done; sections C/D unchanged.
No tidy-up phase moved (Phase 4 remains IN PROGRESS, section B waiting on its naming/homing decisions). This round
records the verification of the phase-8 build (49bd358) — the client comparison — because the pages that carry it
were written and render-checked here.
DerivedPackPrice rows; the3170926; the central store is untouched (six rowveripath-ops/scripts/verify-client-comparison-live.py (committed 08fec29; the host-side evidence is reproducible in one command with scripts/verify-client-comparison-host.py, 3564277); the advisory rule is structuralroute=manual, operator tc.tech.one and the datemanual 1, changed 0, the queue(DEMO)from-hermes (which also removed the reader's wrapper footer that had been re-appended by the handback write), this/tmp on VPS1 and on the app host hold hundreds of/tmp would be aNo tidy-up phase completed (Phase 4 remains IN PROGRESS). Two maintenance items owed since Rounds 13/14 are closed,
and the repair half of Phase 4 section B is applied.
1. Inbox archive pass — both inboxes. from-hermes 17 notes → 6 kept (5 OPEN, 1 PARTLY CLOSED), 11 HANDLED
moved (76,910 → 24,828 chars); from-opencode 7 notes → 0 (36,050 → 744). Moved verbatim, newest on top, with a
dated pass line in each archive header: agents/handoff/archive/2026-09/from-hermes 84 → 95 notes / 269,143 →
322,779 chars and .../from-opencode 67 → 74 notes / 223,445 → 259,803. Every write render-checked: the live
from-hermes shows 6 <h3> notes with the phase-8 brief on top, from-opencode reads "No open notes", and the moved
notes are present in both archives.
Status: OPEN rather than the **OPEN** form the count wasHANDLED and not OPEN/PARTLY, which is what the predicate shoulddsp_clinic/settings.py:262, and seed_pharmacy_stock still carries onefrom-hermes (it was--get had been warning about) and a duplicated header line in the archive's own pass list — the same repair2. The accumulated wrapper artefacts are repaired on the four pages the section-B audit named — and the audit's count
was low. Measured, then fixed, then re-fetched:
| Page | Removed | What was there |
|---|---|---|
security/security-check-in |
4,188 chars | three nested --get header blocks + 28 footer blocks (the audit said 23 blocks / 2,829 chars) |
security/client-vps-backup-plan |
660 | 1 header block + 2 footer blocks |
security/hetzner-storage-box |
251 | 2 footer blocks |
policies/standard_6 |
178 | the bare-trailing-==== cluster |
All four now report 0 header / 0 footer artefacts on a re-fetch, and the rendered pages carry no stray Page: …
line (checked with curl against each URL — 0 occurrences).
/root/work/wiki-clean-artefacts.py — report mode by default, --apply to clean and upsert with theContent (N chars):, a trailing run of rule + Page: lines) so it cannot eat--get output back/root/work/wiki-strip.py, as this session's pages did); the tool above is3. Phase 4 section B is still open on its decisions, not on repairs — unchanged from the Round 8 audit: the
underscore/hyphen policies/ naming and the missing Standard 3 and Standard 7 pages, the two redundant archived copies
(archive/security/hetzner_storage_box, archive/en/policies/standard_6), and section C's canonical home for the two
DSPT set families. Those are naming and homing calls for Matthew; the artefacts were the mechanical half and are done.
No tidy-up phase moved (Phase 4 remains IN PROGRESS, sections B/C audited and waiting). Matthew opened the session
with "we will be starting phase 8" — the pharmacy application's client comparison, not a tidy-up phase — so this
round records the specification work and the wiki pages it touched. He then approved my measured proposal and delegated
the two open questions to me; both rulings are on the spec with their reasons.
price_view), advisory only andagents/handoff/from-hermes.StockItem to a dm+d pack does not exist, and the shorthand for it in the earlier notes was wrong — the centralcentral-reference-data (row 8 of the phase table plus afrom-hermes (OPEN brief on top; the baked-in wrapper footer --get warned about was removed on thisfrom-hermes keeps handled residue; theNo tidy-up phase moved (Phase 4 remains IN PROGRESS, sections B/C audited and waiting). The session verified and
deployed the §9.1 follow-up, then wrote the records and render-checked every page.
1d36ddf + migration 0013 on the app host; the held VMP file copiedNo tidy-up phase moved (Phase 4 remains IN PROGRESS, sections B/C audited and waiting). The session verified the two
builds OpenCode handed back, and the records were written and render-checked as usual.
no_line state split (Matthew's ruling) is live and the pack-link verifier nowfrom-hermesveripath-ops and pushed (e49051b/d923809 plus this session'sverify-derived-pricing-live.py and the extended pack-link checks)./root/work/leftovers-20260818/, never deleted) and the 07:00 freshness check was rewritten toveripath-ops carries the script (d663cee). The correction that came out of it: dm+d does carry the<CONTROL_INFO> entries and the lookup is already loaded, soNo tidy-up phase moved this session (Phase 4 remains IN PROGRESS, sections B/C audited and waiting; the audit
proposal is unchanged below). The session was the verification of the pack-link build OpenCode handed back, and the
record belongs in the wiki even though no housekeeping moved.
matched by rule, the CPE release imported and not live, the register gated, the adminveripath-ops/scripts/verify-pack-link-live.py.from-opencode is clear again, from-hermes keeps its 5 OPEN notes with the closure record on top, andfrom-hermes); the plan page's phase 6/7 rows were updated to match.from-hermes 51K, from-opencode 20K) of the kind Round 8b moved to archive/2026-09/*.development/projects/pharmacy_app directory reorganised (Phase 7, one section)Phase 7 advanced by one section, at Matthew's direction — the pharmacy_app directory only. The rest of
development/ is untouched and the phase stays IN PROGRESS overall. The trigger was practical rather than tidy for
its own sake: he wants a new data-refresh page for that project (the sequence of updates each source needs, at what
interval, and which of them are automated), and the directory had grown to 32 flat pages mixing live plans, delivered
build records, tenancy/ops records and research provenance.
Moved — 23 pages, by --move, so titles, history and navigation followed:
build-records/ (14): phases 1–6, supply-orders-spec, staff-user-management, the two data-feed specsreference-data-pharmacy-view, reference-feed-status) and the four supply-orders pricing stage specs.operations/ (4): client-realm, client-vps, test-client, apex-tenant.research/ (5): research_1, research_2, nhsbsd_data_api, expired_stock, supply_orders.The top level now carries the live set only: index, overview, module-map, central-reference-data,
supply-orders-part3-pricing and regulatory-content-audit.
Archived — 2 pages, both genuinely obsolete rather than merely quiet:
supply-orders-pricing-stage2-consultation — self-declared temporary ("delete this page once the answers are in andsupply_orders_part2 — an external plan to build the same feature inside ERPNext with a custom Frappe DocType. ItLink repair and verification. A full-wiki referrer sweep (540 pages, enumerated with single-character --list
searches) found 34 pages naming these paths. All were repointed: the 23 moved pages themselves, the six top-level
pages, guides/pharmacy_app_guides/coverage-plan, infrastructure/SIAAS/client_vps/on-boarding-new-clients, the live
handoff pages (from-hermes, from-opencode), agents/handoff/hermes-resume and this log — plus the four archived
pages (agents/handoff/archive/2026-09/*, hermes-resume-archive, housekeeping-log-archive), on the view that a
dead link in a record is rot rather than history. The veripath-pharmacy skill source was corrected too and the skills
mirror re-synced. After the pass a second full sweep found no link left pointing at a moved page's old path.
Two tooling notes for the next structural pass:
(?<!archive/) guard — without it a second run re-prefixes an already-archived linkarchive/archive/…). Caught and fixed on two pages during this pass; the guard is now in the helper.wiki-manage.py --list with no search term returns only the first 50 rows (the tool's LIMIT 50). A genuineNot done, deliberately. No content was rewritten beyond link paths: the pages' text, statuses and decisions are
untouched, and the directory's content-level review (which of the delivered records can be consolidated) stays with the
rest of Phase 7.
11b. Both handoff inboxes archived (maintenance pass, not a phase). OpenCode was due to start the phase 4 build the
same day, so the inbox it reads first was cleared of everything already handled: from-hermes is now OPEN-only — 5
notes / 17,345 chars (was 20 notes / 69,077) and from-opencode is 0 notes / 482 chars (was 9 / 37,590). 24
notes moved — verbatim, newest on top, with a dated pass line — to agents/handoff/archive/2026-09/from-hermes (now
84 notes, 269,143 chars) and .../from-opencode (67 notes, 223,445 chars). Two Status lines were reconciled rather
than copied forward: the superseded hold (Matthew ruled the same day — the live position is the phase 4 note) and
the 2026-09-25 TRUD-archive note, whose still-held item (keep every member of every delivery) is carried explicitly
into that phase 4 note. OpenCode's stale Status: OPEN on the phase-1 build note was corrected to HANDLED — its own
archive header already records that archived notes still reading OPEN are superseded by later ones. The five notes left
open for OpenCode are the phase 4 build, the gzipped-backup FYI with its two findings for Matthew, the ERPNext desk
500s, the dental upload fix, and the two stage-4 demo-seed asks. Flag for the next pass: both archive pages are now
269K / 223K chars — read them in slices, or split the month.
No tidy-up phase advanced (Phase 4, policies + compliance + security, remains IN PROGRESS — section A done,
sections B/C audited and waiting, unchanged). from-opencode was read at session start; the session then ran Matthew's
work in his order, so this round records it for continuity rather than logging a tidy-up.
What the session did (full detail on hermes-resume, session 26):
The two phase-2 handback nits cleared — 82871c4 redeployed to the app host by git bundle, verification recorded
on the plan page; one of the two nits was my misreport (the attempt row was already persisted by the blanket
finally save), corrected in the plan and both inboxes so no false defect sits in the audit trail.
Part 1 of the feed-visibility steer built, deployed and verified live (91b9399) — the per-feed backend status
view: Data Feeds — Backend Status View. The defect it
fixed is worth remembering: the TRUD key check was applied globally, so two feeds with no key and no routine
raised credential alerts that read as faults.
Part 2 spec'd — Data Feeds — Pharmacy View:
D1 answered, and §5.3's read route settled as read-through for v1 on the live finding that the pharmacy app's
default connection is the platform database holding the store.
Tooling defect fixed — relevant to every future housekeeping session. wiki-strip.py (which strips the
--get wrapper before a page is re-written) took the body as the text between the first two rule lines, so any
page whose body starts with a rule — this log, the resume note — was truncated to 1 character, which is how a
patch attempt produced an empty file. It now takes the body from the header's rule to the last rule before the
Page: footer and checks the length against the count the tool reports. Verified on five pages.
Part 2 — the pharmacy-side view — built, deployed and verified live the same night (27dc604 on
pharmacy_booking_app): the published feed data is now displayed to pharmacy staff at /medicines/, read-through
on the pharmacy app's own default connection, with the licence label generated from the live release on every page.
All six spec §5 criteria verified live; nothing handed back.
Two inbox pages carried baked-in wrapper footers (a stray rule line and a second Page: line, 126 chars) left by
earlier upserts — removed on this session's rewrites rather than copied forward. The --get helper now warns when
it spots one, which is how they were found.
Client-side availability confirmed already in place — nothing built. Matthew's closing ask ("create a view so the
data can be viewed by clients") is answered by Part 2: /medicines/ on the client host is public and login-gated,
and client-tenant users get 200 with the label and the search, with the Product information link in their
dashboard nav. A duplicate view would have been waste; the open question for the next session is what he saw and
what he wants beyond it (most likely his stock list with the published pack price beside each item).
Both inboxes are clear — nothing open for OpenCode, nothing waiting on me.
No tidy-up phase advanced (Phase 4, policies + compliance + security, remains IN PROGRESS — section A done, sections B/C audited and waiting). from-opencode was read at session start: its one open note was stage 4 (the order's value and the minimum-order-value advisory) built but not deployed, with the W4 margin question answered in writing — that handoff is what this session finished.
Stage 4 deployed and verified live (record: supply-orders-pricing-phase4-value). feature/pharmacy-order-value (9722205) pushed; the app engine fast-forwarded dev to it; pharmacy_stock.0007 applied on the apex (default and sector_pharmacy) and the client tenant; the three containers restarted — the repo is bind-mounted, so compose up -d --no-build is a no-op and the old code keeps running (a trap now recorded in the build-verification skill). 443 tests re-run by me on the engine. New verifier /root/work/veripath-ops/scripts/verify-pharmacy-order-value.py 49/49 (its --reads-only mode through the boundary wrapper: audit/files/rows/cache unchanged) plus the regressions after stage 1's import — stage 1 35/35, stage 2 24/24, stage 3 24/24. None of the three earlier verifiers needed changing for stage 4.
Two value states the demo data cannot reach. The short advisory had no live state (only Synthetic Wholesaler (DEMO) carries a minimum, £150, and its single line has no price → the screen could only show unknown), so I set £150 on Synthetic Wholesaler Two (DEMO) through the supplier screen and asked OpenCode to put a minimum on a second supplier in seed_pharmacy_stock. The partial state is reachable only by posting two rows into one supplier group (the view's re-grouping contract) — a two-line seeded group would make it a screen state. Both are OPEN asks in from-hermes.
One checker fixed, not the app. Stage 1's verifier still asserted "the word 'price' appears nowhere on the build screen" — a boundary stage 2 deliberately reversed; it now asserts the build screen offers no route to the price import. Also recorded: stage 1's default --price-date 21/09/2026 collides with its own previous run, so its "first import" check fails on a re-run — use a fresh date.
Tenant tidy (never delete): the run cancelled its own example drafts with reasons; the tenant now opens with one exported valued order, one short draft, the pre-stage-3 pair ("Value not known") and the cancelled verification trail — one example per state.
Handoff and pages updated: the stage 4 note marked HANDLED in from-opencode; a new OPEN note in from-hermes (the two seed changes, the W4 answer relocation, the two verifier notes); W4's answer now sits on the stage 4 spec page, which is where the spec itself requires it; the parent plan's status now reads stages 1–4 live and verified.
Paused here by Matthew (2026-09-22, end of session): no further coding until the walkthrough pages for stages 1–4 are done (2026-09-23), then the C6/Business-Manager guide wording; the plan's only remaining code is stage 5 (retention/purge), waiting on his Q2 answer. The resume note (agents/handoff/hermes-resume) carries the cold-start block and the four orders left on the demo tenant. The next housekeeping session can start Phase 4 section B (the DSPT sets, the underscore policies/ set, the home/ strays) — audited and waiting, unchanged.
Phase 4 is now IN PROGRESS (next sections below). Matthew opened the phase; method unchanged — audit → propose → confirm → apply → verify, one section at a time.
The audit reversed my starting plan. infrastructure/security/incidents/ is the documented home of incident pages, not security/incidents/: policies/standard_6 says "each incident page in the wiki (located at infrastructure/security/incidents/<incident-name>)" and the published website policies say the same (content/policies/incident-response-policy.md, responding-to-incidents.md). So the six misfiled reports were the ones that moved, not the two May pages.
Applied. Six leaf moves from security/incidents/ into infrastructure/security/incidents/ — never the folder node, because the destination already had children (the recorded drag hazard); verified after the run: source empty, destination holds 8 pages, the May report and its findings page untouched. Link repairs: the two 2026-08-01 reports cross-linked to the archived duplicate path /en/Incidents/... (capital I) → repointed to canonical; security/security-strategy's two relative incidents/... links → absolute canonical. Referrer sweep (standard_6, the DSPT quartet, client-agents pages, operations/, infrastructure/compliance, operations-overview, backups) found no other inbound links. The index at infrastructure/security/incidents was 731 chars listing only the May pages; it is now a register of all eight pages, newest first, with statuses (Answered / Closed / Remediated / Contained / Resolved), the 2026-08-11 → 08-14 chain called out as one incident, an expanded status-definitions table and related-pages links. --prune security/incidents then removed the emptied nav node — output 1 nav node(s), 0 page(s) referenced, the safe case. All six old URLs now 404; one lagged on the SSR cache and cleared with docker restart wikijs (the recorded remedy).
A second wrapper-artefact page fixed in passing. security/security-strategy carried two baked-in ====/Page: footer blocks; rebuilding it for the link fix stripped them (15,178 → 14,982 chars) with content anchors verified intact afterwards — the accumulated-footer hazard the log has recorded before, now repaired rather than re-copied.
Section B/C audit (same session — this is the input for the next section):
security/security-check-in (23 accumulated footer blocks, 2,829 chars — the worst yet), security/hetzner-storage-box (1) and security/client-vps-backup-plan (1); plus a bare trailing ==== on policies/standard_6 (62 chars). Fix on rewrite, then re-fetch to prove the stored body is clean.compliance/governance-compliance/dspt-standards/* (53.8K / 31.2K / 48.0K) vs infrastructure/compliance/dspt-standards-* (23.9K / 14.6K / 15.3K), similarity only 0.36–0.42, and the infrastructure set carries imported underscore titles (DSPT_Standards_1-3_Governance). Needs a content-level read and a single canonical home.security/hetzner-storage-box vs archive/security/hetzner_storage_box (98.6% — the archived copy is the older version) and policies/standard_6 vs archive/en/policies/standard_6 (99.5% — the 62-char artefact apart). Both archived copies are redundant while the live pages stay.policies/ naming: the underscore set (standard_1a, standard_4/4a/4b, standard_5, standard_6, standard_6b_incident-response, standard_7a, standard_8, standard_9/9b/9c/9d_anti_malware, standard_10) sits beside the hyphenated standard-1-access-control / standard-1-role-access-matrix / standard-2-data-security; several pages have bare titles ("Standard 1a", "Standard_4", "Standard 5"); Standard 3 and Standard 7 have no page at all.archive/Incidents/* (content-identical to the live pages — one byte-identical, the other differing only by three trailing newlines) and whether to rename the two off-convention incident filenames (hosting_provider_complaint, kinsing-forgejo-compromise-2026-08-11).8b. Handoff inbox archive pass (2026-09-21, same session — no phase advanced). Both live inboxes emptied of HANDLED notes, per agents/handoff/index: 4 out of from-hermes (the three 2026-09-19 pharmacy tickets — goods-in retry, NHSBSA exemption codes, EPS token matching — plus the 2026-09-16 staff-screen/credential note) and the single 2026-09-19 build report out of from-opencode. Moved, never deleted: agents/handoff/archive/2026-09/from-hermes (60 → 64 notes) and .../from-opencode, each with a dated pass line in its header. The inboxes now read OPEN-only — 4 notes and 0 notes respectively — each with a pointer to its archive, and the from-hermes header keeps the credential-exposure pointer because the rotation and git-history purge are still deferred. Verified in the rendered pages: 4 <h3> on live from-hermes, 0 on from-opencode, the moved notes present in both archives, and no stale-cache repeat (no restart needed). Flag for the next session: the two archive/2026-09/* pages are now 200K / 180K chars — archived-not-deleted by design, but expensive to fetch whole; read them in slices, or split the month at the next pass.
No tidy-up phase advanced (Phase 4, policies + compliance + security, remains NEXT — plan at the foot of this entry). from-opencode read at session start: top note HANDLED 2026-09-19, nothing open for me. Matthew authorised the whole prune list ("don't we need everything to be done"), so this was a maintenance pass: nothing deleted except four broken/empty stubs, everything else archived and still searchable.
1. This log pruned 135,566 → 10,194 chars. Rounds 1–5 and every dated session handover 2026-09-02 → 2026-09-15 (dental charting/perio/radiographs/finance, pharmacy phases 1–6) were moved — verbatim, newest first — to agents/hermes/housekeeping-log-archive, which went 10,464 → 136,321 chars. The live page now carries: header, phase tracker, this round, pointer to the archive, open items, conventions. The 2026-08-25 token split had regrown because session entries were being prepended above the header; the page now keeps only the current round above the tracker. Verified in the rendered page (17,395 bytes; Round 6 present, Round 5 gone; archive page carries Round 5) — no wikijs restart needed.
2. Demo-run ephemera: 15 superseded reports archived, 3 kept. Archiving left exactly the latest PASS per scene — dental_appointment_booking/20260814T075807Z (PASS 46/46), dental_walkthrough/20260804T174044Z, dental_fp17_submission/20260804T110149Z — which is the rule demo_library already stated but no longer honoured (13 of the 15 were ABORTED runs or intermediate repeats from 2026-08-13/14). demo_library needed follow-up work, not just the moves: its 6 links into now-archived reports were repointed to https://wiki.veripath.co.uk/archive/... with a "(report (archived))" label per convention, and the newest booking PASS (20260814T075807Z) was added to the Recent runs table — the table had stalled at 2026-08-13T10:20Z, so the one report the policy says to keep was otherwise unlinked. Rendered check: 21 archive links, 3 live links, 0 stale; two archived reports spot-fetched HTTP 200.
3. Four real deletes (--prune, nav + page rows). archive/get — a 34-char stub ("This page was archived by cleanup.") created by a malformed --archive call, whose intended subject infrastructure/SIAAS/client_vps_setup still exists live; archive/agents/hermes/_tmp_mermaid_min and _tmp_mermaid_test (render tests, 72/146 chars); and home/partner-user-management-plan (0 chars, empty title — the duplicate the open items flagged; the real plan is the gp_booking_app one). Verified gone via --get ("Page not found"), and --list mermaid now returns nothing. The 17-page Phase-1 archive batch that set the demo-run precedent is recorded in the archive page.
Deliberately not done. The single demo_errors/ report stays (it is the audit trail for the booked-out receptionist bug demo_library tracks). home/partner-user-management-plan-phase3 and home/wireguard-vpn-setup are real content and belong to Phase 4's home/ tidy. The nameless (ARCHIVED) root nav node could not be re-confirmed: empty folder nodes do not appear in --list, and I did not read the DB directly — so confirm it as a --prune candidate while inside Phase 4.
PHASE 4 PLAN (audit done this session, awaiting Matthew's go-ahead — a phase is never started unnamed). One section at a time, audit → propose → confirm → apply → verify: (a) three incident trees — security/incidents/* (6 pages incl. the two August outages and the Forgejo compromise set), infrastructure/security/incidents/* (2 pages) and archive/Incidents/* (2 pages, already archived) — pick one canonical home and cross-link; (b) policies/standard_6 duplicated by archive/en/policies/standard_6, and the underscore set (standard_1a, standard_4, standard_4a, standard_4b, standard_5, standard_6b_incident-response, standard_7a, standard_8, standard_9*) sitting beside hyphenated standard-1-access-control / standard-1-role-access-matrix / standard-2-data-security — normalise names and fill the 1–10 gaps; (c) the security/ DSPT quartet (dspt-security-setup, dspt-monitoring-guide, dspt-operations-guide, dspt-quick-reference) against compliance/governance-compliance/dspt-standards/*; (d) the home/ strays. Open items below are otherwise unchanged.
ROUND 6 (2026-09-15, session 18) — the staff user-management view is AUTHORISED and SPEC'D; OpenCode session 17 DEPLOYED and verified.
No tidy-up phase advanced (next remains policies/compliance/security). from-opencode was read at session start: session 17 (ce072f0) delivered my build ask, took both decisions, and corrected two of my claims.
Corrected by OpenCode, accepted: (a) ResponsiblePharmacistLog has had Meta.ordering = ['-signed_on_at'] since Phase 1, so my "the RP log is unsorted" note was wrong — there was no ordering defect; they added the explicit order_by anyway plus a regression test. (b) The seed now creates one identity per role ({prefix}.bm.one, .tech.one, .ca.one) app-side only, so my earlier "no BM identity anywhere" census was stale.
Authorised by Matthew. He asked for an in-app view for adding pharmacy staff "like the dental app", MFA explicitly out of scope, and ruled the gating role: Business Manager only. Spec written at staff-user-management — grounded in the actual shared partner staff-user screens (/partner/users/: list + inline add form username/email/first/last/password; detail with enable/disable and reset password; no MFA), with the differences that matter: a role selector (the partner form hardcodes the role), realm resolution from partner_org.keycloak_realm else the sector realm, one action that sets the role attribute + role group + org_id + a temporary password, no app-side user row (OIDC first login makes it, which removes the two-sided failure that produced demo.tech.one), guard rails against self-disable and disabling the last BM, PharmacyAuditEvent on every action, and a per-realm service account rather than the partner code's master-realm admin credential — that credential must not sit in a path a client-side Business Manager can reach. OpenCode has the build note.
Deployed and verified (mine, no authorisation needed): ce072f0 on the app engine by fast-forward (12 files; repo is bind-mounted, entrypoint runs migrate + collectstatic). pharmacy.0014 applied on the apex (sector_pharmacy) and the client tenant via migrate_client --slug test-client-pharmacy --app-label pharmacy; app healthy, /infrastructure/health/ 200; client e2e PASS (login through realm test-client-pharmacy, RP cycle); realm isolation PASS 4/4. provision_pharmacy_client_org --dry-run verified live (resolves slug/realm, finds the client secret, writes nothing) — so client onboarding is now three documented steps. org_id consumption proven end to end: tc.rp.one/tc.rp.two read org_id='test-client-pharmacy' app-side after login (previously None), tc.bm.one still None because it has not logged in since — exactly the on-login behaviour. 291 tests confirmed independently on my own run.
Open items: the staff view is the next build (with OpenCode); password-vs-invite remains my recommendation only (manager-entered temporary password); the apex still has no BM/CA identity, so only the client tenant can currently exercise the BM-only surfaces — the new view's first use will likely be creating that Dispensing Technician; A8 (the bookkeeper handover pack) remains mine; net-vs-VAT counter pricing still with Matthew.
ROUND 5 (2026-09-15, session 17) — PHARMACY CLIENT REALM CORRECTED: test-client-pharmacy now has its OWN Keycloak realm. The cross-tenant identity leak is closed.
No wiki tidy-up phase advanced this session (the next housekeeping phase remains policies/compliance/security). from-opencode was checked at session start: the top note (OpenCode session 16) is HANDLED, nothing open for me.
Why this happened. Matthew asked for the pharmacy user-registration view and I re-grounded my earlier discussion note against the code. He challenged my claim that "all pharmacy tenants share one realm", which turned out to be correct about pharmacy's state and wrong about its cause: I had asserted the app could not select a per-client realm. It can — the mechanism is shared code (tenancy.middleware.TenantMiddleware takes the first host label, finds a PartnerOrg row with that slug, sets request.partner_org; _is_partner_request() is bool(request.partner_org); _get_partner_realm() returns partner_org.keycloak_realm). GP client (test-client) and dental client (test-client-dental) each have such a row and switch realm correctly; test-client-pharmacy had none, so its host fell through to the sector constant PHARMACY_REALM = 'pharmacy'. Matthew's ruling: the pharmacy app was meant to model this exactly as GP and dental do, and it drifted — correct it. It was an onboarding gap, not architecture: no rewrite was needed.
Consequence that made it a real defect, not a cosmetic one. With both tenants in one realm, tc.rp.one (a client identity) could authenticate at the apex host and sign on duty at the apex premises — proven with a real login, not inferred. demo.rp.one at the client host was exposed symmetrically.
What I did (all live, all verified; identity only — no data touched):
manage.py provision_pharmacy_realm --realm test-client-pharmacy --name "Test Client Pharmacy" --host test-client-pharmacy.pharmacy.veripath.co.uk --erpnext-source-realm pharmacy → realm + pharmacy-booking-app (confidential, that host's redirect URIs) + erpnext client copied + the 4 operational roles + 4 role groups (new, realm-local ids).tc.rp.one, tc.rp.two (RP), tc.bm.one (BM), tc.ca.one (CA) — role attribute + role group verified on each.PartnerOrg row (pk 7; slug/realm test-client-pharmacy, client secret fetched from Keycloak inside the app container so it was never printed). This row is the switch.pharmacy (tc.*, incl. the tc.test.user litter) — enabled=false, not deleted, reversible. This is what closes the leak. tc.test.user has no identity in the new realm either, so its app-side row was deactivated too (is_active=False) — the leaver rule is both sides: realm and app.create-pharmacy-test-user.py and provision-pharmacy-client-keycloak.py had realm-pharmacy group ids hardcoded and no realm argument; both now take --realm and resolve group ids by name (veripath-ops/scripts, syntax-checked).Evidence. Redirects: pharmacy.veripath.co.uk → realm pharmacy (unchanged); test-client-pharmacy.pharmacy.veripath.co.uk → realm test-client-pharmacy. verify-pharmacy-client-e2e.py: PASS on the client host (login now posts to realms/test-client-pharmacy/login-actions/authenticate; sign-on, one-on-duty refusal, history, sign-off, second pharmacist) and PASS on the apex host — no regression. Isolation matrix with real browser logins: before, tc.rp.one@apex ACCEPTED; after, tc.rp.one@apex rejected, tc.rp.one@client ACCEPTED, demo.rp.one@client rejected, demo.rp.one@apex ACCEPTED. Accounting unaffected: as the client's BM, /pharmacy/ and /accounting/ render no realm string and no *accounts* host (the pharmacy accounting surfaces emit no realm-bound SSO link), and the client's PharmacyERPNextConfig site is unchanged.
Two errors of my own corrected in the record. (a) The "shared realm" framing and the claim that the app cannot select a per-client realm — wrong, corrected in from-hermes and on the wiki. (b) The stale identity census ("5 RPs, 1 DT, 0 BM, 0 CA"): the position is now 2 RPs in the apex realm and 2 RPs + 1 BM + 1 CA in the client realm, so the BM-only Phase 6 reports and the Phase 4 counter beats can be walked as test-client-pharmacy; the apex tenant still needs a BM and a CA seeded. Corrected on the pharmacy index. Lesson repeated from round 4: verify a claim against the live system before writing it into a note, and follow the middleware rather than stopping at "no realm field on the tenant".
Wiki write-back for this round: new page development/projects/pharmacy_app/operations/client-realm (model, drift, correction, evidence, runbook for the next client, open items); the pharmacy index (status line + the corrected identity-census paragraph); from-hermes (new note with the corrections and one build ask); and this log. Every write verified in the rendered page.
Hygiene slip, caught and fixed the same session: my round-trip writes appended the tool's ==== / Page: footer into four page bodies — the very hazard the wiki hygiene note below already records, so this is a repeat of a known mistake, not a new one. All four pages were re-written clean and verified by re-fetch (no embedded ====+Page: block, no trailing Page: line). The rule, stated once and worth keeping: --get output carries a header and a footer; strip both, and re-fetch to prove the stored body is clean rather than trusting the write response.
Open items now outstanding (unchanged owner in brackets): the client-org creation is still manual — no app code creates a PartnerOrg row (no .objects.create outside tests, no management command), so I asked OpenCode for manage.py provision_pharmacy_client_org [OpenCode]; PharmacyERPNextConfig has no keycloak_realm field and the ERPNext SSO builder defaults to 'veripath' — latent, decide the field/default [Matthew/OpenCode]; brute-force protection is false on every realm platform-wide [platform decision]; the apex tenant has no BM or CA identity [demo readiness]; the RP log is unsorted on both surfaces with the two-line fix + test unclaimed [OpenCode]; A8 (the bookkeeper handover pack) remains mine; the pharmacy user-registration view is still not authorised to build — decisions (a) and (c)–(h) are with Matthew, and (b) is now largely answered by the realm model.
ROUND 4 (2026-09-15, session 16 cont.) — THE LAST BUILD ITEM IS CLOSED. THE v1 PHARMACY BUILD IS COMPLETE.
OpenCode session 16 (origin/dev = e6a5fbb, one commit, no migrations, 276 tests OK on my own run) fixed the
reset scope — _demo_baskets() matches counter_customer=DEMO_MARKER OR erpnext_invoice='DEMO-SINV-0001' — and
scoped the stock-movement delete to the demo baskets, closing a latent bug that would have deleted a real sale's
movement. I deployed it (containers restarted, migrations a no-op) and re-verified.
Acceptance test 1 PASSES over the FULL period (no window trickery this time): ties=True, mismatches=[],
unlinked=[], gross_variance=0.00, band split STANDARD 5.00/1.00 + ZERO 9.90/0.00; both sides read — local
basket 6 gross=15.90 against ERPNext ACC-SINV-2026-00019 net=14.90 tax=1.00 grand=15.90. The orphaned basket 2 is
gone and the real basket 3 (VOIDED, ACC-SINV-2026-00016) is intact, so keying on the placeholder invoice rather
than the shared customer label is proven right on real data. The movement filter matches the counter's real
sale:{basket.pk}:{line.pk} format (pharmacy_counter/services.py::consume_stock), so the scoping is correct, not just
the test.
I also closed the two criteria that had only been code-and-test-level. Criterion 7 live by direct URL: logged in
as the real user tc.rp.one (PHARMACY_RESPONSIBLE_PHARMACIST) and fetched all eight /accounting/ URLs → HTTP 403
with a permission-denied body on every one (anonymous gets a 302, so a 403 means authenticated-but-wrong-role).
Criterion 8: central holds 0 pharmacy% tables; the apex tenant and the client tenant hold 30 each
(including the three accounting tables); GP and other clients 0.
Phase 6 score: 7 of 8 acceptance criteria proven. The only gap is criterion 6 — A8, the bookkeeper handover pack,
which is mine. I corrected an error in my own earlier record while doing this: I had listed "tests 2, 3, 4, 5, 6
and 7" as holding, but criterion 6 is the handover pack and cannot hold before A8 exists — the correct score is 7 of
8. Lesson: check the criterion numbering against the page's own acceptance list, never against a compressed note.
One demo-readiness gap, not a build defect — it blocks the Phase 6 walkthrough. seed_pharmacy_demo creates only
{prefix}.rp.{suffix}. Platform-wide the pharmacy identities are 5 Responsible Pharmacists, 1 Dispensing Technician,
0 Business Managers, 0 Counter Assistants. So the BM-only accounting reports cannot be opened by any human, which
is why criterion 7 could only be proven by refusal; and the Counter Assistant — the primary actor on the Phase 4
counter surfaces — has no identity either. Raised with OpenCode as a small synthetic-seed extension (one identity per
role, idempotent, same host-local credential discipline).
Then Matthew directed the next phase of work: the live walkthroughs begin, from the beginning. The earlier
"builds before walkthroughs" gate is satisfied because the builds are finished, so Phase 6's formal acceptance is no
longer a precondition for walking. The standing method holds: one walkthrough at a time, in chronological order,
discuss the scope before building the scene, stop for review. A8 (the handover pack) remains outstanding and is
mine.
Wiki write-back for this round: from-opencode (session 16 HANDLED), from-hermes (build closed + the identity
ask), the Phase 6 page (round-4 section, the acceptance table and the corrected count), the resume, the pharmacy index,
the module map, and this log. Every write verified in the rendered page, not the write response. Repos clean at
e6a5fbb locally and on the app engine.
LATER STILL AGAIN (2026-09-15, session 16) — PHASE 6 ROUND-3 RE-VERIFICATION: THE SILENT BUG IS FIXED; TEST 1'S BLOCKER HAS MOVED; TEST 5 SETTLED BY MATTHEW AND PROVEN.
OpenCode session 15 (origin/dev = 96d64b3, three commits, no migrations, 275 tests OK on my own run) fixed
both round-2 defects. I deployed it — app engine; the entrypoint applied nothing (a no-op on default and
sector_pharmacy) and migrate_client --slug test-client-pharmacy was a no-op too — then re-verified live. No new
defect outside the two threads below.
1. The silent reimbursement_summary() bug — FIXED, and proven two ways. Not just the value: _expected_scope(None)
returns a bare Q() whose compiled SQL is literally (AND: ) — no IS NULL, no premises term — while
_expected_scope(premises) carries the filter, so the default call now has the same shape as _receipt_scope, which
is what round 2 asked for. Live on test-client-pharmacy: expected_total = 422.10 with and without premises
(it was 0.00 unscoped), against actual_total = 421.10, variance -1.00, unmatched_count = 0. awaiting_receipt()
carries no IS NULL either, and its both-ways counts agree (0 = 0). Closed.
2. The ProtectedError — FIXED, but acceptance test 1 is still blocked and the cause moved. The crash is genuinely
gone: purge_dispensing_tokens --reference demo-token-endorsed-1 removed my round-one litter and its whole trail, and
seed_pharmacy_counter --tenant test-client-pharmacy --reset completed exit 0 — "Removed synthetic counter
rows." then "sale basket 5 (ACC-SINV-2026-00018) with an NHS charge and a refund." (I confirmed there are no
pre_delete signals on those models, so the new queryset-delete really does bypass the append-only instance barrier.)
But A4 still returns ties=False, variance -15.90, mismatches=[{basket: 2, invoice: 'DEMO-SINV-0001'}] — because
basket 2 (COMPLETED, gross 15.90, counter_customer='Counter Customer', completed 2026-09-14 17:55) is still on the
tenant and the reset cannot see it. Root cause, and it is a regression inside Finding 1's own fix: 9c301ca changed
the reset's identity key from filter(erpnext_invoice=DEMO_INVOICE) to filter(counter_customer=DEMO_MARKER) (and the
create path's label with it), silently orphaning every basket seeded before that commit — from the reset and from
the idempotency check. A4 is behaving honestly; the reset's scope is the defect. Suggested fix (in from-hermes):
widen the reset to Q(counter_customer=DEMO_MARKER) | Q(erpnext_invoice='DEMO-SINV-0001'), or sweep the old rows once.
Do not sweep on counter_customer='Counter Customer' — basket 3 carries that same label with a real invoice
(ACC-SINV-2026-00016), so a label-based sweep would delete good data.
Proof that the orphaned row is the only thing left (the technique worth reusing — narrow the window to exclude
the suspect row rather than editing data): A4 over 2026-09-15 alone gives ties=True, mismatches=[], unlinked=[],
variance 0.00, band_net={STANDARD: 5.00, ZERO: 9.90}, band_vat={STANDARD: 1.00, ZERO: 0.00}, vat_total=1.00.
Both sides read directly: local pk=5 gross=15.90 invoice='ACC-SINV-2026-00018' against ERPNext's
ACC-SINV-2026-00018 net=14.90 tax=1.00 grand=15.90 docstatus=1, item_wise_tax_detail = {"DEMO-OTC-PARA-500":[20.0,1.0],"NHS-PRESCRIPTION-CHARGE":[0.0,0.0]}, and get_sales_invoice('DEMO-SINV-0001') -> None. So the tie arithmetic and the VAT split are right; only the orphan stands in the way.
3. Acceptance test 5 — SETTLED by Matthew (option a), run, and PROVEN. Every Bin row carried quantity and zero
valuation, so A5 faithfully reported 0.00 = 0.00 and proved nothing — a decision, not a defect, so I put it to
Matthew rather than letting a number be invented to make the test pass. He ruled option (a) — value the demo stock
the way session 15 proposed — so I ran seed_pharmacy_stock_valuation --tenant test-client-pharmacy: "2 received, 0
already stocked" (MAT-STE-2026-00029, MAT-STE-2026-00030). A5 now reads total_value = 353.85 over
total_quantity = 273.00, and ERPNext's own Bin sum for the same seven item/warehouse rows reads stock_value = 353.85 over 273.00 — ties: True, quantity ties: True, read from both sides. The four pre-existing demo rows
stay at 0.00, which is exactly the outcome he ruled on; the zeros are an artefact of Phase 3's deliberate
zero-valuation goods-in, which is untouched (in production a client's own goods-in rates make A5 substantive with
no code change). Recorded as a ruled limitation, not a defect.
Result: six of Phase 6's seven acceptance tests are proven (2–7). Exactly one item stands between Phase 6 and
acceptance — test 1's reset scope, above. origin/dev = 96d64b3, deployed; my own run 275 tests OK.
Deploy: app engine pulled to 96d64b3; migrations no-op on default, sector_pharmacy and the client tenant;
pharmacy_app healthy. Housekeeping note: an idle opencode TUI was open on this box the whole session (no
CPU time advancing across 6 s, state Sl+, 1-minute load average falling to 0.63, only an MCP fileserver as a child)
— a running process is not automatically active work, but check the CPU time is frozen before concluding that.
LATER STILL (2026-09-15) — PHASE 6 A2–A11 DEPLOYED AND VERIFIED, NOT ACCEPTED (four findings).
OpenCode session 13 delivered A2–A11 as the client-tier app pharmacy_accounting (origin/dev = 71e80e5).
266 tests OK on my own run. I deployed it — app engine, both client tiers and the client tenant — and the
ERPNext side first, because the counter now sends paid_to (erpnext-deploy 40474b4); the VPS3 image is built
by a manual docker build -t erpnext_v15:latest ., since the compose has no build directive and
docker compose build just says "No services to build". Deploying the app first would have broken the counter in
production.
Verified: the counter till wiring, before and after from ERPNext itself (ACC-PAY-00001 credits the clinical
1010 - Clinic Till / Cash on Hand; ACC-PAY-00002 credits 1170 - Pharmacy Counter Till); A2's unpriced case
returns expected_amount=None with the unpriced items named, never zero; A3 refuses to reconcile an incomplete
estimate and its receipts are append-only; A4–A7 all run against ERPNext and A7 composes them with the six
manual items D6.4 keeps out.
Four findings, so the phase is NOT accepted (acceptance tests 1, 2, 3 and 5 unproven): A4 cannot tie
(ties=False, variance -15.90) because seed_pharmacy_counter writes DEMO-SINV-0001, which is not an ERPNext
document; A5 values 196 units at 0.00; nothing provisions the reimbursement price list and the demo items
carry no item codes, so a complete estimate cannot be produced; and the A11 seed mislabels its basis as
PRICE_LIST_PLUS_FEE while fabricating prices, bypassing the compute path entirely. All four are in
from-hermes for OpenCode; findings 1, 3 and 4 are small.
LATER STILL AGAIN (2026-09-15) — PHASE 6 RE-VERIFIED: 3 of 4 findings closed, 2 NEW defects found (not accepted).
OpenCode session 14 (origin/dev = 9c301ca, 270 tests OK on my run) fixed the four findings I raised.
Findings 2, 3 and 4 are closed and proven live, and acceptance tests 2 and 3 now pass: A4 shows the VAT band
split read from ERPNext's own item_wise_tax_detail (STANDARD net 5.00 / VAT 1.00; ZERO net 9.90 / VAT 0.00), and
two complete estimates computed through the real path (353.85 and 68.25, prices from the NHS Reimbursement price
list) reconcile with a MATCHED receipt (variance 0.00) and a SHORT one (variance -1.00). Finding 1's fix is
real too: seeding without --reset posts a genuine ACC-SINV-2026-00017.
Two new defects, both reported to OpenCode:
seed_pharmacy_counter --reset raises ProtectedError. Three models now PROTECT the dispensing tokenPrescriptionNotification.token, CollectionRecord.token, ExpectedReimbursement.token) and _reset() deletesDEMO-SINV-0001 is stuck, andties=False, mismatch on that invoice, variance -15.90).reimbursement_summary() silently reports expected_total as 0.00 instead of 422.10 when called withoutpremises — proven on the tenant, and correct when given the premises. Cause: token__premises=NoneIS NULL and matches nothing, while the actuals path applies no such filter. A variance view thatAcceptance test 5 still not demonstrable, for a data reason: every Bin row carries quantity (25, 8, 55, 48, 60)
and zero valuation, so A5 faithfully reports 0.00 = 0.00 and nothing is demonstrated. The demo stock needs a
valuation — a decision as much as a patch. I also asked for a synthetic purge path for
expected-reimbursement rows: my round-one verification artefact cannot be removed through the app, because
ExpectedReimbursement.delete() raises by design (the same shape as the Phase 3 tokens that spawned
purge_dispensing_tokens).
A trap found while recording all this — worth remembering for every future wiki write. /opt/wiki-manage.py's
description parameter goes into a varchar(255) column, and overrunning it fails the upsert with
psycopg2.errors.StringDataRightTruncation: value too long for type character varying(255) — but the tool still
prints Content: <n> chars first, so it looks like it worked. The page silently keeps its old body. Two guards:
keep descriptions under 255 characters, and only treat an upsert as successful when it prints Page ID and
Render: OK — then confirm in the rendered body. This cost one silent no-op today before it was caught.
Not a housekeeping pass — the pharmacy programme's gate work and the outstanding user guide, logged for continuity.
A wrong finding in my own record, found and fixed. The Phase 5 spec page carried "a search for a Notify
API wrapper found no dedicated client file in the pharmacy repo" — my own Phase 4-era note, and it was
false. The search looked for the GOV.UK Notify host (api.notifications.service.gov.uk); this app uses
NHS Notify / Communications Manager on the NHS sandbox host, a different service with a different API.
Closing gate G5.1 against the code: services/nhs_notify.py exists (NHSNotifyService(api_key) with one
method, send_notification(nhs_number, message_text, plan_type)) but it is a skeleton — placeholder
routing-plan IDs; recipient keyed on an NHS number only, so the NHS App path is the only one wired and there
is no mobile/email recipient, which D5.3/D5.2 need; free-text personalisation.body, which the real service
does not accept (templates must be approved), making the existing call shape incompatible with G5.2 as
written; no X-Request-ID (Notify requires it), no retry, no status polling, no timeout and no error
wrapping; and no mock mode at all (EPS has USE_MOCK_EPS behind a factory; Notify points straight at the
sandbox). So N2 is net-new work, not reuse. Its callers are appointment-shaped, inherited from the fork.
Two carry-overs: credentials are clean (settings.NHS_API_KEY is env-driven and settings refuses to start
without it), but the inherited audit pattern stores message content
(AuditLogger.log_nhs_notify_message) — against N9, so Phase 5 must not copy it.
Records corrected and written. phase-5-notifications now reads Phase 5 is GO, G5.1 CLOSED, G5.5
CLOSED, with the false finding replaced by the above and the two stale "Phase 4 is not built" statements
fixed. OpenCode has the whole finding in a 2026-09-15 note in agents/handoff/from-hermes (newest on top) so
its next session re-discovers none of it. Both writes were verified by rendered body after the upsert (no
SSR staleness) and the older handoff notes were confirmed still present.
W12 — the roles-and-permissions guide — PUBLISHED:
roles-and-permissions, the third pharmacy guide; the
row on the guides index that read planned now links to it. Written from the code, not the plan — the
ten mixins in core/permissions.py plus the URL table across pharmacy, pharmacy_dispensing,
pharmacy_stock and pharmacy_counter, giving a per-screen permission grid for the four roles. It documents
the two rules that matter (the accuracy check, dispense and endorse are the pharmacist's alone; the Counter
Assistant has no prescription surface at all) and says plainly that a hidden menu item is not the protection,
because the gate is enforced on the view itself. Awaiting Matthew's review, one page at a time as usual.
Matthew's sequencing decision (2026-09-15). Asked to finish building everything before any walkthrough:
the build order is now explicit, so the live walkthrough of Phases 2–4 is deferred until Phases 5 and 6
are built (and 6 still follows 5 through the gate). Recorded on agents/handoff/hermes-resume.
Record clean-up the same session. Stale "unauthorised" text survived the 2026-09-14 sweep in three
places: phase-6-accounting said "Phase 5 is not built or authorised", the module-map status line said
"Phases 3–6 not authorised", and pharmacy_app/index said the same while its pick-up block still led with
Phase 2. All three corrected to the current state (Phases 0–4 accepted; Phase 5 GO with G5.1 closed; Phase 6
gated on Phase 5) and verified by rendered body. Lesson for Phase 9's metadata pass: authority status is
duplicated across four pages and drifts every time a phase is accepted — it wants a single source, or an
explicit check in the phase-acceptance routine, rather than trust in a sweep.
Handoff inboxes swept (2026-09-15), ahead of the Phase 6 run. Per the convention on
agents/handoff/index: from-hermes (all 23 notes HANDLED) and 19 of the 20 in from-opencode were moved
into archive/2026-09/, which now holds 50 and 47 notes. Kept live: only OpenCode's Phase 5 report
(session 11) — the one item still awaiting my verification. Archive-not-delete, and the copy-then-trim
pattern rather than --move; the tree was re-listed after the write (no folder drag) and the rendered note
counts matched exactly.
A trap found while doing it — remember this for every future archive pass. The wiki tool wraps page content
in rules of 60 = characters, and agents/handoff/archive/2026-09/from-hermes contains its own 60-= rules.
Splitting that output on the first rule silently truncated the page from 67,826 chars to 14,439 and lost 20
notes — caught before anything was written. Extract from the FIRST 60-= rule to the LAST, never on the first
occurrence alone.
LATER THE SAME DAY — PHASE 5 SHIPPED AND ACCEPTED; PHASE 6 OPENED AND A1 DELIVERED. Matthew asked for the
builds finished without further authorisation pauses ("stop asking for my authorisation and just get
everything finished"), so the day's second half was delivery rather than verification:
5dc2fea app + audit actions,0df45cb the N3 hook, d77621f the N7 portal view), origin/dev = d77621f; 226 tests OK on my ownpharmacy.0011 + pharmacy_notifications.0001; migrate_client did the clientpharmacy% tables centrally (boundary re-checked, not assumed). Driven live ontest-client-pharmacy: queue_collection_ready returns None and writes nothing for CANCELLED/ASSEMBLED/record_collection records the actor's name snapshot and leaves DISPENSED intact;delete() raises; the opted-out row has 0 attempts; audit detail is channel-only; the D5.2 payload carriespharmacy/accounting.py (account specs + the document→account map,provision_pharmacy_accounts, and nine new config fields (migration pharmacy.0012). Live:account_name is the bare label (“Income”) while the numbered label lives in nameaccount_name silently finds nothing. Both lookups now searchCarry-forward: the live walkthrough of Phases 2–4 stays deferred pending the builds; Phase 6 A2–A11 is the live work (A1 done); W12 awaits
Matthew's review; Phase 5 is BUILT but uncommitted, undeployed and unverified (OpenCode session 11, 226
tests green, origin/dev still eb1ce70) — the deploy decision is with Matthew, and Phase 6 must not start
until I have verified Phase 5; still with Matthew: counter pricing (net vs VAT-inclusive) and the central DB
rotation window (proposed Sun 2026-09-20, 02:00–04:00). Phase tracker: unchanged — the staged tidy-up's next phase is
still policies/compliance/security.
Not a housekeeping pass — a continuity entry, written because the session that ended the day died
before it could write its own.
Where the pharmacy day finished. Phase 4's counter is ACCEPTED — I verified eb1ce70 from the
artifacts rather than the build note (runner 183 tests green on my own run; ACC-SINV-2026-00015 and
ACC-PAY-00001 read from ERPNext with the VAT split as ERPNext's own item_wise_tax_detail and
counter:1 as the only reference — no patient identity on the financial documents; Counter stock
60 → 59 → 60 across sale and refund; six counter tables on both client-tier instances and zero pharmacy
tables centrally). That acceptance was Phase 5's gate, so Phase 5 is GO; Phase 6 is held until
Phase 5 is verified (build 5 → report → I verify → then 6). All six phases are authorised, with every
decision settled and every vocabulary ratified.
What the note records. agents/handoff/hermes-resume is refreshed as the tomorrow pick-up: Phase 5
opens at gate G5.1 — confirm what NHS Notify surface actually exists (no client was found in the
repo) — then the collection-ready send at DISPENSED, the collected record, opt-out, the delivery
trail, the uncollected list and the PWA flow; Phase 6 opens with the pharmacy's chart-of-accounts
wiring (the counter's Payment Entry still credits the fork's GP/dental account name) and then the
expected-reimbursement model, ERPNext-read reporting, the period-end pack and the bookkeeper handover
pack. agents/handoff/opencode-resume's RESUME HERE block was corrected from "awaiting Phase 4
verification" to "Phase 4 accepted — Phase 5 is GO", with a standing instruction not to change counter
pricing until Matthew rules.
Two asks still with Matthew: counter prices are net as built (£2.50 + VAT = £3.00) against a UK
counter price being VAT-inclusive — recommendation is inclusive displayed prices with the invoice
arithmetic staying ERPNext's — and the central database password rotation window, proposed Sunday
2026-09-20, 02:00–04:00 (runbook ready, not executed).
Mine, outstanding: the live walkthrough of Phases 2–4, W12 (the roles-and-permissions guide, one
page at a time), and verification of each phase against the artifacts.
Why this entry is late: the previous session hit a provider read error and exhausted its retries
during wrap-up, so the continuation note was never saved. The work itself was finished and on the wiki —
only the note was missed.
Carry-forward: the Phase 5 build (opening G5.1), then Phase 6 once Phase 5 is verified; the counter
pricing decision. Phase tracker: unchanged — the staged tidy-up's next phase is still
policies/compliance/security.
Not a housekeeping pass — the authorisation clean-up and the handover, logged for continuity.
Record made consistent. Every pharmacy page was checked for stale "unauthorised" text: the module map
said "Phases 3–6 remain unauthorised" (false since the same morning), and the Phase 3, 4 and 6 pages each
carried a stale line. All corrected, plus three genuinely-finished open items closed on the module map
(ClientVPS.Sector's pharmacy choice, ERPNext per-pharmacy provisioning, and the research-page redaction).
There are now zero "unauthorised" statements in the pharmacy section — all six phases are authorised
with their decisions settled and vocabularies ratified.
Five decisions put to Matthew and approved (2026-09-14): (1) purge the pharmacy git history of the
inert old demo credential — a destructive force-push, which is exactly why it needed his explicit yes;
(2) drop the client's orphaned former database on the shared client-tier instance; (3) the RP log
records who closed a shift (a "signed off by" snapshot plus a migration — a small Phase 1 follow-up, not
a redesign, so a colleague's closure is answerable from the log rather than only the audit trail);
(4) the central database password rotation is authorised but needs a planned window (the value is
shared across several stacks), so it is a runbook-and-window job; (5) redact the internal network
details from the pharmacy research pages — which I then did myself: host addresses and the tunnel/DB
gateway are gone from research_1 and research_2, each carrying a dated note, and the rendered pages
were re-checked (the SSR cache needed the usual docker restart wikijs).
Finish-off brief left for OpenCode (agents/handoff/from-hermes, newest note): the whole remaining
queue in order — commit and push the Phase 4 gate work (and not to squash migrations 0007/0008,
both already applied on live databases); the git purge and the database drop; then the phases in order,
each gated on the previous being accepted (Phase 4's counter C1–C13, then Phase 5 opening with gate G5.1,
then Phase 6); then the RP-log addition and the password rotation window; with the carried items
(migrate_client fresh-client gap, migration-history cleanup, Keycloak SSO scriptedness) listed after.
It also states what stays mine: the walkthrough, W12, the guides one page at a time, and verification of
each phase against the artifacts.
Carry-forward: nothing is blocked on a decision any more. Phase tracker: unchanged — the staged
tidy-up's next phase is still policies/compliance/security.
Not a housekeeping pass — verification and specification, logged for continuity.
What OpenCode delivered (session 8): the two Phase 4 gates, not the phase. G4.1 (the ERPNext
tax-template surface) and G4.2 (the Counter pool), with migrations pharmacy.0006/0007/0008, a new
provision_pharmacy_counter command and verify_pharmacy_tax, 157 tests green, and five fixes made on
the way. C1–C13 — the counter itself — was deliberately not built (correctly, per my instruction), and
nothing is committed: origin/dev is still 1716142 and the app-engine tree is dirty.
Verified from ERPNext, not from the note. I fetched the verification invoice myself:
ACC-SINV-2026-00014 — GBP, Pharmacy Counter VAT - TCP, lines 10.00 standard-rated and 5.00 zero-rated
with their Item Tax Templates, tax 2.00 on a 15.00 net, and the band split recorded in
item_wise_tax_detail. So the VAT split is ERPNext's own arithmetic, which was the phase's central
risk, and the caveat that taxes[].net_amount is the whole invoice net is confirmed. The Counter pool is
real too: two OTC items, batches of 60 and 48, prices 2.50 and 3.20, standard-rated templates attached.
OpenCode's finding that the warehouse existed but sat empty was worth checking; its note that no NHS
Notify client exists in the repo is recorded as Phase 5's gate G5.1.
Two asks left with OpenCode. Commit and push the gate work, and do not squash migrations
0007/0008 — both are already recorded as applied on the client database and the apex
sector_pharmacy, so squashing would strand live databases claiming a migration that no longer exists.
Then build C1–C13.
Phases 5 and 6 specified as a pair, at Matthew's instruction, and neither is authorised:
NEW phase-5-notifications — the
collection-ready alert when a token reaches DISPENSED, the collected record that finally gives the
ratified dispensed-vs-handover distinction a home, NHS Notify mock-first, opt-out, the delivery trail, the
uncollected list and the PWA collection flow: five gates, twelve work items, six decisions held, the
sharpest being what a message may contain (recommend the minimum — no medication names to a third party).
NEW phase-6-accounting — chart-of-accounts
wiring, the expected reimbursement model and its reconciliation to what actually arrives,
takings/VAT/stock/margin reporting read from ERPNext, the period-end pack and the bookkeeper handover
pack: five gates, twelve work items, six decisions held, the sharpest being that prescription charges
collected for the NHS go against the receivable, not to income.
Wiki changes this session: NEW phase-5-notifications and NEW phase-6-accounting; the module map's
phase 4 row (gates closed, build pending) and rows 5–6 (specced, with their decisions); the pharmacy index
pick-up list rewritten; a note to OpenCode carrying the verification, the push ask with the
no-squash ruling, and the instruction not to start 5 or 6; this entry; the resume note refreshed.
Carry-forward: the Phase 4 build (C1–C13) then its verification; Phase 5 and 6 authorisations and
their twelve decisions; mine still — the walkthrough and W12. Phase tracker: unchanged —
policies/compliance/security.
Not a housekeeping pass — verification duty on Phase 3 and the next phase's specification, logged for
continuity.
OpenCode delivered Phase 3 across five sessions and closed out Phase 2. Verified against the
artifacts, not the notes: the Phase 2 five findings are all genuinely fixed (mtime-reloading mock
store gated on USE_MOCK_EPS, atomic(using=), generic EPS errors with exc_info, the release
permit-check before EPS, the audit delete() barrier), 0002 is applied on both client-tier instances
and the phase is deployed; G1 is now a real ERPNext client surface (warehouses, batches, stock entries
with submit/cancel, prices, item groups, accounts, journals); G2's site is live with batch/expiry; the
runner gives 134 tests green; migrations are applied on the apex sector_pharmacy and the client
box with zero pharmacy% tables centrally; and I read the live Stock Entries from ERPNext itself —
MAT-STE-2026-00017/-00019 Material Issue docstatus 1 with batch DEMO-AMOX-B2 (FEFO as intended),
-00018 receipt submitted, -00016/-00007 cancelled.
One blocker, and it is the phase's headline behaviour. machine._replace_items() (used by assemble)
re-creates a token's items from a fixed field list that omits snomed_code, so a code captured at
claim time is dropped, consume_for_dispensing() sees nothing to map and skips the item, and a real
dispense moves no stock at all. Reproduced live rather than reasoned about: a mock task carrying
DEMO-SNOMED-001 was claimed → assembled → checked → dispensed, and batch DEMO-AMOX-B2 read 19.0
before and after with no StockMovement written. The seeded demo tokens work only because the seed
writes the code directly onto its items — which is why the build note's live consumption evidence looked
convincing. Phase 3 is therefore NOT ACCEPTED and the walkthrough is blocked with it. Two smaller
findings: an unmapped item dispenses silently (best-effort hook, nothing on screen), and my two
VERIFY-S5-* tokens need a purge the append-only history will not allow through the app.
Phase 4 specced. NEW development/projects/pharmacy_app/build-records/phase-4-counter-checkout —
counter basket, the VAT split (standard-rated OTC vs zero-rated NHS), collecting the NHS charge declared
at dispense, tender, refunds and voids, end of day — in the established shape: five gates, fourteen work
items, eight decisions held for Matthew. Grounding findings: the repo's payment surface is Stripe
PaymentIntent (card-not-present) with a per-practice PaymentConfig/PaymentTransaction pattern, and
the ERPNext client has no tax-template or POS surface at all, so the VAT split is pre-work (G4.1)
rather than a reuse. D4.1 recommends recording the pharmacy's existing standalone terminal's tender
instead of integrating Stripe Terminal, which keeps the app out of card data entirely.
Wiki changes this session: the Phase 3 spec page gained the "As delivered — G1, G2 and S1–S12"
evidence table plus its findings and a NOT-ACCEPTED status; the Phase 2 page gained the "all five findings
CLOSED; deployed" follow-up and a corrected status; NEW phase-4-counter-checkout; the module map's
phases 2–4 rows rewritten; the pharmacy index pick-up list updated; a blocker note to OpenCode in
agents/handoff/from-hermes; agents/handoff/hermes-resume refreshed; this entry.
Carry-forward: the S5 fix plus the two smaller findings are OpenCode's; Phase 4's eight decisions and
authorisation are Matthew's; Phases 5–6 still need specs. The walkthrough and W12 stay mine, both
gated — the walkthrough on the S5 fix, W12 on nothing but time. Phase tracker: unchanged — the staged
tidy-up's next phase is still policies/compliance/security.
Not a housekeeping pass — the pharmacy v1 programme, logged for continuity.
What was asked. Matthew: "okay lets get all the remaining phases done then" — phases 3–6, which the
record still shows as unauthorised and, except for phase 2, unspecified. The house rule (authorise →
spec → build → verify → document, one phase at a time, OpenCode building and Hermes verifying) was kept
rather than dropped, so this session took the first real step: it wrote and published the Phase 3
(stock and inventory) specification —
development/projects/pharmacy_app/build-records/phase-3-stock —
in the same shape as the authorised Phase 2 page: scope in and out, the data boundary restated, what
exists to build on, five pre-work gates, twelve work items with owners, seven decisions held for
Matthew each with a recommendation, the acceptance list and the risks.
Two codebase corrections came out of grounding the spec (both now on the module map's corrections
table, because the plan page had claimed otherwise): the pharmacy repo's
integrations/erpnext_client.py implements only items, customers, sales invoices, payments and PDF
fetch — no warehouses, batches, stock entries, item prices, accounts, journal entries, or submit/cancel
(the dental sibling has the fuller surface), and there is no provision_erpnext_config command
in this repo at all. So the ERPNext surface is Phase 3's critical path (gate G1), not a free reuse. Also
recorded for whoever specs Phase 4: the repo's payment surface is Stripe PaymentIntent
(card-not-present); a counter terminal is a new decision, not a reuse.
Wiki changes this session: NEW development/projects/pharmacy_app/build-records/phase-3-stock; module-map
delivery-phase table (phase 3 row, and a note on the phase 4 row) plus two correction rows; the
pharmacy_app/index pick-up list gained the phases-3–6 item; agents/handoff/hermes-resume refreshed;
this entry.
Carry-forward: Phases 4–6 still need specifications (each gated on the phase before it); Phase 3
build is gated on Phase 2 being deployed and accepted, which is still outstanding (the branch
9d74e53 is unpushed, five defects open, 0002 and the client-box migrate_client step to run); the
seven Phase 3 decisions sit with Matthew. Phase tracker: unchanged — the staged tidy-up's next phase
is still policies/compliance/security.
Later the same session — Phase 3 AUTHORISED by Matthew (2026-09-14). The spec page status is now
AUTHORISED, the module map's phase-3 row and the pharmacy_app/index pick-up list were updated, and
OpenCode has a go-ahead note: finish the Phase 2 close-out first, then start on gates G1 (the ERPNext
client surface) and G2 (a provisioned site) — the only Phase 3 work no decision touches. The
decision-dependent items (S1, S3–S7, and the warehouse-layout parts of S2/S5) are not to be built
until the seven D3.x decisions land; the ratification note follows from me the moment Matthew answers.
Phases 4–6 remain unauthorised and unspecified.
Same session, again — the seven Phase 3 decisions SETTLED, all as recommended (2026-09-14). ERPNext
owns the stock ledger with Django holding only a non-authoritative read-cache (D3.1); barcodes are data
only, no device integration (D3.2); two stock pools, Dispensary and Counter (D3.3); zero/negative stock
warns and records rather than blocking a dispense (D3.4); expiry bands 3 and 1 months with expired stock
never selected by FEFO (D3.5); one actor per receipt with a name snapshot (D3.6); a per-client
SNOMED↔item mapping table with a command import, no dm+d licence dependency (D3.7). So S1–S12 are
buildable once the gates clear. The last gate, G5 the movement vocabulary, was RATIFIED 2026-09-14
(receipt/consumption/adjustment/pack/item/batch, Dispensary/Counter) and joins the module map's pinned
list; OpenCode has the ratification note with the sequencing unchanged: Phase 2 close-out → G1/G2 → the
stock app, now building against fixed names.
Later still, same session — Phase 3 ACCEPTED and Phase 4 AUTHORISED (2026-09-14). OpenCode's session 7
fixed the S5 blocker (in two places: _replace_items now carries snomed_code, and the assembly payload
is derived from DispensingItem._meta rather than a hand-written list — the root cause I had only seen
the symptom of), fixed my smaller finding by recording an append-only STOCK_CONSUMPTION_SKIPPED audit
event and a badge, and added purge_dispensing_tokens, which I used on my own verification token. It also
found a bug my blocker had been hiding: batch_positions double-negated an already-signed quantity, so
every batch was inflated and FEFO read wrong figures — invisible until something was consumed. I
re-verified live myself (claim → assemble → check → dispense: coding survives, a 2.00 CONSUMPTION
movement against DEMO-AMOX-B2, Material Issue MAT-STE-2026-00022 docstatus 1, position 15.0 → 13.0)
and then purged and cancelled my artefacts, leaving the demo tenant tidy. Phase 3 is ACCEPTED. Matthew
then authorised Phase 4, whose spec was written earlier the same session: it now carries an AUTHORISED
status, the eight D4.x decisions still held, a closed G4.5 (Phase 3 accepted) and a drafted counter
vocabulary (gate G4.4: basket, sale, tender, change, refund, void, till session, takings, counter
customer) awaiting ratification. OpenCode has the note to start on G4.1 (the ERPNext tax-template
surface) and G4.2 (the Counter pool's contents and prices) and to leave the decision-dependent work
alone. And again — Phase 4's eight decisions SETTLED and the counter vocabulary RATIFIED (2026-09-14), both
"as recommended". Card tenders are recorded from the pharmacy's existing standalone terminal (the app
never sees card data, so no PCI or DSPT change and no Stripe Terminal work); cash with a cash-up; no split
tender in v1; an NHS charge is an anonymous line plus the opaque token reference against a single standing
counter customer; refunds by RP or Manager with a reason; no offline sales; print-and-email receipts;
one till session split by VAT band. The vocabulary (basket, sale, tender, change, refund, void, till
session, takings, counter customer) joins the module map's pinned list. All of C1–C13 is therefore
buildable once OpenCode's two gates close — G4.1 the ERPNext tax-template surface and G4.2 the
Counter pool's contents and prices — and it has the note with both the settled choices inline and the
warning that the VAT split must be a template fact, not Django arithmetic.
Same session, later — Phases 5 and 6 AUTHORISED with all twelve decisions settled (2026-09-14, "all as
recommended"). Both spec pages now read AUTHORISED with "Decisions — SETTLED 2026-09-14" and their
vocabularies ratified. The four calls that carried real risk are settled as recommended: a notification
carries the minimum (no medication names to a third-party service or whoever holds the phone); contact
details are captured by the pharmacy and held client-side; reimbursement becomes income only when the
money arrives, never at endorsement; and patient prescription charges collected for the NHS go against
the receivable, not to income — the tax error the accounting phase exists to avoid. Build order is
unchanged and deliberately protective: Phase 4's counter (C1–C13) → Phase 5 → Phase 6, so OpenCode has
been told to build neither yet; its immediate task is still to commit and push the Phase 4 gate work (and
not to squash migrations 0007/0008, which are already applied on live databases) and then build the
counter.
Phase tracker: unchanged — policies/compliance/security.
Not a housekeeping pass — the W11 verification duty on the pharmacy Phase 2 build, logged for
continuity.
What OpenCode delivered (one commit 9d74e53 on feature/pharmacy-phase2-dispensing, 24 files on
top of the now-pushed 2d9f64e/5f36292/2816aed): W4 the ratified state machine
(pharmacy_dispensing/machine.py), W5 the dispensary screens at /dispensary/, W6 role gating, W7 the
audit actions, W8 the synthetic seed, W9 the tests. Migration 0002 adds the assembler and checker
separately (D6).
Verified from the artifacts, not the report. machine.py matches the ratified transition/guard
table line by line and is the only place transition logic lives (RP-only check and endorse, both actors
recorded, DISPENSED never re-opened, release returns the token to AVAILABLE with a RELEASED
history row — the reading I intended). The runner gives 90 tests, OK on an independent re-run, and
the seed's second run reported exists (<STATE>), so idempotency is real. Read directly: 0002 is
applied nowhere — the apex sector_pharmacy database has 0001 only, and the client-owned box's
database likewise (0001 only, four tables, 0 rows, read through the app's own decrypted connection).
The central database (dsp_clinic) holds no dispensing table and no pharmacy audit table, so the
data boundary stands. The mock store's JSON file is not web-served (DEBUG is False live and
/media/ is wired only under DEBUG) — checked rather than assumed.
Six findings recorded on the spec page for OpenCode, the first of which is the one to remember
because it affects the walkthrough: the mock's file-backed store is cached as a class attribute for the
life of the process, so a seed run is not visible to an already-running pharmacy_app — the queue
will not show demo-token-available-1 until the container restarts, exactly the shape of the
alias-cache trap from 2026-09-13. The others: two transaction.atomic() calls missing using=
(ClaimTokenView.post, the seed), raw exception text rendered into three screens (internal host/DB
names could reach the UI), release contacting EPS before the local guard can refuse, no delete()
barrier on PharmacyAuditEvent although the transitions table has one, and the mock store file needing
a line in the data-store inventory.
Wiki changes this session: the Phase 2 spec page gained the "As delivered — W4–W9" evidence table
and the six findings, plus a status line; pharmacy_app/index pick-up item 1 rewritten; a note to
OpenCode in agents/handoff/from-hermes carrying the findings and the push ask (my two earlier notes
marked HANDLED / PARTLY HANDLED); agents/handoff/hermes-resume refreshed; this entry.
Carry-forward: 9d74e53 is unpushed and nothing is deployed, so live behaviour is unchanged;
deployment needs 0002 and migrate_client --slug test-client-pharmacy --app-label pharmacy; the
walkthrough and W12 (the roles guide, after W6 is live) are mine; the decisions still with Matthew
are unchanged (orphaned former database, git-history purge, the RP log's "who closed the shift").
Phase tracker: unchanged — the staged tidy-up's next phase is still policies/compliance/security.
(Repair note: this entry's write first overwrote the outage-resume entry below — a stale local
copy of the page was reused instead of a fresh read. Restored verbatim in the same session; the
housekeeping convention now is to re-fetch before every prepend.)
Not a housekeeping pass — the continuation of the pharmacy Phase 2 session, logged for continuity
after a power outage cut the previous one off.
What happened. The session resumed with nothing but the question "where were we?" — so the record
was rebuilt from the two handoff inboxes, the Phase 2 spec, this log and the pick-up point, and then
verified against the artifacts rather than the pages: the three W0/W2/W3 commits are intact on
feature/pharmacy-phase2-dispensing (2d9f64e, 5f36292, 2816aed), the working tree is clean, and
scripts/run_pharmacy_tests.sh still gives 53 tests, OK on the restarted host. Forgejo was checked
directly: origin/dev is still 983df48, so the branch remains a single unpushed copy — chased in
a fresh note to OpenCode.
Matthew ratified all three pre-work gates — "all as drafted". Terminology (W1), the dispensing state
machine (W4) and the mock EPS surface shape were all closed on 2026-09-14. The state machine as ratified:
AVAILABLE → CLAIMED → ASSEMBLED → CHECKED → DISPENSED → ENDORSED, side states RELEASED
and CANCELLED; the accuracy check and endorse are RP-only with the checker recorded separately
from the assembler; the same person may do both (both actors recorded, D6); DISPENSED is never
re-opened, a correction is an appended record. The mock's five-operation surface is the Phase 2
contract, with EPS_DISPENSING_BASE_PATH left a settings value by decision (the live switch, not now).
Consequence carried: migration 0001_initial is already recorded as applied on the apex and client
databases, so the ratified vocabulary must be used as-is — a later rename needs a new migration.
Wiki changes this session: pharmacy_app/phase-2-dispensing — the "Draft gates, do not build
against" section replaced by "Gates — RATIFIED 2026-09-14. Build against these", plus a ratified
mock-shape subsection, the status block, the pre-work gate list and finding 3 updated;
pharmacy_app/index pick-up item 1 rewritten (gates closed, W4/W5 unblocked); a go-ahead note to
OpenCode in agents/handoff/from-hermes carrying the ratified vocabulary, transition/guard table and
the build notes; agents/handoff/hermes-resume refreshed; this entry.
Carry-forward: W4/W5 are OpenCode's and nothing is holding them; the push is still the only cheap
insurance outstanding (origin/dev = 983df48); the two clean-ups from the earlier note stand
(unpushed branch, 0001 applied ahead of code); the roles-and-permissions guide (W12) is written
after W6 lands, one page at a time; the decisions still with Matthew are unchanged (drop the orphaned
former client database, purge the pharmacy git history, whether the RP log records who closed a
colleague's shift). Phase tracker: unchanged — the staged tidy-up's next phase is still
policies/compliance/security.
Not a housekeeping pass — the authorised next step for the pharmacy app, logged here for
continuity (same practice as the 2026-09-13 pharmacy sessions).
What happened. Matthew reviewed the live app (signed in on the client host as the hand-testing
identity) and judged it "little more than the login features" — correctly: Phase 1 is identity,
premises identity and the Responsible Pharmacist Log, and nothing else exists. He then authorised
Phase 2 (dispensing core). This session wrote and published its specification:
development/projects/pharmacy_app/build-records/phase-2-dispensing —
scope in/out with the deliberate "dispensing stops before handover" boundary call, the data-boundary
table restated for patient-identifiable rows, what exists to build on, five pre-work gates, eleven
work items (W0–W11) with owners, six decisions held for Matthew (D1–D6) each with a recommendation,
the phase-acceptance list, and the risks.
Phase 1 re-verified from the live system, not the record. Before writing the spec I checked the
app rather than trusting the pages: both pharmacy hosts render the four Phase 1 surfaces for a
pharmacy-realm identity (dashboard, RP log, premises, verification — all 200), and all four
non-pharmacy roles are refused with 403 at the view layer. The credential question came up and was
answered with evidence, not memory: the host-local demo credential authenticates tc.rp.one and
tc.rp.two but not tc.test.user, whose password was deliberately never written to disk — so it
cannot be recovered, only reset. Verified with a throwaway script that printed outcomes only.
Two findings folded back into the plan (both now on the module map). (1) The inherited EPS
integration is prescriber-side only — issue_prescription / check_status / cancel_prescription /
search_prescriptions, with no claim, release, return, dispense or endorse operation anywhere in the
layer. So Phase 2's pharmacy-side EPS surface is new work, not wiring, and the module map's
"reuse the EPS client" reading was optimistic. (2) The AES client signs documents, not state
transitions (patient/medication/pharmacy in, signed PDF + SHA-256 out), so the module map's
"AES-signed transitions" became a held decision (D1, recommended: build no signing in Phase 2) rather
than an assumption. Also corrected a stale module-map line claiming the RP sign-on/off cycle had never
been exercised live — it was, on 2026-09-12.
Wiki changes this session: NEW development/projects/pharmacy_app/build-records/phase-2-dispensing;
module-map status + phase table + open items (Phase 2 row rewritten, the two findings recorded, the
stale line corrected); index status, pick-up list and housekeeping history; a new OPEN note for
OpenCode in agents/handoff/from-hermes; this log entry; agents/handoff/hermes-resume refreshed.
Carry-forward: the pre-work gates are now the blocker, and W0 (the routing guard) must land
before any Phase 2 code — the alias fallback writes client rows into the central database rather
than failing, which is tolerable for a premises row and not for patient-identifiable dispensing
records. The six Phase 2 decisions were settled later the same session (D1 signing deferred, no
signing built; D2 charge declaration only, no money; D3 endorsement local only; D4 repeat dispensing
deferred but not precluded; D5 medication history out of scope; D6 the accuracy check is an RP step
with both actors recorded) and briefed to OpenCode. Gates 2–4 (terminology, state machine, mock EPS
surface shape) remain open: the first two are drafted on the Phase 2 page and await ratification,
so OpenCode can start W0/W2/W3 but must not build W4 or the state-dependent screens yet. Matthew moved
the roles-and-permissions guide into Phase 2 (new W12, written after W6 lands) and left the RP-log
"who closed the shift" question undecided — held, not blocking. The older open items are unchanged: the orphaned
former client database, the pharmacy git-history purge, the tunnel monitoring gap, and the defectdojo
push-count cosmetic fix.
Same session, later — OpenCode built W0/W2/W3; Hermes verified them. Three commits on
feature/pharmacy-phase2-dispensing (2d9f64e W0 routing guard, 5f36292 W2 client-tier dispensing
models, 2816aed W3 pharmacy-side mock EPS surface). Verified against the artifacts: the guard raises
ClientDatabaseUnavailable instead of falling back to the central database (three tests assert it,
and its log lines fire in the suite run); pharmacy_dispensing is registered in INSTALLED_APPS and
as a one-line PHARMACY_APPS addition; the real EPS client raises NotImplementedError for the
pharmacy surface rather than pretending; W4/W5 were correctly not built (states.py carries an
explicit draft header); nothing was signed; the dispensing tables are client-tier only; 53 tests
green under the documented runner. Two findings recorded on the spec page: the branch is unpushed
and undeployed, and migration 0001 was applied to the live apex and client databases ahead of any
code (empty tables, so a state-vocabulary rename now needs a new migration rather than an edit).
Phase tracker: unchanged — the staged tidy-up's next phase is still policies/compliance/security.
Not a housekeeping pass — the authorised next pharmacy step (decisions taken earlier the same day,
execution deliberately left for a fresh session). Logged here for continuity.
Outcome (verified live, synthetic data only): the pharmacy test client's database now sits on the
client-owned box that already serves the dental client, in its own database with its own login role
in that box's cluster, reached down a dedicated tunnel of its own. The dental tunnel has not
restarted (active since 2026-09-11), the dental database is untouched, the client host, vhost, TLS, app
deployment and Keycloak wiring are unchanged, and the apex database plus the shared client-tier
instance are unchanged. Verified live: real OIDC login through the pharmacy realm for both synthetic
pharmacists, the full RP cycle (including a refused concurrent sign-on), 2 RP log rows and 4 audit
events in the box's database, and a three-way isolation check. A ClientVPS row was recorded for the
client and the shared box's RAM drift (4 GB recorded / 1.8 GB real) corrected.
The finding that matters: the per-client database alias is built once per process
(tenancy/routers.py::_load_dynamic_aliases caches into settings.DATABASES), so the retarget only
took effect after the app restarted. Before the restart the live cycle wrote its sign-on/off pairs into
the old database while every page looked right — nothing failed visibly. Raised with OpenCode; the
runbook now carries the restart as a mandatory step.
Ops (matthew/veripath-ops e442488, pushed): two tunnel units (systemd/pharmacy-tunnel.service
on the box, systemd/tun-vps1-pharmacy.service on the app engine — the latter deliberately separate so
the running GP/dental DB forwards are never restarted), bring-up.sh wiring, and four scripts
(retarget the tenant row via the app seed; re-seed a client tenant with the credential on stdin; the
end-to-end live verifier; the ClientVPS inventory record).
Wiki changes: development/projects/pharmacy_app/operations/client-vps rewritten (status COMPLETE, the
runbook including the restart step, the evidence table, the findings); pharmacy_app/index status,
pick-up list and housekeeping history updated; pharmacy_app/test-client database row and open items
updated with a pointer to the move; new note for OpenCode in agents/handoff/from-hermes;
agents/handoff/hermes-resume refreshed.
Carry-forward: the client's former database is orphaned on the shared client-tier instance (holds
two stray sign-on/off pairs from the pre-restart run) — drop it when Matthew is happy; the roles and
permissions guide is still the last owed Phase 1 document; the pharmacy git-history purge is still
outstanding; the box's loopback pg_hba trust now covers two clients' data and the box has no
tunnel health check for this client (the watchdog covers the GP and dental tenants only).
Phase tracker: unchanged — the staged tidy-up's next phase is policies/compliance/security.
Not a housekeeping pass — this session took the pharmacy app's authorised next step (stand up a test
client so Phase 1 is exercised against a client-owned instance, not only the apex tenant). Logged here
because the wiki pages it touched are the ones this log tracks, and because the pharmacy pick-up point
lives on development/projects/pharmacy_app/index.
Outcome (all verified on the live system, synthetic data only): test-client-pharmacy exists — a
client-sector tenant with its own database and dedicated login role on the client-tier Postgres
instance, two synthetic pharmacists in the pharmacy realm, its own host and Let's Encrypt certificate,
served by the existing app-engine deployment (tenant resolved from the Host header, so no new tunnel
port). Phase 1 was exercised end to end on that host: real OIDC login through the pharmacy realm, RP
sign-on, a concurrent second sign-on refused, history, sign-off — with the client database and the apex
database checked separately to prove the boundary held.
Code shipped (matthew/pharmacy_booking_app dev): 20de069 client-tier alias routing in
tenancy/routers.py (pharmacy models previously always routed to the apex sector database), the
missing ClientVPS.sector migration, the CSRF wildcard, per-tenant seed options, 5 new tests;
1caa3a9 a migrate_client command (the dynamic per-client alias is not registered at argparse time);
c987d5f RP sign-on transaction now follows the tenant's database (it was pinned to sector_pharmacy).
Pharmacy suite 26 green. Ops (matthew/veripath-ops e6bf083): the client vhost plus two
idempotent provisioning scripts, both executed.
Wiki changes made this session:
development/projects/pharmacy_app/operations/test-client — what exists, the isolation evidence, the--app-label pharmacy for clientdevelopment/projects/pharmacy_app/index — status block, next-session pick-up list, "where to pickagents/handoff/from-hermes — new note (router review, the users_customuser gap on fresh clientagents/handoff/hermes-resume — rewritten for the new state.Sidebar check: the new page reports navigation OK and its parent
development/projects/pharmacy_app is confirmed a folder node (checked with --folder, "Already a
folder"). Note for future sessions: this wiki's sidebar is rendered client-side, so a curl of the page
HTML cannot be used to verify the tree — use the tool's own navigation confirmation and --folder.
Carry-forward (unchanged in substance): the pharmacy roles and permissions guide is still the
last owed Phase 1 document; ClientVPS inventory row for this client is a decision for Matthew (none
was created — no client VPS box was provisioned, the data sits on the platform's client-tier Postgres
instance as the GP test client's does); the pharmacy git-history purge of the old demo credential is
still outstanding; and the pre-existing migration drift (infrastructure help text / Meta and
integrations.config_json) remains untouched — this session added only 0007, the sector choice.
Phase tracker: the staged tidy-up's next phase is unchanged — policies/compliance/security.
Published set, live on https://manuals.dental.veripath.co.uk/guides/ — Financial Processing & NHS Claims group, in sidebar order:
| # | Page | State |
|---|---|---|
| 1 | private-ledger-billing |
re-drafted (bcbe8db) + cross-links |
| 2 | nhs-contract-and-fp17-journey |
NEW to the manuals site (34ac1ed) — the finance intro / full-journey guide, 210 lines, Parts 1-7 |
| 3 | fp17-claim-submission |
re-drafted (bcbe8db) + cross-links |
| 4 | invoicing |
re-drafted (bcbe8db) + cross-links |
| — | uda-auditing |
re-drafted port + refreshed dashboard shot (0fb865e) |
| — | co-payment-splitting |
RETIRED (deleted, page removed) |
Link policy now applied (user decision 2026-09-10): NO wiki URLs in the financial guides. Every cross-link points at a published manuals page. The journey page made this possible — all six of its link targets already existed on the manuals site, so publishing it closed the graph. Verified with a site-tree link check: zero broken internal links across all five pages, zero wiki.veripath.co.uk links on them.
Journey page notes: published from the wiki copy with the wiki H1 dropped (title from frontmatter), links rewired to manuals paths, and the wiki status banner trimmed — the honest caveat is kept ("the month-end and associate-pay screens in Part 7 are written from the app but have not yet been walked through live; figures are illustrations using synthetic demo patients"), the internal "detailed guides are being refreshed after this page" line is dropped. Its four new illustrations (nhs-journey-flow, nhs-ledger-home, course-ready-to-close, appointment-billed) were taken from the wiki copies. Group = Financial Processing & NHS Claims, sort 2 (fills the retired co-payment slot).
Wiki-side tidy done at the same time: the dead /guides/work_processes link (that path does not exist — the real page is development/projects/dental_app/work_processes) removed from the wiki copies of uda-auditing, fp17-claim-submission and private-ledger-billing.
Carry-forward (NOT blocking the close, no action taken):
wiki.veripath.co.uk links: appointment-booking, fdi-coordinate-charting, graphic-charting-logs, medical-history-verification, recording-symptoms-and-triage. Ask Matthew before republishing those.emergency-slot-management.md rewrite is still parked in the veripath-site working tree (stashed across every build; old committed version live) with its unreferenced screenshots symptom-checker.png / triage-hard-stop.png.Reusable procedure captured (this round, for the next manuals publish): edit content/user-manuals/<slug>.md -> git stash push the WIP emergency file + move its two PNGs out of public/dental-app-guides/ -> git add only the intended files -> commit + push to origin/main -> /opt/deploy-manuals.sh (rebuilds full + clinic + dental, reloads nginx, verifies) -> verify the built tree under /var/www/manuals-dental (content strings, absence of the retired page, image presence, link check) -> git stash pop + move the PNGs back. remember: the manuals build reads the REPO copies, so any re-draft done only on the wiki silently publishes stale text (that is how uda-auditing was missed).
Why it was missed: the re-drafted guides/dental_app_guides/uda-auditing lived
only on the wiki — the repo mirror content/user-manuals/uda-auditing.md still held
the pre-NHS-accounting text (Band 2 = 3 UDAs, 2025/26 examples, no fee-basis card),
so the manuals build published the stale version. The other three financial guides
had been re-drafted repo-side, which is why only this one lagged.
Published (commit 0fb865e, pushed; /opt/deploy-manuals.sh OK 15:30Z): wiki body
ported to the Decap copy — H1 dropped (title comes from frontmatter), wiki-only
"Status: verified" banner dropped (matches the manuals house style of the trio),
band/UDA table added (2a=3 / 2b=5 / 2c=7 / Band 3=12 / urgent 1.2 default / exempt
0.5-1.2), Delivered NHS fee basis (Leg B) card + Delivered £ column, transmitted-period
counting. Links rewired: manuals pages -> /guides/<slug>/; wiki-only pages
(nhs-contract-and-fp17-journey, work_processes) -> absolute
https://wiki.veripath.co.uk/en/... per the existing precedent in
fdi-coordinate-charting / appointment-booking. uda-dashboard.png refreshed from the
wiki copy (the wiki's was regenerated during the re-draft) — OCR-verified it shows the
"Delivered NHS fee basis" card and the "Delivered £" column. Group left as
Governance, Retention & Tenant Management (matches guides/dental_app_guides/all_activities).
Verification: live page carries the new content (Band 2c, Leg B x4, fee basis x3),
no stale phrases, no relative wiki links left, refreshed screenshot on the live tree
(fee-basis card OCR-confirmed), emergency page still the old version, WIP screenshots
absent. All URLs 302 (auth gate).
OPEN — fidelity divergence, waiting on Matthew: the wiki copies of the four
financial guides are slightly richer than the published manuals copies (they carry the
"full journey" / work_processes cross-links, and fp17 adds a month-end pointer
sentence + "the same table the app uses" + a date-effective-table note). The manuals
build was made from the repo drafts; those cross-link sentences were never ported.
Offer made to sync the manuals copies with absolute wiki links (as used elsewhere on
the site) — awaiting his call.
What was published (commit bcbe8db, pushed to origin/main; /opt/deploy-manuals.sh
ran OK 15:10Z, all 3 sites rebuilt, nginx reloaded): the re-drafted financial
trio + retirement, verified live on https://manuals.dental.veripath.co.uk/guides/:
private-ledger-billing — rewritten vs the NHS-accounting model (charges/statuses,fp17-claim-submission — rewritten (one claim per course from close-and-bill,invoicing — rewritten (invoices born at course close, one per charge, no moreco-payment-splitting — RETIRED (content file deleted; page removed from theuda-auditing repo copy so no 404).Kept OUT of the publish (deliberately): the in-progress
emergency-slot-management.md rewrite (symptom checker / red-flag hard stop /
triage queue) plus its unreferenced screenshots symptom-checker.png &
triage-hard-stop.png — parked in a git stash across the build as before; the OLD
committed emergency page remains live (verified). Restored to the working tree
after the deploy, where it still awaits Matthew.
Verification: site tree byte-checked — 3 financial pages carry the new content,
co-payment-splitting dir gone, zero remaining links to it sitewide, emergency page
unchanged, WIP screenshots absent from /var/www; all guide URLs answer 302 (auth
gate) as expected.
Still open for the doc pass: the wiki mirrors of these three guides were already
in sync (checked); the FP17 claim-detail "Treatment Codes" gap and the month-end
ACCEPTED-status gap are with OpenCode (from-hermes OPEN notes, 2026-09-10).
Why: during the FP17 walkthrough the seeded scenery confused Matthew (attended NHS appointments with no course/claim). Decision: remove nearly all seeded data and start from a handful of appointments so a claim can be seen being born from scratch. Full U-round work remains untouched (code + tests); this is demo-data state only.
What was done (test-client-dental):
Next: live walkthrough from zero — user drives: Mei close (2A £76.60) → claim/tx/invoice born → submit → accepted → UDA; then Daniel private open/close (£60), Fatima exempt open/close (£0 + fee basis); then payments, month-end statement/match/journal, associate pay. Guide page guides/dental_app_guides/nhs-contract-and-fp17-journey is the walkthrough script (DRAFT banner until verified). Doc pass after.
What happened: Matthew did fresh FP17/UDA research (wiki development/projects/dental_app/fp17_uda — full lifecycle, netting, band matrix, highest-band-wins, exempt CoTs, continuation/2-month) and decided to complete the system BEFORE further testing (testing the half-model would be counter-productive).
Gaps found (code-verified): banding engine returns plain 1/2/3 (no 2a/2b/2c); UDA map static {1:1,2:3,3:12}; no urgent UDA credit; no charge-exempt CoT catalogue; no continuation/2-month logic; superannuation refused in posting; no contract-performance view.
Round raised: spec development/projects/dental_app/fp17-uda-completion (U1 band-code engine cascade incl. molar-endodontics 2C / urgent; U2 code-keyed decimal UDA map + nhs_band CharField migration with re-derivation; U3 exempt CoT catalogue £0/£0, Rx-only no claim; U4 two-month rule + continuation window; U5 superannuation 4th journal leg + Superannuation Payable provisioning; U6 contract monitor 96%/clawback-estimate). Decisions D1–D7 confirmed by Matthew ("go with your recommendations") — incl. 2a/2b/2c = 3/5/7 UDAs (matrix beats the cheat-sheet's 5), urgent default 1.2 on the contract, exempt catalogue values per research. from-hermes top note OPEN; OpenCode to commit uncommitted M1–M6 first, hold push until Matthew confirms.
Statuses: from-opencode M1–M6 note → HANDLED (M7 prep done: accounts provisioned + config wired incl. realm fix; seed backfill done; three-story demo + M4/M6 exercise + doc pass deferred to post-round). Test menu A/B/C/D paused. ERPNext config row fully wired (clearing/contract-income/bank/income/receivable + keycloak_realm=test-client-dental) — re-verified during prep.
What happened: after OpenCode shipped M1–M6 (from-opencode note, OPEN), ran the M7 live prep the spec assigns to Hermes:
Test menu ready (user at the wheel): A) close Mei's OPEN band-2 course live → claim DRAFT £76.60 + 'NHS Patient Charge' tx + invoice to the clearing account (verifies M1 account routing); submit/transmit via mock gateway → ACCEPTED → UDA. B) collect Daniel £60 (record-payment) → Payment Entry on ACC-SINV-2026-00030 (417-fix regression). C) Fatima claim 232 exempt submit/transmit (£0 charge, fee basis present). D) month-end: NHS Ledger > Enter Statement, three-way match, ERPNext journal, associate pay on reconciled UDAs. Cosmetic note: UDA allocations show legacy duplicate rows for Sarah/Omar (older period rows 11/12 vs 14/15) — harmless, can tidy later.
Next: user runs the menu; then doc pass (one page at a time): fp17-claim-submission, invoicing, uda-auditing, private-ledger-billing + finance intro (clearing/schedule/pay), co-payment-splitting retire.
What happened: before sending OpenCode the fp17-nhs-accounting-model
implementation round, tidied both handoff inboxes per agents/handoff/index
(archive-not-delete — audit trail kept, embedded '===='/'Page:' wrapper junk
stripped on rebuild, byte-verified after upsert):
Residual (small, tracked): walkthrough-test-users RBAC section (charting R3
doc ask) — role-permissions-matrix clinical-notes coverage + clinical-notes guide
delivered; that section/page still to create if wanted.
Next: user sends OpenCode the NHS-accounting-model note; once M1–M6 green +
migrations applied, Hermes runs M7 live re-prep (ERPNext accounts + config
wiring on practice 58 BEFORE any re-sync, coordinated demo reset, course stories
Mei/Daniel/Fatima + month-end statement/matching + associate pay). Testing menu
A/B/C paused until the build lands.
What happened: Matthew returned from a research discussion (Gemini) on how FP17 money actually works: NHS treatment is NOT traditional bookkeeping — practices never invoice the NHS; income is fixed monthly contract payments (1/12 ACV), claims are UDA tokens against the contract, and patient band charges collected locally are clawed back from the next payment. Private treatment IS standard invoicing (already correct in the app). Conclusion: the P1/P2 redesign (9d8bded4) got the CoT/completion model right but its accounting shape wrong — the NHS patient charge posts to fee income like private work (overstates earnings), the second (NHS) leg per claim is missing, and the deferred "Phase 3" (schedule reconciliation) is actually the core NHS accounting. "Restore the split" comes back in corrected form: dual-leg DATA per claim (patient charge + NHS fee basis), NOT the old two-invoice NHS construction.
Decisions locked (F1–F5, F3b, all confirmed): patient-charge invoices post to a dedicated NHS Patient Charges (Clearing) account (private keeps fee income); fee basis stays the date-anchored statutory band charge (read-only after transmit); schedule reconciliation built NOW (v1 manual month-end statement entry; real DCS file import later); monthly statement → ERPNext Journal Entry Dr Bank (net) + Dr clearing (clawback) / Cr NHS Contract Income (gross); associate pay built now on RECONCILED UDAs only; demo tenant reset after the build.
Posted: spec page development/projects/dental_app/fp17-nhs-accounting-model + from-hermes note (top, Status OPEN). from-hermes statuses updated: 417-bug note + private-income note → HANDLED (shipped in P2 9d8bded4); billing-redesign note → P1+P2 HANDLED, superseded in part by the new note; seed-fix note → OPEN, absorbed as M7.
Next: OpenCode builds M1→M6 (suite green + migrations on central + tenant), then Hermes runs M7 live re-prep: ERPNext accounts + DentalERPNextConfig wiring on practice 58 BEFORE any re-sync, coordinated demo reset, re-run the three course stories (Mei close → £76.60 NHS-charge invoice to clearing; Daniel private £60 → fee income; Fatima exempt £0 + fee basis) + one month-end statement/matching exercise + an associate pay statement. Testing menu A/B/C stays paused until the rebuild lands. Doc pass (one page at a time) after the build: invoicing / fp17-claim-submission / uda-auditing + finance intro — now covering the clearing account, schedule reconciliation and associate pay; co-payment-splitting page to be retired. Working notes: /root/work/notes-fp17-nhs-accounting-model.md.
Post-pivot work completed (this session, after the redesign spec): (1) Full demo reset per D6 — seed_dental_demo --tenant test-client-dental --reset (radiographs preserved; ERPNext ACC-SINV-00026/27/28 were already cancelled by OpenCode's P2 cleanup; orphaned ACC-SINV-2026-00029 cancelled during this reset). (2) CRITICAL handling: the reset deletes DentalPractice + DentalPaymentConfig and CASCADES DentalERPNextConfig — none recreated by the seed. Snapshot + raw upsert restored both configs onto the NEW practice pk 58 (was 57; /dental/financial/57/ URLs now stale — practice is 58) — verified decryption round-trip + ERPNext auth (client works) + Stripe keys intact. (3) Post-seed surgical strip of pre-redesign rows the seed still emits (claims 228–231 course-less legacy incl. REJECTED + ACCEPTED-no-invoice; tx 563–573 legacy local-only PAID) — flagged to OpenCode as a seed fix (from-hermes note, OPEN). (4) Synced Daniel's seeded private invoice 18 → ACC-SINV-2026-00030 (SENT) so Story B is ERPNext-ready.
Clean baseline now: courses 13 OPEN NHS Mei (close live → b2 claim £76.60 + patient-charge invoice), 14 COMPLETED private Daniel (£60 PENDING, invoice 18 SENT ACC-SINV-2026-00030), 15 COMPLETED exempt NHS Fatima (claim 232 DRAFT £0 UNDER_18). Local tx = 574 only; claims = 232 only; invoices = 18 only. ERPNext dental site: 1 open invoice (ACC-SINV-2026-00030). Config practice 58 with site_url/company/accounts/realm intact + GBP precision fixed (verified £60.00 rounding).
Testing paused mid-menu: Matthew had 3 tests queued (A: Mei live course-close → claim+invoice+ERPNext sync; B: collect Daniel £60 → Payment Entry on ACC-SINV-2026-00030 [exercises 417 fix]; C: Fatima exempt claim submit/transmit). Returning to OpenCode first (likely re: seed fix + anything found). Next Hermes session: pick up the test menu or the doc pass (post-build, one page at a time). from-hermes OPEN notes (top→bottom): seed fix / billing redesign / 417 bugs / private-income-to-ERPNext.
STATUS: Major product pivot mid-session, fully spec'd and raised to OpenCode. Live testing exposed that the app's NHS billing model diverges from real FP17 practice. Matthew researched the real process (wiki gp_booking_app/fp17_claims — the authoritative reference page) and decided a redesign: charges follow TREATMENT COMPLETION (not check-in); the Course of Treatment is the billing unit (build the CoT model now); the invented NHS/patient SPLIT-PAYMENT model (£76.60 NHS + £18.40 co-pay on a £95 course) is REMOVED — real practice charges the statutory band charge (or £0-exempt); NHS income = claim acceptance → UDA → contract payment, never an ERPNext "NHS invoice"; no acceptance grey area (validate before transmit). Spec page: development/projects/dental_app/fp17-course-billing-redesign (Page ID 572). OpenCode note on top of from-hermes (Status OPEN) with phasing P1–P3. b181a295 (check-in billing) superseded.
Session findings (all in from-hermes, 2026-09-08): (1) PRIVATE income never reached ERPNext — no private Sales Invoice flow exists (local tx PAID + Stripe PI fine; sync skips 'No ERPNext invoice') → Matthew decided private MUST flow to ERPNext (same business). (2) Co-pay Payment Entry sync fails 417 — TWO root causes: sync task passes tx.total_amount (£95) not the invoice amount; AND the dental ERPNext site rounds GBP to whole pounds (GBP precision 0dp — grand £18.40 → outstanding £18.00; NHS £76.60 → £77.00; check GP site too). No ERPNext residue; recovery = re-run sync post-fix. (3) Claims list Fee column shows £0.00/£N/A — total_nhs_fee never populated at claim creation (214–218). (4) Tom Bennett £60 private payment capture verified good locally (tx 515 PAID, pi_1UDMTxV05DCU8). Matthew's £105 figure was user error — ignore.
Wiki hygiene note: multiple pages (agents/handoff/from-hermes, financial-billing-test-plan, housekeeping-log) had accumulated EMBEDDED tool-wrapper junk ('Page: https://...'/'====' lines stored into the body by older naive --get>upsert round-trips). Test plan page was cleaned during today's edits (byte-verified). from-hermes + housekeeping-log still carry embedded junk — preserved (append-only) to avoid corruption; a careful cleanup pass (strip wrapper lines, verify length) is a future hygiene task — phase 4+ or when next doing a full rewrite of those pages. Also: first-'Content'-marker vs H1-offset slicing both needed; verify with probes before any upsert.
State for next session: financial testing of pre-redesign flows PAUSED until OpenCode ships the redesign (P1 CoT/completion billing; P2 charges/exemption + ERPNext invoicing + GBP-precision fix; P3 NHS schedule side). Remaining testable meanwhile: local payment capture mechanics; FP17 claims walkthrough only under the OLD flow (low value now). Doc pass (one page at a time) after build: private-ledger-billing, co-payment-splitting (retire/rewrite), invoicing, fp17-claim-submission, uda-auditing + finance intro. ERPNext UI-trim site-config parts (Hermes) still queued. Demo re-prep (D6) after build. From-hermes notes 2026-09-08: redesign (OPEN, top) + private-income (OPEN) + 417 bugs (OPEN) + earlier HANDLED items below.
STATUS: Matthew now has full ERPNext dental access (SSO silent, desk renders ACC-SINV-2026-00026). All four blockers from the 2026-09-06 evening handover RESOLVED by Hermes directly (notes marked HANDLED): (1) SSO realm mismatch → client erpnext created in realm test-client-dental + ERPNext site_config keycloak_realm + DentalERPNextConfig row → test-client-dental (GP pattern); (2) Notification Type doctype missing on BOTH ERPNext sites → sync_for('frappe') + verified real user-provisioning path (lean 773-doctype installs, hide_modules only blocks Administrator desk modules — it never deleted doctypes); (3) blank desk = /assets 404s → bench build on VPS3 + re-sync to VPS1 /var/www/erpnext-assets (nginx serves locally; do NOT proxy — gunicorn NO_STATICS-equivalent); (4) "Not permitted" for manager = NOT permissions — headless-Chrome repro showed every user bounced to /app/setup-wizard (site provisioned headlessly, is_setup_complete=False) and desk_page.getpage 403'd (legacy Page doctype, zero DocPerm rows) → set is_setup_complete=1 (Installed Application frappe/erpnext/healthcare_accounting + System Settings) + clear_cache on dental AND GP sites. Verified in real browser: invoice page renders, no 403s.
NEXT MISSION (Matthew, spec'd + OPEN note for OpenCode): ERPNext dental UI trim — hide/deactivate ~90% of unneeded features mirroring GP (reference: gp-booking-erpnext-integration plan "Module Cleanup/Settings Cleanup/Workspace Visibility/Role Mapping"). Spec page: development/projects/dental_app/erpnext-ui-trim. Code parts → OpenCode (login.py role-claim mapping, blocked-modules at user creation + backfill, protocol mapper on erpnext client realm test-client-dental). Site-level config (workspaces, settings hides) → Hermes when Matthew starts. FP17 walkthrough (submit 210/advance 211/212 reconcile demo + fresh £95 band-2 course for two-invoice flow) + Stripe test still QUEUED behind this.
Facts: ERPNext dental site_config.json now keycloak_realm=test-client-dental (secret untouched). GP ERPNext site also fixed for the same latent issues (doctype + setup flags; GP site keycloak_realm was already test-client). Assets: any future bench build on VPS3 must re-sync to /var/www/erpnext-assets (VPS1) — automation ask still OPEN for OpenCode. Frappe v15.118; tabSessions user label 'erpnext_test_client_dental' is a harmless quirk for SSO logins. Browser test rig: /tmp/pwenv + /root/.agent-browser Chrome (playwright, executable_path) — reusable for UI verification.
STATUS: Big progress today. Price list v1 + NHS band charges SHIPPED by OpenCode (353ceee1, suite 135 green, seeds live both tenants — Hermes verified rows + date resolution: Mar-2026 appt → 2025/26 £73.50, Apr+ → 2026/27 £76.60, urgent £27.90). ERPNext invoicing UNBLOCKED end-to-end: first invoice live (claim 212 → ACC-SINV-2026-00026, NHS £70.70, submitted on test-client.accounts.dental, local Invoice row 14 SENT).
ERPNext fixes made by Hermes directly on the practice-57 config row (root causes, all logged to OpenCode): (1) api_key had stray leading '=' (provision ingestion bug) → 401s until stripped; (2) config company 'Test Client Dental Practice' ≠ real site company 'Test Client Dental'; (3) default income/receivable accounts empty → populated from company defaults ('4010 - Fee Income - Private Consultations - TCD' / '1120 - Accounts Receivable - Patients - TCD'). Reconcile + sync then SUCCESS.
STILL OPEN (from-hermes notes, newest on top, all for OpenCode): (a) ERPNext SSO login 500 — 'Notification Type' DocType MISSING on the dental site (hide-modules bootstrap stripped it; breaks first-time Keycloak user provisioning → Matthew hit it clicking "Open in ERPNext" on the invoice; fix = restore doctype + bench migrate on VPS3; same check for the GP site); (b) provision_erpnext_config hardening (the 3 ingestion bugs); (c) reconcile observations: fully-NHS-covered tx marked PARTIAL (semantics question), sync_fp17_claim_to_erpnext idempotency guard.
TOMORROW (Matthew's plan): FP17 walkthrough (submit 210 DRAFT, advance 211 TRANSMITTED → ACCEPTED, reconcile each → invoices; seed one fresh band-2 course total > band charge, e.g. £95 → NHS £76.60 + patient co-pay £18.40 so a PATIENT invoice appears), then the Stripe payment test. Matthew noted he wants to understand the FP17 process properly.
Facts to remember: demo tenant DB reach via tenancy dynamic alias sector_client_test-client-dental (call tenancy.routers._load_dynamic_aliases() first in shell; set_current_tenant before ORM). ERPNext site test-client.accounts.dental.veripath.co.uk — Administrator token auth works with the FIXED config pair; accounts on site: company 'Test Client Dental', income '4010 - Fee Income - Private Consultations - TCD', receivable '1120 - Accounts Receivable - Patients - TCD'. Attended private appts vs price list mismatch noted (£55/£120/£45 historic vs list £60/£350/£80); appt 1295 (Elena EMERGENCY 3 Sep, attended) has NO transaction — billing candidate at £80 when intake exists. Prices = seeded placeholders, practice-editable at /dental/settings/prices/ (DA/DPM/ADMIN/SU).
STATUS: Round-2 live walkthrough done (Matthew on /dental/financial/ + /dental/financial/57/). ONE consolidated change request RAISED to OpenCode (from-hermes 2026-09-06, Status OPEN, newest on top — "house in order"). Matthew has gone to OpenCode to implement. Retest + doc pass resume when it ships.
Live + DB re-verified this session: c25db51a month-scope + Revenue Breakdown work as shipped. Sept revenue £0 everywhere is CORRECT — 14 Sept appointments exist, ZERO Sept transactions (all 11 tx £471.30 are Jan–Aug, all PAID). Confusion was presentational.
Matthew's findings / decisions (all in the OpenCode note + test-plan 2026-09-06 decisions block):
partner:stripe_config = /partner/stripe-config/ (GP app). Dental has NO payment-config screen (DentalPaymentConfig admin-only dental/admin.py:82; only stripe_webhook route exists). Options (a) build practice-scoped dental payment-config screen (Phase-1 Path-A deposit intake needs a home) or (b) hide link. Matthew: sort first.OPERATIONAL FACT (learned this session): demo tenant DB is NOT in static settings.DATABASES. Reach it via the tenancy dynamic alias: in docker exec dental_app python3 manage.py shell, call tenancy.routers._load_dynamic_aliases() then Tenant.objects.using('default').get(subdomain='test-client-dental') → alias sector_client_test-client-dental → DB test_client_dental @ 172.18.0.1:5436 (tunnel). Model field note: DentalPractice.practice_name (NOT .name); FP17Claim band attr is .band but claim rows show band=None (band lives on the appointment / nhs_band; per-claim fee fields total_nhs_fee/patient_charge). Sept practice-57 numbers used above verified via this path.
NEXT SESSION (when OpenCode ships): retest each item live on test-client-dental (manager.test-client-dental demo user); then resume the finance journey — deposit + balance payment paths (Phases 1-2; blocked on Payments-nav fix), UDA dashboard/allocations screens (Phase 6, unaffected by the change), then ERPNext phases 4-5 (config provisioned practice 57; seed fresh £95 band-2 claim for a clean Reconcile demo — claim 212 £70.70 is below the 2025/26 £73.50 band charge = edge case). Doc pass stays queued (ONE page at a time): private-ledger-billing → co-payment-splitting → invoicing → uda-auditing + finance intro page (/root/work/notes-finance-intro.md). --prune the test-plan page when Phase 8 done. Insurance thread + rejected-claim→patient-bill rule stay PARKED (Matthew's research).
STATUS: Financial & billing testing pass STARTED (Phase 0 + first live tour).
Financial Dashboard + Practice Financials verified live + DB on test-client-dental.
CLOSE-OUT 2026-09-05: OpenCode shipped both finance items (c25db51a, deployed —
dashboard revenue now month-scoped; Practice Financials gains a "Revenue Breakdown —
{month}" ledger with funding-stream badges + collected/still-due chips) and
re-provisioned the ERPNext config for practice 57 (row verified live). Hermes
verified all three in the container. Live re-verify on screen + continue the journey
NEXT session. Two OPEN notes in from-hermes now marked HANDLED.
Key facts (detail on development/projects/dental_app/financial-billing-test-plan — READ at next finance session):
NEXT SESSION: read from-opencode first (revenue fix + config likely landed). Then:
re-verify month-scoped dashboard + breakdown live; continue finance journey — UDA
dashboard/allocations, FP17 detail screens, deposit + balance payment paths (surgery
vs patient portal), then ERPNext phases: reconcile (seed fresh £95 band-2 claim),
two-invoice flow, Payment Entry sync, UDA production. Doc pass one page at a time
(private-ledger-billing → co-payment-splitting → invoicing → uda-auditing + NEW
finance intro/start page — gap noted by Matthew at manuals.dental/guides/) using
material at /root/work/notes-finance-intro.md. Publish via /opt/deploy-manuals.sh,
then --prune the plan page. Insurance thread stays parked until Matthew's research.
NEXT SESSION FOCUS (user-declared): BILLING & FINANCIAL WORKFLOWS. When the
user returns, orient on the dental financial module first (guide pages:
private-ledger-billing #15, co-payment-splitting #16, invoicing (ERPNext),
uda-auditing #18 financial side + the /dental/financial/ screens). Quick
pointers: financial templates live in dental/templates/dental/dashboard/
(SUBFOLDER — flat ls misses them); Financial nav visible only to
DA/DPM/AD/SU (use manager.test-client-dental demo user for screenshots);
financial facts in dental-app-guides skill (DentalTransaction,
DentalPaymentConfig, fp17.py NHS_BAND_CHARGES 2025/26 + split_co_payment,
payment.py reconcile_claim_payment); ERPNext invoicing runs on VPS3
(tun-aux-vps3 :8005). User reviews ONE page at a time.
Session 2026-09-05 summary: (1) Verified OpenCode dcff2341 O1/O2 live on
test-client-dental (hygienist Radiographs nav link present+visible; receptionist
403 on radiograph API + HTML, generalised 403 message confirmed) — radiographs
thread fully closed. (2) Perio doc pass COMPLETED 3/3: plaque-and-bleeding-
indices + gingival-recession-mobility-scoring rewritten vs R2/R3 UI (wiki +
repo copies; perio-form.png regenerated); periodontal-charting was already
done 09-04. (3) imaging-and-pacs-storage guide rewritten vs appointment-
anchored upload + DICOM ingest, all 3 radiograph screenshots regenerated from
the live demo, then full-page re-capture of the detail shot (Step-5 image was
clipped at the viewport bottom; fixed 2026-09-05). (4) Published everything to
manuals.dental.veripath.co.uk via /opt/deploy-manuals.sh: commits 5f63998
(perio set), 63bfad7 (imaging rewrite), 03801ca (full-page detail shot — copied
straight to /var/www/manuals-dental too, no rebuild needed). All byte-verified.
(5) HERMES DOC QUEUE NOW EMPTY. Still open (user-side): PARKED AI workspace
session below + emergency-slot-management.md in-progress rewrite in the
veripath-site working tree (kept out of deploys via stash; old version live).
During radiographs testing Matthew spotted the radiograph-detail link "Launch
Surgery Workspace for the full AI workspace (pgvector matching + DeepSeek
diagnostic)" (rx 172). Hermes verified the feature code (consumers.py
SurgeryStreamConsumer._handle_tooth_focus + radiograph_ai.py
get_historical_vector_matches): pgvector search needs the patient to have 2+
EMBEDDED radiographs (excludes the target film; DeepSeek synthesis only runs
when matches exist). Maria Lopez has only ONE embedded film (rx 172) on
test-client-dental, so a live test now would show an empty match list — NOT a
feature bug. USER DECISION: PARKED — Matthew will source more sample DICOM
files and go through the AI features in a SEPARATE session. NOTED in the
imaging-and-pacs-storage user guide (wiki + repo copy, "AI workspace" section,
dated 2026-09-04) so it is not forgotten. NEXT when user resumes: seed 2+
films per patient on test-client-dental (seed_dicom_samples), then live-test
tooth-click -> historical matches -> DeepSeek diagnostic. Also on that
session's plate: perio doc pass pages 2-3, imaging guide full rewrite
(appointment-anchored upload + screenshot regen).
STATUS UPDATE (2026-09-04, end of day): RADIOGRAPHS TESTING COMPLETE — all
green. Sequence of fixes during the session: (1) OpenCode 07236c61 — upload
GET 500 (slice-then-filter, views.py:2163) fixed + regression test (27 pass);
(2) Hermes raised test-client-dental nginx client_max_body_size 1M default ->
100M (413 on real X-ray uploads; matches dental vhost; reloaded); (3) OpenCode
1d00a145 — DICOM uploads via form detected by magic and routed through the real
dicom_ingest pipeline (was: raw .dcm bytes stored under .png key = unviewable);
(4) OpenCode 01212810 — form DICOM uploads attribute patient from the chosen
APPOINTMENT not the DICOM header (real Instrumentarium sample carries PatientID
'0'; strict NHS match would reject); Orthanc/API path unchanged (header
authoritative, no human anchor). Verified live by Matthew: PNG upload views
fine (rx 173 James Carter); DICOM rx 172 (Maria Lopez) reprocessed in place ->
viewable PANORAMIC, AI-reviewed, no re-upload needed. Radiographs area DONE:
list/upload/detail/proxy/API + real-file DICOM rendering all proven. Sample
files kept at /root/work/sample-xrays/ (image-00000.png/.dcm, Instrumentarium
panoramic, anonymised, DSPT-safe). DICOM gaps G1 (CBCT volumetric) + G2
(orientation) assessed by OpenCode — future hardening only, no action.
RESOLVED 2026-09-05: the two cosmetic RBAC items (O1 hygienist nav-gap, O2
receptionist API-read) were fixed by OpenCode commit dcff2341 (hygienist added
to Radiographs nav link; radiograph API swapped to DentalClinicianPermission)
and VERIFIED live by Hermes on test-client-dental the same day: hygienist
session shows the Radiographs nav link (present + visible); receptionist gets
403 on /dental/api/radiographs/ AND the HTML page, message "A dental clinician
role is required for this clinical record." — radiographs thread now FULLY
CLOSED (only the PARKED AI workspace session above remains, awaiting Matthew's
sample DICOM files). PERIO DOC PASS COMPLETE 2026-09-05 (3/3): page 1
periodontal-charting (09-04); page 2 plaque-and-bleeding-indices + page 3
gingival-recession-mobility-scoring REWRITTEN 2026-09-05 against R2/R3 UI
(wiki guides/dental_app_guides/ + repo copies content/user-manuals/;
perio-form.png asset regenerated). MANUALS.DENTAL MIRROR PUBLISHED 2026-09-05
(commit 5f63998 pushed to origin/main; /opt/deploy-manuals.sh ran OK — all 3
sites rebuilt; plaque + gingival + refreshed periodontal-charting + new
perio-form.png live on manuals.dental.veripath.co.uk, byte-verified).
emergency-slot-management.md rewrite + symptom-checker.png / triage-hard-stop.png
STILL in-progress in the working tree (stashed across the deploy, NOT
published — old committed version is live). IMAGING-AND-PACS-STORAGE REWRITE
DONE 2026-09-05 (last queued doc item): wiki guide + repo copy rewritten vs
appointment-anchored upload + DICOM ingest (raw .dcm stored + PNG render, type
from modality, AI auto-run, failure UX); all 3 radiograph screenshots
regenerated from live demo (assets 80-82) — published to manuals.dental with
commit 63bfad7 (deploy OK 12:39Z). HERMES DOC QUEUE NOW EMPTY. Remaining:
PARKED AI workspace session (above) + emergency-slot-management in-progress
rewrite in working tree — both await user.
STARTED 2026-09-04: perio R3 gate cleared first — OpenCode commit 76ef15e1
(F1 generic 403 text + F2 de-stale create-form note; F3 answered, no code
change). Verified live in dental_app. Radiographs is the 3rd and FINAL dental
area under test (after charting R1-R3 + perio R1-R3). Surface mapped: list /
upload (appointment-anchored) / detail (AI Analysis card) / image proxy /
DRF radiographs API + DICOM ingest path (Orthanc->celery->MinIO->pgvector).
Test-client-dental demo holds only 2 placeholder BITE_WING films (no
appointment, no AI); the 9-film AI-reviewed DICOM set lives on the shared
dental tenant. Pre-flagged observations (nav hides Radiographs for
DENTAL_HYGIENIST but views/API allow them; radiograph API list readable by
receptionist vs HTML 403; UI uploads never AI-reviewed — only DICOM path
runs analyze_radiograph; guide + screenshots stale vs appointment-anchored
upload). Perio doc pass + manuals still QUEUED behind radiographs (user's
word). Housekeeping phases 4-9 unchanged (pending).
STATUS UPDATE (2026-09-04): R3 retest COMPLETE — all green. Matthew
retested the full R3 surface live on test-client-dental (exam 109): per-tooth
add (26) -> 67/33 auto-derive; duplicate add rejected; edit (BOP dl) -> 33->50
recompute; delete -> empty state + 0/0; complete -> completed_at/completed_by
stamped (Sarah Holloway), list shows date, Add-tooth button hidden (lock);
receptionist direct URL -> 403 (message text wrong: says "radiographs" — F1 in
handoff); fresh-exam clinician attribution pending a final live check (user
cut testing short). Findings F1-F3 logged to OpenCode in
agents/handoff/from-hermes (2026-09-04). MANUALS AUDIT DONE: all 3 perio wiki
guides (periodontal-charting, plaque-and-bleeding-indices,
gingival-recession-mobility-scoring) are STALE — they still document the
REMOVED type-the-scores workflow (Plaque/Bleeding % inputs gone since R2;
practice select gone since R1; no Add-tooth/complete/RBAC content). Repo
user-manuals: only gingival-recession-mobility-scoring.md exists (stale);
periodontal-charting + plaque-and-bleeding-indices MISSING from the repo.
Perio screenshots (perio-list/form/detail.png) are 2026-08-17 mockups — regen.
STATUS UPDATE (2026-09-04, later): PERIO DOC PASS PAGE 1 DONE. Matthew approved
running the doc pass; periodontal-charting rewritten against the R1-R3 UI and
published everywhere (wiki guides/dental_app_guides/periodontal-charting +
NEW repo content/user-manuals/periodontal-charting.md + manuals.dental; repo
commit 68f217a pushed). Faithful screenshots: perio-list/form/detail.png
REPLACED (were 2026-08-17 mockups) + NEW perio-tooth-form.png; perio-6point.png
exported wiki->repo public/dental-app-guides/. Demo data added for the shots:
exam 111 (Elena, Emergency 03/09 visit marked ATTENDED, OPEN, teeth 16/26/36,
50%/39%) — visitable live. Doc pass pages 2-3 STILL QUEUED (plaque-and-bleeding
-indices = biggest rewrite — type-the-scores narrative gone; then
gingival-recession-mobility-scoring wiki + repo sync). USER: next session =
RADIOGRAPHS testing (new session); perio doc pass resumes on user's word.
OBSERVED: manuals.dental.veripath.co.uk is now AUTH-GATED (nginx auth_request
-> manuals realm :3053; 302 to /api/auth/login on root) — verify intended
(flagged to OpenCode). USER TOPIC PARKED: AI assistance in perio examinations
with Hermes — revisit AFTER workflow/UI testing complete (context: in-surgery
workspace patient chart + Hermes live stream exists; scope for perio TBD).
STATUS UPDATE (end of day 2026-09-03): R1 (0d14ab89), R1b picker loosen
(68db5ec7), R2 derived overalls (d8c7538f) and R3 (e2aa40a2) ALL implemented on
feature/dental-perio-r1. R3 = RBAC clinician gate (receptionist 403s on HTML +
API; appointment_detail button hidden), per-tooth 6-point entry UI
(perio_tooth_add/edit/delete + perio_tooth_form.html single-tooth grid; depth/
recession sanitised; BOP/plaque ticks; overalls auto-recompute), complete action
(perio_exam_complete stamps completed_at + completed_by, migration 0022, locks
completed exams), clinician attribution on create (request user's provider, else
appointment provider). Tests 26 pass. R2 verified live by Matthew (Ahmed 50/39,
Elena 56/61, new exams 0/0). R1b + R2 + R3 live on demo.
NEXT SESSION (user said: continuing with periodontal): Matthew to RETEST the
R3 surfaces live on test-client-dental — per-tooth entry grid (add/edit/delete a
tooth on exam 108/109), complete action (close one exam, check list badge + lock
behaviour), receptionist 403 re-check, clinician attribution on a fresh exam.
Then HERMES DOC PASS one page at a time per user rule: periodontal-charting /
plaque-and-bleeding-indices / gingival-recession-mobility-scoring against real UI
(they over-claim), faithful perio screenshot regen, repo user-manuals copies for
the two missing pages, "when is this done" BPE/6PPC section. STILL QUEUED from
charting R3: role-permissions-matrix + walkthrough-test-users RBAC sections.
--- earlier state (R1/R1b audit trail) ---
STATUS UPDATE (same day, second pass): OpenCode implemented R1
(feature/dental-perio-r1, 0d14ab89 + seed 78dd2df5) — create-form 404 fixed
(patient-scoped /dental/perio/create/patient//, appointment MANDATORY +
patient-owned, migration 0021). Matthew retested live; R1 works. New handoff
note (from-hermes 2026-09-03, Status OPEN, newest on top) = R1b:
Original findings (R1, now largely fixed) below for the audit trail:
Perio live-testing session DONE on test-client-dental. Verdict: module is a thin
skeleton (list / create / read-only detail only). Findings handed to OpenCode
(from-hermes 2026-09-03, Status OPEN):
get_object_or_404(DentalAppointment, pk=patient_id); appointments run*, or active-periodental_periodontal (clinical-reference Q&A, needs tidy +* orHERMES DOC QUEUE (after OpenCode R1 fix + user review, ONE page at a time):
rewrite guides periodontal-charting / plaque-and-bleeding-indices /
gingival-recession-mobility-scoring against real UI (they over-claim a fuller
workflow); regenerate perio screenshots faithfully (perio-list/form/detail.png
stale 2026-08-17 mockups; perio-6point.png wiki-only, not in repo); create repo
user-manuals copies for periodontal-charting + plaque-and-bleeding-indices
(only gingival-recession-mobility-scoring.md exists in repo content/user-manuals);
add "when is this done" BPE/6PPC section to periodontal-charting. STILL QUEUED
from charting R3: role-permissions-matrix + walkthrough-test-users RBAC sections.
Also queued for marketing resume: dental marketing holds (2026-09-02).
Charting walkthrough + doc pass COMPLETE. Matthew tested R1-R3 live on
test-client-dental (appt 1291, clinical note 221). Manuals updated one at a
time with user review, verified live on manuals.dental + wiki:
NEXT SESSION FOCUS: PERIODONTAL — Matthew to test perio in the app.
Wiki guides periodontal-charting / plaque-and-bleeding-indices /
gingival-recession-mobility-scoring exist but were NOT verified against the
real perio UI (housekeeping 09-02: "Perio + radiographs still awaiting
Matthew's testing"). Repo PNGs perio-{list,form,detail}.png likely need the
same faithful-regeneration treatment charting got. One page at a time per
user rule.
MANUALS PIPELINE FACTS (verified 2026-09-03):
node build.mjs dental -> dist-dental. Dental-only publishResumed another time. Next session focus: CHARTING (Matthew has not yet
tested the dentistry sections). Template + voice established on the two
drafted pages (marketing/web-content/dental_surgeries/patient_management/
triage + emergency-slot-management) — draft one wiki page at a time, user
reviews each before the next (per user rule). 7 pages parked w/ holding notes
(PWA, dentistry x3, financial x2) — see "Dental marketing holds" below.
Mirroring to veripath.co.uk content/dental-surgeries/ NOT yet done — decision
deferred until all content exists (page-combining still open). After any wiki
upsert: verify the RENDERED BODY via curl; restart wikijs if stale (SSR cache
bug, recurring).
OpenCode implemented agents/client_agents/implementation (v2) end-to-end in
dental_booking_app (develop, commits 0ea43705 + 69a2942a, pushed; deployed VPS2
manage.py test integrations.tests_memory = 19/19 pass in devagents/handoff/from-hermes (nitpick: stale Trilium commentgp_booking_app/trilium_retriever.py still has hardcoded Wiki.js creds,OpenCode implemented the user-facing Vulnerability Compliance screen (#19) on
2026-08-19 (commit 42b2b4bf, dental develop, pushed; deployed VPS2 + local
container). Hermes VERIFIED against the SIAAS constraints before writing: role
gating (vulnerability_manager_required = DENTAL_ADMIN /
DENTAL_PRACTICE_MANAGER / ADMIN / superuser, 403 otherwise), read-only display
derived from LynisScanResult summary/severity_counts (no PII/secrets),
POST-only on-demand trigger reusing the shared 3/hr + 10/day throttles, 13/13
tests pass in the deployed container (UDA + vuln suites), container code
byte-identical to repo (md5), genuine ON_DEMAND audit record (scan id 95,
COMPLETED, hardening 56) + 46 SCHEDULED history rows, public URL 302→OIDC, no
post-restart tracebacks. Rewrote
guides/dental_app_guides/vulnerability-compliance-automation as a full
guide: 2 real-template mockups rendered headless (vuln-compliance-dashboard =
real scan-95 Lynis view, vuln-compliance-pip-audit = real scan-88 pip-audit
data) uploaded to dental-app-guides/ (assets 119–120, byte-verified
200+MATCH), page upserted (id 438, render OK), sidebar verified (folder node
317 isFolder=t, page node 339 attached). Series state: 18 full guides + 1
holding note (#10 lab-work, skipped by user decision) — WIKI SERIES
COMPLETE. manuals.dental counterpart pages ALSO published 2026-08-19:
uda-auditing + vulnerability-compliance-automation holding notes rewritten as
full manuals (content/user-manuals/, role Practice Manager, sort 1+2) with the
UDA + vuln screenshots copied into public/dental-app-guides/ (commits a45c5c5
OpenCode implemented UDA auditing (#18) on 2026-08-19 (commits 8d4475c6 +
3e71dd6a, dental develop, pushed; deployed VPS2 + local dev container).
Hermes VERIFIED against the SIAAS constraints before writing anything: tenant
isolation (practice/provider scoping everywhere; API practice-scoped like
FP17ClaimViewSet; sector-router DB), role gating (uda_manager_required =
DENTAL_ADMIN/DENTAL_PRACTICE_MANAGER/ADMIN/superuser), no PII, no secrets
(only dummy test fixture), 17/17 dental tests pass in the deployed container,
container code byte-identical to repo (md5), migration 0013 applied
(dental_udaallocation exists, 0 rows — feature left clean), public URLs
302→OIDC (auth-gated, no 404/500), no post-restart tracebacks (only pre-UDA
integrations-removal log noise). Rewrote guides/dental_app_guides/uda-auditing
as a full guide: 4 real-template mockups rendered headless (uda-dashboard,
uda-allocations, uda-allocation-form, uda-deactivate; hyphenated filenames)
uploaded to dental-app-guides/ (assets 115–118, byte-verified 200+MATCH),
page upserted (id 437, render OK), sidebar tree verified (parent node 317
isFolder=t, page attached). Series state: 17 full guides + 2 holding notes
(#10 lab-work, #19 vulnerability-compliance). #19 still waiting on a
user-facing vuln-scan screen. manuals.dental counterpart page for UDA still a
holding note — candidate next page (user names it).
Final section Governance, Retention & Tenant Management finished. Re-verified
#18 UDA Auditing (still unimplemented — only units_of_dental_activity_uda in
dental/fp17.py, claim-fee-derived) and #19 Vulnerability Compliance Automation
(still backend-only: pip-audit dental/tasks.py:249 + Lynis integrations/tasks.py
→ LynisScanResult, no user-facing dental URL/template) against the live
dental_app container. Refreshed both wiki holding notes (checked 07/08,
re-verified 19/08) at guides/dental_app_guides/{uda-auditing, vulnerability-compliance-automation}; published matching holding-note pages to
manuals.dental (content/user-manuals/, frontmatter app: Governance, Retention & Tenant Management, role Practice Manager, sort 1+2; commit 04efd69 main
pushed; /opt/deploy-manuals.sh 19/08 09:44 UTC; built pages + sidebar heading
verified on disk; manuals subdomains 302 auth-gated as expected). Series state:
all 19 workflows covered (16 full guides + 3 holding notes) on wiki AND
manuals.dental. from-opencode dental note (2026-08-17) marked HANDLED 2026-08-19;
from-hermes note left OPEN for OpenCode: implement UDA Auditing (#18) + optional
user-facing vuln-scan screen (#19), then Hermes rewrites holding notes as full
guides. 4 NEW aspirational all_activities items (Outbound Referral Gateway,
Morning Huddle, EOD Reconciliation, Complaint Logging) remain out of scope —
not in the app, per the document-only-what-exists rule.
OpenCode completed the decommission ticket (from-hermes, now Status: HANDLED
2026-08-18). Hermes spot-verified live: INSTALLED_APPS + tenancy/routers.py
clean in both repos (0 grep hits); tables dropped (to_regclass NULL,
content_type count 0); backup /root/backups/ai_sidebar_20260818.sql
(34,620 B); commits 9f0a91a (gp, main) + add39437 (dental, develop) pushed;
containers restarted (docker restart — compose service name is app; no
down/prune); live URL 302→OIDC (auth-gated, no 500); logs clean. Still-OPEN
for Hermes: dental user guides under the Appointments heading (manuals.dental,
from-opencode 2026-08-17) — content source of truth is the wiki stub
guides/dental_app_guides (19 planned child pages).
DeepSeek-efficiency review of agents/agent_llm_efficiency: the real cost
drivers are output tokens + re-sent session history, NOT input cache misses
(the page's flagship --get-cache-ready plan solves a problem we don't have —
our reads are deterministic single pages; scoped out). Applied:
from-hermes 123,530 chars (28 notes) -> OPEN ai_sidebar ticket only (2,555);
from-opencode 49,888 chars (16 notes) -> OPEN dental user-guides note only
(1,940). 27 + 15 HANDLED notes moved verbatim to
agents/handoff/archive/2026-08/from-hermes + from-opencode (audit trail
preserved, still searchable). Convention recorded on agents/handoff/index:
monthly archive, terse notes, UPSERT full-overwrite. Still-OPEN for Hermes:
"Dental user guides: add content under the Appointments heading
(manuals.dental)" (from-opencode 2026-08-17). ai_sidebar decommission ticket
is OPEN for OpenCode (from-hermes 2026-08-18).
Both 2026-08-18 OPEN notes from agents/handoff/from-opencode processed and
marked HANDLED: (1) /integrations/ removed from booking apps + dashboard
cross-nav buttons gone — acknowledged; (2) erpnext_v15 image REBUILT
(ea162d130716, Commercial fix baked in, VPS3 containers recreated + healthy,
recreate-hold lifted, plan page Phase-A bug-2 marker DONE) + wiki tool dedupe
fix — verified --dedupe --check clean (431/431). Stale-row open item closed
(see above). ERPNext recreate is now SAFE; only user-owned item re-flagged:
client-db superuser password rotation (still not done).
Archived 9: root dups gp_booking_app + snomed, en/ dupes (standard_6,
erpnext), the "test" page, the get botch, dentally_homepage (competitor
scrape), local_agent_reponse_1, system-prompt-opencode.
Relocated 12 (copy-to-home + archive original): nuxt-content → infrastructure/cms/,
2× SNOMED pages → development/projects/gp_booking_app/snomed/, 2× Incidents →
security/incidents/, security-dashboard-operations → operations/,
clients-data-compliance-pricing → business_plan/, DSPT deadlines →
compliance/, post_dspt/ (4 pages) → business_plan/post-dspt/.
Descriptions added: clients pricing, DSPT deadlines, policies/standard_6.
Handoff note left for OpenCode (--prune feature, --move bug, prompt mirror).
demo_agent → demo-agent/overview (5 linker pages updated; --move folder-drag
incident detected & fixed). Archived agents/opencode/prompt (byte-identical
duplicate of system_prompt_opencode). All agents/ pages now have descriptions.
17 pages archived (16 demo_runs + demo_agent_research_2); kept 3 reports
(174044Z, 172640Z, 110149Z); stale links repaired on demo_library, demos,
live-demos, demo_agent; 4 never-published reports marked honestly.