Post

Helen Gets a Command Surface

Summary: Helen, the default voice of my lab assistant, picked up three new abilities this week: she can run a small set of real operations by voice, she can answer general questions instead of deflecting anything that isn't about the lab, and she can tell you what time it is without making it up. That last one sounds like a joke, but it was a real bug.

Helen Gets a Command Surface

Where this started

I was uploading a data extract through the dashboard and the last step of that pipeline is an ingest job. My own runbook's answer for kicking it off was an SSH command with a python -c in it. Which is fine when I'm at a keyboard, and useless when I'm walking through the room with coffee asking the assistant to just do the thing.

So the question became: how do you let a voice assistant run jobs without opening the door I have spent this whole series keeping shut? I wrote in the tier system post about keeping limits in code instead of prompts. Adding commands is the test of whether that design holds up when you actually lean on it.

Speech selects, it never composes

The command surface is a catalogue. Every operation Helen can perform is written down ahead of time with its exact command — an argv list, never a string, never assembled from anything I said. Her only job at runtime is to figure out which entry I meant. If nothing matches, nothing runs.

There is no free-text path to a shell anywhere in this. I want to be plain about that because it is the entire design: the model picks from a menu, and the menu was written by me, in an editor, with the argv fixed. An utterance like "run the weekly ingest" matches a catalogue entry that curls a specific internal endpoint. The words I used select the entry. They never become part of the command.

Each entry carries a tier from the same authority system Helen already had:

  • Reads act first and tell me after. "Is the internet down" just pings and answers. A read that asks permission is a gate you learn to switch off, so they don't ask.
  • State changes propose first. Helen states what the action costs — not what it's called, what it costs — and asks. "Every queued run is merged into the corpus, and a run whose id already exists replaces the stored one. Shall I?"
  • The forbidden set stays forbidden. Nothing on the menu can lower a hard refusal, and the refusal wording is still a constant.

The confirmation is scoped to the exact proposal, and it expires after a couple of minutes. A "yes" arriving late, or after a subject change, confirms nothing. That rule exists because "yes" is the most dangerous word in the whole system and I wanted it to mean one thing at a time.

The transcript, because it's satisfying

Me: "Run the weekly ingest."

Helen: "I can do that. Every queued run is merged into the corpus, and a run whose id already exists replaces the stored one. Shall I?"

Me: "Yes, go ahead."

Helen: "The inbox was empty — nothing to process."

That last reply is my favorite part. The results of a job come back as JSON, and Helen never reads JSON out loud. Each catalogue entry has a small function that turns the outcome into a sentence: how many runs were processed, how many rows were refused, whether anything conflicts. An empty inbox is named as empty. If you have ever heard a text-to-speech engine attempt to pronounce a JSON payload, you know why I bothered.

The clock, and a humbling bug

While I was in there, I fixed something that had been quietly embarrassing. I asked Helen the time one morning around six and she said it was "around 3 PM."

Nothing had lied to her. Nothing had told her anything — no tool in the assistant's graph carried the time at all, so the model guessed, and a guess about the clock sounds exactly like an answer. This is the same lesson this lab keeps teaching me in different costumes: a fact the model must state is a fact the code must supply. Counting was that lesson. Reading ages was that lesson. Now the wall clock.

So there's a small timekeeper module now. Direct questions — what time is it, what's the date, what day is it — are answered by code before any model gets involved, in the household's timezone. And every one of Helen's model turns now carries a labeled clock line in its context, so a question that only brushes against time ("should I start the backup now?") is grounded too. The matcher is careful about what counts as a clock question: "how much time did the reindex take" is a question about a duration and still goes to the model.

General questions

Until this week Helen's persona file scoped her to the lab, and questions outside it got a polite deflection. That was the right starting posture and it had gotten annoying. She's the default voice — everything that doesn't name another persona lands on her — so she's the one who gets asked who wrote Moby Dick.

