Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ca7b1b7
feat(claude): add extra_args config for upstream CLI flags (#407) (#408)
nathanschram Apr 22, 2026
b6c6ad6
chore: staging 0.35.3rc1 (#412)
nathanschram Apr 22, 2026
8aa3968
fix(security): Group 1A hygiene — 8 issues (#431)
nathanschram Apr 26, 2026
ac64ee4
fix(security): guard daily cost tracker with threading.Lock (#379) (#…
nathanschram Apr 26, 2026
34d029a
fix(security): voice_transcription_api_key → SecretStr (#378) (#433)
nathanschram Apr 26, 2026
c644394
chore: staging 0.35.3rc2 (#434)
nathanschram Apr 26, 2026
7244fb5
feat(security): user-extensible env allowlist + BWS_ACCESS_TOKEN defa…
nathanschram Apr 26, 2026
45bd9ee
chore: staging 0.35.3rc3 (#436)
nathanschram Apr 26, 2026
b88dc08
fix(security): allowed_user_ids startup-block + v0.35.3rc4 staging (#…
nathanschram Apr 26, 2026
84f7f02
test(security): build Basic auth header at runtime (#404) (#439)
nathanschram Apr 27, 2026
f269784
chore(security): document ControlRewindFiles + ControlMcpMessage auto…
nathanschram Apr 27, 2026
f678c71
feat(telegram): rename /trigger → /listen with deprecation alias (#29…
nathanschram Apr 27, 2026
af231ae
feat(triggers): master pause/resume toggle (#294) (#441)
nathanschram Apr 27, 2026
b613c93
feat(claude): user-configurable stream idle timeout + Type-A/B classi…
nathanschram Apr 27, 2026
f184ba7
feat(usage): subscription-usage observability + /usage debug section …
nathanschram Apr 27, 2026
cfa58e9
feat(triggers): visibility Tier 2 + Tier 3 — /config:tg page expansio…
nathanschram Apr 27, 2026
dc9b0f6
fix(claude): post-result idle timeout + "✓ turn complete" UX hint (#3…
nathanschram Apr 27, 2026
d34bdf6
feat(progress): hot-reload [progress] settings without restart (#269)…
nathanschram Apr 27, 2026
af0b892
chore: staging 0.35.3rc5 (#448)
nathanschram Apr 27, 2026
a41fe51
ci: bump dependabot/fetch-metadata from 2.5.0 to 3.1.0 (#449)
dependabot[bot] Apr 28, 2026
0ca5afd
ci: bump astral-sh/setup-uv from 7.4.0 to 8.1.0 (#451)
dependabot[bot] Apr 28, 2026
422f6c2
ci: bump actions/upload-artifact from 7.0.0 to 7.0.1 (#450)
dependabot[bot] Apr 28, 2026
d1b134f
ci: bump github/codeql-action from 3.32.6 to 4.35.2 (#452)
dependabot[bot] Apr 28, 2026
6eaded5
feat(gemini): --skip-trust default + /at trigger_source follow-up (rc…
nathanschram May 4, 2026
bd7acfb
docs(v0.35.3): comprehensive audit — sweep /trigger→/listen + add mis…
nathanschram May 4, 2026
f9432c8
ci: bump github/codeql-action from 4.35.2 to 4.35.3 (#474)
dependabot[bot] May 5, 2026
de5d37e
v0.35.3: claude runner.start prompt leak (#478) + help-centre FAQ (#4…
nathanschram May 5, 2026
6f8903c
chore: staging 0.35.3rc7 (#480)
nathanschram May 5, 2026
acb6ec0
v0.35.3rc8: long-tool visibility + post-result stall suppression (#47…
nathanschram May 7, 2026
807f858
feat: /loop and ScheduleWakeup support — opt-in Loop mode (#289) (#485)
nathanschram May 7, 2026
db42f29
docs(v0.35.3): comprehensive polish across reference, how-to, and exp…
nathanschram May 8, 2026
ea55889
docs(faq): rename docs/faq/index.md → docs/faq/faq.md (#483) (#487)
nathanschram May 8, 2026
e1a7506
fix: rc10 — AskUserQuestion multi-question crash (#488) + server_tool…
nathanschram May 8, 2026
8a05146
v0.35.3rc11: monitor + bridge bug bundle (#505 #507 #508) (#509)
nathanschram May 10, 2026
cf6f29b
fix: #510 — move ExitPlanMode plan-body prepend onto per-stream path …
nathanschram May 11, 2026
d8233e9
ci: bump github/codeql-action from 4.35.3 to 4.35.4 (#513)
dependabot[bot] May 12, 2026
1b192f1
fix: #515 — rc13 plan-summary over-fire (CLI-style brevity restored) …
nathanschram May 12, 2026
5c62279
deps: update uv-build requirement (#514)
dependabot[bot] May 12, 2026
ea2f894
docs: link Untether blog posts in README footer
nathanschram May 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .claude/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
{
"type": "command",
"command": ".claude/hooks/release-guard.sh"
},
{
"type": "command",
"command": ".claude/hooks/help-faq-protect.sh"
}
]
},
Expand Down Expand Up @@ -42,7 +46,7 @@
"hooks": [
{
"type": "prompt",
"prompt": "Check if this file edit touches an Untether runner:\n\nFile: {{tool_input.file_path}}\n\nIf the path matches src/untether/runners/*.py or src/untether/runner.py, respond with:\nRUNNER CONTEXT:\n- Maintain the 3-event contract: StartedEvent -> ActionEvent(s) -> CompletedEvent (exactly one, always last)\n- Use EventFactory for event creation (src/untether/events.py)\n- If editing claude.py: preserve PTY lifecycle (openpty/setraw/close) and session registries (_SESSION_STDIN, _REQUEST_TO_SESSION)\n- If editing translate(): update corresponding test fixtures and reference docs in docs/reference/runners/\n- Run: uv run pytest tests/test_*_runner.py tests/test_claude_control.py -x\n\nIf the path matches src/untether/schemas/*.py, respond with:\nSCHEMA CONTEXT:\n- msgspec schema changes affect JSONL parsing (fields silently ignored if absent)\n- Check runner translate() still handles the new/changed fields\n- Update reference docs (stream-json-cheatsheet.md) and test fixtures\n\nIf the path matches src/untether/telegram/*.py, respond with:\nTELEGRAM CONTEXT:\n- All writes go through TelegramOutbox (never call Bot API directly from handlers)\n- Callback data max 64 bytes (format: prefix:action:id)\n- Call answerCallbackQuery promptly to clear spinner\n- For approval buttons: use early answering (answer_early=True, early_answer_toast())\n- Ephemeral messages: register via register_ephemeral_message() for auto-cleanup\n\nOtherwise respond: PASS"
"prompt": "Check if this file edit touches an Untether runner:\n\nFile: {{tool_input.file_path}}\n\nIf the path matches src/untether/runners/*.py or src/untether/runner.py, respond with:\nRUNNER CONTEXT:\n- Maintain the 3-event contract: StartedEvent -> ActionEvent(s) -> CompletedEvent (exactly one, always last)\n- Use EventFactory for event creation (src/untether/events.py)\n- If editing claude.py: preserve PTY lifecycle (openpty/setraw/close) and session registries (_SESSION_STDIN, _REQUEST_TO_SESSION)\n- If editing translate(): update corresponding test fixtures and reference docs in docs/reference/runners/\n- Run: uv run pytest tests/test_*_runner.py tests/test_claude_control.py -x\n\nIf the path matches src/untether/schemas/*.py, respond with:\nSCHEMA CONTEXT:\n- msgspec schema changes affect JSONL parsing (fields silently ignored if absent)\n- Check runner translate() still handles the new/changed fields\n- Update reference docs (stream-json-cheatsheet.md) and test fixtures\n\nIf the path matches src/untether/telegram/*.py, respond with:\nTELEGRAM CONTEXT:\n- All writes go through TelegramOutbox (never call Bot API directly from handlers)\n- Callback data max 64 bytes (format: prefix:action:id)\n- Call answerCallbackQuery promptly to clear spinner\n- For approval buttons: use early answering (answer_early=True, early_answer_toast())\n- Ephemeral messages: register via register_ephemeral_message() for auto-cleanup\n\nIf the path matches docs/faq/* or ends with docs/faq/index.md, respond with:\nHELP-FAQ CONTEXT (#477):\n- This file backs the marketing-site FAQPage Schema.org pipeline. It MUST stay in place — the FAQ-protect Bash hook prevents deletion / move / truncate-via-redirect.\n- Edits ARE encouraged: keep H2 questions and answers current as features land in CHANGELOG.md.\n- Maintain the H2-as-question shape (each `## ` heading should end with `?` or start with How / What / Why / When / Where / Can / Do / Does / Is / Are / Should / Will). The FAQPage extractor in littlebearapps/littlebearapps.com only fires on question-shaped H2s.\n- Aim for ≥7 H2 Q/A pairs (currently 12). Don't drop below 7 without coordinating with the marketing site.\n- Keep cross-links to /tutorials/, /how-to/, /help/ alive — broken links degrade the help-centre nav chain.\n- Any new question's answer should reference real Untether behaviour, not aspirational features. Source from README, real GitHub Issues, real Telegram channels.\n- See: .claude/rules/help-faq.md\n\nOtherwise respond: PASS"
}
]
},
Expand All @@ -62,7 +66,7 @@
"hooks": [
{
"type": "prompt",
"prompt": "Check if this edit bumped a version in pyproject.toml:\n\nFile: {{tool_input.file_path}}\n\nIf the file path ends with 'pyproject.toml' AND the edit changed a line containing 'version =', respond with:\nRELEASE CHECKLIST:\n1. Create GitHub issues for all bug fixes/changes in this release (label: bug/enhancement)\n2. Add CHANGELOG.md entry: `## vX.Y.Z (YYYY-MM-DD)` with issue links [#N](...)\n3. Run `uv lock` to sync the lockfile\n4. Verify: `uv run pytest && uv run ruff check src/`\n5. Semantic versioning: patch (bug fixes), minor (new features), major (breaking changes)\n\n⚠️ MANDATORY INTEGRATION TESTING — see docs/reference/integration-testing.md\nAll tiers are fully automated via Telegram MCP tools (send_message, get_history, list_inline_buttons, press_inline_button, reply_to_message, send_voice, send_file) and Bash (journalctl, kill -TERM).\nBefore tagging this release, run the integration test suite against @untether_dev_bot (NEVER use @hetz_lba1_bot for dev testing):\n- PATCH: Tier 7 (command smoke) + Tier 1 (affected engine + Claude) + relevant Tier 6 (~30 min)\n- MINOR: Tier 7 + Tier 1 (all 6 engines) + Tier 2 (Claude interactive) + relevant Tier 3-4 + Tier 6 + upgrade path (~75 min)\n- MAJOR: ALL tiers (1-7), ALL engines, full upgrade path testing (~120 min)\n\nRestart dev bot first: systemctl --user restart untether-dev\nTail logs: journalctl --user -u untether-dev -f\nAfter tests: check logs for warnings/errors, create GitHub issues for Untether bugs, note engine quirks separately.\nDo NOT skip integration testing. Unit tests alone are insufficient.\n\n⚠️ MANDATORY STAGING (minor/major releases):\n6. Stage rc: bump to X.Y.ZrcN, push master → TestPyPI, install via scripts/staging.sh install, dogfood on @hetz_lba1_bot for 1+ week\n7. Only after staging is stable: bump to X.Y.Z final, write changelog, tag vX.Y.Z\nNEVER skip staging for minor/major releases. NEVER go directly from dev testing to PyPI tagging.\n\nOtherwise respond: PASS"
"prompt": "Check if this edit bumped a version in pyproject.toml:\n\nFile: {{tool_input.file_path}}\n\nIf the file path ends with 'pyproject.toml' AND the edit changed a line containing 'version =', respond with:\nRELEASE CHECKLIST:\n1. Create GitHub issues for all bug fixes/changes in this release (label: bug/enhancement)\n2. Add CHANGELOG.md entry: `## vX.Y.Z (YYYY-MM-DD)` with issue links [#N](...)\n3. Run `uv lock` to sync the lockfile\n4. Verify: `uv run pytest && uv run ruff check src/`\n5. Semantic versioning: patch (bug fixes), minor (new features), major (breaking changes)\n6. FAQ TOUCH-UP CHECK (#477): scan the new CHANGELOG entries against `docs/faq/index.md`. If any entry changes engine support, auth/billing, privacy/data flow, approval semantics, cost budgets, voice transcription, or install/update/uninstall paths — update the FAQ in the SAME release branch. Edits via Edit/Write are encouraged; deletion is gate-protected by `.claude/hooks/help-faq-protect.sh`. See `.claude/rules/help-faq.md`.\n\n⚠️ MANDATORY INTEGRATION TESTING — see docs/reference/integration-testing.md\nAll tiers are fully automated via Telegram MCP tools (send_message, get_history, list_inline_buttons, press_inline_button, reply_to_message, send_voice, send_file) and Bash (journalctl, kill -TERM).\nBefore tagging this release, run the integration test suite against @untether_dev_bot (NEVER use @hetz_lba1_bot for dev testing):\n- PATCH: Tier 7 (command smoke) + Tier 1 (affected engine + Claude) + relevant Tier 6 (~30 min)\n- MINOR: Tier 7 + Tier 1 (all 6 engines) + Tier 2 (Claude interactive) + relevant Tier 3-4 + Tier 6 + upgrade path (~75 min)\n- MAJOR: ALL tiers (1-7), ALL engines, full upgrade path testing (~120 min)\n\nRestart dev bot first: systemctl --user restart untether-dev\nTail logs: journalctl --user -u untether-dev -f\nAfter tests: check logs for warnings/errors, create GitHub issues for Untether bugs, note engine quirks separately.\nDo NOT skip integration testing. Unit tests alone are insufficient.\n\n⚠️ MANDATORY STAGING (minor/major releases):\n6. Stage rc: bump to X.Y.ZrcN, push master → TestPyPI, install via scripts/staging.sh install, dogfood on @hetz_lba1_bot for 1+ week\n7. Only after staging is stable: bump to X.Y.Z final, write changelog, tag vX.Y.Z\nNEVER skip staging for minor/major releases. NEVER go directly from dev testing to PyPI tagging.\n\nOtherwise respond: PASS"
},
{
"type": "command",
Expand Down
90 changes: 90 additions & 0 deletions .claude/hooks/help-faq-protect.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/bin/bash
# help-faq-protect.sh — PreToolUse hook for Bash tool
# Blocks deletion / move-out-of-place of `docs/faq/faq.md`.
# The file is part of the marketing-site FAQPage Schema.org pipeline
# (issue #477; renamed from index.md → faq.md in #483 to expose
# `/help/untether/faq/` instead of `/help/untether/index/`).
# Removing it breaks the docs-sync mapping registered in
# `littlebearapps/littlebearapps.com:scripts/docs-sync.config.ts` and
# would silently regress AI-citation surface (ChatGPT, Perplexity,
# Google AI Overviews) on the next deploy.
#
# This hook deliberately does NOT block edits — the FAQ is meant to be
# updated as features land. It only blocks destructive ops (rm, git rm,
# mv away, redirected truncation).

set -euo pipefail

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null)
[ -z "$COMMAND" ] && echo '{}' && exit 0

# Helper: emit Claude Code PreToolUse deny shape (2026+).
deny() {
jq -n --arg r "$1" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: $r
}
}'
exit 0
}

match_target='(^|[^A-Za-z0-9_/])docs/faq/(faq\.md|\*|.\*|\.\.|.*\.md)?'

# 1. `rm` / `unlink` / `shred`
if echo "$COMMAND" | grep -qE '(^|[^A-Za-z_])(rm|unlink|shred)([[:space:]]|$)'; then
if echo "$COMMAND" | grep -qE "$match_target"; then
deny "🛑 HELP-FAQ PROTECTION: docs/faq/faq.md cannot be deleted.

This file backs the marketing-site FAQPage Schema.org pipeline
(see issue #477). Removing it silently regresses AI-citation
surface on the next docs-sync deploy.

You CAN edit it freely — the FAQ should be updated as features
land. To replace content, edit in-place; do not delete and recreate.

To genuinely retire the FAQ, raise an issue first to coordinate
the matching mapping removal in
\`littlebearapps/littlebearapps.com:scripts/docs-sync.config.ts\`."
fi
fi

# 2. `git rm`
if echo "$COMMAND" | grep -qE '\bgit\b[[:space:]]+rm\b'; then
if echo "$COMMAND" | grep -qE "$match_target"; then
deny "🛑 HELP-FAQ PROTECTION: docs/faq/faq.md cannot be \`git rm\`'d.

The file backs the marketing-site FAQPage Schema.org pipeline (#477).
Edit in place instead. If retirement is genuinely needed, coordinate
with littlebearapps/littlebearapps.com first."
fi
fi

# 3. `mv` away from docs/faq/.
if echo "$COMMAND" | grep -qE '(^|[^A-Za-z_])mv([[:space:]]|$)'; then
if echo "$COMMAND" | grep -qE 'docs/faq/faq\.md[[:space:]]+[^[:space:]]+'; then
deny "🛑 HELP-FAQ PROTECTION: docs/faq/faq.md cannot be moved.

The path is referenced by the marketing-site docs-sync config
(\`scripts/docs-sync.config.ts\` in littlebearapps/littlebearapps.com).
Renaming/moving silently breaks the FAQPage schema pipeline (#477).

Edit in place. To genuinely relocate, coordinate with the marketing
site first."
fi
fi

# 4. Redirect truncation (`>` not `>>`).
if echo "$COMMAND" | grep -qE '(^|[^>])>[[:space:]]*docs/faq/faq\.md\b'; then
deny "🛑 HELP-FAQ PROTECTION: shell redirect (\`>\`) would truncate docs/faq/faq.md.

Use the Edit tool for in-place changes, or \`>>\` to append, so the
file's identity (and the FAQPage schema pipeline #477) is preserved.

If you need to fully replace the file content, use the Write tool —
that's an in-place rewrite, not a deletion."
fi

echo '{}'
3 changes: 3 additions & 0 deletions .claude/hooks/release-guard-protect.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ case "$FILE_PATH" in
*/release-guard.sh | */release-guard-protect.sh | */release-guard-mcp.sh)
deny "🛑 RELEASE GUARD: This file is protected.\n\nRelease guard hooks can only be edited manually by Nathan.\nProtected: .claude/hooks/release-guard*.sh"
;;
*/help-faq-protect.sh)
deny "🛑 HELP-FAQ PROTECTION: This hook script is protected.\n\nThe FAQ-protect hook can only be edited manually by Nathan to prevent silent removal of docs/faq/index.md (issue #477).\nProtected: .claude/hooks/help-faq-protect.sh"
;;
*/.claude/hooks.json)
deny "🛑 RELEASE GUARD: .claude/hooks.json is protected.\n\nHook configuration must be edited manually by Nathan to prevent removal of release guard hooks."
;;
Expand Down
117 changes: 117 additions & 0 deletions .claude/rules/help-faq.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Help-Centre FAQ Rules (`docs/faq/faq.md`)

`docs/faq/faq.md` is the user-facing FAQ for Untether. It backs the
marketing-site **FAQPage Schema.org** pipeline shipped in
[`littlebearapps/littlebearapps.com`](https://github.com/littlebearapps/littlebearapps.com)
on `feature/help-seo-geo-items-1-4`. Once the docs-sync mapping (`scripts/docs-sync.config.ts`)
under the `untether` entry references `docs/faq` with `category: faq`,
the marketing site emits `<script type="application/ld+json">` `FAQPage`
JSON-LD on every deploy — unlocking AI-citation surface (ChatGPT,
Perplexity, Google AI Overviews) and SERP rich-snippet eligibility for
the Untether help articles.

Tracking issue: [#477](https://github.com/littlebearapps/untether/issues/477).

## Hard rules

### NEVER delete or move the file

- The path is referenced by the upstream marketing-site sync config.
Removing it silently breaks the docs-sync mapping (`build-error` per
the issue's "Coordinated mapping" note) and regresses the FAQPage
schema on the next deploy.
- The repo enforces this via `.claude/hooks/help-faq-protect.sh`
(PreToolUse Bash hook). It blocks `rm`, `git rm`, `mv`-away, and
shell-redirect (`>`) truncation. Append (`>>`) and Edit/Write are
intentionally NOT blocked — the FAQ is meant to evolve.
- To genuinely retire the FAQ, raise an issue first to coordinate the
matching mapping removal in `littlebearapps/littlebearapps.com`.

### MUST stay current with feature changes

- Treat the FAQ like a contract with users. Whenever a new feature
lands in `CHANGELOG.md`, ask: does the existing FAQ still answer
questions correctly?
- Specifically watch for:
- **Engine support changes** — Q3 ("Which AI coding agents…")
enumerates the 6 supported engines. If a new engine lands or one is
deprecated, update.
- **Subscription / API key model changes** — Q4 ("Do I need an API
key?") describes which engines use OAuth vs API key. Any auth-flow
changes need an FAQ refresh.
- **Privacy / data flow changes** — Q5 ("Where does my code and data
go?") covers Telegram, agent CLI, vendor, and Untether itself.
Any new outbound network call or telemetry MUST be reflected here.
- **Approval-flow changes** — Q6 ("How do I approve tool calls…")
documents Plan mode buttons and `/planmode` semantics. Any change
to ExitPlanMode, ask-mode, or per-engine approval policies needs
an FAQ pass.
- **Cost / budget changes** — Q8 ("How do I keep agents from
spending too much…") shows `[cost_budget]` config. New keys, new
budget types, or new auto-cancel behaviour need an FAQ refresh.
- **Voice transcription changes** — Q9 ("Can I send voice notes…")
references `voice_transcription_*` config keys. Renames or new
keys need an FAQ pass.
- **Install / update / uninstall path changes** — Q2/Q10/Q11 cover
`uv tool` and `pipx` flows. Any change to the wizard, default
config path, or systemd integration needs an FAQ refresh.

## Soft conventions

### Question shape

- Each `## ` heading MUST be a question. The FAQPage extractor in the
marketing site only fires on question-shaped H2s — bare topic
headings like `## Installation` are silently ignored by the schema.
- Phrase as: ends with `?`, OR starts with How / What / Why / When /
Where / Can / Do / Does / Is / Are / Should / Will.
- Aim for ≥7 H2 Q/A pairs (the issue's acceptance criterion). Currently
ships with 12. Don't drop below 7 without coordinating with the
marketing site.

### Answer style

- Each answer is a complete paragraph (or short bullet list with a
closing sentence). No `TODO`, no `[placeholder]`, no `TBD`.
- Cross-link to existing help-guide URLs (`/tutorials/`, `/how-to/`,
`/help/`). Broken links degrade the help-centre nav chain — verify
links resolve before merging.
- Answers should describe **real Untether behaviour**, not aspirational
features. Source from README, real GitHub Issues, Telegram
community channels.

### Frontmatter

- Keep frontmatter minimal: `title` + `description` only. The
marketing-site sync injects `category: faq`, `tool: untether`, and
dates automatically.
- Don't manually set `category` or `tool` here — that's the sync
pipeline's job.

## When to update during a release

Suggested cadence as part of the [release-discipline](./release-discipline.md)
workflow:

1. After drafting the CHANGELOG entry for a new release, scan the
entries against the "MUST stay current" list above.
2. If any FAQ-relevant entry exists, edit `docs/faq/faq.md`
in-place. Rephrase, add a new Q/A, or update the cross-link.
3. Commit the FAQ touch-up alongside the release commits in the same
feature branch (don't fragment into a separate PR unless the FAQ
change is substantial).
4. The marketing-site sync runs nightly and on demand — no manual
trigger needed once the file is updated.

## After changes

```bash
# 1. Verify shape: ≥7 H2 question-shaped headings, no placeholders
grep -c '^## ' docs/faq/faq.md # should be ≥ 7
grep -ciE 'TODO|\[placeholder\]|TBD|XXX' docs/faq/faq.md # should be 0

# 2. Verify each H2 starts with a question word OR ends with ?
grep '^## ' docs/faq/faq.md | \
grep -vE '^##.*\?$|^## (How|What|Why|When|Where|Can|Do|Does|Is|Are|Should|Will)\b'
# (no output = all H2s are question-shaped)
```
1 change: 1 addition & 0 deletions .claude/rules/release-discipline.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
3. Every changelog entry must link to a GitHub issue: `[#N](https://github.com/littlebearapps/untether/issues/N)`
4. Run `uv lock` to sync the lockfile
5. **Run integration tests against `@untether_dev_bot`** — see below and `docs/reference/integration-testing.md`
6. **FAQ touch-up check (`docs/faq/faq.md`)** — scan the new CHANGELOG entries against the help-centre FAQ. If any entry changes engine support, auth/billing model, privacy/data flow, approval semantics, cost budgets, voice transcription config, install/update/uninstall paths, or any other user-facing surface answered by the FAQ, update `docs/faq/faq.md` in the same release branch. The file is gate-protected — Bash `rm`/`mv`/`>` are blocked by `help-faq-protect.sh`, but Edit/Write are encouraged. See [`help-faq.md`](./help-faq.md) for the full update cadence and shape rules. Tracking issue: [#477](https://github.com/littlebearapps/untether/issues/477).

## Semantic versioning

Expand Down
Loading
Loading