# The sales desk

Three agents that each produce one document, and each stop when a fact is missing.

## Honesty first

These three are **staged, not running unattended**. They are defined here with their required facts, their
checks, and their escalation rules in place, and each one produces its document when a person runs it. None
of them runs on a schedule, watches an inbox, or sends anything to anybody. `meta.json` carries
`"status": "staged"` for that reason, and the widget says it in the first paragraph a visitor reads.

Everything is fictional: Apex Instruments, Northlight Group, the Meridian line, the Atlas plans, the people,
the prices, the ticket numbers, and the dates.

## What is in the folder

| File | What it is |
| --- | --- |
| `data/agents.json` | The three agents defined as data. The widget renders this, it does not restate it. |
| `proposal.html` | Six Letter pages. The document the proposal agent produces. |
| `account-brief.html` | One Letter page. The document the account brief agent produces. |
| `onboarding-plan.html` | Two Letter pages. The document the onboarding plan agent produces. |
| `widget.html` | The roster, then the gate demonstrated. One fragment, scoped to `.cap-sales-agents`. |
| `verify.mjs` | Playwright verification. 243 assertions. |
| `out/` | The rendered PDFs and page PNGs from `node collateral/render.mjs sales-agents`. |

## The three agents

Each entry in `data/agents.json` carries: what it is for, the document it produces and that document's
sections, the facts it must have before it may run, the optional facts and what each one changes, the checks
it runs on its own output, and the escalation rules that stop it.

The required lists are deliberately different, and each one includes at least one fact a salesperson cannot
answer from memory:

- **Proposal agent**, six required facts. The two it usually cannot get in the room are the **trailing twelve
  month spend** (finance operations, off the billing report, not the CRM opportunity) and the **approved
  pricing tier with its approver** (deal desk, a record with an id, not a verbal agreement).
- **Account brief agent**, five required facts. The hard ones are the **open support tickets with their open
  dates** (the support system, and the fact reps most often get wrong from memory) and the **contract end
  date** (the contract record, not the CRM opportunity, which is frequently stale).
- **Onboarding plan agent**, five required facts. The hard ones are the **contract signed date** (the
  countersigned copy, not the date the opportunity was marked closed) and the **agreed installation window**
  (operations scheduling, and a booking rather than an intention).

## The gate, and what the widget actually does

The second half of the widget is the point of the piece. It runs the proposal agent's gate for real against
the definition in `data/agents.json`:

1. Two of the six required fields ship empty. Pressing **Run the proposal agent** produces no document. It
   returns a stop with those two facts named, who normally holds each one, why it matters, and what the agent
   will do once it has them. Facts that are present are not listed.
2. **Try a price it will not accept** puts `$4,500` into the negotiated unit price for the Meridian Bench. The
   agent refuses it and says why: the Meridian Bench is `$4,850` on the price list effective October 1, 2026,
   `$4,500` is not a catalogue price, and a price outside the list needs a deal desk approval record. It is
   reported back rather than rounded to the nearest price the agent recognises.
3. A target start date on or before the run date is refused the same way, as is a site count outside 1 to 40.
4. **Fill the two missing facts** takes them from the record and the same button then lets the run through, to
   a summary of what it would produce and a link to the finished six page proposal.

Both stop reasons are reported together when both apply. The agent does not fix one, run, and then discover
the other.

### The arithmetic is real

The summary is not a canned paragraph. The three options are recomputed from the seven line catalogue in
`data/agents.json` every run, from the site count in the form:

- **Option A**: `n` Meridian Bench, Atlas Care on all of them, `n` calibrations, `ceil(n/3)` training days.
- **Option B**: `floor(n/3)` Meridian Pro on Atlas Care Plus and the rest Meridian Bench on Atlas Care, `n`
  calibrations, `ceil(n/2)` training days.
- **Option C**: `ceil(2n/3)` Pro and the rest Bench, all on Atlas Care Plus, plus `floor(n/2)` Field Kits on
  Atlas Care, `n + floor(n/2)` calibrations, `ceil(5n/6)` training days.

At six sites these produce `$37,888`, `$46,078` and `$63,822` for year one, which are exactly the figures
printed in `proposal.html`. Change the site count in the form and the totals move, and the panel says so.
`verify.mjs` asserts both: that the widget's six site run matches the printed document, and that a nine site
run reprices Option A correctly from the catalogue.