Now she answers. The change is an explicit license in the code path that fires when nothing in the lab bears on a question: answer it yourself, plainly, from general knowledge. With two rules kept from the old posture, because they were never the annoying part. Lab figures still only come from live readings she is handed — general knowledge never gets to invent a number about my machines. And the personal-life territory that belongs to another persona still gets referred there.

The other three personas didn't change at all, and there's a test asserting they didn't pick up Helen's new license by osmosis. Their designs are narrower on purpose.

The menu, and the test that keeps it honest

The last piece is documentation, which regular readers will know is not an afterthought in this lab.

There's now a commands-menu runbook: every operation, the phrasings that trigger it, its tier, and what it costs. The words that count as yes and the words that count as no. The refusal categories by name. Which answers come from code rather than the model.

And a test enforces it. Every catalogue key, every example phrasing, and every refusal category must appear on that page verbatim, or CI fails. A menu row for a removed command is a command that silently does nothing. A catalogue entry missing from the menu is a command nobody knows they can speak. I have been burned by both shapes of stale documentation enough times that I now make the test suite hold the pen.

What I'd tell you if you're building one

Adding voice commands turned out to be the easy part — an afternoon, mostly, because the hard parts already existed. The tiers existed. The confirmation flow existed. The constant refusals existed. All the new work leaned on structure that was built back when the assistant could only read.

If I had built commands first and boundaries second, this would have been a very different week. Do the boring authority work early, while the capability list is short. Then extending the assistant is just adding rows to a menu, and the menu can't hurt you.

Questions about any of it, drop me a note.


The lab so far

This post is part of an ongoing series — the home-lab build written down as it happens, in order:

  1. Load-Bearing Docs: How a Homelab Learns to Stop Lying to Itself — Jun 30, 2026
  2. 1.7 Tokens Per Second: How the Lab Got Its GPUs — Jul 2, 2026
  3. Is Any of This Even Running? Building the Lab's Single Pane of Glass — Jul 4, 2026
  4. Zero Redundancy: Backing Up a Striped Array I Chose On Purpose — Jul 6, 2026
  5. Two Platforms On Purpose: systemd, k3s, and Refusing to Migrate — Jul 8, 2026
  6. Body Language: Giving the Lab a Face on the Wall — Jul 10, 2026
  7. An Operator Console, Not a Dashboard — Jul 12, 2026
  8. From Wake Word to Soundbar: The Whole Voice Pipeline — Jul 14, 2026
  9. A Dropped Log Is Worse Than an Ugly One — Jul 16, 2026
  10. Where Does This Sentence Go? The Gateway Problem — Jul 18, 2026
  11. Mostly Arithmetic: Classifying Intent Without Training Anything — Jul 20, 2026
  12. Teaching the Lab to Know Me: Journals Into Structured Data — Jul 22, 2026
  13. The Index That Was Quietly Empty — Jul 24, 2026
  14. Two Stages and a Refusal: Routing Inside the Memory — Jul 26, 2026
  15. Looking Outward: A Small Data Lake for Public Sources — Jul 28, 2026
  16. The Coach That Doesn't Use a Model — Jul 30, 2026
  17. How Helen Came to Be: Four Voices and a State Machine — Aug 1, 2026
  18. A Refusal a Model Can't Be Talked Out Of — Aug 3, 2026
  19. Seven in the Morning: What Helen Says, and Why No Model Writes It — Aug 5, 2026
  20. Helen Gets a Command Surface (you are here) — Aug 11, 2026

Before it was a series: earthquake data and local LLM reasoning, the Slack integration that followed, CLI vs Open WebUI on context, the AnythingLLM ADR, and the LLM-owned wiki pattern.


Credits

Hero photo by Dmitrii E. on Unsplash — a ship's engine-order telegraph, which is the whole design of this post rendered in brass: a fixed menu of commands, and a lever that selects one but can never invent one.

This post is licensed under CC BY 4.0 by the author.