Files
portal/skill/SKILL.md
T
Bendik Aagaard LynghaugandClaude Fable 5.1 255aeb4f4b plan: intake to plan by a model; doors, labelled groups, the whole card
Work from another session, committed as found (tests green):

- `plan` reads an intake into a plan.yaml (a model decides needs,
  stages, forms and budgets; a person reads the result before build).
- Plans gain `doors` (a first question before the forms, when a front
  door would carry more than five needs), `groups` with labels that a
  select may draw its options from (`options_from: groups`), `private`
  fields kept off public pages, `multiple` selects, and `max_fields`
  as what the intake said the person will tolerate.
- `card.rs`: an alternative as the visitor meets it, in text, so the
  answering visitor and `rate` read the served card, not the heading.
- `rate` reads followup pages from the repo and reports "what happens
  next" as not measured rather than zero when it cannot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
2026-09-23 12:31:51 +02:00

6.3 KiB

name, description
name description
portal-trainer 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: 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

The short path, when a business has filled in an intake: plan reads it into a plan.yaml (a model decides needs, stages, forms and budgets; a person reads the result), build turns the plan into every file of the site with no model deciding anything, and a model writes only the words. See the repo README. The steps below are the same procedure done by hand, and what to do when the numbers come back.

  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. That number is max_fields, and it comes from the person, never from counting the form you would like to write. 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).

# 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 }
# 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]
# 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.