============================================================
Split out of hermes-resume on 2026-09-23 because the resume note had grown
to ~72 KB and a new session should not pay for history it does not need. Nothing is lost — this is the old text
verbatim, including the "Open items on my side" table (the cold-start block on the resume page carries forward the
four rows of it that are still live). Sessions 20–45 were moved here from the resume page on 2026-10-02, sessions 52–56 on 2026-10-06, sessions 58–66 on 2026-10-11, and sessions 71–67 on 2026-10-13 (verbatim, each under its own heading below); sessions 73–72, 76–75 and 77 on 2026-10-14, sessions 78 and 79 on 2026-10-15 and session 80 on 2026-10-15 (session 83); session 81 and later stays on the resume page. Sessions 46–51 appear on neither page — their rounds live in housekeeping-log. One link inside
a moved block was repointed to the archive path on 2026-10-02 (the Discounts & Test Portals page it named was itself
archived) — the only edit any moved block has received.
Superseded markers kept deliberately: the session 19 block is marked historical on this page too, and the
"RESUME HERE — pharmacy Phases 5 & 6" section below describes work that has since finished (see sessions 20–25).
SESSION 84 (2026-10-15) — the FIRST CLIENT BOX went in, and doing it by hand found two installer defects.
Matthew opened with "let me know when you're ready", then authorised the build visit — so the session was the
install on the GP test-estate box (test-client), the first on a client box, and everything that follows one:
verification, the app's own records, the credential close, the wiki, and this pass.The install (done, verified live). Credentials minted in the app with the output redirected to a 0600 file (no
value ever in a transcript) and handed to the installer base64 inside abash -sheredoc so nothing sits in
argv; the tree staged fromfeature/client-patch-agent-wo-b; dry run first, then the real run on the box's own
04:07Z slot. Measured afterwards: config 0600 root:root and sourcing cleanly (both values 48 chars — lengths,
never values); four timers live; the firstinventorylanded as record 34 (Ubuntu 26.04, 44 upgradable, 0
security, reboot pending); the poll answered 204; the container untouched (uptime from 2026-07-16); the 44
upgrades and the reboot deliberately not triggered; and the run shows on the client's own page and in both
exports — re-checked as the tenant's ownmanagerinside the deployed container (session84-client-card-check.py,
19/19 PASS), their box only, still 403 on the platform views. The visit is recorded in the app as record
35 (the visit) and 36 (temporary_credential), both client-scoped.Two installer defects, measured rather than suspected. (a) A drop-in's
OnCalendar=APPENDS — it does not
replace the unit's own list. The freshly installed box'sclient-patch.timercarried two schedules and its
next elapse was the base 02:15, not the slot 04:07; the dental canary the same (02:15 vs 02:23). So every
installed box had a duplicate schedule and the per-box quiet window was not in force. Fixed in place on both
boxes with a bareOnCalendar=reset line (verified by merged-unit line count andNextElapseUSecRealtime),
and handed to OpenCode as a one-line installer fix. (b) The env file still expands a$when sourced — this
box's token was safe, the hazard stands. Plus a wrinkle: all three timed units share one slot minute, so the
monthly patch run and the deferred-reboot check start together on the 1st.The credential closed properly. The estate key is authorised for
rootand key-only login proved
first; then the temporary password was rotated over stdin with both directions proved — new works, the
exposed value is refused. Nothing else used that password (our only consumer is the 15-minute TCP-22 uptime probe).One finding for Matthew, and the decision it wants. That box exposes SSH to the internet with
PermitRootLogin yesandPasswordAuthentication yes(UFW22/tcpfrom anywhere, no fail2ban) and has
taken ~62,000 failed root password attempts in seven days. The exposed password is now dead and the estate key
works, so key-only SSH — or restricting22/tcpto our own addresses — is the next hardening step. Flagged,
and he decided it the same session: key-only SSH is in force on both test-estate boxes (host-local
00-veripath-key-only.confdrop-in, named00-because sshd takes the first value per keyword and the
image ships50-cloud-init.confwith password auth on;sshd -tbefore the reload, then verified both
ways — the key signs in, a password attempt is refused). Reversing it is removing one file, and each box
carries a client-scoped record of the change.Then the product question, and the next build. Matthew asked whether the pharmacy app needs a users
section built before the patching work can be finished there. Measured as the client tenant's own Business
Manager: no — the users half exists (/pharmacy/staff/200; pharmacy identities are central with
role+org_id, so the client's own admin isPHARMACY_BUSINESS_MANAGER+org_id), while the
maintenance half is absent (both maintenance routes 404, and the inherited Manage Users / Audit Log
nav items 403 for every pharmacy role). The tables are already in the database that app uses, so it is a
port, not a project. Decided: the pharmacy port is the next build —
spec, work order (23).
Shared box (he confirmed the reading): the pharmacy tenant shares the dental box, so both tenants see the
same box history, with one line saying the server is shared. He is also arranging a separate Client VPS
for the pharmacy tenant, so that shared shape is explicitly interim — the migration plan for when the box
lands is on the pharmacy client-VPS page.A disclosure of mine, and a rotation owed. Finding the tenant's host prefix, I dumped the whole
Tenant
row instead of naming the fields, and the client pharmacy database password landed in my transcript.
Nothing on any page, no value repeated; it still needs rotating (window or sooner). The lesson Round 58
recorded, broken again: name the fields you print.
How the day ended (2026-10-15). Matthew closed the session after the pharmacy plan was recorded; nothing is
half-done. The GP box is installed, hardened and polling on its own; its first real run (the 44 upgrades and the
pending reboot) is deliberately untriggered and belongs to the managed service. Working files, all under
/root/work/:session84-*.py(the install runner, the client-surface checks, the wiki writers, the hardening
script),buildvisit-test-client/(the staged agent tree plus the two runnable scripts:run-install.sh,
key-only-ssh.sh), andpharmacy-client-maintenance-spec.md(the spec as published). The keystore, never to
be printed:/root/.secrets/test-client-root.pass(rotated, now console-only) and
/root/.secrets/client-patch-test-client.raw(the box's agent credentials).Read first next time: this block, then from-opencode (note 18 marked
HANDLED; nothing open from them), from-hermes (23 the pharmacy port — the
next build, with the spec; 22 the build-visit report and the two installer fixes; 21 the request control's
removal and the next-run items; 20 the §6 verification), then
housekeeping-log (round 59; its session-start jobs:from-hermesnow
carries 30 notes, 27 of them open, and every one is live, so expect nothing to move — a processing pass is the
real remedy; and this log is 33 KB, so round 58 is the next prune; trim this note past four blocks), then the
pharmacy client-maintenance spec,
the patching design, the
credential inventory and the
client guide (still awaiting Matthew's review).Next, in this order: the pharmacy port (spec + work order 23; the tenant shares the dental box, so its
agent work is the attribution, not an install) → then,
on Matthew's word, either the platform-facing page that re-homes the estate-card explanations or Phase 4
(DSPT policies). Owed and dated: the DSPT evidence record after the 2026-10-18 02:00–04:00 rotation
window; the pooler's own client-credential rotation; VPS1's pending reboot; the client-tier migration
cadence gap; the box image-refresh has no timer and nothing signs one for a client box; OpenCode's notes (21)
and (22) items; and the new one — the GP test-estate box's SSH exposure.
SESSION 83 (2026-10-15) — the client patching surface was VERIFIED on the deployed estate, and the button came OUT.
Matthew opened by pointing back at the patching thread and telling me to read OpenCode's notes before doing
anything — they had finished the work the previous session found missing — so the session became the handback
verification, then a ruling, then the removal, then the housekeeping pass, then access to the GP box.The verification (my note 20, and the design page).
/root/work/session83-s6-checks.py, run inside the
deployed containers (real views, mixins, templates, tenant routing; the tenants' own users and roles; sessions
written directly so no user row is touched; GETs plus deliberately-refused POSTs; the client's own press not
made). Check 1 pass both apps — the client's ownmanager/adminreach the card and/my-maintenance/and
see only their own box's rows (the dental page renders the canary's real run 26); check 3 pass — both are 403
on/maintenance/,/vps/,/fasthosts/and on the platform host while staff still reach theirs; check 4
pass — aRECEPTIONIST/DENTAL_CLINICIANis 403 on the client surface and on the bring-forward POST, and a
synthetic row written against another client's box (dated2001-01-01, so the leak check cannot pass
vacuously) stayed invisible and was deleted (0 left). Three residuals (note 20): the GP client tenant's own
superuseradminstill gets the fleet dashboard and its page names other clients' boxes
(is_fleet_view = user.is_superuserruns before the client branch); the dental dashboard has no gate at all;
the dentalMaintenanceRecordmirror is missing the real table's NOT NULLsource.The ruling, and it simplified the build. Matthew: the client-vps views exist purely so a client can see the
managed service they receive and evidence it to a regulator; patching and maintenance are part of the managed
service; he never asked for the bring-forward button. The control was my design addition (method B's
"Install now", carried into WO-D) — §7.2 only ever ruled the restart policy, where the client's press appeared as
the anti-pattern to avoid. The button was removed (client card + run list, both apps), live without a restart
(templates are not cached), commits GPd524a40+efb0eb7, dental2890ab3+b946f3c, both pushed, files
backed up at/root/work/deploy-backups-20261015-button-removal/; the test that asserted the removed line was
inverted into the ruling's regression guard rather than deleted. Evidence: GPinfrastructure72 OK,
dentalinfrastructure19 OK,hermes verifyon the GP tree ok (35 tests), dental bootstrap
(manage.py checkin the deployed container) ok. Verified live after the change as each client's own
manager: record and both exports intact, gate unchanged, control absent, staff platform views untouched. The
docs half of WO-D is done too: the dashboard guide is
retitled Understanding Your Maintenance & Updates View and rewritten around the record — measured first, which
showed a client sees only the Maintenance & updates card (no health/database/backup/tunnel/charts/badge), so
the old page was documenting six cards a client never sees. Those sections are kept verbatim at
/root/work/client-vps-estate-cards-removed-20261015.mdfor re-homing on a platform-facing page. Awaiting his
review of that page.Also found, and it changes the work order: the card cannot state a next run — nothing in the code writes or
advancesMaintenanceCadence.next_run(no command, signal or view; the six rows are platform-only with
hand-set dates), so a client's page reads not scheduled while their box's timer is real; and a bring-forward
request carries no expiry, so a press on an agentless box would stay claimable forever. Both are in note
21 with the removal, and the next-run date is demoted (the client needs the history, not a schedule).Housekeeping (round 58).
from-opencode: 18 HANDLED notes archived (95.6 → 50.1 KB).from-hermes:
nothing movable — it holds 27 notes, not the 23 its own header claimed, and every one carries live work
(the claim was corrected; the size is content, not clutter, so it wants a processing pass one note at a time).
hermes-resume: session 80 archived. Log: round 56 archived, round 58 written. And an unplanned
find: three pages carried the tool's own wrapper baked into their stored content (from-opencode9 lines,
security/security-check-in6,from-hermes3) — pre-existing, accumulating one per bad write and surviving
every later pass. Whole-wiki detector 641 pages / 3 affected / 0 prose-risk; cleaned with the round-41
tooling after capturing every page to/root/work/wrapper-sweep-backup/— 127 artefact lines removed, 0 prose
lost, re-run 0 of 641.The access correction, and the credential (read this first). Our record said the GP box's SSH key was
"revoked 2026-08-11"; Matthew says he never revoked it, and he is right — I re-tested the three keys we hold
(documentedclientadmin-keyasclientadmin;vps_clientand the primary's own key asroot): all refused
while the box answered on 22 offering password auth. So it was a credential gap recorded as a decision; the
task page and the client-vps-ops skill are both corrected. He then suppliedrootcredentials for
77.68.97.101, now at/root/.secrets/test-client-root.pass(0600) — use it as
sshpass -f /root/.secrets/test-client-root.pass ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no root@77.68.97.101,
never on a command line and never in a page. Verified working once: Ubuntu 26.04, up 13 weeks, only
client_dbrunning, noclient-*timers, no/etc/veripath, 43 packages behind. Disclosure, as with the
rotation runbook: the password arrived in a chat transcript, so it should be rotated after the install (or
replaced by the estate key:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE+FpgguJuCLTMoTh/UdAkTESN4rZhjLywa16a6tWjXs root@mail.veripath.co.uk,
installed forroot) — that public key is the safer standing posture.Read first next time: this block, then from-opencode (notes 18/19 — the
gate deployed; nothing open from them), from-hermes (20 the verification,
21 the work order — the control removed, the next-run/expiry items), then
housekeeping-log (round 58; its session-start jobs:from-hermesis
126 KB and all of it live, so expect nothing to move — a processing pass is the real remedy; trim this note
only if it grows past four blocks), then the patching design and
the client guide.Next, in this order: install the patch agent on test-client (access now works; the flow is proven on the
dental box — mint credentials through the app, install the agent + its four timers on the box's own slot, first
inventoryposts and the card starts filling; rotate the password afterwards) → the pharmacy box (its
user store still has to be found before it can be gated) → then, on Matthew's word, either the platform-facing
page that re-homes the estate-card explanations or Phase 4 (DSPT policies), the phase he named as the
natural home for the policy drafting. Owed and dated: the DSPT evidence record after the
2026-10-18 02:00–04:00 rotation window (pack rehearsed, 85 → 0); the pooler's own client-credential
rotation; the canary's agent-credential re-mint (my earlier disclosure); VPS1's pending reboot; the
client-tier migration cadence gap; the box image-refresh has no timer and nothing signs one for a client
box; and OpenCode's three residuals in note 21.
SESSION 82 (2026-10-15) — the canary install, and the find that matters: the client surface is gated to the
wrong people. Started with the three housekeeping 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 your ruling on the test estate, installed the client patch agent on the canary — and, chasing where the
client's new card can actually be seen, found it reachable only by VeriPath staff.The defect, and it is the thing to pick up. Your 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. Measured: every client-scoped view inheritsPlatformAdminRequiredMixin(SYSTEM_ADMIN/ADMIN), so
the client variant of the maintenance card,/infrastructure/my-maintenance/and the bring-forward control are
staff-only, while the mixin's own docstring says a client practice admin sees their own box's rows; the WO-C
tests authenticate asADMIN, which is why nobody caught it. Two more facts on the same axis: the dental app
has none of these views at all, sotest-client-dentalhas no route to the card whatever the gate says; and
no client box has cadence rows, so a box's "next scheduled run" does not exist in the app even though its
timers are real. Work order from-hermes (19) (roles measured per tenant:test-client→ADMIN,
PARTNER_REGISTERED_MANAGER;test-client-dental→DENTAL_ADMIN,DENTAL_PRACTICE_MANAGER;
test-client-pharmacy→ its database has no users table, so its users need finding first) and an OPEN
DEFECT block on the patching design page.Your sequence, noted for next time: backend
/infrastructure/maintenance/first (a box's cadence rows; then
the host filter and the status strip covering client boxes) → test-client → test-client-dental →
test-client-pharmacy, one at a time → then documentation + DSPT policies.The canary install (done, verified). 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 firstinventoryposted
outbound and landed as a record (client_inventory, success — Ubuntu 26.04, 40 upgradable, 0 security, reboot
pending). Containers kept their uptime. That closes WO-M4's step 6 (the box'simage_refreshlist now names
the client-tier PostgreSQL, SeaweedFS and Orthanc). Two gaps for OpenCode in note (18): the installer's env
file expands a$in a generated token when sourced (≈half of tokens would be silently corrupted), and that
box has nodocker compose, soimage_refresh --applycan pull and report but not recreate. The box is 40
packages behind with a reboot pending — deliberately not triggered; the better first run is Matthew pressing
the client's own Request a run now, once the client can see it.My own correction, from OpenCode being right. They asked me to re-test the pooler over TCP from a non-local
host before recording it as a live defect. The pooler's pooledSELECT 1succeeds, and over TCP from the app
engine a wrong password is refused while the stored one is accepted: its twopostgresentries hold the
literal (hash-identical to the app's credential), lengths 8/20/32 — my 10/22/34 was the ini's quotes,
which is exactly what broke my probe. It is a carrier, not a defect; corrected in place on the rotation
runbook with the method error kept visible, retracted in note (16), lesson in the client-vps-ops skill. Their
85-hit baseline was reproduced by running their own scanner from the primary.Documentation done: WO-M7 finished (SeaweedFS named on the three patching pages; the Phase 8 page retitled;
the clinician imaging guide no longer says MinIO; the drill page's "risk halved, not closed" corrected) and
WO-D's docs half written from the deployed UI (dashboard guide's card section + FAQs; onboarding runbook's
Phase 4b with the real credential/install commands). Your test-estate ruling is recorded on four pages and in
my skill: the three boxes are a permanent test estate — development estate and patch canary — with access
switching SSH ↔ tunnel-only as a standing mode, and "we cannot log in" now scoped to a real client's box.Read first next time: this block, then from-opencode (they have work order
(19); (13)–(16) are marked HANDLED), from-hermes ((16) the pooler retraction,
(17) the estate ruling, (18) the canary install, (19) the access work order), then
housekeeping-log — Round 57 is on it now and round 55 has moved to the
archive log, so its session-start jobs are: archivefrom-hermes(112 KB / 26 notes — the trigger is met
again; every note on it is open, so expect little to move) and trim this note (it will hold 82/81/80, so
80 comes off).Also still owed: the DSPT evidence record after the 2026-10-18 02:00–04:00 rotation window (pack
rehearsed, 85 → 0); the pooler's own client-credential rotation (my disclosure, its own item); the swap
residuals (the dental box's MinIO decommission on its own backup-cycle gate, the surplus local image awaiting your
confirmation, the orphanpatient-imagesbucket); VPS1's pending reboot (the 2026-11-01 run); and the
client-tier migration cadence gap and weekly image scanning covering the primary only.
SESSION 81 (2026-10-15) — the dental Client VPS access question settled twice, and the second answer was the right one; the Sunday rotation window's premise was wrong.
Correction, same session. This block's heading originally read "no credential exists anywhere". That was
wrong, and OpenCode caught it. Reproduced by me the same day:ssh -i /root/.ssh/vps_client root@<dental Client VPS>returnsubuntu/rootfrom the primary, and the VPS runsclient_db,dental_orthanc,minio.
The cause was three errors all pointing the same way: I read the GP Client VPS's 2026-08-11 key revocation as
if it applied to the dental VPS, I read a truncatedlsof the primary's/root/.ssh(cut off before
vps_client, which sorts last) as proof no key existed, and I tested with the wrong keys from the wrong host
(the app engine's, which never included it). The lesson is now sharper than what this block first claimed:
reachability and authorisation are separate questions, and both must be tested on the host in question with the
credential that host would use. WO-M4 is executable and waits only on Matthew's go; the runbook, the plan
page, the drill page and my note (12) all carry the correction, and the Access section keeps it visible rather
than quietly rewritten. Also while answering OpenCode (note 14): terminology standardises on "Client VPS"
(not "box" — that shorthand is what caused the two VPSes to be conflated), and the access-mode switch
(SSH/dev vs tunnel-only/production) is now a design item with my answers on it.
He opened pointing back at the maintenance/patching thread and asked me to say when I was ready. Two threads ran,
and the first is a mistake of mine that he had to push me into correcting.The correction he pushed me into. I had recorded that the dental client box "cannot be done remotely" and
needed a build visit. He pushed back twice — "if it's a vps then I don't understand why you are asking me for
credentials", then "I don't have any anyway" — and he was right. The box answers on port 22 from the app
engine, on both its public and its tunnel address; the failure is authentication. The reason was already in our
own records: the SIAAS no-SSH decision of 2026-08-11
(Restore Test-Client VPS — SSH Access & Monitoring) — key
access revoked, monitoring by a TCP port-22 probe, and "the client, or a Fasthosts control-panel reinstall" as
the only way back in. Its registry row carries no hosting account and no provisioning record, so it was built
outside the pipeline and its credential was never captured. The lesson, now in theveripath-client-vps-ops
skill: a failed login is evidence about authorisation, not reachability — and check our own records for a
revocation decision before asking an owner for a credential he may never have held. Corrected on four pages: the
runbook (retitled Client Box Radiograph Storage Swap — Access Runbook (WO-M4) with an Access section and a
correction record), the plan page's two WO-M4 rows and open-decision line (OpenCode's Phase 0 / build record /
erratum verified intact), OpenCode's note (10) marked PARTLY HANDLED in place, and a new handoff note (12).
WO-M4 has no executable route today — the risk stays halved, not closed.The window's premise was stale. Pre-checking Sunday's 2026-10-18 02:00–04:00 rotation found the runbook
asserting "there is no connection pooler in the estate" — false:gp_booking_pgbouncer(edoburu/pgbouncer)
runs on the app engine with its own config directory/etc/veripath/pgbouncer/. Checked rather than assumed: no
app container'sDB_HOSTpoints at it (it serves the scalability rig), its credentials are distinct literals
from 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 has zero references to that path, so the closing "0 hits" evidence is blind
to it. Both corrections are on the runbook; the scan scope request is note (12) for OpenCode. The pooler's
own credential rotation stays a separate owed item (my 2026-10-14 disclosure). Also fixed: the pooler was
missing from the app-engine image-refresh list — added toimage-refresh/vps2.conf, the deployed
/etc/veripath/image-refresh.conf(backup taken) and committed as0b4ab48on
feature/seaweedfs-wo-m1-m5.Read first next time: this block, then from-opencode (notes (13) and (14) —
they executed WO-M4 and answered the access-mode work; note (10) was re-characterised in place), and
from-hermes (15 = my verification of WO-M4 and the pooler corrections, with
(13) WO-D and (14) the design review behind it), then
housekeeping-log — its three session-start jobs are: prune (nothing to move
on rounds — it holds 55 and 56 — so what needs attention is the open-items block, which now carries the size;
trim resolved items rather than expecting rounds to shift), archivefrom-hermes(now ~100 KB, over the ~100 KB
trigger), and trim this note (it holds four blocks; session 78 comes off, never the block just written). Then
the WO-M4 runbook, the plan page and the rotation runbook.
Next (in this order): the pooler regeneration step in the rotation pack and the WO-M4 byte figure
reconciled, both before Sunday 2026-10-18 02:00–04:00, then the dated DSPT evidence record after the window
→ WO-D (OpenCode accepted, deferred) → my WO-M7 docs tail (SeaweedFS inpatching-how-it-works,
patching-promises, design §7.1) → Phase 4 (he names it) → WO-E. With Matthew: encryption at rest,
imaging retention, the orphanpatient-imagesbucket, the shared-store reachability question, §7.6 and the SLA
wording — plus one small thing: deleting the surplus local MinIO image (retained read-only until he confirms).
The open engineering thread is the agent vehicle — it exists on no VPS, so WO-M4's second acceptance half and
the patching service's first real install are the same piece of work. Standing: the pooler credential rotation
(its own item, and now more urgent — see the disclosure below), the secrets-sweep rotations, the ERPNext
accounting bootstrap, the backup-gzip items, the report-only row vocabulary.
End of session — how it actually finished (this is the part worth reading first). After I answered their first
correction, OpenCode came back with three more things: WO-M4 executed (Matthew authorised it), the access-mode
design for review, and three rotation-pack findings that included one of mine being wrong again. All three
handled, and the session's real lesson is that I got the same class of thing wrong twice in one day: first the
Client VPS access (reading the GP VPS's revocation as if it applied to the dental one), then the rotation scan
(grepping their scripts for the pooler's path instead of reading which roots the scan covers). Both were
"I checked a proxy for the thing instead of the thing". Verify by executing the artefact, on the host in
question, with the value in question — not by reading a description of it.
- WO-M4 verified (read-only, one deliberate login): MinIO
Exited (0)with volume retained, SeaweedFS pinned
and bound at port parity on 9000, tenant row unchanged,migrate_tenant_radiographs --verify→ 6/7 through
the tunnel. One number does not reconcile — their 76,392,599 bytes vs the store's 80,309,999 for the same
123 objects, not a subtree, all written inside the mirror hour — sent back to re-take.- The pooler holds no literal, but it is broken anyway: its
[databases]entries fordsp_clinicand
veripath_dentalforceuser=postgreswith credentials that fail to authenticate today (tested; the role's
real verifier connects). Dormant only because no app'sDB_HOSTpoints at it (it listens on 4321). So
regenerate before the window, not only after.- Disclosure: while testing that credential I built the command wrong and the rotating 8-character value
appeared in my transcript (the pack's ownold-value; nothing written to any page; hash-only comparison after).
Recording it makes Sunday's window overdue rather than urgent — and it is the second time that value has been
in a transcript.- Plan page swept and free again ("client box" gone from the prose, both copies marked done, my WO-M6/WO-M4
verification blocks on it); the access-mode design reviewed with six additions, the sharpest being that the
two access layers must be verified separately and--statusmust work without logging in.
SESSION 80 (2026-10-14) — patching ruled and built, the MinIO → SeaweedFS swap done on the platform, and one
access window left between us and the client box.
He opened pointing back at yesterday's maintenance/patching thread and asked me to say when I was ready. Two big
threads ran, and the second one proved I was wrong twice — the estate, not my plan, settled both.Patching (thread 1). The gate was his §7 rulings, and the shape of the session was one ruling at a time,
each verified against the live estate before it was recorded: §7.1 cadence (continuous security updates +
monthly full run + quarterly image refresh, per-box minute-level slots — he approved it conditional on the
shape actually achieving the objective under tunnel-only access, which it does because every recurring step
runs on the box), §7.2 the hybrid restart policy (never silent; 7 days' notice for a security-critical
restart, a 30-day ceiling with a second notice otherwise; the client's own press is bring-forward only), and
then he delegated the rest to my recommendation: method (the agent baseline, central orchestration rejected),
evidence (CSV and a quarterly pack held with their backups; 24 months kept / 12 shown), detail level, the
7-day/monthly/quarterly SLA, waves (canary 1 box → 10% → rest, halt automatically on a failed check or a
missed window, no automatic re-run), patch-in-place for the database host vs rebuild-from-image elsewhere,
and a layered recovery path (self-heal, a client-side runbook, a break-glass credential logged as its own
event; a stored credential rejected). Twelve ruled, one open (§7.6 commercial). Three plain-English pages
written because he said plainly he had not followed my explanation:patching-how-it-works,
patching-evidence,patching-promises— the convention that came out of it: the engineering page carries
the reasoning, a plain-English companion carries the decision, one page per ruling cluster.
OpenCode then built WO-A/B/C the same day and I verified rather than accepted: migrations0012–0014
applied, the new routes gating exactly as the pre-existing platform view does (403unauthenticated, not404),
the agent scripts and build-visit runbook inveripath-ops, the leak fix read in the code, and the suite
re-run by hand — 53 tests green. WO-D was raised, then parked behind the SeaweedFS swap by his ruling;
WO-E (the quarterly statement pack) split out as its own work order.MinIO → SeaweedFS (thread 2). He chose SeaweedFS, so I turned the scoping note into PLAN v1.0
(minio-replacement-scope, retitled): WO-M1…WO-M7 with acceptance criteria, a WO-M1 appendix, and the
side-by-side rollback rule. Then I was wrong twice, and both corrections matter. (i) My recorded amd64
digest was the arm/v7 manifest — their Phase 0 caught it, I re-checked, and the page carries a dated
erratum with all three digests. (ii) My WO-M3 would have reversed SIAAS: their Phase 0 showed the
tenant's live images are on its own box, the primary's store is the shared/dev one, and the app's
minio:9000endpoint does not even resolve on the apps host. So I ruled (a) keep SIAAS — a correction of my
plan, not a product decision, since consolidating would put clinical imaging on shared infrastructure — and
recorded that the plan is not the authority; the estate is. Built the same day: WO-M1 (acceptance 10/10 —
the app's seven calls plus a presigned URL fetched and byte-compared), WO-M2 (67 objects mirrored), WO-M3 (no
live consumer existed — a name alignment, not an enabled path), WO-M5 (cold standby, with the honest gap
that it runs no dental app), WO-M7 (my half: the patching design page stops naming MinIO as the store).
I verified the mirror independently through the S3 API from a throwaway container on the hub network —
67 in both, 18,389,458 bytes in both, nothing missing or extra,.dcm+ two PNGs md5-identical — and checked
the estate and app health, with MinIO untouched throughout.The one process failure I own. I overwrote OpenCode's record on the plan page (they had written their
Phase 0 and build record; I upserted from a copy fetched before their write, and the wiki has no merge, so the
last write won silently). Their notes carried the substance, so I reconstructed it labelled as my
reconstruction, asked them to replace it (they did), disclosed it in the handoff note, and changed my method:
re-fetch immediately before every write and diff. That is now in the wiki-housekeeping skill.Read first next time: this block, then from-opencode (note (10) is
OPEN — it needs an access window for the client box; note (9) is HANDLED with my verification),
from-hermes (11 = the SIAAS ruling and the disclosure),
housekeeping-log (Round 55 — the log is 30 KB and
from-hermes is 94 KB, so both want their pass at the next session start:
prune the log, archive the closed notes off the inbox) , then the SeaweedFS plan page and the patching page.Next (tomorrow, in this order): an access window to the dental client box for WO-M4 (the live leg;
its build-visit runbook is written; until it lands the accepted risk is halved, not closed) → WO-M6 once
a backup cycle and a restore drill have passed, with the do-not-prune rule standing and the client box's
image_refreshlist not switched early → WO-D resumes → the 2026-10-18 rotation window (Sunday) and the
dated DSPT evidence record I owe after it → Phase 4 (policies/compliance/security), the next named phase
and the natural home for the DSPT drafting he wants to start. Open on the plan page for Matthew: encryption
at rest, imaging retention, the rollback window, the orphanpatient-imagesbucket, and whether the shared
store ever needs to be reachable from the app engine. Also owed from earlier rounds: the pooler credential
rotation (Round 53, my disclosure), the ERPNext accounting bootstrap, the backup-gzip items, the report-only row
vocabulary.
SESSION 77 (2026-10-14) — scaling: the apparatus, the maths, and a gate that failed. Matthew opened by
pointing me at the Gemini chat captured oninfrastructure/scale_siaas(a full conversation, stored under a
placeholder title). I read it end to end twice and reviewed it: the methodology is sound, but the proposed rig
would not have measured the app tier at all (no tenant registration, only half the tunnel path) and could not
have run on this kit. Review + corrected rig: Scalability Testing Ground.Then what he actually wanted — measure, then model. Two instruments in
veripath-ops/scripts
(scale-measure.py: connect / roundtrip --compare / cpu;scale-model.py: constants + N → break-points) and
the per-client coefficients measured on the primary, read-only. Headlines: a round trip over a client
tunnel is 7.0 ms on an established connection but 66.6 ms on a fresh one (a 59.6 ms setup tax) — and
CONN_MAX_AGE = 0means every request pays it; hub sshd CPU is 0.75 ms per connection (so the tax is latency,
not CPU); ~15 MB private memory per client tunnel session; 96 capacity + ~201 tunnel rows per client per day;
centralmax_connections100. The app tier is what binds, but my first diagnosis of it was wrong (corrected the same day — see the
handback paragraph below): not a serialised process, but two fresh database connections per request
(~50 ms each over the tunnel) with throughput flat at 25–30 req/s. The per-client request rate is
still the number that decides the headcount, and its measured demo floor is 0.002 req/s per client. All of it on
the apparatus and the maths.The gate failed. Inbound UDP never reaches the primary: two unrelated networks, two ports (one the
firewall already permits), a capture on the hub — zero datagrams, while TCP from the same hosts answers in
10–25 ms and their DNS-over-UDP works fine. Corroboration from inside: the overlay interfaces standing on the
primary and the app engine report 635 KiB sent, 0 bytes received, no handshake ever. So a
WireGuard-over-UDP overlay cannot be deployed as drawn — UDP-over-TCP wrapping, another hub provider, or a
different transport; and 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 — no
alert, so nothing surfaced it.The app-tier work order came back and corrected my model. OpenCode shipped gunicorn + 2 uvicorn workers
(container raised to 2 vCPU / 1536 MB), falsified the "one process serialises" premise (Django's ASGI
handler enters a freshThreadSensitiveContextper request, so sync views run in parallel — I confirmed
it in the installed source), and withdrewCONN_MAX_AGE: under ASGI every request gets its own
thread and therefore its own connection, so persistence cannot reuse and in fact leaks (60 s of
max-age exhausted the central connection limit at ~150 requests). Measured instead: two fresh
connections per request (central + tenant, ~50 ms each), a tenant page of ~200 ms of which half is
setup, and throughput flat at 25–30 req/s across 1–2 workers and concurrency 10→40 — a serialised
connection path, not CPU. The pooler I had ranked sixth is now first. Apparatus §4/§5 corrected and
their §8 added. New instrument:veripath-ops/scripts/scale-app-measure.py(session/db/http).Deployed: the per-client capacity cron (one line and one credentials file per client, all firing in the
same minute — a hard stop at ~91 clients againstmax_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.Decisions: the app-tier fixes are agreed and posted as a work order to
from-hermes(connection reuse,
worker processes — measure before/after); the collection host is deferred by my own decision, with
triggers, superseding the capacity plan's "cheapest big reliability win"; the gate was run and failed; and the
rig question is restated in plain terms on the review page §8 (where it runs, and that it drives a separate
app container — never the live one).Read first next time: this block, then from-opencode (the work order),
from-hermes (my note), housekeeping-log,
then the apparatus and
the gate.Next: the pooler work order is posted and awaiting handback (2026-10-14 (2), newest note in
from-hermes — pooler on the app side of the tunnel, central pool first,
generated per-tenant entries, bounded pool size, isolation stated explicitly; acceptance is measured with
their own instruments, and "the plateau did not move" is a valid finding that would put the tunnel on the
app-tier critical path). Then: retention + rollup before the metrics tables reach 5M rows; the UDP
mechanism decision for the transport project; and the rig, which 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. Housekeeping debt:from-hermespassed 100 KB, so the archive pass is due this session. Also owed: a resume trim — five blocks sit here now (archive sessions 72/73).
SESSION 79 (2026-10-14, continued) — the front door opened, then made realm-native only, and the rig got
its operating guide. Matthew opened with no view on the ramp's anchor rate, so we chased the one thing the
front door was waiting on instead: the site auth service's host map, which OpenCode's queue had not reached.
I checked the running service rather than the repo (only the two manuals hosts were mapped), reproduced
the login loop it would cause, and applied my half in Keycloak (thecmsclient now allows the scaling
callback). Then he ruled: if no site-code work was queued for OpenCode, do the edit myself — so I did.The finding mattered more than the edit. Testing his stated requirement ("anyone in the veripath realm is
fine, outside it would be a risk") turned up that the realm brokers thepartnersrealm — the button
renders on this host's login page,linkOnly: false, three partner users — so a partner-organisation
identity would have been accepted by the gate as a veripath session. Closed at our gate: a host-map entry can
now setnativeOnly: true, which refuses a login whose ID token carries Keycloak'sidentity_provider
claim before a session exists. The decision lives inserver/auth-policy.mjswith a shipped unit test
(11 assertions) rather than in a session script; both manuals hosts are unchanged. The refusal path is
unit-tested but not exercised with a real partner credential — recorded as an open item, not glossed.Also done. The gate process was an unsupervised
nohuporphan — the auth service serving every gated
host, and the only site-stack piece without a unit — so it now runs underveripath-auth-server.service,
which is also why a host-map change has a restart path at all. The vhost is live and verified end to end
(/scaling/302s to the login and never the tool; undefined paths 404), and Matthew's own login proved the
allow path (session minted, then the tool's 200s). The rig got its operating guide:
the scalability rig — how to use it, grounded in the rig's own model
(figures re-derived 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), covering the three panels, the ladder, binds-first and the knee,
the arm shapes and the honest limits; the scaling topic page carries an inbound link and the repo README
points at it. Two smaller findings recorded and left untouched: an open redirect on
/api/auth/login?redirect=(affects every gated host) and the OIDC one-timecodelanding in the nginx
access log.Read first next time: this block, then from-opencode (branch
hermes/scaling-auth-host-mapto merge; the broker decision), from-hermes
((5)/(6) HANDLED, (7) OPEN), housekeeping-log (Round 54), then the
patching pages Matthew is picking up next.Next — patching/maintenance, in a NEW session (his choice, 2026-10-14). The gate on that work is his
§7 rulings on Client VPS Patching — Managed Service (plus
§9.6's additions); WO-A…WO-D are raised only after those, and WO-D is shared with me. Still live on that
page: the primary's own pending reboot (2026-11-01 run), the client-tier migration cadence gap, weekly image
scanning covering the primary only, the minio replacement (deferred to its own session; accepted risk with a
do-not-prune caution), the 2026-10-18 rotation window and the dated DSPT evidence record.
SESSION 78 (2026-10-14) — housekeeping cleared, the pooler verified independently, and THE RIG IS BUILT (with a tool Matthew can drive himself). He opened with "let's do some housekeeping first", then "shall we continue with getting the rig set-up", and pushed back once — rightly — when I framed rig-host capacity as a reason to stop at 25 clients: the rig exists to establish the curve and the knee as N rises, and the generator host is a rig artifact, not a platform limit. That correction shaped the build.
Housekeeping — both owed passes cleared, and a missing round back-filled (Round 53).
from-hermes100,807 → 56,794 chars (13 closed notes intoarchive/2026-10/from-hermes; 27 → 14 notes, every OPEN one kept); resume 18,387 → 14,154 (sessions 73/72 archived); Round 52 written for session 77 (it ended without one). Two traps recorded: the page description field is a 255-char varchar, and a failed write left the pass half-applied (the scripts are now idempotent); and a file bind-mount pins the inode, so a synced manifest left the container reading a stale one and silently halved the fleet.OpenCode's pooler handback verified independently (apparatus §9a). Setup tax 49.0 ms → 1.47 ms (the direct pair taken in one
docker exec, so the live app was untouched);ab -k51.7 req/s at c10, 78.3 at c40, zero failures; central connections idle 6 → peak 11 across 1,000 requests; 7/7 web aliases resolve through the pooler; the three isolation refusals reproduced. Three residuals recorded (the pooler user is one key to all five databases; admin/stats user is the app's own credential;log_connectionsoff, so no per-connection record) — and one disclosure: I printed the pooler credential into a session transcript (aprintenvgrep I should have masked); rotation is owed and is with Matthew.The RIG IS BUILT —
veripath-ops rig/scaling-rig, commitsad0b788→fa22718, verifier 74/74. Own network (172.28.0.0/24) and reserved hub range (5530 + 5540–5639, loopback only, behind a restricted key that cannot publish outside it — proven: the key authenticates, a shell command is refused); the matched production image on the auxiliary host (not a rebuild); both tunnel legs per simulated client; register rows the application builds real aliases from; synthetic volume through the real models (123 users / 600 appointments / 400 notes per clinic,nhs_numberdeliberately NULL); a real tenant page serving 200 authenticated / 302 anonymous over both legs. Two arm shapes, because the rig must never be what binds: fidelity (one connection per client — the hub's per-session cost is in the reading) and multiplex (tunnels.sh start multiplex: one connection carrying every leg, for driving N high). First readings: 1 req/s per client → 4.81 achieved, p50 152 ms; 5 req/s per client → 24.32, p50 188 ms, zero failures.Three findings from building it, none of them about scale. (1) The application cannot migrate an empty database into its own schema (
admin.0001_initialasks forusers_customuserbefore it exists) — it matters for a rebuild or a cold-standby restore, not just the rig; the rig clones structure + migration bookkeeping instead (zero rows copied). (2) A session minted for one simulated tenant authenticated on another tenant's host (200 where anonymous got 302); whether data crosses is unresolved and is OpenCode's question. (3) My own model had inherited the falsified premises:rig/model.pynow labels every constant measured / ruling / assumption and treats the central cliff as a parameter, so undoing the staggered sweep + pooler shows the old break at 94 clients instead of hiding it.The tool (
rig/rig-ui.py, loopback only, published to the hub by the rig'srev-uileg, systemdrig-ui): type a client count and a per-client rate → what binds first, the curve, the knee (0.02 req/s per client: hub binds, ~498 clients; 0.2: 263 with the pooler vs 68 without); start/stop a real run and watch achieved rate, p50/p95, non-200s; a provenance table for every constant.The public front door is one step from live. Matthew chose
scaling.veripath.co.uk, added the A record (verified: 88.208.212.211 from an external resolver) and I issued the certificate — via a temporary HTTP-only bootstrap, then put the estate back exactly as it was (no vhost enabled, nginx valid; the ordering bug that would have madenginx -tfail before the cert existed is fixed in the script).nginx/scaling.veripath.co.uk.conf+scripts/enable-scaling-vhost.sh --applyinstall it behind the house auth gate. Blocked on OpenCode: the site's auth service host map has no entry for the new hostname, so a click would loop between the gate and the login (WO infrom-hermes2026-10-14 (5) — reuse thecmsclient or mint a dedicated one, plus my half, the Keycloak redirect URI).Read first next time: this block, then from-opencode, from-hermes, housekeeping-log (Round 53) and the rig README (Status + the two arm shapes). If OpenCode's auth entry has landed, the next action is
enable-scaling-vhost.sh --applyand verifying the gate.Next: the ramp — 5 → 10 → 25 → 50 → 100 at a fixed per-client rate with hub and app-tier telemetry per rung, once Matthew gives the anchor rate (the demo floor is 0.002 req/s per client, which makes any fleet look idle); the UDP transport mechanism decision; retention + rollup before the metrics tables reach 5M rows; the pooler is GP-only; and the collection host, whose trigger ("the rig needs a telemetry home") is now approaching. Also owed: a resume trim — this block takes the page to ~20 KB, so sessions 75 and older should move to hermes-resume-archive (never the block just written). Outstanding with Matthew: the pooler credential rotation, minio, the ERPNext accounting bootstrap, the backup-gzip items, the report-only row vocabulary.
SESSION 76 (2026-10-13, clear-up) — the whole-wiki wrapper sweep is DONE and both overdue housekeeping
passes are cleared. Matthew opened with "let's have a clear-up" and parked the Phase-8 DICOM/PACS minio
content (its wrapper defect was repaired; the imaging prose waits for the minio replacement). The sweep
enumerated all 628 pages —--listcaps at 50 rows without a search term, so it is called with the
wildcard%— and removed 126 artefact lines from 33 pages, with 0 non-artefact lines lost, audited
per page against pre-change captures in/root/work/wrapper-sweep-backup/.A real defect in my own tooling, found by that audit and fixed.
wiki-clean-artefacts.pydeleted from
the start of a baked trailer run to the end of the body, so legitimate prose after a stale trailer went
with it:eViewPlus/enhanced-lab-order-flowhad lost a whole QA-testing checklist (3,177 chars) and
guides/client-admin-guide/users-and-rolesits Related Pages section. Both were restored from the
captures and re-cleaned with a corrected algorithm — every deletion is now confined to artefact-shaped lines
inside a run that aContent (N chars):orPage: <url>line proves is the tool's own wrapper. Second
bug worth remembering: the trailer regex\S+cannot match a URL containing a space, so every page whose
path contains a space (guides/AES Guide/*, eight pages) reported the tool's own trailer as a baked
artefact; the corrected parser takes theContent (N chars):line as ground truth. Tooling is now
/root/work/wiki-wrapper-lib.py(parse/detect/strip, read-only),wiki-clean-artefacts.py(CLI,--deep
for mid-body clusters),wiki-sweep-wrappers.py(the sweep) andwiki-sweep-verify.py(the per-page audit).
Round 41's defect class is now cleared wiki-wide, not just where a session happened to touch it.Housekeeping:
from-hermes111 KB → 91 KB (6 HANDLED/ANSWERED notes into the new
agents/handoff/archive/2026-10/from-hermes; only open notes stay, and the four still-open from that period
are listed in the page footer);hermes-resume26 KB → 6.5 KB (sessions 71 → 67 archived, this note
now keeps 76, 75, 73 and 72); this log 248 KB → 23 KB (rounds 6–49 archived — the current round had
been buried under 44 earlier ones).Client-VPS patching — in scope (same session, 2026-10-13). Matthew ruled that patching the SIAAS client
VPSs forms part of the managed service, and that each client must be able to see when their box was
patched for their own inspection/audit. Spec written and live:
Client VPS Patching — Managed Service — an agent-side run
modelled on the existing client-backup heartbeat (the boxes dial out; no inbound access needed), a
"Maintenance & updates" card on the client dashboard that already exists, and the data-model change
(MaintenanceRecord.hostFKs toPlatformVPS, which is why no client box can appear in the platform view).
Leak found while writing it: the client dashboard's existing maintenance card queries the platform-wide
table with no tenant filter, so any client practice admin currently sees the platform hosts and their
patch state — platform metadata, not patient data, but not theirs, and the card must become client-scoped.
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. Two further sections came
from the same conversation: §9 — scaling to a few hundred clients (the transport breaks before the
patching does, wave rollouts behind a health gate, the metrics volume, and the platform tier's own capacity)
and §10 — the access model for real clients: after handover we hold only the tunnel and the temporary
credential is terminated, so patching has to be software we installed at the build visit, reporting outbound,
with the disruptive step optionally authorised by the client's own practice admin — Matthew's "Microsoft
Updates" instinct, kept managed and recorded. The open item that must settle before WO-B is the recovery
path: no standing access means no remote recovery if a patch leaves a box unbootable. WO-A…WO-D await
Matthew's rulings (§7 and §10.6 of the spec).Matthew wrapped the session here (2026-10-13). The scaling discussion continues 2026-10-14 — discussion
home: Scaling the Platform (the seven levers and their state, the decision log,
what is measured vs unknown, and what to do first), with the trigger/purchase mechanics on the
Platform Capacity & Growth Plan.Read first next time: this block, then from-opencode (their
2026-10-13 (7) handback — nothing open for me), then from-hermes (our note
(10): one vocabulary question with them, the minio decision with Matthew), then
housekeeping-log (Round 51), then
Scaling the Platform and the patching spec's §7/§9.6/§10.6 rulings for
tomorrow's continuation.Next thing that happens: the app engine's 2026-10-14 03:00 bootstrap (first real package row, first
performedreboot), then the 2026-10-18 rotation window and my dated DSPT evidence record.Open items: the four still-open inbox notes (the two 2026-10-06 collection-guide findings, the receipt
print-stylesheet refinement, the counter-views brief's refinement finding, the 2026-09-26 backup-gzip items
for Matthew); the minio replacement session; and the resume archive now carrying two write-ups of sessions
66/65 — flagged in Round 51, left verbatim rather than silently dropped.
SESSION 75 (2026-10-13, evening) — CLOSED: WO-7 and WO-8 closed, and
miniofound unpullable. The
quarterly image refresh now has a real trigger on all three hosts (report-only sweep, next fire 2027-01-02)
with an apply-then-rollback proof done onnetdata— their choice, and mine (minio) would have failed;
per-run package detail is live (detail_json, expandable rows, a packages CSV, ingest object-only and
512 KB-capped). The finding:docker manifest inspect minio/minio:latestfails, Docker Hub's repo API
returns 404 and quay will not serve it — MinIO has withdrawn the community image. Digest-pinned + bind
mounts +unless-stoppedmeans no availability risk today, but it is unrecoverable if the local image is
lost, and the cold-standby compose has no minio service. Workload: the dental tenancy's radiograph storage.
Matthew's decision (2026-10-13): replace MinIO — deferred to its own session, and no mitigation (the
tar was offered and declined; effort goes to the replacement). Recorded as an accepted risk with a
do-not-prune caution. He researches the alternatives; candidates and criteria are staged in
MinIO Replacement — Scope.Read first next time: this block, then from-opencode (their
2026-10-13 (7) handback), then from-hermes (our note (10) — the ack and one
vocabulary question), then housekeeping-log (Round 50).Next: the app engine's 2026-10-14 03:00 bootstrap (first real package row, first reboot reporting
performed); the 2026-10-18 rotation window; then the dated DSPT evidence record. Also owed:
agents/handoff/from-hermespassed 108 KB so the archive pass is due, and the resume-page trim (sessions
65–72 sit here verbatim).
Trimmed 2026-10-14 (session 78). Sessions 73 → 72 moved verbatim and newest-first to
hermes-resume-archive. The cold-start rule is unchanged: read the
top block, then the "read first" order, then from-opencode; the
housekeeping-log archive carries the older rounds.
Moved from hermes-resume in the session-78 trim. The live page keeps
sessions 77, 76 and 75 and the cold-start rule; the 2026-10-13 transport/patching rounds are below and in
housekeeping-log-archive. Nothing was edited in transit.
SESSION 73 (2026-10-13, late) — CLOSED: WO-5 is LIVE and the patching record moved into the app. The
in-app maintenance view is at https://gp.veripath.co.uk/infrastructure/maintenance/; the app is now the
live record andtasks/dspt-patching-logis the cadence + pre-2026-10-13 history + policy summary. WO-4
landed too: the auxiliary host patches at 04:30 (drop-in slot, one unit file per host), gap 4 closed,
restart-safety checked before the reboot step. Verified read-only: row id 1, four cadence rows, tokens on
all three hosts, the env file at 0600, the route live (403 unauth / 404 bogus), four ops commits. Standard 9
now reads as one story in four places with the app as the source. Known limitation recorded: the weekly
Trivy scans have no remote-host step, so the app-engine and auxiliary images are in the quarterly refresh
scope but outside the weekly scan scope.Read first next time: this block, then from-opencode (their
2026-10-13 (5) handback; note (1) stays OPEN until the rotation executes), then
from-hermes (our note (6)), then
housekeeping-log (Round 48).Next thing that happens: the rotation executes Sunday 2026-10-18 02:00–04:00; its
finalize.sh
reports the firstcredential_rotationrow automatically, and I then write the dated DSPT evidence record
(including confirming that row). WO-6 done 2026-10-13, 14:32→14:36 UTC: 53 of 55 packages applied (2 held by apt phasing), reboot
performed and verified — 5/5 ERPNext containers healthy, both test-client hostnames 200, corroborated from
the primary too. Standard 9 is now complete: all four gaps closed, every run records itself in the app.Trim note: this page carries sessions 65–72 verbatim — the next trim should move 65–71 into
hermes-resume-archive.
SESSION 72 (2026-10-13, night) — CLOSED: two work orders raised after Matthew's approval. WO-5 is
the in-app maintenance view — his idea, now specified and approved
(Maintenance Records — In-App View):
MaintenanceRecordin theinfrastructureapp, ingest on the existing backup-heartbeat pattern,
reporters inpatch_monthly.sh/patch-host.sh/refresh-image.sh/ the rotation'sfinalize, a
/infrastructure/maintenance/view with filters, status strip and CSV export, staff-only manual entry.
Settled with him: the app becomes the live record, manual entry allowed, v1 covers all six kinds.
WO-4 extends the monthly patch run to the auxiliary host (same parameterised script, own timer,
staggered slot), with the caveat that its containers' restart policy must be confirmed before the
reboot step is enabled.Also this session:
maintenance_records— the fourth AI research page — was verified false (its query
uses columns that do not exist; three named scripts exist nowhere; it claims a patching window executed
13/10/2026 that never ran), bannered and archived. All four research pages of that class are now retired
without deleting any content.Read first next time: this block, then from-opencode (their
2026-10-13 (3) handback; note (1) stays OPEN until the rotation executes), then
from-hermes (our note (5) — the two work orders), then
housekeeping-log (Round 47).Next thing that happens: the rotation executes Sunday 2026-10-18 02:00–04:00; then I write the
dated DSPT evidence record. If the maintenance view lands first, the rotation is its first
credential_rotationrow; if not, it is recorded in the wiki as planned and back-filled. When the view
goes live: converttasks/dspt-patching-logto summary + history, point it at the view, update the
assessment's evidence map.Trim note: this page now carries sessions 65–71 verbatim — the next trim should move 65–70 into
hermes-resume-archive.
Trimmed 2026-10-13 (session 76). Sessions 71 → 67 (with the two residual copies of sessions 66
and 65 that had stayed on this page) moved verbatim and newest-first to
hermes-resume-archive. The cold-start rule is unchanged:
read the top block, then the "read first" order, then from-opencode.
The housekeeping-log archive carries the older rounds.
Moved from hermes-resume in the session-76 trim, together with the
page's own condensed closure blocks for sessions 66 and 65 and the 2026-10-06 trim note that followed
them. The fuller 2026-10-11 write-ups for sessions 66 and 65 sit in the "Sessions 66 → 58" section
below; the live page had kept its own copies, so this section carries them too rather than dropping
them. Nothing was edited in transit.
SESSION 71 (2026-10-13, evening) — CLOSED: the patching work orders landed; the Standard 9 set is now
consistent. OpenCode implemented all three (WO-1 app-engine monthly patch/rebootpatch-host.sh+
patch-monthly.timer1st 03:00, first run 2026-11-01; WO-2 quarterly per-stack image refresh via
refresh-image.sh, first refresh 2027-01-02; WO-3 auxiliary host's patch state, read-only). I verified
the deployment myself (commits779b808+3634e6a, timer defs, the dry-run line insummary.log)
before documenting anything, then reconciled the
assessment, the log and the
policy so all three describe the same real estate.One new gap, from WO-3's own data: the auxiliary host has 55 packages upgradable, a reboot
outstanding, and no schedule — the app engine's shape from this morning. Recorded as assessment gap 4
withpatch-host.shrecommended (already parameterised); awaiting Matthew's decision, briefed to
OpenCode as a possible WO-4.Two operational details recorded: OpenCode's
shutdown -r +1fix (anohupreboot dies with the
systemd oneshot cgroup) and the opposite stack-guard treatment for the two nearby events — running
for the 2026-10-14 03:00 UTC patch reboot (it is the recovery path), paused for the 2026-10-18 rotation
window (so a credential failure cannot be masked). Reading only one of those would lead an operator wrong.Next thing that happens: the rotation executes Sunday 2026-10-18 02:00–04:00; then I write the dated
DSPT evidence record from OpenCode's execution note (window, choice, counts → 0 from an 83-hit baseline,
step-11 verification, jobs quiesced/re-enabled,old-valuepurged viafinalize.sh, purge confirmation).
Read first next time: this block, then from-opencode (their
2026-10-13 (3) is the handback; note (1) stays OPEN until the execution result), then the
runbook and housekeeping-log (Round 46).Trim note: this page now carries sessions 65–70 verbatim — the next trim should move 65–69 into
hermes-resume-archive.
SESSION 70 (2026-10-13, later) — CLOSED: OpenCode's preparation pass verified, and a wrong claim of
mine corrected in the runbook. They built the execution pack (/root/rotation-20261018/), answered the
three asks, and corrected my window plan:/opt/restart-services.shonly restarts two website Node
services (3052/3053) so it cannot mask a wrong-credential failure — the real self-healer is
vps2-stack-guard.timer(2 min) plus the app-enginecontainer-watchdog. Verified on the host and
folded in, with the correction stated in the runbook row. Counts reconciled: my "49" was a file count,
theirs an occurrence count; the runbook now publishes primary 33 files / 45 occurrences, app engine
13 / 23, auxiliary 8 + 1. My scanner's missing/usr/localroot (2 scripts they found) is fixed in
both scanners. The execution pack'sold-valueis now an explicit purge step. Answers folded into the
Standard 9 pages: auxiliary is redis-only in scope; the app-engine patch gap is a confirmed
oversight; client-tiermigrate_clienthas no cadence; nothing refreshes pinned images.Read first next time: this block, then from-opencode (their
2026-10-13 note is OPEN — the execution result is the next note there), then
from-hermes (my ack, 2026-10-13 (2)), then
housekeeping-log (Round 45), then the
rotation runbook.The next thing that happens: the rotation executes in the window Sunday 2026-10-18, 02:00–04:00.
After their handback: write the dated DSPT evidence record (window, choice, counts → 0, step-11
verification, jobs quiesced/re-enabled,old-valueremoved, purge confirmation). Three patching work orders are out to them — a monthly patch/reboot run on the app engine, an image-refresh
cadence, and the auxiliary host's patch state (its secret scan is done, its OS/reboot/image position is not) If Matthew wants it: the dedicated application role (DSPT 4.1.1/8.1.4) as its own runbook section.Trim note: this page now carries sessions 65–69 verbatim as well — the next trim should move 65–68
intohermes-resume-archive.
SESSION 69 (2026-10-13) — CLOSED: DSPT preparation. The central-DB rotation runbook is re-verified and
the two AI research pages are superseded. Matthew had created/en/rotating_passwordand
/en/database_patchingfrom AI research and asked for a look, then for a plan ("you lead the way").
Orientation showed both topics already had verified pages, so this was duplication as well as
unverified advice. Done: the rotation runbook re-verified and rewritten (5069 → 8775 chars) — the
research page's dual-password cutover does not exist here (PostgreSQL 15, one verifier per role, no
pooler) and its "five locations" is really 49 files (36 VPS1 + 13 VPS2, the app engine scanned over
theapp-enginessh alias); both raw pages now carry a dated SUPERSEDED banner; a handoff note went to
OpenCode (from-hermes, OPEN, two read-only asks); Round 44 written.
Two reusable read-only scanners added in/root/work/:dspt-cred-blast-radius.py,dspt-cred-scan-remote.py.Read first next time: this block, then housekeeping-log
(Round 44, current), then from-hermes (the 2026-10-13 note), then the
rotation runbook.Decided and done the same session (Matthew: "let's proceed — your decision"): the rotation shape is
decouple first; the two raw pages are now archived (/en/archive/rotating_password,
/en/archive/database_patching) behind their superseded banners; the six profile skill scripts read the
canonical config and their SKILL.md guidance was corrected to match; and the patching half is
documented —tasks/dspt-patching-assessmentre-verified from live evidence and
/tasks/dspt-patching-logcreated (the never-created evidence page). Window set 2026-10-13: Sunday 2026-10-18, 02:00–04:00 Europe/London — Matthew accepted the slot and
is going to OpenCode next; execution is theirs, in that window. The runbook now carries the window plan
(six jobs to quiesce, including the per-minute restart watchdog that would mask a credential mistake).
window date. New app-engine finding for OpenCode: that host has no scheduled patch/reboot run at all
— reboot outstanding since 2026-10-07, 42 packages upgradable.Trim note: this page still carries sessions 65–67 verbatim — the next trim should move them into
hermes-resume-archive(already past 300 KB), leaving the current and previous sessions only.
SESSION 68 (2026-10-11) — CLOSED the same day: the finding was fixed and verified. The session was the
tunnel-and-middleware section of development/infrastructure_improvements
(a verbatim Gemini research exchange). It now carries a dated verdict block (OpenZiti rejected for now, a
Traefik/Envoy database router rejected as the wrong protocol layer, WireGuard-over-TCP deferred with a trigger,
plus the one genuine performance item —CONN_MAX_AGE = 0on the client database aliases). The live finding
underneath it — the four client-databasetenant-gateway@socats on the primary publishing every client DB port
(5435/5436/5438/5439) to every container on that host — was briefed, corrected (the patient portal genuinely uses
@8000, so only the four database units were in scope;@8043stays unidentified), authorised by Matthew,
applied by OpenCode and independently re-verified by me: all four ports connection-refused from a hub-network
container, 8000/8043 open by design, loopback listeners untouched, the three client tenants' tunnel-health rows
healthy at 10:50, and every tenant hostname still answering.veripath-opsd10c139.Read first next time: this block, then from-opencode for OpenCode's report
on the socat retirement, then from-hermes (the work order), then
housekeeping-log (Round 43, current). Done on the handback: dated notes on the two pages that still
described a socat hop — the 2026-08-01 tunnel incident page's chain diagram and
infrastructure/siaas/hermes-infrastructure-preparation's Phase 3 line — plus Round 43 carrying the
applied-and-verified record, and OpenCode's repo comments, template commit andAGENTS.mdfixes checked on
master.@8043closed the same day too: fingerprinted through the tunnel it is the client box's Orthanc — the dental
pipeline's client-side PACS, documented oninfrastructure/SIAAS/medical_imaging— and no container, vhost,
script or app repo referenced it, so its gateway socat was retired as well. Five units gone in total; only
@8000(the patient portal's/api/path) remains, and the five retired units werereset-failedso
monitoring sees no failed unit. Left deliberately: the stale 5437 forward on the app engine — removing it
means restarting the unit that carries the tenant database forwards, for a cosmetic line, andAGENTS.md
records it as stale. That closes the round: nothing in this matter is outstanding. Two tools added this session:/root/work/wiki-prepend.py
(insert a block after a page's H1 without re-appending the--getoutput trailer) and
/root/work/wiki-replace-note.py(revise one handoff note in place).
SESSION 67 CLOSED, 2026-10-08 (evening) at Matthew's request — the Health Clinics set is PUBLISHED, and the
website's Health Clinics section is LIVE. Nothing is mid-flight: the app scripts and the site are committed and
pushed,hermes verify --jsonis green (35/35) for the app,npm run buildis green for the site, and every
change is deployed and re-checked on the live page.Next session: the first job is the two items this session left deliberately open — (1) put the mirror helper
for the wiki→website flow into the site repo as the repeatable tool (today it is/tmp/mirror_health_clinics.py;
it must strip the drafts' new LIVE on the website banner as well as the drafting note, or a later run publishes
the banner onto the public page), and (2) the HP appraisal of the homepage's remaining sections — the hero, the
SIaaS block and pricing have not been reviewed by Matthew yet. Below that: the four session-66 app defects, still
open infrom-hermes, and the OpenCode note left today.Read first next time: this block, then from-opencode (nothing new from
them), from-hermes (the note left today), then
housekeeping-log (Round 42).
Health Clinics section built** (navbar, section intro, six pages, fifteen figures) plus the homepage banner and
cards reworked
What this session was. Matthew's running order: finish the Accounting section of the Health Clinics web set
(the Accounting app page's missing books-side figure, then Insurance claims & shortfalls), park VAT, then get
the drafted pages onto veripath.co.uk — starting with the navbar — and finally four changes to the home page.
One page (and one site change) at a time, each verified against the running platform or the built site before it
went live.
1. The clinic's books were bootstrapped, so the Accounting page could be finished. The clinic's ERPNext site
(test-client.accounts.gp.veripath.co.uk) had never been set up — 0 companies, 0 accounts, 0 fiscal years. New
erpnext-deploy/scripts/bootstrap_gp_company.py (run in erpnext_web on the ERPNext host) created Test Client
GP (abbr TCG) on the UK Healthcare Clinic chart (114 accounts), fiscal years 2026/2027, GBP rounding, the
Standard Selling/Buying price lists, a fresh API key, and the Keycloak social login key. Two provisioning faults
had to be fixed for anything to post: Global Defaults were on INR with no default company (every invoice was
refused with "currency (GBP) and document currency (INR) should be same") and no price lists existed (a Sales
Invoice will not save without one). healthcare_accounting.patches.add_custom_patient_id had to be re-run by hand
too — the patch log claimed it had run, the Customer.custom_patient_id field did not exist, and the app's
patient→customer lookup answered 500 until it did.
2. The charges were posted through the app's own client, and the figures re-shot. The test-client tenant —
the one every other page of the set uses — had no ERPNextConfig at all; it now has one pointing at the
books, and the gp tenant's two rows were re-pointed (their key was a placeholder that both sites rejected with
401). scripts/post_clinic_invoices.py posted the tenant's five charges through integrations/erpnext_client.py
and submitted them (ACC-SINV-2026-00002 … -00006), settling two by payment entry (ACC-PAY-00002/-00003);
one invoice from proving the path is cancelled (ACC-SINV-2026-00001), which nets to nil. The books-side
invoice figure was shot with erpnext-captures/erp_capture.py (headless Chrome over CDP, session cookie, chrome
stripped by a per-capture .js), and the two clinic-side figures were re-shot from test-client because the old
ones came from the gp tenant and carried document numbers that exist nowhere.
3. A PII finding, and a guard. The gp tenant holds charges on a staff account with a live mail domain
(gmail.com) — that is why the old clinic-side figures were not reused. Nothing of that account reached the books
or any figure, and scripts/post_clinic_invoices.py now refuses any patient outside example.com/.org/.net
unless explicitly overridden.
4. Insurance claims & shortfalls drafted — no guide existed, so it was written from the app:
insurance_billing/models.py (claim, codes, shortfall state, saved card, ClaimExportBatch), the queue with its
status filter, the XML export, services.charge_shortfall and its webhook, and integrations/tasks.py, which
raises the insurer's invoice and the patient's balance as two documents in the books. Two figures from
test-client, with four claims seeded by scripts/seed_clinic_insurance_claims.py (one exported through the app's
own view → batch BATCH-20261008-135931) and no Stripe charge made; the page says so plainly.
5. VAT parked at Matthew's instruction — nothing has been built (no VAT return work, no guide anywhere in
/guides/), so there is nothing to write from. Recorded in the set index as parked 2026-10-08, and the set index
now carries the Insurance row as drafted. With that, the whole Health Clinics set is drafted, skipped or parked.
6. Published to the website. content/health-clinics/ gained a section intro plus the six pages (mirrored from
the drafts by /tmp/mirror_health_clinics.py: drafting note cut, wiki links and image paths rewritten, front
matter added in the CMS's shape); build.mjs gained HEALTH_CLINICS_GROUPS and the section now builds as a
grouped landing page like dental and pharmacies, with the navbar entry rendering as a dropdown of the three
groups. The homepage's GP card was repointed to /health-clinics/ and the old placeholder stub retired. Deployed
via /opt/deploy-veripath-site.sh and verified live: seven URLs 200, one title per page, every figure loading, the
retired stub 404. A second pass carried across the hero photos Matthew had added to the drafts after the first
mirror (four pages were missing theirs).
7. Home page changes. Hero heading → A New Dimension for Healthcare Technology; tagline → Configurable
agents for assisting staff and clinicians; the label For independent clinics and dental surgeries removed; the
hero image now hidden on mobile (the copy takes the full width); a Pharmacy App card added to the app
services section — pointing at /health-clinics/appointments/ as instructed, with the pharmacy section itself
at /pharmacies/ flagged to Matthew; the three cards re-laid out as one row of three (measured in a browser,
not assumed); and a hamburger navbar below 1024px that opens the full menu as a panel on tap.
State left behind. gp_booking_app on feature/wda-register-import at 553484d (two script commits,
pushed; the pre-existing assign_roles.py change is not mine and is still uncommitted). veripath-site on
main at 265c436 (seven commits, pushed and deployed: 4b90cce, 978405c, 75bf652, c80c109,
72e802d, 4dbc52e, 265c436). Wiki: the four drafts of this set carry live figures, the Accounting app
draft's note rewritten, the set index current.
What is NOT done. The site has no commit for the mirror helper (it lives in /tmp — see Next above), the
six drafts have no LIVE on the website banner yet, and the home page's lower sections (SIaaS, pricing, migration)
have not been touched.
SESSION 66 CLOSED, 2026-10-08, at Matthew's request — work continues in a new session. Everything is landed
and verified: the branch is at7b3d3dc(feature/wda-register-import, pushed, nothing unpushed),
hermes verify --jsongreen at 35/35, and the set's pages and all eight figures are published and
byte-verified. One open item, and it is a decision rather than work: the clinic's ERPNext accounting site is
still unbootstrapped (re-checked today — 0 companies / 0 accounts / 0 fiscal years / 0 invoices), so
/marketing/web-content/health_clinics/accounting/accounting_appstays the set's only page without a
books-side figure. Matthew has the call: Hermes bootstraps the clinic's company + chart of accounts + fiscal year
and posts synthetic consultation invoices through the app's own integration client (needs fresh API keys — the
app's stored ones are 401), or the page stays clinic-side-only. Open the next session by putting that question
to him. Nothing else is mid-flight.
Trimmed 2026-10-06 (again). Sessions 58–66 below, newest on top; sessions 57–52 moved verbatim to
hermes-resume-archive. Read first: the top block, then
from-opencode (nothing open),
from-hermes (seven notes, the counter result newest), and
housekeeping-log.
SESSION 65 CLOSED, 2026-10-07, at Matthew's request. Everything is landed and verified: the branch is at
3260d65(pushed),hermes verify --jsongreen at 35/35, the three Recording Symptoms figures published and
byte-verified, the insurance-panel 500 fixed and live, the stray claim row his own click created on
appointment #121 removed with his clearance (the create path can be walked from scratch again), and the
SNOMED search finding turned into a measured spec at the top offrom-hermes. Nothing is mid-flight and nothing
is half-published. Still the one open ask: the apps host carries hand-copied files because its checkout has no
git remote — three sessions old now.
What this session was. Matthew's running order for the health-clinic web set: Medical notes, then Blood tests & The Doctor's Laboratory, then the Accounting section's first page, Accounting app — E-Prescriptions and the Personal Web App expressly skipped for now. Every page was checked against the running app before a line was written, each shipped with figures shot from the synthetic test tenant, and the set's worklist kept current. Nothing was mirrored to the website: nothing in this set is live.
Pages drafted (all under marketing/web-content/health_clinics/, all awaiting his review):
| Page | Path | Figures |
|---|---|---|
| Medical notes | prescriptions/medical_notes |
3 — the note form with a coded problem (site/side/severity), the saved note read-only on its appointment, the patient's notes list |
| Blood tests & The Doctor's Laboratory | prescriptions/blood_tests_and_tdl |
3 — the Log Lab Order modal, the filed pathology report with its parsed results and abnormal flags, the Lab Orders/Lab Reports cards on the record |
| Accounting app | accounting/accounting_app |
2 — the invoice list (each charge with its accounting document) and one invoice with its open in the books button |
Verification, per page. Medical notes: the GP ClinicalNote model and form (snomed_attributes, encrypted narrative), the read-only update view, role gating on the appointment page. Blood tests: the whole TDL path in code — lab_documents/tasks.py, imap_ingestion.py, pdf_utils.py (name + DOB scoring, ≥90 auto-match), the 300 s Celery beat poll, per-test ranges and flagging. Accounting: ERPNext 15.121.6 on the site, 189 reports (the 16 named in the copy present), the app's erpnext_client.py / invoice_service.py / per-invoice SSO link, and a proven CSV export (frappe.desk.reportview.export_query, file_format_type: CSV — returns attachment; filename=Currency.csv, 149 rows).
Four app defects found while shooting, all briefed in from-hermes (top notes). (1) The clinical note form's debug console is switched on at load and prints raw SNOMED JSON into the page. (2) Copy buttons leak their own JavaScript as page text — the templates write onclick with escaped quotes, so the browser ends the attribute early. (3) A long note is clipped rather than wrapped on the appointment page (bare <pre>, no pre rule). (4) The lab demo seed asserts NHS-number matching (processing_notes, matched_by: nhs_number) that the matcher never does. The figures work around each of them and are re-shot when the fixes land.
The one open blocker — the clinic's accounting site has never been bootstrapped. test-client.accounts.gp.veripath.co.uk runs (775 doctypes, 189 reports) but holds 0 companies, 0 accounts, 0 fiscal years, 0 customers, 0 invoices, where the dental sector's own site holds 118 accounts / 4 customers / 7 invoices / 2 payments. So the books-side figures the dental page carries (chart of accounts, general ledger, profit and loss) have no clinic equivalent — the two integration figures shot here come from the clinic app instead. Also: the app's stored ERPNext API keys are rejected 401 on both sites, and the tenant every other page of this set uses (test-client) has no ERPNext configuration (its five invoices carry no accounting document; the older gp tenant's ten do — ACC-SINV-2026-00009…-00016, synced 20–21 July). Matthew has the decision: I bootstrap it (company + the app's UK healthcare chart of accounts + fiscal year, then synthetic consultation invoices and payments posted through the app's own client, and fresh keys) or OpenCode takes it. It is in from-hermes either way.
Also this session — the wiki drafts' junk-trailer repair. Several marketing drafts and both handoff inboxes carried the wiki tool's own output trailer baked into their stored content (trailing ==== rulers with repeated Page: https://… lines, rendering as rubbish at the foot of the page). The set's index, from-hermes and the Medical notes page were stripped as they were edited. Root cause, worth remembering: a helper that captures a page with --get and feeds the captured text straight back into an upsert keeps the closing ruler and the Page: line — each edit adds one block, which is why those pages carry several. Cut content at the first trailing ruler when re-writing a page from a --get.
State left behind. feature/wda-register-import at 7b3d3dc, pushed, working tree clean apart from the pre-existing assign_roles.py change (19 September, not mine). Four commits, each a reproducible render/verify pipeline: b7e75a8 (clinical-note screens + SNOMED attributes probe), 9f393f2 (lab screens), aa80b55 (probe_erpnext.py + the accounting screens), 7b3d3dc (the invoice the accounting figure is shot from). hermes verify --json green, 35/35, last run after the final script change; the one system-check warning (lab_documents.ClinicLabConfig.practice, W342) is pre-existing. Eight figures are uploaded under health_clinic/ and byte-verified against the wiki.
Read first next time: this block, then from-opencode, from-hermes (its top note is the accounting blocker), then housekeeping-log. Next: the Accounting section's remaining two pages — Insurance claims & shortfalls and VAT, both still scope to confirm; the books-side Accounting figures once the site is bootstrapped; and Matthew's review of the three drafts above. Behind them: his two open calls on the SIAAS page, his review of the live pharmacy pages, group F (premises and registration) and the conversion pass.
/appointments/new/ booking form reworked (patient lookup, the clinic label, the payment and insurance section, the PCN card hidden, and later the hidden Reason label), and the Health Clinics web-content set begun (two pages and its first figure)What this session was. Matthew named exactly four changes to the booking view and nothing else:
the Patient field to become a lookup; the Clinic label removed ("it serves no purpose");
the Payment and insurance section tidied ("a bit mashed and partially cut-off"); and the
Other clinics in your PCN card hidden. Worked one page, verified before anything was deployed,
deployed to the apps host and re-verified there.
1. The "mashed" section was a real defect, and it was ours. Session 64's restyle put Bootstrap's
class on every widget, and for a radio group Django copies the widget's own 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, spilling and
clipping the three radios. Fixed at the source: AppointmentForm now skips RadioSelect in that loop
and the template builds the group's .form-check form-check-inline markup by hand. Insurance fields
move into a panel shown only for Private Insurance (they were always on, under a Self Pay default).
2. Patient is a type-ahead, over exactly the options the select offered. A server-side search is
not possible today: first_name, last_name and nhs_number are EncryptedCharField, so
first_name__icontains matches every row — measured in both tenants (9/9 on gp, 12/12 on
test-client). The lookup therefore filters the options already in the page; the <select> stays in
the DOM, hidden, as the posted value, so what can be booked is unchanged. It is rendered by hand and
without required, because 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. A pre-set patient (?patient=, or the patient role) still renders as plain text —
that branch now shows a "Patient" label too, where the form had blanked it.
3. The Clinic label. In a single-clinic tenant the view pre-selects the clinic and swaps the widget
for a HiddenInput, and the template still printed a Clinic * label over nothing. The block is now
rendered only when the field is visible; the value is still posted. In gp, where the clinic is a real
choice, the labelled select is unchanged.
4. The PCN card is hidden, not deleted ({% comment %} with a dated note) — and its toggle,
results panel and both handlers are commented out with it: removing the element alone would throw
on getElementById('search_pcn_clinics').addEventListener and kill the SNOMED wiring that follows in
the same <script> block. The three go back together.
Verified. Rendered 200 in both tenants (test-client and gp) as receptionist and
clinician, and 200 for the pre-set-patient variant; both <script> blocks pass node --check;
the form still validates — patient posted from the lookup → valid, nothing chosen → still "This field
is required", the insurance rules and the corporate-sponsor path unchanged. The deployed page's
markup is byte-identical to the locally verified render. Commit 81a4cd1 on
feature/wda-register-import, pushed to Forgejo; both files copied to the apps host (whose checkout
still has no git remote and sits on 634c139) and docker restart gp_booking_app; health 200,
/appointments/new/ 302 to OIDC as expected. Verification — resolved later in the session (commit f162b62). hermes verify --json now passes. It could
not, until then: the recipe built its test database on the central cluster, where the app's Postgres role has no
CREATEDB, so it died before collecting a test, and granting that privilege would let test runs create and drop
databases on the cluster the platform runs on. The recipe now calls scripts/test_with_disposable_db.sh, which
starts a throwaway postgres:15-alpine on a free loopback port, points the suite at it and removes it on exit — so
hermes verify reports ok: true with 35/35 on this host, and no test run touches production. (manage.py check is still clean but for a pre-existing lab_documents model warning.)
Noted, not changed: the PCN card is hidden, and a
practice with thousands of patients would need a real server-side lookup — which needs a searchable
name index first, since the encrypted fields cannot be searched with icontains.
Also this session — the Health Clinics web-content set, begun. Matthew moved on to website copy: the set's first
two pages are drafted in marketing/web-content/health_clinics/patient_management/ — Appointments and Recording
Symptoms (retitled from SNOMED & Symptoms at his request, the wiki path renamed with it) — with their worklist rows
marked drafted, awaiting review and the Personal Web App left parked until its rework. His rulings are settled
for this set: these clinics are private, so no NHS Notify anywhere; name lookup only (no NHS number, and
none is possible — those columns are encrypted); SNOMED is never explained, only referred to; the PWA is referred
to without figures; and the register is the pharmacy/dental sales voice. The set's first figure is published —
/health_clinic/recording-symptoms-coded.png, embedded in the Recording Symptoms draft — built from a real render
of the booking form with a genuinely-matched term (Chest pain (finding), the actual top match in the tenant's
SNOMED store) coded in, and its markup is byte-identical to the deployed app's. Shooting it surfaced a fifth app
change: the hidden Reason field carried a visible label and help text — commit 9fdc56d, deployed. Full detail,
including the figure pipeline and the working Chrome path (the one in the pinned veripath-wiki-assets script is a
stale cache — use /root/.agent-browser/browsers/chrome-152.0.7977.42/chrome, and check a fresh figure with
tesseract and pixel counts because there is no vision tool), is in Round 38.
Later in the same session — the insurance panel's 500, found by Matthew in his own testing. He created
appointment #121 (Colin Whitaker) to try the coded reason and the code appeared, which settles the earlier "blank"
figure: it was faithful, and the impression came from how the bar reads. He then found /appointments/121/ saying
"No insurance invoice yet" after he had chosen an insurer, and its "Create one" link answering 500. Cause: the
panel template reversed its own URL with view.kwargs.pk, which resolves where the panel is included on the
appointment page but not where it is served by its own view (kwarg appointment_pk) — and because that view
creates the invoice before rendering, the click wrote the row and then failed, which is why the claim seemed to
vanish. Both views now supply panel_appointment_pk. Commit aca8ac5, deployed, verified 200 with the
insurer, policy, pre-authorisation code and total prefilled (transaction rolled back — nothing written). His click
created that invoice row, not any tool of mine: its created_at (17:53:54) precedes my first diagnostic script
(18:01). Three figures now carry the Recording Symptoms page — the search with its matches, the coded reason on
the booking form, and the coded reason on the appointment record — all real renders, OCR- and byte-checked. Round 39
also carries a finding for OpenCode: the SNOMED search matches FSN descriptions only and the store holds
duplicate terms, so the results read like terminology and can repeat.
Read first next time: this block, then from-opencode, from-hermes, then
housekeeping-log (Round 39). Next: the Health Clinics pages —
E-Prescriptions (five clinician guides; expect a split, or lead with issuing and signing), then Medical
notes, then the three Accounting pages (scope still to confirm); the Personal Web App only once its
rework lands. Behind them: his two open calls on the SIAAS page, his review of the live pharmacy pages
(three unreviewed ones first), group F (premises and registration) and the conversion pass.
siaas_intro page replaced with the wiki draftWhat this session was. Two website jobs for Matthew, both ending in a confirmed deploy. First he reported he
could not see a pharmacy entry in the navbar; the cause was established before anything was touched — session 63's
a2f6416 was committed in the working tree and never pushed, so the live navbar still carried only
SIAAS / Health Clinics / Dental Surgeries / Staff and /pharmacies answered 404. Second, with the section live
and under his own review, he asked for the SIAAS page siaas_intro to be replaced with the wiki draft
marketing/web-content/siaas/siaas_explained.
1. The Pharmacies deploy. git push origin main (8a9c2dd..a2f6416), then /opt/deploy-veripath-site.sh:
pull, npm install, node build.mjs ("Build complete! Target=full"), dist/ copied to /var/www/veripath,
nginx -t OK, nginx reloaded, site responding. The working tree was clean before the run, so no unapproved file
went live. The only nginx output was the usual pre-existing warnings (duplicate TLS protocol options on the
patient and pharmacy vhosts, a duplicate TLSv1.2 value on the test-client vhost).
2. Verified live. /pharmacies/ 200 (the bare path answers 301 to the trailing slash — the site's normal
directory-route behaviour); the navbar carries the item and links all eight children; the landing renders the four
group headings in his order — Prescriptions / Stock / Accounting & Counter / Compliance/Admin — and every page
returns 200: dispensing, stock, ordering, data-feeds, supplier-portals, accounting,
counter-checkout, responsible-pharmacist-log. All 24 images 200.
3. The three unreviewed pages went live with the rest. data_feeds, accounting and counter_checkout were
flagged before the deploy; he chose to publish the section whole and review it live. The consequence to remember:
a fix he names on any of those three is now an edit to the live CMS file, not a re-mirror.
4. The eight pharmacy drafts carry the LIVE marker (dated 2026-10-07 with their /pharmacies/<slug>/ URLs),
in the same wording the published dental drafts use. Applied by /tmp/mark_live_pharmacy.py (idempotent; reads each
draft with --get, re-upserts preserving title and description); every post-write character count matched its
dry-run figure, so nothing was truncated.
5. The SIAAS page replaced from its wiki draft. content/siaas/siass-intro.md (front matter untouched — the
slug stays siaas_intro, so the URL is unchanged) now carries the draft verbatim: its five island
illustrations (/siaas/pexels-*.jpg on the wiki, 0.5–2 MB each) were fetched, resized to 1600 px / q82 and
added to public/images/uploads/siaas/, the embeds rewired to /images/uploads/siaas/, and the body H1 dropped
(the builder renders its own from title). The draft's typos were 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. Verified live: the page 200 with the H1 and its five H2s (Introduction /
Enter SiaaS / How is this achieved? / So what is the cost? / SiaaS Explained) and 5/5 images 200. Committed
f2d5e04, pushed, deployed; the draft now carries its LIVE marker. Two items he left undecided, so neither went
up: the live page's closing "What happens if the agreement terminated?" is absent from the draft and is
therefore now off the page (no other SIAAS page covers it), and the cost section still reads "Our pricing is
given on [....]" — a placeholder the live page carried before this change too. The image alt text was written
from the draft's own captions, since there is no vision tool on this host; flagged to him for correction.
6. The health-clinics test tenant was seeded with synthetic demo data. Matthew: "we haven't touched this for a
while and it looks like we don't have any patients or records". Confirmed before acting: the test-client tenant
(sector client, tenant DB 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 things learned the hard way and now in the
script: 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 broken PatientProfile.partner_org was caught —
the tenant database's own partner_partnerorg table is empty, so the profile is left unscoped), and the
prescription token_id must be unique per row. Committed by copying the script into the container and running it
with --commit on the apps host. Verified by counts and through the app's own queryset under tenant context:
12 patients (with 12 health and 12 registration profiles), 24 appointments (11 still scheduled; one
no-show, one cancelled, two insurance cases carrying 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 flagged/within-range results, one
outstanding — 5 invoices with 7 line items, 3 appointment types and 1 insurance provider. A second run adds
nothing. Noted, not caused by the seed: the app's reporting signal logs "Cannot determine practice for
appointment N" on every appointment save — a pre-existing placeholder in reporting/signals.py.
He then caught a real fault in the first pass: the seeded addresses were plausible real ones — Leeds streets with genuine postcodes. 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 it is also the fix. The pre-existing staff rows carry no address data at all. The conventions are recorded on the set's landing page so every page we draft matches them.
7. GP-app maintenance for the screenshots — both changes applied to the test app. (a) The NHS Notify button on the receptionist patient view is hidden, not removed (a {% comment %} block in receptionist_patient_detail.html), verified by rendering the page as a real receptionist. (b) The /appointments/ period filters now work — AppointmentListView.get_queryset had ignored ?period= 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 — and the Invoices / Appointment Types / Lab / Manuals links are removed from the navbar, with the dead pending-lab-badge fetch. Verified across four roles: today 0 (correctly empty), this_week 2, next_week 5, last_week 4, past 13, all 24, all 200. His "505" was not an app error: nginx logged 502 upstream prematurely closed connection at 14:29:19–14:29:24 and the container's StartedAt is 14:29:19 — my own restart interrupting his clicks; RestartCount is 0 and the logs hold no 5xx. Committed 694b35f and 6609b19 — on feature/wda-register-import, not main (a correction: the handoff note had said main; this working copy is checked out on the feature branch) — pushed to Forgejo and applied to the apps host by copying the files, because that checkout has no git remote.
8. The appointment booking form restyled to match the app. Matthew: the /appointments/new/ form "looks unfinished and quite raw" — it was {{ form.as_p }} in hand-rolled inline-styled divs, with a visible debug panel ("Selected Code: None | Selected Term: None") and leftover [5] markers in the labels. It now uses the house Bootstrap 5 card pattern (section cards with text-primary headings, two-column grid, form-label small fw-bold, per-field errors and help, primary/secondary buttons, a back link) and AppointmentForm gives every widget its Bootstrap class, keeping the insurance-field classes the page's JavaScript uses. All the SNOMED/PCN/price script and the ids it needs are unchanged. Verified by rendering both role branches against a real form (receptionist 17 inputs all classed, coded-reason block; clinician 16, block correctly absent) and on the live page as a receptionist (200, no as_p, no debug panel, no [5]). Committed e915938, pushed, copied to the apps host. Noted, not changed: validateSNOMEDSelection() is defined but never wired to submit, so the receptionist coded-reason check does not run.
Captured as a skill: booking-app-tenant-work — seeding a tenant's synthetic demo data, and the change → copy to the apps host → render-check loop, with the pitfalls found this session (the deferred-FK dry run, the checkout with no remote, the 502s from your own restart). Mirrored at
agents/hermes/skills/booking-app-tenant-work.
Read first next time: this block, then from-opencode, from-hermes, then
housekeeping-log (Round 36). Next: Matthew's two open calls on the
SIAAS page (the dropped agreement terminated section and the [....] pricing placeholder), then his review of
the live pharmacy pages (the three unreviewed ones first), group F (premises and registration), and the
conversion pass once each set is signed off.
What this session was. Matthew asked first whether the counter/till work was finished, then took the marketing page:
he had reached that part of the till during the walk-through and thought it "basic" at the time. Agreed scope —
one page, one pass: re-shoot the three figures from the delivered screens and make three text corrections, with his
review before anything is deployed.
1. The figures were the fault, not the prose. All three embedded images were 2026-10-05 mockups of the pre-CV
counter: the basket mock had 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 … by ." (no
holder) and a cash-up recording only the count; the takings mock had no cash banked and no float carried
forward. So the page illustrated the very behaviour CV1/CV2/CV5/CV6 replaced.
2. The new figures are the verified live captures — the group-D screens already taken off the delivered app, trimmed
to marketing shape by /tmp/mkt/build_marketing_figures.py: basket = d2-charge-added (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);
till = d6-till (Cass Tillworth, float 100.00, takings by tender and band, expected cash 113.10, the cash-up with
Counted cash / Cash banked); takings = d6-takings (Sales 2, cash 13.10 / card 2.50, standard 4.75 / 0.95,
zero-rated 9.90 / 0.00). The basket figure stops at the running estimate; 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), OCR-checked. Nothing was written to the tenant — the
till stays closed and basket 17 open, so the D6 walk-through still opens by opening the till.
3. The copy — three touches, paid for elsewhere: the price shown is the price the customer pays (£2.50 shelf =
£2.50 sale, not £3.00 at the till); the receipt stands as a VAT document (identity, time of supply, a total per rate,
the NHS charge zero-rated and never exempt); the till belongs to the person signed in, hands over with the counted cash
as the next float, and the close records banked and carried forward, the variance reported rather than smoothed.
3,465 characters against the previous 3,355. Deliberately not claimed: clean printing (the print-stylesheet
finding is still open with OpenCode) and the charge-picker caveat.
4. A false line elsewhere, corrected. The set landing page (marketing/web-content/pharmacy_app) had said of this
row "(No screenshots — group D's guides are not written yet.)" — now the revision, the figures and their source.
5. Verified. Page 690 / tree 603 renders 200 with all three images referenced, each byte-identical and 200;
the page sits beside accounting and rp_log in the tree. Nothing deployed (the wiki is the drafting surface; the
CMS and a deploy are separate, confirmed steps).
6. The website gained its Pharmacies section. Matthew asked for the navbar item with the headings
Prescriptions / Stock / Accounting & Counter / Compliance-Admin. There was no pharmacy content on the site at all
(/pharmacies 404, no CMS collection), so the section was built: new pharmacies collection in
public/admin/config.yml, PHARMACIES_GROUPS + grouped navbar/section build in build.mjs,
content/pharmacies/_intro.md, and the eight drafted pages mirrored from the wiki by
/tmp/pharm/mirror_pharmacy.py (H1 stripped, cross-links retargeted to /pharmacies/<slug>, wiki guide links made
absolute, 24 assets into public/images/uploads/pharmacy/). Verified in the build: navbar shows the item with all
four headings, the landing groups them, 324 internal links and 24 images resolve. Not deployed — that waits on
him, and data_feeds, accounting, counter_checkout are still awaiting review (the parked notifications is out).
Committed locally as a2f6416 and NOT pushed (the deploy waits on him); the mirror and section-verifier scripts
now live in the marketing skill's scripts/.
Read first next time: this block, then from-opencode (nothing open), from-hermes, then
housekeeping-log (Round 35). Next: his go-ahead for the Pharmacies
deploy (three unreviewed pages flagged), Matthew's review of the counter_checkout page, then group F
(premises and registration), one page at a time.
What this session was. Matthew took up the D2 offer ("okay D1 looks fine - let's do D2") — the next of group D's
counter guides, written off the verified live screens.
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 (NHS prescription charge [token:10]), the running estimate, the
"no patient identity" rule, and both refusals. The guides index and the coverage plan carry the D2 row; D3–D6 are
next.
2. Four figures from the live counter as tc.ca.one, on basket 18 — d2-charge-picker, d2-charge-added,
d2-charge-refused-exempt, d2-charge-refused-collected. Uploaded to pharmacy-app-guides and byte-compared
after upload (all HTTP 200); the rendered page returns 200 with all four referenced. The charge figure is the
evidence that matters: £3.20 standard-rated + £9.90 zero-rated → estimate 12.57 net / 0.53 VAT / 13.10 total
— the VAT does not move, because the NHS charge carries none.
3. The token census, and a stale claim corrected. The picker offers ten tokens (DISPENSED or ENDORSED, newest
first, labelled by EPS reference); 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 had claimed
"every dispensing token the charge picker offers has its charge collected" — corrected in place, with the
fixtures D2 used. The refusal strings, exact and live: "The declaration says no charge is due (exempt).",
"The charge for this token has already been collected.", "Choose a dispensing token." (nothing chosen). The CV9
refinement stands and stays with OpenCode.
4. Tooling: the recipe extended and the render helper fixed. capture_counter.py now carries the charge scenes
(charge-card, charge-add, charge-exempt, charge-collected), token fixtures named by EPS reference, and
capture_post() — a refusal banner is consumed by the redirect's own render, so the POST's response must be
captured, never a fresh GET. shoot_trim.py was building file://<relative name> when given a relative path and
Chrome rendered ERR_INVALID_URL (a small centred error box) while the trim reported height == window, which
mimics "clipped"; it resolves the path with os.path.abspath() now. Tesseract is on the host — OCR of the rendered
PNG is how a figure was caught out and how every figure here was checked.
5. D3 published, and basket 18 completed to make it. 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 Take the tender card carrying ERPNext's own net / VAT / total with the
standard-rated and zero-rated bands side by side, the below-total refusal ("The tender must be at least 13.10; the
total is 13.10."), the change (cash 20.00 → 6.90), 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. The sale posted for real on the demo tenant — invoice
ACC-SINV-2026-00029 submitted, payment entry posted against the counter till account, counter stock consumed for the
goods (never for the charge) — and it is left standing as the completed sale D4's receipt figures are taken from.
The recipe gained the posting scenes, and they refuse to run without --allow-posting.
6. D4 published — the customer's receipt. Counter — Give the Customer a Receipt
(Wiki page 706, tree 686, ~6.3K chars): what the receipt carries (identity, time of supply, sale reference, a
line per item, net / VAT / total, tender and change, and the per-rate totals), printing it, emailing a copy only
where the customer asks ("Receipt emailed to …", then 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).
Finding filed to OpenCode: no print stylesheet — Print hands the whole page (header, menu, buttons, the email
form) to the browser's print dialogue, so a customer's slip prints inside the app's chrome. The guide states it.
7. D5 published — refund or void. Counter — Refund or Void a Sale
(Wiki page 707, tree 687, ~6.9K chars): the RP/BM gate, the form (Kind, Reason, Return stock), what
a reversal does, what the sale reads afterwards, and the guard "Only a completed sale can be refunded or voided."
Four figures (d5-refund-form, d5-refund-refused, d5-refund-done, d5-reversed-basket). Verified live: the
CA and Technician get 403 on the reversal screen and no button; the RP and BM get both. And verified in the
client's books: ACC-SINV-2026-00030 and its payment ACC-PAY-00016 Cancelled (docstatus 2), the stock back
as an ADJUSTMENT +1.00, reason refund, into the batch it was sold from, with COUNTER_REFUND on the audit.
Both kinds leave the sale Voided; the kind lives on the reversal row.
8. D6 published — the till and the day's takings, and 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 ("Take the till over", and the refusal that names the holder — both verified verbatim), the takings view
by tender method and VAT band, and the cash-up close — counted, banked, float carried forward, variance
reported, never smoothed. Five figures (d6-till, d6-till-handover, d6-takings, d6-till-closed,
d6-takings-closed) — D6 is the largest counter page. The day closed with expected 113.10, counted 112.10,
variance −1.00, 50.00 banked and 62.10 carried forward. A reversed sale drops out of the session's takings
(the closed figure reads one sale, not two) and is listed under Recent refunds and voids — that list holds the
counter's most recent reversals, not only the session's.
9. Group E published — collection and patient contact, six pages, opened and closed in the same session. The group
had no build spec (the screens are as delivered), so every claim came off the running app:
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-*), byte-verified after upload and OCR-checked. Verified on the live tenant: a handover
recorded both 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, on the same
row), a contact preference recorded on demo-token-dispensed-1 (SMS 07700 900456), and an opt-out recorded and then
reversed on demo-collection-uncollected-1 so the fixtures stand. The group's recipe is
scripts/capture_collection_sequence.py (phases e1 → e1-ca → e2 → e3 → e4 → e5 → e6).
10. The role gates, live — and two findings. 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 RP, Technician and BM reach all six. (i) The Collection screen's
Dispensed column is the record's 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: a CA POST to the opt-out URL works
("Patient opted back in to notifications."), but the only screen offering it is the contact-preference page the CA is
refused. Both filed in from-hermes; the guides say so plainly and tell the counter to pass the opt-out on.
11. State left standing. The till is closed — session 20 closed as D6's example with 62.10 carried forward, so
D6's walk-through opens by opening the till. Baskets: 18 is the completed sale (D3/D4), 19 the reversed one (D5),
17 (D1's two lines) still open, nothing parked. Collection fixtures: demo-accounting-claim-1/-2 now carry
handover records; demo-collection-failed-1's collection-ready is Sent (2 attempts) rather than Failed, so a
resend walk-through now wants demo-token-dispensed-1's failed uncollected reminder (no contact details) or a fresh
failure; demo-collection-optedout-1 (opted out) and demo-collection-uncollected-1 (two Sent rows, Email) are as
they were.
Pitfall re-confirmed. /opt/wiki-manage.py's description argument is capped at 255 characters — keep
descriptions short.
Read first next time: this block, then from-opencode (nothing open), from-hermes (the two collection findings
are the newest note), then housekeeping-log (Round 34). 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.
What this session was. Matthew took up the stand-up offer ("I'm happy for you to take a lead on the guides if
you want to proceed"): the counter/till work is built, deployed and verified live, so the guides that describe it in
an operator's language are the next deliverable — D1–D6 of the coverage plan's group D, the counter's everyday
tasks, written off the verified screens.
1. D1 published. Counter — Sell an Over-the-Counter Item
(Wiki page 703, tree 683, 7,682 chars) — the counter screen, starting a basket, scanning versus the picker,
quantities, the price the customer pays (VAT worked inside the shelf price, with the app's own running total
labelled an estimate until ERPNext confirms), parking, and the failure paths. Four figures captured from the live
counter as tc.ca.one (d1-counter, d1-basket-empty, d1-basket-scanned, d1-basket-two-lines): CSRF tokens
blanked, screened for secrets and internal addresses, uploaded to the pharmacy-app-guides folder, and byte-compared
after upload — identical, all HTTP 200; the rendered page returns 200 with all four images referenced.
2. The links around it. The guides index now carries the D1 row (above the Receive-a-Delivery anchor) and the
coverage plan's group D reads Published (2026-10-06); housekeeping-log
Round 33 §6 records the same.
3. The two findings the captures forced. A scan that matches nothing — or several items — never guesses: it
hands the operator the picker. And a parked basket posts nothing and reserves no stock, so parking is a
convenience, not a claim on the item. Both are documented as behaviour; neither is a defect.
4. The reviewer's state, left deliberately. The till is open (float 100.00) and basket 17 holds two lines —
Ibuprofen 200mg at £3.20 and 2 × Paracetamol 500mg (£5.00), estimate net 6.84 / VAT 1.36 / total 8.20 — so Matthew can
walk the guide against the live screen. Nothing posts until the sale is completed; leave or close it as he likes.
5. The recipe became a skill script. pharmacy-app-guides/scripts/capture_counter.py — the D1 capture generalised
(login as any test identity, a read-only --check, and named scenes: counter, basket-new, scan, search, park, resume,
till, takings, receipt), so D2–D6 do not re-derive it. Verified with --check (login and /counter/ fetch, nothing
posted).
6. Pitfall re-confirmed. /opt/wiki-manage.py has a 255-character limit on the description argument — a longer
one aborts the write with StringDataRightTruncation. Keep descriptions short.
Read first next time: this block, then from-opencode (nothing open), from-hermes, then
housekeeping-log (Round 33). Next: D2 — add the NHS prescription
charge, then D3–D6, one page at a time for Matthew's review (the CV9 charge-picker finding — already-collected
tokens still offered — sits with OpenCode, so read from-opencode before D2).
What this session was. Matthew resumed development on the pharmacy app's counter/till views, handing over a
Gemini design exchange (Counter Til) with the rule that
the scope is set by what an operating pharmacy must do to trade.
1. The test reordered the work. 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), plus the Business
Manager's reconciliation in pharmacy_accounting and the contact preference/opt-out in pharmacy_notifications. The
top gap is not the queue strip: a counter price is net, so a £2.50 shelf-priced standard-rated item is charged
£3.00 — Phase 4's last open finding. Scored and written up as The Counter and the Till — Scope by the
Operating-Pharmacy Test.
2. Decisions. Matthew settled inclusive counter prices and the till session per operator (the person
signed in; sequential — a handover is close-then-open with the counted figure as the next float, the single
open-session index untouched); split tender stays out on D4.3; and the receipt's content was read from HMRC VAT
Notice 700 §16.6.2 (name, address and VAT registration number, time of supply, a description identifying the goods,
per-rate totals including VAT and the rate; £250 threshold), with §7.3.1's VAT fraction (rate ÷ (100 + rate),
one-sixth at 20%) as the inclusive-price arithmetic. No source requires the GPhC premises number on a receipt —
display-only config, nothing asserted.
3. The spec, awaiting his go. The Counter Views — Inclusive Prices, the Operator's Till, and the Counter at
Work — CV1–CV9, gates G1–G4, acceptance
criteria, risks. Not yet in OpenCode's inbox.
4. The handoff directory tidied. A 25-note archive pass (15 off from-hermes, all 10 off from-opencode), the
resume note trimmed 43 KB → 17 KB, the stale 2026-09-24 ERPNext-500s status corrected to HANDLED, the "ORDER TO WORK
IN" note superseded, and the Linux-launcher duplicate labelled. The live inboxes now read: from-hermes five notes,
from-opencode nothing open.
5. Two findings kept. Sessions 46–51 are on neither the resume note nor its archive (their rounds are in the
housekeeping log), and the inboxes had drifted into long tails of handled notes — the very cost the handoff token
rules exist to prevent.
Continued the same day — CV1–CV8 verified live and ACCEPTED (28/28). OpenCode built the counter views in five
commits and deployed them; Hermes verified them against the client's own ERPNext with synthetic data through the new
committed verifier veripath-ops fa46eb0 (scripts/verify-pharmacy-counter-views-live.py) — inclusive shelf
prices (a £2.50 item invoices at £2.50 with 0.42 VAT inside, under VAT @ 20% (included in rate)), the operator's
till with a named refusal and a counted-figure handover, park/resume, scan-to-line, the VAT receipt with the NHS
charge zero-rated, the cash routine's banked cash and carried float, the audit rows, and the client-tier routing
(the central database holds no pharmacy table). One refinement finding (the charge picker offers already-collected
tokens) is with OpenCode. The spec page carries the criterion-by-criterion record; the coverage plan's group D now
reads verified.
Read first next time: this block, then from-opencode (nothing open), from-hermes (the CV9 result), then
housekeeping-log (Rounds 32–33). Next: the group D guides, D1–D6
(the counter's everyday tasks), written off the verified live screens — one page at a time for Matthew's review.
What this session was. Opened as "continue the web content — mirror the wiki drafts to the website". The CMS turned out not to work at all, so that came first; then the section was grouped and the entire dental set published.
1. The CMS was broken — a dead Forgejo token, hardcoded in a public repo. https://veripath.co.uk/admin/ (Decap, behind Keycloak realm veripath, client cms) listed nothing: both backends carried the same literal PAT, which Forgejo rejects (access token does not exist). Minted a scoped write:repository token for matthew via the Forgejo CLI, stored at /etc/veripath/cms-forgejo.env (0600, outside git); server/proxy-server.mjs now reads that env file instead of hardcoding it. The Decap proxy is now supervised by the new veripath-cms-proxy.service (it had been started by nohup from the deploy script — a reboot gap). Verified read and write (a create against an existing path returns 422, not 403). Still open for Matthew: the dead token remains in the repo's history on a public Forgejo repo (harmless now it is revoked), and /opt/deploy-webhook.js keeps its webhook secret in plain text.
2. Navbar tidy. Home / Policies / Blog / Guides removed from the header. The footer still links Blog, Policies and Guides — he did not ask for those to go.
3. The Dental Surgeries section is grouped, and the whole set is live. build.mjs gained a group front-matter field rendering Patient Management / Dentistry / Financial as subheadings on /dental-surgeries/ and in the navbar dropdown (DENTAL_GROUPS), plus Group + Sort Order fields in public/admin/config.yml so he can move pages himself. 13 pages: Patient Management — Appointment Booking, Triage, Emergency Slot Management, Recalls; Dentistry — Clinical Charting, Periodontal Charting, Plaque & Bleeding Indices, Gingival Recession & Mobility, Imaging & PACS Storage; Financial — NHS Contract & FP17 Claims, UDA Auditing, Invoicing, Accounting. PWA is parked (his call). Every one of the 13 wiki drafts now carries a dated LIVE on the website marker — that marker set is the worklist for the end-of-set conversion pass.
4. The section intro moved into the landing; the orphan page is retired. /dental-surgeries/dental_surgeries/ is gone (404) — its copy now lives in content/dental-surgeries/_intro.md (leading underscore, so loadContent skips it and it is not a page) and renders above the three groups; the homepage's dental card now points at /dental-surgeries/.
5. The long-held WIP shipped. The uncommitted Emergency Slot Management manual rewrite (held out of every deploy for weeks) is published at /guides/emergency-slot-management/. Nothing is held or stashed now — tree clean, in sync with origin.
6. Two workflow decisions, both recorded. (i) Once published the site copy is canonical — never re-mirror a marked draft without reading the live file; back-port the live wording into the draft first. (ii) The wiki copy is surplus once published: at the end of a set, reduce each marked draft to a short pointer (keeping the page, path and comment thread) and archive the tree — logged as an open item in housekeeping-log. Method lives in the veripath/marketing skill.
7. Traps re-confirmed. The deploy builds the working tree, not the commit — anything uncommitted goes live, so check git status and stash unapproved work before a push. The push webhook auto-deploys in about 2.5 s, and the CMS uses the same path, so a CMS save is live within a minute.
NEXT: the pharmacy app web pages are ON HOLD (Matthew, end of session) — there is view work to do first and OpenCode may be needed. Nothing else is owed from this session.
Read first next time: this block, then from-opencode, then from-hermes, then housekeeping-log.
What this session was. Opened as "we are continuing work from a previous session in which we were drafting web content for the dental app", then pointed first at the parked financial/accounting_app holding note and then at /guides/dental_app_guides/uda-auditing. Both pages are published and verified; the UDA work also produced a live defect finding and a disclosed staging pass.
1. Two pages published (both HTTP 200, Wiki.js page cache flushed after each write — the 2026-10-05 trap still applies):
marketing/web-content/dental_surgeries/financial/accounting_app — title Accounting, ~2,900 chars, six sections, three captures. It replaces the 2026-09-02 holding note ("parked pending ERPNext") and is now the finance section's opening page. Cross-links to FP17 and Invoicing.marketing/web-content/dental_surgeries/financial/uda_auditing — title UDA Auditing, ~3,000 chars, six sections, three captures. New page.dental-app-guides wiki folder, byte-compared after upload): erpnext-sales-invoice.png, erpnext-general-ledger.png, erpnext-profit-and-loss.png, dental-uda-dashboard.png, dental-uda-allocations.png, dental-uda-allocation-form.png.2. Facts verified live, not from the guide. The practice's own ERPNext (company "Test Client Dental", site https://test-client.accounts.dental.veripath.co.uk) carries a UK healthcare-clinic chart of accounts in GBP (clinic till, Stripe clearing, receivables from patients, fee income, associate fees, clinical waste, software) plus the NHS accounts provisioned by provision_erpnext_config --provision-nhs-accounts (NHS Patient Charges (Clearing), NHS Contract Income, Superannuation Payable, Associate Fees). Live proof: ACC-SINV-2026-00031 (NHS band charge £76.60) posted its item to NHS Patient Charges (Clearing) — GL Dr 1120 Receivables / Cr clearing; ACC-SINV-2026-00032 (private £60) to 4010 Fee Income; both Paid with payment entries. The month-end schedule journal (Dr bank net + Dr clearing clawback / Cr contract income) and the associate-pay journal (Dr Associate Fees gross / Cr bank net / Cr superannuation payable) are verified in dental/services/schedule.py — nothing is posted in this tenant yet, so both are described as capability. UDA arithmetic read from dental/uda.py (band values 1/3/5/7/12, urgent from the contract, exempt catalogue; production statuses TRANSMITTED/ACCEPTED/PARTIAL; period = transmission date).
3. New recipe — ERPNext desk captures. /root/work/erpnext-captures/erp_capture.py (headless Chrome over CDP, out/<name>.js for in-page report filter driving). The desk needs a real session: mint one on VPS3 (ssh root@77.68.112.126 → docker exec erpnext_web, bench --site … execute healthcare_accounting.mint_session.mint, model the LoginManager/Session call as in the deleted mintmod.py), then set the sid cookie with Network.setCookie. ⚠️ API-token auth renders the desk only partly — GET-whitelisted methods (the workspace sidebar) come back 403/405, so the token route cannot produce a clean workspace shot. Report pages also need ~15 s settle before the screenshot.
4. ⚠️ LIVE DEFECT FOUND — NOT YET FILED to OpenCode. On the deployed VPS3 ERPNext, frappe.desk.desktop.get_workspace_sidebar_items answers 403 "Function gunicorn_app.get_workspace_sidebar_items is not whitelisted" — the patches/gunicorn_app.py boot-degradation wrapper (session-55 handback) lost the @frappe.whitelist decorator. Effect: the Accounting workspace and the Chart of Accounts page do not render in the desk (the Sales Invoice form and the reports do). File it in agents/handoff/from-hermes first thing next session.
5. Staging on the demo tenant test-client-dental — disclosed to Matthew, script kept. (a) 2026/27 UDA allocations recorded through the app's own New Allocation form (Holloway 170, Haddad 45, Nguyen 65); an earlier pass this session had set 600/450 and was then edited to the values that make the year read mid-contract. (b) 32 synthetic courses (appointment + course + TRANSMITTED FP17 claim) across the existing 11 demo patients, spread evenly 2026-04-07 → 2026-10-02, mixed bands (1/2a/2b/3), each created with the app's own create_claim_from_appointment and marked TRANSMITTED with a demo NHSBSA reference. Dashboard now reads allocated 280 / delivered 160 (57.1%) / £4,258.80 fee basis; Holloway 49.4% (green), Haddad 84.4% (amber), Nguyen 58.5%. Production only — no charges or invoices were raised, so the UDA figures run ahead of the tenant's invoiced charges and the client's ERPNext was untouched. Script: /root/work/dental-web-captures/stage_uda_year.py (re-runnable — clears its own rows first).
6. OPEN / OWED.
Read first next time: this block, then from-opencode, then from-hermes, then housekeeping-log. Next: file the ERPNext desk defect to OpenCode, then the next dental web-content page — Matthew names it.
What this session was. Opened as "we are continuing work with web content … about to start working on the dental app". Matthew put the content aside: the dental app's top nav came first. That done, the session produced seven marketing pages for the dental set, each illustrated from the running app.
1. The dental navbar — four dropdown groups (DONE, live). dental/templates/dental/base_dental.html was 22 flat links in one row; it is now Dashboard plus four Bootstrap dropdowns: Appointments (Appointments, Book, Patients, Take Payment, Scheduling, Triage, Callbacks, Recalls), Dentistry (Charting, Periodontal, Radiographs), Accounting (FP17, NHS Ledger, UDA, Invoices, Financial, Price List, Payment Settings) and Admin (Practices, Staff, Providers, Vulnerabilities, Hermes AI, Zones, Schedules). On his next note the duplicate Dental Dashboard item was removed from the site bar (templates/base.html) and Manuals moved to the right-hand group, immediately before the user profile. Commits 8e620e7 and 2e96083 on feature/dental-nhs-accounting, deployed to the app engine (git pull + docker compose restart app) and verified by rendering every role (all 200; per-item role gates preserved — a receptionist sees no Dentistry or Accounting group, a clinician no UDA/Financial/Price List/Payment Settings). My calls, unconfirmed by him: Financial/Price List/Payment Settings/UDA/Invoices sit in Accounting; Vulnerabilities/Hermes AI/Zones/Schedules in Admin; Practices/Staff/Providers stay visible to receptionists.
2. Nine dental web-content pages now drafted (under marketing/web-content/dental_surgeries/, all with live captures, all verified serving):
patient_management/appointment_booking — Appointment Booking (its opening still describes rather than sells).dentistry/charting — Clinical Charting (same opening fault).financial/fp17 — NHS Contract & FP17 Claims — replaced the 2026-09-02 park, whose ERPNext dependency is now met (the journey's ERPNext leg is visible in the captures).patient_management/recalls — Recalls.dentistry/radiographs — Imaging & PACS Storage.dentistry/periodontal — Periodontal Charting (path renamed from the stub's misspelled perodontal).financial/invoicing — Invoicing.dentistry/plaque_and_bleeding_indices and dentistry/gingival_recession_mobility — the perio siblings.3. The style rule he enforced (now in the skill). His catch on the NHS page: "you're still being a bit too descriptive … alter this and anything else to how the app helps deal with complex matters like this" — the opening explained the two NHS money legs to a practice manager who lives them. veripath/web-copywriting now carries the rule (open on the cost, turn to what the app does about it inside the same paragraph, never explain the domain back to the reader), with his before→after as a table row; mirror resynced. The two earliest pages still carry the old descriptive opening; he has not asked for them to be redrafted.
4. ⚠️ THE PUBLISHING TRAP — flush the page cache after an upsert of an existing page. /opt/wiki-manage.py writes the DB and the render column correctly, but Wiki.js keeps serving the old render from its page cache, indefinitely (observed on dentistry/charting: two upserts, a fresh render(id) call and several minutes of re-fetching, all stale, while the DB held the new content). One call after the write: mutation { pages { flushCache { responseResult { succeeded } } } } (key from apiKeys.name='tsk-system', as the tool uses). The full capture-and-publish recipe — tenant threading, direct-view rendering, measured crops, the flush — is in /root/work/dental-web-captures/README.md, with every script used this session beside it.
5. Staging on the demo tenant (both reversible). (a) Periodontal exam 130 — Fatima Hassan, 4 teeth, plaque 25% / bleeding 21%, at her attended appointment of 24/09/2026, created through the app's own forms (the tenant had no perio exams at all, so the screens would have been empty). (b) The radiographs: three of the five demo records are real clinical DICOM studies (genuine vendor UIDs, capture dates 2014–2016) re-labelled onto synthetic patients. Matthew's ruling, 2026-10-05: the image came from a sample pool and it is a test client, so publishing is acceptable. The page shows it, and the shot is cropped 16px above the storage-details card because that card carries the study's own object name.
6. OPEN / OWED. (i) Private Ledger Billing is blocked, not skipped: its screen is the practice financial view for the current month, and the tenant's money is September's, so October reads £0 — making it real needs a staged current-month charge, which raises an invoice in the client's ERPNext, so it is his call. (ii) UDA Auditing has the same shape (nothing delivered against the contract; a page today is a screenshot of zeros). (iii) Check-In & Arrival, Clinical Notes and Digital Patient Intake have no data in the tenant and need light staging. (iv) fp17-claim-submission folds into the NHS page rather than becoming a near-duplicate. (v) The parked stubs financial/accounting_app and patient_management/pwa still carry their September holding notes.
Read first next time: this block, then from-opencode, then from-hermes, then housekeeping-log. Next: finish the dental web-content set — private ledger billing (decision needed), UDA auditing, graphic charting logs, medical history verification, recall settings, lab work, vulnerability compliance, and the three that need staging.
What this session was. Opened as "we are continuing from a session in which you crashed". The crash was the model provider, not this box: at 11:15 DeepSeek returned a non-retryable 400 Content Exists Risk mid-turn. The killed session was already doing the two things Matthew had asked for — the skills housekeeping (a web-copywriting skill built from Gemini's advice, restructured as veripath/web-copywriting + veripath/marketing, with pmm_playbook kept for positioning) and the pharmacy web-content re-drafts. Both were finished here.
1. Where Matthew's review comments actually live — read this first. His per-page comments are Wiki.js page comments (the comments table joined to pages.path), not in the page text, so /opt/wiki-manage.py --get never shows them. Query them before touching any marketing/web-content/ page. Today they named: data_feeds (over-explained), ordering ("up the sales pitch — the flagship"), supplier_portals (three notes), rp_log, counter_checkout (needs images), accounting (rewrite + images). All are now addressed.
2. The pharmacy set is complete bar two pages. Re-drafted sales-led this session: data_feeds (+ the corrected flow diagram), ordering, supplier_portals (twice — first the setup→addresses rewrite, then Matthew's EDI/API + premium-PMR framing), rp_log, counter_checkout (text unchanged, images added) and accounting (rewritten + images). dispensing is accepted as-is; notifications stays parked until NHS Notify is settled. House target is ~2,500–3,000 characters of body copy, benefit-led H2s, no mechanism, no role matrix — the rules are in veripath/web-copywriting.
3. The image method that worked (reuse it). The app's pages are self-contained (inline CSS, zero external refs), so: drive the live tenant over HTTP through the real OIDC login, save the screen's own HTML, render it in headless Chrome (/root/.agent-browser/browsers/chrome-152.0.7977.42/chrome; 1240 px for report screens, 1600 px for the ordering grid), trim to the content bbox, then upload with veripath-wiki-assets/scripts/wiki_insert_asset.py. Captured today: the ordering view, the counter basket (VAT split), the till, the day's takings, expected reimbursements, takings & VAT. Staging scripts are saved in /root/work/marketing-diagrams/staged-price-files/.
4. Two staging operations on the demo tenant, both reversible — with the caveats. (a) Price files: the ordering view showed every offer as stale, so I imported current-dated (05/10/2026) price files for the four synthetic wholesalers through the app's own import form. They age into "Stale" after a couple of working days, so any future re-shoot must re-stage first. (b) Three fixture rows ("Synthetic four-supplier / one-day-price / stale-price item") cluttered the shot: I gave them stock through the app's own goods-in, refreshed positions, captured, then reversed it with Technician adjustments — the tenant is back exactly as it was. Never "tidy" those fixtures permanently: verify-pharmacy-price-ordering.py depends on the stale-price one.
5. Corrected on Matthew's catch — the concession cadence. The diagram said price concessions were monthly; they are re-issued several times a month (September 2026 ran notices 1st–9th, 4 Sep → 2 Oct). The release is per reimbursement month. Recorded in pmm_playbook under the pharmacy block, with the live store as the evidence.
6. OPEN / OWED.
accounting is Matthew's call. He asked for ERPNext views too; I captured none, because the only ERPNext I can log into is VeriPath's own accounts (real financial data — never publish) and the tenant's ERPNext is not exposed. Needs a synthetic ERPNext view nominated.accounting is Matthew's claim and is NOT verified in the app — pharmacy_accounting carries no export/CSV route in the working copy. What is true is that ERPNext itself exports reports (Excel/CSV). Soften it or evidence it before publishing.(DEMO) names appear in every capture (synthetic wholesalers, demo claims). Matthew accepted this on the ordering shot; a curated demo tenant is the fix if it ever matters.public/admin/config.yml plus a deploy, and Matthew confirms that step.Read first next time: this block, then from-opencode, then from-hermes, then housekeeping-log. Next: dental app web content — a NEW session, and Matthew may tidy the app before any image shots are taken.
What this session was. Opened as "continue yesterday's web content" (session 54 had opened the pharmacy branch and was waiting on the page list). It became two things: a live infrastructure fix, and the drafting of the entire pharmacy set.
1. The wiki image-upload fault — nginx, not Wiki.js. Matthew could not upload images. Cause: wiki.veripath.co.uk was the only app vhost on VPS1 with no client_max_body_size, so nginx's 1MB default refused anything larger with 413 before Wiki.js ever saw it — which is why nothing appeared in the app's logs. Fixed to 50M, live and committed in veripath-ops (d6a55dd, local; master 4 ahead of origin). Proved: a 3MB POST returned nginx's 192-byte 413 page before; after, a 5MB multipart upload reaches the app. Ruled out: no Cloudflare (DNS → VPS1 directly), container writes fine as node, no app-side cap. FYI note left in from-hermes. Asset bytes live in the Postgres assetData table (the disk storage module's own dump query joins it), so wiki media ride the DB backups; /wiki/data/uploads and /wiki/data/cache are disposable.
2. The pharmacy web-content set — all ten pages drafted. In the wiki under marketing/web-content/pharmacy_app, grouped as Matthew confirmed: Stock — stock, ordering, supplier_portals, data_feeds (supplier_register skipped by Matthew); Prescriptions — dispensing, notifications (parked, holding note: the feature is built with a mock NHS Notify client, but no verified guide or screenshot backs it); Compliance & administration — rp_log, counter_checkout, accounting. Each page is grounded in the app's own code/templates and the live-verified guide records, with real UI captures where they exist.
The register was settled with Matthew over three passes on dispensing — his verdict on the first two: too much like a user manual. Agreed style, applied to every later page: sales-led, roughly 2,500–3,000 characters, benefit-led headings, no mechanism (no state machines, row locks, append-only, role gates), plain English, one screen of reading. Also watch for sentences ending on a pronoun or a vague flourish ("the other is told") — he caught one; read for it before publishing.
Accepted: stock, ordering, supplier_portals, dispensing, rp_log. Awaiting his review: data_feeds, counter_checkout, accounting. Parked: notifications.
3. A false claim found in my own playbook — corrected. pmm_playbook asserted "AES-signed dispensing transitions". Checked before writing (2026-10-04): not true of pharmacy — AES signs prescriptions issued in the GP and dental apps (clinical_data/integrations/aes_client.py), and pharmacy_dispensing contains no signing code at all. Replaced with the verified truth (state-and-role guards, one row lock per claim, append-only local PharmacyAuditEvent trail carrying no clinical content) and struck the AES row with a dated do-not-publish marker; mirror refreshed, 0 pending. If AES signing is intended for pharmacy dispensing one day, it belongs on the deferred list until built — Matthew to confirm.
4. Two defects in the drafts' own pages, both fixed. (a) My extraction helper had stored a stray wrapper separator line inside the stock page; stripped, and every page now verifies seps=2. (b) Matthew's hero images had been pasted onto the same line as the # H1, which stops the heading rendering — the ordering page literally displayed # Ordering as text with no <h1>. Four pages fixed: heading restored, image left at the top as a hero, and alt text written from the image itself using Gemini vision through the existing GOOGLE_API_KEY (no install needed — gemini-2.5-flash, maxOutputTokens must be generous or the thinking budget truncates the reply). Two heroes flagged as possible replacements; four pages still have none.
Publishing is untouched and confirmed-separate: the website has no pharmacy collection — publishing needs a new Decap CMS collection in public/admin/config.yml plus a deploy, and Matthew confirms that step.
Read first next time: this block, then from-opencode, then from-hermes, then housekeeping-log. Next: Matthew reviews data_feeds, counter_checkout and accounting; decides the two flagged hero images and supplies the four pages still without one; then more web content — he is to name what (the pharmacy set is complete, so this is likely the GP set, or taking the pharmacy copy into the CMS).
What this session was. Opened as "unfinished work from yesterday on the pharmacy app backend / data feeds", then pivoted to the website. Matthew parked the feed follow-ups to Monday. The session: (1) closed out the leftover feed check, (2) opened the website work, (3) added the pharmacy persona to the marketing skills.
1. The parked feed item is CLEAR — verified live. The 03:15 UTC pull ran and published dm+d TRUD 9.3.0 (release_identifier 3240926, publication 2026-09-28) at 03:16Z on 2026-10-03; the 21/09 release is superseded, and the WDA register refreshed at 04:15. Rendering the app's own reference_data.health against the central store: all five feeds read Current, dm+d key accepted, nothing overdue. (A host-shell render shows concessions "mailbox not configured" only because the mailbox env is not in the host shell — production polls that mailbox every 15 min and holds cleanly.) Still open on that thread, parked to Monday: the wiki build record for the data-feeds work; mirroring the three compliance-dashboard fixes into pharmacy_booking_app (the Data Feeds card is gp-only); and who should receive feed alerts.
2. Website focus started — app content pages. Plan: each app (pharmacy, dental, GP booking) gets dedicated feature/benefit pages. Drafting happens in the wiki first, then publishes into the CMS. Decision: extend the existing marketing/web-content/ tree — not development/projects/<app>/web-content. Created marketing/web-content/pharmacy_app (folder node); its landing page carries the plan and a proposed page list: overview, dispensing, stock, ordering, supplier portals, supplier register, data feeds, RP log, counter checkout/VAT, notifications, accounting. Nothing drafted yet — the page list awaits Matthew's confirmation, then he names the first page. Note the site has no pharmacy page/section yet (GP = /health-clinics/, dental = /dental-surgeries/), so the pharmacy set is greenfield.
3. Skills updated — the pharmacy persona. pmm_playbook gained §3D Pharmacy (features to pitch, the sovereign-data hook, the data boundary written as the pitch, and an explicit deferred list so nothing unbuilt is advertised), five translation-matrix rows, and a pharmacy competitor block (cloud PMRs / legacy PMR estates / ordering add-ons) with a verify-before-publishing caution. The marketing skill gained a drafting-stage section (wiki marketing/web-content/<product> → CMS → deploy) plus description/tags. Mirror gap found and fixed: /opt/hermes-skills-sync.py's VERIPATH_SKILLS list omitted marketing, so that mirror page had been going stale silently and pmm_playbook was stuck flagged pending against an older copy with no real edits. Both mirrors refreshed, marketing added to the list → sync now 0 pending; both pages HTTP 200.
Read first next time: this block, then from-opencode, then from-hermes, then housekeeping-log. Next: Matthew confirms the pharmacy web-content page list (or names the first page) and we draft it.
What this session was. Opened as "the backend of the pharmacy app"; Matthew's first ask was a view monitoring the
data feeds (NHSBSA / CPE / TRUD). We found that view already existed and was simply unreachable from the pages he
uses — so the session became: clear the false alarms on his dashboard, surface the feeds, and unblock the one feed that
was genuinely stuck.
1. Three false alarms on /dashboard/compliance-admin/ — fixed, deployed, verified.
veripath-ops/scripts/platform-vps-health.sh31b293f). The dashboard reads 3 of 3 ready./tmp/scan-analysis.json,platform-backup-status.json and security-checkin-status.json had been replaced by empty directories (createdIsADirectoryError, the 5-minute sync silently skipped them, and the app served stale data. Directories removed,2. The feeds, surfaced. reference_data lives in gp_booking_app — not pharmacy_booking_app, which has no
reference_data app at all (so "the pharmacy app" and gp.veripath.co.uk were being conflated all session). The views
were linked only from base.html; the nav used by every dashboard page (dashboards/base_dashboard.html) had no
link — which is exactly why Matthew believed there were no such views. Added Reference Data (Infrastructure) and
Medical Device Register (Compliance) there, and added a Data Feeds card first on the compliance dashboard:
one-line summary, action/info alerts, and per feed the state, live release + publication date, age, next expected and
last attempt. Sourced from reference_data.health so it cannot disagree with the Reference Data → Freshness &
health page, and it degrades to "unavailable" rather than taking the dashboard down.
REFERENCE_DATA_ALERT_EMAILS was unset, so notify() always returned False and thehealth_rows() never set has_routine, so the health page's "Last successfulkey_status() called any attempt that was not explicitly invalid3. dm+d unblocked. Matthew supplied his TRUD account credentials; I logged in, read the key from the account's
Your account → Your API key page, and neither the password nor the key was printed anywhere. TRUD_API_KEY is now
set (40 chars) in the gitignored env on both hosts; the temporary credential files were shredded; nothing on the host
carries the password. Verified through our own client, not curl:
TrudClient().list_releases(latest=True) → HTTP 200, and TRUD offers release 9.3.0, dated 28/09/2026, against
our live 21/09 release. dm+d now reads Release overdue (danger) — the truth — instead of "Not running — no key".
4. ▶ DO THIS FIRST TOMORROW. Check whether the 03:15 UTC pull ran and published dm+d 9.3.0 (celery beat is
up, the key is in place). If it published, dm+d reads Current and the summary reads "All configured feeds are
current". If it failed, the ReferenceFetchAttempt row and the alert email give the reason. The import is the heavy one
and was deliberately left to the scheduled run rather than started by hand.
5. OPEN / PARKED.
reference-feed-status,pharmacy_booking_app (identical dashboards.py andreference_data exists only in gp_booking_app.Deploy reality (re-confirmed this session). The app host's checkout has no git remote — commits ship as git
bundles. The code is bind-mounted, so a code change needs a restart, not a rebuild; an env_file change needs
docker compose up -d (recreate), not docker restart. Trees: VPS1 and VPS2 both at 634c139;
assign_roles.py is modified-but-uncommitted on VPS1 and is not mine.
Tests. hermes verify --json (recipe in gp_booking_app/.hermes/environment.json) — 35 OK. The suite needed the
new SchemaCompleteRunner because Django's schema build skips managed=False models, which had made the dashboard tests
unrunnable from a clean database.
Pruned 2026-10-02 (session 50). Sessions 45 and earlier now live on
hermes-resume-archive — moved verbatim, nothing lost. This page
keeps the current cold start, the last few sessions, the read-first order and the Do not re-discover list, so
a new session pays only for what it needs.
What this session was. A cold start that went straight at the one parked item — "okay let's do it", then, on the
illustration choice, "let's do as you suggest". C3 was cleared, and the page came out bigger than the item, because
the app had drifted twice under it.
1. The parked claim was half the drift. 508aea3 prunes the cache on refresh, so the "a batch that has left the
shelf still shows a quantity" row is gone — replaced by the removal behaviour, plus a new row for "a batch that was
listed here, and now is not". Reading the live tenant first, as the parked note required, showed the rest: the
cache held 5 rows and the report was empty, because the two Amoxicillin rows the guide's worked example was built on
(DEMO-AMOX-B3, B2) had themselves been pruned by a refresh at 04:32 that morning. A run of manual presses is the
only refresh there is (no beat task, no host cron) — verified again.
2. The second drift: the report carries price columns. f05fc3f (the ordering views, 2026-10-01) put the five
price_view columns on the expiry and low-stock reports, for the three stock-managing roles. C3's column table
stopped at Quantity; it now names them and says the Counter Assistant's page has none. Proved live on the same
show_prices path by capturing the low-stock report as both roles — 8 columns for the RP, 3 for the Counter
Assistant — because an empty expiry report has no rows to show them on.
3. The illustration rebuilt, with his word. Two short-dated batches were received through the app's own goods-in
form as tc.tech.one (DEMO-AMOX-B4, 24 packs, 20 Oct → critical; DEMO-SALB-B2, 12 packs, 1 Dec → no
pill), receipt GIN-20261002-6C6D66 / ERPNext MAT-STE-2026-00033; a refresh then reported 7 and the overview
reads Expired 0 / Short-dated (90d) 2. The guide's example table and figure are that live page, captured as
tc.ca.one (asset 160, downloaded and byte-verified).
4. What could not be staged — and why the guide says so. An expired row: goods-in refuses a past expiry date,
and ERPNext refused the adjustment with BatchExpiredError: … DEMO-AMOX-B3 has already expired (HTTP 417). Nothing
moved — the batch document predates this and holds no stock. The page now states plainly that an expired row is
found, not created. The refusal's banner ("could not be posted to ERPNext. Please try again.") invites a retry that
can never succeed, so it is noted for C4 / OpenCode, not fixed here.
5. C5 corrected in full — the other half of the parked pair. The same false claim sat in four places (the
"Refreshed 5 stock position(s)" row, the "it never removes a position" paragraph, the "neither counted nor
removed" 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, deleting a cached position in the scope just read that ERPNext
did not report, and only after that scope read without error, so a failed refresh cannot empty the cache. Figure
re-shot from a real refresh as tc.tech.one ("Refreshed 7 stock position(s)", timestamp 2:55 → 3:01 p.m.).
6. C2 — the knock-on, closed in the same pass. Two drifts: it sent the reader to Low stock in the top menu (it
is a button under the overview's figures — the live nav settles it) and it did not mention the five price columns
the three stock roles see. Both fixed; both figures re-shot live as tc.ca.one.
7. B4/B5 finished — and a "pending" that had stopped being true. The coverage plan's "B5 — two corrections
pending" (drop the endorsement reference box; move to the coded exemption values) had already landed on
2026-09-19 (5b4e135, 0a55eea) and been deployed; B5's text already carried the older-records note, so the entry
was 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; the dispense screen renders the coded EPS list whose first entry is the blank
value "— no exemption (charge due) —", which B4 now names (a dropdown reader is not "leaving it blank"). Three
figures re-shot (b4-dispense-form, b4-dispensed-token, b5-dispensed-token; assets 147/148/150);
b5-endorsed-token.png left deliberately — its endorsements predate the field's removal, which B5 explains.
8. C4 amended, and the app-side fix authorised and briefed. Staging C3's expired row turned up the second half of
the finding: the adjustment form reports every ERPNext failure as "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. C4 gained a refusal row, a retry passage split into
refusal vs post-then-fail, and a Direction note. Matthew authorised the app fix ("okay let's fix that"), so it is
now an OPEN build brief in from-hermes — the second ask in OpenCode's inbox,
after the dental Notify brief. Nothing was built from this side.
9. Records. The coverage plan carries dated corrections on C2,
C3 and C5 (the prune, the price columns, the seeded batches, the tc.ca.one figures) and the B5 finished entry;
housekeeping-log Round 31 carries the round, C4 included.
Read first next time: this block, then from-opencode, then from-hermes (two open asks), then
housekeeping-log (Round 31). Nothing in the pharmacy guide series is
owing. Two app asks sit with OpenCode: the adjustment-refusal message (authorised by Matthew on 2026-10-02 — when
it lands, verify the strings live and drop C4's "the app cannot yet tell them apart") and the dental Notify brief.
Elsewhere: the parked Phase 4 tidy-up, and whatever he names next.
These blocks were split off the resume page when it reached ~198 KB and 30 session blocks. They are the history of the pharmacy reference-data, pricing, satellite and register work; nothing below has been edited.
Matthew's instruction, in his words. He has read the C8 guide; "there is more work to do with the satellite", and
the next thing is "some test portals" — "some kind of simple testing system wherein an app contains test data, similar
to what a wholesaler would hold", so the satellite can be tested properly: "3/4 suppliers ... each one has its own
portal in which we seed data"; each "will of course require their own database"; they "can be made as simple and basic
as necessary as they're only purpose is for testing" and "will never be used in production". He dropped the
multi-IP spread ("that will make our testing easier"), and named the data concern that shapes the seeds: discounts,
"which wholesalers are forever changing, with particular emphasis on short-liners", pointing at
Discounts & Test Portals (his own exchange with
Gemini that morning). He then said: "I think we can put a plan together for opencode".
What was written this session (wiki only — no application code, no portal contacted, nothing built):
from-hermes — the plan note is now the top note (12 held — 7 OPEN, 1 partly closed, 4 HANDLED), markedThree findings worth carrying. (1) The Gemini page holds two mutually exclusive positions on short-liner files
(baseline prices needing a matrix layered on them, versus files already pre-discounted net) and is written against a
different stack (Frappe/ERPNext doctypes, a satellite that scrapes rows and holds them in memory); the spec's §5 keeps the
substance, and the page is a fourth research page owing a dated assessment banner (Round 24, item 3). (2) The
discount question is app-side and unspecified — the import has no discount column and no list-versus-net concept, and
phase 4 deliberately left the margin column unbuilt — so the rig must not be allowed to define the feature.
(3) He added a further section to that page in the same session — No CSV download facility — asking whether any
wholesaler could lack a CSV download. Real, and now written into the rig as P4's three states (no download with the
prices on screen; no download and nothing on screen, so the manual upload carries it; and a print-only view standing in
for PDF price lists). Not adopted: the section's answer of reading the table off the page and writing it into the
database breaks three standing rules (the app is the only writer; the satellite never writes or holds a credential;
reading the portal back is out), its "if a human can read it, we can read it" claim is neither a technical nor a legal
position, and its only citation is a wholesaler's marketing page — not evidence. Spec §4.1 records it.
His answers, given the same session, and the go-ahead. §10's two gating questions: (a) "anyone in the network
(veripath) can access the rig" — one host on our own network, never public; (b) "may as well get all 4 done" —
all four portals in the first build. The other two I answered on my own recommendations (the dummy login stays on
two of the four; a test rig first, showable inside the network but no demo polish). He also ruled out the
no-portal scenario — "don't bother with a scenario where there is no portal" — so the emailed-file route is not
built, and §4.1 records the ruling rather than dropping it silently. The spec's status line, §1 table, §4.1, §6 R1/R2
and §10 were updated to match, and the go-ahead brief went to the top of from-hermes (13 notes held — 8 OPEN),
with the plan note marked AMENDED so its read-don't-build line cannot be read as live.
OpenCode then asked two questions and built nothing (its own rule: ask, do not guess) — the rig's host and four
reserved names, and the origin allow-list's mechanism. Both are design calls and neither needs Matthew, so I
answered them the same session and recorded the answers as the spec's §11: VPS1 /opt/synthetic-portal-rig in its
own Docker network (not the hub network), no product module imported, four names with no public DNS record, and
a dedicated Postgres container rather than the central instance; and the allow-list as a domain-suffix list compiled
into satellite/ (OpenCode's option A — option B is refused, it would name a tenant host in the program), with
label-boundary matching, HTTPS except loopback, the check before anything is acted on, and the impostor on a
fifth name outside the suffix. Two traps went with it: P3's two files must be two real downloads, not a zip, and a
signed-out portal must offer no download control. The answer is the new top note on from-hermes (14 held — 9
OPEN) and OpenCode's note is marked ANSWERED.
OpenCode built it and I verified it live, the same session, and accepted it — §12 of the spec is the record.
09058e3 (feature/pharmacy-satellite-allowlist, origin/dev, the deployed revision confirmed by me on the app host; no
migration): the launcher waits for the order to be opened, the seed's --reset help line names its blast radius,
the origin allow-list is paid (stage-2 §5 W3) and the rig is built, R1–R7. Everything below was run by me, on the
live system: 600 tests OK (233.8 s); verify-satellite-view-live.py 97 checks; verify-satellite-skeleton-live.py
all passed; verify-satellite-view-admin-host.py 9 host-side; and three new verifiers —
verify-synthetic-rig-live.py 88, verify-synthetic-rig-browser-live.py 22 (the impostor refused exit 8 with
the exact sentence, read on loopback, refused on file://, and P3's two-file control really put two files in the folder
in headless Chrome — not a zip, not one), verify-rig-files-into-app-live.py 16 (the rig's files at the live app's
mapping step: P1's discount column matching none of the hints, P3's headerless file with its first line as a row, and
no price file written).
Two findings, both mine rather than the build's. (1) Three of my own verifier expectations were stale the moment
the allow-list landed: the sheet quotes nine report states, the domain is deliberately carried by origins.py, and
saved pages must be served over loopback http because file:// is now refused. Corrected in the verifiers; §12
records it. (2) delete.sh removed the rig's directory and the source lived only on the host — a one-command delete
with no way back, for a rig stages 3–4 and I2–I3 both need. The source is now committed as
veripath-ops/rig/synthetic-portal-rig/ (with RESTORE.md), and the delete-and-rebuild was then proved: with no
rig present the product's end-to-end satellite check passed in full, and start.sh rebuilt and reseeded the rig from the
versioned copy, after which the whole rig verifier passed again (88 checks). The rig is left running for Matthew.
One wiki-hygiene action taken without asking, and reported. The handback note carried the rig's dummy login on the
page. A credential — even a synthetic one on a loopback-only rig — does not sit on a wiki page, so I redacted it there
and left a marker saying where it belongs (the rig's own notes). Nothing else in the page was touched.
Not proved, and said plainly on the spec: the satellite driving a portal (outbound stages 3–4) and the inbound
catch (I2–I3) are not built, so the drives they perform on these portals cannot be run yet — §12 records the rig as
ready for them, not those stages as done. The Windows pass (the .bat cannot be exercised here) and W9 remain
owed.
Then the hosting, which is where the session ended — and it is the one thing a new session must know. He asked why we
were not simply hosting the portals, and rejected my first answer (a separate domain, an IP wall) as "extremely messy
and requires a lot of running around", with the argument that "nobody is going to mistake them for actual wholesalers
considering we will not be adding any content". He then added four subdomains — portal1 to portal4.veripath.co.uk
— and the session ended with all four live over HTTPS: nginx in front of the rig's internal port, one Let's Encrypt
certificate covering the four names (portal1 and portal4 first, then --expand when portal2 and portal3 landed),
plain http 301 → https, and the rig's own port still bound to loopback and the internal addresses only, so the vhost is
the only way in. Verified: HTTPS 200 on each of the four, certificate validated per name, the rig verifier still
88 checks on the internal path. The record is amended where it said otherwise: the spec's §7 never-public bullet,
§11.1, §10 Q1 and §12 now say the rig is hosted, on whose decision, and what still holds (synthetic banner, noindex,
nothing real, no credential, port internal). The rig's README and the versioned copy carry the hosted addresses;
veripath-ops is at 9cb0818 with the rename (5ed6ab6) and the rig source (358a2bc).
The thing that cost the most time, so a new session does not repeat it: the whole reachability detour. The rig was
built internal-only on my own §11.1, no peer had ever connected to either VPN, and everything else here is reached through
nginx on 443 under a public name — so the internal-only rule could never have worked, and the fix was the front door
above. If a future test asset needs reaching from a machine outside this host, assume the nginx-on-443 route first.
At his request, a practical page now sits beside the spec — The Synthetic Wholesaler Portals — the Test Environment — under a new
development/projects/pharmacy_app/testing/ node: the four addresses, what each portal offers, how to change its state,
the three verifiers and their counts, what the environment is not. The spec is the record; that page is the one to read
before touching the rig, and both carry a line to the other.
Where that leaves the programme. The rig is ready for stages 3–4 (outbound) and I2–I3 (inbound); those are the
next build, not built, and the satellite still does not drive a portal. Owed and unchanged: Matthew's Windows pass
(the .bat cannot be exercised here, and it owns the packaging decision) and W9. Owed by me: the three research
pages that owe dated assessment banners (research/csv_format, research/supplier_csv_files,
research/selenium_gadget_supplier_portals) and the two pages still quoting the old header row — his to name, one at a
time. Nothing of mine is running; the rig is left up, hosted, with its host entries on this host only.
Still owed by me, unchanged: the three research pages that owe dated assessment banners and the two pages that
still quote the old header row — his to name, one at a time.
Updated 2026-09-29 (session 44). OpenCode built The Satellite View — Setup and Controls
(feature/pharmacy-satellite-view @ d11cbd0, origin/dev, migration pharmacy_stock/0011) and I verified it
live the same session and accepted it — the record is §9 of the spec. Nothing is running now, and the pass touched
wiki records only after the verification was done.
veripath-ops/scripts/verify-satellite-view-live.py) plus 9 host-side for the platform administratorverify-satellite-view-admin-host.py), with every expectation read read-only out of the tenant databaserecon-satellite-view.py); the deployed revision was confirmed on the app host by me, and the full pharmacy suitewaiting with "The order satellite is switchedsatellite-launcher-linux.sh) ran against a real export — exit 0, the realSYSTEM_ADMIN user's session forced on (200, the Support view sentence, no form, no download link,pharmacy_booking_apporigin/dev = d11cbd0; veripath-ops carries my repoint f523848 plus OpenCode's 8687ecf / 4e1f101 and theresearch/csv_format, research/supplier_csv_files, research/selenium_gadget_supplier_portals) and the two pagesorder-stock-and-export-the-order-file,supply-orders-pricing-phase4-value). Neither group was touched this session — still his to name.gp_booking_app (platform) work, notfrom-hermes.c8-satellite-view.png, c8-satellite-report.png, assets 171–172) and it isfrom-hermes: theverify-satellite-view-live.py, verify-satellite-view-admin-host.py,recon-satellite-view.py, satellite-launcher-linux.sh, and wiki-page-body.py (extracts a page's markdown from a--get, so no page edit needs the wrapper hand-trimmed again).Updated 2026-09-29 (session 43). Matthew's instruction for the session: concentrate on the Selenium satellite —
"the gadget that will be downloaded on a pharmacy's PC and act as a bridge between the pharmacy app and wholesaler
portals" — and his pointer was Download & Scrape (he
first named the CSV Format page, then corrected it to that one). Nothing is running; no application code was written by
me this session; the wiki gained one new draft page and the handoff inbox gained one note.
His framing, in his words, because it fixes the scope. The satellite's download-and-scrape route is "an
alternative and more automated way of getting data imported and mapped to the pharmacy app"; it does not replace
the manual procedures, "as they must always exist"; he cannot confirm that every wholesaler offers a CSV
download, though he would find it strange if any could get by without one; the reason for doing it is that "with
appropriate AI/machine learning, we can make this process almost as automated as the traditional, more expensive PMR
systems that rely on EDI integration"; and how and when AI/ML applies is a later discussion, "once we have the
essential components in place" — a scope instruction: components first, intelligence afterwards.
Published: Supplier Price Downloads — the Automated Route (Scoping Note)
(page 666, DRAFT, awaiting his review) — the inbound mirror of the supplier-portal scoping note, which is not
restated. It carries the invariants (the manual upload is never retired or made second-class; the app is the only
writer — the satellite hands the caught file to the app's own upload step, never the database; the portal's own
download control only, never a guessed URL; the evidence rule; the operator's mapping and confirm stay; nothing is
deleted out from under the browser), the end goal decomposed A–F, stages I1–I5 (I1 the inbound signal on the
price-files page; I2 the satellite presses the portal's own download control against a synthetic portal, and pays
the origin allow-list stage 2 W3 records as owed; I3 the catch — exactly one new file, two refused, handed
into the app's own upload step; I4 the record, including which mode the file arrived by; I5 the first real wholesaler
from dated evidence), the dead items from the research not to be re-proposed (the Hermes auto-healer as a service;
filename-embedded supplier/WDA ids; C:\VeriPath\Pipeline\ + the "instant purge"; the "17-digit AMPP" claim and PIP
in dm+d; the nginx-CORS/WebSocket "developer checklist" and the manifesto), and six questions for him (is the stage
list right; may we build a synthetic portal; the catch rule; may the satellite drive our own page; who sets the price
date on an automatic fetch — the 29/09 date trap is why it matters; and what to do about a supplier with no download).
He then read the stage plan and said: "okay it sounds we need to get some already planned work implemented? If that
is the case I'll pop over to opencode and then we can continue" — taken as the go-ahead for the already-specified
work, and answered with an explicit brief so one line can be corrected if that reading is wrong. The new top note
on from-hermes is the brief: build Supplier Portals stage 2, the satellite
skeleton, §5 W1–W7, §6 the fixed sentences, §7 the never-do list, §8 the tests, §9 the live plan; Linux rig first;
the spec's §1 refinement confirmed — there is NO download in this stage; the Windows pass (W6) is his, so the
packaging decision stays an output; the two small OPEN asks (the mapping pre-selection, the minimum on Synthetic
Wholesaler Three (DEMO)) may ride along; and nothing from the draft inbound note is to be built (unauthorised, he
is reviewing it). The note also repeats the origin/dev revert hazard (dev must equal the deployed revision).
Next from me: verify the skeleton live on the Linux rig when OpenCode hands back — the spec's §9 plan, headless
Chrome for Testing 152 on a loopback debugging port, both ready and waiting cases — and write the dated as-built
note on the stage-2 spec. Then the three research pages that owe dated assessment banners (research/csv_format,
research/supplier_csv_files, research/selenium_gadget_supplier_portals) — one page at a time, his to name — and
my two carried owed items (repoint verify-pharmacy-price-ordering.py's stale-price check; the two pages that still
quote the old header row).
Noticed, not touched (a housekeeping item for a later pass): the root-level page pharmacy_app_selenium_gadget
(~69k, no description) is the original research transcript of this whole programme and overlaps
research/selenium_gadget_supplier_portals heavily — a root-stray / consolidation candidate for Phase 3/4, not a
content question for this work.
Stage 2 VERIFIED LIVE by me, the same session, and accepted. OpenCode built it (4e947c8; I confirmed on the
app host that origin/dev equals the deployed revision), I read the whole program and the app-side diff, ran the
suite myself (562 tests, OK, 193 s, no skips — the browser tests really drove Chrome for Testing 152), and drove
every state §6 fixes on the Linux rig against the real page and the real export: exit 2 (nothing listening),
3 (no VeriPath page), 0 (ready, six-row report, the real filename and its real 3 lines), 0 (already
handled), 6 (file missing), 7 (file stale), 0 (a cancelled order carrying the app's own sentence). Also
proved: the export's Content-Disposition name equals the signal's file, the state file holds only
order/filename/size/time, a waiting order writes none, the only CDP method is Runtime.evaluate, and
importing satellite does not import Django (nor the reverse). Record: §12 of the stage-2 spec; my scratch probe
is /tmp/hermes-satellite-live.py; the mapping check is /tmp/hermes-mapping-live.py.
Two findings handed back (new top note on from-hermes). (1) The stale rule compares the file against the
page load, while its sentence claims "before this order was exported" — measured live: export with the page
open → accepted; reload the order page → the very same file reads as stale, which in stage 3 would refuse a
legitimate hand-over. (2) A doubled full stop in the waiting sentence ("…is cancelled.."), from §6's template.
The two asks: one verified live, one deliberately not closed. The mapping pre-select is fixed and verified on
the live confirm step for all four supplier files (A pre-selects PIP_CODE/TRADE_PRICE/DESCRIPTION/PACK_SIZE;
Two's Product and Four's absent column leave description empty; the basis is never guessed), and the tenant held
9 price files before and 9 after — the confirm step records nothing. The seed minimum is in the code but not on
the live tenant, and that is deliberate: a re-seed re-imports 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 and my verifiers quote. So my
verify-pharmacy-order-value.py repoint (Two → Three) stays owed until Matthew picks a scratch-tenant seed or a
hand-set minimum; I also asked OpenCode for the test that pins the three value states, which does not exist. I tried
and failed to set Three's minimum through the supplier screen (two POSTs, both refused by the form, nothing
written — supplier list read back unchanged) and stopped rather than fight the form.
Records written: stage-2 spec §12 (+ the §1 refinement marked RULED); from-opencode — its handback marked
HANDLED with my verdict note; from-hermes — list rebuilt (8 notes: 4 OPEN, 1 partly closed, 3 HANDLED, all three
verified live by me) plus the findings note; and the from-hermes archive pass earlier in the same session (16 → 7
notes, 9 moved verbatim with a dated pass line). Next from me: the three research pages that owe dated assessment
banners (csv_format, supplier_csv_files, selenium_gadget_supplier_portals) and the two pages that still quote
the old header row — one page at a time, his to name.
Second OpenCode pass, same session: the four findings and the seed, fixed and VERIFIED LIVE by me. 87cc874
(e9f9ff8 + 87cc874; origin/dev = deployed). The signal contract is now version 2 — it carries the order's last
export time (exported, data-exported), so the stale rule compares against the export rather than the page load;
the waiting sentence has one full stop; Synthetic Wholesaler Three carries £150.00 in the seed; the stock seed now
pins every demo item's cached position; Four gained DEMO-FOUR-001 so the no-minimum control exists; and a test
pins the four order-value states. My own verification, not taken on report: contract 2 read off the live page (a
never-exported draft serves an empty export time; a cancelled-but-once-exported order keeps one); the reload case
is now accepted — the exact failure I reported — while a back-dated file is still refused with exit 7 and the
sentence naming the export; one full stop; verify-pharmacy-order-value.py 50/50 and
verify-pharmacy-price-ordering.py 26/26 on my own runs; the price-file list reads 6; my own suite run: 565
tests, OK (191 s, no skips). Records: §13 of the stage-2 spec, and the contract's version 2 in §6 of the stage-1
spec; my probes are /tmp/hermes-verify-contract2-live.py and /tmp/hermes-check-draft-exported.py.
Two corrections to my own record, both mine rather than theirs. (a) The repoint I had listed as owed on
verify-pharmacy-price-ordering.py was not needed — that check keys on the price's label, not on a named
supplier, so it followed the re-seed by itself. (b) My probe asserted that a waiting order carries no export time;
a cancelled-but-once-exported order correctly keeps one, so the assertion was wrong, not the app.
⚠️ One side effect to tell Matthew about: the price seed now calls _reset() unconditionally, so every price
file belonging to the four synthetic suppliers is deleted before re-importing — that removed his four imported files
from the 2026-09-29 test run on the demo tenant (they are committed in the repo, so re-importing is minutes of work).
Right call for a demo re-seed; recorded on §13 and asked to be noted in the command's help text, because the same
command run against a tenant holding a real pharmacy's imported prices would delete them.
Updated 2026-09-29 (session 42). Matthew ran the four per-supplier price files through the live importer himself, in the browser, signed in as Bess Marginwell (tc.bm.one, PHARMACY_BUSINESS_MANAGER — the role the import requires). The files landed and the comparison works; the round produced one real defect (handed to OpenCode) and two accepted deviations from the intended end state. Next session is the Selenium satellite gadget — he asked to open a new session for it, and the order-file/portal half stays postponed until he has built the test portals. Nothing is running.
price_profiles.suggest_columns() returns the keys code / price / description / pack, the form's fields are code_column / price_column / …, and PriceFileImportView._upload passes the dict as form-level initial, which Django only copies onto fields of the same name — so every select renders — none —. Evidence both ends: locally suggest_columns(['Product Code','Price']) → {'code': 'Product Code', 'price': 'Price'}, while the live confirm page for the Four file has all four selects on — none —. No test covers the wiring (tests.py only exercises the pure function), which is why my earlier "the hints pre-select on the realistic export" claim was wrong — I read the options list, not the selected attribute. Handed to OpenCode; it costs the operator four dropdowns per file, nothing else.seed_pharmacy_prices --tenant test-client-pharmacy --reset, then redo Three and Four with 28/09 and 01/09 typed in (a reset wipes every synthetic file, so all four would be redone). The warning box on the confirm step is a warning, not a refusal — it names the row about to be superseded and the Import button still submits.seed_pharmacy_prices --tenant test-client-pharmacy --reset on the app host (it deletes every price file of the four synthetic wholesalers and re-imports the five seeded examples) — verified back to 5 files / 11 rows between runs.veripath-ops/scripts/verify-pharmacy-price-list-read.py — read-only, signs in as the Business Manager and prints one line per price file with its counts and status, for diffing before and after an operator run. Still owed by me: repoint verify-pharmacy-price-ordering.py's stale-price check (A now carries today's prices), the two pages that still quote the header row, and the three research pages that owe a dated assessment banner.Updated 2026-09-29 (session 41). Matthew settled the file contract, the work is now import and export first,
the satellite afterwards, and the round is closed on both sides: OpenCode built and deployed it the same day
(085f7ba) and I verified it live — one small seed ask has gone back (below). Nothing is running.
supplier_csv_files and selenium_gadget_supplier_portals.pharmacy_stock/sample_price_files/ plus a README.md withtest-client-pharmacy (counts and findings below).feature/pharmacy-session-20260929 @ 085f7ba with migration pharmacy_stock.0010; my verificationDEMO-OTC-BARCODE-0002,1 and 2345678,32 — headerless, one line permigrate_client --plan → No planned migration operations), measured the partial state live,from-hermes): the stage-4 short advisory has no live state. Synthetic Wholesalerverify-pharmacy-order-value.py is 47/50 on exactlyveripath-ops 3e67a9b): both order verifiers now find the created draft byverify-pharmacy-price-headerless-live.py (12/12) covers the headerless mode, the list marker and the supersededorigin/dev is now fast-forwarded085f7ba, so dev = the deployed revision (the app host had been serving the feature branch while dev sat 23dev would have reverted the contract).pharmacy_stock/supplier_price_files/ (+ a README.md holding thefa0de11, pushed to dev and the feature branch — both hosts are/root/supplier-price-files-20260929.tgze2ca258a9d7094e1b2c203720009e11614a810cb53fc8d9d521693d65766fe79). Counts to expect on thescp<source> <destination>, two arguments) and will run the imports himself in the browser.verify-pharmacy-price-ordering.py — it keys on "the stale price is never the proposed one — Syntheticresearch/csv_format assessed 2026-09-29,supplier_csv_files and selenium_gadget_supplier_portals, which lost theirs on the 2026-09-28 re-upload).from-hermes: (1) the headerless import route issynthetic-no-header-positional.csv is its acceptance fixture (must read 3 lines,Updated 2026-09-28 (session 40). Mid-discussion with Matthew on what sample data to create for the pharmacy
app's supplier-portal / satellite route. No sample file has been written yet (he paused the delivery route with
"no hold on a moment"). The two open decisions in the queue at the foot of this block are the whole live state.
development/projects/pharmacy_app/research/supplier_csv_files grew to ~82k and.../research/selenium_gadget_supplier_portals to ~70k, and the two now overlap heavily (the Seleniumveripath_order_<supplier>_<id>.csv filename parsing, the Hermes auto-healer, a.exe, and "pick the newest CSV in Downloads".supplier_id column ("Option B"), the auto-healer,help.hotschedules.com article 4406961238029, updated 2024-08-22). The article page itself ishttps://help.hotschedules.com/api/v2/help_center/en-us/articles/4406961238029.json returns 200 with the body.article.body. It specifies the return leg: two files a pharmacy and a supplier exchange as documents.YYYYMMDD; numbers with an explicitH header line (supplier code, POD detail line per original order line, each carrying a status —read_file takes theheaders=['0123456','1.20'], rows=2, and the import reports a cleanSupplier.export_profile hook having to express "no header" as the standard are all in the session-41 block andpharmacy_stock/sample_price_files/ — a new folder, so the seed (which reads its ownexample_price_files/ list) is untouched — with a README.md carrying every count: (1) the realistic five-columnPIP_CODE,DESCRIPTION,PACK_SIZE,TRADE_PRICE,AVAILABILITY, with a £ sign, a thousands separator, a.tsv (a Trade CostList Price decoy, and a code whose leading zero was lost), (3) the headerless positional file,test-client-pharmacy:/var/www/pharmacy_booking_app_static/ holds only the Django static directories, andhttps://test-client-pharmacy.pharmacy.veripath.co.uk/static/price-file-samples/ answers 404. The live options aregit.veripath.co.uk so the samples are an ordinary repo download, or a place he names.agents/handoff/from-opencode has no open notes (the 2026-09-28 handback is HANDLED;from-hermes). Nothing is running — no background processes, both/tmp/sc2.md (the ~82k page dump), /tmp/api.json (the/tmp/resume.md.COLD-START QUEUE, in order: (1) read agents/handoff/from-opencode and agents/hermes/housekeeping-log;
(2) get Matthew's answer on the three price samples and on whether the confirmation/return leg is recorded as scoped
capability (flag-only); (3) only then write the samples; (4) reinstate the dated assessment banners on both
supplier research pages; (5) the next wiki page re-draft is his to name — one page at a time, then stop for review.
Updated 2026-09-28 (session 39 — THE RIGS ARE SETTLED and stage 2 is written). Same day as session 38, continued:
the supplier-portal stage is open, stage 1 (the mothership half) is with OpenCode, and stage 2 — the satellite
skeleton — is now specified and waiting on one yes/no from Matthew.
/root/work/satellite-stage2-probe/: probe.py, signal-mock.html, FINDINGS.md) attached to a browser on aOrigin header (so the client must send none — and this is why thereadyState — "no signal" and "not ready yet"data-portal-target reads asportalTarget, so a kebab-case read silently sees an empty target).satellite/ inside the pharmacy repo (imports nostate.json that127.0.0.1. §6 fixes the report sentences; §8 lists the tests including averipath-ops, then Matthew's Windows PC)..exe, no PyInstaller, no autostart); the authenticated download is specified in stage 3.from-hermes. The one thing in it that is<script> tags inpharmacy*/templates/, no static JS, one hand-written <style>, and forms written to work server-sideaccept = .csv,.txt,.tsv (the form's own help text, neverfileMultiValueDict returns the last).feature/pharmacy-session-20260928 @ ee45617, deployed to the app host): the price-file drop zone, supplierpharmacy_stock/0009 wassector_client_test-client-pharmacy (DB test_client_pharmacy), so every stock page answered HTTPmigrate_client --slug test-client-pharmacy --app-label pharmacy_stock; the apex DB has nopharmacy_stock_supplier table at all, so a migration run there proves nothing. Then, after re-seedingseed_pharmacy_stock, seed_pharmacy_prices): drop zone — verified in the browser as well as over HTTP (a.txtContent-Disposition byte-identical to data-file, with the camel-casing trap (portalTarget) confirmed on theDEMO-OTC-IBU-200 has a cached on-hand of 48 against a levelSynthetic Wholesaler Two group holds one pricedfrom-hermes; four verifiers are committed to veripath-opsverify-pharmacy-portal-stage1-host.py, verify-pharmacy-portal-signal-live.py,verify-price-drop-zone-live.py, verify-price-drop-zone-browser.py).pharmacy-app-guides. As the series dictates, C7 stops here: the next guide item is Matthew's to name.veripath-wiki-assets skill is stale — it points at/root/.cache/puppeteer/chrome/linux-148.0.7778.97/..., which does not exist. The working browser on this host is/root/.agent-browser/browsers/chrome-152.0.7977.42/chrome. That skill is pinned,from-hermes (the tenant migration in the deploy; the seed reaching the partialhome/Updated 2026-09-28 (session 38 — SUPPLIER PORTALS IS OPEN; stage 1 specified for OpenCode). The reference-data
programme is finished as a build; the next stage of the pharmacy app is the other end of the ordering cycle — how the
app reaches the wholesaler's own portal — and it is now authorised.
Supplier.clean(), the inert #veripath-probe-signal block on the order page, the export filename in one place, anditem_code,quantity where our export writes ProductCode,Quantity, and itDEMO and shown as such. That is why stages 1–5 are verified against a syntheticagents/handoff/from-hermes); I verify it live and write the dated as-builthome/ strays with their referrer sweep, the DSPT merge read and theUpdated 2026-09-27 (session 37 — the deferred pricing walkthrough is WRITTEN; Phase 4 section B re-scoped). Matthew
delegated both recommendations, so the session delivered the walkthrough and took the section-B decisions.
{'key': 'reimbursement', …}) under Your price — one template field ({{ info.cells|last }} → .text). Thepolicies/ pages holdstandard_5 answers "the provided context does not contain anystandard_7a and standard_8 open "Based on the context…"), and standard_1a'scontent/policies/*.md), which is what a reader is actually shown.home/ strays were not moved. Every move in this log needs its referrer sweep,home/partner-user-management-plan-phase3 →development/projects/gp_booking_app/partner_client/ (its two siblings already live there) andhome/wireguard-vpn-setup → infrastructure/.home/ strays with their sweep, then the DSPT merge read; separately,Updated 2026-09-27 (session 36 — PHASE 8 IS BUILT, DEPLOYED AND VERIFIED LIVE; no application code written by me.)
Matthew handed over to OpenCode after session 35 and came back with "opencode has done its bit", so this session read the
handback and verified the build myself: on the app host and over HTTPS on the client surfaces.
DerivedPackPrice rows on net,3170926; the central storeveripath-ops (08fec29): scripts/verify-client-comparison-live.py — thescripts/verify-client-comparison-host.py (3564277), reproduces the whole evidence table in one command inside the--items-file (name → pk) because not every catalogue row appears on those screens.route=manual, evidence='linked by hand', operatortc.tech.one and the date on the row, an audit event written, the resolver left it alone (manual 1, changed 0,order_routing, order_value and order_profiles contain nopharmacy_stock 159 and the full(DEMO) marker, because the dm+d wording keeps the deterministic name + pack route availableDEMO-…), thefrom-hermes (the wrapper footer the handback write re-appended was removed rather than copied forward), this page andfrom-opencode is clear — its report is now HANDLED, and one small ask went backarchive pass done and four artefact-damaged pages repaired
Updated 2026-09-27 (session 35 — PHASE 8 SPECIFIED AND AUTHORISED; no application code written by me.) Matthew
opened with "we will be starting phase 8". I oriented from this page and both inboxes, measured the position the phase
actually rests on, proposed it, and he approved the recommendations and delegated the two open questions to me — so
both rulings are recorded on the spec with their reasons and are his to overrule.
price_view), advisoryfrom-hermes).gtin and nothing PIP-shaped), so PIP can neverStockItem and stops there. The routes are GTIN — the livecategory 2,618 of 3,248 ·none_part_two 596 of 598 · none_group 349 of 377 · none_concession 216 of 216) — then name + pack, whichdrug_tariff_news, OGL v3,consent_required = false, release published and live) and not the gated CPE stream (price_concessions,/medicines/ view already follows — "the CPE-derived rows are nevercentral-reference-data §6 row 8 plus afrom-hermes (the OPEN phase-8 brief on top — and the baked-in wrapper footer that--get warned about was removed on this rewrite rather than copied forward), this page, and the housekeeping logfrom-opencode carries no open notes.from-hermesfrom-opencode 7 → 0, with 24 notes moved verbatim, newest onarchive/2026-09/from-hermes (95 notes / 322,779 chars) and .../from-opencode (74 / 259,803) — read thosesecurity/security-check-in carried three nested--get header blocks and 28 footer blocks (4,188 chars — the audit's 23-block / 2,829 figure was low),security/client-vps-backup-plan 660, security/hetzner-storage-box 251, policies/standard_6 178. All four nowPage: … line. New tool:/root/work/wiki-clean-artefacts.py (report / --apply, verifies every write by re-fetching) — and the cause is--get output back into the upsert.from-hermes header claimed "OPEN only — 4 notes" while 6Status: OPEN rather than the **OPEN** form the count wasHANDLED and not OPEN/PARTLY. And two aging items were re-checked live rather than trusted todsp_clinic/settings.py andseed_pharmacy_stock still carries one supplier minimum — both still owed to OpenCode.from-hermes, and I verify it live as usual;policies/ underscore-vs-hyphen set,Updated 2026-09-27 (session 34 — the Part II GROUP RULES are built, deployed and VERIFIED LIVE; no application code
written by me). OpenCode handed back the §9.1 follow-up and Matthew's blanket approval covered the whole estate, so
this session both verified it and closed two ops items.
feature/part-two-group-rules @ 1d36ddf on the app host (migrationreference_data/0013), deployed by me: the held VMP file copied over from VPS1 (there is no dm+d release on the appreference_data_control_drugs, reference_data_part_two_groups, reference_data_derive_prices,reference_data_check clean. Five of Part II's eight groups are now applied — controlled drugsapplied=False with a dated derivation, stated as such on the queue view.reference_data_control_drugs loaded 24,662 entries / 964 in Schedules 1–3reference_data 220 (2 skipped) / pharmacy_medicines 35 / combined 178; verifier 26/26; client surfaces<CATDT>/<CAT_PREVCD> — a schedule change with itscurated and reviewable on the queue view./root/work/leftovers-20260818/), and check-backup-freshness.sh was rewritten —gzip -t proof on wikijs_pg/gp_booking_pg, the volume archives, andveripath-ops d663cee); BackupStockItem PIP → pack bridge, then the contractor's supplier price beside the reimbursement).Updated 2026-09-27 (session 33 — BOTH the pack link and PHASE 7 are VERIFIED LIVE; no application code written by me).
Matthew's steer opened the session ("check the handoff notes"), OpenCode had built two things in one pass, and both
are verified live by me on the app host and on the client surfaces:
linked / not_linked (queue) / no_lineno_source. Live and confirmed: theverify-pack-link-live.py — 32 PASS / 0 FAIL.reference_data 209 (1 skipped) / pharmacy_medicines(ALL), nothing dropped or invented; 1,433 matched, 29 queued, all by rule); the rateverify-derived-pricing-live.py (concession £3.61, a Part II pack un-deducted, the new no_line state, an absentfrom-hermes), the VPS2 repo leftovers are moved<CONTROL_DRUG_INFO> / <CONTROL_INFO><VPID>…<CATCD>CATCD maps to the CONTROL_DRUG_CATEGORY lookup already loaded (0001–0003 Schedules 1/2,/root/work/leftovers-20260818/ (with a README; moved, never deleted) — the tree is clean again; andcheck-backup-freshness.sh was rewritten (it had been reporting a permanent false CRITICAL against retired paths/var/backups/dailygzip -t integrity proof on wikijs_pg and gp_booking_pg, the volume archives with a size sanity check,veripath-ops d663cee, retired version kept beside it); the two volume-backup.sh no-ops are recorded asinfrastructure/central-db-password-rotation) — that one needs a changefrom-opencode no open notes; from-hermes keeps its older OPEN items, topped by theStockItem PIP → pack bridge, then the contractor's supplierUpdated 2026-09-27 (session 32 — the PACK LINK is BUILT and VERIFIED LIVE; no application code written by me).
Matthew's steer opened the session: continue from the resume note and process the handoff from OpenCode, which had
completed the pack-link brief I left. Everything in OpenCode's report reproduced on the app host, and I verified the
one criterion their verifier leaves out — the client screens — separately:
veripath-ops/scripts/verify-pack-link.py — 23 PASS / 0 FAIL on the app host (veripath-ops @fea4275); suites reference_data 182 OK (1 skipped), pharmacy_medicines 33 OK, combinedtenancy pharmacy pharmacy_dispensing pharmacy_medicines 176 OK.matched, all decided_by='rule', no empty vmpp_id,2026-09 sits imported and not live; the register readsconsent_required=True / consent_granted=False / may_publish=False (the gate is shut in the data, not just therebuild-pack-links at 04:30, and thetariff_news_pull and is guarded — a linking failure is logged and retried dailypharmacy-price-counts.py identical either side of theveripath-ops/scripts/verify-pack-link-live.py (e49051b) — 31 PASS / 0 FAIL, driving the realtc.tech.one ontest-client-pharmacy.pharmacy.veripath.co.uk): W3 renders 216 lines across 7 notices, every line naming its£0.00; the Aciclovir 400mg tablets 56 panel shows theissued 16/09/2026; a pack the month does not list (Anadin…/dojo/api/v2/findings/) — failing since DefectDojo moved to VPS1 on 2026-09-13, worth a look.no_line = "No price concession islinked; W3 untouched; I extendverify-pack-link-live.py for the new state) — see the top of from-hermes.from-hermes, to be built with the §9.1 panel-state split in one pass): The Derived Pricing — Phase 7.from-opencode has no open notes; from-hermes keeps 6 OPEN (newest: the §9.1 wording build).StockItem PIP → pack bridge, then the contractor's supplier price besideUpdated 2026-09-27 (session 31 — phase 6 v1 AUTHORISED, BUILT, VERIFIED CLEAN, and the PACK LINK authorised and
MEASURED; no application code written by me). Matthew's steer opened the session: the sourcing is complete; the purpose
is that a contractor compares their own imported supplier prices with our processed NHS figures; and the three
sources must be processed correctly first. I reviewed the research page he brought (Processing Imported Data)
against the live store and recorded five corrections as a dated review section on that page — the deduction is
three flat rates by category (20 / 5 / 9.85%), not a sliding scale by dispensing volume; the reimbursement level is
the VMPP, not the AMPP, and 94,862 of 187,156 packs have no price at all; the 152p SAF is per item and must
never sit inside a per-pack figure; no tunnel or price vector is needed (the app already reads the store
read-through); and dm+d carries no PIP codes, only GTIN, while StockItem has no dm+d link of any kind — so the
matching problem is one problem in two queues. Also recorded: the tariff ingestion does not parse Part II, so a
discount-not-deducted line would today be wrongly deducted (phase 7's correctness item). Phase 6 v1 was
authorised, specced and built by OpenCode, and I verified it live myself: verifier 47/47, release 2026-09 live
with 7 notices, 216/216 lines, 0/0/0 against the CPE store, cost 574,887 bytes / 0.21 s, 2 signal rows
with real Message-IDs, the CPE gate shut, the client-facing path carrying no price_concessions reference at all, and
health reading current. My first pass found three DEPLOY defects (the host not at a revision; the test files never
deployed, so it reported 136 not 163; the pharmacy suite red at 18 tests / 2 errors) and four smaller notes; all were
fixed the same day and I re-verified everything on the app host — feature/nhs-pricing-view @ e0d4f9a,
feature/pharmacy-price-concessions-view @ 3f45af7, both trees clean, bundles beside the deployment, suites
164 / 31 / 174 OK, verifier 47/47 with the tightened checks. The licensing blocker was removed by changing the
source, not the licence: the same 216 lines are published by NHSBSA/DHSC on the Drug Tariff updates page as dated
"Part VIIIA reimbursement prices" notices under OGL v3 (register consent_required false), and the mailbox is now
a CHANGE SIGNAL ONLY — envelope-level, Message-ID ledgered, no body parse, no .eml retained, nothing from CPE ever
client-facing (§19.2; cadence 7–9 notices a month, §19.1; costs measured, §19.3). The pack link is authorised,
measured and briefed: run over 68 months / 8,848 lines, normalised name + quantity and unit matches 98.8%
and every line of the last 16 months (2,626 lines) with 0 ambiguous and no fuzzy matching anywhere — which
corrects phase 5 §18 item 2 ("the real work"): it is a normalisation rule with an empty queue, and the ~110-line
historic residue is eight named patterns for an override table. Spec: Data Feeds — the Pack Link;
brief on top of from-hermes (6 OPEN); next: OpenCode builds it, then I verify live. Then phase 7 (the derived
pricing arithmetic) and phase 8 (the StockItem PIP → pack bridge, then the contractor's supplier price beside
the reimbursement). Two self-inflicted errors, both now rules in the veripath-build-verification skill: I ran the
pharmacy suites in the wrong container and briefly filed the import errors as a failure, and I overwrote the
from-opencode page with a stale /tmp copy (rebuilt from the render I had read — re-fetch a shared page immediately
before editing it).
Updated 2026-09-27 (session 30 — phase 5 of the pharmacy reference data is VERIFIED LIVE and CLOSED).
OpenCode's build 8abc4bd is deployed and verified live: the real mailbox was ingested — 5th update 182 = 182,
6th 216 = 216 — with release 2026-09 imported and never live; the live verifier is 37/37, the suite 136 OK
(1 skipped), dm+d and the Tariff untouched, and the same-day cross-check against CPE's own page matched exactly (216 rows,
0 missing, 0 extra, 0 prices differing). Record: phase 5 spec §17.
from-opencode is clear (the build note is HANDLED) and from-hermes carries no new build. A config finding worth
keeping: the app reads config/secrets/env.production, not .env, and the concession credentials now live there (file
tightened to 600). Matthew's steer (2026-09-27): the concession data is to be made available to clients, and he owns the
licensing tasks — so the next piece is spec §18 items 1–3 (the register record; the pack matching, which is the real
work because the rows carry no codes; and the client-facing surface), not yet authorised or briefed.
Updated 2026-09-26 (session 29 — the third and last NHS source planned, decided and handed over; no application code written).
Phase 5 — CPE price concessions — is authorised and briefed to OpenCode. Matthew set the route (the data arrives as an
email alert stream, with the values in the message body and no attachment) and then delegated the open calls ("just apply
what you think is best"), so D14–D16 are applied (and the third proved unnecessary): ingest and store centrally with platform-admin views
only, and a poll of the existing veripath.co.uk mailbox through a sender-and-subject-scoped, flag-free
search — the cold-start backfill was superseded because each alert carries the whole month. Plan: Price Concessions — Phase 5 Ingestion.
The build is two phases — a backend upload path that proves the parse (blocked by nothing) and then the poll — and the source's
consent gate stays closed: no request is being sent to CPE (Matthew's decision, 2026-09-26), so nothing concession-derived
reaches a client screen and the data is admin-side only. The acquisition is complete: the alerts already arrive in that mailbox, the credentials are in the app host's
environment, egress and login are proven, and the parse is validated against two real messages — nothing is left
for Matthew to provide.
Phase 4 stays live and verified (release 2026-09, verifiers 49/49 and 16/16), from-opencode is still clear, and
from-hermes now carries 6 notes with the phase 5 brief on top.
Updated 2026-09-26 (session 28 — a VPS1 capacity pass only; the app code and the pick-up queue below are unchanged
from session 27).
The Central Reference Data plan stays APPROVED with
phases 1 and 2 live (dm+d release 3170926, publication date 2026-09-21), plus this session's two stages: the
phase-2 handback nits redeployed (82871c4, verification recorded, one of the two nits was my misreport — it
was never a real gap), and Part 1 of Matthew's feed-visibility steer — the per-feed backend status view —
built, deployed and verified live (91b9399). The app host checkout is now 91b9399, the repo bind-mounted
at /app, so a restart picks up code; Forgejo is unreachable from the app host, so commits travel as a git
bundle.
Session 28 (2026-09-26) — the VPS1 capacity pass; no application code changed. Matthew reported that free space was
"beginning to diminish quite a bit each day". The drain was not the data: a kernel AppArmor audit flood from
Netdata's apps.plugin (~172 MB/day across syslog/kern.log/journal) plus a gp-booking-dev.service crash loop
(~123 MB/day). Both are stopped and verified by before/after counts (~295 MB/day → ~5 MB/day), 12 GB reclaimed
(149G → 137G used, 96G free), and primary-backup.sh now gzips every dump (≈4.4 GB/day → ≈1 GB/day, restore drill
passed). Full record: Disk Capacity Review — VPS1; captured as the
veripath-capacity-review skill. Two findings sit with Matthew — do not action either unilaterally: the 07:00
check-backup-freshness.sh CRITICAL is a false alarm, and a database password exposed on a wiki page is treated
as disclosed and still needs rotating.
Session 27 (2026-09-25) in brief. The TRUD delivery is audited and hash-verified — from the zip we have
everything there is (7 XMLs + the GTIN file, all matching MANIFEST.txt), and nine non-data documents in it are
simply not loaded. The reimbursement parameters (discount deduction scale, fees, appliance parts) are not in dm+d
and not on the Open Data Portal (re-checked live — §13), so they come from the monthly NHSBSA Drug Tariff
documents; the v9.2.0 release note is read (§12) and the unsurfaced invalid flag is recorded (§12.1). The next build
waits on one answer: manual monthly upload of the tariff, or fetch-and-parse. OpenCode has the note; both inboxes were
clear at session start.
Handoff state. Both inboxes are clear: the Part 2 build (27dc604) and the Part 1 wording nit
(45b8dd8) were deployed and verified live at the end of the session, with nothing handed back to OpenCode.
What is next. Part 2 is live, so the continuation is Matthew's modifications: SNOMED-code and barcode lookup
(the data is already loaded — DmdGtin, and pharmacy_stock.SnomedItemMapping for EPS codes) and the pg_trgm GIN
index on reference_data_dmdampp.name if the 186 ms search ever feels slow — that index belongs in the platform
app's migrations, never a pharmacy-side write to the store's tables, and the search already runs through both the apex
and client hosts with identical rows.
Pick up here tomorrow. Matthew asked for "a view so that the data can be viewed by clients" — that already
exists and was re-checked at the end of this session: test-client-pharmacy.pharmacy.veripath.co.uk/medicines/ is
public and login-gated, a client-tenant technician and a client-tenant business manager both get 200 with the label
and the 25-row search, and the dashboard they land on (/pharmacy/) carries a Product information link for every
pharmacy role. The open question is what he saw and what he wants beyond it — the likely answer being the natural
modification: the client's own stock list with the published pack price beside each item, rather than a standalone
lookup. Do not build a second client view until he answers.
Decisions recorded. D1 is answered — a read-only product-information view in the pharmacy application, v1 a
capped name search at /medicines/, built and live — and the §5.3 route for v1 is read-through, proven by a
live check: the pharmacy app's default connection is the platform database that holds the store. One human action still sits with Matthew: the TRUD account (item 24 subscribed,
licence accepted, key copied host-locally — there is no key to apply for: TRUD derives it from the account's
email + password). (Corrected 2026-09-26 after session 29: the CPE consent item recorded here as his to action is
not being pursued — he ruled that no request is sent, so the concession display gate stays closed and the data is
admin-side only. Phase 5 §15.)
New since session 27 (2026-09-26). The Drug Tariff documents are mapped — plan
§14 — and D11 is answered (Matthew, 2026-09-26):
fetch-and-parse, with v1 parsing the four parameters and publishing unattended behind a validation gate. The
spec is written — The Drug Tariff — Phase 4 Ingestion —
and the build is handed to OpenCode at the top of from-hermes (approved work; the only item still held there is the
2026-09-25 "keep every member of every delivery"). Two things to hold: §14's correction — Part V is not published
as a separate part, so the fetch is the full 6.25 MB edition, not "~300 KB of parts" — and the sequence: phase 4
lands and is verified live, then Matthew checks the data in the view, then CPE concessions is the last part. This
session also reorganised the project directory (23 pages into build-records/, operations/, research/; 2 archived)
and created the Data Refresh Schedule — both recorded
in the housekeeping log's Round 11.
Still owed by me, in this order: the walkthrough pages for pricing stages 1–4 (deferred, not cancelled), then
the C6 + Business-Manager guide wording, then the guide corrections listed in session 22. Housekeeping is parked
mid-phase: phase 4 (policies/compliance/security) is IN PROGRESS — section A done, sections B/C audited and waiting.
Facts to hold. Pricing is FINISHED and verified live (stages 1–4: 35/35, 24/24, 24/24, 49/49; engine dev =
9722205); stage 5 (retention/purge) waits on Matthew's Q2. The demo tenant holds one order per value state
(ORD-20260922-37CA4E, ORD-20260922-4E0448, ORD-20260921-D123C9, ORD-20260921-898C9C). A zero is never a
price. The held dm+d release is at /srv/datasets/dmd/2026-09-22 (on VPS1, not on the app host). The three
configured feeds read dm+d = Current, Drug Tariff and Price Concessions = Not automated yet.
Standing items still open from the 2026-09-14 list (full table on the archive page): VPS3's single vCPU (his call);
the GP/dental migration reconciliation (do it as a reviewed change, not a copy); the per-tenant tunnel watchdog gap for
the pharmacy client; off-host backup (deferred by decision).
OpenCode's build is verified live and the phase is closed. 8abc4bd was deployed to the app host from the bundle
(migration reference_data/0008 already applied by the entrypoint; no image build — the parser is stdlib only). The
real mailbox was ingested, unattended: reference_data_fetch --source price_concessions → "5th: applied 182 rows
(stated 182) | 6th: applied 216 rows (stated 216)", release 2026-09 imported and never live. The live verifier is
37/37 (veripath-ops/scripts/verify-concessions-live.py, committed 54cf2e1), the suite is 136 OK (1 skipped),
dm+d (3170926) and the Tariff (2026-09, four parameters) are untouched, the health row reads "Email alert stream /
Current", and the same-day cross-check against CPE's own page matched exactly — 216 rows, 0 missing, 0 extra, 0 prices
differing. from-opencode is now clear. Full record: spec §17.
One real finding on the way in — the configuration location. The application reads
config/secrets/env.production (env_file in the compose file); .env only feeds compose's ${…}
interpolation. The concession group had first been written to .env, where the container could not see it at all
(docker compose exec app env showed nothing). It now lives in env.production (dated backup kept, and the file
tightened from 644 to 600), the duplicate block was removed from .env, and all eight keys are confirmed visible
inside the running container. This will bite any future central-source credential, so it is recorded rather than
just fixed.
Two lessons from my own verifier, kept because they cost a run each. ReferenceConcessionUpdate keys on
ordinal_number/ordinal_text (not ordinal), ReferenceConcessionPrice on previous_pence (not
previous_price_pence), and concessions.effective_rows(release) returns rendered display strings, not pence —
parse both sides to the same unit before comparing. Two first-run "failures" were mine, not the build's, and the
record says so.
Matthew's steer (2026-09-27): the concession data is to be made available to clients, and he owns the licensing
tasks. So the direction is client availability — and the only thing that opens the code gate is the basis being
recorded on the licence register (consent_granted with its note and date, or consent_required=False with a reason);
until then the display stays shut by design, so nothing leaks by accident. Spec §18 sets out what that phase
needs, in dependency order: (1) the register record; (2) the pack matching — the real work, because the rows carry
no code of any kind; (3) the client-facing surface (this month's concession price beside the contractor's pack, with
the zero-discount note and the attribution, verified on a client tenant host); (4) the guardrail coupling; and
(5) month supersession and rollover on the screen. Recommendation on the page: spec items 1–3 together — the
display is small, the matching is the substance, and they are useless apart. Not authorised and not briefed yet.
Housekeeping still parked mid-phase (phase 4, sections B/C audited and waiting), and the two phase-4 platform
defects remain open in from-hermes — neither touched by this build, both awaiting Matthew's call.
The destination, in his words (2026-09-27) — the reason all of this exists. The concession figures must work with
the TRUD data and the reimbursement data, and the combined pricing is what a client compares against their own
supplier pricing imported into the app. What is done so far is obtaining the sources; next is to see this data
and ensure it is processed accordingly, and there is depth and breadth still to come. That is now recorded on
the plan page as phases 6–8: 6 — see the data (dm+d price, reimbursement position and the concession override
side by side per pack, once the packs are matched), 7 — process it (the derived-pricing/guardrail calculation, with
its own accuracy argument and the D9 cross-check against the Open Data Portal), 8 — the client comparison (their
purchase price beside the derived reimbursement, every figure labelled with its source and release). The plan page
also carries his steer as the framing for §6, and D10 there is marked superseded twice.
Trigger. Matthew named the session's scope himself: the third and last of the NHS sources — CPE price
concessions — and the route is settled by him: the data now arrives by email alert, with the values in the body and
no attachment, so those emails become the source. His words: "we need to plan how we will do this." Nothing was built,
briefed or changed in code at that point. He then delegated the calls ("I'm not sure how to make a decision with
what you're saying — just apply what you think is best"), so in the same session D14–D16 were applied, the operating
schedule corrected, and the build briefed to OpenCode. No application code was written — the deliverables are the
spec and the brief.
The plan. Price Concessions — Phase 5 Ingestion (Build Spec)
— 15 sections in the phase spec pattern: why it exists, scope in/out, the email route and mailbox (with the
acquisition runbook), what the messages
contain, the parse anchors and traps, the storage model, the routine and gate, health for a stream, the views,
acceptance criteria, tests, deployment, the decisions, what is deliberately left open, and §15 — why no request is being sent to CPE, and what that closes. It is headed DECIDED AND AUTHORISED FOR BUILD.
What was measured live on the day — the design rests on this, not on assumption. CPE's hub page carries the
cumulative month tables (September 2026: 216 lines, matching its own sentence "…to 216"; August: 243; rolled
over: 9), while an update post carries only that update's lines: the 1st update of September (16/09) carried
67 new lines + 9 rolled over = 76, exactly its own stated total; the 6th (25/09) carried 36. The August
final update (04/09) had been corrected after publication — "the table below has been updated with an additional
line in red". Therefore an email is a delta and the month is the union of its updates in order, later overriding
earlier — which is also how "improved price for ramipril 10mg capsules" and rolled-over prices "adjusted upwards"
happen inside one month. The rows carry no code at all (drug text, bare pack size, £ price), so matching to dm+d
packs is explicitly not in this phase.
The two structural calls. (1) One release per month, with each email an update appended to it — a
release-per-email design would be destroyed by the purge rule already in force, which keeps only the live and previous
release and would delete the current month's earlier updates. (2) The gate is arithmetic: the message's own
stated total must equal the distinct (drug, pack size) lines held for the month after applying — the 76 = 67 + 9
case, measured. That single check is what makes an amendment, a re-send or a bad parse visible instead of silent. The
cold-start case (subscribing mid-month) needs one recorded backfill of the month's cumulative table from CPE's
page — D16, recommended, because without it either the strongest control is weakened on day one or the first month
cannot be reconciled.
Verified in the repository, not assumed. The price_concessions source row already exists —
consent_required=True, the CPE licence text ("copying is authorised for personal use only … prior consent"),
routine=NONE, zero releases. What is reusable: the release store, the verify/import/publish flow, retention and
rollback, ReferenceFetchAttempt, the health surface, the licence snapshot, the consent gate (may_publish), and the
beat schedule. What is new: a routine choice cpe_email, two tables (update, price), two attempt statuses
(no_message, ignored), a 15-minute poll task, mailbox configuration in the app host's .env, and the
stream-aware health rules (the monthly "overdue after the 5th" rule is wrong for a source that starts publishing
mid-month: September's first update was the 16th).
Two reuse traps named in the plan. The platform does already poll IMAP
(lab_documents/imap_ingestion.py, PracticeMailbox, every 5 minutes) — reuse the pattern, not the code: it is
clinic-scoped (its output is a patient's lab document) and its 5-minute task still fails on every run (the
PracticeMailbox NameError sitting with OpenCode). And the parse uses the standard library only — there is no
bs4 and no lxml in the app image — so phase 5 should need no image build, unlike phase 4's pypdf.
The acquisition plan. The route needs no new provider: veripath.co.uk mail is hosted for us and the
platform host already runs a webmail client against that provider's IMAP endpoint, so a dedicated mailbox on
the same service is polled by the platform app. The third-party inbound-parse webhook
(SendGrid/Mailgun/Postmark) and a public ingest endpoint — both proposed on the orphan cpe_concessions note —
are rejected: a processor and an inbound surface for a sovereign-hosted DSPT Cat 3 platform, for data that
already lands in our own mailbox. §3 now carries a step/owner/proof runbook, the Phase A → Phase B
sequencing (the parse is proven first through a manual upload path — which doubles as the fallback when the
mailbox is unreachable — so the build is not blocked behind the subscription), and the failure modes: mailbox
password rotated, quota full, a sender change, late or out-of-order delivery (ordering by the confirmed
date and ordinal, never arrival), re-sends, and a read-only poll that marks seen only after storage.
Then the acquisition was finished off — the mailbox already existed and was found (2026-09-26, same session).
Matthew asked me to "perform whatever you're recommending", so rather than wait: the webmail container's
managesieve plugin was disabled (the mail host runs no Sieve server — ports 4190/2000 closed, IMAP open — so its
Filters screen could only fail; config backup kept), and a root-only drop directory was made for messages. Then the
decisive check: the veripath.co.uk system mailbox that the host's alerting already uses is receiving CPE's alerts
— the 5th and 6th September 2026 price-concession updates — so no new mailbox was needed at all, and the empty
folder Matthew created was deleted (the poll needs no folder). Egress, login and the sender+subject-scoped search
were proven from the app host, and the REFERENCE_DATA_CONCESSIONS_IMAP_* group is now in the app host's .env
(values over stdin, never printed). The parse was validated against both real messages and corrected three
assumptions: the HTML is a Mailchimp wrapper (11 tables) so the parse reads the text/plain part; the subject
is Q-encoded; and the message is not a delta — it carries the whole month (a block per earlier update plus
rolled-over), so D16's backfill is superseded. The arithmetic reconciled exactly: 182 = its own stated 182, and
216 = its stated 216, with 216 distinct lines across both. Two row shapes only the real thing showed: a fractional
pack size (2.5 ml) and an amended price (£92.86 (previously £85.45) — the rolled-over block shrank 9 → 7 for
exactly that reason). A hazard was found and closed: the alerts had already been read in webmail, so an
UNSEEN-only poll would have found nothing — the poll now ignores \Seen, re-reads its scoped search each run,
de-duplicates on Message-ID and body hash, and never writes a flag. A reference parser is committed at
veripath-ops/scripts/cpe-concession-email-parse.py. One correction owed and made: my earlier claim that no
"previously" marker exists in the real data was wrong — it is in the alert — and the CPE Concessions page now says
so.
Corrections applied in the same session — no longer owed. data-refresh-schedule was stale in six places: the
status block and the tariff row still read "not built / D11 undecided", §3 step 6 listed D11 as open, §4 described the
concessions route as "manual/parse", §6 still carried the D11 upload option as undecided, §7's task table lacked both
the 03:45 tariff task and the concessions poll, and §8 still listed "a concession model" as missing. All corrected to
the built/authorised state in one pass, with the phase 5 spec linked. OpenCode was briefed (from-hermes, the newest
note): the two-phase build, the measured facts not to re-derive, what is not authorised, and the bundle handoff.
Still owed by me: the walkthrough pages for pricing stages 1–4, the C6 + Business-Manager guide wording, and the
session-22 guide corrections. Housekeeping stays parked mid-phase (phase 4, sections B/C audited and waiting).
Trigger and result. Matthew: free space "beginning to diminish quite a bit each day", capacity otherwise fine.
Starting point 149G used of 232G (65%), 83G free; after the pass 137G used, 96G free (59%) — 12 GB reclaimed,
and before that the growth was ~295 MB/day, now ~5 MB/day.
Three suspects were conflated; only one was a leak. (1) A kernel AppArmor audit flood — one record, written
continuously and indefinitely: apps.plugin inside the Netdata container attempting a ptrace that the
docker-default profile denies (991,130 denial lines / ~1.05 GB in one rotation; baseline 158,400 lines and 172 MB/day
into syslog, the same records again in kern.log and the journal). Netdata's apps.plugin is now disabled
(4f50d20). (2) A gp-booking-dev.service crash loop — 1,095 lines/90 s; disabled (d47e648), now 13 lines/90 s.
(3) The journal had no size cap (systemd default 10% of the filesystem, i.e. free to grow towards ~23G) — now capped,
4.0G → 924 MB.
The backup change — read this before touching backups or the restore docs. primary-backup.sh now streams every
dump through gzip -6 at nice -n 10: veripath-ops 4eb099b, deployed to /opt/scripts/primary-backup.sh. All
dumps are <name>_<ts>.sql.gz — the old restore path is gone, so anything reading *.sql must zcat first.
Footprint ≈4.4 GB/day → ≈1.0 GB/day (7-day window ≈33 GB → ≈7 GB, measured). Each dump streams into a .tmp,
gzip -t verifies it, and only then is it moved into place; set -o pipefail was added because a failing dump piping
into a successful gzip used to log OK. Verified: gzip -t clean on all five streams, the CREATE DATABASE set
identical to the prior uncompressed dump, and a full restore drill passed (548 pages vs 548 in production, scratch
database dropped). infrastructure/backups/overview and infrastructure/backups/quarterly-restore-drill were updated
in the same change — the latter was badly stale (a /root/backup.sh that does not exist, the pre-rename container name)
and carried a plaintext password.
Two findings are with Matthew; neither is mine to fix unasked (both in from-hermes, 2026-09-26): the 07:00
check-backup-freshness.sh CRITICAL every morning is false (it asserts _daily archives under /backups/wiki/ are
under 36 h old — the newest is months old — and looks for dated directories under /root/backups/ that no longer exist;
volume-backup.sh's wiki_content_*.tar.gz is a 125-byte empty-volume no-op), and a plaintext central-database
password was on a live wiki page — redacted and verified against the rendered page, but the value still needs
rotating (infrastructure/central-db-password-rotation); it is the third instance of the class and is why the
housekeeping log's "secrets sweep needed" item should be a sweep, not page-by-page fixes.
Left open deliberately. ~37 GB of larger one-off reclaimables were identified and not touched; disk alerting
already exists (platform-vps-health.sh posts disk_usage_pct every 5 minutes) and was not touched.
How it started. Matthew asked for the pharmacy app's TRUD/PIP health, then whether the reimbursement data was in the
zip the release was imported from. It was not — and proving it took the session. Everything is on the plan page: §11
(the archive audit + its verification record), §12 (the v9.2.0 release communications), §13 (the Open Data Portal
re-check).
The audit (measured, on his machine). His original download ~/Desktop/nhsbsa_dmd_9.2.0_20260921000001.zip —
16,880,148 bytes, 16 members — holds the 7 core XML files, 7 XSD schemas, docs/dmd_extract_comms_9_2_0.pdf,
and the GTIN data as a nested zip (week392026-r2_3-GTIN.zip, 4 members). The copy on the app host kept the GTIN
expanded and dropped the XSDs and the PDF — that is the whole of the 523,476-byte difference. Every loaded member is
hash-verified against his download: the seven XMLs match MANIFEST.txt exactly, and the GTIN XML is caaa16d8…. The odd
f_gtin2_0170926.xml name is TRUD's own — the filename item I had handed OpenCode was withdrawn as a false defect
(same lesson as the phase-2 nit: check before you report).
The reimbursement question: no, and it is two gaps, not one. dm+d gives the prices — its own Drug Tariff block
(DTINFO, 8,686 rows: Cat A 489 priced, Cat C 2,432, Cat M 624, Cat H 19, VIIIB 474, VIIID 55, Part IXa only 337 of 4,354,
IXb/IXc/IXr unpriced, Part X stale) — but it carries no discount deduction scale and no fee of any kind (searched the
whole delivery, member by member). Those, and the appliance parts, exist only in the monthly NHSBSA Drug Tariff
documents (main parts PDF, Part IX also XLSX, no account or key needed). The only reimbursement pricing in the app today
is synthetic: compute_expected_reimbursement prices an endorsed claim from an ERPNext price list seeded by
provision_pharmacy_reimbursement out of pharmacy_accounting/demo.py ("Nothing here is real, and none of it is a tariff
figure") plus a configurable professional_fee_per_item. And there is nothing in the app for concessions at all — no
model, no table, no screen, no import (checked: zero tables, zero references).
The Open Data Portal — Matthew's steer, checked live (§13). Half right: the ODP answers anonymously (no key
needed — verified by fetching records), but it holds no tariff: 162 datasets match "drug tariff" and 164 match
"tariff", of which seven are not FOI responses and all seven are prescribing/activity/spend (EPD with SNOMED, PCA,
contractor dispensing data, hospital prescribing, FP57 refunds, flu service). Recorded so it is not re-checked — and
noted as a future accuracy cross-check for the guardrail (actual spend per drug; the zero-discount/standard-rate
split), never as the source of its prices.
The v9.2.0 release note (§12) — forward look worth having. SNOMED CT UK Edition 43.0.0 due 30 September 2026 raises
the maximum FSN/Synonym length 255 → 4,096 (snomed_descriptions.term is text, so storage is safe; the risk is a
255-character assumption in code or forms, and our SNOMED load is manual so it needs a refresh plan, not a re-run);
43.1.0 on 28 October 2026 will carry dm+d GTINs inside SNOMED (a second route to data we already hold); Trade
Family / Trade Family Group now live in dm+d's supplementary extracts, which we do not load. The note also confirms
the reimbursement conclusion a third time — no tariff, no deduction, no fee wording in it.
One finding of my own (§12.1). The release's invalid flag is loaded and nothing reads it: 4,200 invalid AMPPs
of 187,156 in the live release (AMP 3,285, VMPP 1,161, VMP 620, VTM 106), and pharmacy_medicines/catalogue.py never
references it — so a withdrawn pack can be searched, opened and shown with its price and GTINs, unmarked. Not a defect
against the spec; a display decision for Matthew (flag, hide, or leave). OpenCode has been told explicitly not to
change it unprompted.
Handed to OpenCode (agents/handoff/from-hermes, note 2026-09-25, five items): keep the nine unread delivery
documents with the release; phase 4 is the document route, nothing from TRUD; do not build phase 4 until D11;
the SNOMED/GTIN forward look; the invalid-flag finding.
OPEN DECISIONS for the morning. (1) The tariff intake route — Matthew uploads the month's files by hand, or
OpenCode builds fetch-and-parse. This is the question he was asked and has not yet answered, and the next build waits on
it. (2) Whether the client view flags or hides invalid packs (§12.1). (3) Still unanswered from session 26: what
he wants beyond /medicines/. Human actions unchanged: the TRUD account (D12 — the 03:15 pull still aborts
not_configured daily) and CPE consent (D10).
1. Cleared the two phase-2 handback nits first — and corrected one of my own claims. 82871c4 redeployed to the app
host by bundle (2301095 → 82871c4), containers restarted, app healthy. Nit 1 (the publication date) was proved on
the real release: in the deployed container derive_publication_date on TRUD's own archive name returns
2026-09-21 — the date I had set by hand — an ISO name returns 2026-09-22, a name with no date returns None (the
warn branch), and the deployed tree's own suite re-ran 61 tests OK. Nit 2 was my misreport: the attempt row was
already recorded pre-fix by fetch_latest_release's blanket finally save (live rows at 15:30 pre-fix and 15:48
post-fix, identical in shape), so the fix changed nothing observable. The correction is on the plan page, in both inboxes
and here — a false defect left in an audit trail is worse than the wasted effort.
2. Part 1 — the backend feed-status view: built by OpenCode (91b9399), deployed and verified live by me. Spec:
Data Feeds — Backend Status View. Matthew's steer was "as
basic as it can be", so it is read-only with no new controls. Verified: 0005 backfills routine correctly without
re-running the seed (dmd = trud_pull, the other two none); no credential is reported against a feed that uses
none (key_status() returns None, the key column reads "not used"); states read Current (dm+d, release 3170926,
age 2 days, next expected 28/09/2026) and Not automated yet (both monthly feeds, level secondary); the health page
now says "No action needed." where it previously raised two false alerts; the landing page carries "All configured
feeds are current" plus a per-feed card; the flips work (feed_state(dmd, today=2026-10-30) → Not running — no
key); all four screens stay 403 anonymously. One wording nit handed back: every card reads "Last successful
fetch: never" — meaningless for the two unautomated feeds, and true but odd for dm+d (its release was imported by hand).
3. Part 2 spec'd — the pharmacy-side view. Data Feeds — Pharmacy View:
D1 answered (a read-only product-information view in the pharmacy application) and §5.3's route settled as
read-through for v1, because the finding killed the only objection — the pharmacy app's default connection is the
platform database that holds the store, verified from inside pharmacy_app (16 reference_data tables, the live
release, 167,307 AMP / 187,156 AMPP rows, prices 92,294 real / 94,862 none / 0 zeros), so no new credential,
no new path and no copy. v1 = a new /medicines/ entry, one capped name search, pack detail showing the price or "no
published price", the §5.6 label on every page generated from the release record, no export, no write path. Build
note is OPEN in from-hermes; the plan's §5.3 leaning is updated in place, dated.
4. Tooling fixed. wiki-strip.py (the helper that strips the --get wrapper) had a real defect: it took the body as
the text between the first two rule lines, so any page whose body starts with a rule line was truncated to 1
character — which is why an earlier attempt to patch this very resume note produced an empty file. Rewritten to take
the body from the header's rule to the last rule before the Page: footer, with a length check against the count the
tool reports and a warning if a wrapper line leaks through. Verified clean on five pages of different shapes.
5. Part 2 — the pharmacy-side view: also built, deployed and verified live the same night (27dc604). OpenCode's
pharmacy_medicines app landed on pharmacy_app (9722205 → 27dc604) by bundle, 0001_initial applied on the
default and the pharmacy tenant, app healthy. All six spec §5 criteria verified live: anonymous
/medicines/ → 302 on both the apex and the client host, signed in → 200, the release label on list/detail/about;
a priced pack renders £44.20, an unpriced one "no published price", with £0.00 nowhere; every row came from
the live release and an out-of-release pack pk 404s; the search returns the 25-row cap out of 614 matches in
186 ms (317 ms end-to-end), the SQL carrying the release filter and LIMIT 26; no write path in the module; and the
same 25 rows for an apex user and a client-tenant user. Module tests re-run by me: 18 OK, and the full pharmacy
suite: 461 tests OK (195 s). No index added —
186 ms is comfortable, so pg_trgm stays the first modification, in the platform app's migrations. Nothing handed back
to OpenCode; both inboxes are clear.
6. Two page artefacts cleaned in passing. The from-opencode and from-hermes inboxes carried baked-in wrapper
footers (a stray rule line and a second Page: line) from earlier upserts — the hazard the housekeeping log has
recorded before. Both were removed on this session's rewrites rather than copied forward; the helper now warns when a
page carries one (it is what the length check caught).
7. Client-side availability — checked, and already in place; nothing built. Matthew's closing ask ("create a view so
that the data can be viewed by clients") is answered by Part 2 itself: /medicines/ on
test-client-pharmacy.pharmacy.veripath.co.uk is public and login-gated (302 → OIDC), and in-app checks as
tc.tech.one and tc.bm.one (org test-client-pharmacy) return 200 with the label and the 25-row search, with
the Product information link on the dashboard they land on. Rows identical to the apex. A second view would have been
duplicate work. Open for the next session: what Matthew saw and what he wants beyond it — most likely the client's
own stock list with the published pack price beside each item. The one check I could not make is a real browser
login as a client (no password), which is his to do.
1. Both branches landed in order, each verified on its own. 438e565 (phase 1) first, then 2301095 (phase 2), on
the app host. Forgejo is not reachable from the app host, so the commits travelled as a git bundle; the host
tree is now a real checkout, with the old tree backed up at
/root/gp_booking_app-pre-refdata-20260923-1516.tar.gz and config/secrets untouched. Correction — I had told
OpenCode otherwise, and both the plan page and the note now say so: no image rebuild is needed. The repo is
bind-mounted at /app (as are media and staticfiles), so a restart picks up a code change; the image still
carries a build-time copy, which is why an old image holds pre-redaction secrets — a rotation item to close at the
next proper deploy.
2. The env values the redaction fix needs are in place, host-locally — CHANNEL_LAYER_URL and
NHS_NOTIFY_API_KEY — taken from what production was already running, with the Redis password cross-checked against
the deployed channel-layer URL. Values never printed, never committed.
3. Phase 1 — live evidence. Imported through the container's own command, the same lifecycle the upload screen
drives: "Verified 8 files, 238,847,891 bytes, release 3170926" → "752,256 rows across 8 files" → live.
reference_data_check → "All checks passed." — integrity re-proved against the manifest, 10 closure checks 0
dangling, 0 packs with no price row, 0 prices stored as zero — with every count matching the figures I derived
independently from the files. A second import is a no-op; the audit trail carries imported + published and an
OGL licence snapshot. The consent gate was exercised live: publishing price_concessions raised "Price
Concessions cannot be published until its consent condition is recorded" inside a transaction that rolled back, so
nothing persisted. Anonymous /infrastructure/reference-data/ and …/upload/ → 403.
4. Phase 2 — live evidence. 0004 applied. The health surface reports release 3170926, age 2 days, next
expected 2026-09-28, overdue False, key not_configured and the alert "No API key configured; the scheduled pull
is idle"; a fetch with no key refuses cleanly ("TRUD_API_KEY is not set", exit 0); reference_data_purge →
"0 release(s) purged; manifests and hashes retained."; beat entries registered (pull 03:15, health 06:00, purge
weekly). The real TRUD download is the one thing I could not verify — it needs the account (D12), so OpenCode's
question about the live archive's MANIFEST.txt stays open; the held release does carry one and all 8 files
verified against it.
5. Two nits handed back (neither blocking): the CLI import leaves publication_date null, which blanked the
freshness figures until I set it — I used 2026-09-21, the date in TRUD's own archive name
(nhsbsa_dmd_9.2.0_20260921000001.zip), recording the content's 17/09 generation date as the other candidate rather
than choosing silently; and an unconfigured fetch writes no ReferenceFetchAttempt row, so the attempt log is
empty for that case (the health surface still reports it).
6. Published and tidied. The plan page's §9/§10 carry both verification records and the bind-mount correction;
from-hermes carries the same to OpenCode; OpenCode's two central-reference-data notes are marked HANDLED; the app
host's temporary files are deleted and the pre-change backup kept. Containers healthy throughout (gp_booking_app Up
(healthy), both Celery containers up).
1. dm+d completeness — proven, not asserted. The release Matthew fetched by hand on 22/09 (TRUD release 3170926,
8 files, 228 MB at /srv/datasets/dmd/2026-09-22) is internally complete and intact: all 8 files match the
manifest's SHA-256 and byte size, and over the whole release every parent link resolves with 0 dangling (pack →
product 167,307; pack → pack-structure 37,394; product → virtual product 24,662; product-version → generic 24,662;
GTIN → pack 82,316; ingredient → substance 3,506). Counts: 3,250 VTM / 24,662 VMP / 37,397 VMPP / 167,307 AMP /
187,156 packs / 4,532 ingredients. Nothing reads it: no importer exists anywhere, so "we imported TRUD" was really
"we hold the release". Price reality: only 92,294 of 187,156 packs carry a real price (85,825 "No Price
Available", 9,029 "priced when manufactured", 5 centrally funded); three rows claim an indicative price with a value
of 0 — a zero is never a price. GTIN coverage is partial (82,316 packs, 104,840 without). The 4,200 unnamed packs are
exactly the rows dm+d flags INVALID.
2. The plan page — written today, approved today. Central Reference Data:
the capability (platform-level, pharmacy first consumer), the SIAAS boundary argument and the proposed carve-out for
licensed national reference data, the SNOMED precedent, the per-source routes (§5.4), the labelling requirement as a
licence condition (§5.6), what each source still needs (§5.8), the research page's errors (§5.7/§5.9), the
reconciliation (§5.10), the licence register (§5.11), the backend management view (§5.12, inside /infrastructure/
behind PlatformAdminRequiredMixin), the approved phases (§6) and the decisions (§7).
3. What the research got wrong, and how it was settled. nhsbsa.net has no DNS record; the Open Data Portal
(which is real CKAN — datastore_search proved it: 17,338,724 rows in EPD_SNOMED_202011, fields BNF_CODE,
NIC, ACTUAL_COST) carries no Drug Tariff, no concessions and no dm+d (2,147 datasets, none matching tariff,
concession, discount, dmd or drug); the phase 4 page's Trud blueprint does not match the real file (prices sit
in a sibling MEDICINAL_PRODUCT_PRICE/PRICE_INFO section keyed by pack id, basis element PRICE_BASISCD, real
filenames f_ampp2_<release>.xml). Gemini retracted every substantive claim when challenged (§5.10) — but its
evidence columns were unusable (it reported GET https://nhsbsa.net → HTTP 200 OK for a host that does not resolve).
Matthew's page and the phase 4 page now carry dated corrections.
4. Licences — settled, and better than assumed. dm+d = Open Government Licence for TRUD (an open TRUD account,
errors reported to the provider, a record of use kept). Drug Tariff = NHSBSA copyright, reusable under OGL v3 with
their prescribed statement: "[NHSBSA Title of Information], NHSBSA Copyright [year]". Price concessions = PSNC/CPE
copyright, personal use only, no re-publication without prior consent — so CPE consent is required before
concessions are shown to clients (D10). Part VIIIA Cat A/M/C is PDF only (Part IX also XLSX), so v1 draws tariff
prices from dm+d's own Drug Tariff block (4,444 priced rows) for the packs a pharmacy actually stocks.
5. Method lesson, recorded because it cost real time. Reading the 120 MB AMPP file into Python and running regexes
over it took minutes per pass and one run was killed on this 4-core box, which is also serving production. The same
proof with grep/sort/comm over extracted ids takes 25 seconds. Use the cheap method.
1. Stage 4 deployed and verified live — the last buildable stage of the pricing plan. OpenCode's note
(9722205) was built but not deployed. I pushed feature/pharmacy-order-value, fast-forwarded the engine's dev,
applied pharmacy_stock.0007 on the apex (default + sector_pharmacy) and the client tenant
(migrate_client --slug test-client-pharmacy --app-label pharmacy_stock), and restarted the containers — the repo
is bind-mounted, so compose up -d alone leaves the old code serving. 443 tests re-run by me (my own run, never
the reported count). New verifier /root/work/veripath-ops/scripts/verify-pharmacy-order-value.py — 49/49, its
--reads-only mode through the boundary wrapper showing audit/files/rows/cache unchanged, and the regressions
re-run on the final price state: stage 1 35/35, stage 2 24/24, stage 3 24/24.
2. What the live run proved. All three value states on the build screen at once — priced with no minimum
(silent), "value not known, so it is not checked", and "£25.28 of a £150.00 minimum — £124.72 short. Advisory only;
the order can still be created." The short draft was created anyway at the submitted quantity with no line added or
changed; a pre-stage-3 order reads "Value not known" on list and detail (never £0.00); a partly priced order
reads £30.40 (partial — 1 line(s) without a price) and is never compared to the minimum; the export of a valued
order is still ProductCode,Quantity and value-free; the Counter Assistant is refused the build screen, the orders
list and an order page. Record:
supply-orders-pricing-phase4-value.
3. Two demo-data gaps found and closed by hand (now an ask to OpenCode). The short advisory had no live
state: only Synthetic Wholesaler (DEMO) carries a minimum (£150) and its single line has no price, so the screen
could only ever show unknown. I set £150 on Synthetic Wholesaler Two (DEMO) through the supplier screen so the
state is walkable; the partial state only appears when two rows land in one supplier group. Both belong in
seed_pharmacy_stock so a re-seed keeps them — that is the OPEN from-hermes note.
4. One checker fixed, not the app. Stage 1's verifier still asserted "the word 'price' nowhere on the build
screen" — a boundary stage 2 deliberately reversed. It now asserts the build screen offers no route to the price
import, and the change is recorded on the phase 4 page rather than made quietly. Also flagged: 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. Committed to veripath-ops (17ba642); the lesson is in the build-verification skill.
5. Tenant tidied (never delete) and the pages updated. The run cancelled its own drafts with reasons, leaving
one example per value state (below). W4's answer (the margin column) was moved onto the phase 4 page — it is
answered and deliberately not built, because the only reimbursement figures in the system are synthetic demo prices
against unlinked items. The plan page now reads stages 1–4 live; the housekeeping log carries Round 9; the
skill mirror was re-synced.
6. Tomorrow (2026-09-23), in order. The walkthrough pages (Matthew walking stages 1–4 with me, captured with
real figures), then the C6 + Business-Manager guide wording for the value/advisory copy, then the owed guide
corrections below. No coding unless asked — the plan's only remaining code is stage 5 (retention/purge), which
waits on Matthew's Q2 answer and blocks nothing.
1. Guides published, one at a time, each verified against the running app. C3 Run the Expiry
Check — captured live as tc.ca.one, the Pharmacist's table
byte-identical; the report is the expired and short-dated list, earliest first, bands 90/30 and not printed on the
page. C4 Record an Adjustment or Wastage — RP/Technician
only (BM and CA both 403), reason required, whole packs only, and the unsafe retry written in as a warning.
C5 Sync Stock Positions — the refresh is POST-only,
manual, nothing schedules it, and it never removes a position. C6 Order Stock and Export the Order
File — the new ordering cycle, three live
figures, with the real exported file quoted verbatim (ProductCode,Quantity / 2345678,32 / 0123456,21 /
DEMO-BARCODE-0003,5).
2. Defects found while writing, and fixed by OpenCode the same day. Findings 9–10 (the expiry box read
slashed dates the American way, and a refused date came back with no message at all) → ad83f9b; the supplier
screen → 9bfb19c; cache pruning on refresh → 508aea3; then supply ordering W1–W9 → 38e2271, migrations
applied to the apex and test-client-pharmacy, demo tenant re-seeded.
3. Supply ordering verified live by me (not taken on trust): the exported file byte-matched the profile and
re-exported identically; stock movements 11 before and 11 after, the position cache unchanged at 6 rows /
180.00, ERPNext's newest Stock Entry still 2026-09-19 (nothing posted); ORDER_CREATED + two ORDER_EXPORTED audit
rows; the Counter Assistant 403 on all four order surfaces; a fractional quantity refused at build time with no order
created.
4. Dataset and licence rulings — recorded on supply_orders
(§ Datasets, licences…). dm+d: acquire centrally via TRUD (which is the licensing mechanism, not bare open
access), weekly XML subpack that includes the AMPP→GTIN mapping — so it fills the barcode/clinical half, never
PIP. PIP codes: published under C+D Data (not IQVIA as a research note claimed — unverified); importing a
pharmacy's own item file into their own tenant is legitimate, seeding a shared platform dictionary from a
pharmacy's dump is not (licence breach); a vendor-level dictionary needs its own licence. Wholesaler credentials
and SFTP price-list pulls: refused, per the settled spec rule that the app never holds a wholesaler password.
5. ⚠️ OWED — my next work, in this order. (a) C1 gains the Against order step (C6 references it);
(b) C1 + C4 drop the "type the date year-first" workaround — the field now takes UK dates with a picker;
(c) C3 + C5 drop the stale-position caveats — the cache is pruned on a successful refresh; (d) C2's one-line
correction — the low-stock list is not in the top menu, the stock screens hang off the overview's button row;
(e) C7 — the supplier-list guide (/stock/suppliers/ list/add/edit/deactivate, item already on the Coverage
Plan); (f) optional: the "what the code is (and isn't)" note added to C6.
6. Handoff state. from-opencode: no open notes (both build reports archived). from-hermes: one OPEN
note — the verification, and the nit that the build screen's copy says "edit or drop any line" while only the
draft's edit screen can drop one.
7. Housekeeping done this session: both inboxes archived to agents/handoff/archive/2026-09/* and re-verified;
those archive pages are now ~200K chars. Next housekeeping pass: prune this resume page (58K) the way the
housekeeping log was pruned — sessions ≤ 20 are superseded.
1. A negative action was opened and is the frame for everything below. The endorsement reference proved to be a
product invention with no NHSBSA counterpart; Matthew called it a negative action and the standing rule is now in
writing on the audit page: a guide may name a regulatory artefact only when the app implements it and it is
traceable to an authoritative source, with the source and version recorded. Register:
Regulatory Content Audit — Findings 1–7, each
with evidence, root cause and corrective action.
2. Fixed and deployed 2026-09-19 (verified by me on the running app, not taken on trust): Finding 1 (endorsement
reference removed; receipts match on the EPS token reference; the column kept for historical rows); Finding 2 (the
dispense screen now offers the NHSBSA EPS exemption/charge-paid codes — 16 valid, obsolete retained but not
offered, charge_due derived); the goods-in retry. Documentation done: B4 and B5 corrected, all four figures
re-shot and byte-verified, Coverage Plan and index updated.
3. Parked — his call 2026-09-19: "I cannot answer the four rulings until we get to section F." Findings 3
(the RP record: SI 2008/2789 reg 5 requires absence particulars and five-year preservation; the app has neither, and
"A pharmacy cannot lawfully operate without an RP on duty" overstates the law — reg 3 allows two hours), 4 (the
EPS vocabulary collision — our "claim" is EPS release/download, our "release" is EPS return; and what the local
stop is), 5 (no EPS dispense notification; the dispensary queue's ambiguous Claimed column), 6 (no way to
record a problem with a delivery) and 7 (no way to add a supplier). Do not raise them again until group F.
4. Group C is underway. C1 published (goods-in: the form, one line per batch, posting to ERPNext, the
history, the retry), C2 published (stock overview + low stock; the figures are cached positions, not the
ledger). Both written on the authority — GPhC standards 1.6/4.2/4.3/4.4 and the professional guidance on the safe
and secure handling of medicines 3.11–3.14, 3.38, 3.44. C3 (expiry check) is next; C4 (adjustment) is BLOCKED
— OpenCode confirmed services.adjust() has no idempotency guard, so "try again" can double-post; C5 (sync).
5. Fixtures (client tenant test-client-pharmacy). 3 synthetic suppliers (seeded this session, all active);
one receipt DEMO-GIN-0001 (Submitted, 4 lines); tokens: Alfa AVAILABLE (released during the B6 walk — the
released-token wrinkle stands: it shows as claimable but cannot be re-claimed here), Bravo CLAIMED, Charlie ASSEMBLED,
Delta and Mike CHECKED, Echo DISPENSED, Golf CANCELLED, November AVAILABLE at EPS. Walk C1 as tc.tech.one; C2 reads
as any role (tc.ca.one verified).
6. Do first next time. Read from-opencode (the supplier screen may have landed — if so, turn C1's supplier line
into a real step, add the supplier-management item to the Coverage Plan, and answer the note), then take Matthew's
next instruction. The dental verification pass (11 flagged pages, 24 dumped) is still mine and unfinished.
⚠️ OPEN NEGATIVE ACTION (2026-09-19) — read this before trusting any guide statement about an NHS or
regulatory artefact. Raised by Matthew during the B5 review; recorded at
Regulatory Content Audit — Negative Action.
(a) Endorsement reference — DONE: RULED, BUILT, DEPLOYED 2026-09-19 (5b4e135). Removed from
the app; receipts match on the EPS token reference only; historical rows keep their value. B5
corrected — the box passage is gone and both figures re-shot.
(b) Exemption categories — DONE: RULED, BUILT, DEPLOYED, GUIDE CORRECTED 2026-09-19. The eight
free-text categories are gone; the dispense screen now offers the NHSBSA EPS codes (16 valid ones; obsolete codes
retained but not offered; Wales 1001/1002 omitted pending a decision on Welsh contractors), and charge_due is
derived. Guide B4 corrected and its figures refreshed; the Coverage Plan and the guides index updated.
Parked until we reach guide group F (his call 2026-09-19): Finding 4 (the EPS vocabulary collision — B1/B6 wording, and what the local stop is),
Finding 5 (the Dispensed column on the dispensary queue) and Finding 6 (no way to record a problem with a delivery, found
while writing C1). The adjustment-retry idempotency fix is needed before C4 is documented.
(c) The RP record — AWAITING HIS RULING, and it is not just wording. The authority (SI 2008/2789 reg 5) requires
the record to hold the RP's name, registration number, time on and time off and, for any absence, the date and the
times it began and ended, kept for five years and available at the premises for inspection. The app holds the
first four and has no absence recording at all; and the Coverage Plan's "cannot lawfully operate without a
Responsible Pharmacist on duty" overstates the law — reg 3 permits up to two hours' absence. Two decisions are
with him: the guide wording (cite the regulation) and whether the absence/retention gaps become a build item.
(d) Dental sweep STARTED (his instruction): 24 dental pages dumped, 11 make claims of this class — densest
NHS Contract & FP17 journey (33 hits), UDA Auditing (27), FP17 Claim Submission (24). Verification pass against the
dental app and the authority is the next piece of work. Pharmacy sweep found nothing else invented; manuals and site
copy not yet swept.
Updated 2026-09-18 (session 20 — Matthew called time; "we'll continue with the rest of the pharmacy guides
tomorrow"). The build is unchanged: the app engine is on 4426e20 (the staff user-management screen,
deployed and verified 2026-09-16 — migrations partner.0007 + pharmacy.0015, 309 tests OK on my own run) and
nothing is waiting on a build. origin/dev = 4426e20.
THE LIVE WORK IS THE GUIDE SERIES, AND GROUP B (DISPENSING) IS NOW COMPLETE. Published: A1–A3 (RP sign
on/off), B1–B7 (claim, assemble, accuracy check, dispense, endorse, release-or-cancel, history), Finding 1–Finding 2
(premises) and H1 (roles and permissions) — ten guide pages plus the Coverage Plan, all re-verified
2026-09-18 (bodies clean, 17 figures served and byte-matching, every page in the sidebar tree). B1–B7 were drafted
one page at a time, each reviewed by Matthew before the next was started — that is the method that suits him.
Nothing next is assumed: he names the group (C stock, D counter, E collection, G money) and I draft one page.
Also outstanding: the F illustrations, and A8, the bookkeeper handover pack — still MINE, still unwritten
(the last Phase 6 acceptance criterion). Four things are waiting on HIS ruling before anything goes to OpenCode
— see SESSION 20, item 4. Do not raise them unilaterally.
1. Published today (each drafted, figured, published, then verified): B2 Assemble a Prescription, B3 The
Accuracy Check, B4 Dispense a Prescription, B5 Endorse a Prescription, B6 Release or Cancel a Claim,
B7 Dispensing History. With B1 from session 19, group B is finished. Two stale things found and fixed while
orienting: the guides index was missing the B1 row (a published page unreachable from the series index), and
this skill carried two wrong "verified" facts — the RP log is sorted most-recent-first
(Meta.ordering = ['-signed_on_at'], models.py:144) and the BM/CA identity gap is closed on the client
tenant, so no group on the plan is blocked on a missing login.
2. The method that worked — reuse it. One page per exchange. Read-only captures: log in over HTTP, GET the
screen, blank every CSRF value, screenshot, trim to content (/tmp/mockups/capture-b{2..7}.py), so the
reviewer's fixture token is never consumed — the "after" figure comes from the token already parked in that
state (demo-token-assembled-1, -checked-1, -dispensed-1). Behaviour a guide turns on is proven
non-destructively: build the form/formset in manage.py shell and read is_valid()/errors, no save
(/tmp/probe_b2_forms.py, /tmp/probe_b4_form.py). A series-wide integrity sweep exists at
/tmp/sweep_guides.py (bodies + every figure, byte-compared against the local files).
3. Where the fixtures stand — nothing was consumed today. demo-token-claimed-1 (Bravo) and
demo-token-available-1 (Alfa) are both Claimed, so the spine's dispensable list is now empty; the
assembled / checked / dispensed / cancelled fixtures are untouched. Warn him before any dispense: every
seeded token's items carry no SNOMED code, so a dispense moves no stock and the prescription shows the amber
"Dispensed without a stock movement for at least one item" line — designed behaviour, documented in B4 and noted
on the Coverage Plan.
4. FOUR OBSERVATIONS AWAITING HIS RULING — do not raise them with OpenCode yet. (a) Release is offered from
Claimed/Assembled but not once the accuracy check is recorded (documented in B6 as behaviour — intended?);
(b) an over-length endorsement reference (over 128 characters) is silently dropped while the endorsement
still records; (c) refusal messages reach users in raw form, e.g. "cancel requires the token to be AVAILABLE or
CLAIMED or ASSEMBLED or CHECKED (it is DISPENSED)."; (d) carried over from session 19: the password-vs-invite
choice for new staff accounts is still my recommendation only — the built screen sets a manager-entered
temporary password, and the invite alternative has not been ruled on.
5. Handoff housekeeping done this session. Both inboxes swept: 8 HANDLED notes from from-opencode and
10 from from-hermes moved into archive/2026-09/* (now 55 and 60 archived notes), each inbox left
with a pointer and a pass line in the archive header. from-opencode now holds NO open notes — nothing is owed
to me. from-hermes keeps ONE OPEN note: the 2026-09-16 Keycloak master-credential exposure, and I
re-checked it today — /opt/kcadmin.sh is still mode 755 and still holds the credential as a literal,
with no external credential file, so that finding stands and still needs his decision (rotate; chmod 700 plus a
0600 file; the repo copies).
6. Do first tomorrow. Read from-opencode (empty — nothing owed), then let him name the group and work the
Coverage Plan one item at a time. The pharmacy-app-guides skill now carries the verified app facts for all
of group B plus the read-only capture rule, so nothing needs re-discovering.
1. RESOLVED 2026-09-18 — he chose ONE AT A TIME (review each page, then name the next; see SESSION 20). Ask him one thing: chain B2–B7? He asked me to draft the whole of group B ("we need to
draft the guides B1-B7"). I drafted B1 only, deliberately, because our agreed method is one item, then his
test, then the next — and I asked whether to chain the rest in one pass. He has not answered. Do not assume
either way. Content for B2–B7 is fully grounded already (see item 3).
2. What was published today. Dispensing — Claim a Prescription
(B1, two figures). The figures are captures of the app's own rendered screens (logged in as tc.rp.one, real
HTML, CSRF blanked) — reuse /tmp/mockups/capture-groupB.py for B2–B7. The Coverage Plan was updated: test
users at the top, a Test as: line per group, B1 Published, group A marked complete, and a table of the tokens
parked in every state (so group B walks without seeding: demo-token-available-1 claimable now;
claimed-1/assembled-1/checked-1/dispensed-1 for B2–B5; cancelled-1).
3. Grounding already done for group B — do not redo it. pharmacy_dispensing: routes (/dispensary/,
/claim/, /<pk>/, assemble/, check/, dispense/, endorse/, release/, cancel/, history/); the state
machine in machine.py (the only place transitions live: AVAILABLE→CLAIMED→ASSEMBLED→CHECKED→DISPENSED→ENDORSED,
RELEASED/CANCELLED side states; permitted_actions drives the buttons); forms (assemble = a formset of
quantity_supplied; dispense = ChargeDeclarationForm with charge_due XOR exemption_category + evidence flag;
release needs a reason, cancel's is optional; endorse takes an optional local reference); and the five templates.
Watch-out for the walkthrough: the pre-assembled demo tokens (assembled-1, checked-1, dispensed-1) have
no SNOMED code on their items, unlike claimed-1/the claimable one — so a dispense on those may not move
stock. If he sees "dispensed without a stock movement", check whether assembly dropped the SNOMED code before
writing anything; that would be a real finding for OpenCode.
4. The identities are consistent now — this is what he logs in with. All on the client tenant
test-client-pharmacy: tc.rp.one, tc.rp.two, tc.test.user (RP), tc.bm.one (BM), tc.ca.one (CA),
tc.tech.one (Technician). One shared credential, held host-local at /root/.config/pharmacy-demo-password.txt
(0600) — never in the wiki, never in git; he set it to his standard value and I applied it to all nine identities
in both realms. tc.tech.one was created through the app's own staff screen, which closed the last
live-unverified path of that screen (create → requiredActions=[UPDATE_PASSWORD] → audit STAFF_USER_CREATED →
guard rail refused self/last-BM disable → then reset non-temporary so it shares the one credential). demo.*
identities exist only at the apex. A fully-scoped alternative: one browser holds one identity (proved
live) — simultaneous roles need separate cookie jars (Firefox containers / profiles / two browsers).
5. Decided by him (2026-09-16): the apex tenant serves no purpose and is to be removed.
pharmacy.veripath.co.uk does not operate as SIAAS (no client-VPS behind it), so it is not the test model;
kept for now, removal deferred. Written up at
apex-tenant with the removal checklist
(do not pull the shared :8020 forward blindly — both vhosts proxy to it; KEEP realm pharmacy, it is the
provisioning source for new client realms).
6. Parked by him (2026-09-16): "several staff working at the same counter". Recorded on the pharmacy index as an
open design question, do not build. The app already attributes every act to a person (Basket.operator,
Tender.operator, Refund.actor, TillSession.opened_by/closed_by, dispensing actor + name snapshot,
PharmacyAuditEvent.actor); unsettled are shared-terminal vs device-per-person, per-operator vs per-counter till
session, and whether guides should teach multiple identities at all.
7. Waiting on Matthew: (a) the B2–B7 answer; (b) A8; (c) the Keycloak admin credential exposure — it is
world-readable in /opt/kcadmin.sh, on argv (visible in ps), and committed in veripath-ops; recommendation is
rotate (evidence and remediation in from-hermes, 2026-09-16); (d) a glance at the sidebar to confirm B1
appears — I cannot verify the nav from the shell: Wiki.js renders the sidebar client-side, so the served HTML
contains no navigation at all. My earlier "sidebar verified" checks were grepping body links; the skill is
corrected.
8. Skill changes made today (~/.hermes/skills/veripath/pharmacy-app-guides/SKILL.md): the sidebar check
corrected as above, and a new "Capturing figures from the live app" section (capture the real HTML → blank
CSRF → screenshot → trim; better than hand-built mockups, which drift).
Historical — session 18 (2026-09-15), kept for the audit trail:
test-client-pharmacy now authenticates through its own Keycloakpharmacy), its staff exist only in that realm, and apex/client isolation isPartnerOrg row), the last of which is aprovision_pharmacy_client_org), verified live with --dry-run.origin/dev = 4426e20 (two commits — 78c1d7c a realm-scoped service account, 4426e20 the screen),/pharmacy/staff/ list + inline add-user with a role selector, detail,ce072f0 — and their note is OPEN against me for verification + deploy: STEP 1 tomorrow.So the live work is: (1) the guide + test series, one item at a time (A1 is drafted and awaiting Matthew's
test), (2) write A8, (3) the two OpenCode items below. Nothing is waiting on Matthew except his next guide
item and counter pricing.
This is NOT a demo-browser walkthrough, and not build-phase order. Matthew corrected it on 2026-09-15:
it is everyday practical pharmacy operation, all roles, and HE logs in as the user and runs the steps
himself. I do not drive a browser; the demo-agent machinery is not used for this.
The agreed method — follow it exactly:
Where the series stands (as at 2026-09-16): Group A is FINISHED (Matthew: "we are now done with the first
part") — Responsible Pharmacist — Sign On & Sign Off,
three figures. Group B (dispensing) has started: B1 is PUBLISHED —
Dispensing — Claim a Prescription
— and B2–B7 are NOT drafted. See the SESSION 19 block above for the state and the open question.
Two things to tell him before he runs A1 (both now stale — verified 2026-09-16):
Uses these skills: pharmacy-app-guides (minted 2026-09-15 — the worklist, the verified role-gate matrix,
the app's real UI conventions and the mockup pipeline) + veripath-docs + veripath-wiki-assets.
Still owed by me: A8, the bookkeeper handover pack (the last Phase 6 acceptance criterion); and the
rest of the guide series, one item at a time.
✅ RESOLVED 2026-09-16 — the identities are consistent; nothing is half-finished here. The session-18 items are
all closed: tc.ca.one and tc.bm.one are verified working (role attribute + role group + org_id read back
from Keycloak, and a real login per surface), tc.tech.one now exists, and demo.tech.one now has a Keycloak
identity so it can log in. All nine identities carry one credential, correct role attribute, correct role group
and no forced action — see the SESSION 19 block at the top for how that was done and the one credential's location
(host-local, 0600 — never the wiki, never git). Do not re-enable the five disabled tc.* users in realm
pharmacy: they are the pre-realm-fix cross-tenant path, deliberately disabled.
veripath-ops/scripts/create-pharmacy-test-user.py is idempotent and takes --realm; its password path now--realm (commit aa319b0, 2026-09-16 — it used to target realm pharmacy always). Password travelskcadm auth call still puts the admin password on argvps) — that is part of the exposure recorded in from-hermes, not a bug I can fix quietly./pharmacy/staff/, Business Manager) rather than the script — itorg_id, temporary password, audit).▶ NEXT SESSION — DISCUSSION ITEM (Matthew's request, 2026-09-15): a pharmacy user-management view that talks to Keycloak.
He wants a dedicated view for adding a user, connecting to Keycloak, "like we done in the gp and dental
apps" — so pharmacy staff can be added from the app rather than by hand in Keycloak. NOT authorised yet: he
will spec it with me next session. I have written it up in from-hermes, explicitly marked do not start
building.
Grounding already done — do not redo it:
staff_management IS installed and routed in this app at /staff/create/, butstaff_management/views.py::StaffCreateView creates only the Django user (form.save() + aStaffRoleRecord) — there is no Keycloak call anywhere in that app — and it is gated byAdminRequiredMixin (platform admin roles), so no pharmacy role can reach it. It is inheriteddemo.tech.one is the proof of the failure mode: it has an app-side CustomUser and no Keycloakpartner/ (create_partner_realm.py, partner/utils.py) and itveripath-ops/scripts/create-pharmacy-test-user.py — sets the role user attribute thatusers/auth.py::_pharmacy_role_from_claims maps to an app role, joins the matching role group, passwordonboarding has an invitation flow (practice-staff/invite/) — the alternative to password-setting.OIDC_CREATE_USER = True), so theOpen questions to settle with him: which role gates the view (BM / RP / both); password-set vs invitation
link; partial-failure handling (Keycloak succeeds, app fails, or the reverse); and whether it lives in a new
pharmacy_users client-tier app or inside pharmacy.
OpenCode's outstanding items (two notes OPEN in from-hermes):
test-client-pharmacytc.rp.one, tc.rp.two, tc.test.user) — the single Techniciandemo.tech.one) is on the apex, so the Technician's own view cannot be tested on the client tenant.PharmacyDashboardView [:10], RPHistoryView), so the cardorder_by('-signed_on_at'), filter beforeRead in this order (all via /opt/wiki-manage.py --get):
agents/handoff/from-opencode — my inbox, check at the very start of the session; process Status: OPEN notesagents/handoff/from-hermes — what is waiting for OpenCode (newest on top)development/projects/pharmacy_app/build-records/phase-6-accounting — the phase's verdict + acceptance-test evidenceagents/hermes/housekeeping-log — conventions, open items, and the tooling traps belowRe-verify acceptance test 1 when OpenCode's next fix lands (a note is OPEN in from-hermes):
seed_pharmacy_counter --reset runs and A4 then reports ties=True. Round 3 proved the reset now runsProtectedError is genuinely fixed) — but the tie still fails, because the reset cannot see the staleties=True on the full period, not on a narrowed window.reimbursement_summary() returns 422.10 both with and without premises — DONE, proven round 3 (valueseed_pharmacy_stock_valuation:A8, the bookkeeper handover pack, is MINE and is written after acceptance — do not start it early.
Matthew has since ruled that the walkthroughs START NOW, from the beginning — the earlier "builds first" gate is
satisfied, because the builds are finished. So do not treat Phase 6's formal acceptance as a precondition for walking;
do keep applying the same discipline (one at a time, agree scope first, stop for review).
/root/work/pharmacy_booking_app, branch dev, local = origin = e6a5fbb, working tree clean,ties=True, mismatches=[], variance 0.00, band splitSTANDARD 5.00/1.00, ZERO 9.90/0.00) after --reset, with local basket 6 gross=15.90 against ERPNextACC-SINV-2026-00019 net=14.90 tax=1.00 grand=15.90. The old orphan (basket 2) is gone and the real basket 3 is9c301ca changed the reset's identity key from the placeholdere6a5fbb matches both.tc.rp.one (RP) and fetched all eight/accounting/ URLs → HTTP 403 with a permission-denied body on every one. (Anonymous gets 302, so a 403 meanspharmacy% tables; apex and client tenant 30seed_pharmacy_demo creates RPs only. OpenCode has the ask. It blocks the Phase 6 walkthrough entirely (theopencode process is not automatically active work — check before you stand down. An idleps -o pid,etime,time,pcpu,stat -p <pid>TIME and state Sl+, and uptime's 1-minutegit log -S"<marker>").Page ID AND Render: OK — and the rendered body confirms it.Content: <n> chars before it can fail, so an over-long description (>255 chars, avarchar(255) column) silently no-ops the page while looking successful. This cost one silent no-op today.=-rule to the LAST, never the first occurrence alone. /tmp/wget.sh./wget.sh <path> <outfile> → then edit → upsert with -f.patch → upsert -f. And when prepending a note,pk 4, Test User, is_on_duty=True) so a sign-on returns "already on duty" — signing that off is step 4 of thepharmacy-app-guidesPharmacyDashboardView [:10], RPHistoryView) so the "Recent RP log" card shows the ten oldest —origin/dev = e6a5fbb, one commit, no migrations, 276 tests OK on my run) fixed the_demo_baskets() now matches counter_customer=DEMO_MARKER OR erpnext_invoice='DEMO-SINV-0001'ties=True, mismatches=[], unlinked=[],STANDARD 5.00/1.00 + ZERO 9.90/0.00), both sides read (local basket 6 gross=15.90ACC-SINV-2026-00019 net=14.90 tax=1.00). The orphan (basket 2) is gone and the real basket 3VOIDED, ACC-SINV-2026-00016) is intact — so keying on the placeholder invoice, not the shared customer label,sale:<pk>:<line>).tc.rp.one and all eight /accounting/ URLs returned HTTP 403 (anonymouspharmacy% tables, apex andseed_pharmacy_demo seeds RPs only — so the BM-only accounting reports cannot be opened by a human,origin/dev = 96d64b3, no migrations, 275 tests OK on my run) fixed both round-2 bugs.default, sector_pharmacy and the client tenant) and re-verified:reimbursement_summary() bug is FIXED and proven — value and SQL (_expected_scope(None) compiles(AND: ), no IS NULL; expected_total 422.10 with and without premises, against actual 421.10, varianceProtectedError is FIXED — --reset ran exit 0 and posted a real ACC-SINV-2026-00018, and thepurge.py works (no pre_delete signals, so the queryset delete genuinely bypasses the9c301caerpnext_invoice=DEMO_INVOICE to counter_customer=DEMO_MARKER, orphaningCOMPLETED, gross 15.90, invoice DEMO-SINV-0001, counter_customer='Counter Customer'), which theties=False, variance -15.90). A4 is honest — the2026-09-15 givesties=True, no mismatches, variance 0.00, band split STANDARD 5.00/1.00, ZERO 9.90/0.00 — matching ERPNext'sitem_wise_tax_detail on ACC-SINV-2026-00018 (net 14.90 + tax 1.00 = 15.90), read both sides. Suggested fixfrom-hermes; do not sweep on counter_customer='Counter Customer' — basket 3 carries that label with aseed_pharmacy_stock_valuation --tenant test-client-pharmacy (2 received,MAT-STE-2026-00029/30) and A5 now reads 353.85 / 273 units = ERPNext's own Bin sum exactly, read from bothpharmacy_accounting, origin/dev = 71e80e5); 266 tests OK onpaid_to, erpnext-deploy 40474b4; the compose has no build directive, so it is a manualdocker build -t erpnext_v15:latest . then up -d — deploying the app first would have broken the counter).ACC-PAY-00001 credits 1010 - Clinic Till / Cash on Hand, ACC-PAY-00002 credits 1170 - Pharmacy Counter Till. A2/A3's refusal behaviours verified liveties=False, variance -15.90) becauseseed_pharmacy_counter writes DEMO-SINV-0001, which is not an ERPNext document; A5 values 196 units atfrom-hermes for OpenCode.origin/dev = 9c301ca, 270 tests OK on my run) fixed findings 2–4 and they are proven live:item_wise_tax_detail — STANDARD 5.00/1.00, ZEROseed_pharmacy_counter --reset raises ProtectedError (threeDEMO-SINV-0001 basket in place and keepsreimbursement_summary() reports expected_total as 0.00 instead of 422.10 whenpremises — a silent under-report in the variance view, because token__premises=None compilesIS NULL. Acceptance test 5 needs a valuation on the demo stock (every Bin row is 0.00). Also asked for adelete() raises by design.pharmacy_accounting. It also flagged that D6.6's "band (B1/B2/B3-style, per the NHS schedule)" wasfrom-hermes.origin/dev =d77621f), deployed it to the app engine, applied pharmacy.0011 + pharmacy_notifications.0001 (centralDISPENSED untouched; delete()pharmacy/accounting.py + provision_pharmacy_accounts, 5 accounts created and 3 mappedapi.notifications.service.gov.uk) whereas this app uses NHS Notify / Communicationsservices/nhs_notify.py) but it is aagents/handoff/from-hermes (2026-09-15) so its next session re-discovers none of it.core/permissions.py andphase-6-accounting still said "Phase 5 is not built or authorised", the module-map status line stillpharmacy_app/index said the same while its pick-up block ledDISPENSED and never before (a tokenRELEASED or CANCELLED never sends), the collected handover record (append-only;DISPENSED immutable), opt-out honoured on every send, the delivery trail and the "did they know?" view,1010 - Clinic Till / Cash on Hand)from-hermes carries the ask).infrastructure/central-db-password-rotation, not executed; proposed window Sundaytest-client-pharmacy) is live with its own database, identities, host and TLS on the client-owned0002 applied on both client-tier instances. Outstanding: W12, thesnomed_codeorigin/dev = eb1ce70, 183 tests green): one submitted Sales Invoice whose VATitem_wise_tax_detail, a Payment Entry referencing counter:1 with no patientmigrate_client defaults to the tenant'sveripath-ops; and the research pages' internal addresses redacted by me.agents/handoff/from-opencode — anything OpenCode has left for me. Check for Status: OPEN.agents/hermes/housekeeping-log — the wiki tidy-up phase tracker, open items and session historyagents/hermes/housekeeping-log-archive).development/projects/pharmacy_app/index — the pick-up list — then the phase spec for whatever is liveagents/handoff/opencode-resume is OpenCode's note, not mine — but read it for branch and commit pointers.
Done and verified: own role and database in the box's cluster (schema ownership handed over and checked),
its own reverse-tunnel unit on the box, a gateway unit on the primary, a separate forward unit on the
app engine (so the running GP/dental forwards were never restarted), the tenant row retargeted through the
app's own seed, the schema applied pharmacy-only, synthetic data re-seeded, real OIDC login plus the full
RP cycle re-exercised, three-way isolation checked and the ClientVPS row recorded. Full record, runbook
and findings: client-vps.
The one thing to carry forward: the per-client database alias is built once per process
(tenancy/routers.py::_load_dynamic_aliases), so a retarget only takes effect after the app restarts —
before that, writes silently continue to the former database while every page looks correct. The runbook
now makes the restart explicit.
The client host's demo credential stays host-local, as before (never in the wiki, never in git); the
seeding and verification scripts read it from the host-local file the provisioning scripts already use.
/opt/defectdojo — loopback :8090, public URL unchangedsecurity.veripath.co.uk/dojo/). Images pinned by digest; compose version-controlled atveripath-ops/defectdojo/.test-client-pharmacy (client sector), served by the same app-engineAGENTS.md's topology section is corrected and approved; a read-only mirror (addresses masked) is atagents/agents-md.origin/dev = eb1ce70 (Phases 2–4 + the finish-off items, deployed);veripath-ops master = 5aa3326 (Keycloak script + rotation runbook).| Item | State |
|---|---|
| Counter pricing: net vs VAT-inclusive | OPEN — decision for Matthew. Blocks the counter being customer-ready, not Phase 5 |
| Central DB password rotation | Runbook ready, not executed; window proposed Sun 2026-09-20 02:00–04:00 — awaiting his yes |
| Pharmacy roles-and-permissions guide (W12) | PUBLISHED 2026-09-15 — roles-and-permissions, awaiting Matthew's review |
| Pharmacy guide + test series | THE LIVE WORK. Worklist published as the Coverage Plan (30 items, groups A–H). A1 drafted + illustrated, awaiting Matthew's test; he names the next item — one at a time |
| RP log ordering | OPEN with OpenCode — both the dashboard card and the RP Log page are unsorted, so "Recent RP log" shows the ten oldest shifts. Fix: order_by('-signed_on_at'), filter before the slice |
| Phase 6 build (A1–A11) | COMPLETE — e6a5fbb, deployed, 276 tests green on my run. The v1 build has no coding left |
| Phase 6 acceptance test 1 (reports tie) | PASSES 2026-09-15 over the full period (ties=True, variance 0.00, both sides read) after OpenCode's e6a5fbb reset-scope fix |
| A8 — the bookkeeper handover pack | OWED BY ME — the last Phase 6 acceptance criterion (criterion 6); nothing else is outstanding on the phase |
| Pharmacy demo identities | OPEN — blocks the Phase 6 walkthrough. seed_pharmacy_demo seeds RPs only: 0 Business Managers, 0 Counter Assistants platform-wide, so the BM-only accounting reports cannot be opened by anyone. OpenCode has the ask |
| CLOSED 2026-09-15 — Matthew ruled option (a); the demo valuation was run and A5 = 353.85 = ERPNext's own Bin sum (273 units), both sides read. Ruled limitation: four demo rows stay at 0.00 by Phase 3's zero-valuation design | |
| Pharmacy client tunnel monitoring | Gap: the per-tenant tunnel watchdog covers the GP and dental tenants only |
| GP/dental migration reconciliation | The four reconciliations are identical in the GP repo but GP's router must first allow GP apps on default — do it as a reviewed change, not a copy |
| VPS3's stopped DefectDojo containers | Kept deliberately as rollback. Remove whenever Matthew is comfortable — nothing depends on it |
| VPS3's single vCPU | Open decision for Matthew: ERPNext alone may or may not justify a second core |
| Off-host backup | Deferred by decision. No off-host backup exists; research at security/client-vps-backup-plan |
| Dental UDA demo re-run + FP17 doc pass | Parked on Matthew's instruction — do not start until he unpauses |
| Marketing site build | Any emergency-slot-management.md rewrite is parked in the veripath-site working tree |
| DONE 2026-09-14 (filter-repo + force-push; pre-purge bundle retained) | |
| DONE 2026-09-14 — dumped, then dropped | |
ClientVPS inventory row |
DONE 2026-09-13 |