Thirty-three findings were triaged and deferred during the build, plus
the ones the whole-branch review raised. The task ledger was scratch and
gets deleted, so this is the record - and the list is worth having: the
bridge guard that rejects pasted lines containing '@collection.', the
skill file's family tables covering 68 of 148 tools, and the reorder
points that gate the deferred dashboard view.
Giellie can now update stock quantities from a pasted list. The operator
pastes SKUs and quantities into the chat; Giellie resolves each line
against stock_items, asks whether the numbers are the new quantity on
hand or an addition, shows the plan, and writes the change against the
signed-in operator's name in the stock ledger.
The write path is ledger-first and audited, a failed write is its own
status and is compensated so the ledger cannot drift from the quantity,
and a dry run writes nothing at all. Nothing blocks on a low quantity -
the till oversells on purpose and the stocktake is what corrects it.
The till now shows the quantity on each catalogue tile, drawn under the
price as it is, negatives included.
The system prompt justified avoiding the stock_items.list +
stock_items.adjust loop by claiming that path "writes a ledger row with no
actor". That stopped being true when the branch routed adjust through the
audited write service: adjust now requires actor.id and its ledger rows
carry created_by. The instruction itself is unchanged and not weakened —
the signed delta and the missing batch call are still the reasons to
avoid the loop, and the new tool's own advantages are now named.
stock_items.create now also requires actor.id, so an external MCP client
that omits it gets a 500 JSON-RPC error. The generated catalogue row is
truncated at 240 chars before that sentence, so both authoritative docs
get a hand-written row and an input-shape block instead. The generator is
not re-run; the catalogue is still 148 tools.
The spec's till bullet described the first design, not the code that
shipped: the quantity is drawn under the price on the right (the
operator's own decision during the build), and the service-worker cache
was bumped to `pos-v39`.
- The tile bullet now points at `pos-app.js:290` and says the `.sub`
line keeps `sku · brand · unit · M in cart` while the quantity sits
under the price.
- The cache bump reads `pos-v37` -> `pos-v39`.
`.agents/skills/wfdash-giellie/SKILL.md` says "trust the docs", so the
two source-of-truth docs override the branch's guidance — and they still
sent every quantity write to the pre-branch `stock_items.adjust`.
- Regenerated the tool catalogue with the generator the doc itself
names (`python3 scripts/mcp-doc-gen.py`): the count is now 148 (was
144), `stock_items.set_quantities` is in the catalogue and in the
alphabetical index, and the `stock_items.adjust` row carries its new
description. `python3 scripts/mcp-doc-gen.py --check` now passes.
- Added a `stock_items.set_quantities` row to the stock tool table in
both docs, matching the tool's real input shape: `mode` ("set" |
"add"), `lines` (1-100, each `{ sku?, description?, qty }`),
`dry_run`, plus the required `actor.id`.
- Repointed the quantity guidance in both docs away from
`stock_items.adjust` and toward `stock_items.set_quantities`, and
marked `adjust` as superseded for quantity work.
`stock_items.adjust` wrote its `stock_ledger` row with no acting user
(`created_by: ""`), and it updated `current_qty` with no compensation —
the same orphan-ledger shape the new quantity tool removed. The docs
advertise this tool, so it is a load-bearing write path.
- `updateStockItemsQuantities` takes optional `reason`, `referenceType`,
`referenceId` and `notes` overrides. The defaults are unchanged
("adjustment", "manual", the service's own note), so the /stock page
and the agent write exactly what they wrote before.
- The adjust handler now requires `actor.id` (the same 15-char check and
error style as `stock_items.set_quantities`), reads the row's current
quantity, turns the signed delta into an absolute target and calls the
service. Attribution, ledger-first ordering and the compensating
reversal row come from the service instead of being duplicated here.
- The caller's `reason` and `notes` survive the conversion, and a
negative resulting quantity is allowed — the till oversells on purpose.
- The tool description marks the tool as superseded for quantity work
and points at `stock_items.set_quantities`.
The route treats the string true as a dry run, so the policy must too,
otherwise a dry run still raises an approval card for a write that never
happens.
The ledger row is written first on purpose, so a crash cannot leave the
audit trail behind the cached total. But when the quantity write then
fails, the ledger sat ahead of the quantity - and the operator was told
to retry, which wrote a second ledger row for the same change and
double-counted it.
A failed quantity write now reverses its own ledger row, so SUM(change_qty)
still reconstructs current_qty and a retry is safe.
Reporting a failed write as an unmatched line tells the operator they
typed the SKU wrong when the cause was a transient database error. That
sends them hunting for a typo instead of retrying - different problems,
different responses.
Also records the dry-run fix: on a dry run a matched line is reported
applied with its before and after numbers, and nothing is written.
The operator decided that when a pasted SKU matches nothing exactly,
the SKU string should get a second chance at matching by how the item
is named - pasting "M16 Flat Washer" into the SKU box should still find
the flat washer.
The line's match text is now the description and the SKU joined, so a
line carrying only a SKU still gets a text match. Everything else about
the text step is unchanged: the same 0.6 threshold, the same "top
candidate wins unless two are tied at the top" rule.
Also from the Task 2 review: an inactive best match now names itself
instead of vanishing, the resolver truncates the quantity so the dry-run
plan and the stored value agree, and the test list picks up the
fall-through path that had no coverage.
The approval card renders the JSON inside a <pre> with max-h-48 classes,
the composer already imports Textarea, and the POS tile is built in a
template string rather than a class-join expression. Each instruction
now names the code that is actually there.
The spec settles what was decided; the plan breaks it into 9 tasks
with the code each one needs.
Task order is the dependency order: the service learns to write a
negative quantity, a pure resolver learns to map a pasted line to a
stock item, a bulk service learns to apply and mirror, and only then
does the MCP tool, the agent prompt, the approval card, the composer
shortcut and the till tile hang off it.
Two corrections to the spec while writing the plan: the approval card
receives dotted MCP names, not model-facing ones, and the description
match takes the top candidate unless two are tied at the top - a bare
threshold would report 'ambiguous' for almost every description and
defeat the feature.
Giellie can already reach stock_items, but only through ad-hoc
list+adjust loops: a signed delta instead of an absolute count, no
batch call, fuzzy SKU picks the model cannot see are wrong, and
stock_items.adjust throws the actor away so the ledger row has no
attribution.
Adds a stock_items.set_quantities MCP tool that resolves each pasted
line (exact SKU, then description fuzzy), computes the delta, and
writes through updateStockItemsQuantities so the ledger is audited
against the signed-in user. Also mirrors price_list.stock_qty, which
price-list-service.ts documents as a denormalised snapshot of
current_qty.
The live data makes this matter: 522 of the 4,083 sellable items carry
a negative tracked quantity and 2,901 sit at zero. That drift is by
design - the till oversells on purpose (pos-service.ts:603) and is
corrected at the next stocktake, which is what this feature is.
Nothing blocks on low stock. One service flag (allowNegativeQty)
lifts the non-negative floor for the agent's writes only, leaving the
/stock page untouched.
The item supplier payments and approvals queue: a payment hold on order
items, the Payments and Approvals page, supporting documents and proof
of payment, and the PB hook that stops an unpaid item becoming ready for
collection.
The theme registry carried a hand-maintained set of hex swatches for
every palette, and they had drifted away from the CSS the app actually
paints. The picker previewed colours the product never showed.
The registry now holds only what cannot be read from CSS — id, label,
tagline, fonts, radius — and the picker, the Settings swatches and the
preview cards resolve --background, --primary and --foreground off the
theme itself. A preview can no longer disagree with the screen it
previews.
themes.css is rewritten so every palette carries a real dark surface,
and the palettes that are dark by nature stay dark whichever way the
mode switch is set. The pre-hydration script validates the stored theme
id against the registry, so a retired id falls back to WFDASH instead of
painting an unstyled page, and the old "warm" mode migrates to Light.
The legacy text-{warn,success,info}-foreground classes now resolve to
the semantic text colour and switch to the filled ink only on an exact
solid fill, so one class reads correctly on both a tinted badge and a
filled button. A solid focus outline replaces the faint ring, and the
charts and quote status badges take their colours from tokens rather
than hard-coded dark strokes.
Tests: theme-bootstrap.test.tsx covers saved-theme recovery and the
storage-blocked path; e2e/themes.spec.ts walks every palette in both
modes checking surface contrast, tinted and solid semantic pairs, focus
colour, chart tokens and the destructive hover, and asserts that the
picker preview matches the applied theme.
Remove payment amount, reference, notes and manual combining forms. Link supplier POs automatically and record paid from supplier evidence plus proof, keeping readiness and attachment safeguards.
Keep active supplier/company views current and retire stale supplier evidence on routing changes. Verified 5167 tests with only the two existing script-import errors.
Keep payment holds independent of workflow and margin approval. Require proof before collection readiness; deduplicate uploads transactionally in PocketBase and preserve payment history.
Includes schema migration, deployment runbook, browser checks and isolated PocketBase regression coverage. Full suite has 5156 passing tests and retains two pre-existing script-import errors.
Three tasks, each ending with the suite green: give every release a
number and a commit, switch the popup over and drop the date-shaped
version string, then document the convention and verify.
The order matters — the new fields land first so the component never
sits on a broken type mid-change.
The What's New popup shows the date each release went out, and the
version strings are dates too. That gives the office a way to ask
"what did you do on the 30th?".
Design: each release is identified by a release number and the short
commit SHA it shipped with. The date stays in the data file as an
authoring reference but is never rendered, and the date formatter is
deleted so no date can reach the screen.
Five existing entries are renumbered 1-5, each anchored to the last
commit that landed in it. A guard test fails the build if a date ever
comes back into the popup.
Owner's rule: the POS counter only carries priced stock.
- POS catalogue lists only active stock items with selling_price > 0
(POS_CATALOG_FILTER); checkout refuses any line priced at R0 or less.
- New stock order now gives a new stock item the PO line's cost and the
default 80% markup. It used to drop the cost, which is how 157 stock
items reached the till at R0.
- Collection Confirm "Add to POS" becomes "Add to price list": customer
lines go to price_list at cost + 80% (needs SKU, cost and supplier),
never into stock_items. Route renamed to add-to-price-list.
- scripts/2026-10-30-pos-zero-price-cleanup.ts: priced 79 items from their
PO line cost, switched off 48 with no cost and no stock, left 30 stocked
ones active for costing (already applied on live PB).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New /delivery-pool page, a clone of the Collection Pool for deliveries
with no pending stage. Invoiced lines are grouped per company, one card
per customer invoice; a card expands to its lines. Assign a driver and
day per invoice (or bulk per company); uploading the signed copy adds it
to every line, marks the lines completed + delivered_at, and closes a
delivery note once none of its lines is still open.
- Driver lives in new order_items.delivery_driver / delivery_assigned_at
(npm run pb:migrate:delivery-pool), never drv_id (collection driver).
- Pool starts at DELIVERY_POOL_START (2026-10-30); lines whose DN is
already completed/cancelled (POS counter sales) are left out.
- Service + /api/v1/delivery-pool routes (browser session); the service
503s until the migration has run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On PO202610005TEL the AI read a 15% discount that was not on the invoice
and cut a line's cost from R146.47 to R124.50. It no longer writes a cost
to order or stock lines. A cost that differs from the invoice's discounted
price shows as a red Price tick, which also keeps the card out of
auto-push, so a person settles it. Earlier cost changes stay visible on
the line. Tests now assert no cost is ever written; runbook and AGENTS.md
updated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
After each AI tick, every Collection Confirm card the page shows purple
(cardAiState verified) is confirmed without a person. Customer lines go
to ready for invoicing with the invoice number and date the AI read; stock
orders go to stock booking once every line is collected. Cards with a
problem flag, a photo for another PO, or no readable invoice number/date
stay for review.
Each push is recorded in ai_settings.collection_auto_push_log and shown at
the top of the page for today and yesterday (PO, supplier, items, value).
Off by default behind a Settings > AI switch; needs the manual live step
npm run pb:migrate:collection-auto-push before it will run.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Tested the handover against live PB on 2026-10-27. The read path works as
written; four defects fixed:
- The filter only caught item_status = 'assigned'. The collection_pool
tab group is [collection_pool, assigned, ready_delivery] and
order_items.confirm_collected accepts collection_pool or assigned, so
a line stuck in either was invisible to the sweep.
- Stock POs were not covered at all. Stock lines live in
stock_purchase_items and their photos on the parent
stock_purchases.additional_attachments.
- The write path hand-rolled order_items.update + update_status, which
skips the eligibility check, the over-collect guard, drv_collected_at
and the actor-stamped audit note. Now uses
order_items.confirm_collected / stock_purchase_items.mark_collected.
- No de-duplication: the NSS M24 line is still 'assigned' with a photo,
so every run would re-report it. Added a 'Waiting on a person' list.
Also noted that no cron runs this yet, that order_items_attachements is
multi-file (read every filename), and added a supplier check.
A hidden, installable PWA: one link, opened on a phone, showing where
every order is. New / Processing / Completed, with a tap-to-preview
popup and the invoice/line detail Gary needs. Read-only, and NOT in
the sidebar — the route lives outside `(app)`, so `<Sidebar />` never
renders there.
The preview popup is reused, not rebuilt: the service emits
`UniversalHit`s and the page hands them to the existing
`SearchPreviewDialog`, so the manager gets the desk's quick-overview
verbatim.
Also fixes the /orders Completed tab, which was matching ZERO orders.
`delivered_at` is stamped on only 2 of 12,957 delivered lines and the
business stopped writing `delivered` on 2026-08-14 (the live pipeline
now ends at `invoiced` / `checked_in`), so the old filter never
matched. "Settled" is now defined once — `SETTLED_STATUSES` +
`SETTLED_DATE_FIELDS` — and dated per status: `invoiced_date` for
invoiced (the only properly stamped field, 2,199 lines), `delivered_at`
for delivered, `updated` only where no dedicated column exists.
`delivery_note` and `checked_in` move into `completed` as instructed,
and the standalone `post_processing` tab is retired (a DN is not a
stage of its own). `invoiced` stays in `collected` as well, so
invoiced orders don't vanish from the workflow tabs.
Completed tab: 0 -> 341 orders over 30 days, 112 over 7.
The runbook and AGENTS.md described the old word-only shortlist and
did not mention the shared matcher. Both now say how the size is used,
that a price is written only for an exact match, and what
ai_fill.closest holds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rule-based matcher no longer writes or shows suggestions: on
2026-10-24 it had left 2,888 on unlinked orders, one quote suggested
to 309 of them.
- createOrder runs the AI order assistant (matchOrder) straight after
intake when it is switched on, instead of the rule-based suggestion
- order page: Re-match / Try again and the rule-based suggestion card
are gone; Match with AI is the action, and "Match by reference" saves
the reference then runs the AI (the run route takes { reference })
- the unauthenticated /orders/[id]/quote-match route is removed
- the sweep route and MCP orders.quote_match_sweep run the AI batch
(matchUnlinkedOrders, shared with the tick)
- scripts/2026-10-24-clear-rule-quote-suggestions.ts clears the old
suggestions (dry run unless --apply); replaces the rematch script,
which would have written rule-based ones again
- also fixes the setState-in-effect lint error on the order page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runbook now matches the code as built.
- New section: filling the lines from the quote — which lines may be
filled, what is written, the 5% price doubt, the purple review and
how it ends, and the board badge.
- The "Fill lines from the quote" switch is no longer described as
reserved for a later phase, and the tick's bounds now say what the
code does: 10 orders matched, 10 orders filled, and an order with
nothing to do does not use up that budget.
- Reading order_items.ai_fill is documented alongside orders.ai_quote_match.
- Live steps confirmed against the code: the migration is
pb:migrate:order-assistant, ai_quote_match and ai_fill are both in
IGNORED_FIELDS, the n8n tick is POST /api/v1/orders/assistant/tick,
and the settings labels are "Order assistant enabled", "Run every
(minutes)", "Auto-link when sure" and "Fill lines from the quote".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Task 7 of the AI order assistant plan.
- POST /api/v1/orders/assistant/tick (n8n, assertAgentAuth)
- POST /api/v1/orders/{id}/assistant/run (browser session; 409 for
"AI is disabled" and "not set up yet")
- MCP orders.assistant_match { orderId } -> matchOrder(force) with a
TOOL_HINTS entry; deliberately NOT in READ_TOOLS
- docs/order-assistant.md runbook and the AGENTS.md section
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The driver app records the full quantity as received when it collects a
stock pickup, and Collection Confirm only listed stock lines that were
not fully received — so collected stock orders skipped it and sat at
"sent", outside the accountant's booking queue, until someone clicked
Mark received (29 stuck on 2026-10-19).
- STOCK_ON_PAGE_FILTER / stockLineOnPage: a collected stock line stays on
Collection Confirm until its stock order is received; cancelled orders
never show. Shared by the list and the AI check (so those lines get
photos read and ticks too).
- Confirm stock: markStockOrdersReceived flips each stock order to
"received" once every line is collected/booked (→ booking queue);
orders with lines still out stay open, reported as stillOpen. The
route returns { updatedIds, received, stillOpen }; the toast says so.
- Stock photos: `npm run pb:migrate:stock-photos` makes
stock_purchases.additional_attachments multi-file;
addStockPurchaseAttachment, Move photo and Delete photo add/remove with
`+`/`-` instead of replacing the single slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Delete photo next to "Add supplier invoice" removes the photo open in
the previewer (wrong upload, or one of several for the same PO) after an
inline "Delete …? Yes, delete" confirm; the confirm resets when you
switch photos. Not offered for the PO PDF.
- POST /api/v1/collection-confirm/delete-photo (browser session) →
deleteCollectionPhoto: takes the file off every order line carrying it
(or the stock purchase's photo slot) and deletes the AI's reads of it
plus the checks built on them (shared forgetReads with Move photo).
- Removed the header's blue "Run agent scan" button and its results panel
(not in use); the /api/v1/agent/scan route stays for the agent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drivers photograph invoices per item and can't see which order a photo
lands on. B&R invoice 00282979 (stock order PO26100702STK) ended up on
the thinners line of PO202610017BLY; the AI read "PO26100702STK" off it
but the card only said "not found".
- The card list now carries each read's po_refs and source file, and sets
ai_doc.other_po when the document names a PO that exists and isn't the
row's PO (none named, or this card's PO among them: no hint).
- PO card: "Photo may be for <PO>" in the header; when open, "Invoice <no>
is for <PO>, not this PO" with a Move photo button. Nothing moves on its
own.
- POST /api/v1/collection-confirm/move-photo (browser session) →
movePhotoToPo: copies the photo to the target (a collected line of a
customer PO, or the stock purchase's single photo slot — refused when
taken), removes it from the source, deletes the AI's read of it and the
checks built on it so it is re-read where it now belongs.
- normalizePoReference moves to the client-safe src/lib/po-reference.ts
(re-exported from the matching service).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plumblink hands out delivery notes with prices on them. The Price tick
was always grey on a delivery note, so a card could go purple without
its prices ever being checked, and the AI's reading notes ("delivery
note, not a tax invoice", "year not printed") were never shown.
- Price tick: compare whenever the document line has a price, invoice
or delivery note; grey only when no price is printed. The AI still
only corrects a cost from a tax invoice.
- ai_doc now carries the read's notes; cardNotes() unions them per card
and the AI findings box lists them under "Reading notes" (the box now
shows when there are notes but no findings).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HYCO (I009753) prints full prices on the lines and "Discount 25.00%" in
the totals; Swagelok (622831) has a "Disc %" column on each line. The AI
compared our cost (already the discounted price) with the printed price,
so the Price tick went red on both.
- The read now captures a line's discount_percent and a whole-document
doc_discount_percent (prompt rule 5b) and stores net_unit_price on each
line (both discounts stacked).
- The Price tick compares the cost with the price after discount; the
tooltip and the document-lines panel spell it out
("R15.10 less 25% invoice discount = R11.33").
- Owner's call: when a "sure" tax-invoice line has a discount and our
cost isn't the discounted price, the AI sets the cost to it (customer
lines through updateOrderItemCost, which adds a cost note; stock lines
through updateStockPurchaseItem) and records why in ai_check.cost_changed,
shown in purple under the line. No discount, a delivery note or a
"likely" match: the cost is never touched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F2: tick table (SKU green via the code match incl. AI fills, grey per tick,
pairing failure -> all grey, different product -> red), stock lines use the
line SKU, Add to POS needs a SKU, SKU fills audited for order lines only.
F3: content_hash is a non-unique index (identical uploads share a hash,
skipped PDFs have an empty hash) - matches the migration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Runbook: pre-migration settings don't persist (not "saves fail"),
eligible statuses, 15 lines per quote, audit attribution, clone/template
behaviour, supplier kept, doubts dot, confidence guidance (Medium is the
default; raise to High if reps often change AI prices).
- quotes.assistant_fill added to docs/api-mcp-layer.md,
docs/audit/api-mcp-coverage.md and the Giellie skill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- decideWithAi throws on stop_reason max_tokens instead of returning a
partial answer; fillQuote marks a requested line the AI did not answer
as error (retried after 1 h), never a permanent no_match.
- QUOTE_MAX_LINES 30 -> 15; the tick also caps lines per quote so the
60-line budget reaches other quotes.
- runAssistantTick stops starting quotes after a 90 s time budget and
returns reason running while a newer start than the last summary is
under 10 minutes old (lease).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quote assistant card in Settings, AI (on/off, interval, min confidence,
editable prompt with reset, last-run line); parseSettingsForm handles the
qa_* fields. docs/quote-assistant.md has the runbook and the n8n tick
workflow spec; AGENTS.md gets a short section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>