Agent skill

Limoni Agent Surface

by thebanri in thebanri/limoni

How Limoni exposes applications to AI agents and tests — the semantic tree, the automation socket, cmd/limoni-mcp, and the uitest package.

Apache-2.0Auto-check passedFrontend & Design

Install Limoni Agent Surface

skills CLI
$ npx skills add thebanri/limoni --skill limoni-agent-surface -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install thebanri/limoni limoni-agent-surface --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/thebanri/limoni.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/limoni-agent-surface .claude/skills/limoni-agent-surface && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
limoni-agent-surface
GitHub stars
152
Token cost
~2.8k tokens
SKILL.md length
1,332 words
Files
1
Skills in repo
4
Repo updated
First seen
Licence
Apache-2.0

At a glance

How Limoni exposes applications to AI agents and tests — the semantic tree, the automation socket, cmd/limoni-mcp, and the uitest package.

  • Works in 4 steps: Unit — run a real automation.Listen with… → Mutation — break the thing the test… → PTY — build with -tags limoni_debug and… → …
  • Tasks that involve Accessibility
  • SKILL.md covers Invariants that must not regress, Traps already paid for, MCP specifics (cmd/limoni-mcp,… and Testing recipe, plus 6 more sections
  • Calls go and claude

What it does

Limoni Agent Surface is an agent skill from thebanri/limoni. How Limoni exposes applications to AI agents and tests — the semantic tree, the automation socket, cmd/limoni-mcp, and the uitest package. Load before touching core/accessibility, automation/, cmd/limoni-mcp, uitest/, a widget's AccessibilityNode, or anything about agents driving or testing a TUI.

Its SKILL.md is about 2.8k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Frontend & Design, covering Accessibility and MCP servers. It works with Model Context Protocol. The repository describes itself as: Terminal UI engine for Go that tests can click and AI agents can drive (MCP). Zero-allocation rendering, immediate mode + Elm architecture, 3D, images, charts, accessibility… The licence is Apache-2.0.

When your agent uses it

  • Tasks that involve Accessibility
  • Tasks that involve MCP servers

Example prompts

  • “/limoni-agent-surface”

Workflow steps

4 steps, taken from the first numbered list in SKILL.md.

  1. Unit — run a real automation.Listen with a fake app that applies input
  2. Mutation — break the thing the test claims to protect and watch it fail
  3. PTY — build with -tags limoni_debug and run under a real pty; scripted
  4. A real agent — headless run, tools restricted

What it can do on your machine

Read from SKILL.md and the folder at commit 025c5d5. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • go
    • claude

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Limoni Agent Surface loads about 2.8k tokens when it runs. Until then it costs about 80 tokens; SKILL.md has 1,332 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~80
When it runs · the whole SKILL.md, loaded when a task matches
~2.8k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from thebanri/limoni at commit 025c5d5, republished under its Apache-2.0 licence (© thebanri). 1,332 words, ~2,812 tokens.

Download SKILL.mdSave it as .claude/skills/limoni-agent-surface/SKILL.md (or your agent's skills folder).
name
limoni-agent-surface
description
How Limoni exposes applications to AI agents and tests — the semantic tree, the automation socket, cmd/limoni-mcp, and the uitest package. Load before touching core/accessibility, automation/, cmd/limoni-mcp, uitest/, a widget's AccessibilityNode, or anything about agents driving or testing a TUI.

The agent surface: semantic tree → socket → MCP → tests

One tree serves four consumers: screen readers, the automation socket, AI agents through MCP, and uitest. Adding a semantic node helps all four; breaking the tree breaks all four.

widget (accessibility.Provider)
  → Frame.RegisterAccessibility          f.Accessibility, valid for THIS frame only
  → Frame.AccessibilityTree()            deep copy — the only safe way to keep it
  → gateway publish (limoni_debug only)  automation.Snapshot{Tree, Screen, Focused, Injector}
  → automation server                    redacts, resolves selectors, injects input
  → cmd/limoni-mcp                       MCP tools over stdio for Claude Code etc.
  → uitest                               locators + waiting assertions (same selectors)

Invariants that must not regress

  • Secrets never leave. TextInput{Secret: true} draws a mask glyph, so the text never reaches the cell buffer; its node is StateSensitive with no value; accessibility.Redact clears sensitive values again whatever the policy says.
  • Selectors resolve against the redacted tree. Resolving against the raw one turns a withheld value into an oracle: find value="hunter2" would answer.
  • Policy is closed by default. The zero AutomationPolicy exposes structure only. AllowInput, ExposeScreen, ExposeInputValues are separate opt-ins.
  • Peer verification. Linux/macOS/FreeBSD ask the kernel who owns the connecting process; elsewhere connections are refused unless AllowUnverifiedPeers. Tests must set it off those three platforms.
  • Nothing in release binaries. The gateway exists only under -tags limoni_debug. CI greps go list -deps and the symbol table of built examples/simple and examples/agent_checklist for limoni/(automation|session).
  • Zero allocations on the draw path, including node construction — widgets.TestAccessibilityNodeConstructionDoesNotAllocate enforces it.

Traps already paid for

  • f.Accessibility aliases reused buffers. List writes its row nodes into a buffer owned by ListState to stay allocation-free, so the slice is only valid until the next draw. Frame.AccessibilityTree deep-copies; anything keeping a tree (gateway, session recording, testkit, tests) must go through it.
  • FocusManager.Clear runs before the draw function. An immediate-mode app handles its event at the top of the callback, where the focusable list is empty — Tab did nothing in every limoni.Run app. Navigation now falls back to the previous frame's registrations (lastFocusable, lastBounds).
  • Server.Close must not wait for clients. It closes open connections; otherwise an app hangs on exit while an agent or test is attached.
  • Input is asynchronous. The socket acknowledges a key before the frame it causes. A tool that returns immediately shows the agent the screen before its own click. app.act waits for the tree to change, then to hold still for 50ms, bounded by -settle.
  • "Nothing changed" needs a reason. Typing into a secret field or with ExposeInputValues off looks identical to a lost keystroke; the tool says which, or the agent types the same text again.
  • Input that ends the app is success. Esc or a Quit button closes the socket right after the input lands; report it as done, not as a failed call.
  • Retry reads, never input. A dropped connection after a write may mean the click already landed. Read-only calls retry once (the app may have restarted).
  • Reject unknown tool arguments. A misspelled selector field would otherwise vanish and silently widen the selector.

MCP specifics (cmd/limoni-mcp, stdlib only, no SDK)

  • Line-delimited JSON-RPC 2.0 on stdio: initialize, ping, tools/list, tools/call, notifications/cancelled. Versions answered: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05; unknown → newest.
  • Batches ([) were removed from MCP in 2025-06-18 — refuse with -32600.
  • A cancelled request gets no response. Notifications get no response.
  • Tool failures are isError text results the model can act on, not JSON-RPC errors. Unknown tool name is -32602.
  • defer wg.Wait() must be registered before defer cancel(), or closing stdin waits out a minute-long wait_for.
  • Input tools carry destructiveHint: true; reads readOnlyHint: true.

Testing recipe

  1. Unit — run a real automation.Listen with a fake app that applies input on a later frame (one event per frame, in order), and drive the bridge over io.Pipe. See cmd/limoni-mcp/main_test.go.
  2. Mutation — break the thing the test claims to protect and watch it fail: cancellation skip, settling, reconnect, deep copy, Not(), focus wait.
  3. PTY — build with -tags limoni_debug and run under a real pty; scripted JSON-RPC against the built binary. Unit tests missed the Tab bug; this found it.
  4. A real agent — headless run, tools restricted:
bash
claude -p "<goal>" --mcp-config mcp.json --strict-mcp-config \
  --allowedTools "mcp__limoni__*" --output-format stream-json --verbose
# add --tools "" to prove the agent used no shell or file access

Sockets: sockaddr_un caps the path at ~103 bytes. The scratchpad path is too long — use $XDG_RUNTIME_DIR (also where a real app's socket belongs).

uitest, in one screen

go
page := uitest.Run(t, 80, 24, app.draw)            // immediate mode, in process
page := uitest.Program(t, 80, 24, &model{})        // declarative, real message loop
page := uitest.Connect(t, socket)                  // running binary, over the socket

page.GetByRole("button", "Add task").Click()
page.GetByID("name").Type("x")                     // clicks to focus first, waits for focus
rows := page.GetByRole("list-item", "").Within(page.GetByRole("list", "Tasks"))
page.Expect(rows.Nth(-1)).ToContainLabel("[x]")
page.Expect(page.GetByID("status")).Not().ToBeVisible()

Actions wait for exactly one match; assertions retry; failures print the last frame's tree. WithTimeout, WithSlowMo (demo pacing). Every action is logged with t.Logf, without the typed text — it may be a password.

TestLiveDemo in examples/agent_checklist drives a visible app over the socket for recording:

bash
go run -tags limoni_debug ./examples/agent_checklist          # terminal 1
LIMONI_DEMO_SOCKET=$XDG_RUNTIME_DIR/limoni-checklist.sock \
  go test -v -run TestLiveDemo ./examples/agent_checklist     # terminal 2
Show full SKILL.md (646 more words)Show less

Structure beyond List

  • Rows, tabs, tree items are children too. Table rows (row, labelled by the first cell, with a cell child per column), TreeView items (tree-item, with StateExpanded) and tabs (tab-list/tab, only when Tabs.State is set). Table and Tabs build their nodes during Draw — only Draw knows which filtered and sorted row lands on which screen row — into buffers on their state, so a node built without a Draw has no children. Table's cellNodes is sized before the row loop: rows keep sub-slices of it, and a later append that grows it would leave them pointing at the old array.
  • Nested widgets need ctx.Describe. The frame only registered widgets it rendered itself, so a widget drawn as a Block's Child or through AsComponent was invisible — most of a real app. A container that draws a child itself calls ctx.Describe(child, area) after drawing it; Block and WidgetAdapter do. New containers must too.
  • Copy the whole Context. Block once built its child's context field by field, so every Context field added later (click actions, wheel scrolling, Describe) silently never reached nested widgets. childCtx := ctx, then change Area/Style.
  • Anything on screen an agent must know belongs in the tree. Text drawn with SetString is invisible to it. zest's "Esc clears" hint was plain text and hidden while typing; a real agent cleared a filter with twenty Backspaces. Draw status and key hints as widgets (Paragraph{ID: "keys"}).

uitest, the parts added later

  • Locator.Check/Uncheck/Select click only when needed and wait for the state; MCP click takes ensure: checked|unchecked|selected. Use them in any step that may run twice — click toggles.
  • Expect(...).ToContainValue(s): a Paragraph's text is its value (its label is "Text"), so ToContainLabel never matches it.
  • Locator.Node() waits for a match, not for a change. After an action, assert with Expect (which retries); reading Node().Value straight away raced the counter template's redraw.
  • Page.ExpectExit() waits for the app to quit. Exited() is immediate, and a declarative program quits through its message loop a moment after the key.
  • Declarative mode: type with page.Type/page.Press into whatever the model focuses; Locator.Type clicks to focus, which Program mode does not route.

Testing with a real agent: what one run taught

A headless run against zest (--tools "", only mcp__limoni__*, seeded demo log so the answer was known in advance: line 16, service api) answered correctly in 35 calls, 33 s, $1.08. Its detours were real bugs: Ctrl+U was typed as a "u" (TextInput inserted every Ctrl/Alt key — now ignored, readline keys implemented) and the missing hints above. Read transcripts for detours, not just the final answer. Summarise them from the stream-json with the tool calls and the first line of each result.

Harness traps (each cost a debugging session)

  • Drain the PTY continuously. A harness that reads the app's output only between tool calls lets the PTY buffer fill; the app blocks in write, stops drawing, and every tool reports "the tree did not change" — it looks exactly like an app hang. Pump output on a thread. To tell which it is, redirect the app's stderr to a file and send SIGQUIT: a goroutine dump showing Terminal.Draw → Backend.Write → syscall.write is the harness.
  • pkill -f <pattern> kills your own shell when the pattern appears in the command line that runs it. Stop processes by pid file.
  • Windows resets AF_UNIX connections closed with unread data. The server's refusal message was lost because the client's request was still unread; it now half-closes and drains (bounded) before closing. Only the Windows CI runner shows this.
  • Ground truth first. Compute the expected answer from a seeded source before running an agent, or a plausible wrong answer passes.

Known gaps

  • (Fixed) Custom widgets embedding widgets.Accessible are focusable now: Accessible.WantsFocus is true for an ID with an interactive role, and Frame.RenderWidget registers such a widget itself. TestTabReachesAccessibleWidgets in testkit.
  • (Fixed in d2d4355, #47) Declarative mode routes mouse events to the frame's click regions (Program.routeMouse).

© thebanri, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in .claude/skills/limoni-agent-surface of thebanri/limoni.

Open the folder on GitHubat commit 025c5d5

Compare with similar skills

Limoni Agent Surface next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Limoni Agent Surface compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Limoni Agent Surface this skillthebanri/limoni152—~2.8kAutomated safety check: PassApache-2.0
DocsPrefectHQ/fastmcp28k—~1kAutomated safety check: PassApache-2.0
Better Designmarvkr/better-design254—~1.2kAutomated safety check: PassMIT
UI Automation Workflowsconorluddy/xclaude-plugin183—~2.2kAutomated safety check: PassMIT
Penpot Uiux Designgithub/awesome-copilot40k1 repos~3kAutomated safety check: PassMIT
MCP Developmentcoollabsio/coolify63k1 repos~949Automated safety check: PassMIT

Similar skills

  • Docs

    PrefectHQ/fastmcp

    Write or revise a page under docs/ for gofastmcp.com. An agent skill from PrefectHQ/fastmcp.

    28k GitHub stars~1k tokensUpdated today
    Frontend & DesignAuto-check passed
  • Better Design

    marvkr/better-design

    Build, improve, and review production interfaces with the Better Design MCP.

    254 GitHub stars~1.2k tokensUpdated 17 days ago
    Frontend & DesignAuto-check passed
  • UI Automation Workflows

    conorluddy/xclaude-plugin

    Accessibility-first UI automation using IDB. An agent skill from conorluddy/xclaude-plugin.

    183 GitHub stars~2.2k tokensUpdated 27 days ago
    Frontend & DesignAuto-check passed
  • Penpot Uiux Design

    github/awesome-copilot

    Official

    Comprehensive guide for creating professional UI/UX designs in Penpot using MCP tools.

    40k GitHub starsUsed in 1 repo~3k tokens
    Frontend & DesignAuto-check passed
  • MCP Development

    coollabsio/coolify

    A skill your agent uses for Laravel MCP development. An agent skill from coollabsio/coolify.

    63k GitHub starsUsed in 1 repo~949 tokens
    Frontend & DesignAuto-check passed
  • Better Icons

    dtsola/xiaoyaosearch

    Searches more than 200 Iconify icon libraries and fetches icons as SVG from a command line tool or an MCP server.

    1k GitHub starsUsed in 2 repos~895 tokens
    Frontend & DesignAuto-check passed

More from thebanri/limoni

  • Limoni Performance

    thebanri/limoni

    Where allocations hide in a Limoni frame, how to find them, how to compare benchmarks honestly, and the release and CI checks that catch what one machine cannot.

    152 GitHub stars~1.4k tokensUpdated 4 days ago
    Auto-check passed
  • Limoni Text Rendering

    thebanri/limoni

    How Limoni measures and stores text — UAX. An agent skill from thebanri/limoni.

    152 GitHub stars~1.9k tokensUpdated 4 days ago
    Auto-check passed
  • Limoni Zest

    thebanri/limoni

    zest, the log viewer built on Limoni — how its store, filtering and LogView fit together, how it is verified (in-process, in kitty, over MCP, in the browser), and what it deliberately does not do.

    152 GitHub stars~877 tokensUpdated 4 days ago
    Auto-check passed

Questions about Limoni Agent Surface

What does Limoni Agent Surface do?

How Limoni exposes applications to AI agents and tests — the semantic tree, the automation socket, cmd/limoni-mcp, and the uitest package. Limoni Agent Surface is an agent skill from thebanri/limoni. How Limoni exposes applications to AI agents and tests — the semantic tree, the automation socket, cmd/limoni-mcp, and the uitest package.

When should I use Limoni Agent Surface?

Limoni Agent Surface fits situations like: tasks that involve Accessibility; tasks that involve MCP servers.

How do I install Limoni Agent Surface in Claude Code?

Run `npx skills add thebanri/limoni --skill limoni-agent-surface -a claude-code`. Or copy the skill folder (.claude/skills/limoni-agent-surface in thebanri/limoni) into .claude/skills/limoni-agent-surface in your project. Claude Code loads it when a task matches its description.

How do I install Limoni Agent Surface in Codex?

Run `npx skills add thebanri/limoni --skill limoni-agent-surface -a codex`. Or copy the skill folder (.claude/skills/limoni-agent-surface in thebanri/limoni) into .agents/skills/limoni-agent-surface in your project. Codex loads it when a task matches its description.

Can I use Limoni Agent Surface in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add thebanri/limoni --skill limoni-agent-surface -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/limoni-agent-surface, .gemini/skills/limoni-agent-surface, .github/skills/limoni-agent-surface and .opencode/skills/limoni-agent-surface in your project.

What does Limoni Agent Surface need to run?

Going by SKILL.md and its folder, Limoni Agent Surface needs the command-line tools its instructions call (go and claude).

Does Limoni Agent Surface access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is Limoni Agent Surface safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Limoni Agent Surface use?

Limoni Agent Surface is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Limoni Agent Surface use?

About 2.8k tokens (SKILL.md is roughly 11k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Limoni Agent Surface?

Skills that share tags, products or a category with Limoni Agent Surface: Docs (PrefectHQ/fastmcp, 28k stars), Better Design (marvkr/better-design, 254 stars), UI Automation Workflows (conorluddy/xclaude-plugin, 183 stars) and Penpot Uiux Design (github/awesome-copilot, 40k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Limoni Agent Surface?

thebanri (a GitHub user) maintains it in thebanri/limoni, which has 152 GitHub stars. The repository holds 4 skills in this directory. The repository was last updated on October 5, 2026.

Source: thebanri/limoni on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.