Engel’s notebook

Model router (meta-LLM profiles)

F12 · 25 mapped behaviors in the Self-Healing Codebase Project. Follow the prerequisites, actions and expected results; expand the original CLI recipe to replay it.

778 behavior IDs · 28 families · 844 named recipes & support steps
16 October 2026 · PR #18334 at 9a3c3a69
Canvas 1.27.1 · Agent Server 1.55.0 · automation 1.20.0

This is the map at the revision above, including its open bug notes. It does not incorporate later changes on main or claim a fresh verification run. Setup & reading guide · Download the scenario inventory

All scenarios & marker key · Open a script to read Given / Do / Expect. Expand the exact recipe for replayable commands.

F12 Model router (meta-LLM profiles)

25 behaviors · 28 scripts & contracts · Maintained source

Entry points & prerequisites

What this family covers

A model router (the UI also calls it a "meta-profile") names a classifier LLM profile, a prompt template and a model table. While a router is active, every new conversation gets a route_task_to_model tool: the classifier picks one saved LLM profile from the table and the agent switches to it (the chat shows Switched to profile <name>). With Run on first message on, the agent is told to route the first user message before doing anything else. Users create routers from the Router Pro / Router Flash templates or from scratch, can have the missing router LLM profiles created from a provider connection, and activate, edit and delete routers under Settings → Model Router.

How a user gets here

  • Sidebar settings gear (backend-selector-settings-link), then Model Router in the settings navigation (sidebar-settings-/settings/meta-llm, between LLM and Condenser). Used by F12.page.
  • Direct URL /settings/meta-llm. Used by most recipes (browser goto).
  • Command menu (Control+k or Meta+k, or command-menu-trigger in the sidebar): search "Model Router" (or "router", "meta"), choose Model Router ("Meta-profiles that route each task to the right LLM profile."). Used by F12.page.
  • Per-router actions: the row ... button (meta-profile-menu-trigger-<name>), then Edit / Set active / Delete.
  • The effect of a router is seen in a conversation started from the home composer (F12.route-first-message; see F05/F06 for the composer and activity rendering).

Given