## The documents

Print rules per the collateral brief: `@page { size: letter; margin: 0 }`, one `.page` per physical page at
exactly 8.5in by 11in, `print-color-adjust: exact`, fonts loaded only from `/assets/fonts/fonts.css`, the
demo data comment on the first line, no en or em dashes.

- **`proposal.html`**, six pages: cover with the contents; the situation in three quoted lines from the
  September site reviews with where the account stands today; three options priced line by line; what is
  included and explicitly what is not; the schedule and the commercial terms; the acceptance page with the
  option boxes, four fields, and both signature blocks. Every unit price is a catalogue price and every total
  is the sum of its own lines. Verified by re-reading the rendered table: equipment subtotals, twelve months
  of service from the monthly figure, the year one totals, the recurring figures, and the three year totals.
- **`account-brief.html`**, one page: identity strip, the relationship in four dated lines, what they buy and
  what they do not, three open items each with a named owner and a due date, two risks, and one recommended
  next step.
- **`onboarding-plan.html`**, two pages: four phases across 60 days counted from the contract signed date,
  22 dated steps each with an owner at Apex and an owner at the partner and the artifact it produces, a done
  condition per phase, the three decision points with a decider and the artifact each decision is made
  against, and the escalation ladder.

The three documents are consistent with one another. The account brief's trailing twelve month figure of
`$15,992` is the number the proposal agent asks finance for, and the proposal's schedule and the onboarding
plan's phases sit on the same calendar.

## Judgement calls

- **Only the five named people.** The collateral brief allows exactly five fictional names, and they are all
  Apex staff. Rather than invent Northlight people, everyone on the partner side is referred to by role
  (regional operations lead, site managers, site readiness owner, billing contact). It reads correctly and it
  keeps every name inside the allowed set.
- **A pinned run date.** The gate validates dates against a fixed run date of October 12, 2026 held in
  `data/agents.json`, not against today. The form shows the run date next to the fields. Using the real clock
  would have made the sample drift: the prefilled target start date would eventually become a date in the
  past for reasons that had nothing to do with the demonstration.
- **The catalogue price rejection is the primary bad value.** A date in the past is also refused, but the
  catalogue check is the one the copy leads with because it does not depend on when the page is read.
- **Both stop reasons at once.** The result panel reports missing facts and refused values in the same run
  rather than one at a time, because that is what an agent that checks everything before writing would do.
- **The decision points became a table.** They started as three cards on page two of the onboarding plan and
  would not fit alongside the four phases and the ladder. A five column table carries the same content in
  about a third of the height.

## Three bugs worth recording, all the same shape

- A table row in `onboarding-plan.html` carried `class="gate"` to mark a decision point, and the page's own
  decision cards used an unscoped `.gate { display: flex; flex-direction: column }`. The row inherited it and
  turned into a vertical stack one character wide. The row class is now `dpt` and the cards are `dp`.
- `.page` is a flex column, so its children could shrink below their content and hide a genuine overflow from
  the renderer's check. `account-brief.html` was silently clipping the second risk while reporting no
  overflow. All three documents now carry `.page > * { flex: 0 0 auto }`, which makes an overflow visible to
  the check that is meant to catch it.
- On page four of the proposal, `.col li span { color: muted }` is more specific than `.tick { color: #fff }`,
  so the white checkmark inside each included item was being drawn in muted grey on a steel chip and was
  invisible. The same collision had already turned the recommended badge on page three from white to muted.
  Both are fixed with explicit `span.tick` and `span.badge` rules, and `verify.mjs` now asserts the computed
  colour of both so it cannot happen again quietly.

## Running it

```
cd rebuild
node collateral/render.mjs sales-agents     # PDFs and page PNGs into systems/sales-agents/out/
node systems/sales-agents/verify.mjs        # 243 assertions
node systems/sales-agents/verify.mjs --shots  # also writes widget-1440.png and widget-390.png
```

## What is not resolved

- The widget is verified against a local static server that maps `/assets/systems/sales-agents/` to this
  folder. If the build publishes the documents at a different path, the three links in the roster and the one
  in the run summary all resolve through the single `DOCBASE` constant at the top of the widget's script.
- The gate is real but it is one agent's gate. The account brief and onboarding plan agents are shown as
  definitions only, since three live forms on one page would bury the thing worth looking at.
