supply-chain-fulfil
Move a labs contract's goods and money: shipment, receipt, invoice, payment, confirmation. Needs contracts.
SKILL.mdskills/supply-chain-fulfil/SKILL.md
GitHubSupply-chain fulfil
Takes the contracts supply-chain-procure signed and records what happens next: the supplier ships,
the central store receives (the event that creates stock), the supplier invoices, ACE pays what
arrived, and the supplier confirms the money came. Labs' three-way match (contract_match) is the
authority on what is owed, not ACE's arithmetic.
Platform reference: playbook/integrations/connect-labs-supply-chain.md
§ Logistics and § Finance. In /ace:run this is part of the optional supply_chain step (spec
docs/superpowers/specs/2026-10-18-optional-labs-e2e-steps.md § F4), run once in week 0 of the
backdated calendar, after procurement.
Inputs
| Parameter | Used for | Resolver |
|---|---|---|
program_id |
the labs programme | explicit arg; default: run_state phases.execution-management.products.supply_chain.program_id |
contract_ids |
the orders to fulfil | explicit arg; default: run_state products.supply_chain.contract_ids |
central_supply_point_id |
where goods arrive | explicit arg; default: run_state products.supply_chain.supply_points.central |
scope |
the source.ref scope |
the run id inside /ace:run; outside one, programScope(program_id) (lib/supply-chain-ref.ts) |
simulated |
invent the supplier's side, label every write, backdate | true when Phase 9 mode: simulated; explicit arg otherwise |
sim_week0_start |
the Monday fulfilment is dated in | run_state products.simulation.calendar.weeks[0].start (lib/sim-calendar.ts, PR10); explicit arg otherwise |
No run_state.yaml is needed when every parameter is passed. Run-state write-back (Step 9) happens
only when a run is bound.
Pipeline-only in one respect: on the simulated track ACE invents the supplier's dispatch, its
invoice (including one deliberate over-billing) and its payment confirmation. Outside a simulation
each of those is recorded only when the supplier actually sends it, with source set to who told
you.
Products
9-execution-manager/supply-chain-fulfil_plan.yaml: per contract, the shipment, the receipt (accepted / rejected, with the planned partial rejection), the invoice (with the planned over-billing), and the payments, written before any write.9-execution-manager/supply-chain-fulfil_record.md: shipment, receipt, invoice and payment ids, each contract's three-way match as labs computed it after each stage, and the dispute and its resolution.- run_state
phases.execution-management.products.supply_chain(inside a run): addsshipment_ids,receipt_ids,invoice_ids,payment_ids.
Process
- Precondition.
supply_chain_programme_runner_getmust sayprogramme(oroperator). Read each contract (supply_chain_contract_get) and itssupply_chain_contract_match; a contract alreadyfully_receivedis skipped, not re-received. - Plan (Products, first file). On the simulated track, across the contracts:
- one partial rejection: one receipt line rejects a small quantity (a few damaged units) with
a
rejection_reason, to exercise the loss path; - one over-billing: one invoice bills
quantity_billedabove what was accepted, so the three-way match flags it and Step 6 disputes it; - one advance: the contract procure placed on
payment_terms: advanceis paid before shipment.
- one partial rejection: one receipt line rejects a small quantity (a few damaged units) with
a
- Advance (advance-terms contracts only, before shipping).
supply_chain_payment_record({program_id, data: {contract_id, amount, currency, paid_on, method, source: "we_recorded"}, source})for the agreed advance. The invoice later names it inacknowledges_payment_ids. - Ship.
supply_chain_shipment_record({program_id, data: {contract_id, lines: [{item_id, quantity, quantity_unit, batch, expiry}], dispatched_on, expected_on, carrier, status: "dispatched", source}, source})withdata.source=supplier_reported(the supplier told us). It posts nothing to stock. When it arrives:supply_chain_shipment_update({shipment_id, data: {status: "delivered"}}). - Receive.
supply_chain_receipt_record({program_id, data: {contract_id, shipment_id, supply_point_id: central, received_on, lines: [{item_id, batch, expiry, quantity_accepted, quantity_rejected, rejection_reason, quantity_unit}], source: "we_recorded"}, source}). This is the event that creates stock: each accepted line posts a movement, and a rejected quantity stays on the line and never enters the ledger. Batch and expiry match the shipment's. Then readsupply_chain_contract_matchinto the record. - Invoice.
supply_chain_invoice_record({program_id, data: {contract_id, reference, issued_on, quantity_billed, quantity_unit, unit_price, amount, currency, acknowledges_payment_ids?, source: "supplier_reported"}, source}). Giveunit_priceandquantity_billedas billed; never correct them on entry. Readcontract_matchagain: on the planned over-billing,over_invoicedis positive andchecks_listcarriesinvoice_over_billed. Dispute it:supply_chain_invoice_dispute({invoice_id, reason, disputed_on, source}). It marks the invoicequeriedand sends nothing to the supplier. On the simulated track the supplier's corrected bill arrives a day later:supply_chain_invoice_update({invoice_id, data: {quantity_billed: <accepted>, amount, status: "approved"}, source}), withsource.senderthe supplier and the excerpt quoting the (simulated) correction. - Pay what arrived. For each invoice,
supply_chain_payment_record({program_id, data: {invoice_id, amount, currency, paid_on, method, source: "we_recorded"}, source}).amount=contract_match.payable_nowas labs returns it, never the billed amount, and never minus the advance:payable_nowis already net of every payment recorded, the advance included. Live, labs programme 361 contract 323 (814 received at UGX 41,500, an advance of 10,000,000) readpayable_now23,781,000 = 33,781,000 − 10,000,000; subtracting the advance again would have paid 13,781,000 (ace#3260). Once paid,payable_nowreads 0. An invoice's status follows from what has been paid; never set it topaidby hand. - Confirm.
supply_chain_payment_confirm({payment_id, confirmed_on, source})for every payment,confirmed_oninside week 0 and not beforepaid_on. A backdated payment is already more than a fortnight old in real time, so an unconfirmed one shows inchecks_listaspayment_unconfirmedat once. - Record. Write the record file, then, inside a run, the ids into
products.supply_chainwithupdate_yaml_fileandmerge: 'deep', resending each id array whole (deep merge replaces arrays). - Hand to QA. Dispatch
supply-chain-fulfil-qa. Do not mark the step done on the write responses alone.
Every write carries source = buildSource({scope, skill: "supply-chain-fulfil", step, key, sender, sentOn, simulated, today})
from lib/supply-chain-ref.ts. The key is <contract_id> (plus :advance, :correction, the
payment's invoice id), so a re-run replays instead of writing twice. Dates are
simulatedDate(sim_week0_start, n) in order: advance, dispatch, delivery, receipt, invoice,
dispute, correction, payment, confirmation.
MCP Tools Used
- connect-labs:
supply_chain_programme_runner_get,supply_chain_contract_get,supply_chain_contract_match,supply_chain_shipment_record,supply_chain_shipment_update,supply_chain_receipt_record,supply_chain_invoice_record,supply_chain_invoice_dispute,supply_chain_invoice_update,supply_chain_payment_record,supply_chain_payment_confirm,supply_chain_checks_list - ace-gdrive:
drive_create_file(plan + record),update_yaml_file(run write-back only)
Existing objects
- Nothing here can be deleted (spec § F6): not a shipment, receipt, invoice or payment. A wrong
receipt is corrected with a stock movement (
supply_chain_movement_record), a wrong invoice withinvoice_updateor a rejection, an overpayment is recovered against the next invoice. Never record a second receipt for goods already received. - A receipt is stock. Recording one against a live programme changes what every stock read, forecast and resupply plan says. On a real programme, receive only what the store actually counted in.
- A dispute sends nothing. Telling a real supplier is a separate, human-approved email.
- Never run against a programme ACE does not own (§ F6).
Mode Behavior
- Auto: Steps 1-10.
- Review: shows the Step 2 plan and waits before Step 3.
Dry-Run Behavior
When --dry-run is active, run Step 1's reads and Step 2. Write the plan. Make no
supply_chain_* write.
Change Log
| Date | Change | Author |
|---|---|---|
| 2026-10-20 | Step 7 pays payable_now as returned: it is already net of the advance and every payment recorded (ace#3260) |
ACE |
| 2026-10-18 | Initial version (optional-track PR8): advance, shipment, receipt (stock), invoice with dispute and correction, payment of what arrived, confirmation | ACE |