portal serve runs a site; portal lint is what iris was; portal plan, build, report and the rest are what portal-trainer was. One source at one version, so the three never disagree about what content means. Both histories are merged in (subtree). The release attaches the bare binary for a content repo's CI. The trainer's plans, intakes, engines and reference prompt come along; its runs stay in the old repo. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
6.7 KiB
name, description
| name | description |
|---|---|
| portal-trainer | Onboard a business onto portal - interview them, write a plan, build the content repo (pages, aggregates.yaml, desks, mails) with the words written by a model, and score it until iris proves every need is met and a blind visitor routes the personas right. Use when someone wants a new portal site, a new flow on an existing one, a rebuild of a live one, or asks why visitors pick the wrong alternative. |
Portal trainer
You are onboarding a business onto 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, with a mail to the person when their case moves.
You have a verifier. Use it instead of your judgment wherever it can
speak: iris check --path <repo>/questions (uhhm/iris, the release
matching the portal the site runs) exits non-zero until the structure
meets every need in <repo>/needs.yaml.
The order of work
- Interview, then write the plan. Never pages first.
planreads a filled-in intake intoplan.yaml(a model decides needs, stages, forms and budgets; a person reads the result before anything is built). By hand, the same questions: who comes to the business and what each of them wants, who inside handles it, what "handled" means (the states a case ends in), what it costs the visitor to ask (max_fields, from the person, never from counting the form you would like). Start from asectors/file when there is one. - Say what the plan can say.
does:(one or two features, one role each, the front door's opener that asks for nothing);shows:(things to look at before asking);doors:only when the offers are more than a page holds - measure it, a flat page with ten offers scored worse than four doors;mails:per need (who hears when a case enters a stage) withmail: { from }on the plan;moves_by: submitterso the person has something to play after sending;invites_byfor onboarding;events:when rebuilding a live site, so its records keep replaying. - Write personas in the visitor's words, not the business's. Two or three per need, one who would not use the trade's vocabulary. Never reuse a phrase you intend to put on a page. They are held out from every model that writes words.
- Build.
build --plan ... --lint irisderives every file with no model deciding structure;--llmlets a model write the words,--llm-for <path prefix>only some of them (the mails alone);--words-from <live repo>keeps a live site's hand-edited words through a rebuild, following each card across pages;--words <json>overrides slots. The builder refuses a reply in the wrong language and puts back a submitter's link a model dropped. - Score.
iris check --needs-tasksthenanswer, then--needs-score, with and without descriptions;rate --repofor readability and the four understanding questions, mails included. A card with no button is context, never a choice. Trust the descriptions score; the headings-only score swings by a third between seeds. Before believing a change,runs/spread.shit over three seeds and require it to beat the spread. A change that does not is rejected and kept as a run.- A misroute is a wording defect until the same one survives every rewording; then it is a seam in the plan.
- Change the page or the plan, never the persona.
- Keep the run.
runs/<business>-<date>/with the words as used (words.json), tasks, answers, scores per seed, and a README with what moved and why. Runs are the record of what worked.
What a page is
Full reference: portal's README.md; a live example is
uhhm/questions. The tree under questions/ is the router
(index.yaml is /, _section.yaml applies qualifies to a
directory, nested non-index files are followups).
# questions/index.yaml
name: What brings you here? # the question - always a question
responsible: { name: ..., contact: ... }
alternatives:
- name: What we do # the opener: features, no button
features:
- { name: <a role's win>, description: <what we do for them> }
- 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 }
# aggregates.yaml
aggregates:
- bucket: leads
initial: open
states:
open: { event: received, mail: { to: submitter, subject: "We have it", body: "..." } }
quoted: { event: quoted }
won: { event: won, mail: { to: submitter, subject: "...", body: "..." } }
lost: { event: lost }
transitions:
open: [quoted, lost]
quoted: [won, lost]
# site.yaml
mail: { from: "Nordvik <post@nordvik.example>", reply_to: ola@nordvik.example }
# 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
- { from: quoted, to: won, label: Won }
House rules for the copy
- The page name is a question the visitor could ask themselves.
- The front door opens modestly: what the business does for one or two roles, then what there is to look at, then the offers. No second tier.
- 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 use to answer.
- A followup page says who reads it, when they answer, and what the visitor can do meanwhile. A mail says what happened and, where the person has a move, the link to make it.
- Code in English; the site in the business's language.
Engines
Any engine that reads a task's state (body is the persona,
page what the page says before its options) and questions and
returns {"id","choice","confidence"} per line works. The visitor
and the writer must be different models. See the repo README for what
has been measured about each, and for the noise floor.