Preconditions:

  • F12.empty-states needs a fresh launch --new with onboard --skip done and before llm preset deepseek (no LLM profiles, no routers).
  • Everything after it needs control-openhands llm preset deepseek --api-key-file <key file> (profiles deepseek-flash and deepseek-pro), no meta-profile named QA_* (control-openhands api GET /api/meta-profiles), no provider connection (control-openhands api GET /api/llm/provider-connections is []) and no LLM profile named deepseek-chat (control-openhands api GET /api/profiles).
  • F12.add-connection and F12.route-first-message use the DeepSeek key file; never print it.
  • Blocked here: F12.unsupported (needs an agent server older than software-agent-sdk #3744, for example launch --new --sdk-ref <older release>), F12.cloud (needs a Cloud account, with and without an organization) and F12.load-error (needs /api/meta-profiles to fail while the rest of the agent server answers; service stop agent-server swaps the whole app for the Manage backends card instead).
  • Desktop viewport unless a bullet says otherwise.
Open from navigationF12.pageMapped behavior

Scenario F12--open-from-navigation · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Settings”.
  2. Do
    Choose “Meta llm settings”.

Expect

Settings → Model Router opens /settings/meta-llm with the subtitle Meta-profiles that route each task to the right LLM profile.; the command menu offers a Model Router command that opens it, like the other settings pages.

Exact CLI recipe & bug checks

From / run control-openhands browser click 'testid=backend-selector-settings-link' --expect-url '/settings', control-openhands browser click 'testid=sidebar-settings-/settings/meta-llm' --expect-url '/settings/meta-llm(\?|$)', control-openhands browser text 'testid=settings-page-subtitle' and control-openhands browser attr 'testid=sidebar-settings-/settings/meta-llm' aria-current. The subtitle is Meta-profiles that route each task to the right LLM profile. and aria-current is page.

Command menuF12.pageMapped behavior

Scenario F12--command-menu · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Open /.
  2. Do
    Press Control+k.
  3. Do
    Enter “Model Router” in “Command menu” / the combobox.
  4. Do
    Enter “router” in “Command menu” / the combobox.
  5. Do
    Press Enter.
  6. Wait
    Wait for the element identified in the CLI recipe to appear.

Expect

Settings → Model Router opens /settings/meta-llm with the subtitle Meta-profiles that route each task to the right LLM profile.; the command menu offers a Model Router command that opens it, like the other settings pages.

Exact CLI recipe & bug checks

Run control-openhands browser goto / (the previous bullet ends on /settings/meta-llm, where the URL wait would pass at once), control-openhands browser press Control+k, control-openhands browser type 'testid=command-menu >> role=combobox' 'Model Router' and control-openhands browser snapshot 'testid=command-menu': the only option is Model Router Meta-profiles that route each task to the right LLM profile. Go [selected]. control-openhands browser fill 'testid=command-menu >> role=combobox' router (and then meta) keeps that single option (control-openhands browser count 'testid=command-menu >> role=option' is 1). Run control-openhands browser press Enter and control-openhands browser wait-url '/settings/meta-llm(\?|$)'; browser text 'testid=settings-page-subtitle' is the subtitle above.

Empty statesF12.empty-statesMapped behavior

Scenario F12--empty-states · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Open /settings/meta-llm.

Expect

with no LLM profiles the page says Create LLM profiles first — meta-profiles route between your saved LLM profiles.; with no routers it says No meta-profiles yet. Add one to route tasks across your LLM profiles. and Run on first message is off and disabled.

Exact CLI recipe & bug checks

On the fresh run run control-openhands browser goto /settings/meta-llm, control-openhands browser text 'testid=meta-profile-no-llm-profiles', control-openhands browser text 'testid=meta-profile-empty' and control-openhands browser eval "(()=>{const i=document.querySelector('[data-testid=meta-profile-run-at-conversation-start-switch]');return {checked:i.checked,disabled:i.disabled}})()". The texts are the two messages in Sub-features and the switch is {checked:false, disabled:true}. control-openhands browser screenshot --feature F12.empty-states --name no-llm-profiles shows both messages above a greyed switch.

Template chooserF12.template-chooserMapped behavior

Scenario F12--template-chooser · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Add meta profile”.
  2. Do
    Choose “Meta profile template cancel”.
  3. Do
    Press Escape.
  4. Do
    Choose “Meta profile template router pro”.
  5. Do
    Choose “Meta profile cancel”.
  6. Do
    Use browser mouse click as specified in the exact recipe.

Expect

Add Model Router opens Choose Model Router template (Add OpenHands Router Pro, Add OpenHands Router Flash, Custom, Cancel); Cancel, Escape and a backdrop click close it; Pro and Flash prefill the name, classifier minimax-m3, their own long routing prompt and the same 11-model table.

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=add-meta-profile' and control-openhands browser snapshot 'role=dialog': dialog Choose Model Router template with buttons Add OpenHands Router Pro, Add OpenHands Router Flash, Custom, Cancel. control-openhands browser click 'testid=meta-profile-template-cancel' then control-openhands browser count 'testid=meta-profile-template-modal' is 0; reopen and control-openhands browser press Escape gives 0 too. Reopen, control-openhands browser click 'testid=meta-profile-template-router-pro', then control-openhands browser value 'testid=meta-profile-name-input' (openhands-router-pro), control-openhands browser value 'testid=meta-profile-classifier-input' (minimax-m3), control-openhands browser eval "document.querySelector('[data-testid=meta-profile-model-table]').value.split('\n').length" (11), control-openhands browser eval "document.querySelector('[data-testid=meta-profile-prompt-template]').value.includes('{{ instance_text }}')" (true), control-openhands browser value 'testid=meta-profile-router-connection' (Don't create profiles while no connection exists) and control-openhands browser enabled 'testid=meta-profile-save' (true). Leave with control-openhands browser click 'testid=meta-profile-cancel'. The Flash template (testid=meta-profile-template-router-flash) gives openhands-router-flash, the same classifier and identical table, and a different prompt (about 5,900 characters against Pro's 5,000; compare with browser eval "document.querySelector('[data-testid=meta-profile-prompt-template]').value.length"). A backdrop click also closes the chooser: reopen it and control-openhands browser mouse-click 20 980, then count is 0.

Custom templateF12.template-customMapped behavior

Scenario F12--custom-template · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Add meta profile”.
  2. Do
    Choose “Meta profile template custom”.

Expect

Custom opens a blank editor; the placeholders show an example prompt and a two-line example model table.

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=add-meta-profile', control-openhands browser click 'testid=meta-profile-template-custom', then control-openhands browser value on testid=meta-profile-name-input, testid=meta-profile-classifier-input, testid=meta-profile-prompt-template and testid=meta-profile-model-table (all ""), control-openhands browser enabled 'testid=meta-profile-save' (false) and control-openhands browser eval "document.querySelector('[data-testid=meta-profile-model-table]').placeholder.split('\n')": two example rows, ["- GPT-5.4: strong for coding and reasoning", "- MiniMax-M3: efficient fallback"], joined by a real line break (#18035), and control-openhands browser screenshot --feature F12.template-custom --name custom-empty shows them on two lines under the prompt placeholder, which breaks lines the same way.

ValidationF12.editor-validationMapped behavior

Scenario F12--validation · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Enter “QA router!” in “Meta profile name”.
  2. Do
    Enter “qa-classifier” in “Meta profile classifier”.
  3. Do
    Enter “Pick a model for: {{ instance_text }}” in “Meta profile prompt template”.
  4. Do
    Enter “QA_router” in “Meta profile name”.
  5. Do
    Enter “Pick a model for: {{ task }}” in “Meta profile prompt template”.
  6. Do
    Enter “” in “Meta profile classifier”.

Expect

Save stays disabled until the name is valid, a classifier is set and the prompt contains {{ instance_text }} (spaces inside the braces optional).

Exact CLI recipe & bug checks

In that Custom editor run control-openhands browser fill 'testid=meta-profile-name-input' 'QA router!', control-openhands browser fill 'testid=meta-profile-classifier-input' qa-classifier, control-openhands browser fill 'testid=meta-profile-prompt-template' 'Pick a model for: {{ instance_text }}' and control-openhands browser enabled 'testid=meta-profile-save' (false: bad name). control-openhands browser fill 'testid=meta-profile-name-input' QA_router turns it true; control-openhands browser fill 'testid=meta-profile-prompt-template' 'Pick a model for: {{ task }}' turns it false; 'Pick a model for: {{instance_text}}' turns it true again; control-openhands browser fill 'testid=meta-profile-classifier-input' '' turns it false. Keep the editor open.

Add a connectionF12.add-connectionMapped behavior

Scenario F12--add-a-connection · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Meta profile add provider connection”.
  2. Do
    Enter “QA_deepseek” in “Provider connection name”.
  3. Do
    Enter “deepseek” in “Provider connection provider”.
  4. Do
    Choose “deepseek”.
  5. Do
    Enter “the value specified in the recipe” in “Provider connection api key”.
  6. Do
    Choose “Provider connection submit”.
  7. Wait
    Wait for “Provider connection modal” to disappear.

Expect

Add connection in the create editor opens Add provider connection; saving it selects the new connection in the picker.

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=meta-profile-add-provider-connection', control-openhands browser fill 'testid=provider-connection-name-input' QA_deepseek, control-openhands browser fill 'testid=provider-connection-provider-input' deepseek, control-openhands browser click 'role=option[name="deepseek"]', control-openhands browser fill 'testid=provider-connection-api-key-input' --value-file <key file>, control-openhands browser click 'testid=provider-connection-submit' and control-openhands browser wait 'testid=provider-connection-modal' --state detached. control-openhands browser toasts has Connection "QA_deepseek" created and control-openhands browser value 'testid=meta-profile-router-connection' is QA_deepseek (deepseek).

Create the first routerF12.create-first · F12.router-profilesKnown issue

Scenario F12--create-the-first-router · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Meta profile classifier”.
  2. Do
    Choose “deepseek-flash”.
  3. Do
    Enter “QA_router” in “Meta profile name”.
  4. Do
    Enter “the value specified in the recipe” in “Meta profile prompt template”.
  5. Do
    Enter “the value specified in the recipe” in “Meta profile model table”.
  6. Do
    Choose “Meta profile save”.
  7. Wait
    Wait for the element identified in the CLI recipe to appear.
  8. Do
    Reload the page.

Expect

saving the first router lists it, makes it Active and turns Run on first message on.

with a connection picked under Create missing router LLM profiles using, Save creates an LLM profile <provider>/<name> linked to that connection for every model-table name (and the classifier) that does not exist yet; Don't create profiles creates none.

Linked known issues

Known issue OpenHands/software-agent-sdk#5878: Router files can be written under a literal ~ directory when Agent Server was configured with leading-tilde OH_PERSISTENCE_DIR. Blocked on the normal absolute-path launcher fixture: needs a disposable server with private HOME/cwd and literal ~/... or relative persistence configuration, then a router save and filesystem readback. Never point it at user state. (F12.create-first, F12.list).

Exact CLI recipe & bug checks

Write the prompt and table to files: printf 'You route tasks. Reply only with JSON {"model": "<name>", "reason": "<short>"} using one name from this table:\n{{ model_table }}\n\nTask:\n{{ instance_text }}' > <prompt file> and printf -- '- deepseek-flash: cheap, for simple tasks\n- deepseek-pro: stronger, for hard tasks\n- deepseek-chat: general chat' > <table file>. Run control-openhands browser click 'testid=meta-profile-classifier-input', control-openhands browser click 'role=option[name="deepseek-flash"]', control-openhands browser fill 'testid=meta-profile-name-input' QA_router, control-openhands browser fill 'testid=meta-profile-prompt-template' --value-file <prompt file>, control-openhands browser fill 'testid=meta-profile-model-table' --value-file <table file>, control-openhands browser click 'testid=meta-profile-save' and control-openhands browser wait-text 'Meta-profile "QA_router" saved'. After control-openhands browser reload: control-openhands browser text 'testid=meta-profile-row-QA_router' is QA_router\ndeepseek-flash · Direct prompt\nActive, the switch eval above returns {checked:true, disabled:false}, control-openhands api GET /api/meta-profiles has active_meta_profile QA_router, control-openhands api GET /api/settings has "run_router_at_conversation_start": true, and control-openhands api GET /api/profiles has a new deepseek-chat (deepseek/deepseek-chat, provider_connection_id set) while deepseek-flash/deepseek-pro are unchanged. control-openhands browser screenshot --feature F12.create-first --name list-after-create shows the row with the gold Active badge and the switch on.

Duplicate nameF12.duplicate-nameMapped behavior

Scenario F12--duplicate-name · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Add meta profile”.
  2. Do
    Choose “Meta profile template custom”.
  3. Do
    Enter “QA_router” in “Meta profile name”.
  4. Do
    Enter “QA_router_2” in “Meta profile name”.

Expect

a new router cannot take an existing router's name (A meta-profile with this name already exists..., Save disabled).

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=add-meta-profile', control-openhands browser click 'testid=meta-profile-template-custom', control-openhands browser fill 'testid=meta-profile-name-input' QA_router, control-openhands browser text 'testid=meta-profile-name-taken' (A meta-profile with this name already exists. Choose a different name, or edit the existing one.), pick classifier deepseek-pro as above, fill the prompt with 'Route: {{ instance_text }}', and control-openhands browser enabled 'testid=meta-profile-save' (false). control-openhands browser fill 'testid=meta-profile-name-input' QA_router_2 makes control-openhands browser count 'testid=meta-profile-name-taken' 0 and Save true. Click testid=meta-profile-cancel; control-openhands browser count 'testid=meta-profile-row-QA_router_2' is 0.

"Don't create profiles" in a templateF12.router-profilesMapped behavior

Scenario F12--don-t-create-profiles-in-a-template · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Add meta profile”.
  2. Do
    Choose “Meta profile template router pro”.
  3. Do
    Choose “Meta profile router connection”.
  4. Do
    Choose “Don't create profiles”.
  5. Do
    Choose “Meta profile cancel”.

Expect

with a connection picked under Create missing router LLM profiles using, Save creates an LLM profile <provider>/<name> linked to that connection for every model-table name (and the classifier) that does not exist yet; Don't create profiles creates none.

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=add-meta-profile', control-openhands browser click 'testid=meta-profile-template-router-pro', control-openhands browser value 'testid=meta-profile-router-connection' (QA_deepseek (deepseek), preselected), control-openhands browser click 'testid=meta-profile-router-connection' and control-openhands browser click "role=option[name=\"Don't create profiles\"]" --observe 'testid=meta-profile-router-connection' --observe-ms 1500. The picker stays on Don't create profiles: the observed states are QA_deepseek (deepseek) at 0 ms and Don't create profiles about 300 ms later, with nothing after it, and control-openhands browser value 'testid=meta-profile-router-connection' is Don't create profiles. Cancel the editor (control-openhands browser click 'testid=meta-profile-cancel'); F12.create-more saves a template with this choice. The Custom template starts on Don't create profiles even with a connection present; there, pick role=option[name="QA_deepseek (deepseek)"] first and then Don't create profiles: the second pick sticks. Cancel the editor.

Create another router without new profilesF12.create-more · F12.router-profilesMapped behavior

Scenario F12--create-another-router-without-new-profiles · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Add meta profile”.
  2. Do
    Choose “Meta profile template router flash”.
  3. Do
    Enter “QA_router_flash” in “Meta profile name”.
  4. Do
    Choose “Meta profile router connection”.
  5. Do
    Choose “Don't create profiles”.
  6. Do
    Choose “Meta profile save”.
  7. Wait
    Wait for the element identified in the CLI recipe to appear.
  8. Do
    Reload the page.

Expect

saving another router lists it and leaves the Active badge where it was.

with a connection picked under Create missing router LLM profiles using, Save creates an LLM profile <provider>/<name> linked to that connection for every model-table name (and the classifier) that does not exist yet; Don't create profiles creates none.

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=add-meta-profile', control-openhands browser click 'testid=meta-profile-template-router-flash', control-openhands browser value 'testid=meta-profile-router-connection' (QA_deepseek (deepseek), preselected), control-openhands browser fill 'testid=meta-profile-name-input' QA_router_flash, control-openhands browser click 'testid=meta-profile-router-connection', control-openhands browser click "role=option[name=\"Don't create profiles\"]" and control-openhands browser value 'testid=meta-profile-router-connection' (Don't create profiles). Then control-openhands browser click 'testid=meta-profile-save' and control-openhands browser wait-text 'Meta-profile "QA_router_flash" saved'. After control-openhands browser reload, control-openhands browser text 'testid=meta-profile-list' is QA_router\ndeepseek-flash · Direct prompt\nActive\nQA_router_flash\nminimax-m3 · Direct prompt and control-openhands browser count 'testid=meta-profile-active-badge' is 1. control-openhands api GET /api/profiles still lists only deepseek-chat, deepseek-flash and deepseek-pro: none of the template's 11 models became a profile, so QA_router_flash names a classifier (minimax-m3) that does not exist, which saving allows (see Gotchas).

Row menu and activateF12.row-menu · F12.activateMapped behavior

Scenario F12--row-menu-and-activate · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Meta profile menu trigger qa router”.
  2. Do
    Press Escape.
  3. Do
    Choose “Meta profile menu trigger qa router flash”.
  4. Do
    Choose “Meta profile actions menu” / “Meta profile set active”.
  5. Wait
    Wait for the element identified in the CLI recipe to appear.
  6. Do
    Reload the page.

Expect

the row menu offers Edit, Set active (disabled on the active row) and Delete, and closes on Escape.

Set active toasts Meta-profile "<name>" activated and moves the badge.

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=meta-profile-menu-trigger-QA_router' and control-openhands browser snapshot 'testid=meta-profile-actions-menu' (menuitem "Edit", menuitem "Set active" [disabled], menuitem "Delete"); control-openhands browser press Escape closes it (count 0). Then control-openhands browser click 'testid=meta-profile-menu-trigger-QA_router_flash', control-openhands browser click 'testid=meta-profile-actions-menu >> testid=meta-profile-set-active' and control-openhands browser wait-text 'Meta-profile "QA_router_flash" activated'. After control-openhands browser reload, control-openhands browser count 'testid=meta-profile-row-QA_router_flash >> testid=meta-profile-active-badge' is 1, the total badge count is 1, and api GET /api/meta-profiles agrees.

EditF12.editMapped behavior

Scenario F12--edit · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Meta profile menu trigger qa router”.
  2. Do
    Choose “Meta profile actions menu” / “Meta profile edit”.
  3. Wait
    Wait for the element identified in the CLI recipe to appear.

Expect

Edit opens Edit meta-profile with the name locked and no connection picker; changes persist.

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=meta-profile-menu-trigger-QA_router', control-openhands browser click 'testid=meta-profile-actions-menu >> testid=meta-profile-edit', control-openhands browser text 'testid=meta-profile-editor >> role=heading' (Edit meta-profile), control-openhands browser enabled 'testid=meta-profile-name-input' (false), control-openhands browser count 'testid=meta-profile-router-connection' (0). Pick classifier deepseek-pro, fill testid=meta-profile-model-table with the first two table lines only, click testid=meta-profile-save and control-openhands browser wait-text 'Meta-profile "QA_router" saved'. After a reload the row reads QA_router\ndeepseek-pro · Direct prompt, the badge stays on QA_router_flash, and control-openhands api GET /api/meta-profiles/QA_router shows the new classifier and table.

Cancel the connection dialogF12.add-connection-cancelMapped behavior

Scenario F12--cancel-the-connection-dialog · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Add meta profile”.
  2. Do
    Choose “Meta profile template custom”.
  3. Do
    Enter “QA_router_cancel” in “Meta profile name”.
  4. Do
    Choose “Meta profile add provider connection”.
  5. Do
    Enter “QA_unused” in “Provider connection name”.
  6. Do
    Choose the dialog / “Cancel”.
  7. Wait
    Wait for “Provider connection modal” to disappear.

Expect

Cancel in Add provider connection closes it without creating a connection and keeps the editor's values and picker choice.

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=add-meta-profile', control-openhands browser click 'testid=meta-profile-template-custom', control-openhands browser fill 'testid=meta-profile-name-input' QA_router_cancel, control-openhands browser click 'testid=meta-profile-add-provider-connection', control-openhands browser fill 'testid=provider-connection-name-input' QA_unused, control-openhands browser click 'role=dialog >> role=button[name="Cancel"]' (the Cancel button has no test id) and control-openhands browser wait 'testid=provider-connection-modal' --state detached. browser value 'testid=meta-profile-name-input' is still QA_router_cancel, the picker still reads Don't create profiles, and control-openhands api GET /api/llm/provider-connections has no QA_unused (display_name is the field).

Leave the editorF12.editor-cancelMapped behavior

Scenario F12--leave-the-editor · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Enter “Changed: {{ instance_text }}” in “Meta profile prompt template”.

Expect

while the create or edit editor is open it replaces the list, Add Model Router and the switch; Cancel returns to the list without saving anything.

Exact CLI recipe & bug checks

In that editor control-openhands browser count on testid=meta-profile-list, testid=add-meta-profile and testid=meta-profile-run-at-conversation-start is 0 each. Pick classifier deepseek-flash, fill the prompt 'Pick: {{ instance_text }}', click testid=meta-profile-cancel: count 'testid=meta-profile-list' is 1, count 'testid=meta-profile-row-QA_router_cancel' is 0 and api GET /api/meta-profiles has no QA_router_cancel. In edit mode (row menu → Edit on QA_router), control-openhands browser fill 'testid=meta-profile-prompt-template' 'Changed: {{ instance_text }}' then Cancel leaves control-openhands api GET /api/meta-profiles/QA_router with its saved prompt_template.

Run on first messageF12.run-first-message-toggleMapped behavior

Scenario F12--run-on-first-message · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Choose “Meta profile run at conversation start” / “Run on first message”.
  2. Do
    Reload the page.

Expect

the switch saves immediately (no Save button, no toast) and survives a reload.

Exact CLI recipe & bug checks

Run control-openhands browser click 'testid=meta-profile-run-at-conversation-start >> text=Run on first message', then control-openhands browser reload and the switch eval: checked flips and stays flipped; api GET /api/settings shows run_router_at_conversation_start with the same value; control-openhands browser toasts is empty. Click again to restore true.

Routing in a conversationF12.route-first-messageMapped behavior

Scenario F12--routing-in-a-conversation · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Start a conversation with the prompt “Run echo routed in the terminal and tell me its output.”.
  2. Wait
    Wait for the element identified in the CLI recipe to appear.
  3. Do
    Open /settings/meta-llm.

Expect

with a router active and the switch on, a new conversation routes its first message (route_task_to_model action, Switched to profile <name> in the chat); with the switch off the tool is still attached but the instruction is absent.

Exact CLI recipe & bug checks

Make QA_router active (row menu → Set active, wait-text 'Meta-profile "QA_router" activated') with the switch on. Run control-openhands conversation start --prompt "Run echo routed in the terminal and tell me its output." --wait --timeout 300, then control-openhands conversation events <id> --kinds ActionEvent,ObservationEvent,MessageEvent (<id> from the start output). The first action is route_task_to_model, its observation starts Classified task as 'model: deepseek-flash' and switched to LLM profile 'deepseek-flash', then the terminal runs echo routed. On the conversation page control-openhands browser wait-text 'Switched to profile' succeeds and control-openhands browser screenshot --feature F12.route-first-message --name conversation shows the Switched to profile deepseek-flash line above the answer. control-openhands api GET "/api/conversations/<id>/events/search?limit=1" --max-bytes 200000 (the system prompt) contains ROUTE_AT_CONVERSATION_START and "route_task_to_model". Switch Run on first message off (control-openhands browser goto /settings/meta-llm first: the browser is on the conversation), start --prompt "Reply with only the word pong.", and the same GET contains "route_task_to_model" but no ROUTE_AT_CONVERSATION_START; there is no route action.

DeleteF12.deleteMapped behavior

Scenario F12--delete · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Open /settings/meta-llm.
  2. Do
    Choose “Meta profile menu trigger qa router flash”.
  3. Do
    Choose “Meta profile actions menu” / “Meta profile delete”.
  4. Do
    Choose the dialog / “Cancel”.
  5. Do
    Choose “Delete meta profile confirm”.
  6. Wait
    Wait for the element identified in the CLI recipe to appear.
  7. Do
    Reload the page.

Expect

Delete asks Are you sure you want to delete "<name>"? This cannot be undone.; Cancel keeps the row, Delete removes it.

Exact CLI recipe & bug checks

Run control-openhands browser goto /settings/meta-llm, control-openhands browser click 'testid=meta-profile-menu-trigger-QA_router_flash', control-openhands browser click 'testid=meta-profile-actions-menu >> testid=meta-profile-delete' and control-openhands browser snapshot 'role=dialog' (Delete meta-profile, Are you sure you want to delete "QA_router_flash"? This cannot be undone., Cancel, Delete). control-openhands browser click 'role=dialog >> role=button[name="Cancel"]' keeps the row (count 1). Repeat the menu click, then control-openhands browser click 'testid=delete-meta-profile-confirm', control-openhands browser wait-text 'Meta-profile "QA_router_flash" deleted', control-openhands browser reload; control-openhands browser count 'testid=meta-profile-row-QA_router_flash' is 0.

Delete the active routerF12.delete-activeMapped behavior

Scenario F12--delete-the-active-router · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Start a conversation with the prompt “Reply with only the word pong.”.

Expect

deleting the active router leaves no router active; the switch shows off and disabled, and new conversations get neither the tool nor the first-message instruction.

Exact CLI recipe & bug checks

With QA_router active and the switch on, delete it the same way (wait-text 'Meta-profile "QA_router" deleted'). After a reload testid=meta-profile-empty is back, the switch eval is {checked:false, disabled:true} and api GET /api/meta-profiles has active_meta_profile null, while api GET /api/settings still has "run_router_at_conversation_start": true. A new conversation start --prompt "Reply with only the word pong." --wait has neither "route_task_to_model" nor ROUTE_AT_CONVERSATION_START in its events/search output.

Light themeF12.light-themeMapped behavior

Scenario F12--light-theme · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Start a conversation as specified in the recipe.
  2. Do
    Open /settings/meta-llm.
  3. Do
    Open /settings/app.
  4. Do
    Choose “Color theme”.
  5. Do
    Choose “Light+”.
  6. Do
    Open /settings/meta-llm.

Expect

in a light colour theme the list heading and router names are readable.

Exact CLI recipe & bug checks

No router is left after F12.delete-active, whose last conversation start left the browser on a conversation: run control-openhands browser goto /settings/meta-llm and create one through Custom as in F12.phone (name QA_router_light, classifier deepseek-flash, prompt 'Pick one model for: {{ instance_text }}', wait-text 'Meta-profile "QA_router_light" saved'), then control-openhands browser goto /settings/app, control-openhands browser click 'testid=color-theme-input', control-openhands browser click 'role=option[name="Light+"]', control-openhands browser goto /settings/meta-llm and control-openhands browser eval "(()=>{const h=[...document.querySelectorAll('h2')].find(e=>e.textContent.includes('Available meta-profiles'));const n=document.querySelector('[data-testid^=meta-profile-row-] span');return {heading:getComputedStyle(h).color,rowName:getComputedStyle(n).color,bg:getComputedStyle(document.body).backgroundColor}})()". It returns {heading: "rgb(31, 31, 31)", rowName: "rgb(31, 31, 31)", bg: "rgb(255, 255, 255)"}: dark text, the same ink as LLM profile names on /settings/llm. control-openhands browser screenshot --feature F12.light-theme --name list shows the heading Available meta-profiles and the name QA_router_light next to its summary and Active badge. Restore with role=option[name="OpenHands-Neutral"] the same way, then delete QA_router_light through its row menu (wait-text 'Meta-profile "QA_router_light" deleted') so F12.phone starts on a router-less page.

Phone (`F12.phone`, also re-proves `F12.create-first` on a router-less page)F12.phone · F12.create-firstKnown issue

Scenario F12--phone-f12-phone-also-re-proves-f12-create-first-on-a-router-less-page · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Setup
    Use the phone viewport.
  2. Do
    Open /settings/meta-llm.
  3. Do
    Choose “Add meta profile”.
  4. Do
    Choose “Meta profile menu trigger qa router phone”.
  5. Setup
    Use the desktop viewport.

Expect

the list, template modal, editor and row menu fit a 390 px viewport without horizontal overflow.

saving the first router lists it, makes it Active and turns Run on first message on.

Linked known issues

Known issue OpenHands/software-agent-sdk#5878: Router files can be written under a literal ~ directory when Agent Server was configured with leading-tilde OH_PERSISTENCE_DIR. Blocked on the normal absolute-path launcher fixture: needs a disposable server with private HOME/cwd and literal ~/... or relative persistence configuration, then a router save and filesystem readback. Never point it at user state. (F12.create-first, F12.list).

Exact CLI recipe & bug checks

Run control-openhands browser viewport phone, control-openhands browser goto /settings/meta-llm, control-openhands browser click 'testid=add-meta-profile' and control-openhands browser bbox 'testid=meta-profile-template-modal' (insideViewport true, pageHorizontalOverflow false). Choose Custom, fill QA_router_phone, classifier deepseek-flash and prompt 'Pick one model for: {{ instance_text }}'; control-openhands browser bbox 'testid=meta-profile-editor' has no page overflow (taller than the screen, scrolls). Save, reload, then control-openhands browser bbox 'testid=meta-profile-row-QA_router_phone' and, after control-openhands browser click 'testid=meta-profile-menu-trigger-QA_router_phone', control-openhands browser bbox 'testid=meta-profile-actions-menu' are inside the viewport without page overflow. control-openhands browser screenshot --feature F12.phone --name list shows QA_route… truncated, the summary, the badge and the switch on. Return with control-openhands browser viewport desktop.

Restart persistenceF12.listKnown issue

Scenario F12--restart-persistence · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Restart the run-owned stack as specified.
  2. Do
    Open /settings/meta-llm.

Expect

each row shows the name, <classifier> · Direct prompt, an Active badge on the active router and a ... menu; routers and the active choice survive a stack restart.

Linked known issues

Known issue OpenHands/software-agent-sdk#5878: Router files can be written under a literal ~ directory when Agent Server was configured with leading-tilde OH_PERSISTENCE_DIR. Blocked on the normal absolute-path launcher fixture: needs a disposable server with private HOME/cwd and literal ~/... or relative persistence configuration, then a router save and filesystem readback. Never point it at user state. (F12.create-first, F12.list).

Exact CLI recipe & bug checks

Run control-openhands restart, control-openhands doctor, control-openhands browser goto /settings/meta-llm and control-openhands browser count 'testid=meta-profile-row-QA_router_phone >> testid=meta-profile-active-badge'; it is 1.

CleanupF12 · setup / cleanupSupport step

Scenario F12--cleanup · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Setup
    Arrange the prerequisite backend state through the API.
  2. Setup
    Arrange the prerequisite backend state through the API.

Expect

Complete this support step before continuing to the next scenario.

Exact CLI recipe & bug checks

Switch Run on first message off through its label first (it disables once no router is left), then delete each QA_* router through the row menu. Remove the arranged LLM profile and connection with control-openhands api DELETE /api/profiles/deepseek-chat --write and control-openhands api DELETE /api/llm/provider-connections/<connection id> --write (<connection id> from control-openhands api GET /api/llm/provider-connections), or through Settings → LLM (F10).

Router activation is honest on ACPF12.activateKnown failure

Scenario F12--router-activation-is-honest-on-acp · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    ACP cannot attach the router; activation must report that refusal rather than plain success. Known failure (reproduced twice 2026-10-16, OpenHands/software-agent-sdk#5879): actual global ACP settings produced an activated toast while both stored pointers stayed null. An active ACP profile pointer alone does not create this condition. The block restores the original owned settings bytes; its assertion rejects the misleading success toast.

Expect

Set active toasts Meta-profile "<name>" activated and moves the badge.

Known bug observations

Known failure (reproduced twice 2026-10-16, OpenHands/software-agent-sdk#5879): actual global ACP settings produced an activated toast while both stored pointers stayed null. An active ACP profile pointer alone does not create this condition. The block restores the original owned settings bytes; its assertion rejects the misleading success toast.

Exact CLI recipe & bug checks

ACP cannot attach the router; activation must report that refusal rather than plain success. Known failure (reproduced twice 2026-10-16, OpenHands/software-agent-sdk#5879): actual global ACP settings produced an activated toast while both stored pointers stayed null. An active ACP profile pointer alone does not create this condition. The block restores the original owned settings bytes; its assertion rejects the misleading success toast.

sh known-bug F12.activate acp-settings

  QA_ACP_SETTINGS="$OH_VERIFY_RUN/private/qa-known-acp-settings.json"
  cp "$OH_VERIFY_RUN/private/settings.json" "$QA_ACP_SETTINGS"
  trap 'cp "$QA_ACP_SETTINGS" "$OH_VERIFY_RUN/private/settings.json"; rm -f "$QA_ACP_SETTINGS"; control-openhands api DELETE /api/meta-profiles/QA_KNOWN_ACP_ROUTER --write >/dev/null; control-openhands browser reload >/dev/null' EXIT
  control-openhands api POST /api/meta-profiles/QA_KNOWN_ACP_ROUTER --data '{"classifier_model":"QA_KNOWN_CLASSIFIER","prompt_template":"Pick {{ instance_text }}"}' --write
  control-openhands api PATCH /api/settings --data '{"agent_settings_diff":{"agent_kind":"acp"}}' --write --pick agent_settings.agent_kind
  control-openhands assert --field value --equals acp -- api GET /api/settings --pick agent_settings.agent_kind
  control-openhands browser viewport desktop
  control-openhands browser goto /settings/meta-llm
  control-openhands browser wait 'testid=meta-profile-row-QA_KNOWN_ACP_ROUTER'
  control-openhands browser click 'testid=meta-profile-menu-trigger-QA_KNOWN_ACP_ROUTER'
  control-openhands browser click 'testid=meta-profile-set-active' --observe 'role=status' --observe-ms 1000
  control-openhands assert --field value --equals false --bug-shows true -- browser eval 'document.body.innerText.includes("Meta-profile \"QA_KNOWN_ACP_ROUTER\" activated")'  # bug
Unreadable settings refuse router activationF12.activateKnown failure

Scenario F12--unreadable-settings-refuse-router-activation · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do — reading outline

  1. Do
    Expected: a handled409 without a filesystem path and no store overwrite. Known failure (reproduced twice 2026-10-16, OpenHands/software-agent-sdk#5922): the real Set active path showed the generic500 and the settings path. The corrupt bytes survived. Back up only this run's settings, and restore them in EXIT before the runner's health check.

Expect

Set active toasts Meta-profile "<name>" activated and moves the badge.

Known bug observations

Known failure (reproduced twice 2026-10-16, OpenHands/software-agent-sdk#5922): the real Set active path showed the generic500 and the settings path. The corrupt bytes survived. Back up only this run's settings, and restore them in EXIT before the runner's health check.

Exact CLI recipe & bug checks

Expected: a handled409 without a filesystem path and no store overwrite. Known failure (reproduced twice 2026-10-16, OpenHands/software-agent-sdk#5922): the real Set active path showed the generic500 and the settings path. The corrupt bytes survived. Back up only this run's settings, and restore them in EXIT before the runner's health check.

sh known-bug F12.activate corrupt-settings

  QA_ROUTER_SETTINGS="$OH_VERIFY_RUN/private/qa-known-router-settings.json"
  cp "$OH_VERIFY_RUN/private/settings.json" "$QA_ROUTER_SETTINGS"
  trap 'cp "$QA_ROUTER_SETTINGS" "$OH_VERIFY_RUN/private/settings.json"; rm -f "$QA_ROUTER_SETTINGS"; control-openhands api DELETE /api/meta-profiles/QA_KNOWN_CORRUPT_ROUTER --write >/dev/null; control-openhands browser reload >/dev/null' EXIT
  control-openhands api POST /api/meta-profiles/QA_KNOWN_CORRUPT_ROUTER --data '{"classifier_model":"QA_KNOWN_CLASSIFIER","prompt_template":"Pick {{ instance_text }}"}' --write
  control-openhands browser viewport desktop
  control-openhands browser goto /settings/meta-llm
  control-openhands browser wait 'testid=meta-profile-row-QA_KNOWN_CORRUPT_ROUTER'
  printf '{"agent_settings": ' > "$OH_VERIFY_RUN/private/settings.json"
  control-openhands browser click 'testid=meta-profile-menu-trigger-QA_KNOWN_CORRUPT_ROUTER'
  control-openhands browser network --clear
  control-openhands browser click 'testid=meta-profile-set-active' --observe 'role=status' --observe-ms 1000
  control-openhands assert --field recent.0.status --equals 409 --bug 'recent.0.status eq 500 && recent.0.path eq "/api/meta-profiles/QA_KNOWN_CORRUPT_ROUTER/activate"' -- browser network --filter '/api/meta-profiles/QA_KNOWN_CORRUPT_ROUTER/activate'  # bug
Load errorF12.load-errorBehavior contract

Scenario F12--contract-f12-load-error · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do

This behavior has no separately named recipe in this snapshot. Use the family’s entry points and the contract below; the page does not invent a replay.

Expect

when only the meta-profile request fails, a red generic error shows under the header.

UnsupportedF12.unsupportedBehavior contract

Scenario F12--contract-f12-unsupported · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do

This behavior has no separately named recipe in this snapshot. Use the family’s entry points and the contract below; the page does not invent a replay.

Expect

an agent server without /api/meta-profiles (404) shows This backend doesn't support model routing yet. Update the agent server to manage meta-profiles.

CloudF12.cloudBehavior contract

Scenario F12--contract-f12-cloud · Permalink · Source

Given

Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.

Do

This behavior has no separately named recipe in this snapshot. Use the family’s entry points and the contract below; the page does not invent a replay.

Expect

a Cloud backend without an organization shows Meta-profiles are only available for local backends.; an organization-bound Cloud backend shows the full page.

Family notes, prerequisites for known issues & limits Known issue
  • Settings nav test ids embed the route: quote 'testid=sidebar-settings-/settings/meta-llm' as written.
  • The empty-state hint about LLM profiles only exists before any profile is saved: drive F12.empty-states first on a fresh run.
  • The first router you create is activated and turns Run on first message on by itself; later routers do neither. Any recipe that creates a router on an empty list changes both.
  • Run on first message is an sr-only checkbox (keyboard reachable): click the label text inside testid=meta-profile-run-at-conversation-start and read .checked/.disabled with browser eval. The stored preference (misc_settings.app_preferences.run_router_at_conversation_start) is independent of what the disabled switch shows; with no active router it may still be true. Turn it off before deleting the last router, or a later "first router" will not re-enable it because it is already on.
  • Router templates name 11 models (gpt-5.4 … claude-sonnet-4-5) that will not exist in a sandbox. With a provider connection present, a template preselects it, and saving creates all 11 as LLM profiles linked to that connection unless you pick Don't create profiles first (F12.create-more does). If a run created them, remove them with for p in gpt-5.4 gpt-5.5 minimax-m3 claude-opus-4-6 claude-opus-4-7 claude-opus-4-8 claude-fable-5 kimi-k2.5 kimi-k2.6 deepseek-v3.2-reasoner claude-sonnet-4-5; do control-openhands api DELETE /api/profiles/$p --write; done (they live in the run's private state and only add rows to Settings → LLM).
  • Profile matching is case-insensitive and exact by name: table rows are - <profile name>: description; existing deepseek-flash/deepseek-pro are not re-created, and the endpoint is derived as <provider lowercased>/<name>.
  • The classifier and the model table refer to LLM profile names, not model ids. A router whose classifier profile does not exist (for example a template's minimax-m3 without a connection) saves fine and only fails when a conversation tries to route.
  • The agent decides whether to call route_task_to_model: a prompt that conflicts with routing (Reply with only the word pong.) made deepseek-flash skip the call even with the instruction present. Use a task-like prompt for the positive check and assert the system prompt via events/search for the instruction itself.
  • conversation events does not print the system prompt; read it with control-openhands api GET "/api/conversations/<id>/events/search?limit=1" --max-bytes 200000 and grep the output.
  • service stop agent-server does not show this page's error state: the whole app switches to the Manage backends card (F02). Run control-openhands restart afterwards.
  • The selection in the classifier and connection pickers is a combobox: open it with a click on the input, then click role=option[name="..."]; browser fill on the classifier sets a free-text value (allowed) instead of picking a profile.
  • At 390 px the row truncates the name (QA_route…) and summary; read names with browser text, not the screenshot.
  • The Model table placeholder displays separate lines (F12.template-custom).
  • Known issue OpenHands/software-agent-sdk#5878: Router files can be written under a literal ~ directory when Agent Server was configured with leading-tilde OH_PERSISTENCE_DIR. Blocked on the normal absolute-path launcher fixture: needs a disposable server with private HOME/cwd and literal ~/... or relative persistence configuration, then a router save and filesystem readback. Never point it at user state. (F12.create-first, F12.list).

Before you start

Use an OpenHands checkout at the source revision, with Node 24+, installed npm dependencies, uv / uvx, and a supported Chromium browser. The verification skill is the complete setup contract.

export PATH="$PWD/.agents/skills/verify-openhands/scripts:$PATH"
export OH_VERIFY_RUN=$(control-openhands launch --new --print-run)
control-openhands doctor
control-openhands onboard --skip
# Configure a model only for a scenario that needs one:
control-openhands llm preset deepseek
# When the run is complete:
control-openhands stop

F01 walks onboarding instead of skipping it. No-model checks run before model configuration. Keep OH_VERIFY_RUN set so commands address your own run. Desktop is 1440 × 1000; phone is 390 × 844. Model-backed checks need the configured provider key and budget.

Recipes run in source order within each family and can share fixtures. The page preserves that order; “as above” is a dependency on an earlier case. Setup commands and API writes arrange state. They do not prove a UI path. Follow the family cleanup steps.

A known failure preserves the intended expectation beside the observed defect. Executable known-bug blocks use xfail for a reproduced symptom and xpass when it stops reproducing. Cloud accounts, enterprise capabilities and other unavailable prerequisites remain explicit. “Not run” is different from “blocked”.

The maintained map describes application behavior at its pinned revision. Once a fix is verified in the application version under test, remove the obsolete failure note and keep the normal expectation. Dated evidence belongs in the backfill ledger.

Read the full map index and coverage boundaries, the CLI guide, or the earlier illustrated edition. The verification approach adapts Lauren Tan’s (@poteto) pstack verification-skill generators; attribution and adaptation.