The trainer procedure as an agent skill, the engine contract, two
sector specs (software-studio: UHHM's own, green; construction: a
target with no pages yet), and the first wording run against uhhm.no.
The verifier itself is portal's question_lint (uhhm/portal 188f9bf).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
137 lines
5.7 KiB
Markdown
137 lines
5.7 KiB
Markdown
---
|
|
name: portal-trainer
|
|
description: Onboard a business onto portal - interview them, write their needs.yaml, then write the content repo (pages, aggregates.yaml, desks) until question_lint proves every need is met and an answering engine routes the personas right. Use when someone wants a new portal site, a new flow on an existing one, or asks why visitors pick the wrong alternative.
|
|
---
|
|
|
|
# Portal trainer
|
|
|
|
You are onboarding a business onto [portal](https://project.uhhm.no/uhhm/portal):
|
|
a site that is one question per page, a few alternatives per question,
|
|
each alternative optionally a form whose answer is recorded into a
|
|
bucket with a state machine, worked from a group-gated desk.
|
|
|
|
You have a verifier. Use it instead of your judgment wherever it can
|
|
speak: `question_lint --path <repo>/questions` exits non-zero until
|
|
the structure meets every need in `<repo>/needs.yaml`.
|
|
|
|
## The order of work
|
|
|
|
1. **Interview, then write `needs.yaml` first.** Never pages first.
|
|
Ask who comes to the business and what each of them wants, who in
|
|
the business handles it, and what "handled" means - the states a
|
|
case ends in. Ask what it costs the visitor to ask: how many
|
|
submissions, how many required fields they will tolerate. If the
|
|
business is in a sector under `sectors/`, start from that file and
|
|
cut it down with them; do not start blank.
|
|
2. **Write personas in the visitor's words, not the business's.** Two
|
|
or three per need, including one who would not use the trade's own
|
|
vocabulary. Never reuse a phrase you intend to put on a page - a
|
|
persona that quotes the page tests nothing.
|
|
3. **Run the lint. It fails. Read every line.** Each failure names
|
|
what is missing: no path, a closed (`disabled`) path, a field budget
|
|
blown, no desk held by the handling group, a finished state no desk
|
|
move reaches, a state records strand in.
|
|
4. **Write the least content that turns it green**: `aggregates.yaml`
|
|
(one aggregate per bucket, states the business named, only the
|
|
transitions they described), the visitor pages, one followup page
|
|
per recording alternative saying what happens next, and one desk
|
|
page per handling group with exactly the moves the state machine
|
|
allows. Mark a need `planned: true` only when the business says
|
|
"later" - it keeps warning.
|
|
5. **Then word it.** `question_lint ... --needs-tasks tasks.jsonl`,
|
|
and again with `--skim` (headings only - most visitors read nothing
|
|
else). Give the tasks to an answering engine that never sees
|
|
`expect`; grade with `--needs-score answers.jsonl`.
|
|
- A **misroute** is a wording defect. Fix the heading before the
|
|
description: a heading that needs its description has failed.
|
|
- Two alternatives a persona confuses are usually one distinction
|
|
the business makes and the visitor does not. Name the visitor's
|
|
side of it.
|
|
- **Hesitant** (right, under 0.6) means it works and is still a coin
|
|
flip. Same treatment.
|
|
- Change the page, never the persona, to make a score go up.
|
|
6. **Keep the run.** Copy tasks, answers and the score into
|
|
`runs/<business>-<date>/` with a line on which engine answered.
|
|
Runs are the record of which wording worked, per sector.
|
|
|
|
## What a page is
|
|
|
|
Full reference: portal's `README.md` and
|
|
`docs/design/filesystem-routes.md`; a live example is
|
|
`uhhm/questions`. The file tree under `questions/` is the router
|
|
(`index.yaml` is `/`, `_section.yaml` applies `qualifies` to a
|
|
directory, nested non-index files are followups).
|
|
|
|
```yaml
|
|
# questions/index.yaml
|
|
name: What brings you here? # the question - always a question
|
|
responsible: { name: ..., contact: ... }
|
|
alternatives:
|
|
- name: <heading the visitor picks> # graded alone under --skim
|
|
description: <one or two sentences>
|
|
action: /received # where the submission lands them
|
|
consequence: [Send] # the button
|
|
record_as: leads # bucket - must exist in aggregates.yaml
|
|
features:
|
|
- name: <form section>
|
|
requirements:
|
|
- { name: email, label: Email, type: email }
|
|
- { name: problem, label: What's wrong, type: textarea }
|
|
- { name: address, label: Address, optional: true }
|
|
```
|
|
|
|
```yaml
|
|
# aggregates.yaml
|
|
aggregates:
|
|
- bucket: leads
|
|
initial: open
|
|
states:
|
|
open: { event: received }
|
|
quoted: { event: quoted }
|
|
won: { event: won }
|
|
lost: { event: lost }
|
|
transitions:
|
|
open: [quoted, lost]
|
|
quoted: [won, lost]
|
|
```
|
|
|
|
```yaml
|
|
# questions/desk/_section.yaml
|
|
qualifies: project_managers
|
|
# questions/desk/index.yaml
|
|
name: What came in?
|
|
alternatives:
|
|
- name: Leads
|
|
action: /desk
|
|
consequence: [Confirm]
|
|
features:
|
|
- name: ""
|
|
resource:
|
|
source: { kind: kv, bucket: leads }
|
|
requires_group: project_managers
|
|
transitions:
|
|
- { to: quoted, label: Quote sent } # from defaults to open
|
|
- { to: lost, label: Not for us }
|
|
- { from: quoted, to: won, label: Won }
|
|
- { from: quoted, to: lost, label: Lost }
|
|
```
|
|
|
|
## House rules for the copy
|
|
|
|
- The page name is a question the visitor could ask themselves.
|
|
- Headings say what the visitor gets, in the visitor's words. Lead
|
|
with what a thing enables; never define it by what it isn't.
|
|
- Every required field must be one the business will actually use to
|
|
answer. If they can't say what they do with it, it's optional or
|
|
gone.
|
|
- A followup page says who reads it, when they answer, and what the
|
|
visitor can do meanwhile.
|
|
|
|
## Engines
|
|
|
|
Any engine that reads a task's `state` and `questions` and returns
|
|
`{"id","choice","confidence"}` per line works. Compare at least two
|
|
before trusting a score, and re-run the same engine before trusting a
|
|
change: one run of eight tasks is an anecdote. See the repo README for
|
|
what has been measured about each.
|