Files
portal/skill/SKILL.md
Bendik Aagaard LynghaugandClaude Fable 5.1 96b3746643
Test / test (pull_request) Successful in 1m7s
One binary: the lint and the trainer are commands of portal
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
2026-09-30 15:29:30 +02:00

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

  1. Interview, then write the plan. Never pages first. plan reads a filled-in intake into plan.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 a sectors/ file when there is one.
  2. 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) with mail: { from } on the plan; moves_by: submitter so the person has something to play after sending; invites_by for onboarding; events: when rebuilding a live site, so its records keep replaying.
  3. 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.
  4. Build. build --plan ... --lint iris derives every file with no model deciding structure; --llm lets 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.
  5. Score. iris check --needs-tasks then answer, then --needs-score, with and without descriptions; rate --repo for 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.sh it 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.
  6. 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.