Update CLAUDE.md with release tagging instructions and enhance is_question logic to detect question marks anywhere in the text

This commit is contained in:
2026-08-05 17:53:32 -06:00
parent c4e805defd
commit 8d4751d80f
3 changed files with 34 additions and 12 deletions
+23
View File
@@ -47,6 +47,12 @@ python scripts/generate_bolt_sprites.py # --out /tmp/x to preview fi
python scripts/slice_spritesheet.py path/to/sheet.png assets/sprites/idle --cols 6 --rows 1
```
**Cutting a release:** bump `__version__` in `bolt_pet/__init__.py` in the same
commit you tag, because that string — not the git history — is what every
already-installed pet compares against the newest Gitea tag (`updater.py`). A
tag without the bump means nobody updates; a bump without the tag means the
next tag looks older than what's running.
There is no lint/build step configured beyond pytest. `cp .env.example .env`
and fill in `BOLT_SERVER_URL` / `DESK_API_KEY` (+ `DEEPGRAM_API_KEY`,
`ELEVENLABS_API_KEY`) before running — without server config the controller
@@ -207,6 +213,23 @@ logs a missing-config message and exits its thread instead of starting.
notes below); it's a safer path to the same capability. Pure parsing
(`parse`) is separated from the filesystem I/O (`execute`), matching
pet_actions.py's parse/describe split.
- **`relay_json.py`** — the JSON parser both `filectl` and `dialoguectl` use
instead of `json.loads`, because their payload is hand-typed by a model into
a tool marker and fails in a small, repeatable set of ways (stray quote after
a bare literal, trailing comma, single or smart quotes, Python `True`/`False`,
a markdown fence). Strict parsing already cost a live turn: the call was
rejected, the model re-sent the identical line, was rejected again, and then
told the user "I'll check now" without ever calling anything. So `loads()`
tries strict first, then applies **named, individually-narrow repairs** and
accepts one only if the result parses — and on total failure raises
`RelayJsonError` carrying a caret pointed at the offending character, since
a model can act on a pointed-at fragment but not on "Expecting ',' delimiter:
char 74". Two conventions matter for any new relayed-JSON command: repairs
are **never silent**`parse` stashes them on the action as `_repairs` and
`describe` appends `relay_json.repair_note(...)` to the tool result, so the
model is told it sent something broken while it still has the turn — and new
repairs go in the `_REPAIRS` tuple ordered cheapest/safest first. Tested
inside `tests/test_file_ops.py`, not a file of its own.
- **`screen_context.py`** — active-window title (xprop/xdotool, Win32,
osascript) appended to each utterance via `context_for()`, plus
`is_fullscreen_active()` for do-not-disturb. Text only — the desk API takes
+8 -10
View File
@@ -119,19 +119,17 @@ def for_speech(text: str) -> str:
def is_question(text: str) -> bool:
"""True if the reply *ends* by asking the user something — the cue for
the pet to keep listening instead of making you say the wake word again.
"""True if the spoken reply contains a question mark — the cue for the
pet to keep listening instead of making you say the wake word again.
Deliberately only looks at the end. A reply that asks something in
passing ("What time is it? It's 7:15.") isn't waiting on an answer,
whereas one that finishes on a question mark is. The test runs on the
spoken form, so a '?' that only exists inside a stripped code block or a
URL doesn't count, and trailing decoration (emoji, quotes, brackets) is
peeled off first so "Ready to go? 🚀" still reads as a question."""
The check runs on the spoken form, so a '?' that only exists inside a
stripped code block or a URL doesn't count, and trailing decoration
(emoji, quotes, brackets) is peeled off first so "Ready to go? 🚀"
still reads as a question."""
spoken = for_speech(text)
while spoken and not (spoken[-1].isalnum() or spoken[-1] == "?"):
while spoken and not (spoken[-1].isalnum() or spoken[-1] in "?."):
spoken = spoken[:-1]
return spoken.endswith("?")
return "?" in spoken
def for_display(text: str) -> str:
+3 -2
View File
@@ -59,11 +59,12 @@ def test_display_keeps_emoji_but_drops_markdown():
assert for_display("* one\n* two") == "• one • two"
def test_is_question_only_fires_on_a_trailing_question():
def test_is_question_fires_when_a_question_mark_appears_anywhere():
assert is_question("Ready to run a command or start a project?")
assert is_question("It's 7:15 AM. Want me to set a timer?")
assert is_question("What time is it? It's 7:15 AM.")
assert is_question("Can you help me with this? I need a quick answer.")
assert not is_question("It's 7:15 AM on July 23, 2026.")
assert not is_question("What time is it? It's 7:15 AM.") # asked in passing
def test_is_question_ignores_trailing_decoration():