fix(dn): agent board shows only the open site question; Foreman skill lessons from run 1

- inbox_board: while a site question is open, blocking lists only that
  question (the reader's older doubts were misleading Foreman, e.g. the
  corrected FH Chamberlain order). next now points agents at
  prepare_delivery, and at the site manager for Ready-to-post slips;
  agents no longer book.
- Foreman skill: describe parked documents only from the open question;
  VERDICT counts only what is new this run (still-waiting items become a
  count line); quote refusals and their hint instead of guessing a cause;
  candidate_closed on regrouping means a document in the batch is set
  aside or posted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DEls 2026-10-09 18:42:18 +02:00
parent 009ac0e30f
commit e66ac520dc
3 changed files with 34 additions and 4 deletions

View file

@ -29,9 +29,12 @@ Work **one PO at a time**, and finish each slip before the next. Saving or booki
2. One complete document with other paper only in the background → `set_delivery_grouping` with that page as its own group, then `process_delivery_batch`. The re-read can take a few minutes. Come back to it at the end of the run.
3. Two complete documents on one photo → `split_or_crop_page` with one region per document (fractions 0–1 of the image, with a little margin). The crops become new uploads and the original is set aside. Note the new batch ids in the report.
4. A page that isn't a delivery document (blank, a PO, a quotation) → leave the grouping to the site with `request_site_decision`, or use `move_to_purchase_orders` for a PO photo (see C).
5. If `set_delivery_grouping` is refused with `candidate_closed`, a document in that batch is already set aside or posted, and grouping can't be changed any more. Check with `get_delivery_candidate`, leave the batch alone, and say so in one line. Don't guess another cause.
### B. Documents waiting for site (board `next` starts with "waiting for site")
- Leave them alone. The reviewer handles answered questions. Just list them in the report.
- Leave them alone. The reviewer handles answered questions.
- Describe them only from the open question (the board's `blocking` then shows just `site_decision: …`). The reader's original guesses in other checks are often misreads that were already corrected. For example, FH Chamberlain 07565729's order was first read as "MEDPO/09030" but is MEGCPO9430. Never repeat those guesses as facts.
- These are **not** new attention items. Count them on one line in the report (see Report).
### C. Every other open document
1. **Read the photo.** Note the document number, date, supplier, order number (PO), and for every line: description, printed quantity, unit, unit price, and the handwritten **Quantity Delivered**. "NIL", "0" or a dash means not delivered. A struck-out figure means read the corrected one. If a figure is truly unreadable, that line goes to the site.
@ -57,12 +60,16 @@ Work **one PO at a time**, and finish each slip before the next. Saving or booki
- `waiting_for_site`, `preview_stale`, `lines_not_planned` → follow the hint in the error.
8. **Double-check yourself** before moving on. `get_delivery_candidate` again and compare, line by line, the saved quantities (`reconciliation_preview.allocations[].quantity`, `.netAmount`) against the photo. Add up the nets: if the slip prints amounts, your total should match the slip's excl-VAT total within the price differences you noted. Anything that doesn't add up → undo by asking the site (`request_site_decision`) and say why in the report.
### When a write is refused
Quote the error `code` and `hint` in the report and follow the hint. Only state a cause you have checked in the app; if you don't know why, say "cause not checked" rather than explain it.
### D. Records to delete
- Never delete. If something should go (a wrong alias, a duplicate record), use `request_deletion` with the reason.
## Rules
- **Never book.** Do not call `post_delivery`, `post_delivery_line`, `correct_delivery_receipt`, `reassign_receipt` or `merge_suppliers`. If a posted receipt is wrong, put it in the report for the reviewer.
- Never accept a quantity over the PO, never guess an unreadable figure, never link a PO you aren't sure of.
- The board's `next` for a slip in Ready to post reads "site manager to check totals and Confirm delivery": that slip is done as far as you are concerned.
- Fine, no attention needed: slip price differs from the PO rate (book at the PO rate and note it); a slip line covered by a PO set line (kit).
- Ask the site (one clear question, with suggestions they can tap): missing PO, over the PO, unreadable quantity, delivered somewhere else, hire contracts, anything you'd be guessing.
- Every write needs a fresh `operationId` (a new UUID v4) and a short `reason`. If a call fails, read the error's `hint`. Retry once with the same operationId only for a timeout or lock.
@ -80,6 +87,10 @@ Work **one PO at a time**, and finish each slip before the next. Saving or booki
- A photo of a PO uploaded as a delivery document → `move_to_purchase_orders`.
## Report (in addition to Foreman's standard sections)
**VERDICT counts only what is new this run and needs a person:** questions you asked the site this run, posted receipts that look wrong, things you could not do, and anything for David. Documents that were already waiting for the site at the start of the run are not attention items, and neither are deletion flags raised in earlier runs. If nothing new, the verdict is `CLEAR` even when questions are still open. Put those on two lines under "## Summary":
- `Still waiting for site: <n> (oldest asked <date time>)`
- `Flagged for deletion, awaiting David: <n>`
Add a section **"## For the reviewer"** with one table row per document you touched or looked at:
| Site | Document | Supplier | PO | Where it ended (Ready to post / Waiting for site / Set aside / Untouched) | What you changed (tool: what) | Prepared total excl VAT | Photo check (quantities you read) | Confidence (high / medium / low) + why |

View file

@ -32,7 +32,7 @@ it('lists every open document with its blocking reasons and next action',async()
expect(board.documents.map(d=>d.id)).not.toContain(NOTE_D);
const byId=new Map(board.documents.map(d=>[d.id,d]));
expect(byId.get(NOTE_A)?.next).toBe('link supplier and PO (resolve_delivery_entities)');
expect(byId.get(NOTE_B)).toMatchObject({next:'post_delivery',purchaseOrderNumber:'PO-501',supplierName:'Avidan Steel',siteName:'Alpha Yard',lines:1,documentNumber:'DN-2'});
expect(byId.get(NOTE_B)).toMatchObject({next:'site manager to check totals and Confirm delivery',purchaseOrderNumber:'PO-501',supplierName:'Avidan Steel',siteName:'Alpha Yard',lines:1,documentNumber:'DN-2'});
expect(byId.get(NOTE_C)?.next).toBe('confirm document type or set aside (edit_document / set_aside_document)');
expect(byId.get(NOTE_C)?.lines).toBe(2);
expect(byId.get(NOTE_C)?.blocking).toHaveLength(1);
@ -57,10 +57,23 @@ it('leads the next action with the question the site still has to answer',async(
// An answered question stops leading the queue.
await pb.collection('dn_batch_notes').update(NOTE_B,{checks:[{id:'site_decision',passed:true,reason:'Answered: 6 m',detail:{answer:'6 m'}}]});
const after=await inboxBoard(c,{site:SITE_A});
expect(after.documents.find(d=>d.id===NOTE_B)!.next).toBe('post_delivery');
expect(after.documents.find(d=>d.id===NOTE_B)!.next).toBe('site manager to check totals and Confirm delivery');
// A matched slip that is not yet prepared points the agent at prepare_delivery, never at booking.
await pb.collection('dn_batch_notes').update(NOTE_B,{status:'blocked',stage:'needs_review'});
expect((await inboxBoard(c,{site:SITE_A})).documents.find(d=>d.id===NOTE_B)!.next).toBe('prepare_delivery (Ready to post for the site manager)');
expect(store('dn_batch_notes').size).toBe(5);
});
it('shows only the open site question as blocking, not the reader\'s older doubts',async()=>{
const {pb,ctx:c}=fixture();await seed(pb);
await pb.collection('dn_batch_notes').update(NOTE_B,{checks:[{id:'po_resolved',passed:false,reason:'customer order MEDPO/09030 not found'},{id:'site_decision',passed:false,reason:'Please upload PO MEGCPO9430',detail:{question:'Please upload PO MEGCPO9430'}}]});
const doc=(await inboxBoard(c,{site:SITE_A})).documents.find(d=>d.id===NOTE_B)!;
expect(doc.blocking).toEqual(['site_decision: Please upload PO MEGCPO9430']);
// Once answered, the remaining checks show again.
await pb.collection('dn_batch_notes').update(NOTE_B,{checks:[{id:'po_resolved',passed:false,reason:'customer order MEDPO/09030 not found'},{id:'site_decision',passed:true,reason:'Answered: uploaded',detail:{answer:'uploaded'}}]});
expect((await inboxBoard(c,{site:SITE_A})).documents.find(d=>d.id===NOTE_B)!.blocking).toEqual(['po_resolved: customer order MEDPO/09030 not found']);
});
it('filters to one site without touching the other',async()=>{
const {pb,ctx:c}=fixture();await seed(pb);
const board=await inboxBoard(c,{site:SITE_A});

View file

@ -12,6 +12,10 @@ const REASON_LIMIT=120;
/** A failing check is a sentence an agent can act on; a 200-character OCR dump is not. */
const blockingOf=(note:Record<string,unknown>):string[]=>{
const checks=Array.isArray(note.checks)?(note.checks as Check[]):[];
// While the site has the question, the reader's older doubts (often a misread
// the agent already corrected) only mislead whoever reads the board next.
const asked=openSiteDecision(note.checks);
if(asked)return [`site_decision: ${asked.question}`.slice(0,REASON_LIMIT)];
return checks.filter(check=>check.passed===false).map(check=>`${String(check.id??'')}: ${String(check.reason??'')}`.slice(0,REASON_LIMIT));
};
/** The first thing that would unblock this document, in the order a person would do it. */
@ -22,8 +26,10 @@ const nextAction=(note:Record<string,unknown>):string=>{
if(!note.supplier||!note.purchase_order)return 'link supplier and PO (resolve_delivery_entities)';
if(note.doc_type==='unknown')return 'confirm document type or set aside (edit_document / set_aside_document)';
if(!note.reconciliation_preview)return 'process_delivery_candidate';
// Agents prepare; the site manager books with Confirm delivery.
if(note.stage==='ready_to_post')return 'site manager to check totals and Confirm delivery';
const preview=note.reconciliation_preview as {complete?:boolean}|null;
return preview?.complete===true?'post_delivery':'fix quantities/links then post_delivery';
return preview?.complete===true?'prepare_delivery (Ready to post for the site manager)':'fix quantities/links then prepare_delivery';
};
/**
* One compact read of the whole receiving inbox: what is open, what is