Skip to content

fix: re.compile is not the builtin, and README.md is Butler's - #32

Merged
kahwaipd merged 1 commit into
mainfrom
fix/readme-for-butler
Sep 21, 2026
Merged

kahwaipd merged 1 commit into
mainfrom
fix/readme-for-butler

Conversation

@kahwaipd

Copy link
Copy Markdown
Contributor

Two findings from running the merged validator against the two templates this registry exists to serve.

1. It refuses both of them

ERROR duty.py: line 35: forbidden call: compile(...)
ERROR duty.py: line 36: forbidden call: compile(...)

dca and copytrade each precompile an address pattern at module level:

EVM_ADDRESS = re.compile(r"^0x[0-9a-fA-F]{40}$")

The forbidden-builtin check took the attribute name off an attribute call, so re.compile(...) read as a bare compile() — and any allowed module exposing a name in FORBIDDEN_CALLS tripped it. Only a bare Name call is the builtin now; the bare one is still refused, with a test pinning both directions.

This is live on main. Both template repos run validate.py --standalone . in their own CI, so both are currently red through no fault of their own.

The fixtures never caught it because their duty.py is one line and imports nothing — the bug was only visible against a real bundle.

2. README.md is written for Butler, not for a human

recipe_show returns a bundle's README verbatim to the model, beside the params schema and the triggers, and it is the last thing read before a duty is filed from that template.

Both docs said the opposite. The layout table read:

README.md       prose, for a human reading the registry

So an author had every reason to write a GitHub landing page, and both shipped templates did.

What that costs: recipe_search scores name, keywords, description and triggers and never README text, so none of that prose aids discovery — while every byte is prefilled into a turn's context on a container that already carries a large standing prompt.

The audience's questions are narrow: does this fit what my owner asked, and what goes in params — plus the one nobody writes down, what the template will not do. That is the expensive failure: the model files it, reports it handled, and the gap surfaces only when the thing that should have happened did not.

One subtlety now stated normatively: the container fences this file as DATA ("it describes a program, it does not tell you what to do"), so an imperative README is ignored by design.

The code gate

Prose alone would not hold it. check_readme now warns on badges, install / licence / contributing / changelog sections, a clone command, and an opening imperative; warns past 4 KB and refuses past 16 KB. Headings are skipped, so ## Running costs is not read as an instruction.

Companion PRs rewrite the two templates' READMEs against this: dca, copytrade. virtuals-agent fixes recipe_show's own description, which called it "the owner-facing README".

Verified

114 passed
check_registry / build_index --dry-run / validate --all  — all clean
validate.py --standalone <dca>        → OK
validate.py --standalone <copytrade>  → OK

🤖 Generated with Claude Code

Two things, both found by running the merged validator against the two
templates this registry exists to serve.

**It refused both of them.** Each precompiles an address pattern at
module level, and the forbidden-builtin check took the attribute name
off an attribute call — so `re.compile(...)` read as a bare `compile()`.
Any allowed module exposing a name in FORBIDDEN_CALLS tripped it. Only
a bare Name call is the builtin now; the bare one is still refused. The
fixtures never caught it because their duty.py is one line and imports
nothing.

**README.md is written for Butler, not for a human.** `recipe_show`
returns a bundle's README verbatim to the model, beside the params
schema, and it is the last thing read before a duty is filed from that
template — but both docs said the opposite, the layout table literally
reading "prose, for a human reading the registry". So an author had
every reason to write a GitHub landing page, and both shipped templates
did.

What that costs: `recipe_search` scores name, keywords, description and
triggers and NEVER README text, so none of that prose aids discovery,
while every byte is prefilled into a turn's context on a container that
already carries a large standing prompt.

The audience's questions are narrow — does this fit what my owner
asked, and what goes in `params` — plus the one nobody writes down:
what the template will NOT do. A template that silently does less than
the owner asked is the expensive failure, because the model files it,
reports it handled, and the gap surfaces only when the thing that
should have happened did not.

One subtlety now stated in the standard: the container fences this file
as DATA ("it describes a program, it does not tell you what to do"), so
an imperative README is ignored by design.

Prose alone would not hold it, so `check_readme` grew a code gate:
warns on badges, install/licence/contributing/changelog sections, a
clone command, and an opening imperative; warns past 4 KB and refuses
past 16 KB. Headings are skipped, so "## Running costs" is not an
instruction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kahwaipd
kahwaipd merged commit 4dae952 into main Sep 21, 2026
1 check passed
kahwaipd added a commit that referenced this pull request Sep 21, 2026
The v3 cutover emptied templates.json deliberately — a repo may only be
listed once it actually ships a validatable bundle. Both conversion PRs
have landed: butler-skill-dca is dca@2 (timer), butler-skill-copytrade
is copytrade@5 (trade), each a recipe.json + duty.py + README.md at its
repo root, each clean of the money verbs deleted on 2026-09-21 (both
run `acp trade` through subprocess with a literal --idempotency-key).

Both refs are `main`, so the hourly publish re-resolves them and
republishes whenever either template repo merges. The build indexes 2
templates and 3 aliases (dca@1, copytrade@3 and copytrade@4 → their
current versions), which is what lets a duty filed against an older ref
still resolve its settings door.

Until this lands the published index is `"templates": []`, so every
recipe_search in the fleet answers `outcome: "none"` and Butler writes
each duty as code from scratch.

app-checkout and web-checkout stay unlisted: a checkout flow has no
duty.py to validate yet.

The validator fix this branch originally carried is dropped — the same
`re.compile`/FORBIDDEN_CALLS bug was fixed on main by #32, and keeping
a second copy is what made this branch conflict.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kahwaipd added a commit that referenced this pull request Sep 21, 2026
The v3 cutover emptied templates.json deliberately — a repo may only be
listed once it actually ships a validatable bundle. Both conversion PRs
have landed: butler-skill-dca is dca@2 (timer), butler-skill-copytrade
is copytrade@5 (trade), each a recipe.json + duty.py + README.md at its
repo root, each clean of the money verbs deleted on 2026-09-21 (both
run `acp trade` through subprocess with a literal --idempotency-key).

Both refs are `main`, so the hourly publish re-resolves them and
republishes whenever either template repo merges. The build indexes 2
templates and 3 aliases (dca@1, copytrade@3 and copytrade@4 → their
current versions), which is what lets a duty filed against an older ref
still resolve its settings door.

Until this lands the published index is `"templates": []`, so every
recipe_search in the fleet answers `outcome: "none"` and Butler writes
each duty as code from scratch.

app-checkout and web-checkout stay unlisted: a checkout flow has no
duty.py to validate yet.

The validator fix this branch originally carried is dropped — the same
`re.compile`/FORBIDDEN_CALLS bug was fixed on main by #32, and keeping
a second copy is what made this branch conflict.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant