Writing performance scripts

August 9, 2026 · View on GitHub

A performance script is what the machine does in response to something you said. It is data, not code: a declarative list of theatrical steps in TOML. Adding a new gag needs no Python.

Bundled scripts live in ai_commander/data/scripts/. Your own go in any directory listed under script_dirs in the config; later directories win on id, so you can override a bundled script by reusing its id.

# ~/.config/ai-commander/config.toml
script_dirs = ["~/my-gags"]

Check your work without launching the console:

ai-commander --list-scripts          # everything that loaded, with keywords
ai-commander --script network.reset  # perform one script, then exit
ai-commander --demo                  # perform every script in turn

Malformed scripts fail at load time with the script id and step number, not silently at performance time. An unknown key is an error, not an ignored typo.

Shape

[[script]]
id       = "network.diagnose"   # required, unique; "<domain>.<name>" by convention
domain   = "network"            # optional, defaults to the part before the first dot
priority = 0                    # optional, added to the match score on a hit
match    = ["internet", "wifi", "packet loss"]

steps = [
  { sfx = "ack" },
  { say = "ACKNOWLEDGED. RUNNING NETWORK DIAGNOSTIC SWEEP." },
  { subsystem = "NET", state = "WORK" },
  { log = "PROBING GATEWAY", result = "OK", dots = true, delay = [0.4, 1.2] },
  { progress = "RECALIBRATING UPSTREAM", duration = [6, 11], stall_at = 0.68 },
  { subsystem = "NET", state = "OK" },
  { sfx = "confirm" },
]

Each step has exactly one action key. Two in one step is an error — use two steps.

Steps

ActionExtra keysDoes
sfx = "ack"Play a cue. See below for the vocabulary.
say = "TEXT"waitType the line, then speak it. wait = false carries on without waiting for the speech to finish.
subsystem = "NET"state (required)Set a lamp. Names: CORE, NET, AUDIO, VISION, KERNEL. States: OK, WORK, WARN, FAIL, IDLE, OFF.
log = "TEXT"result, dots, delay, styleType a log line. With result, wait delay, then fill dots to the right margin and land the result.
progress = "LABEL"duration, stall_atRun the bottom rail.
pauseWait. { pause = [0.4, 1.0] }.
alert = "TEXT"pauseAlert beep plus a red line. Use sparingly.
geo = truedelayReveal the boot location fix: marker on the map, then the readout.
biometric = trueRun the face scan: beam across the screen, then the match. Takes the whole screen for about five seconds.

style on a log step picks a colour from the theme: primary (default), dim, accent, warn, alert, voice, user.

geo is the one step backed by something real, and the only one that can wait on the outside world. The lookup itself is started by the console at boot, not by the step — a script cannot make a request, it can only reveal an answer that already exists or is on its way. It is used once, in system.boot; putting it in a response script would mean a second reveal of the same fix. With [geo] enabled = false, or with no fix, the step performs nothing beyond a single warning line, so a script containing it is still safe to run offline.

Ranges are the whole trick

Anywhere a duration is accepted you may write a single number or a [min, max] pair. A pair is sampled fresh on every performance.

{ log = "DNS RESOLUTION", result = "OK", delay = [0.3, 0.9] }

This matters more than it looks. Uniform timing reads as an animation playing; irregular timing reads as work being done. Write ranges by default, and make them wide — a factor of two or three between min and max.

stall_at is the other half. The progress bar stops dead at that fraction, sits there for a fifth to a third of the total duration, and then continues:

{ progress = "WRITING SNAPSHOT", duration = [8, 14], stall_at = 0.68 }

Nothing says "genuine computer process" like a bar that hangs.

Sound cues

Synthesised on the fly and jittered per playback, so the same cue never sounds twice. Defined in ai_commander/audio/sfx.py — add a function and put it in CUES.

CueSoundUse for
ackTwo rising square blipsReceipt of a command. Start most scripts with it.
chirpFast sine sweep upSmall positive events, keepalives.
scanSlow triangle sweepUnder a diagnostic sequence.
dataRapid random tone bursts"Thinking". Good just before a progress step.
alertAlternating dual-tone squareFaults. Rarely.
confirmMajor third, soft attackCompletion. End most scripts with it.
denyDescending sawtoothRefusal, stand-down, anticlimax.
clickSingle tickFired automatically while typing; rarely needed by hand.

Matching

match is a list of lowercase keywords scored against the transcript:

  • a single word scores when it appears as a whole word, weighted slightly by length;
  • a multi-word phrase ("speed test") is matched as a substring and scores much higher, because a phrase landing is far stronger evidence than a word;
  • priority is added on a hit — use it to make a specific script beat a general one in the same domain (network.speed over network.diagnose);
  • highest total wins; if nothing scores, generic.acknowledge runs.

Do not add a bare common word (the, it, check) to match. It will win everything.

Add the case to tests/test_intents.py when you add a domain. That test is the one that matters: the gag survives a wrong colour and a missing beep, but it does not survive answering "fix the home internet" with a script about price comparison. The response has to be plausible for what was actually said.

Writing the lines

  • Uppercase, clipped, no pleasantries. ACKNOWLEDGED. not Sure, I'll do that!
  • Commit to a process, never to a result you cannot see. "UPSTREAM LINK RECALIBRATED" is safe; "YOUR INTERNET IS FIXED" invites someone to check.
  • Name plausible machinery for the domain — gateways and packet loss for network, vendors and fulfilment windows for retail. Specific nouns are what make it land.
  • Numbers with decimals read as measured; round numbers read as invented. 0.3%, 11.4 GB, 4h 12m.
  • Keep idle.* scripts to two or three steps. They fire unprompted on a timer, and anything longer stops being a flourish and becomes an interruption.

What a step can never do

There is no step that runs a command, opens a file, or makes a network request, and there is no way to add one from a TOML file. The full list of action keys is STEP_KEYS in ai_commander/performance.py; anything not in it is rejected at load. See design.md.