diff --git a/.claude/hooks.json b/.claude/hooks.json index 446cbc88..2bee3573 100644 --- a/.claude/hooks.json +++ b/.claude/hooks.json @@ -7,6 +7,10 @@ { "type": "command", "command": ".claude/hooks/release-guard.sh" + }, + { + "type": "command", + "command": ".claude/hooks/help-faq-protect.sh" } ] }, @@ -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" } ] }, @@ -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", diff --git a/.claude/hooks/help-faq-protect.sh b/.claude/hooks/help-faq-protect.sh new file mode 100755 index 00000000..1f3975f8 --- /dev/null +++ b/.claude/hooks/help-faq-protect.sh @@ -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 '{}' diff --git a/.claude/hooks/release-guard-protect.sh b/.claude/hooks/release-guard-protect.sh index 6cc1e682..f3487155 100755 --- a/.claude/hooks/release-guard-protect.sh +++ b/.claude/hooks/release-guard-protect.sh @@ -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." ;; diff --git a/.claude/rules/help-faq.md b/.claude/rules/help-faq.md new file mode 100644 index 00000000..1de2701a --- /dev/null +++ b/.claude/rules/help-faq.md @@ -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 `