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
|
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`
|
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`,
|
and fill in `BOLT_SERVER_URL` / `DESK_API_KEY` (+ `DEEPGRAM_API_KEY`,
|
||||||
`ELEVENLABS_API_KEY`) before running — without server config the controller
|
`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
|
notes below); it's a safer path to the same capability. Pure parsing
|
||||||
(`parse`) is separated from the filesystem I/O (`execute`), matching
|
(`parse`) is separated from the filesystem I/O (`execute`), matching
|
||||||
pet_actions.py's parse/describe split.
|
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,
|
- **`screen_context.py`** — active-window title (xprop/xdotool, Win32,
|
||||||
osascript) appended to each utterance via `context_for()`, plus
|
osascript) appended to each utterance via `context_for()`, plus
|
||||||
`is_fullscreen_active()` for do-not-disturb. Text only — the desk API takes
|
`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:
|
def is_question(text: str) -> bool:
|
||||||
"""True if the reply *ends* by asking the user something — the cue for
|
"""True if the spoken reply contains a question mark — the cue for the
|
||||||
the pet to keep listening instead of making you say the wake word again.
|
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
|
The check runs on the spoken form, so a '?' that only exists inside a
|
||||||
passing ("What time is it? It's 7:15.") isn't waiting on an answer,
|
stripped code block or a URL doesn't count, and trailing decoration
|
||||||
whereas one that finishes on a question mark is. The test runs on the
|
(emoji, quotes, brackets) is peeled off first so "Ready to go? 🚀"
|
||||||
spoken form, so a '?' that only exists inside a stripped code block or a
|
still reads as a question."""
|
||||||
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)
|
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]
|
spoken = spoken[:-1]
|
||||||
return spoken.endswith("?")
|
return "?" in spoken
|
||||||
|
|
||||||
|
|
||||||
def for_display(text: str) -> str:
|
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"
|
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("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("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("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():
|
def test_is_question_ignores_trailing_decoration():
|
||||||
|
|||||||
Reference in New Issue
Block a user