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:
@@ -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
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user