Skip to content

feat: v0.34.5rc2 — stall suppression, cost footer, config labels, CI hardening#122

Merged
Nathan Schram (nathanschram) merged 7 commits intomasterfrom
feature/github-hardening
Mar 13, 2026
Merged

feat: v0.34.5rc2 — stall suppression, cost footer, config labels, CI hardening#122
Nathan Schram (nathanschram) merged 7 commits intomasterfrom
feature/github-hardening

Conversation

@nathanschram
Copy link
Copy Markdown
Member

@nathanschram Nathan Schram (nathanschram) commented Mar 12, 2026

Summary

  • Stall notification suppression — suppress false "No progress" Telegram notifications when cpu_active=True (extended thinking, background agents); heartbeat re-render keeps elapsed time counter ticking #121
  • Cost footer fix — suppress redundant 💰 cost footer on error runs where diagnostic context line already shows cost data #120
  • Config label cleanup — clarify /config default labels, remove redundant "Works with" lines from sub-pages #119
  • CI hardening — CODEOWNERS, action SHA pins with permission comments, release guard hooks
  • Version bump0.34.5rc10.34.5rc2 for staging dogfooding

Test plan

  • 1556 unit tests pass (81% coverage)
  • Lint clean (ruff check + format)
  • Release validation passes
  • Merge → CI publishes to TestPyPI
  • scripts/staging.sh install 0.34.5rc2 on staging bot
  • Dogfood on @hetz_lba1_bot

Closes #119, #120, #121

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added multi-layer release-guard hooks to block unsafe push/merge/edit operations.
  • Bug Fixes

    • Suppress redundant cost footer on error runs
    • Suppress stall Telegram notifications when CPU-active
  • Changes

    • Heartbeat rendering preserves elapsed time during extended thinking
    • Clarified config default labels to reflect engine-decided behavior
  • Documentation

    • Expanded release workflow and release-guard docs in CLAUDE.md and CHANGELOG
  • Tests

    • Added tests for cost-footer behavior and stall notification CPU-driven suppression
  • Chores

    • Version bumped to 0.34.5rc2
    • Updated GitHub Actions workflow versions
    • Added CODEOWNERS configuration

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 91c39545-5c02-4fb4-a4d3-af6109f6f542

📥 Commits

Reviewing files that changed from the base of the PR and between da4c6ce and 24a54e9.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml

📝 Walkthrough

Walkthrough

This PR adds three release-guard pre-tool hooks, protective scripts for Bash/Edit/MCP operations, CI workflow action bumps, CODEOWNERS, UI label updates (diff preview default corrected), stall-notification suppression when CPU is active, cost-footer gating on successful runs, a version bump, docs updates, and tests to cover the new behaviors.

Changes

Cohort / File(s) Summary
Release Guard Infrastructure
\.claude/hooks.json, \.claude/hooks/release-guard.sh, \.claude/hooks/release-guard-protect.sh, \.claude/hooks/release-guard-mcp.sh
Added three PreToolUse hooks and corresponding scripts: block unsafe Bash operations (push/merge/tag/release patterns), prevent edits to guard files and hooks.json, and block GitHub MCP merge/write ops targeting protected branches.
CI Workflows & Ownership
\.github/CODEOWNERS, \.github/workflows/ci.yml, \.github/workflows/codeql.yml, \.github/workflows/dependabot-auto-merge.yml, \.github/workflows/prerelease-deps.yml, \.github/workflows/release.yml
Bumped action versions (setup-uv → v7.4.0, download-artifact → v8.0.1, pypi publish → v1.13.0, CodeQL → v3.32.6, etc.), added inline permission comments, and set CODEOWNERS to core.
Runner / Stall & Cost Logic
src/untether/runner_bridge.py
Suppress Telegram stall notifications when cpu_active is true and trigger a heartbeat re-render; only render cost footer when run is not explicitly failed.
Mock Runner Data Model
src/untether/runners/mock.py
Added usage: dict[str, Any] to Return and ErrorReturn dataclasses and propagate step usage through ScriptRunner returns.
Config UI Labels
src/untether/telegram/commands/config.py
Changed default label semantics to "(engine decides)", removed several "Works with" lines, added _HOME_HINTS["pm"]["default"] = "agent decides", and corrected diff preview default wording to "default (off)".
Tests
tests/test_config_command.py, tests/test_exec_bridge.py
Updated diff preview test expectation to "default (off)"; added tests for cost footer suppression on error runs and visibility on success runs; added tests for stall-notification suppression vs. CPU activity.
Docs & Release
CLAUDE.md, CHANGELOG.md, pyproject.toml
Expanded release workflow and guardrail documentation, added changelog entries for hooks and stall/cost behavior, and bumped version to 0.34.5rc2 in pyproject.toml.

Sequence Diagram

sequenceDiagram
    participant Claude as Claude Code
    participant Hook as release-guard Hook
    participant GitHub as Git/GitHub (MCP/API)
    participant User as Nathan (Human)

    Claude->>Hook: PreToolUse request (tool, input)
    Hook->>Hook: Parse tool_name & tool_input
    alt Edit/Write targets protected files
        Hook-->>Claude: {decision: "block", reason: "...edit manually..."}
        Claude->>User: Show block & reason
    else Bash or MCP operation
        Hook->>Hook: Inspect command / branch / PR / refspec
        alt Unsafe (master/main, tag, merge, gh release, MCP merge/write)
            Hook-->>Claude: {decision: "block", reason: "...use feature branch / manual merge..."}
            Claude->>User: Show block & reason
        else Allowed
            Hook-->>Claude: {}
            Claude->>GitHub: Execute operation
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Poem

🐰 Guards up, I hop and stand alert,

Branches safe where humans must assert,
Hooks whisper "feature, not main,"
Nathan reviews, I cheer refrain,
Repo snug — a tidy, guarded fort.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Additional scope includes stall suppression (#121), cost footer fix (#120), CI hardening (CODEOWNERS, release guards, action updates), and version bump—all claimed in PR objectives but not explicitly linked as issues. Clarify whether issues #120 and #121 should be explicitly linked, or confirm that the extended scope aligns with the feature/github-hardening branch's broader goals.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: v0.34.5rc2 with stall suppression, cost footer, config label updates, and CI hardening enhancements.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issue #119: changed diff preview default label from '(on)' to '(off)', updated _HOME_HINTS for dp to 'buttons only', and removed redundant 'Works with' lines.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/github-hardening
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use Trivy to scan for security misconfigurations and secrets in Infrastructure as Code files.

Add a .trivyignore file to your project to customize which findings Trivy reports.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/untether/runner_bridge.py (1)

626-653: ⚠️ Potential issue | 🟠 Major

Seed the CPU baseline before sending the first stall alert.

cpu_active is guaranteed to be None on the first stalled interval because _prev_diag is only set after the warning path runs. So CPU-bound "extended thinking" sessions still emit the first false No progress Telegram notification before suppression starts.

Proposed fix
-            diag = collect_proc_diag(self.pid) if self.pid else None
+            diag = collect_proc_diag(self.pid) if self.pid else None
             last_action = self._last_action_summary()
@@
-            cpu_active = (
-                is_cpu_active(self._prev_diag, diag)
-                if self._prev_diag and diag
-                else None
-            )
+            cpu_active = (
+                is_cpu_active(self._prev_diag, diag)
+                if self._prev_diag and diag
+                else None
+            )
+
+            # Seed a baseline snapshot before emitting the first warning for a
+            # live process, otherwise every CPU-active stall gets one false
+            # "No progress" notification before suppression can kick in.
+            if self._prev_diag is None and diag is not None and diag.alive:
+                self._prev_diag = diag
+                continue

Also applies to: 707-753

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/untether/runner_bridge.py` around lines 626 - 653, The first stall
warning is emitted with cpu_active=None because self._prev_diag is only seeded
after logging; fix by seeding the baseline and skipping the initial stall alert:
before computing cpu_active/logging in the stalled-path (the block that uses
is_cpu_active, cpu_active, logger.warning and sets self._prev_diag), if
self._prev_diag is falsy and diag is present then set self._prev_diag = diag and
return/skip the warning so the next interval can compute a meaningful cpu_active
via is_cpu_active; apply the same change to the other identical stalled-path
(the block referenced around lines 707–753).
🧹 Nitpick comments (1)
.claude/hooks/release-guard.sh (1)

80-90: Self-protection may have false positives with compound commands.

The check on lines 80-81 matches if release-guard appears anywhere in the command AND a modifying command appears anywhere. A compound command like:

grep release-guard logs.txt && rm /tmp/unrelated-file

Would be blocked even though it's not modifying guard files.

Consider tightening the pattern to check if the modifying command operates on the guard file path, not just co-occurs.

💡 Potential refinement (optional)

One approach is to check for patterns where the guard file is the argument to a modifying command:

-if echo "$COMMAND" | grep -qF 'release-guard' && \
-   echo "$COMMAND" | grep -qPi '\b(rm|mv|cp|install|dd|tee|chmod|chown|unlink|truncate|shred|ln)\b|>\s'; then
+# Check if a modifying command targets the guard files directly
+if echo "$COMMAND" | grep -qPi '\b(rm|mv|cp|install|dd|tee|chmod|chown|unlink|truncate|shred|ln)\s+[^\s]*release-guard|>\s*[^\s]*release-guard'; then

However, the current conservative approach errs on the side of safety, which may be intentional for a security control.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/hooks/release-guard.sh around lines 80 - 90, The current checks set
BLOCKED/REASON when a modifying command and the strings "release-guard" or
"hooks.json" co-occur anywhere in COMMAND, causing false positives for compound
commands; update both if conditions so the regex requires the modifying verb to
be the command operating on the guard path (e.g., match patterns where a
modifying command like
rm|mv|cp|install|dd|tee|chmod|chown|unlink|truncate|shred|ln appears and is
followed within the same command segment by a path containing "release-guard" or
".claude/hooks.json" — for example use a regex that looks for
\b(cmd)\b[[:space:]]+[^&;|]*release-guard or
\b(cmd)\b[[:space:]]+[^&;|]*\.claude/hooks\.json) and apply this tightened
pattern to the two if blocks referencing "release-guard" and "hooks.json"
(preserve setting BLOCKED=true and REASON as before).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude/hooks/release-guard-protect.sh:
- Around line 12-19: The case pattern for protecting hooks misses the
repo-relative path ".claude/hooks.json", allowing bypass; update the case
statement in the release-guard-protect.sh where FILE_PATH is matched (the block
handling "*/.claude/hooks.json") to also match the repo-root
".claude/hooks.json" (either by adding a separate pattern ".claude/hooks.json"
or extending the existing pattern list) and ensure the jq error message remains
the same; modify the matching clause that emits the jq
'{"decision":"block","reason":...}' and exits so it triggers for both
".claude/hooks.json" and "*/.claude/hooks.json".

In `@CLAUDE.md`:
- Line 212: Fix the typo in the heading string "**GitHub server-side
(unbyppassable):**" by replacing "unbyppassable" with the correct word
"unbypassable" so the heading reads "**GitHub server-side (unbypassable):**".

---

Outside diff comments:
In `@src/untether/runner_bridge.py`:
- Around line 626-653: The first stall warning is emitted with cpu_active=None
because self._prev_diag is only seeded after logging; fix by seeding the
baseline and skipping the initial stall alert: before computing
cpu_active/logging in the stalled-path (the block that uses is_cpu_active,
cpu_active, logger.warning and sets self._prev_diag), if self._prev_diag is
falsy and diag is present then set self._prev_diag = diag and return/skip the
warning so the next interval can compute a meaningful cpu_active via
is_cpu_active; apply the same change to the other identical stalled-path (the
block referenced around lines 707–753).

---

Nitpick comments:
In @.claude/hooks/release-guard.sh:
- Around line 80-90: The current checks set BLOCKED/REASON when a modifying
command and the strings "release-guard" or "hooks.json" co-occur anywhere in
COMMAND, causing false positives for compound commands; update both if
conditions so the regex requires the modifying verb to be the command operating
on the guard path (e.g., match patterns where a modifying command like
rm|mv|cp|install|dd|tee|chmod|chown|unlink|truncate|shred|ln appears and is
followed within the same command segment by a path containing "release-guard" or
".claude/hooks.json" — for example use a regex that looks for
\b(cmd)\b[[:space:]]+[^&;|]*release-guard or
\b(cmd)\b[[:space:]]+[^&;|]*\.claude/hooks\.json) and apply this tightened
pattern to the two if blocks referencing "release-guard" and "hooks.json"
(preserve setting BLOCKED=true and REASON as before).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 793910ef-2c62-4dfd-991d-cbacf4b35402

📥 Commits

Reviewing files that changed from the base of the PR and between 0923054 and da4c6ce.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • .claude/hooks.json
  • .claude/hooks/release-guard-mcp.sh
  • .claude/hooks/release-guard-protect.sh
  • .claude/hooks/release-guard.sh
  • .github/CODEOWNERS
  • .github/workflows/ci.yml
  • .github/workflows/codeql.yml
  • .github/workflows/dependabot-auto-merge.yml
  • .github/workflows/prerelease-deps.yml
  • .github/workflows/release.yml
  • CHANGELOG.md
  • CLAUDE.md
  • pyproject.toml
  • src/untether/runner_bridge.py
  • src/untether/runners/mock.py
  • src/untether/telegram/commands/config.py
  • tests/test_config_command.py
  • tests/test_exec_bridge.py

Comment on lines +12 to +19
case "$FILE_PATH" in
*/release-guard.sh | */release-guard-protect.sh | */release-guard-mcp.sh)
jq -n '{"decision":"block","reason":"🛑 RELEASE GUARD: This file is protected.\n\nRelease guard hooks can only be edited manually by Nathan.\nProtected: .claude/hooks/release-guard*.sh"}'
exit 0
;;
*/.claude/hooks.json)
jq -n '{"decision":"block","reason":"🛑 RELEASE GUARD: .claude/hooks.json is protected.\n\nHook configuration must be edited manually by Nathan to prevent removal of release guard hooks."}'
exit 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Protect the repo-root .claude/hooks.json path too.

*/.claude/hooks.json misses the common repo-relative path .claude/hooks.json, so this self-protection can be bypassed by editing that file directly.

Proposed fix
 case "$FILE_PATH" in
-  */release-guard.sh | */release-guard-protect.sh | */release-guard-mcp.sh)
+  .claude/hooks/release-guard.sh | */release-guard.sh | \
+  .claude/hooks/release-guard-protect.sh | */release-guard-protect.sh | \
+  .claude/hooks/release-guard-mcp.sh | */release-guard-mcp.sh)
     jq -n '{"decision":"block","reason":"🛑 RELEASE GUARD: This file is protected.\n\nRelease guard hooks can only be edited manually by Nathan.\nProtected: .claude/hooks/release-guard*.sh"}'
     exit 0
     ;;
-  */.claude/hooks.json)
+  .claude/hooks.json | */.claude/hooks.json)
     jq -n '{"decision":"block","reason":"🛑 RELEASE GUARD: .claude/hooks.json is protected.\n\nHook configuration must be edited manually by Nathan to prevent removal of release guard hooks."}'
     exit 0
     ;;
 esac
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "$FILE_PATH" in
*/release-guard.sh | */release-guard-protect.sh | */release-guard-mcp.sh)
jq -n '{"decision":"block","reason":"🛑 RELEASE GUARD: This file is protected.\n\nRelease guard hooks can only be edited manually by Nathan.\nProtected: .claude/hooks/release-guard*.sh"}'
exit 0
;;
*/.claude/hooks.json)
jq -n '{"decision":"block","reason":"🛑 RELEASE GUARD: .claude/hooks.json is protected.\n\nHook configuration must be edited manually by Nathan to prevent removal of release guard hooks."}'
exit 0
case "$FILE_PATH" in
.claude/hooks/release-guard.sh | */release-guard.sh | \
.claude/hooks/release-guard-protect.sh | */release-guard-protect.sh | \
.claude/hooks/release-guard-mcp.sh | */release-guard-mcp.sh)
jq -n '{"decision":"block","reason":"🛑 RELEASE GUARD: This file is protected.\n\nRelease guard hooks can only be edited manually by Nathan.\nProtected: .claude/hooks/release-guard*.sh"}'
exit 0
;;
.claude/hooks.json | */.claude/hooks.json)
jq -n '{"decision":"block","reason":"🛑 RELEASE GUARD: .claude/hooks.json is protected.\n\nHook configuration must be edited manually by Nathan to prevent removal of release guard hooks."}'
exit 0
;;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/hooks/release-guard-protect.sh around lines 12 - 19, The case
pattern for protecting hooks misses the repo-relative path ".claude/hooks.json",
allowing bypass; update the case statement in the release-guard-protect.sh where
FILE_PATH is matched (the block handling "*/.claude/hooks.json") to also match
the repo-root ".claude/hooks.json" (either by adding a separate pattern
".claude/hooks.json" or extending the existing pattern list) and ensure the jq
error message remains the same; modify the matching clause that emits the jq
'{"decision":"block","reason":...}' and exits so it triggers for both
".claude/hooks.json" and "*/.claude/hooks.json".


Multi-layer protection prevents accidental merges to master and PyPI publishes. Claude Code cannot circumvent these protections.

**GitHub server-side (unbyppassable):**
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the typo in this heading.

unbyppassable should be unbypassable.

🧰 Tools
🪛 LanguageTool

[grammar] ~212-~212: Ensure spelling is correct
Context: ...ese protections. GitHub server-side (unbyppassable): - Branch ruleset "Protect maste...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` at line 212, Fix the typo in the heading string "**GitHub
server-side (unbyppassable):**" by replacing "unbyppassable" with the correct
word "unbypassable" so the heading reads "**GitHub server-side
(unbypassable):**".

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nathanschram Nathan Schram (nathanschram) merged commit c96a7bc into master Mar 13, 2026
21 checks passed
@nathanschram Nathan Schram (nathanschram) deleted the feature/github-hardening branch March 13, 2026 00:31
Nathan Schram (nathanschram) added a commit that referenced this pull request Mar 14, 2026
* ci: add CODEOWNERS, update action SHA pins, add permission comments

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add release guard hooks and document protection in CLAUDE.md

Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify /config default labels and remove redundant "Works with" lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress redundant cost footer on error runs

When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: suppress stall notifications when CPU-active + heartbeat re-render

When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.34.5rc2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: CI release-validation tomllib bytes/str mismatch

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: integrate screenshots into docs with correct JPG references

- Add 44 screenshots to docs/assets/screenshots/
- Fix all image refs from .png to .jpg across 25 doc files
- README uses absolute raw.githubusercontent.com URLs for PyPI rendering
- Fix 5 filename mismatches (session-auto-resume→chat-auto-resume, etc.)
- Comment out 11 missing screenshots with TODO markers
- Add CAPTURES.md checklist tracking capture status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: convert markdown images to HTML img tags for GitHub compatibility

Switch from MkDocs `![alt](src){ loading=lazy }` syntax to HTML `<img>`
tags with width="360" and loading="lazy". Fixes two GitHub rendering
issues: `{ loading=lazy }` appearing as visible text, and oversized
images with no width constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix 3 screenshot mismatches and replace 3 screenshots

- first-run.md: rewrite resume line text to match footer screenshot
- interactive-control.md: update planmode show admonition to match screenshot (auto not on)
- switch-engines.md: swap engine-footer.jpg for multi-engine-switch.jpg
- Replace startup-message.jpg with clean v0.34.4 capture (was rc/6-projects)
- Replace cooldown-auto-deny.jpg with post-outline approve/deny buttons
- Replace file-put.jpg with photo save confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add iOS caption limitation note to file transfer guide

Telegram iOS doesn't show a caption field when sending documents via the
File picker, so /file put <path> captions aren't easily accessible.
Added a note with workarounds (use Desktop, send as photo, or let
auto-save handle it). Updated screenshot alt text to match actual
screenshot content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: temp swap README image URLs to feature branch for preview

Will revert to master before merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: lay out all 3 README screenshots in a single row

Reduce from 360px to 270px each and combine into one <p> block
so all three hero screenshots sit side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap 3rd hero screenshot for config-menu for visual variety

Replace plan-outline-approve (too similar to approval-diff-preview)
with config-menu showing the /config settings grid. The three hero
images now tell: voice input → approve changes → configure everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add captions under README hero screenshots

Small <sub> captions: "Send tasks by voice (Whisper transcription)",
"Approve changes remotely", "Configure from Telegram".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: use table layout for README hero screenshots with captions

Fixes stacking issue — <br> in a <p> broke inline flow. A table
keeps images side by side with captions underneath each one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace table layout with single hero collage image

Composite image scales proportionally on mobile instead of requiring
horizontal scroll. Captions baked into the image via ImageMagick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap middle hero screenshot for full 3-button approval view

Replace approval-diff-preview with approval-buttons-howto showing
Approve / Deny / Pause & Outline Plan — more visually impressive.
Caption now reads "Approve changes remotely (Claude Code)".
Added footnote linking to engine compatibility table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap config-menu for parallel-projects in hero collage

Third hero screenshot now shows 10+ projects running simultaneously
across different repos — much more compelling than a settings menu.
New caption: "Run agents across projects in parallel".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: revert README image URL to master for merge

Swap hero-collage URL back from feature/github-hardening to master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc3

- fix: preserve all EngineOverrides fields when setting model/planmode/reasoning
  (was silently wiping ask_questions, diff_preview, show_api_cost, etc.)
- fix: /config home page resolves "default" to effective values
- feat: file upload auto-deduplication (append _1, _2 instead of requiring --force)
- feat: media groups without captions now auto-save instead of showing usage text
- feat: resume line visual separation (blank line + ↩️ prefix)
- fix: claude auto-approve echoes updatedInput in control response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Nathan Schram (nathanschram) added a commit that referenced this pull request Mar 16, 2026
…el metadata (#132)

* ci: add CODEOWNERS, update action SHA pins, add permission comments

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add release guard hooks and document protection in CLAUDE.md

Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify /config default labels and remove redundant "Works with" lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress redundant cost footer on error runs

When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: suppress stall notifications when CPU-active + heartbeat re-render

When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.34.5rc2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: CI release-validation tomllib bytes/str mismatch

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: integrate screenshots into docs with correct JPG references

- Add 44 screenshots to docs/assets/screenshots/
- Fix all image refs from .png to .jpg across 25 doc files
- README uses absolute raw.githubusercontent.com URLs for PyPI rendering
- Fix 5 filename mismatches (session-auto-resume→chat-auto-resume, etc.)
- Comment out 11 missing screenshots with TODO markers
- Add CAPTURES.md checklist tracking capture status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: convert markdown images to HTML img tags for GitHub compatibility

Switch from MkDocs `![alt](src){ loading=lazy }` syntax to HTML `<img>`
tags with width="360" and loading="lazy". Fixes two GitHub rendering
issues: `{ loading=lazy }` appearing as visible text, and oversized
images with no width constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix 3 screenshot mismatches and replace 3 screenshots

- first-run.md: rewrite resume line text to match footer screenshot
- interactive-control.md: update planmode show admonition to match screenshot (auto not on)
- switch-engines.md: swap engine-footer.jpg for multi-engine-switch.jpg
- Replace startup-message.jpg with clean v0.34.4 capture (was rc/6-projects)
- Replace cooldown-auto-deny.jpg with post-outline approve/deny buttons
- Replace file-put.jpg with photo save confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add iOS caption limitation note to file transfer guide

Telegram iOS doesn't show a caption field when sending documents via the
File picker, so /file put <path> captions aren't easily accessible.
Added a note with workarounds (use Desktop, send as photo, or let
auto-save handle it). Updated screenshot alt text to match actual
screenshot content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: temp swap README image URLs to feature branch for preview

Will revert to master before merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: lay out all 3 README screenshots in a single row

Reduce from 360px to 270px each and combine into one <p> block
so all three hero screenshots sit side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap 3rd hero screenshot for config-menu for visual variety

Replace plan-outline-approve (too similar to approval-diff-preview)
with config-menu showing the /config settings grid. The three hero
images now tell: voice input → approve changes → configure everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add captions under README hero screenshots

Small <sub> captions: "Send tasks by voice (Whisper transcription)",
"Approve changes remotely", "Configure from Telegram".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: use table layout for README hero screenshots with captions

Fixes stacking issue — <br> in a <p> broke inline flow. A table
keeps images side by side with captions underneath each one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace table layout with single hero collage image

Composite image scales proportionally on mobile instead of requiring
horizontal scroll. Captions baked into the image via ImageMagick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap middle hero screenshot for full 3-button approval view

Replace approval-diff-preview with approval-buttons-howto showing
Approve / Deny / Pause & Outline Plan — more visually impressive.
Caption now reads "Approve changes remotely (Claude Code)".
Added footnote linking to engine compatibility table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap config-menu for parallel-projects in hero collage

Third hero screenshot now shows 10+ projects running simultaneously
across different repos — much more compelling than a settings menu.
New caption: "Run agents across projects in parallel".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: revert README image URL to master for merge

Swap hero-collage URL back from feature/github-hardening to master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc3

- fix: preserve all EngineOverrides fields when setting model/planmode/reasoning
  (was silently wiping ask_questions, diff_preview, show_api_cost, etc.)
- fix: /config home page resolves "default" to effective values
- feat: file upload auto-deduplication (append _1, _2 instead of requiring --force)
- feat: media groups without captions now auto-save instead of showing usage text
- feat: resume line visual separation (blank line + ↩️ prefix)
- fix: claude auto-approve echoes updatedInput in control response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: expand permission policies for Codex CLI and Gemini CLI in /config

Codex gets a new "Approval policy" page (full auto / safe) that passes
--ask-for-approval untrusted when safe mode is selected. Gemini's approval
mode expands from 2 to 3 tiers (read-only / edit files / full access) with
--approval-mode auto_edit for the middle tier. Both engines now show an
"Agent controls" section on the /config home page. Engine-specific model
default hints replace the generic "from CLI settings" text.

Also adds staging.sh helper, context-guard-stop hook, and docs updates.

Closes #131

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /config UX overhaul, resume line toggle, cost budget settings, model metadata

/config UX cleanup:
- Convert all binary toggles from 3-column (on/off/clear) to 2-column
  (toggle + clear) for better mobile tap targets
- Merge Engine + Model into combined "Engine & model" page
- Reorganise home page to max 2 buttons per row across all engines
- Split plan mode 3-option rows (off/on/auto) into 2+1 layout
- Add _toggle_row() helper for consistent toggle button rendering

New features:
- #128: Resume line /config toggle — per-chat show_resume_line override
  via EngineOverrides with On/Off/Clear buttons, wired into executor
- #129: Cost budget /config settings — per-chat budget_enabled and
  budget_auto_cancel overrides on the Cost & Usage page, wired into
  _check_cost_budget() in runner_bridge.py

Model metadata improvements:
- Show Claude Code [1m] context window suffix: "opus 4.6 (1M)"
- Strip Gemini CLI "auto-" prefix: "auto-gemini-3" → "gemini-3"
- Future-proof: unknown suffixes default to .upper() (e.g. [500k] → 500K)

Bug fixes:
- #124: Standalone override commands (/planmode, /model, /reasoning) now
  preserve all EngineOverrides fields including new ones
- Error handling: control_response.write_failed catch-all in claude.py,
  ask_question.extraction_failed warning, model.override.failed logging

Hardening:
- Plan outline sent as separate ephemeral message (avoids 4096 char truncation)
- Added show_resume_line, budget_enabled, budget_auto_cancel to
  EngineOverrides, EngineRunOptions, normalize/merge, and all constructors

Tests: 1610 passed, 80.56% coverage, ruff clean.
Integration tested on @untether_dev_bot across all 6 engine chats.

Closes #128, closes #129, fixes #124

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI for PR #132

* fix: address 11 CodeRabbit review comments on PR #132

Bug fixes:
- claude.py: fix UnboundLocalError when factory.resume is falsy in
  ask_question.extraction_failed logging path
- ask_question.py: reject malformed option callbacks instead of
  silently falling back to option 0
- files.py: raise FileExistsError when deduplicate_target exhausts
  999 suffixes instead of returning the original (overwrite risk)
- config.py: disambiguate Codex "Full auto" (fa) vs Gemini "Full
  access" (ya) callback IDs and toast labels

Hardening:
- codex.py: add --ask-for-approval to _EXEC_ONLY_FLAGS guard
- model.py: add try/except to clear path (matching set path)
- reasoning.py: add try/except to clear path (matching set path)
- loop.py: notify user when media group upload fails instead of
  silently dropping
- export.py: log session count instead of identifiers at info level
- config.py: resolve resume-line default from config instead of
  hardcoding True
- staging.sh: pin PyPI index in rollback/reset with --pip-args

Skipped (not applicable):
- CHANGELOG.md: RC versions don't get changelog entries per release
  discipline
- docs/tutorials TODO screenshot: pre-existing, not introduced by PR
- .claude/hooks/context-guard-stop.sh: ContextDocs plugin hook, not
  Untether source

Tests: 1611 passed, 80.48% coverage, ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace bare pass with debug log to satisfy bandit B110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: setup wizard security + UX improvements

- Auto-set allowed_user_ids from captured Telegram user ID during
  onboarding (security: restricts bot to the setup user's account)
- Add "next steps" panel after wizard completion with pointers to
  /config, voice notes, projects, and account lock confirmation
- Update install.md: Python 3.12+ (not just 3.14), dynamic version
  string, /config mention for post-setup changes
- Update first-run.md: /config → Engine & model for default engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Nathan Schram (nathanschram) added a commit that referenced this pull request Mar 17, 2026
* ci: add CODEOWNERS, update action SHA pins, add permission comments

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add release guard hooks and document protection in CLAUDE.md

Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify /config default labels and remove redundant "Works with" lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress redundant cost footer on error runs

When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: suppress stall notifications when CPU-active + heartbeat re-render

When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.34.5rc2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: CI release-validation tomllib bytes/str mismatch

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: integrate screenshots into docs with correct JPG references

- Add 44 screenshots to docs/assets/screenshots/
- Fix all image refs from .png to .jpg across 25 doc files
- README uses absolute raw.githubusercontent.com URLs for PyPI rendering
- Fix 5 filename mismatches (session-auto-resume→chat-auto-resume, etc.)
- Comment out 11 missing screenshots with TODO markers
- Add CAPTURES.md checklist tracking capture status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: convert markdown images to HTML img tags for GitHub compatibility

Switch from MkDocs `![alt](src){ loading=lazy }` syntax to HTML `<img>`
tags with width="360" and loading="lazy". Fixes two GitHub rendering
issues: `{ loading=lazy }` appearing as visible text, and oversized
images with no width constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix 3 screenshot mismatches and replace 3 screenshots

- first-run.md: rewrite resume line text to match footer screenshot
- interactive-control.md: update planmode show admonition to match screenshot (auto not on)
- switch-engines.md: swap engine-footer.jpg for multi-engine-switch.jpg
- Replace startup-message.jpg with clean v0.34.4 capture (was rc/6-projects)
- Replace cooldown-auto-deny.jpg with post-outline approve/deny buttons
- Replace file-put.jpg with photo save confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add iOS caption limitation note to file transfer guide

Telegram iOS doesn't show a caption field when sending documents via the
File picker, so /file put <path> captions aren't easily accessible.
Added a note with workarounds (use Desktop, send as photo, or let
auto-save handle it). Updated screenshot alt text to match actual
screenshot content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: temp swap README image URLs to feature branch for preview

Will revert to master before merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: lay out all 3 README screenshots in a single row

Reduce from 360px to 270px each and combine into one <p> block
so all three hero screenshots sit side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap 3rd hero screenshot for config-menu for visual variety

Replace plan-outline-approve (too similar to approval-diff-preview)
with config-menu showing the /config settings grid. The three hero
images now tell: voice input → approve changes → configure everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add captions under README hero screenshots

Small <sub> captions: "Send tasks by voice (Whisper transcription)",
"Approve changes remotely", "Configure from Telegram".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: use table layout for README hero screenshots with captions

Fixes stacking issue — <br> in a <p> broke inline flow. A table
keeps images side by side with captions underneath each one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace table layout with single hero collage image

Composite image scales proportionally on mobile instead of requiring
horizontal scroll. Captions baked into the image via ImageMagick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap middle hero screenshot for full 3-button approval view

Replace approval-diff-preview with approval-buttons-howto showing
Approve / Deny / Pause & Outline Plan — more visually impressive.
Caption now reads "Approve changes remotely (Claude Code)".
Added footnote linking to engine compatibility table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap config-menu for parallel-projects in hero collage

Third hero screenshot now shows 10+ projects running simultaneously
across different repos — much more compelling than a settings menu.
New caption: "Run agents across projects in parallel".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: revert README image URL to master for merge

Swap hero-collage URL back from feature/github-hardening to master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc3

- fix: preserve all EngineOverrides fields when setting model/planmode/reasoning
  (was silently wiping ask_questions, diff_preview, show_api_cost, etc.)
- fix: /config home page resolves "default" to effective values
- feat: file upload auto-deduplication (append _1, _2 instead of requiring --force)
- feat: media groups without captions now auto-save instead of showing usage text
- feat: resume line visual separation (blank line + ↩️ prefix)
- fix: claude auto-approve echoes updatedInput in control response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: expand permission policies for Codex CLI and Gemini CLI in /config

Codex gets a new "Approval policy" page (full auto / safe) that passes
--ask-for-approval untrusted when safe mode is selected. Gemini's approval
mode expands from 2 to 3 tiers (read-only / edit files / full access) with
--approval-mode auto_edit for the middle tier. Both engines now show an
"Agent controls" section on the /config home page. Engine-specific model
default hints replace the generic "from CLI settings" text.

Also adds staging.sh helper, context-guard-stop hook, and docs updates.

Closes #131

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /config UX overhaul, resume line toggle, cost budget settings, model metadata

/config UX cleanup:
- Convert all binary toggles from 3-column (on/off/clear) to 2-column
  (toggle + clear) for better mobile tap targets
- Merge Engine + Model into combined "Engine & model" page
- Reorganise home page to max 2 buttons per row across all engines
- Split plan mode 3-option rows (off/on/auto) into 2+1 layout
- Add _toggle_row() helper for consistent toggle button rendering

New features:
- #128: Resume line /config toggle — per-chat show_resume_line override
  via EngineOverrides with On/Off/Clear buttons, wired into executor
- #129: Cost budget /config settings — per-chat budget_enabled and
  budget_auto_cancel overrides on the Cost & Usage page, wired into
  _check_cost_budget() in runner_bridge.py

Model metadata improvements:
- Show Claude Code [1m] context window suffix: "opus 4.6 (1M)"
- Strip Gemini CLI "auto-" prefix: "auto-gemini-3" → "gemini-3"
- Future-proof: unknown suffixes default to .upper() (e.g. [500k] → 500K)

Bug fixes:
- #124: Standalone override commands (/planmode, /model, /reasoning) now
  preserve all EngineOverrides fields including new ones
- Error handling: control_response.write_failed catch-all in claude.py,
  ask_question.extraction_failed warning, model.override.failed logging

Hardening:
- Plan outline sent as separate ephemeral message (avoids 4096 char truncation)
- Added show_resume_line, budget_enabled, budget_auto_cancel to
  EngineOverrides, EngineRunOptions, normalize/merge, and all constructors

Tests: 1610 passed, 80.56% coverage, ruff clean.
Integration tested on @untether_dev_bot across all 6 engine chats.

Closes #128, closes #129, fixes #124

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI for PR #132

* fix: address 11 CodeRabbit review comments on PR #132

Bug fixes:
- claude.py: fix UnboundLocalError when factory.resume is falsy in
  ask_question.extraction_failed logging path
- ask_question.py: reject malformed option callbacks instead of
  silently falling back to option 0
- files.py: raise FileExistsError when deduplicate_target exhausts
  999 suffixes instead of returning the original (overwrite risk)
- config.py: disambiguate Codex "Full auto" (fa) vs Gemini "Full
  access" (ya) callback IDs and toast labels

Hardening:
- codex.py: add --ask-for-approval to _EXEC_ONLY_FLAGS guard
- model.py: add try/except to clear path (matching set path)
- reasoning.py: add try/except to clear path (matching set path)
- loop.py: notify user when media group upload fails instead of
  silently dropping
- export.py: log session count instead of identifiers at info level
- config.py: resolve resume-line default from config instead of
  hardcoding True
- staging.sh: pin PyPI index in rollback/reset with --pip-args

Skipped (not applicable):
- CHANGELOG.md: RC versions don't get changelog entries per release
  discipline
- docs/tutorials TODO screenshot: pre-existing, not introduced by PR
- .claude/hooks/context-guard-stop.sh: ContextDocs plugin hook, not
  Untether source

Tests: 1611 passed, 80.48% coverage, ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace bare pass with debug log to satisfy bandit B110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: setup wizard security + UX improvements

- Auto-set allowed_user_ids from captured Telegram user ID during
  onboarding (security: restricts bot to the setup user's account)
- Add "next steps" panel after wizard completion with pointers to
  /config, voice notes, projects, and account lock confirmation
- Update install.md: Python 3.12+ (not just 3.14), dynamic version
  string, /config mention for post-setup changes
- Update first-run.md: /config → Engine & model for default engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: plan outline UX — markdown rendering, buttons, cleanup (#139, #140, #141)

- Render outline messages as formatted text via render_markdown() +
  split_markdown_body() instead of raw markdown (#139)
- Add approve/deny buttons to last outline message so users don't
  have to scroll up past long outlines (#140)
- Delete outline messages on approve/deny via module-level
  _OUTLINE_REGISTRY callable from callback handler; suppress stale
  keyboard on progress message (#141)
- 8 new tests for outline rendering, keyboard placement, and cleanup
- Bump version to 0.35.0rc5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /continue command — cross-environment resume for all engines (#135)

New `/continue` command resumes the most recent CLI session in the
project directory from Telegram. Enables starting a session in your
terminal and picking it up from your phone.

Engine support: Claude (--continue), Codex (resume --last), OpenCode
(--continue), Pi (--continue), Gemini (--resume latest). AMP not
supported (requires explicit thread ID).

Includes ResumeToken.is_continue flag, build_args for all 6 runners,
reserved command registration, resume emoji prefix stripping for
reply-to-continue, docs (how-to guide, README, commands ref, routing
explanation, conversation modes tutorial), and 99 new test assertions.

Integration tested against @untether_dev_bot — all 5 supported engines
passed secret-recall verification via Telegram MCP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Nathan Schram (nathanschram) added a commit that referenced this pull request Mar 18, 2026
* ci: add CODEOWNERS, update action SHA pins, add permission comments

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add release guard hooks and document protection in CLAUDE.md

Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify /config default labels and remove redundant "Works with" lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress redundant cost footer on error runs

When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: suppress stall notifications when CPU-active + heartbeat re-render

When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.34.5rc2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: CI release-validation tomllib bytes/str mismatch

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: integrate screenshots into docs with correct JPG references

- Add 44 screenshots to docs/assets/screenshots/
- Fix all image refs from .png to .jpg across 25 doc files
- README uses absolute raw.githubusercontent.com URLs for PyPI rendering
- Fix 5 filename mismatches (session-auto-resume→chat-auto-resume, etc.)
- Comment out 11 missing screenshots with TODO markers
- Add CAPTURES.md checklist tracking capture status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: convert markdown images to HTML img tags for GitHub compatibility

Switch from MkDocs `![alt](src){ loading=lazy }` syntax to HTML `<img>`
tags with width="360" and loading="lazy". Fixes two GitHub rendering
issues: `{ loading=lazy }` appearing as visible text, and oversized
images with no width constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix 3 screenshot mismatches and replace 3 screenshots

- first-run.md: rewrite resume line text to match footer screenshot
- interactive-control.md: update planmode show admonition to match screenshot (auto not on)
- switch-engines.md: swap engine-footer.jpg for multi-engine-switch.jpg
- Replace startup-message.jpg with clean v0.34.4 capture (was rc/6-projects)
- Replace cooldown-auto-deny.jpg with post-outline approve/deny buttons
- Replace file-put.jpg with photo save confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add iOS caption limitation note to file transfer guide

Telegram iOS doesn't show a caption field when sending documents via the
File picker, so /file put <path> captions aren't easily accessible.
Added a note with workarounds (use Desktop, send as photo, or let
auto-save handle it). Updated screenshot alt text to match actual
screenshot content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: temp swap README image URLs to feature branch for preview

Will revert to master before merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: lay out all 3 README screenshots in a single row

Reduce from 360px to 270px each and combine into one <p> block
so all three hero screenshots sit side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap 3rd hero screenshot for config-menu for visual variety

Replace plan-outline-approve (too similar to approval-diff-preview)
with config-menu showing the /config settings grid. The three hero
images now tell: voice input → approve changes → configure everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add captions under README hero screenshots

Small <sub> captions: "Send tasks by voice (Whisper transcription)",
"Approve changes remotely", "Configure from Telegram".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: use table layout for README hero screenshots with captions

Fixes stacking issue — <br> in a <p> broke inline flow. A table
keeps images side by side with captions underneath each one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace table layout with single hero collage image

Composite image scales proportionally on mobile instead of requiring
horizontal scroll. Captions baked into the image via ImageMagick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap middle hero screenshot for full 3-button approval view

Replace approval-diff-preview with approval-buttons-howto showing
Approve / Deny / Pause & Outline Plan — more visually impressive.
Caption now reads "Approve changes remotely (Claude Code)".
Added footnote linking to engine compatibility table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap config-menu for parallel-projects in hero collage

Third hero screenshot now shows 10+ projects running simultaneously
across different repos — much more compelling than a settings menu.
New caption: "Run agents across projects in parallel".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: revert README image URL to master for merge

Swap hero-collage URL back from feature/github-hardening to master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc3

- fix: preserve all EngineOverrides fields when setting model/planmode/reasoning
  (was silently wiping ask_questions, diff_preview, show_api_cost, etc.)
- fix: /config home page resolves "default" to effective values
- feat: file upload auto-deduplication (append _1, _2 instead of requiring --force)
- feat: media groups without captions now auto-save instead of showing usage text
- feat: resume line visual separation (blank line + ↩️ prefix)
- fix: claude auto-approve echoes updatedInput in control response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: expand permission policies for Codex CLI and Gemini CLI in /config

Codex gets a new "Approval policy" page (full auto / safe) that passes
--ask-for-approval untrusted when safe mode is selected. Gemini's approval
mode expands from 2 to 3 tiers (read-only / edit files / full access) with
--approval-mode auto_edit for the middle tier. Both engines now show an
"Agent controls" section on the /config home page. Engine-specific model
default hints replace the generic "from CLI settings" text.

Also adds staging.sh helper, context-guard-stop hook, and docs updates.

Closes #131

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /config UX overhaul, resume line toggle, cost budget settings, model metadata

/config UX cleanup:
- Convert all binary toggles from 3-column (on/off/clear) to 2-column
  (toggle + clear) for better mobile tap targets
- Merge Engine + Model into combined "Engine & model" page
- Reorganise home page to max 2 buttons per row across all engines
- Split plan mode 3-option rows (off/on/auto) into 2+1 layout
- Add _toggle_row() helper for consistent toggle button rendering

New features:
- #128: Resume line /config toggle — per-chat show_resume_line override
  via EngineOverrides with On/Off/Clear buttons, wired into executor
- #129: Cost budget /config settings — per-chat budget_enabled and
  budget_auto_cancel overrides on the Cost & Usage page, wired into
  _check_cost_budget() in runner_bridge.py

Model metadata improvements:
- Show Claude Code [1m] context window suffix: "opus 4.6 (1M)"
- Strip Gemini CLI "auto-" prefix: "auto-gemini-3" → "gemini-3"
- Future-proof: unknown suffixes default to .upper() (e.g. [500k] → 500K)

Bug fixes:
- #124: Standalone override commands (/planmode, /model, /reasoning) now
  preserve all EngineOverrides fields including new ones
- Error handling: control_response.write_failed catch-all in claude.py,
  ask_question.extraction_failed warning, model.override.failed logging

Hardening:
- Plan outline sent as separate ephemeral message (avoids 4096 char truncation)
- Added show_resume_line, budget_enabled, budget_auto_cancel to
  EngineOverrides, EngineRunOptions, normalize/merge, and all constructors

Tests: 1610 passed, 80.56% coverage, ruff clean.
Integration tested on @untether_dev_bot across all 6 engine chats.

Closes #128, closes #129, fixes #124

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI for PR #132

* fix: address 11 CodeRabbit review comments on PR #132

Bug fixes:
- claude.py: fix UnboundLocalError when factory.resume is falsy in
  ask_question.extraction_failed logging path
- ask_question.py: reject malformed option callbacks instead of
  silently falling back to option 0
- files.py: raise FileExistsError when deduplicate_target exhausts
  999 suffixes instead of returning the original (overwrite risk)
- config.py: disambiguate Codex "Full auto" (fa) vs Gemini "Full
  access" (ya) callback IDs and toast labels

Hardening:
- codex.py: add --ask-for-approval to _EXEC_ONLY_FLAGS guard
- model.py: add try/except to clear path (matching set path)
- reasoning.py: add try/except to clear path (matching set path)
- loop.py: notify user when media group upload fails instead of
  silently dropping
- export.py: log session count instead of identifiers at info level
- config.py: resolve resume-line default from config instead of
  hardcoding True
- staging.sh: pin PyPI index in rollback/reset with --pip-args

Skipped (not applicable):
- CHANGELOG.md: RC versions don't get changelog entries per release
  discipline
- docs/tutorials TODO screenshot: pre-existing, not introduced by PR
- .claude/hooks/context-guard-stop.sh: ContextDocs plugin hook, not
  Untether source

Tests: 1611 passed, 80.48% coverage, ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace bare pass with debug log to satisfy bandit B110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: setup wizard security + UX improvements

- Auto-set allowed_user_ids from captured Telegram user ID during
  onboarding (security: restricts bot to the setup user's account)
- Add "next steps" panel after wizard completion with pointers to
  /config, voice notes, projects, and account lock confirmation
- Update install.md: Python 3.12+ (not just 3.14), dynamic version
  string, /config mention for post-setup changes
- Update first-run.md: /config → Engine & model for default engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: plan outline UX — markdown rendering, buttons, cleanup (#139, #140, #141)

- Render outline messages as formatted text via render_markdown() +
  split_markdown_body() instead of raw markdown (#139)
- Add approve/deny buttons to last outline message so users don't
  have to scroll up past long outlines (#140)
- Delete outline messages on approve/deny via module-level
  _OUTLINE_REGISTRY callable from callback handler; suppress stale
  keyboard on progress message (#141)
- 8 new tests for outline rendering, keyboard placement, and cleanup
- Bump version to 0.35.0rc5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /continue command — cross-environment resume for all engines (#135)

New `/continue` command resumes the most recent CLI session in the
project directory from Telegram. Enables starting a session in your
terminal and picking it up from your phone.

Engine support: Claude (--continue), Codex (resume --last), OpenCode
(--continue), Pi (--continue), Gemini (--resume latest). AMP not
supported (requires explicit thread ID).

Includes ResumeToken.is_continue flag, build_args for all 6 runners,
reserved command registration, resume emoji prefix stripping for
reply-to-continue, docs (how-to guide, README, commands ref, routing
explanation, conversation modes tutorial), and 99 new test assertions.

Integration tested against @untether_dev_bot — all 5 supported engines
passed secret-recall verification via Telegram MCP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: agent outbox file delivery + fix cross-chat ask stealing (#143, #144)

Outbox delivery (#143): agents write files to .untether-outbox/ during a
run; Untether sends them as Telegram documents on completion with 📎
captions. Config: outbox_enabled, outbox_dir, outbox_max_files,
outbox_cleanup. Deny-glob security, size limits, auto-cleanup.
Preamble updated for all 6 engines. Integration tested across
Claude, Codex, OpenCode, Pi, and Gemini.

AskUserQuestion fix (#144): _PENDING_ASK_REQUESTS and
_ASK_QUESTION_FLOWS were global dicts with no chat_id scoping — a
pending ask in one chat would steal the next message from any other
chat. Added channel_id contextvar and scoped all ask lookups by it.
Session cleanup now also clears stale pending asks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: v0.35.0 changelog completion + fix #123 updatedInput

- Complete v0.35.0 changelog: add missing entries for /continue (#135),
  /config UX overhaul (#132), resume line toggle (#128), cost budget
  (#129), model metadata, resume line formatting (#127), override
  preservation (#124), and updatedInput fix (#123)
- Fix #123: register input for system-level auto-approved control
  requests so updatedInput is included in the response
- Add parameterised test for all 5 auto-approve types input registration
- Remove unused OutboxResult import (ruff fix)

Issues closed: #115, #118, #123, #124, #126, #127, #134

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.35.0rc6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rc6 integration test fixes (#145, #146, #147, #148, #149)

- Reduce Telegram API timeout from 120s to 30s (#145)
- OpenCode error runs show error text instead of empty body (#146)
- Pi /continue captures session ID via allow_id_promotion (#147)
- Post-outline approval uses skip_reply to avoid "not found" (#148)
- Orphan progress message cleanup on restart (#149)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: post-outline notification reply + OpenCode empty body (#148, #150)

- #148: skip_reply callback results now bypass the executor's default
  reply_to fallback, sending directly via the transport with no
  reply_to_message_id. Previously, the executor treated reply_to=None
  as "use default" which pointed to the (deleted) outline message.

- #150: OpenCode normal completion with no Text events now falls back
  to last_tool_error. Added state.last_tool_error field populated on
  ToolUse error status. Covers both translate() and stream_end_events().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: suppress post-outline notification to avoid "message not found" (#148)

After outline approval/denial, the progress loop's _send_notify was
firing for the next tool approval, but the notification's reply_to
anchor could reference deleted state. Added _outline_just_resolved
flag to skip one notification cycle after outline cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: note OpenCode lacks auto-compaction — long sessions degrade (#150)

Added known limitation to OpenCode runner docs and integration testing
playbook. OpenCode sessions accumulate unbounded context (no compaction
events unlike Pi). Workaround: use /new before isolated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip_reply on regular approve path when outline was deleted (#148)

The "Approve Plan" button on outline messages uses the real
ExitPlanMode request_id, routing through the regular approve path
(not the da: synthetic path). When outline messages exist, set
skip_reply=True on the CommandResult to avoid replying to the
just-deleted outline message.

Also added reply_to_message_id and text_preview to transport.send.failed
warning for easier debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc6 integration test fixes (#145-#150)

Updated fix descriptions for #146/#150 (OpenCode last_tool_error
fallback) and #148 (regular approve path skip_reply). Added docs
section for OpenCode compaction limitation. Updated test counts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix formatting after merge resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review comments on PR #151

- bridge.py: replace text_preview with text_len in send failure warning
  to avoid logging raw message content (security)
- runner_bridge.py: move unregister_progress() after send_result_message()
  to avoid orphan window between ephemeral cleanup and final message send
- cross-environment-resume.md: add language spec to code block

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Nathan Schram (nathanschram) added a commit that referenced this pull request Mar 20, 2026
…#158, #159)

* ci: add CODEOWNERS, update action SHA pins, add permission comments

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add release guard hooks and document protection in CLAUDE.md

Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify /config default labels and remove redundant "Works with" lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress redundant cost footer on error runs

When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: suppress stall notifications when CPU-active + heartbeat re-render

When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.34.5rc2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: CI release-validation tomllib bytes/str mismatch

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: integrate screenshots into docs with correct JPG references

- Add 44 screenshots to docs/assets/screenshots/
- Fix all image refs from .png to .jpg across 25 doc files
- README uses absolute raw.githubusercontent.com URLs for PyPI rendering
- Fix 5 filename mismatches (session-auto-resume→chat-auto-resume, etc.)
- Comment out 11 missing screenshots with TODO markers
- Add CAPTURES.md checklist tracking capture status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: convert markdown images to HTML img tags for GitHub compatibility

Switch from MkDocs `![alt](src){ loading=lazy }` syntax to HTML `<img>`
tags with width="360" and loading="lazy". Fixes two GitHub rendering
issues: `{ loading=lazy }` appearing as visible text, and oversized
images with no width constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix 3 screenshot mismatches and replace 3 screenshots

- first-run.md: rewrite resume line text to match footer screenshot
- interactive-control.md: update planmode show admonition to match screenshot (auto not on)
- switch-engines.md: swap engine-footer.jpg for multi-engine-switch.jpg
- Replace startup-message.jpg with clean v0.34.4 capture (was rc/6-projects)
- Replace cooldown-auto-deny.jpg with post-outline approve/deny buttons
- Replace file-put.jpg with photo save confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add iOS caption limitation note to file transfer guide

Telegram iOS doesn't show a caption field when sending documents via the
File picker, so /file put <path> captions aren't easily accessible.
Added a note with workarounds (use Desktop, send as photo, or let
auto-save handle it). Updated screenshot alt text to match actual
screenshot content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: temp swap README image URLs to feature branch for preview

Will revert to master before merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: lay out all 3 README screenshots in a single row

Reduce from 360px to 270px each and combine into one <p> block
so all three hero screenshots sit side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap 3rd hero screenshot for config-menu for visual variety

Replace plan-outline-approve (too similar to approval-diff-preview)
with config-menu showing the /config settings grid. The three hero
images now tell: voice input → approve changes → configure everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add captions under README hero screenshots

Small <sub> captions: "Send tasks by voice (Whisper transcription)",
"Approve changes remotely", "Configure from Telegram".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: use table layout for README hero screenshots with captions

Fixes stacking issue — <br> in a <p> broke inline flow. A table
keeps images side by side with captions underneath each one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace table layout with single hero collage image

Composite image scales proportionally on mobile instead of requiring
horizontal scroll. Captions baked into the image via ImageMagick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap middle hero screenshot for full 3-button approval view

Replace approval-diff-preview with approval-buttons-howto showing
Approve / Deny / Pause & Outline Plan — more visually impressive.
Caption now reads "Approve changes remotely (Claude Code)".
Added footnote linking to engine compatibility table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap config-menu for parallel-projects in hero collage

Third hero screenshot now shows 10+ projects running simultaneously
across different repos — much more compelling than a settings menu.
New caption: "Run agents across projects in parallel".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: revert README image URL to master for merge

Swap hero-collage URL back from feature/github-hardening to master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc3

- fix: preserve all EngineOverrides fields when setting model/planmode/reasoning
  (was silently wiping ask_questions, diff_preview, show_api_cost, etc.)
- fix: /config home page resolves "default" to effective values
- feat: file upload auto-deduplication (append _1, _2 instead of requiring --force)
- feat: media groups without captions now auto-save instead of showing usage text
- feat: resume line visual separation (blank line + ↩️ prefix)
- fix: claude auto-approve echoes updatedInput in control response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: expand permission policies for Codex CLI and Gemini CLI in /config

Codex gets a new "Approval policy" page (full auto / safe) that passes
--ask-for-approval untrusted when safe mode is selected. Gemini's approval
mode expands from 2 to 3 tiers (read-only / edit files / full access) with
--approval-mode auto_edit for the middle tier. Both engines now show an
"Agent controls" section on the /config home page. Engine-specific model
default hints replace the generic "from CLI settings" text.

Also adds staging.sh helper, context-guard-stop hook, and docs updates.

Closes #131

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /config UX overhaul, resume line toggle, cost budget settings, model metadata

/config UX cleanup:
- Convert all binary toggles from 3-column (on/off/clear) to 2-column
  (toggle + clear) for better mobile tap targets
- Merge Engine + Model into combined "Engine & model" page
- Reorganise home page to max 2 buttons per row across all engines
- Split plan mode 3-option rows (off/on/auto) into 2+1 layout
- Add _toggle_row() helper for consistent toggle button rendering

New features:
- #128: Resume line /config toggle — per-chat show_resume_line override
  via EngineOverrides with On/Off/Clear buttons, wired into executor
- #129: Cost budget /config settings — per-chat budget_enabled and
  budget_auto_cancel overrides on the Cost & Usage page, wired into
  _check_cost_budget() in runner_bridge.py

Model metadata improvements:
- Show Claude Code [1m] context window suffix: "opus 4.6 (1M)"
- Strip Gemini CLI "auto-" prefix: "auto-gemini-3" → "gemini-3"
- Future-proof: unknown suffixes default to .upper() (e.g. [500k] → 500K)

Bug fixes:
- #124: Standalone override commands (/planmode, /model, /reasoning) now
  preserve all EngineOverrides fields including new ones
- Error handling: control_response.write_failed catch-all in claude.py,
  ask_question.extraction_failed warning, model.override.failed logging

Hardening:
- Plan outline sent as separate ephemeral message (avoids 4096 char truncation)
- Added show_resume_line, budget_enabled, budget_auto_cancel to
  EngineOverrides, EngineRunOptions, normalize/merge, and all constructors

Tests: 1610 passed, 80.56% coverage, ruff clean.
Integration tested on @untether_dev_bot across all 6 engine chats.

Closes #128, closes #129, fixes #124

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI for PR #132

* fix: address 11 CodeRabbit review comments on PR #132

Bug fixes:
- claude.py: fix UnboundLocalError when factory.resume is falsy in
  ask_question.extraction_failed logging path
- ask_question.py: reject malformed option callbacks instead of
  silently falling back to option 0
- files.py: raise FileExistsError when deduplicate_target exhausts
  999 suffixes instead of returning the original (overwrite risk)
- config.py: disambiguate Codex "Full auto" (fa) vs Gemini "Full
  access" (ya) callback IDs and toast labels

Hardening:
- codex.py: add --ask-for-approval to _EXEC_ONLY_FLAGS guard
- model.py: add try/except to clear path (matching set path)
- reasoning.py: add try/except to clear path (matching set path)
- loop.py: notify user when media group upload fails instead of
  silently dropping
- export.py: log session count instead of identifiers at info level
- config.py: resolve resume-line default from config instead of
  hardcoding True
- staging.sh: pin PyPI index in rollback/reset with --pip-args

Skipped (not applicable):
- CHANGELOG.md: RC versions don't get changelog entries per release
  discipline
- docs/tutorials TODO screenshot: pre-existing, not introduced by PR
- .claude/hooks/context-guard-stop.sh: ContextDocs plugin hook, not
  Untether source

Tests: 1611 passed, 80.48% coverage, ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace bare pass with debug log to satisfy bandit B110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: setup wizard security + UX improvements

- Auto-set allowed_user_ids from captured Telegram user ID during
  onboarding (security: restricts bot to the setup user's account)
- Add "next steps" panel after wizard completion with pointers to
  /config, voice notes, projects, and account lock confirmation
- Update install.md: Python 3.12+ (not just 3.14), dynamic version
  string, /config mention for post-setup changes
- Update first-run.md: /config → Engine & model for default engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: plan outline UX — markdown rendering, buttons, cleanup (#139, #140, #141)

- Render outline messages as formatted text via render_markdown() +
  split_markdown_body() instead of raw markdown (#139)
- Add approve/deny buttons to last outline message so users don't
  have to scroll up past long outlines (#140)
- Delete outline messages on approve/deny via module-level
  _OUTLINE_REGISTRY callable from callback handler; suppress stale
  keyboard on progress message (#141)
- 8 new tests for outline rendering, keyboard placement, and cleanup
- Bump version to 0.35.0rc5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /continue command — cross-environment resume for all engines (#135)

New `/continue` command resumes the most recent CLI session in the
project directory from Telegram. Enables starting a session in your
terminal and picking it up from your phone.

Engine support: Claude (--continue), Codex (resume --last), OpenCode
(--continue), Pi (--continue), Gemini (--resume latest). AMP not
supported (requires explicit thread ID).

Includes ResumeToken.is_continue flag, build_args for all 6 runners,
reserved command registration, resume emoji prefix stripping for
reply-to-continue, docs (how-to guide, README, commands ref, routing
explanation, conversation modes tutorial), and 99 new test assertions.

Integration tested against @untether_dev_bot — all 5 supported engines
passed secret-recall verification via Telegram MCP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: agent outbox file delivery + fix cross-chat ask stealing (#143, #144)

Outbox delivery (#143): agents write files to .untether-outbox/ during a
run; Untether sends them as Telegram documents on completion with 📎
captions. Config: outbox_enabled, outbox_dir, outbox_max_files,
outbox_cleanup. Deny-glob security, size limits, auto-cleanup.
Preamble updated for all 6 engines. Integration tested across
Claude, Codex, OpenCode, Pi, and Gemini.

AskUserQuestion fix (#144): _PENDING_ASK_REQUESTS and
_ASK_QUESTION_FLOWS were global dicts with no chat_id scoping — a
pending ask in one chat would steal the next message from any other
chat. Added channel_id contextvar and scoped all ask lookups by it.
Session cleanup now also clears stale pending asks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: v0.35.0 changelog completion + fix #123 updatedInput

- Complete v0.35.0 changelog: add missing entries for /continue (#135),
  /config UX overhaul (#132), resume line toggle (#128), cost budget
  (#129), model metadata, resume line formatting (#127), override
  preservation (#124), and updatedInput fix (#123)
- Fix #123: register input for system-level auto-approved control
  requests so updatedInput is included in the response
- Add parameterised test for all 5 auto-approve types input registration
- Remove unused OutboxResult import (ruff fix)

Issues closed: #115, #118, #123, #124, #126, #127, #134

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.35.0rc6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rc6 integration test fixes (#145, #146, #147, #148, #149)

- Reduce Telegram API timeout from 120s to 30s (#145)
- OpenCode error runs show error text instead of empty body (#146)
- Pi /continue captures session ID via allow_id_promotion (#147)
- Post-outline approval uses skip_reply to avoid "not found" (#148)
- Orphan progress message cleanup on restart (#149)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: post-outline notification reply + OpenCode empty body (#148, #150)

- #148: skip_reply callback results now bypass the executor's default
  reply_to fallback, sending directly via the transport with no
  reply_to_message_id. Previously, the executor treated reply_to=None
  as "use default" which pointed to the (deleted) outline message.

- #150: OpenCode normal completion with no Text events now falls back
  to last_tool_error. Added state.last_tool_error field populated on
  ToolUse error status. Covers both translate() and stream_end_events().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: suppress post-outline notification to avoid "message not found" (#148)

After outline approval/denial, the progress loop's _send_notify was
firing for the next tool approval, but the notification's reply_to
anchor could reference deleted state. Added _outline_just_resolved
flag to skip one notification cycle after outline cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: note OpenCode lacks auto-compaction — long sessions degrade (#150)

Added known limitation to OpenCode runner docs and integration testing
playbook. OpenCode sessions accumulate unbounded context (no compaction
events unlike Pi). Workaround: use /new before isolated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip_reply on regular approve path when outline was deleted (#148)

The "Approve Plan" button on outline messages uses the real
ExitPlanMode request_id, routing through the regular approve path
(not the da: synthetic path). When outline messages exist, set
skip_reply=True on the CommandResult to avoid replying to the
just-deleted outline message.

Also added reply_to_message_id and text_preview to transport.send.failed
warning for easier debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc6 integration test fixes (#145-#150)

Updated fix descriptions for #146/#150 (OpenCode last_tool_error
fallback) and #148 (regular approve path skip_reply). Added docs
section for OpenCode compaction limitation. Updated test counts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix formatting after merge resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review comments on PR #151

- bridge.py: replace text_preview with text_len in send failure warning
  to avoid logging raw message content (security)
- runner_bridge.py: move unregister_progress() after send_result_message()
  to avoid orphan window between ephemeral cleanup and final message send
- cross-environment-resume.md: add language spec to code block

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve /config "default" labels to effective on/off values (#152)

Sub-pages showed "Current: default" or "default (on/off)" while buttons
already showed the resolved value. Now all boolean-toggle settings show
the effective on/off value in both text and buttons.

Affected: verbose, ask mode, diff preview, API cost, subscription usage,
budget enabled/auto-cancel, resume line. Home page cost & resume labels
also resolved.

Plan mode, model, and reasoning keep "default" since they depend on CLI
settings and aren't simple on/off booleans.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc7 config default labels fix (#152)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update documentation for v0.35.0

- fix missing nav entries in zensical.toml (cross-env resume, Gemini/Amp runners)
- rewrite inline-settings.md for /config UX overhaul (2-column toggles, budget/resume toggles)
- update plan-mode.md with outline rendering, buttons-on-last-chunk, ephemeral cleanup
- update interactive-control tutorial with outline UX improvements
- add orphan progress cleanup section to operations.md
- add engine-specific approval policies to interactive-approval.md
- add per-chat budget overrides to cost-budgets.md
- update module-map.md with Gemini/Amp and new modules (outbox, progress persistence, proc_diag)
- update architecture.md mermaid diagrams with all 6 engines
- bump specification.md to v0.35.0, add progress persistence and outbox sections
- add v0.35.0 screenshot entries to CAPTURES.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: broaden frozen ring buffer stall escalation beyond MCP tools (#155)

Frozen ring buffer escalation was gated on `mcp_server is not None`,
so general stalls with cpu_active=True and no MCP tool running were
silently suppressed indefinitely. Broadened to fire for all stalls
after 3+ checks with no new JSONL events regardless of tool type.

New notification: "CPU active, no new events" for non-MCP frozen stalls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: tool approval buttons no longer suppressed after outline approval (#156)

After "Approve Plan" on an outline, the stale discuss_approve action
remained in ProgressTracker with completed=False. The renderer picked
up its stale "Approve Plan"/"Deny" buttons first, then the suppression
logic at line 994 stripped ALL buttons — including new Write/Edit/Bash
approval buttons. Claude blocked indefinitely waiting for approval.

Fix: after suppressing stale buttons, complete the discuss_approve
action(s) in the tracker, reset _outline_sent, and trigger a re-render
so subsequent tool requests get their own Approve/Deny buttons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add workflow mode indicator to startup message, fix startup crash on topics validation failure (#158, #159)

Features:
- Startup message now shows mode: assistant/workspace/handoff
- Derived from session_mode + topics.enabled config values
- _resolve_mode_label() helper in backend.py

Bug fixes:
- Fix UnboundLocalError crash when topics validation fails on startup (#158)
  - Moved import signal and shutdown imports before try block in loop.py
- Downgrade can_manage_topics check from fatal error to warning (#159)
  - Bot can now start without manage_topics admin right
  - Existing topics work fine; only topic creation/editing affected

Tests:
- 17 new unit tests for stateless/handoff mode (test_stateless_mode.py)
  - _should_show_resume_line, _chat_session_key, ResumeResolver, ResumeLineProxy
  - Integration-level: stateless shows resume lines, no auto-resume, chat hides lines
- 3 new tests for mode indicator in startup message (test_telegram_backend.py)

Docs:
- New docs/reference/modes.md — comprehensive reference for all 3 workflow modes
- Updated docs/reference/index.md and zensical.toml nav with modes page

* docs: comprehensive three-mode coverage across all documentation

New:
- docs/how-to/choose-a-mode.md — decision tree, mode comparison, mermaid
  sequence diagrams, configuration examples, switching guide, workspace
  prerequisites

Updated:
- README.md — improved three-mode description in features list
- docs/tutorials/install.md — added mode selection step (section 10)
- docs/tutorials/first-run.md — added 'What mode am I in?' tip
- docs/reference/config.md — cross-linked session_mode/show_resume_line to modes.md
- docs/reference/transports/telegram.md — added mode requirement callouts
  for forum topics and chat sessions sections
- docs/how-to/chat-sessions.md — added session persistence explanation
  (state files, auto-resume mechanics, handoff note)
- docs/how-to/topics.md — expanded prerequisites checklist with group
  privacy, can_manage_topics, and re-add steps
- docs/how-to/cross-environment-resume.md — added handoff mode terminal
  workflow with mermaid sequence diagram
- docs/how-to/index.md — added 'Getting started' section with choose-a-mode
- zensical.toml — added choose-a-mode to nav

* docs: add three-mode summary table to README Quick Start section

* feat: migrate to dev branch workflow — dev→TestPyPI, master→PyPI

Branch model:
- feature/* → PR → dev (TestPyPI auto-publish) → PR → master (PyPI)
- master always matches latest PyPI release
- dev is the integration/staging branch

CI changes:
- ci.yml: TestPyPI publish triggers on dev push (was master)
- ci.yml, codeql.yml: CI runs on both master and dev pushes
- dependabot.yml: PRs target dev branch

Hook changes:
- release-guard.sh: updated messages to mention dev branch
- release-guard-mcp.sh: updated messages to mention dev branch
- Both hooks already allow dev pushes (only block master/main)

Documentation:
- CLAUDE.md: updated 3-phase workflow, CI table, release guard docs
- dev-workflow.md: added branch model section
- release-discipline.md: added dev branch staging notes

* ci: retrigger CI for PR #160

* feat: allow Claude Code to merge PRs targeting dev branch only

Release guard hooks now check the PR's base branch:
- dev → allowed (TestPyPI/staging)
- master/main → blocked (PyPI releases remain Nathan-only)

Both Bash hook (gh pr merge) and MCP hook (merge_pull_request)
updated with base branch checking via gh pr view.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Nathan Schram (nathanschram) added a commit that referenced this pull request Mar 20, 2026
* ci: add CODEOWNERS, update action SHA pins, add permission comments

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add release guard hooks and document protection in CLAUDE.md

Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify /config default labels and remove redundant "Works with" lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress redundant cost footer on error runs

When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: suppress stall notifications when CPU-active + heartbeat re-render

When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.34.5rc2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: CI release-validation tomllib bytes/str mismatch

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: integrate screenshots into docs with correct JPG references

- Add 44 screenshots to docs/assets/screenshots/
- Fix all image refs from .png to .jpg across 25 doc files
- README uses absolute raw.githubusercontent.com URLs for PyPI rendering
- Fix 5 filename mismatches (session-auto-resume→chat-auto-resume, etc.)
- Comment out 11 missing screenshots with TODO markers
- Add CAPTURES.md checklist tracking capture status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: convert markdown images to HTML img tags for GitHub compatibility

Switch from MkDocs `![alt](src){ loading=lazy }` syntax to HTML `<img>`
tags with width="360" and loading="lazy". Fixes two GitHub rendering
issues: `{ loading=lazy }` appearing as visible text, and oversized
images with no width constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix 3 screenshot mismatches and replace 3 screenshots

- first-run.md: rewrite resume line text to match footer screenshot
- interactive-control.md: update planmode show admonition to match screenshot (auto not on)
- switch-engines.md: swap engine-footer.jpg for multi-engine-switch.jpg
- Replace startup-message.jpg with clean v0.34.4 capture (was rc/6-projects)
- Replace cooldown-auto-deny.jpg with post-outline approve/deny buttons
- Replace file-put.jpg with photo save confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add iOS caption limitation note to file transfer guide

Telegram iOS doesn't show a caption field when sending documents via the
File picker, so /file put <path> captions aren't easily accessible.
Added a note with workarounds (use Desktop, send as photo, or let
auto-save handle it). Updated screenshot alt text to match actual
screenshot content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: temp swap README image URLs to feature branch for preview

Will revert to master before merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: lay out all 3 README screenshots in a single row

Reduce from 360px to 270px each and combine into one <p> block
so all three hero screenshots sit side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap 3rd hero screenshot for config-menu for visual variety

Replace plan-outline-approve (too similar to approval-diff-preview)
with config-menu showing the /config settings grid. The three hero
images now tell: voice input → approve changes → configure everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add captions under README hero screenshots

Small <sub> captions: "Send tasks by voice (Whisper transcription)",
"Approve changes remotely", "Configure from Telegram".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: use table layout for README hero screenshots with captions

Fixes stacking issue — <br> in a <p> broke inline flow. A table
keeps images side by side with captions underneath each one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace table layout with single hero collage image

Composite image scales proportionally on mobile instead of requiring
horizontal scroll. Captions baked into the image via ImageMagick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap middle hero screenshot for full 3-button approval view

Replace approval-diff-preview with approval-buttons-howto showing
Approve / Deny / Pause & Outline Plan — more visually impressive.
Caption now reads "Approve changes remotely (Claude Code)".
Added footnote linking to engine compatibility table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap config-menu for parallel-projects in hero collage

Third hero screenshot now shows 10+ projects running simultaneously
across different repos — much more compelling than a settings menu.
New caption: "Run agents across projects in parallel".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: revert README image URL to master for merge

Swap hero-collage URL back from feature/github-hardening to master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc3

- fix: preserve all EngineOverrides fields when setting model/planmode/reasoning
  (was silently wiping ask_questions, diff_preview, show_api_cost, etc.)
- fix: /config home page resolves "default" to effective values
- feat: file upload auto-deduplication (append _1, _2 instead of requiring --force)
- feat: media groups without captions now auto-save instead of showing usage text
- feat: resume line visual separation (blank line + ↩️ prefix)
- fix: claude auto-approve echoes updatedInput in control response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: expand permission policies for Codex CLI and Gemini CLI in /config

Codex gets a new "Approval policy" page (full auto / safe) that passes
--ask-for-approval untrusted when safe mode is selected. Gemini's approval
mode expands from 2 to 3 tiers (read-only / edit files / full access) with
--approval-mode auto_edit for the middle tier. Both engines now show an
"Agent controls" section on the /config home page. Engine-specific model
default hints replace the generic "from CLI settings" text.

Also adds staging.sh helper, context-guard-stop hook, and docs updates.

Closes #131

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /config UX overhaul, resume line toggle, cost budget settings, model metadata

/config UX cleanup:
- Convert all binary toggles from 3-column (on/off/clear) to 2-column
  (toggle + clear) for better mobile tap targets
- Merge Engine + Model into combined "Engine & model" page
- Reorganise home page to max 2 buttons per row across all engines
- Split plan mode 3-option rows (off/on/auto) into 2+1 layout
- Add _toggle_row() helper for consistent toggle button rendering

New features:
- #128: Resume line /config toggle — per-chat show_resume_line override
  via EngineOverrides with On/Off/Clear buttons, wired into executor
- #129: Cost budget /config settings — per-chat budget_enabled and
  budget_auto_cancel overrides on the Cost & Usage page, wired into
  _check_cost_budget() in runner_bridge.py

Model metadata improvements:
- Show Claude Code [1m] context window suffix: "opus 4.6 (1M)"
- Strip Gemini CLI "auto-" prefix: "auto-gemini-3" → "gemini-3"
- Future-proof: unknown suffixes default to .upper() (e.g. [500k] → 500K)

Bug fixes:
- #124: Standalone override commands (/planmode, /model, /reasoning) now
  preserve all EngineOverrides fields including new ones
- Error handling: control_response.write_failed catch-all in claude.py,
  ask_question.extraction_failed warning, model.override.failed logging

Hardening:
- Plan outline sent as separate ephemeral message (avoids 4096 char truncation)
- Added show_resume_line, budget_enabled, budget_auto_cancel to
  EngineOverrides, EngineRunOptions, normalize/merge, and all constructors

Tests: 1610 passed, 80.56% coverage, ruff clean.
Integration tested on @untether_dev_bot across all 6 engine chats.

Closes #128, closes #129, fixes #124

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI for PR #132

* fix: address 11 CodeRabbit review comments on PR #132

Bug fixes:
- claude.py: fix UnboundLocalError when factory.resume is falsy in
  ask_question.extraction_failed logging path
- ask_question.py: reject malformed option callbacks instead of
  silently falling back to option 0
- files.py: raise FileExistsError when deduplicate_target exhausts
  999 suffixes instead of returning the original (overwrite risk)
- config.py: disambiguate Codex "Full auto" (fa) vs Gemini "Full
  access" (ya) callback IDs and toast labels

Hardening:
- codex.py: add --ask-for-approval to _EXEC_ONLY_FLAGS guard
- model.py: add try/except to clear path (matching set path)
- reasoning.py: add try/except to clear path (matching set path)
- loop.py: notify user when media group upload fails instead of
  silently dropping
- export.py: log session count instead of identifiers at info level
- config.py: resolve resume-line default from config instead of
  hardcoding True
- staging.sh: pin PyPI index in rollback/reset with --pip-args

Skipped (not applicable):
- CHANGELOG.md: RC versions don't get changelog entries per release
  discipline
- docs/tutorials TODO screenshot: pre-existing, not introduced by PR
- .claude/hooks/context-guard-stop.sh: ContextDocs plugin hook, not
  Untether source

Tests: 1611 passed, 80.48% coverage, ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace bare pass with debug log to satisfy bandit B110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: setup wizard security + UX improvements

- Auto-set allowed_user_ids from captured Telegram user ID during
  onboarding (security: restricts bot to the setup user's account)
- Add "next steps" panel after wizard completion with pointers to
  /config, voice notes, projects, and account lock confirmation
- Update install.md: Python 3.12+ (not just 3.14), dynamic version
  string, /config mention for post-setup changes
- Update first-run.md: /config → Engine & model for default engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: plan outline UX — markdown rendering, buttons, cleanup (#139, #140, #141)

- Render outline messages as formatted text via render_markdown() +
  split_markdown_body() instead of raw markdown (#139)
- Add approve/deny buttons to last outline message so users don't
  have to scroll up past long outlines (#140)
- Delete outline messages on approve/deny via module-level
  _OUTLINE_REGISTRY callable from callback handler; suppress stale
  keyboard on progress message (#141)
- 8 new tests for outline rendering, keyboard placement, and cleanup
- Bump version to 0.35.0rc5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /continue command — cross-environment resume for all engines (#135)

New `/continue` command resumes the most recent CLI session in the
project directory from Telegram. Enables starting a session in your
terminal and picking it up from your phone.

Engine support: Claude (--continue), Codex (resume --last), OpenCode
(--continue), Pi (--continue), Gemini (--resume latest). AMP not
supported (requires explicit thread ID).

Includes ResumeToken.is_continue flag, build_args for all 6 runners,
reserved command registration, resume emoji prefix stripping for
reply-to-continue, docs (how-to guide, README, commands ref, routing
explanation, conversation modes tutorial), and 99 new test assertions.

Integration tested against @untether_dev_bot — all 5 supported engines
passed secret-recall verification via Telegram MCP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: agent outbox file delivery + fix cross-chat ask stealing (#143, #144)

Outbox delivery (#143): agents write files to .untether-outbox/ during a
run; Untether sends them as Telegram documents on completion with 📎
captions. Config: outbox_enabled, outbox_dir, outbox_max_files,
outbox_cleanup. Deny-glob security, size limits, auto-cleanup.
Preamble updated for all 6 engines. Integration tested across
Claude, Codex, OpenCode, Pi, and Gemini.

AskUserQuestion fix (#144): _PENDING_ASK_REQUESTS and
_ASK_QUESTION_FLOWS were global dicts with no chat_id scoping — a
pending ask in one chat would steal the next message from any other
chat. Added channel_id contextvar and scoped all ask lookups by it.
Session cleanup now also clears stale pending asks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: v0.35.0 changelog completion + fix #123 updatedInput

- Complete v0.35.0 changelog: add missing entries for /continue (#135),
  /config UX overhaul (#132), resume line toggle (#128), cost budget
  (#129), model metadata, resume line formatting (#127), override
  preservation (#124), and updatedInput fix (#123)
- Fix #123: register input for system-level auto-approved control
  requests so updatedInput is included in the response
- Add parameterised test for all 5 auto-approve types input registration
- Remove unused OutboxResult import (ruff fix)

Issues closed: #115, #118, #123, #124, #126, #127, #134

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.35.0rc6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rc6 integration test fixes (#145, #146, #147, #148, #149)

- Reduce Telegram API timeout from 120s to 30s (#145)
- OpenCode error runs show error text instead of empty body (#146)
- Pi /continue captures session ID via allow_id_promotion (#147)
- Post-outline approval uses skip_reply to avoid "not found" (#148)
- Orphan progress message cleanup on restart (#149)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: post-outline notification reply + OpenCode empty body (#148, #150)

- #148: skip_reply callback results now bypass the executor's default
  reply_to fallback, sending directly via the transport with no
  reply_to_message_id. Previously, the executor treated reply_to=None
  as "use default" which pointed to the (deleted) outline message.

- #150: OpenCode normal completion with no Text events now falls back
  to last_tool_error. Added state.last_tool_error field populated on
  ToolUse error status. Covers both translate() and stream_end_events().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: suppress post-outline notification to avoid "message not found" (#148)

After outline approval/denial, the progress loop's _send_notify was
firing for the next tool approval, but the notification's reply_to
anchor could reference deleted state. Added _outline_just_resolved
flag to skip one notification cycle after outline cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: note OpenCode lacks auto-compaction — long sessions degrade (#150)

Added known limitation to OpenCode runner docs and integration testing
playbook. OpenCode sessions accumulate unbounded context (no compaction
events unlike Pi). Workaround: use /new before isolated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip_reply on regular approve path when outline was deleted (#148)

The "Approve Plan" button on outline messages uses the real
ExitPlanMode request_id, routing through the regular approve path
(not the da: synthetic path). When outline messages exist, set
skip_reply=True on the CommandResult to avoid replying to the
just-deleted outline message.

Also added reply_to_message_id and text_preview to transport.send.failed
warning for easier debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc6 integration test fixes (#145-#150)

Updated fix descriptions for #146/#150 (OpenCode last_tool_error
fallback) and #148 (regular approve path skip_reply). Added docs
section for OpenCode compaction limitation. Updated test counts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix formatting after merge resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review comments on PR #151

- bridge.py: replace text_preview with text_len in send failure warning
  to avoid logging raw message content (security)
- runner_bridge.py: move unregister_progress() after send_result_message()
  to avoid orphan window between ephemeral cleanup and final message send
- cross-environment-resume.md: add language spec to code block

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve /config "default" labels to effective on/off values (#152)

Sub-pages showed "Current: default" or "default (on/off)" while buttons
already showed the resolved value. Now all boolean-toggle settings show
the effective on/off value in both text and buttons.

Affected: verbose, ask mode, diff preview, API cost, subscription usage,
budget enabled/auto-cancel, resume line. Home page cost & resume labels
also resolved.

Plan mode, model, and reasoning keep "default" since they depend on CLI
settings and aren't simple on/off booleans.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc7 config default labels fix (#152)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update documentation for v0.35.0

- fix missing nav entries in zensical.toml (cross-env resume, Gemini/Amp runners)
- rewrite inline-settings.md for /config UX overhaul (2-column toggles, budget/resume toggles)
- update plan-mode.md with outline rendering, buttons-on-last-chunk, ephemeral cleanup
- update interactive-control tutorial with outline UX improvements
- add orphan progress cleanup section to operations.md
- add engine-specific approval policies to interactive-approval.md
- add per-chat budget overrides to cost-budgets.md
- update module-map.md with Gemini/Amp and new modules (outbox, progress persistence, proc_diag)
- update architecture.md mermaid diagrams with all 6 engines
- bump specification.md to v0.35.0, add progress persistence and outbox sections
- add v0.35.0 screenshot entries to CAPTURES.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: broaden frozen ring buffer stall escalation beyond MCP tools (#155)

Frozen ring buffer escalation was gated on `mcp_server is not None`,
so general stalls with cpu_active=True and no MCP tool running were
silently suppressed indefinitely. Broadened to fire for all stalls
after 3+ checks with no new JSONL events regardless of tool type.

New notification: "CPU active, no new events" for non-MCP frozen stalls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: tool approval buttons no longer suppressed after outline approval (#156)

After "Approve Plan" on an outline, the stale discuss_approve action
remained in ProgressTracker with completed=False. The renderer picked
up its stale "Approve Plan"/"Deny" buttons first, then the suppression
logic at line 994 stripped ALL buttons — including new Write/Edit/Bash
approval buttons. Claude blocked indefinitely waiting for approval.

Fix: after suppressing stale buttons, complete the discuss_approve
action(s) in the tracker, reset _outline_sent, and trigger a re-render
so subsequent tool requests get their own Approve/Deny buttons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add workflow mode indicator to startup message, fix startup crash on topics validation failure (#158, #159)

Features:
- Startup message now shows mode: assistant/workspace/handoff
- Derived from session_mode + topics.enabled config values
- _resolve_mode_label() helper in backend.py

Bug fixes:
- Fix UnboundLocalError crash when topics validation fails on startup (#158)
  - Moved import signal and shutdown imports before try block in loop.py
- Downgrade can_manage_topics check from fatal error to warning (#159)
  - Bot can now start without manage_topics admin right
  - Existing topics work fine; only topic creation/editing affected

Tests:
- 17 new unit tests for stateless/handoff mode (test_stateless_mode.py)
  - _should_show_resume_line, _chat_session_key, ResumeResolver, ResumeLineProxy
  - Integration-level: stateless shows resume lines, no auto-resume, chat hides lines
- 3 new tests for mode indicator in startup message (test_telegram_backend.py)

Docs:
- New docs/reference/modes.md — comprehensive reference for all 3 workflow modes
- Updated docs/reference/index.md and zensical.toml nav with modes page

* docs: comprehensive three-mode coverage across all documentation

New:
- docs/how-to/choose-a-mode.md — decision tree, mode comparison, mermaid
  sequence diagrams, configuration examples, switching guide, workspace
  prerequisites

Updated:
- README.md — improved three-mode description in features list
- docs/tutorials/install.md — added mode selection step (section 10)
- docs/tutorials/first-run.md — added 'What mode am I in?' tip
- docs/reference/config.md — cross-linked session_mode/show_resume_line to modes.md
- docs/reference/transports/telegram.md — added mode requirement callouts
  for forum topics and chat sessions sections
- docs/how-to/chat-sessions.md — added session persistence explanation
  (state files, auto-resume mechanics, handoff note)
- docs/how-to/topics.md — expanded prerequisites checklist with group
  privacy, can_manage_topics, and re-add steps
- docs/how-to/cross-environment-resume.md — added handoff mode terminal
  workflow with mermaid sequence diagram
- docs/how-to/index.md — added 'Getting started' section with choose-a-mode
- zensical.toml — added choose-a-mode to nav

* docs: add three-mode summary table to README Quick Start section

* feat: migrate to dev branch workflow — dev→TestPyPI, master→PyPI

Branch model:
- feature/* → PR → dev (TestPyPI auto-publish) → PR → master (PyPI)
- master always matches latest PyPI release
- dev is the integration/staging branch

CI changes:
- ci.yml: TestPyPI publish triggers on dev push (was master)
- ci.yml, codeql.yml: CI runs on both master and dev pushes
- dependabot.yml: PRs target dev branch

Hook changes:
- release-guard.sh: updated messages to mention dev branch
- release-guard-mcp.sh: updated messages to mention dev branch
- Both hooks already allow dev pushes (only block master/main)

Documentation:
- CLAUDE.md: updated 3-phase workflow, CI table, release guard docs
- dev-workflow.md: added branch model section
- release-discipline.md: added dev branch staging notes

* ci: retrigger CI for PR #160

* feat: allow Claude Code to merge PRs targeting dev branch only

Release guard hooks now check the PR's base branch:
- dev → allowed (TestPyPI/staging)
- master/main → blocked (PyPI releases remain Nathan-only)

Both Bash hook (gh pr merge) and MCP hook (merge_pull_request)
updated with base branch checking via gh pr view.

* docs: add workflow mode indicator and modes.md to CLAUDE.md

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Nathan Schram (nathanschram) added a commit that referenced this pull request Mar 21, 2026
….0rc8

* ci: add CODEOWNERS, update action SHA pins, add permission comments

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add release guard hooks and document protection in CLAUDE.md

Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify /config default labels and remove redundant "Works with" lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress redundant cost footer on error runs

When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: suppress stall notifications when CPU-active + heartbeat re-render

When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.34.5rc2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: CI release-validation tomllib bytes/str mismatch

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: integrate screenshots into docs with correct JPG references

- Add 44 screenshots to docs/assets/screenshots/
- Fix all image refs from .png to .jpg across 25 doc files
- README uses absolute raw.githubusercontent.com URLs for PyPI rendering
- Fix 5 filename mismatches (session-auto-resume→chat-auto-resume, etc.)
- Comment out 11 missing screenshots with TODO markers
- Add CAPTURES.md checklist tracking capture status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: convert markdown images to HTML img tags for GitHub compatibility

Switch from MkDocs `![alt](src){ loading=lazy }` syntax to HTML `<img>`
tags with width="360" and loading="lazy". Fixes two GitHub rendering
issues: `{ loading=lazy }` appearing as visible text, and oversized
images with no width constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix 3 screenshot mismatches and replace 3 screenshots

- first-run.md: rewrite resume line text to match footer screenshot
- interactive-control.md: update planmode show admonition to match screenshot (auto not on)
- switch-engines.md: swap engine-footer.jpg for multi-engine-switch.jpg
- Replace startup-message.jpg with clean v0.34.4 capture (was rc/6-projects)
- Replace cooldown-auto-deny.jpg with post-outline approve/deny buttons
- Replace file-put.jpg with photo save confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add iOS caption limitation note to file transfer guide

Telegram iOS doesn't show a caption field when sending documents via the
File picker, so /file put <path> captions aren't easily accessible.
Added a note with workarounds (use Desktop, send as photo, or let
auto-save handle it). Updated screenshot alt text to match actual
screenshot content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: temp swap README image URLs to feature branch for preview

Will revert to master before merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: lay out all 3 README screenshots in a single row

Reduce from 360px to 270px each and combine into one <p> block
so all three hero screenshots sit side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap 3rd hero screenshot for config-menu for visual variety

Replace plan-outline-approve (too similar to approval-diff-preview)
with config-menu showing the /config settings grid. The three hero
images now tell: voice input → approve changes → configure everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add captions under README hero screenshots

Small <sub> captions: "Send tasks by voice (Whisper transcription)",
"Approve changes remotely", "Configure from Telegram".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: use table layout for README hero screenshots with captions

Fixes stacking issue — <br> in a <p> broke inline flow. A table
keeps images side by side with captions underneath each one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace table layout with single hero collage image

Composite image scales proportionally on mobile instead of requiring
horizontal scroll. Captions baked into the image via ImageMagick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap middle hero screenshot for full 3-button approval view

Replace approval-diff-preview with approval-buttons-howto showing
Approve / Deny / Pause & Outline Plan — more visually impressive.
Caption now reads "Approve changes remotely (Claude Code)".
Added footnote linking to engine compatibility table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap config-menu for parallel-projects in hero collage

Third hero screenshot now shows 10+ projects running simultaneously
across different repos — much more compelling than a settings menu.
New caption: "Run agents across projects in parallel".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: revert README image URL to master for merge

Swap hero-collage URL back from feature/github-hardening to master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc3

- fix: preserve all EngineOverrides fields when setting model/planmode/reasoning
  (was silently wiping ask_questions, diff_preview, show_api_cost, etc.)
- fix: /config home page resolves "default" to effective values
- feat: file upload auto-deduplication (append _1, _2 instead of requiring --force)
- feat: media groups without captions now auto-save instead of showing usage text
- feat: resume line visual separation (blank line + ↩️ prefix)
- fix: claude auto-approve echoes updatedInput in control response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: expand permission policies for Codex CLI and Gemini CLI in /config

Codex gets a new "Approval policy" page (full auto / safe) that passes
--ask-for-approval untrusted when safe mode is selected. Gemini's approval
mode expands from 2 to 3 tiers (read-only / edit files / full access) with
--approval-mode auto_edit for the middle tier. Both engines now show an
"Agent controls" section on the /config home page. Engine-specific model
default hints replace the generic "from CLI settings" text.

Also adds staging.sh helper, context-guard-stop hook, and docs updates.

Closes #131

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /config UX overhaul, resume line toggle, cost budget settings, model metadata

/config UX cleanup:
- Convert all binary toggles from 3-column (on/off/clear) to 2-column
  (toggle + clear) for better mobile tap targets
- Merge Engine + Model into combined "Engine & model" page
- Reorganise home page to max 2 buttons per row across all engines
- Split plan mode 3-option rows (off/on/auto) into 2+1 layout
- Add _toggle_row() helper for consistent toggle button rendering

New features:
- #128: Resume line /config toggle — per-chat show_resume_line override
  via EngineOverrides with On/Off/Clear buttons, wired into executor
- #129: Cost budget /config settings — per-chat budget_enabled and
  budget_auto_cancel overrides on the Cost & Usage page, wired into
  _check_cost_budget() in runner_bridge.py

Model metadata improvements:
- Show Claude Code [1m] context window suffix: "opus 4.6 (1M)"
- Strip Gemini CLI "auto-" prefix: "auto-gemini-3" → "gemini-3"
- Future-proof: unknown suffixes default to .upper() (e.g. [500k] → 500K)

Bug fixes:
- #124: Standalone override commands (/planmode, /model, /reasoning) now
  preserve all EngineOverrides fields including new ones
- Error handling: control_response.write_failed catch-all in claude.py,
  ask_question.extraction_failed warning, model.override.failed logging

Hardening:
- Plan outline sent as separate ephemeral message (avoids 4096 char truncation)
- Added show_resume_line, budget_enabled, budget_auto_cancel to
  EngineOverrides, EngineRunOptions, normalize/merge, and all constructors

Tests: 1610 passed, 80.56% coverage, ruff clean.
Integration tested on @untether_dev_bot across all 6 engine chats.

Closes #128, closes #129, fixes #124

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI for PR #132

* fix: address 11 CodeRabbit review comments on PR #132

Bug fixes:
- claude.py: fix UnboundLocalError when factory.resume is falsy in
  ask_question.extraction_failed logging path
- ask_question.py: reject malformed option callbacks instead of
  silently falling back to option 0
- files.py: raise FileExistsError when deduplicate_target exhausts
  999 suffixes instead of returning the original (overwrite risk)
- config.py: disambiguate Codex "Full auto" (fa) vs Gemini "Full
  access" (ya) callback IDs and toast labels

Hardening:
- codex.py: add --ask-for-approval to _EXEC_ONLY_FLAGS guard
- model.py: add try/except to clear path (matching set path)
- reasoning.py: add try/except to clear path (matching set path)
- loop.py: notify user when media group upload fails instead of
  silently dropping
- export.py: log session count instead of identifiers at info level
- config.py: resolve resume-line default from config instead of
  hardcoding True
- staging.sh: pin PyPI index in rollback/reset with --pip-args

Skipped (not applicable):
- CHANGELOG.md: RC versions don't get changelog entries per release
  discipline
- docs/tutorials TODO screenshot: pre-existing, not introduced by PR
- .claude/hooks/context-guard-stop.sh: ContextDocs plugin hook, not
  Untether source

Tests: 1611 passed, 80.48% coverage, ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace bare pass with debug log to satisfy bandit B110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: setup wizard security + UX improvements

- Auto-set allowed_user_ids from captured Telegram user ID during
  onboarding (security: restricts bot to the setup user's account)
- Add "next steps" panel after wizard completion with pointers to
  /config, voice notes, projects, and account lock confirmation
- Update install.md: Python 3.12+ (not just 3.14), dynamic version
  string, /config mention for post-setup changes
- Update first-run.md: /config → Engine & model for default engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: plan outline UX — markdown rendering, buttons, cleanup (#139, #140, #141)

- Render outline messages as formatted text via render_markdown() +
  split_markdown_body() instead of raw markdown (#139)
- Add approve/deny buttons to last outline message so users don't
  have to scroll up past long outlines (#140)
- Delete outline messages on approve/deny via module-level
  _OUTLINE_REGISTRY callable from callback handler; suppress stale
  keyboard on progress message (#141)
- 8 new tests for outline rendering, keyboard placement, and cleanup
- Bump version to 0.35.0rc5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /continue command — cross-environment resume for all engines (#135)

New `/continue` command resumes the most recent CLI session in the
project directory from Telegram. Enables starting a session in your
terminal and picking it up from your phone.

Engine support: Claude (--continue), Codex (resume --last), OpenCode
(--continue), Pi (--continue), Gemini (--resume latest). AMP not
supported (requires explicit thread ID).

Includes ResumeToken.is_continue flag, build_args for all 6 runners,
reserved command registration, resume emoji prefix stripping for
reply-to-continue, docs (how-to guide, README, commands ref, routing
explanation, conversation modes tutorial), and 99 new test assertions.

Integration tested against @untether_dev_bot — all 5 supported engines
passed secret-recall verification via Telegram MCP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: agent outbox file delivery + fix cross-chat ask stealing (#143, #144)

Outbox delivery (#143): agents write files to .untether-outbox/ during a
run; Untether sends them as Telegram documents on completion with 📎
captions. Config: outbox_enabled, outbox_dir, outbox_max_files,
outbox_cleanup. Deny-glob security, size limits, auto-cleanup.
Preamble updated for all 6 engines. Integration tested across
Claude, Codex, OpenCode, Pi, and Gemini.

AskUserQuestion fix (#144): _PENDING_ASK_REQUESTS and
_ASK_QUESTION_FLOWS were global dicts with no chat_id scoping — a
pending ask in one chat would steal the next message from any other
chat. Added channel_id contextvar and scoped all ask lookups by it.
Session cleanup now also clears stale pending asks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: v0.35.0 changelog completion + fix #123 updatedInput

- Complete v0.35.0 changelog: add missing entries for /continue (#135),
  /config UX overhaul (#132), resume line toggle (#128), cost budget
  (#129), model metadata, resume line formatting (#127), override
  preservation (#124), and updatedInput fix (#123)
- Fix #123: register input for system-level auto-approved control
  requests so updatedInput is included in the response
- Add parameterised test for all 5 auto-approve types input registration
- Remove unused OutboxResult import (ruff fix)

Issues closed: #115, #118, #123, #124, #126, #127, #134

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.35.0rc6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rc6 integration test fixes (#145, #146, #147, #148, #149)

- Reduce Telegram API timeout from 120s to 30s (#145)
- OpenCode error runs show error text instead of empty body (#146)
- Pi /continue captures session ID via allow_id_promotion (#147)
- Post-outline approval uses skip_reply to avoid "not found" (#148)
- Orphan progress message cleanup on restart (#149)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: post-outline notification reply + OpenCode empty body (#148, #150)

- #148: skip_reply callback results now bypass the executor's default
  reply_to fallback, sending directly via the transport with no
  reply_to_message_id. Previously, the executor treated reply_to=None
  as "use default" which pointed to the (deleted) outline message.

- #150: OpenCode normal completion with no Text events now falls back
  to last_tool_error. Added state.last_tool_error field populated on
  ToolUse error status. Covers both translate() and stream_end_events().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: suppress post-outline notification to avoid "message not found" (#148)

After outline approval/denial, the progress loop's _send_notify was
firing for the next tool approval, but the notification's reply_to
anchor could reference deleted state. Added _outline_just_resolved
flag to skip one notification cycle after outline cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: note OpenCode lacks auto-compaction — long sessions degrade (#150)

Added known limitation to OpenCode runner docs and integration testing
playbook. OpenCode sessions accumulate unbounded context (no compaction
events unlike Pi). Workaround: use /new before isolated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip_reply on regular approve path when outline was deleted (#148)

The "Approve Plan" button on outline messages uses the real
ExitPlanMode request_id, routing through the regular approve path
(not the da: synthetic path). When outline messages exist, set
skip_reply=True on the CommandResult to avoid replying to the
just-deleted outline message.

Also added reply_to_message_id and text_preview to transport.send.failed
warning for easier debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc6 integration test fixes (#145-#150)

Updated fix descriptions for #146/#150 (OpenCode last_tool_error
fallback) and #148 (regular approve path skip_reply). Added docs
section for OpenCode compaction limitation. Updated test counts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix formatting after merge resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review comments on PR #151

- bridge.py: replace text_preview with text_len in send failure warning
  to avoid logging raw message content (security)
- runner_bridge.py: move unregister_progress() after send_result_message()
  to avoid orphan window between ephemeral cleanup and final message send
- cross-environment-resume.md: add language spec to code block

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve /config "default" labels to effective on/off values (#152)

Sub-pages showed "Current: default" or "default (on/off)" while buttons
already showed the resolved value. Now all boolean-toggle settings show
the effective on/off value in both text and buttons.

Affected: verbose, ask mode, diff preview, API cost, subscription usage,
budget enabled/auto-cancel, resume line. Home page cost & resume labels
also resolved.

Plan mode, model, and reasoning keep "default" since they depend on CLI
settings and aren't simple on/off booleans.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc7 config default labels fix (#152)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update documentation for v0.35.0

- fix missing nav entries in zensical.toml (cross-env resume, Gemini/Amp runners)
- rewrite inline-settings.md for /config UX overhaul (2-column toggles, budget/resume toggles)
- update plan-mode.md with outline rendering, buttons-on-last-chunk, ephemeral cleanup
- update interactive-control tutorial with outline UX improvements
- add orphan progress cleanup section to operations.md
- add engine-specific approval policies to interactive-approval.md
- add per-chat budget overrides to cost-budgets.md
- update module-map.md with Gemini/Amp and new modules (outbox, progress persistence, proc_diag)
- update architecture.md mermaid diagrams with all 6 engines
- bump specification.md to v0.35.0, add progress persistence and outbox sections
- add v0.35.0 screenshot entries to CAPTURES.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: broaden frozen ring buffer stall escalation beyond MCP tools (#155)

Frozen ring buffer escalation was gated on `mcp_server is not None`,
so general stalls with cpu_active=True and no MCP tool running were
silently suppressed indefinitely. Broadened to fire for all stalls
after 3+ checks with no new JSONL events regardless of tool type.

New notification: "CPU active, no new events" for non-MCP frozen stalls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: tool approval buttons no longer suppressed after outline approval (#156)

After "Approve Plan" on an outline, the stale discuss_approve action
remained in ProgressTracker with completed=False. The renderer picked
up its stale "Approve Plan"/"Deny" buttons first, then the suppression
logic at line 994 stripped ALL buttons — including new Write/Edit/Bash
approval buttons. Claude blocked indefinitely waiting for approval.

Fix: after suppressing stale buttons, complete the discuss_approve
action(s) in the tracker, reset _outline_sent, and trigger a re-render
so subsequent tool requests get their own Approve/Deny buttons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add workflow mode indicator to startup message, fix startup crash on topics validation failure (#158, #159)

Features:
- Startup message now shows mode: assistant/workspace/handoff
- Derived from session_mode + topics.enabled config values
- _resolve_mode_label() helper in backend.py

Bug fixes:
- Fix UnboundLocalError crash when topics validation fails on startup (#158)
  - Moved import signal and shutdown imports before try block in loop.py
- Downgrade can_manage_topics check from fatal error to warning (#159)
  - Bot can now start without manage_topics admin right
  - Existing topics work fine; only topic creation/editing affected

Tests:
- 17 new unit tests for stateless/handoff mode (test_stateless_mode.py)
  - _should_show_resume_line, _chat_session_key, ResumeResolver, ResumeLineProxy
  - Integration-level: stateless shows resume lines, no auto-resume, chat hides lines
- 3 new tests for mode indicator in startup message (test_telegram_backend.py)

Docs:
- New docs/reference/modes.md — comprehensive reference for all 3 workflow modes
- Updated docs/reference/index.md and zensical.toml nav with modes page

* docs: comprehensive three-mode coverage across all documentation

New:
- docs/how-to/choose-a-mode.md — decision tree, mode comparison, mermaid
  sequence diagrams, configuration examples, switching guide, workspace
  prerequisites

Updated:
- README.md — improved three-mode description in features list
- docs/tutorials/install.md — added mode selection step (section 10)
- docs/tutorials/first-run.md — added 'What mode am I in?' tip
- docs/reference/config.md — cross-linked session_mode/show_resume_line to modes.md
- docs/reference/transports/telegram.md — added mode requirement callouts
  for forum topics and chat sessions sections
- docs/how-to/chat-sessions.md — added session persistence explanation
  (state files, auto-resume mechanics, handoff note)
- docs/how-to/topics.md — expanded prerequisites checklist with group
  privacy, can_manage_topics, and re-add steps
- docs/how-to/cross-environment-resume.md — added handoff mode terminal
  workflow with mermaid sequence diagram
- docs/how-to/index.md — added 'Getting started' section with choose-a-mode
- zensical.toml — added choose-a-mode to nav

* docs: add three-mode summary table to README Quick Start section

* feat: migrate to dev branch workflow — dev→TestPyPI, master→PyPI

Branch model:
- feature/* → PR → dev (TestPyPI auto-publish) → PR → master (PyPI)
- master always matches latest PyPI release
- dev is the integration/staging branch

CI changes:
- ci.yml: TestPyPI publish triggers on dev push (was master)
- ci.yml, codeql.yml: CI runs on both master and dev pushes
- dependabot.yml: PRs target dev branch

Hook changes:
- release-guard.sh: updated messages to mention dev branch
- release-guard-mcp.sh: updated messages to mention dev branch
- Both hooks already allow dev pushes (only block master/main)

Documentation:
- CLAUDE.md: updated 3-phase workflow, CI table, release guard docs
- dev-workflow.md: added branch model section
- release-discipline.md: added dev branch staging notes

* ci: retrigger CI for PR #160

* feat: allow Claude Code to merge PRs targeting dev branch only

Release guard hooks now check the PR's base branch:
- dev → allowed (TestPyPI/staging)
- master/main → blocked (PyPI releases remain Nathan-only)

Both Bash hook (gh pr merge) and MCP hook (merge_pull_request)
updated with base branch checking via gh pr view.

* docs: add workflow mode indicator and modes.md to CLAUDE.md

* fix: dual outline buttons (#163), entity URL sanitisation (#157), changelog migration

- Strip approval buttons from progress message when outline is visible —
  only outline message shows Approve/Deny/Cancel (#163)
- Reset outline state via source_has_approval tracking so future
  ExitPlanMode requests work correctly (#163)
- Sanitise text_link entities with invalid URLs (localhost, loopback,
  file paths, bare hostnames) by converting to code entities — prevents
  silent 400 errors that drop the entire final message (#157)
- Merge v0.34.5 changelog into v0.35.0 — v0.34.5 was never released
  (latest PyPI is v0.34.4), all rc1-rc7 work is v0.35.0

17 new tests (2 for #163, 15 for #157).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.35.0rc8

fix: restore frozen ring buffer stall escalation (#155)

The #163 fix (6f43e5b) accidentally removed all frozen ring buffer
code from runner_bridge.py. Restored from 8fcad32:

- _frozen_ring_count tracking and ring buffer snapshot comparison
- frozen_escalate gating (fires notification after 3+ frozen checks
  despite cpu_active=True)
- _has_running_mcp_tool() for MCP server name extraction
- _STALL_THRESHOLD_MCP_TOOL (15 min, configurable via watchdog)
- MCP-aware notification text ("MCP tool may be hung", "CPU active,
  no new events", "MCP tool running")
- 8 new tests + 2 updated existing tests
- mcp_tool_timeout watchdog setting

docs: integration testing S1 MCP threshold, tutorials index,
glossary, outbox screenshot, CAPTURES checklist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: CI lint — unused import in test, bandit nosec for loopback blocklist

- Remove unused ActionEvent import in test_has_running_mcp_tool_returns_server_name
- Add # nosec B104 to _LOOPBACK_HOSTS — it's a URL blocklist, not a bind address

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Nathan Schram (nathanschram) added a commit that referenced this pull request Mar 21, 2026
* ci: add CODEOWNERS, update action SHA pins, add permission comments

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add release guard hooks and document protection in CLAUDE.md

Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify /config default labels and remove redundant "Works with" lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress redundant cost footer on error runs

When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: suppress stall notifications when CPU-active + heartbeat re-render

When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.34.5rc2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: CI release-validation tomllib bytes/str mismatch

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: integrate screenshots into docs with correct JPG references

- Add 44 screenshots to docs/assets/screenshots/
- Fix all image refs from .png to .jpg across 25 doc files
- README uses absolute raw.githubusercontent.com URLs for PyPI rendering
- Fix 5 filename mismatches (session-auto-resume→chat-auto-resume, etc.)
- Comment out 11 missing screenshots with TODO markers
- Add CAPTURES.md checklist tracking capture status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: convert markdown images to HTML img tags for GitHub compatibility

Switch from MkDocs `![alt](src){ loading=lazy }` syntax to HTML `<img>`
tags with width="360" and loading="lazy". Fixes two GitHub rendering
issues: `{ loading=lazy }` appearing as visible text, and oversized
images with no width constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix 3 screenshot mismatches and replace 3 screenshots

- first-run.md: rewrite resume line text to match footer screenshot
- interactive-control.md: update planmode show admonition to match screenshot (auto not on)
- switch-engines.md: swap engine-footer.jpg for multi-engine-switch.jpg
- Replace startup-message.jpg with clean v0.34.4 capture (was rc/6-projects)
- Replace cooldown-auto-deny.jpg with post-outline approve/deny buttons
- Replace file-put.jpg with photo save confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add iOS caption limitation note to file transfer guide

Telegram iOS doesn't show a caption field when sending documents via the
File picker, so /file put <path> captions aren't easily accessible.
Added a note with workarounds (use Desktop, send as photo, or let
auto-save handle it). Updated screenshot alt text to match actual
screenshot content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: temp swap README image URLs to feature branch for preview

Will revert to master before merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: lay out all 3 README screenshots in a single row

Reduce from 360px to 270px each and combine into one <p> block
so all three hero screenshots sit side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap 3rd hero screenshot for config-menu for visual variety

Replace plan-outline-approve (too similar to approval-diff-preview)
with config-menu showing the /config settings grid. The three hero
images now tell: voice input → approve changes → configure everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add captions under README hero screenshots

Small <sub> captions: "Send tasks by voice (Whisper transcription)",
"Approve changes remotely", "Configure from Telegram".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: use table layout for README hero screenshots with captions

Fixes stacking issue — <br> in a <p> broke inline flow. A table
keeps images side by side with captions underneath each one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace table layout with single hero collage image

Composite image scales proportionally on mobile instead of requiring
horizontal scroll. Captions baked into the image via ImageMagick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap middle hero screenshot for full 3-button approval view

Replace approval-diff-preview with approval-buttons-howto showing
Approve / Deny / Pause & Outline Plan — more visually impressive.
Caption now reads "Approve changes remotely (Claude Code)".
Added footnote linking to engine compatibility table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap config-menu for parallel-projects in hero collage

Third hero screenshot now shows 10+ projects running simultaneously
across different repos — much more compelling than a settings menu.
New caption: "Run agents across projects in parallel".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: revert README image URL to master for merge

Swap hero-collage URL back from feature/github-hardening to master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc3

- fix: preserve all EngineOverrides fields when setting model/planmode/reasoning
  (was silently wiping ask_questions, diff_preview, show_api_cost, etc.)
- fix: /config home page resolves "default" to effective values
- feat: file upload auto-deduplication (append _1, _2 instead of requiring --force)
- feat: media groups without captions now auto-save instead of showing usage text
- feat: resume line visual separation (blank line + ↩️ prefix)
- fix: claude auto-approve echoes updatedInput in control response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: expand permission policies for Codex CLI and Gemini CLI in /config

Codex gets a new "Approval policy" page (full auto / safe) that passes
--ask-for-approval untrusted when safe mode is selected. Gemini's approval
mode expands from 2 to 3 tiers (read-only / edit files / full access) with
--approval-mode auto_edit for the middle tier. Both engines now show an
"Agent controls" section on the /config home page. Engine-specific model
default hints replace the generic "from CLI settings" text.

Also adds staging.sh helper, context-guard-stop hook, and docs updates.

Closes #131

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /config UX overhaul, resume line toggle, cost budget settings, model metadata

/config UX cleanup:
- Convert all binary toggles from 3-column (on/off/clear) to 2-column
  (toggle + clear) for better mobile tap targets
- Merge Engine + Model into combined "Engine & model" page
- Reorganise home page to max 2 buttons per row across all engines
- Split plan mode 3-option rows (off/on/auto) into 2+1 layout
- Add _toggle_row() helper for consistent toggle button rendering

New features:
- #128: Resume line /config toggle — per-chat show_resume_line override
  via EngineOverrides with On/Off/Clear buttons, wired into executor
- #129: Cost budget /config settings — per-chat budget_enabled and
  budget_auto_cancel overrides on the Cost & Usage page, wired into
  _check_cost_budget() in runner_bridge.py

Model metadata improvements:
- Show Claude Code [1m] context window suffix: "opus 4.6 (1M)"
- Strip Gemini CLI "auto-" prefix: "auto-gemini-3" → "gemini-3"
- Future-proof: unknown suffixes default to .upper() (e.g. [500k] → 500K)

Bug fixes:
- #124: Standalone override commands (/planmode, /model, /reasoning) now
  preserve all EngineOverrides fields including new ones
- Error handling: control_response.write_failed catch-all in claude.py,
  ask_question.extraction_failed warning, model.override.failed logging

Hardening:
- Plan outline sent as separate ephemeral message (avoids 4096 char truncation)
- Added show_resume_line, budget_enabled, budget_auto_cancel to
  EngineOverrides, EngineRunOptions, normalize/merge, and all constructors

Tests: 1610 passed, 80.56% coverage, ruff clean.
Integration tested on @untether_dev_bot across all 6 engine chats.

Closes #128, closes #129, fixes #124

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI for PR #132

* fix: address 11 CodeRabbit review comments on PR #132

Bug fixes:
- claude.py: fix UnboundLocalError when factory.resume is falsy in
  ask_question.extraction_failed logging path
- ask_question.py: reject malformed option callbacks instead of
  silently falling back to option 0
- files.py: raise FileExistsError when deduplicate_target exhausts
  999 suffixes instead of returning the original (overwrite risk)
- config.py: disambiguate Codex "Full auto" (fa) vs Gemini "Full
  access" (ya) callback IDs and toast labels

Hardening:
- codex.py: add --ask-for-approval to _EXEC_ONLY_FLAGS guard
- model.py: add try/except to clear path (matching set path)
- reasoning.py: add try/except to clear path (matching set path)
- loop.py: notify user when media group upload fails instead of
  silently dropping
- export.py: log session count instead of identifiers at info level
- config.py: resolve resume-line default from config instead of
  hardcoding True
- staging.sh: pin PyPI index in rollback/reset with --pip-args

Skipped (not applicable):
- CHANGELOG.md: RC versions don't get changelog entries per release
  discipline
- docs/tutorials TODO screenshot: pre-existing, not introduced by PR
- .claude/hooks/context-guard-stop.sh: ContextDocs plugin hook, not
  Untether source

Tests: 1611 passed, 80.48% coverage, ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace bare pass with debug log to satisfy bandit B110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: setup wizard security + UX improvements

- Auto-set allowed_user_ids from captured Telegram user ID during
  onboarding (security: restricts bot to the setup user's account)
- Add "next steps" panel after wizard completion with pointers to
  /config, voice notes, projects, and account lock confirmation
- Update install.md: Python 3.12+ (not just 3.14), dynamic version
  string, /config mention for post-setup changes
- Update first-run.md: /config → Engine & model for default engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: plan outline UX — markdown rendering, buttons, cleanup (#139, #140, #141)

- Render outline messages as formatted text via render_markdown() +
  split_markdown_body() instead of raw markdown (#139)
- Add approve/deny buttons to last outline message so users don't
  have to scroll up past long outlines (#140)
- Delete outline messages on approve/deny via module-level
  _OUTLINE_REGISTRY callable from callback handler; suppress stale
  keyboard on progress message (#141)
- 8 new tests for outline rendering, keyboard placement, and cleanup
- Bump version to 0.35.0rc5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /continue command — cross-environment resume for all engines (#135)

New `/continue` command resumes the most recent CLI session in the
project directory from Telegram. Enables starting a session in your
terminal and picking it up from your phone.

Engine support: Claude (--continue), Codex (resume --last), OpenCode
(--continue), Pi (--continue), Gemini (--resume latest). AMP not
supported (requires explicit thread ID).

Includes ResumeToken.is_continue flag, build_args for all 6 runners,
reserved command registration, resume emoji prefix stripping for
reply-to-continue, docs (how-to guide, README, commands ref, routing
explanation, conversation modes tutorial), and 99 new test assertions.

Integration tested against @untether_dev_bot — all 5 supported engines
passed secret-recall verification via Telegram MCP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: agent outbox file delivery + fix cross-chat ask stealing (#143, #144)

Outbox delivery (#143): agents write files to .untether-outbox/ during a
run; Untether sends them as Telegram documents on completion with 📎
captions. Config: outbox_enabled, outbox_dir, outbox_max_files,
outbox_cleanup. Deny-glob security, size limits, auto-cleanup.
Preamble updated for all 6 engines. Integration tested across
Claude, Codex, OpenCode, Pi, and Gemini.

AskUserQuestion fix (#144): _PENDING_ASK_REQUESTS and
_ASK_QUESTION_FLOWS were global dicts with no chat_id scoping — a
pending ask in one chat would steal the next message from any other
chat. Added channel_id contextvar and scoped all ask lookups by it.
Session cleanup now also clears stale pending asks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: v0.35.0 changelog completion + fix #123 updatedInput

- Complete v0.35.0 changelog: add missing entries for /continue (#135),
  /config UX overhaul (#132), resume line toggle (#128), cost budget
  (#129), model metadata, resume line formatting (#127), override
  preservation (#124), and updatedInput fix (#123)
- Fix #123: register input for system-level auto-approved control
  requests so updatedInput is included in the response
- Add parameterised test for all 5 auto-approve types input registration
- Remove unused OutboxResult import (ruff fix)

Issues closed: #115, #118, #123, #124, #126, #127, #134

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.35.0rc6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rc6 integration test fixes (#145, #146, #147, #148, #149)

- Reduce Telegram API timeout from 120s to 30s (#145)
- OpenCode error runs show error text instead of empty body (#146)
- Pi /continue captures session ID via allow_id_promotion (#147)
- Post-outline approval uses skip_reply to avoid "not found" (#148)
- Orphan progress message cleanup on restart (#149)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: post-outline notification reply + OpenCode empty body (#148, #150)

- #148: skip_reply callback results now bypass the executor's default
  reply_to fallback, sending directly via the transport with no
  reply_to_message_id. Previously, the executor treated reply_to=None
  as "use default" which pointed to the (deleted) outline message.

- #150: OpenCode normal completion with no Text events now falls back
  to last_tool_error. Added state.last_tool_error field populated on
  ToolUse error status. Covers both translate() and stream_end_events().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: suppress post-outline notification to avoid "message not found" (#148)

After outline approval/denial, the progress loop's _send_notify was
firing for the next tool approval, but the notification's reply_to
anchor could reference deleted state. Added _outline_just_resolved
flag to skip one notification cycle after outline cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: note OpenCode lacks auto-compaction — long sessions degrade (#150)

Added known limitation to OpenCode runner docs and integration testing
playbook. OpenCode sessions accumulate unbounded context (no compaction
events unlike Pi). Workaround: use /new before isolated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip_reply on regular approve path when outline was deleted (#148)

The "Approve Plan" button on outline messages uses the real
ExitPlanMode request_id, routing through the regular approve path
(not the da: synthetic path). When outline messages exist, set
skip_reply=True on the CommandResult to avoid replying to the
just-deleted outline message.

Also added reply_to_message_id and text_preview to transport.send.failed
warning for easier debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc6 integration test fixes (#145-#150)

Updated fix descriptions for #146/#150 (OpenCode last_tool_error
fallback) and #148 (regular approve path skip_reply). Added docs
section for OpenCode compaction limitation. Updated test counts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix formatting after merge resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review comments on PR #151

- bridge.py: replace text_preview with text_len in send failure warning
  to avoid logging raw message content (security)
- runner_bridge.py: move unregister_progress() after send_result_message()
  to avoid orphan window between ephemeral cleanup and final message send
- cross-environment-resume.md: add language spec to code block

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve /config "default" labels to effective on/off values (#152)

Sub-pages showed "Current: default" or "default (on/off)" while buttons
already showed the resolved value. Now all boolean-toggle settings show
the effective on/off value in both text and buttons.

Affected: verbose, ask mode, diff preview, API cost, subscription usage,
budget enabled/auto-cancel, resume line. Home page cost & resume labels
also resolved.

Plan mode, model, and reasoning keep "default" since they depend on CLI
settings and aren't simple on/off booleans.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc7 config default labels fix (#152)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update documentation for v0.35.0

- fix missing nav entries in zensical.toml (cross-env resume, Gemini/Amp runners)
- rewrite inline-settings.md for /config UX overhaul (2-column toggles, budget/resume toggles)
- update plan-mode.md with outline rendering, buttons-on-last-chunk, ephemeral cleanup
- update interactive-control tutorial with outline UX improvements
- add orphan progress cleanup section to operations.md
- add engine-specific approval policies to interactive-approval.md
- add per-chat budget overrides to cost-budgets.md
- update module-map.md with Gemini/Amp and new modules (outbox, progress persistence, proc_diag)
- update architecture.md mermaid diagrams with all 6 engines
- bump specification.md to v0.35.0, add progress persistence and outbox sections
- add v0.35.0 screenshot entries to CAPTURES.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: broaden frozen ring buffer stall escalation beyond MCP tools (#155)

Frozen ring buffer escalation was gated on `mcp_server is not None`,
so general stalls with cpu_active=True and no MCP tool running were
silently suppressed indefinitely. Broadened to fire for all stalls
after 3+ checks with no new JSONL events regardless of tool type.

New notification: "CPU active, no new events" for non-MCP frozen stalls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: tool approval buttons no longer suppressed after outline approval (#156)

After "Approve Plan" on an outline, the stale discuss_approve action
remained in ProgressTracker with completed=False. The renderer picked
up its stale "Approve Plan"/"Deny" buttons first, then the suppression
logic at line 994 stripped ALL buttons — including new Write/Edit/Bash
approval buttons. Claude blocked indefinitely waiting for approval.

Fix: after suppressing stale buttons, complete the discuss_approve
action(s) in the tracker, reset _outline_sent, and trigger a re-render
so subsequent tool requests get their own Approve/Deny buttons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add workflow mode indicator to startup message, fix startup crash on topics validation failure (#158, #159)

Features:
- Startup message now shows mode: assistant/workspace/handoff
- Derived from session_mode + topics.enabled config values
- _resolve_mode_label() helper in backend.py

Bug fixes:
- Fix UnboundLocalError crash when topics validation fails on startup (#158)
  - Moved import signal and shutdown imports before try block in loop.py
- Downgrade can_manage_topics check from fatal error to warning (#159)
  - Bot can now start without manage_topics admin right
  - Existing topics work fine; only topic creation/editing affected

Tests:
- 17 new unit tests for stateless/handoff mode (test_stateless_mode.py)
  - _should_show_resume_line, _chat_session_key, ResumeResolver, ResumeLineProxy
  - Integration-level: stateless shows resume lines, no auto-resume, chat hides lines
- 3 new tests for mode indicator in startup message (test_telegram_backend.py)

Docs:
- New docs/reference/modes.md — comprehensive reference for all 3 workflow modes
- Updated docs/reference/index.md and zensical.toml nav with modes page

* docs: comprehensive three-mode coverage across all documentation

New:
- docs/how-to/choose-a-mode.md — decision tree, mode comparison, mermaid
  sequence diagrams, configuration examples, switching guide, workspace
  prerequisites

Updated:
- README.md — improved three-mode description in features list
- docs/tutorials/install.md — added mode selection step (section 10)
- docs/tutorials/first-run.md — added 'What mode am I in?' tip
- docs/reference/config.md — cross-linked session_mode/show_resume_line to modes.md
- docs/reference/transports/telegram.md — added mode requirement callouts
  for forum topics and chat sessions sections
- docs/how-to/chat-sessions.md — added session persistence explanation
  (state files, auto-resume mechanics, handoff note)
- docs/how-to/topics.md — expanded prerequisites checklist with group
  privacy, can_manage_topics, and re-add steps
- docs/how-to/cross-environment-resume.md — added handoff mode terminal
  workflow with mermaid sequence diagram
- docs/how-to/index.md — added 'Getting started' section with choose-a-mode
- zensical.toml — added choose-a-mode to nav

* docs: add three-mode summary table to README Quick Start section

* feat: migrate to dev branch workflow — dev→TestPyPI, master→PyPI

Branch model:
- feature/* → PR → dev (TestPyPI auto-publish) → PR → master (PyPI)
- master always matches latest PyPI release
- dev is the integration/staging branch

CI changes:
- ci.yml: TestPyPI publish triggers on dev push (was master)
- ci.yml, codeql.yml: CI runs on both master and dev pushes
- dependabot.yml: PRs target dev branch

Hook changes:
- release-guard.sh: updated messages to mention dev branch
- release-guard-mcp.sh: updated messages to mention dev branch
- Both hooks already allow dev pushes (only block master/main)

Documentation:
- CLAUDE.md: updated 3-phase workflow, CI table, release guard docs
- dev-workflow.md: added branch model section
- release-discipline.md: added dev branch staging notes

* ci: retrigger CI for PR #160

* feat: allow Claude Code to merge PRs targeting dev branch only

Release guard hooks now check the PR's base branch:
- dev → allowed (TestPyPI/staging)
- master/main → blocked (PyPI releases remain Nathan-only)

Both Bash hook (gh pr merge) and MCP hook (merge_pull_request)
updated with base branch checking via gh pr view.

* docs: add workflow mode indicator and modes.md to CLAUDE.md

* fix: dual outline buttons (#163), entity URL sanitisation (#157), changelog migration

- Strip approval buttons from progress message when outline is visible —
  only outline message shows Approve/Deny/Cancel (#163)
- Reset outline state via source_has_approval tracking so future
  ExitPlanMode requests work correctly (#163)
- Sanitise text_link entities with invalid URLs (localhost, loopback,
  file paths, bare hostnames) by converting to code entities — prevents
  silent 400 errors that drop the entire final message (#157)
- Merge v0.34.5 changelog into v0.35.0 — v0.34.5 was never released
  (latest PyPI is v0.34.4), all rc1-rc7 work is v0.35.0

17 new tests (2 for #163, 15 for #157).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.35.0rc8

fix: restore frozen ring buffer stall escalation (#155)

The #163 fix (6f43e5b) accidentally removed all frozen ring buffer
code from runner_bridge.py. Restored from 8fcad32:

- _frozen_ring_count tracking and ring buffer snapshot comparison
- frozen_escalate gating (fires notification after 3+ frozen checks
  despite cpu_active=True)
- _has_running_mcp_tool() for MCP server name extraction
- _STALL_THRESHOLD_MCP_TOOL (15 min, configurable via watchdog)
- MCP-aware notification text ("MCP tool may be hung", "CPU active,
  no new events", "MCP tool running")
- 8 new tests + 2 updated existing tests
- mcp_tool_timeout watchdog setting

docs: integration testing S1 MCP threshold, tutorials index,
glossary, outbox screenshot, CAPTURES checklist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: CI lint — unused import in test, bandit nosec for loopback blocklist

- Remove unused ActionEvent import in test_has_running_mcp_tool_returns_server_name
- Add # nosec B104 to _LOOPBACK_HOSTS — it's a URL blocklist, not a bind address

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update CLAUDE.md test counts for v0.35.0rc8

Total: 1578 → 1743 tests
Per-file: test_exec_bridge 109→112, test_claude_control 82→89,
test_callback_dispatch 25→26, test_ask_user_question 25→29,
test_meta_line 43→54, test_preamble 5→6, test_config_command
195→218, test_build_args 33→39

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Nathan Schram (nathanschram) added a commit that referenced this pull request Mar 31, 2026
* feat: three-mode support, startup mode indicator, dev branch workflow (#158, #159)

* ci: add CODEOWNERS, update action SHA pins, add permission comments

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add release guard hooks and document protection in CLAUDE.md

Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify /config default labels and remove redundant "Works with" lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress redundant cost footer on error runs

When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: suppress stall notifications when CPU-active + heartbeat re-render

When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.34.5rc2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: CI release-validation tomllib bytes/str mismatch

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: integrate screenshots into docs with correct JPG references

- Add 44 screenshots to docs/assets/screenshots/
- Fix all image refs from .png to .jpg across 25 doc files
- README uses absolute raw.githubusercontent.com URLs for PyPI rendering
- Fix 5 filename mismatches (session-auto-resume→chat-auto-resume, etc.)
- Comment out 11 missing screenshots with TODO markers
- Add CAPTURES.md checklist tracking capture status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: convert markdown images to HTML img tags for GitHub compatibility

Switch from MkDocs `![alt](src){ loading=lazy }` syntax to HTML `<img>`
tags with width="360" and loading="lazy". Fixes two GitHub rendering
issues: `{ loading=lazy }` appearing as visible text, and oversized
images with no width constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix 3 screenshot mismatches and replace 3 screenshots

- first-run.md: rewrite resume line text to match footer screenshot
- interactive-control.md: update planmode show admonition to match screenshot (auto not on)
- switch-engines.md: swap engine-footer.jpg for multi-engine-switch.jpg
- Replace startup-message.jpg with clean v0.34.4 capture (was rc/6-projects)
- Replace cooldown-auto-deny.jpg with post-outline approve/deny buttons
- Replace file-put.jpg with photo save confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add iOS caption limitation note to file transfer guide

Telegram iOS doesn't show a caption field when sending documents via the
File picker, so /file put <path> captions aren't easily accessible.
Added a note with workarounds (use Desktop, send as photo, or let
auto-save handle it). Updated screenshot alt text to match actual
screenshot content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: temp swap README image URLs to feature branch for preview

Will revert to master before merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: lay out all 3 README screenshots in a single row

Reduce from 360px to 270px each and combine into one <p> block
so all three hero screenshots sit side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap 3rd hero screenshot for config-menu for visual variety

Replace plan-outline-approve (too similar to approval-diff-preview)
with config-menu showing the /config settings grid. The three hero
images now tell: voice input → approve changes → configure everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add captions under README hero screenshots

Small <sub> captions: "Send tasks by voice (Whisper transcription)",
"Approve changes remotely", "Configure from Telegram".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: use table layout for README hero screenshots with captions

Fixes stacking issue — <br> in a <p> broke inline flow. A table
keeps images side by side with captions underneath each one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace table layout with single hero collage image

Composite image scales proportionally on mobile instead of requiring
horizontal scroll. Captions baked into the image via ImageMagick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap middle hero screenshot for full 3-button approval view

Replace approval-diff-preview with approval-buttons-howto showing
Approve / Deny / Pause & Outline Plan — more visually impressive.
Caption now reads "Approve changes remotely (Claude Code)".
Added footnote linking to engine compatibility table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap config-menu for parallel-projects in hero collage

Third hero screenshot now shows 10+ projects running simultaneously
across different repos — much more compelling than a settings menu.
New caption: "Run agents across projects in parallel".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: revert README image URL to master for merge

Swap hero-collage URL back from feature/github-hardening to master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc3

- fix: preserve all EngineOverrides fields when setting model/planmode/reasoning
  (was silently wiping ask_questions, diff_preview, show_api_cost, etc.)
- fix: /config home page resolves "default" to effective values
- feat: file upload auto-deduplication (append _1, _2 instead of requiring --force)
- feat: media groups without captions now auto-save instead of showing usage text
- feat: resume line visual separation (blank line + ↩️ prefix)
- fix: claude auto-approve echoes updatedInput in control response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: expand permission policies for Codex CLI and Gemini CLI in /config

Codex gets a new "Approval policy" page (full auto / safe) that passes
--ask-for-approval untrusted when safe mode is selected. Gemini's approval
mode expands from 2 to 3 tiers (read-only / edit files / full access) with
--approval-mode auto_edit for the middle tier. Both engines now show an
"Agent controls" section on the /config home page. Engine-specific model
default hints replace the generic "from CLI settings" text.

Also adds staging.sh helper, context-guard-stop hook, and docs updates.

Closes #131

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /config UX overhaul, resume line toggle, cost budget settings, model metadata

/config UX cleanup:
- Convert all binary toggles from 3-column (on/off/clear) to 2-column
  (toggle + clear) for better mobile tap targets
- Merge Engine + Model into combined "Engine & model" page
- Reorganise home page to max 2 buttons per row across all engines
- Split plan mode 3-option rows (off/on/auto) into 2+1 layout
- Add _toggle_row() helper for consistent toggle button rendering

New features:
- #128: Resume line /config toggle — per-chat show_resume_line override
  via EngineOverrides with On/Off/Clear buttons, wired into executor
- #129: Cost budget /config settings — per-chat budget_enabled and
  budget_auto_cancel overrides on the Cost & Usage page, wired into
  _check_cost_budget() in runner_bridge.py

Model metadata improvements:
- Show Claude Code [1m] context window suffix: "opus 4.6 (1M)"
- Strip Gemini CLI "auto-" prefix: "auto-gemini-3" → "gemini-3"
- Future-proof: unknown suffixes default to .upper() (e.g. [500k] → 500K)

Bug fixes:
- #124: Standalone override commands (/planmode, /model, /reasoning) now
  preserve all EngineOverrides fields including new ones
- Error handling: control_response.write_failed catch-all in claude.py,
  ask_question.extraction_failed warning, model.override.failed logging

Hardening:
- Plan outline sent as separate ephemeral message (avoids 4096 char truncation)
- Added show_resume_line, budget_enabled, budget_auto_cancel to
  EngineOverrides, EngineRunOptions, normalize/merge, and all constructors

Tests: 1610 passed, 80.56% coverage, ruff clean.
Integration tested on @untether_dev_bot across all 6 engine chats.

Closes #128, closes #129, fixes #124

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI for PR #132

* fix: address 11 CodeRabbit review comments on PR #132

Bug fixes:
- claude.py: fix UnboundLocalError when factory.resume is falsy in
  ask_question.extraction_failed logging path
- ask_question.py: reject malformed option callbacks instead of
  silently falling back to option 0
- files.py: raise FileExistsError when deduplicate_target exhausts
  999 suffixes instead of returning the original (overwrite risk)
- config.py: disambiguate Codex "Full auto" (fa) vs Gemini "Full
  access" (ya) callback IDs and toast labels

Hardening:
- codex.py: add --ask-for-approval to _EXEC_ONLY_FLAGS guard
- model.py: add try/except to clear path (matching set path)
- reasoning.py: add try/except to clear path (matching set path)
- loop.py: notify user when media group upload fails instead of
  silently dropping
- export.py: log session count instead of identifiers at info level
- config.py: resolve resume-line default from config instead of
  hardcoding True
- staging.sh: pin PyPI index in rollback/reset with --pip-args

Skipped (not applicable):
- CHANGELOG.md: RC versions don't get changelog entries per release
  discipline
- docs/tutorials TODO screenshot: pre-existing, not introduced by PR
- .claude/hooks/context-guard-stop.sh: ContextDocs plugin hook, not
  Untether source

Tests: 1611 passed, 80.48% coverage, ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace bare pass with debug log to satisfy bandit B110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: setup wizard security + UX improvements

- Auto-set allowed_user_ids from captured Telegram user ID during
  onboarding (security: restricts bot to the setup user's account)
- Add "next steps" panel after wizard completion with pointers to
  /config, voice notes, projects, and account lock confirmation
- Update install.md: Python 3.12+ (not just 3.14), dynamic version
  string, /config mention for post-setup changes
- Update first-run.md: /config → Engine & model for default engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: plan outline UX — markdown rendering, buttons, cleanup (#139, #140, #141)

- Render outline messages as formatted text via render_markdown() +
  split_markdown_body() instead of raw markdown (#139)
- Add approve/deny buttons to last outline message so users don't
  have to scroll up past long outlines (#140)
- Delete outline messages on approve/deny via module-level
  _OUTLINE_REGISTRY callable from callback handler; suppress stale
  keyboard on progress message (#141)
- 8 new tests for outline rendering, keyboard placement, and cleanup
- Bump version to 0.35.0rc5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /continue command — cross-environment resume for all engines (#135)

New `/continue` command resumes the most recent CLI session in the
project directory from Telegram. Enables starting a session in your
terminal and picking it up from your phone.

Engine support: Claude (--continue), Codex (resume --last), OpenCode
(--continue), Pi (--continue), Gemini (--resume latest). AMP not
supported (requires explicit thread ID).

Includes ResumeToken.is_continue flag, build_args for all 6 runners,
reserved command registration, resume emoji prefix stripping for
reply-to-continue, docs (how-to guide, README, commands ref, routing
explanation, conversation modes tutorial), and 99 new test assertions.

Integration tested against @untether_dev_bot — all 5 supported engines
passed secret-recall verification via Telegram MCP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: agent outbox file delivery + fix cross-chat ask stealing (#143, #144)

Outbox delivery (#143): agents write files to .untether-outbox/ during a
run; Untether sends them as Telegram documents on completion with 📎
captions. Config: outbox_enabled, outbox_dir, outbox_max_files,
outbox_cleanup. Deny-glob security, size limits, auto-cleanup.
Preamble updated for all 6 engines. Integration tested across
Claude, Codex, OpenCode, Pi, and Gemini.

AskUserQuestion fix (#144): _PENDING_ASK_REQUESTS and
_ASK_QUESTION_FLOWS were global dicts with no chat_id scoping — a
pending ask in one chat would steal the next message from any other
chat. Added channel_id contextvar and scoped all ask lookups by it.
Session cleanup now also clears stale pending asks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: v0.35.0 changelog completion + fix #123 updatedInput

- Complete v0.35.0 changelog: add missing entries for /continue (#135),
  /config UX overhaul (#132), resume line toggle (#128), cost budget
  (#129), model metadata, resume line formatting (#127), override
  preservation (#124), and updatedInput fix (#123)
- Fix #123: register input for system-level auto-approved control
  requests so updatedInput is included in the response
- Add parameterised test for all 5 auto-approve types input registration
- Remove unused OutboxResult import (ruff fix)

Issues closed: #115, #118, #123, #124, #126, #127, #134

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.35.0rc6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rc6 integration test fixes (#145, #146, #147, #148, #149)

- Reduce Telegram API timeout from 120s to 30s (#145)
- OpenCode error runs show error text instead of empty body (#146)
- Pi /continue captures session ID via allow_id_promotion (#147)
- Post-outline approval uses skip_reply to avoid "not found" (#148)
- Orphan progress message cleanup on restart (#149)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: post-outline notification reply + OpenCode empty body (#148, #150)

- #148: skip_reply callback results now bypass the executor's default
  reply_to fallback, sending directly via the transport with no
  reply_to_message_id. Previously, the executor treated reply_to=None
  as "use default" which pointed to the (deleted) outline message.

- #150: OpenCode normal completion with no Text events now falls back
  to last_tool_error. Added state.last_tool_error field populated on
  ToolUse error status. Covers both translate() and stream_end_events().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: suppress post-outline notification to avoid "message not found" (#148)

After outline approval/denial, the progress loop's _send_notify was
firing for the next tool approval, but the notification's reply_to
anchor could reference deleted state. Added _outline_just_resolved
flag to skip one notification cycle after outline cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: note OpenCode lacks auto-compaction — long sessions degrade (#150)

Added known limitation to OpenCode runner docs and integration testing
playbook. OpenCode sessions accumulate unbounded context (no compaction
events unlike Pi). Workaround: use /new before isolated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip_reply on regular approve path when outline was deleted (#148)

The "Approve Plan" button on outline messages uses the real
ExitPlanMode request_id, routing through the regular approve path
(not the da: synthetic path). When outline messages exist, set
skip_reply=True on the CommandResult to avoid replying to the
just-deleted outline message.

Also added reply_to_message_id and text_preview to transport.send.failed
warning for easier debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc6 integration test fixes (#145-#150)

Updated fix descriptions for #146/#150 (OpenCode last_tool_error
fallback) and #148 (regular approve path skip_reply). Added docs
section for OpenCode compaction limitation. Updated test counts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix formatting after merge resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review comments on PR #151

- bridge.py: replace text_preview with text_len in send failure warning
  to avoid logging raw message content (security)
- runner_bridge.py: move unregister_progress() after send_result_message()
  to avoid orphan window between ephemeral cleanup and final message send
- cross-environment-resume.md: add language spec to code block

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve /config "default" labels to effective on/off values (#152)

Sub-pages showed "Current: default" or "default (on/off)" while buttons
already showed the resolved value. Now all boolean-toggle settings show
the effective on/off value in both text and buttons.

Affected: verbose, ask mode, diff preview, API cost, subscription usage,
budget enabled/auto-cancel, resume line. Home page cost & resume labels
also resolved.

Plan mode, model, and reasoning keep "default" since they depend on CLI
settings and aren't simple on/off booleans.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc7 config default labels fix (#152)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update documentation for v0.35.0

- fix missing nav entries in zensical.toml (cross-env resume, Gemini/Amp runners)
- rewrite inline-settings.md for /config UX overhaul (2-column toggles, budget/resume toggles)
- update plan-mode.md with outline rendering, buttons-on-last-chunk, ephemeral cleanup
- update interactive-control tutorial with outline UX improvements
- add orphan progress cleanup section to operations.md
- add engine-specific approval policies to interactive-approval.md
- add per-chat budget overrides to cost-budgets.md
- update module-map.md with Gemini/Amp and new modules (outbox, progress persistence, proc_diag)
- update architecture.md mermaid diagrams with all 6 engines
- bump specification.md to v0.35.0, add progress persistence and outbox sections
- add v0.35.0 screenshot entries to CAPTURES.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: broaden frozen ring buffer stall escalation beyond MCP tools (#155)

Frozen ring buffer escalation was gated on `mcp_server is not None`,
so general stalls with cpu_active=True and no MCP tool running were
silently suppressed indefinitely. Broadened to fire for all stalls
after 3+ checks with no new JSONL events regardless of tool type.

New notification: "CPU active, no new events" for non-MCP frozen stalls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: tool approval buttons no longer suppressed after outline approval (#156)

After "Approve Plan" on an outline, the stale discuss_approve action
remained in ProgressTracker with completed=False. The renderer picked
up its stale "Approve Plan"/"Deny" buttons first, then the suppression
logic at line 994 stripped ALL buttons — including new Write/Edit/Bash
approval buttons. Claude blocked indefinitely waiting for approval.

Fix: after suppressing stale buttons, complete the discuss_approve
action(s) in the tracker, reset _outline_sent, and trigger a re-render
so subsequent tool requests get their own Approve/Deny buttons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add workflow mode indicator to startup message, fix startup crash on topics validation failure (#158, #159)

Features:
- Startup message now shows mode: assistant/workspace/handoff
- Derived from session_mode + topics.enabled config values
- _resolve_mode_label() helper in backend.py

Bug fixes:
- Fix UnboundLocalError crash when topics validation fails on startup (#158)
  - Moved import signal and shutdown imports before try block in loop.py
- Downgrade can_manage_topics check from fatal error to warning (#159)
  - Bot can now start without manage_topics admin right
  - Existing topics work fine; only topic creation/editing affected

Tests:
- 17 new unit tests for stateless/handoff mode (test_stateless_mode.py)
  - _should_show_resume_line, _chat_session_key, ResumeResolver, ResumeLineProxy
  - Integration-level: stateless shows resume lines, no auto-resume, chat hides lines
- 3 new tests for mode indicator in startup message (test_telegram_backend.py)

Docs:
- New docs/reference/modes.md — comprehensive reference for all 3 workflow modes
- Updated docs/reference/index.md and zensical.toml nav with modes page

* docs: comprehensive three-mode coverage across all documentation

New:
- docs/how-to/choose-a-mode.md — decision tree, mode comparison, mermaid
  sequence diagrams, configuration examples, switching guide, workspace
  prerequisites

Updated:
- README.md — improved three-mode description in features list
- docs/tutorials/install.md — added mode selection step (section 10)
- docs/tutorials/first-run.md — added 'What mode am I in?' tip
- docs/reference/config.md — cross-linked session_mode/show_resume_line to modes.md
- docs/reference/transports/telegram.md — added mode requirement callouts
  for forum topics and chat sessions sections
- docs/how-to/chat-sessions.md — added session persistence explanation
  (state files, auto-resume mechanics, handoff note)
- docs/how-to/topics.md — expanded prerequisites checklist with group
  privacy, can_manage_topics, and re-add steps
- docs/how-to/cross-environment-resume.md — added handoff mode terminal
  workflow with mermaid sequence diagram
- docs/how-to/index.md — added 'Getting started' section with choose-a-mode
- zensical.toml — added choose-a-mode to nav

* docs: add three-mode summary table to README Quick Start section

* feat: migrate to dev branch workflow — dev→TestPyPI, master→PyPI

Branch model:
- feature/* → PR → dev (TestPyPI auto-publish) → PR → master (PyPI)
- master always matches latest PyPI release
- dev is the integration/staging branch

CI changes:
- ci.yml: TestPyPI publish triggers on dev push (was master)
- ci.yml, codeql.yml: CI runs on both master and dev pushes
- dependabot.yml: PRs target dev branch

Hook changes:
- release-guard.sh: updated messages to mention dev branch
- release-guard-mcp.sh: updated messages to mention dev branch
- Both hooks already allow dev pushes (only block master/main)

Documentation:
- CLAUDE.md: updated 3-phase workflow, CI table, release guard docs
- dev-workflow.md: added branch model section
- release-discipline.md: added dev branch staging notes

* ci: retrigger CI for PR #160

* feat: allow Claude Code to merge PRs targeting dev branch only

Release guard hooks now check the PR's base branch:
- dev → allowed (TestPyPI/staging)
- master/main → blocked (PyPI releases remain Nathan-only)

Both Bash hook (gh pr merge) and MCP hook (merge_pull_request)
updated with base branch checking via gh pr view.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.35.0rc7

* docs: add workflow mode indicator to CLAUDE.md (#162)

* ci: add CODEOWNERS, update action SHA pins, add permission comments

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add release guard hooks and document protection in CLAUDE.md

Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify /config default labels and remove redundant "Works with" lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress redundant cost footer on error runs

When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: suppress stall notifications when CPU-active + heartbeat re-render

When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.34.5rc2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: CI release-validation tomllib bytes/str mismatch

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: integrate screenshots into docs with correct JPG references

- Add 44 screenshots to docs/assets/screenshots/
- Fix all image refs from .png to .jpg across 25 doc files
- README uses absolute raw.githubusercontent.com URLs for PyPI rendering
- Fix 5 filename mismatches (session-auto-resume→chat-auto-resume, etc.)
- Comment out 11 missing screenshots with TODO markers
- Add CAPTURES.md checklist tracking capture status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: convert markdown images to HTML img tags for GitHub compatibility

Switch from MkDocs `![alt](src){ loading=lazy }` syntax to HTML `<img>`
tags with width="360" and loading="lazy". Fixes two GitHub rendering
issues: `{ loading=lazy }` appearing as visible text, and oversized
images with no width constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix 3 screenshot mismatches and replace 3 screenshots

- first-run.md: rewrite resume line text to match footer screenshot
- interactive-control.md: update planmode show admonition to match screenshot (auto not on)
- switch-engines.md: swap engine-footer.jpg for multi-engine-switch.jpg
- Replace startup-message.jpg with clean v0.34.4 capture (was rc/6-projects)
- Replace cooldown-auto-deny.jpg with post-outline approve/deny buttons
- Replace file-put.jpg with photo save confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add iOS caption limitation note to file transfer guide

Telegram iOS doesn't show a caption field when sending documents via the
File picker, so /file put <path> captions aren't easily accessible.
Added a note with workarounds (use Desktop, send as photo, or let
auto-save handle it). Updated screenshot alt text to match actual
screenshot content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: temp swap README image URLs to feature branch for preview

Will revert to master before merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: lay out all 3 README screenshots in a single row

Reduce from 360px to 270px each and combine into one <p> block
so all three hero screenshots sit side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap 3rd hero screenshot for config-menu for visual variety

Replace plan-outline-approve (too similar to approval-diff-preview)
with config-menu showing the /config settings grid. The three hero
images now tell: voice input → approve changes → configure everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add captions under README hero screenshots

Small <sub> captions: "Send tasks by voice (Whisper transcription)",
"Approve changes remotely", "Configure from Telegram".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: use table layout for README hero screenshots with captions

Fixes stacking issue — <br> in a <p> broke inline flow. A table
keeps images side by side with captions underneath each one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace table layout with single hero collage image

Composite image scales proportionally on mobile instead of requiring
horizontal scroll. Captions baked into the image via ImageMagick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap middle hero screenshot for full 3-button approval view

Replace approval-diff-preview with approval-buttons-howto showing
Approve / Deny / Pause & Outline Plan — more visually impressive.
Caption now reads "Approve changes remotely (Claude Code)".
Added footnote linking to engine compatibility table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap config-menu for parallel-projects in hero collage

Third hero screenshot now shows 10+ projects running simultaneously
across different repos — much more compelling than a settings menu.
New caption: "Run agents across projects in parallel".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: revert README image URL to master for merge

Swap hero-collage URL back from feature/github-hardening to master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc3

- fix: preserve all EngineOverrides fields when setting model/planmode/reasoning
  (was silently wiping ask_questions, diff_preview, show_api_cost, etc.)
- fix: /config home page resolves "default" to effective values
- feat: file upload auto-deduplication (append _1, _2 instead of requiring --force)
- feat: media groups without captions now auto-save instead of showing usage text
- feat: resume line visual separation (blank line + ↩️ prefix)
- fix: claude auto-approve echoes updatedInput in control response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: expand permission policies for Codex CLI and Gemini CLI in /config

Codex gets a new "Approval policy" page (full auto / safe) that passes
--ask-for-approval untrusted when safe mode is selected. Gemini's approval
mode expands from 2 to 3 tiers (read-only / edit files / full access) with
--approval-mode auto_edit for the middle tier. Both engines now show an
"Agent controls" section on the /config home page. Engine-specific model
default hints replace the generic "from CLI settings" text.

Also adds staging.sh helper, context-guard-stop hook, and docs updates.

Closes #131

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /config UX overhaul, resume line toggle, cost budget settings, model metadata

/config UX cleanup:
- Convert all binary toggles from 3-column (on/off/clear) to 2-column
  (toggle + clear) for better mobile tap targets
- Merge Engine + Model into combined "Engine & model" page
- Reorganise home page to max 2 buttons per row across all engines
- Split plan mode 3-option rows (off/on/auto) into 2+1 layout
- Add _toggle_row() helper for consistent toggle button rendering

New features:
- #128: Resume line /config toggle — per-chat show_resume_line override
  via EngineOverrides with On/Off/Clear buttons, wired into executor
- #129: Cost budget /config settings — per-chat budget_enabled and
  budget_auto_cancel overrides on the Cost & Usage page, wired into
  _check_cost_budget() in runner_bridge.py

Model metadata improvements:
- Show Claude Code [1m] context window suffix: "opus 4.6 (1M)"
- Strip Gemini CLI "auto-" prefix: "auto-gemini-3" → "gemini-3"
- Future-proof: unknown suffixes default to .upper() (e.g. [500k] → 500K)

Bug fixes:
- #124: Standalone override commands (/planmode, /model, /reasoning) now
  preserve all EngineOverrides fields including new ones
- Error handling: control_response.write_failed catch-all in claude.py,
  ask_question.extraction_failed warning, model.override.failed logging

Hardening:
- Plan outline sent as separate ephemeral message (avoids 4096 char truncation)
- Added show_resume_line, budget_enabled, budget_auto_cancel to
  EngineOverrides, EngineRunOptions, normalize/merge, and all constructors

Tests: 1610 passed, 80.56% coverage, ruff clean.
Integration tested on @untether_dev_bot across all 6 engine chats.

Closes #128, closes #129, fixes #124

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI for PR #132

* fix: address 11 CodeRabbit review comments on PR #132

Bug fixes:
- claude.py: fix UnboundLocalError when factory.resume is falsy in
  ask_question.extraction_failed logging path
- ask_question.py: reject malformed option callbacks instead of
  silently falling back to option 0
- files.py: raise FileExistsError when deduplicate_target exhausts
  999 suffixes instead of returning the original (overwrite risk)
- config.py: disambiguate Codex "Full auto" (fa) vs Gemini "Full
  access" (ya) callback IDs and toast labels

Hardening:
- codex.py: add --ask-for-approval to _EXEC_ONLY_FLAGS guard
- model.py: add try/except to clear path (matching set path)
- reasoning.py: add try/except to clear path (matching set path)
- loop.py: notify user when media group upload fails instead of
  silently dropping
- export.py: log session count instead of identifiers at info level
- config.py: resolve resume-line default from config instead of
  hardcoding True
- staging.sh: pin PyPI index in rollback/reset with --pip-args

Skipped (not applicable):
- CHANGELOG.md: RC versions don't get changelog entries per release
  discipline
- docs/tutorials TODO screenshot: pre-existing, not introduced by PR
- .claude/hooks/context-guard-stop.sh: ContextDocs plugin hook, not
  Untether source

Tests: 1611 passed, 80.48% coverage, ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace bare pass with debug log to satisfy bandit B110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: setup wizard security + UX improvements

- Auto-set allowed_user_ids from captured Telegram user ID during
  onboarding (security: restricts bot to the setup user's account)
- Add "next steps" panel after wizard completion with pointers to
  /config, voice notes, projects, and account lock confirmation
- Update install.md: Python 3.12+ (not just 3.14), dynamic version
  string, /config mention for post-setup changes
- Update first-run.md: /config → Engine & model for default engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: plan outline UX — markdown rendering, buttons, cleanup (#139, #140, #141)

- Render outline messages as formatted text via render_markdown() +
  split_markdown_body() instead of raw markdown (#139)
- Add approve/deny buttons to last outline message so users don't
  have to scroll up past long outlines (#140)
- Delete outline messages on approve/deny via module-level
  _OUTLINE_REGISTRY callable from callback handler; suppress stale
  keyboard on progress message (#141)
- 8 new tests for outline rendering, keyboard placement, and cleanup
- Bump version to 0.35.0rc5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /continue command — cross-environment resume for all engines (#135)

New `/continue` command resumes the most recent CLI session in the
project directory from Telegram. Enables starting a session in your
terminal and picking it up from your phone.

Engine support: Claude (--continue), Codex (resume --last), OpenCode
(--continue), Pi (--continue), Gemini (--resume latest). AMP not
supported (requires explicit thread ID).

Includes ResumeToken.is_continue flag, build_args for all 6 runners,
reserved command registration, resume emoji prefix stripping for
reply-to-continue, docs (how-to guide, README, commands ref, routing
explanation, conversation modes tutorial), and 99 new test assertions.

Integration tested against @untether_dev_bot — all 5 supported engines
passed secret-recall verification via Telegram MCP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: agent outbox file delivery + fix cross-chat ask stealing (#143, #144)

Outbox delivery (#143): agents write files to .untether-outbox/ during a
run; Untether sends them as Telegram documents on completion with 📎
captions. Config: outbox_enabled, outbox_dir, outbox_max_files,
outbox_cleanup. Deny-glob security, size limits, auto-cleanup.
Preamble updated for all 6 engines. Integration tested across
Claude, Codex, OpenCode, Pi, and Gemini.

AskUserQuestion fix (#144): _PENDING_ASK_REQUESTS and
_ASK_QUESTION_FLOWS were global dicts with no chat_id scoping — a
pending ask in one chat would steal the next message from any other
chat. Added channel_id contextvar and scoped all ask lookups by it.
Session cleanup now also clears stale pending asks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: v0.35.0 changelog completion + fix #123 updatedInput

- Complete v0.35.0 changelog: add missing entries for /continue (#135),
  /config UX overhaul (#132), resume line toggle (#128), cost budget
  (#129), model metadata, resume line formatting (#127), override
  preservation (#124), and updatedInput fix (#123)
- Fix #123: register input for system-level auto-approved control
  requests so updatedInput is included in the response
- Add parameterised test for all 5 auto-approve types input registration
- Remove unused OutboxResult import (ruff fix)

Issues closed: #115, #118, #123, #124, #126, #127, #134

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.35.0rc6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rc6 integration test fixes (#145, #146, #147, #148, #149)

- Reduce Telegram API timeout from 120s to 30s (#145)
- OpenCode error runs show error text instead of empty body (#146)
- Pi /continue captures session ID via allow_id_promotion (#147)
- Post-outline approval uses skip_reply to avoid "not found" (#148)
- Orphan progress message cleanup on restart (#149)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: post-outline notification reply + OpenCode empty body (#148, #150)

- #148: skip_reply callback results now bypass the executor's default
  reply_to fallback, sending directly via the transport with no
  reply_to_message_id. Previously, the executor treated reply_to=None
  as "use default" which pointed to the (deleted) outline message.

- #150: OpenCode normal completion with no Text events now falls back
  to last_tool_error. Added state.last_tool_error field populated on
  ToolUse error status. Covers both translate() and stream_end_events().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: suppress post-outline notification to avoid "message not found" (#148)

After outline approval/denial, the progress loop's _send_notify was
firing for the next tool approval, but the notification's reply_to
anchor could reference deleted state. Added _outline_just_resolved
flag to skip one notification cycle after outline cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: note OpenCode lacks auto-compaction — long sessions degrade (#150)

Added known limitation to OpenCode runner docs and integration testing
playbook. OpenCode sessions accumulate unbounded context (no compaction
events unlike Pi). Workaround: use /new before isolated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip_reply on regular approve path when outline was deleted (#148)

The "Approve Plan" button on outline messages uses the real
ExitPlanMode request_id, routing through the regular approve path
(not the da: synthetic path). When outline messages exist, set
skip_reply=True on the CommandResult to avoid replying to the
just-deleted outline message.

Also added reply_to_message_id and text_preview to transport.send.failed
warning for easier debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc6 integration test fixes (#145-#150)

Updated fix descriptions for #146/#150 (OpenCode last_tool_error
fallback) and #148 (regular approve path skip_reply). Added docs
section for OpenCode compaction limitation. Updated test counts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix formatting after merge resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review comments on PR #151

- bridge.py: replace text_preview with text_len in send failure warning
  to avoid logging raw message content (security)
- runner_bridge.py: move unregister_progress() after send_result_message()
  to avoid orphan window between ephemeral cleanup and final message send
- cross-environment-resume.md: add language spec to code block

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve /config "default" labels to effective on/off values (#152)

Sub-pages showed "Current: default" or "default (on/off)" while buttons
already showed the resolved value. Now all boolean-toggle settings show
the effective on/off value in both text and buttons.

Affected: verbose, ask mode, diff preview, API cost, subscription usage,
budget enabled/auto-cancel, resume line. Home page cost & resume labels
also resolved.

Plan mode, model, and reasoning keep "default" since they depend on CLI
settings and aren't simple on/off booleans.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc7 config default labels fix (#152)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update documentation for v0.35.0

- fix missing nav entries in zensical.toml (cross-env resume, Gemini/Amp runners)
- rewrite inline-settings.md for /config UX overhaul (2-column toggles, budget/resume toggles)
- update plan-mode.md with outline rendering, buttons-on-last-chunk, ephemeral cleanup
- update interactive-control tutorial with outline UX improvements
- add orphan progress cleanup section to operations.md
- add engine-specific approval policies to interactive-approval.md
- add per-chat budget overrides to cost-budgets.md
- update module-map.md with Gemini/Amp and new modules (outbox, progress persistence, proc_diag)
- update architecture.md mermaid diagrams with all 6 engines
- bump specification.md to v0.35.0, add progress persistence and outbox sections
- add v0.35.0 screenshot entries to CAPTURES.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: broaden frozen ring buffer stall escalation beyond MCP tools (#155)

Frozen ring buffer escalation was gated on `mcp_server is not None`,
so general stalls with cpu_active=True and no MCP tool running were
silently suppressed indefinitely. Broadened to fire for all stalls
after 3+ checks with no new JSONL events regardless of tool type.

New notification: "CPU active, no new events" for non-MCP frozen stalls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: tool approval buttons no longer suppressed after outline approval (#156)

After "Approve Plan" on an outline, the stale discuss_approve action
remained in ProgressTracker with completed=False. The renderer picked
up its stale "Approve Plan"/"Deny" buttons first, then the suppression
logic at line 994 stripped ALL buttons — including new Write/Edit/Bash
approval buttons. Claude blocked indefinitely waiting for approval.

Fix: after suppressing stale buttons, complete the discuss_approve
action(s) in the tracker, reset _outline_sent, and trigger a re-render
so subsequent tool requests get their own Approve/Deny buttons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add workflow mode indicator to startup message, fix startup crash on topics validation failure (#158, #159)

Features:
- Startup message now shows mode: assistant/workspace/handoff
- Derived from session_mode + topics.enabled config values
- _resolve_mode_label() helper in backend.py

Bug fixes:
- Fix UnboundLocalError crash when topics validation fails on startup (#158)
  - Moved import signal and shutdown imports before try block in loop.py
- Downgrade can_manage_topics check from fatal error to warning (#159)
  - Bot can now start without manage_topics admin right
  - Existing topics work fine; only topic creation/editing affected

Tests:
- 17 new unit tests for stateless/handoff mode (test_stateless_mode.py)
  - _should_show_resume_line, _chat_session_key, ResumeResolver, ResumeLineProxy
  - Integration-level: stateless shows resume lines, no auto-resume, chat hides lines
- 3 new tests for mode indicator in startup message (test_telegram_backend.py)

Docs:
- New docs/reference/modes.md — comprehensive reference for all 3 workflow modes
- Updated docs/reference/index.md and zensical.toml nav with modes page

* docs: comprehensive three-mode coverage across all documentation

New:
- docs/how-to/choose-a-mode.md — decision tree, mode comparison, mermaid
  sequence diagrams, configuration examples, switching guide, workspace
  prerequisites

Updated:
- README.md — improved three-mode description in features list
- docs/tutorials/install.md — added mode selection step (section 10)
- docs/tutorials/first-run.md — added 'What mode am I in?' tip
- docs/reference/config.md — cross-linked session_mode/show_resume_line to modes.md
- docs/reference/transports/telegram.md — added mode requirement callouts
  for forum topics and chat sessions sections
- docs/how-to/chat-sessions.md — added session persistence explanation
  (state files, auto-resume mechanics, handoff note)
- docs/how-to/topics.md — expanded prerequisites checklist with group
  privacy, can_manage_topics, and re-add steps
- docs/how-to/cross-environment-resume.md — added handoff mode terminal
  workflow with mermaid sequence diagram
- docs/how-to/index.md — added 'Getting started' section with choose-a-mode
- zensical.toml — added choose-a-mode to nav

* docs: add three-mode summary table to README Quick Start section

* feat: migrate to dev branch workflow — dev→TestPyPI, master→PyPI

Branch model:
- feature/* → PR → dev (TestPyPI auto-publish) → PR → master (PyPI)
- master always matches latest PyPI release
- dev is the integration/staging branch

CI changes:
- ci.yml: TestPyPI publish triggers on dev push (was master)
- ci.yml, codeql.yml: CI runs on both master and dev pushes
- dependabot.yml: PRs target dev branch

Hook changes:
- release-guard.sh: updated messages to mention dev branch
- release-guard-mcp.sh: updated messages to mention dev branch
- Both hooks already allow dev pushes (only block master/main)

Documentation:
- CLAUDE.md: updated 3-phase workflow, CI table, release guard docs
- dev-workflow.md: added branch model section
- release-discipline.md: added dev branch staging notes

* ci: retrigger CI for PR #160

* feat: allow Claude Code to merge PRs targeting dev branch only

Release guard hooks now check the PR's base branch:
- dev → allowed (TestPyPI/staging)
- master/main → blocked (PyPI releases remain Nathan-only)

Both Bash hook (gh pr merge) and MCP hook (merge_pull_request)
updated with base branch checking via gh pr view.

* docs: add workflow mode indicator and modes.md to CLAUDE.md

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: restore frozen ring buffer stall escalation (#155), staging 0.35.0rc8

* ci: add CODEOWNERS, update action SHA pins, add permission comments

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add release guard hooks and document protection in CLAUDE.md

Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify /config default labels and remove redundant "Works with" lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress redundant cost footer on error runs

When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: suppress stall notifications when CPU-active + heartbeat re-render

When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.34.5rc2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: CI release-validation tomllib bytes/str mismatch

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: integrate screenshots into docs with correct JPG references

- Add 44 screenshots to docs/assets/screenshots/
- Fix all image refs from .png to .jpg across 25 doc files
- README uses absolute raw.githubusercontent.com URLs for PyPI rendering
- Fix 5 filename mismatches (session-auto-resume→chat-auto-resume, etc.)
- Comment out 11 missing screenshots with TODO markers
- Add CAPTURES.md checklist tracking capture status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: convert markdown images to HTML img tags for GitHub compatibility

Switch from MkDocs `![alt](src){ loading=lazy }` syntax to HTML `<img>`
tags with width="360" and loading="lazy". Fixes two GitHub rendering
issues: `{ loading=lazy }` appearing as visible text, and oversized
images with no width constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix 3 screenshot mismatches and replace 3 screenshots

- first-run.md: rewrite resume line text to match footer screenshot
- interactive-control.md: update planmode show admonition to match screenshot (auto not on)
- switch-engines.md: swap engine-footer.jpg for multi-engine-switch.jpg
- Replace startup-message.jpg with clean v0.34.4 capture (was rc/6-projects)
- Replace cooldown-auto-deny.jpg with post-outline approve/deny buttons
- Replace file-put.jpg with photo save confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add iOS caption limitation note to file transfer guide

Telegram iOS doesn't show a caption field when sending documents via the
File picker, so /file put <path> captions aren't easily accessible.
Added a note with workarounds (use Desktop, send as photo, or let
auto-save handle it). Updated screenshot alt text to match actual
screenshot content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: temp swap README image URLs to feature branch for preview

Will revert to master before merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: lay out all 3 README screenshots in a single row

Reduce from 360px to 270px each and combine into one <p> block
so all three hero screenshots sit side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap 3rd hero screenshot for config-menu for visual variety

Replace plan-outline-approve (too similar to approval-diff-preview)
with config-menu showing the /config settings grid. The three hero
images now tell: voice input → approve changes → configure everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add captions under README hero screenshots

Small <sub> captions: "Send tasks by voice (Whisper transcription)",
"Approve changes remotely", "Configure from Telegram".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: use table layout for README hero screenshots with captions

Fixes stacking issue — <br> in a <p> broke inline flow. A table
keeps images side by side with captions underneath each one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace table layout with single hero collage image

Composite image scales proportionally on mobile instead of requiring
horizontal scroll. Captions baked into the image via ImageMagick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap middle hero screenshot for full 3-button approval view

Replace approval-diff-preview with approval-buttons-howto showing
Approve / Deny / Pause & Outline Plan — more visually impressive.
Caption now reads "Approve changes remotely (Claude Code)".
Added footnote linking to engine compatibility table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap config-menu for parallel-projects in hero collage

Third hero screenshot now shows 10+ projects running simultaneously
across different repos — much more compelling than a settings menu.
New caption: "Run agents across projects in parallel".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: revert README image URL to master for merge

Swap hero-collage URL back from feature/github-hardening to master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc3

- fix: preserve all EngineOverrides fields when setting model/planmode/reasoning
  (was silently wiping ask_questions, diff_preview, show_api_cost, etc.)
- fix: /config home page resolves "default" to effective values
- feat: file upload auto-deduplication (append _1, _2 instead of requiring --force)
- feat: media groups without captions now auto-save instead of showing usage text
- feat: resume line visual separation (blank line + ↩️ prefix)
- fix: claude auto-approve echoes updatedInput in control response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: expand permission policies for Codex CLI and Gemini CLI in /config

Codex gets a new "Approval policy" page (full auto / safe) that passes
--ask-for-approval untrusted when safe mode is selected. Gemini's approval
mode expands from 2 to 3 tiers (read-only / edit files / full access) with
--approval-mode auto_edit for the middle tier. Both engines now show an
"Agent controls" section on the /config home page. Engine-specific model
default hints replace the generic "from CLI settings" text.

Also adds staging.sh helper, context-guard-stop hook, and docs updates.

Closes #131

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /config UX overhaul, resume line toggle, cost budget settings, model metadata

/config UX cleanup:
- Convert all binary toggles from 3-column (on/off/clear) to 2-column
  (toggle + clear) for better mobile tap targets
- Merge Engine + Model into combined "Engine & model" page
- Reorganise home page to max 2 buttons per row across all engines
- Split plan mode 3-option rows (off/on/auto) into 2+1 layout
- Add _toggle_row() helper for consistent toggle button rendering

New features:
- #128: Resume line /config toggle — per-chat show_resume_line override
  via EngineOverrides with On/Off/Clear buttons, wired into executor
- #129: Cost budget /config settings — per-chat budget_enabled and
  budget_auto_cancel overrides on the Cost & Usage page, wired into
  _check_cost_budget() in runner_bridge.py

Model metadata improvements:
- Show Claude Code [1m] context window suffix: "opus 4.6 (1M)"
- Strip Gemini CLI "auto-" prefix: "auto-gemini-3" → "gemini-3"
- Future-proof: unknown suffixes default to .upper() (e.g. [500k] → 500K)

Bug fixes:
- #124: Standalone override commands (/planmode, /model, /reasoning) now
  preserve all EngineOverrides fields including new ones
- Error handling: control_response.write_failed catch-all in claude.py,
  ask_question.extraction_failed warning, model.override.failed logging

Hardening:
- Plan outline sent as separate ephemeral message (avoids 4096 char truncation)
- Added show_resume_line, budget_enabled, budget_auto_cancel to
  EngineOverrides, EngineRunOptions, normalize/merge, and all constructors

Tests: 1610 passed, 80.56% coverage, ruff clean.
Integration tested on @untether_dev_bot across all 6 engine chats.

Closes #128, closes #129, fixes #124

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI for PR #132

* fix: address 11 CodeRabbit review comments on PR #132

Bug fixes:
- claude.py: fix UnboundLocalError when factory.resume is falsy in
  ask_question.extraction_failed logging path
- ask_question.py: reject malformed option callbacks instead of
  silently falling back to option 0
- files.py: raise FileExistsError when deduplicate_target exhausts
  999 suffixes instead of returning the original (overwrite risk)
- config.py: disambiguate Codex "Full auto" (fa) vs Gemini "Full
  access" (ya) callback IDs and toast labels

Hardening:
- codex.py: add --ask-for-approval to _EXEC_ONLY_FLAGS guard
- model.py: add try/except to clear path (matching set path)
- reasoning.py: add try/except to clear path (matching set path)
- loop.py: notify user when media group upload fails instead of
  silently dropping
- export.py: log session count instead of identifiers at info level
- config.py: resolve resume-line default from config instead of
  hardcoding True
- staging.sh: pin PyPI index in rollback/reset with --pip-args

Skipped (not applicable):
- CHANGELOG.md: RC versions don't get changelog entries per release
  discipline
- docs/tutorials TODO screenshot: pre-existing, not introduced by PR
- .claude/hooks/context-guard-stop.sh: ContextDocs plugin hook, not
  Untether source

Tests: 1611 passed, 80.48% coverage, ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace bare pass with debug log to satisfy bandit B110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: setup wizard security + UX improvements

- Auto-set allowed_user_ids from captured Telegram user ID during
  onboarding (security: restricts bot to the setup user's account)
- Add "next steps" panel after wizard completion with pointers to
  /config, voice notes, projects, and account lock confirmation
- Update install.md: Python 3.12+ (not just 3.14), dynamic version
  string, /config mention for post-setup changes
- Update first-run.md: /config → Engine & model for default engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: plan outline UX — markdown rendering, buttons, cleanup (#139, #140, #141)

- Render outline messages as formatted text via render_markdown() +
  split_markdown_body() instead of raw markdown (#139)
- Add approve/deny buttons to last outline message so users don't
  have to scroll up past long outlines (#140)
- Delete outline messages on approve/deny via module-level
  _OUTLINE_REGISTRY callable from callback handler; suppress stale
  keyboard on progress message (#141)
- 8 new tests for outline rendering, …
Nathan Schram (nathanschram) added a commit that referenced this pull request Apr 15, 2026
* feat: three-mode support, startup mode indicator, dev branch workflow (#158, #159)

* ci: add CODEOWNERS, update action SHA pins, add permission comments

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add release guard hooks and document protection in CLAUDE.md

Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify /config default labels and remove redundant "Works with" lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress redundant cost footer on error runs

When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: suppress stall notifications when CPU-active + heartbeat re-render

When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.34.5rc2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: CI release-validation tomllib bytes/str mismatch

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: integrate screenshots into docs with correct JPG references

- Add 44 screenshots to docs/assets/screenshots/
- Fix all image refs from .png to .jpg across 25 doc files
- README uses absolute raw.githubusercontent.com URLs for PyPI rendering
- Fix 5 filename mismatches (session-auto-resume→chat-auto-resume, etc.)
- Comment out 11 missing screenshots with TODO markers
- Add CAPTURES.md checklist tracking capture status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: convert markdown images to HTML img tags for GitHub compatibility

Switch from MkDocs `![alt](src){ loading=lazy }` syntax to HTML `<img>`
tags with width="360" and loading="lazy". Fixes two GitHub rendering
issues: `{ loading=lazy }` appearing as visible text, and oversized
images with no width constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix 3 screenshot mismatches and replace 3 screenshots

- first-run.md: rewrite resume line text to match footer screenshot
- interactive-control.md: update planmode show admonition to match screenshot (auto not on)
- switch-engines.md: swap engine-footer.jpg for multi-engine-switch.jpg
- Replace startup-message.jpg with clean v0.34.4 capture (was rc/6-projects)
- Replace cooldown-auto-deny.jpg with post-outline approve/deny buttons
- Replace file-put.jpg with photo save confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add iOS caption limitation note to file transfer guide

Telegram iOS doesn't show a caption field when sending documents via the
File picker, so /file put <path> captions aren't easily accessible.
Added a note with workarounds (use Desktop, send as photo, or let
auto-save handle it). Updated screenshot alt text to match actual
screenshot content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: temp swap README image URLs to feature branch for preview

Will revert to master before merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: lay out all 3 README screenshots in a single row

Reduce from 360px to 270px each and combine into one <p> block
so all three hero screenshots sit side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap 3rd hero screenshot for config-menu for visual variety

Replace plan-outline-approve (too similar to approval-diff-preview)
with config-menu showing the /config settings grid. The three hero
images now tell: voice input → approve changes → configure everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add captions under README hero screenshots

Small <sub> captions: "Send tasks by voice (Whisper transcription)",
"Approve changes remotely", "Configure from Telegram".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: use table layout for README hero screenshots with captions

Fixes stacking issue — <br> in a <p> broke inline flow. A table
keeps images side by side with captions underneath each one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace table layout with single hero collage image

Composite image scales proportionally on mobile instead of requiring
horizontal scroll. Captions baked into the image via ImageMagick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap middle hero screenshot for full 3-button approval view

Replace approval-diff-preview with approval-buttons-howto showing
Approve / Deny / Pause & Outline Plan — more visually impressive.
Caption now reads "Approve changes remotely (Claude Code)".
Added footnote linking to engine compatibility table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap config-menu for parallel-projects in hero collage

Third hero screenshot now shows 10+ projects running simultaneously
across different repos — much more compelling than a settings menu.
New caption: "Run agents across projects in parallel".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: revert README image URL to master for merge

Swap hero-collage URL back from feature/github-hardening to master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc3

- fix: preserve all EngineOverrides fields when setting model/planmode/reasoning
  (was silently wiping ask_questions, diff_preview, show_api_cost, etc.)
- fix: /config home page resolves "default" to effective values
- feat: file upload auto-deduplication (append _1, _2 instead of requiring --force)
- feat: media groups without captions now auto-save instead of showing usage text
- feat: resume line visual separation (blank line + ↩️ prefix)
- fix: claude auto-approve echoes updatedInput in control response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: expand permission policies for Codex CLI and Gemini CLI in /config

Codex gets a new "Approval policy" page (full auto / safe) that passes
--ask-for-approval untrusted when safe mode is selected. Gemini's approval
mode expands from 2 to 3 tiers (read-only / edit files / full access) with
--approval-mode auto_edit for the middle tier. Both engines now show an
"Agent controls" section on the /config home page. Engine-specific model
default hints replace the generic "from CLI settings" text.

Also adds staging.sh helper, context-guard-stop hook, and docs updates.

Closes #131

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /config UX overhaul, resume line toggle, cost budget settings, model metadata

/config UX cleanup:
- Convert all binary toggles from 3-column (on/off/clear) to 2-column
  (toggle + clear) for better mobile tap targets
- Merge Engine + Model into combined "Engine & model" page
- Reorganise home page to max 2 buttons per row across all engines
- Split plan mode 3-option rows (off/on/auto) into 2+1 layout
- Add _toggle_row() helper for consistent toggle button rendering

New features:
- #128: Resume line /config toggle — per-chat show_resume_line override
  via EngineOverrides with On/Off/Clear buttons, wired into executor
- #129: Cost budget /config settings — per-chat budget_enabled and
  budget_auto_cancel overrides on the Cost & Usage page, wired into
  _check_cost_budget() in runner_bridge.py

Model metadata improvements:
- Show Claude Code [1m] context window suffix: "opus 4.6 (1M)"
- Strip Gemini CLI "auto-" prefix: "auto-gemini-3" → "gemini-3"
- Future-proof: unknown suffixes default to .upper() (e.g. [500k] → 500K)

Bug fixes:
- #124: Standalone override commands (/planmode, /model, /reasoning) now
  preserve all EngineOverrides fields including new ones
- Error handling: control_response.write_failed catch-all in claude.py,
  ask_question.extraction_failed warning, model.override.failed logging

Hardening:
- Plan outline sent as separate ephemeral message (avoids 4096 char truncation)
- Added show_resume_line, budget_enabled, budget_auto_cancel to
  EngineOverrides, EngineRunOptions, normalize/merge, and all constructors

Tests: 1610 passed, 80.56% coverage, ruff clean.
Integration tested on @untether_dev_bot across all 6 engine chats.

Closes #128, closes #129, fixes #124

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI for PR #132

* fix: address 11 CodeRabbit review comments on PR #132

Bug fixes:
- claude.py: fix UnboundLocalError when factory.resume is falsy in
  ask_question.extraction_failed logging path
- ask_question.py: reject malformed option callbacks instead of
  silently falling back to option 0
- files.py: raise FileExistsError when deduplicate_target exhausts
  999 suffixes instead of returning the original (overwrite risk)
- config.py: disambiguate Codex "Full auto" (fa) vs Gemini "Full
  access" (ya) callback IDs and toast labels

Hardening:
- codex.py: add --ask-for-approval to _EXEC_ONLY_FLAGS guard
- model.py: add try/except to clear path (matching set path)
- reasoning.py: add try/except to clear path (matching set path)
- loop.py: notify user when media group upload fails instead of
  silently dropping
- export.py: log session count instead of identifiers at info level
- config.py: resolve resume-line default from config instead of
  hardcoding True
- staging.sh: pin PyPI index in rollback/reset with --pip-args

Skipped (not applicable):
- CHANGELOG.md: RC versions don't get changelog entries per release
  discipline
- docs/tutorials TODO screenshot: pre-existing, not introduced by PR
- .claude/hooks/context-guard-stop.sh: ContextDocs plugin hook, not
  Untether source

Tests: 1611 passed, 80.48% coverage, ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace bare pass with debug log to satisfy bandit B110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: setup wizard security + UX improvements

- Auto-set allowed_user_ids from captured Telegram user ID during
  onboarding (security: restricts bot to the setup user's account)
- Add "next steps" panel after wizard completion with pointers to
  /config, voice notes, projects, and account lock confirmation
- Update install.md: Python 3.12+ (not just 3.14), dynamic version
  string, /config mention for post-setup changes
- Update first-run.md: /config → Engine & model for default engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: plan outline UX — markdown rendering, buttons, cleanup (#139, #140, #141)

- Render outline messages as formatted text via render_markdown() +
  split_markdown_body() instead of raw markdown (#139)
- Add approve/deny buttons to last outline message so users don't
  have to scroll up past long outlines (#140)
- Delete outline messages on approve/deny via module-level
  _OUTLINE_REGISTRY callable from callback handler; suppress stale
  keyboard on progress message (#141)
- 8 new tests for outline rendering, keyboard placement, and cleanup
- Bump version to 0.35.0rc5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /continue command — cross-environment resume for all engines (#135)

New `/continue` command resumes the most recent CLI session in the
project directory from Telegram. Enables starting a session in your
terminal and picking it up from your phone.

Engine support: Claude (--continue), Codex (resume --last), OpenCode
(--continue), Pi (--continue), Gemini (--resume latest). AMP not
supported (requires explicit thread ID).

Includes ResumeToken.is_continue flag, build_args for all 6 runners,
reserved command registration, resume emoji prefix stripping for
reply-to-continue, docs (how-to guide, README, commands ref, routing
explanation, conversation modes tutorial), and 99 new test assertions.

Integration tested against @untether_dev_bot — all 5 supported engines
passed secret-recall verification via Telegram MCP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: agent outbox file delivery + fix cross-chat ask stealing (#143, #144)

Outbox delivery (#143): agents write files to .untether-outbox/ during a
run; Untether sends them as Telegram documents on completion with 📎
captions. Config: outbox_enabled, outbox_dir, outbox_max_files,
outbox_cleanup. Deny-glob security, size limits, auto-cleanup.
Preamble updated for all 6 engines. Integration tested across
Claude, Codex, OpenCode, Pi, and Gemini.

AskUserQuestion fix (#144): _PENDING_ASK_REQUESTS and
_ASK_QUESTION_FLOWS were global dicts with no chat_id scoping — a
pending ask in one chat would steal the next message from any other
chat. Added channel_id contextvar and scoped all ask lookups by it.
Session cleanup now also clears stale pending asks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: v0.35.0 changelog completion + fix #123 updatedInput

- Complete v0.35.0 changelog: add missing entries for /continue (#135),
  /config UX overhaul (#132), resume line toggle (#128), cost budget
  (#129), model metadata, resume line formatting (#127), override
  preservation (#124), and updatedInput fix (#123)
- Fix #123: register input for system-level auto-approved control
  requests so updatedInput is included in the response
- Add parameterised test for all 5 auto-approve types input registration
- Remove unused OutboxResult import (ruff fix)

Issues closed: #115, #118, #123, #124, #126, #127, #134

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.35.0rc6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rc6 integration test fixes (#145, #146, #147, #148, #149)

- Reduce Telegram API timeout from 120s to 30s (#145)
- OpenCode error runs show error text instead of empty body (#146)
- Pi /continue captures session ID via allow_id_promotion (#147)
- Post-outline approval uses skip_reply to avoid "not found" (#148)
- Orphan progress message cleanup on restart (#149)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: post-outline notification reply + OpenCode empty body (#148, #150)

- #148: skip_reply callback results now bypass the executor's default
  reply_to fallback, sending directly via the transport with no
  reply_to_message_id. Previously, the executor treated reply_to=None
  as "use default" which pointed to the (deleted) outline message.

- #150: OpenCode normal completion with no Text events now falls back
  to last_tool_error. Added state.last_tool_error field populated on
  ToolUse error status. Covers both translate() and stream_end_events().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: suppress post-outline notification to avoid "message not found" (#148)

After outline approval/denial, the progress loop's _send_notify was
firing for the next tool approval, but the notification's reply_to
anchor could reference deleted state. Added _outline_just_resolved
flag to skip one notification cycle after outline cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: note OpenCode lacks auto-compaction — long sessions degrade (#150)

Added known limitation to OpenCode runner docs and integration testing
playbook. OpenCode sessions accumulate unbounded context (no compaction
events unlike Pi). Workaround: use /new before isolated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip_reply on regular approve path when outline was deleted (#148)

The "Approve Plan" button on outline messages uses the real
ExitPlanMode request_id, routing through the regular approve path
(not the da: synthetic path). When outline messages exist, set
skip_reply=True on the CommandResult to avoid replying to the
just-deleted outline message.

Also added reply_to_message_id and text_preview to transport.send.failed
warning for easier debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc6 integration test fixes (#145-#150)

Updated fix descriptions for #146/#150 (OpenCode last_tool_error
fallback) and #148 (regular approve path skip_reply). Added docs
section for OpenCode compaction limitation. Updated test counts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix formatting after merge resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review comments on PR #151

- bridge.py: replace text_preview with text_len in send failure warning
  to avoid logging raw message content (security)
- runner_bridge.py: move unregister_progress() after send_result_message()
  to avoid orphan window between ephemeral cleanup and final message send
- cross-environment-resume.md: add language spec to code block

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve /config "default" labels to effective on/off values (#152)

Sub-pages showed "Current: default" or "default (on/off)" while buttons
already showed the resolved value. Now all boolean-toggle settings show
the effective on/off value in both text and buttons.

Affected: verbose, ask mode, diff preview, API cost, subscription usage,
budget enabled/auto-cancel, resume line. Home page cost & resume labels
also resolved.

Plan mode, model, and reasoning keep "default" since they depend on CLI
settings and aren't simple on/off booleans.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc7 config default labels fix (#152)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update documentation for v0.35.0

- fix missing nav entries in zensical.toml (cross-env resume, Gemini/Amp runners)
- rewrite inline-settings.md for /config UX overhaul (2-column toggles, budget/resume toggles)
- update plan-mode.md with outline rendering, buttons-on-last-chunk, ephemeral cleanup
- update interactive-control tutorial with outline UX improvements
- add orphan progress cleanup section to operations.md
- add engine-specific approval policies to interactive-approval.md
- add per-chat budget overrides to cost-budgets.md
- update module-map.md with Gemini/Amp and new modules (outbox, progress persistence, proc_diag)
- update architecture.md mermaid diagrams with all 6 engines
- bump specification.md to v0.35.0, add progress persistence and outbox sections
- add v0.35.0 screenshot entries to CAPTURES.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: broaden frozen ring buffer stall escalation beyond MCP tools (#155)

Frozen ring buffer escalation was gated on `mcp_server is not None`,
so general stalls with cpu_active=True and no MCP tool running were
silently suppressed indefinitely. Broadened to fire for all stalls
after 3+ checks with no new JSONL events regardless of tool type.

New notification: "CPU active, no new events" for non-MCP frozen stalls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: tool approval buttons no longer suppressed after outline approval (#156)

After "Approve Plan" on an outline, the stale discuss_approve action
remained in ProgressTracker with completed=False. The renderer picked
up its stale "Approve Plan"/"Deny" buttons first, then the suppression
logic at line 994 stripped ALL buttons — including new Write/Edit/Bash
approval buttons. Claude blocked indefinitely waiting for approval.

Fix: after suppressing stale buttons, complete the discuss_approve
action(s) in the tracker, reset _outline_sent, and trigger a re-render
so subsequent tool requests get their own Approve/Deny buttons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add workflow mode indicator to startup message, fix startup crash on topics validation failure (#158, #159)

Features:
- Startup message now shows mode: assistant/workspace/handoff
- Derived from session_mode + topics.enabled config values
- _resolve_mode_label() helper in backend.py

Bug fixes:
- Fix UnboundLocalError crash when topics validation fails on startup (#158)
  - Moved import signal and shutdown imports before try block in loop.py
- Downgrade can_manage_topics check from fatal error to warning (#159)
  - Bot can now start without manage_topics admin right
  - Existing topics work fine; only topic creation/editing affected

Tests:
- 17 new unit tests for stateless/handoff mode (test_stateless_mode.py)
  - _should_show_resume_line, _chat_session_key, ResumeResolver, ResumeLineProxy
  - Integration-level: stateless shows resume lines, no auto-resume, chat hides lines
- 3 new tests for mode indicator in startup message (test_telegram_backend.py)

Docs:
- New docs/reference/modes.md — comprehensive reference for all 3 workflow modes
- Updated docs/reference/index.md and zensical.toml nav with modes page

* docs: comprehensive three-mode coverage across all documentation

New:
- docs/how-to/choose-a-mode.md — decision tree, mode comparison, mermaid
  sequence diagrams, configuration examples, switching guide, workspace
  prerequisites

Updated:
- README.md — improved three-mode description in features list
- docs/tutorials/install.md — added mode selection step (section 10)
- docs/tutorials/first-run.md — added 'What mode am I in?' tip
- docs/reference/config.md — cross-linked session_mode/show_resume_line to modes.md
- docs/reference/transports/telegram.md — added mode requirement callouts
  for forum topics and chat sessions sections
- docs/how-to/chat-sessions.md — added session persistence explanation
  (state files, auto-resume mechanics, handoff note)
- docs/how-to/topics.md — expanded prerequisites checklist with group
  privacy, can_manage_topics, and re-add steps
- docs/how-to/cross-environment-resume.md — added handoff mode terminal
  workflow with mermaid sequence diagram
- docs/how-to/index.md — added 'Getting started' section with choose-a-mode
- zensical.toml — added choose-a-mode to nav

* docs: add three-mode summary table to README Quick Start section

* feat: migrate to dev branch workflow — dev→TestPyPI, master→PyPI

Branch model:
- feature/* → PR → dev (TestPyPI auto-publish) → PR → master (PyPI)
- master always matches latest PyPI release
- dev is the integration/staging branch

CI changes:
- ci.yml: TestPyPI publish triggers on dev push (was master)
- ci.yml, codeql.yml: CI runs on both master and dev pushes
- dependabot.yml: PRs target dev branch

Hook changes:
- release-guard.sh: updated messages to mention dev branch
- release-guard-mcp.sh: updated messages to mention dev branch
- Both hooks already allow dev pushes (only block master/main)

Documentation:
- CLAUDE.md: updated 3-phase workflow, CI table, release guard docs
- dev-workflow.md: added branch model section
- release-discipline.md: added dev branch staging notes

* ci: retrigger CI for PR #160

* feat: allow Claude Code to merge PRs targeting dev branch only

Release guard hooks now check the PR's base branch:
- dev → allowed (TestPyPI/staging)
- master/main → blocked (PyPI releases remain Nathan-only)

Both Bash hook (gh pr merge) and MCP hook (merge_pull_request)
updated with base branch checking via gh pr view.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.35.0rc7

* docs: add workflow mode indicator to CLAUDE.md (#162)

* ci: add CODEOWNERS, update action SHA pins, add permission comments

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add release guard hooks and document protection in CLAUDE.md

Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify /config default labels and remove redundant "Works with" lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress redundant cost footer on error runs

When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: suppress stall notifications when CPU-active + heartbeat re-render

When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.34.5rc2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: CI release-validation tomllib bytes/str mismatch

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: integrate screenshots into docs with correct JPG references

- Add 44 screenshots to docs/assets/screenshots/
- Fix all image refs from .png to .jpg across 25 doc files
- README uses absolute raw.githubusercontent.com URLs for PyPI rendering
- Fix 5 filename mismatches (session-auto-resume→chat-auto-resume, etc.)
- Comment out 11 missing screenshots with TODO markers
- Add CAPTURES.md checklist tracking capture status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: convert markdown images to HTML img tags for GitHub compatibility

Switch from MkDocs `![alt](src){ loading=lazy }` syntax to HTML `<img>`
tags with width="360" and loading="lazy". Fixes two GitHub rendering
issues: `{ loading=lazy }` appearing as visible text, and oversized
images with no width constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix 3 screenshot mismatches and replace 3 screenshots

- first-run.md: rewrite resume line text to match footer screenshot
- interactive-control.md: update planmode show admonition to match screenshot (auto not on)
- switch-engines.md: swap engine-footer.jpg for multi-engine-switch.jpg
- Replace startup-message.jpg with clean v0.34.4 capture (was rc/6-projects)
- Replace cooldown-auto-deny.jpg with post-outline approve/deny buttons
- Replace file-put.jpg with photo save confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add iOS caption limitation note to file transfer guide

Telegram iOS doesn't show a caption field when sending documents via the
File picker, so /file put <path> captions aren't easily accessible.
Added a note with workarounds (use Desktop, send as photo, or let
auto-save handle it). Updated screenshot alt text to match actual
screenshot content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: temp swap README image URLs to feature branch for preview

Will revert to master before merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: lay out all 3 README screenshots in a single row

Reduce from 360px to 270px each and combine into one <p> block
so all three hero screenshots sit side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap 3rd hero screenshot for config-menu for visual variety

Replace plan-outline-approve (too similar to approval-diff-preview)
with config-menu showing the /config settings grid. The three hero
images now tell: voice input → approve changes → configure everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add captions under README hero screenshots

Small <sub> captions: "Send tasks by voice (Whisper transcription)",
"Approve changes remotely", "Configure from Telegram".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: use table layout for README hero screenshots with captions

Fixes stacking issue — <br> in a <p> broke inline flow. A table
keeps images side by side with captions underneath each one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace table layout with single hero collage image

Composite image scales proportionally on mobile instead of requiring
horizontal scroll. Captions baked into the image via ImageMagick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap middle hero screenshot for full 3-button approval view

Replace approval-diff-preview with approval-buttons-howto showing
Approve / Deny / Pause & Outline Plan — more visually impressive.
Caption now reads "Approve changes remotely (Claude Code)".
Added footnote linking to engine compatibility table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap config-menu for parallel-projects in hero collage

Third hero screenshot now shows 10+ projects running simultaneously
across different repos — much more compelling than a settings menu.
New caption: "Run agents across projects in parallel".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: revert README image URL to master for merge

Swap hero-collage URL back from feature/github-hardening to master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc3

- fix: preserve all EngineOverrides fields when setting model/planmode/reasoning
  (was silently wiping ask_questions, diff_preview, show_api_cost, etc.)
- fix: /config home page resolves "default" to effective values
- feat: file upload auto-deduplication (append _1, _2 instead of requiring --force)
- feat: media groups without captions now auto-save instead of showing usage text
- feat: resume line visual separation (blank line + ↩️ prefix)
- fix: claude auto-approve echoes updatedInput in control response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: expand permission policies for Codex CLI and Gemini CLI in /config

Codex gets a new "Approval policy" page (full auto / safe) that passes
--ask-for-approval untrusted when safe mode is selected. Gemini's approval
mode expands from 2 to 3 tiers (read-only / edit files / full access) with
--approval-mode auto_edit for the middle tier. Both engines now show an
"Agent controls" section on the /config home page. Engine-specific model
default hints replace the generic "from CLI settings" text.

Also adds staging.sh helper, context-guard-stop hook, and docs updates.

Closes #131

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /config UX overhaul, resume line toggle, cost budget settings, model metadata

/config UX cleanup:
- Convert all binary toggles from 3-column (on/off/clear) to 2-column
  (toggle + clear) for better mobile tap targets
- Merge Engine + Model into combined "Engine & model" page
- Reorganise home page to max 2 buttons per row across all engines
- Split plan mode 3-option rows (off/on/auto) into 2+1 layout
- Add _toggle_row() helper for consistent toggle button rendering

New features:
- #128: Resume line /config toggle — per-chat show_resume_line override
  via EngineOverrides with On/Off/Clear buttons, wired into executor
- #129: Cost budget /config settings — per-chat budget_enabled and
  budget_auto_cancel overrides on the Cost & Usage page, wired into
  _check_cost_budget() in runner_bridge.py

Model metadata improvements:
- Show Claude Code [1m] context window suffix: "opus 4.6 (1M)"
- Strip Gemini CLI "auto-" prefix: "auto-gemini-3" → "gemini-3"
- Future-proof: unknown suffixes default to .upper() (e.g. [500k] → 500K)

Bug fixes:
- #124: Standalone override commands (/planmode, /model, /reasoning) now
  preserve all EngineOverrides fields including new ones
- Error handling: control_response.write_failed catch-all in claude.py,
  ask_question.extraction_failed warning, model.override.failed logging

Hardening:
- Plan outline sent as separate ephemeral message (avoids 4096 char truncation)
- Added show_resume_line, budget_enabled, budget_auto_cancel to
  EngineOverrides, EngineRunOptions, normalize/merge, and all constructors

Tests: 1610 passed, 80.56% coverage, ruff clean.
Integration tested on @untether_dev_bot across all 6 engine chats.

Closes #128, closes #129, fixes #124

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI for PR #132

* fix: address 11 CodeRabbit review comments on PR #132

Bug fixes:
- claude.py: fix UnboundLocalError when factory.resume is falsy in
  ask_question.extraction_failed logging path
- ask_question.py: reject malformed option callbacks instead of
  silently falling back to option 0
- files.py: raise FileExistsError when deduplicate_target exhausts
  999 suffixes instead of returning the original (overwrite risk)
- config.py: disambiguate Codex "Full auto" (fa) vs Gemini "Full
  access" (ya) callback IDs and toast labels

Hardening:
- codex.py: add --ask-for-approval to _EXEC_ONLY_FLAGS guard
- model.py: add try/except to clear path (matching set path)
- reasoning.py: add try/except to clear path (matching set path)
- loop.py: notify user when media group upload fails instead of
  silently dropping
- export.py: log session count instead of identifiers at info level
- config.py: resolve resume-line default from config instead of
  hardcoding True
- staging.sh: pin PyPI index in rollback/reset with --pip-args

Skipped (not applicable):
- CHANGELOG.md: RC versions don't get changelog entries per release
  discipline
- docs/tutorials TODO screenshot: pre-existing, not introduced by PR
- .claude/hooks/context-guard-stop.sh: ContextDocs plugin hook, not
  Untether source

Tests: 1611 passed, 80.48% coverage, ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace bare pass with debug log to satisfy bandit B110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: setup wizard security + UX improvements

- Auto-set allowed_user_ids from captured Telegram user ID during
  onboarding (security: restricts bot to the setup user's account)
- Add "next steps" panel after wizard completion with pointers to
  /config, voice notes, projects, and account lock confirmation
- Update install.md: Python 3.12+ (not just 3.14), dynamic version
  string, /config mention for post-setup changes
- Update first-run.md: /config → Engine & model for default engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: plan outline UX — markdown rendering, buttons, cleanup (#139, #140, #141)

- Render outline messages as formatted text via render_markdown() +
  split_markdown_body() instead of raw markdown (#139)
- Add approve/deny buttons to last outline message so users don't
  have to scroll up past long outlines (#140)
- Delete outline messages on approve/deny via module-level
  _OUTLINE_REGISTRY callable from callback handler; suppress stale
  keyboard on progress message (#141)
- 8 new tests for outline rendering, keyboard placement, and cleanup
- Bump version to 0.35.0rc5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /continue command — cross-environment resume for all engines (#135)

New `/continue` command resumes the most recent CLI session in the
project directory from Telegram. Enables starting a session in your
terminal and picking it up from your phone.

Engine support: Claude (--continue), Codex (resume --last), OpenCode
(--continue), Pi (--continue), Gemini (--resume latest). AMP not
supported (requires explicit thread ID).

Includes ResumeToken.is_continue flag, build_args for all 6 runners,
reserved command registration, resume emoji prefix stripping for
reply-to-continue, docs (how-to guide, README, commands ref, routing
explanation, conversation modes tutorial), and 99 new test assertions.

Integration tested against @untether_dev_bot — all 5 supported engines
passed secret-recall verification via Telegram MCP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: agent outbox file delivery + fix cross-chat ask stealing (#143, #144)

Outbox delivery (#143): agents write files to .untether-outbox/ during a
run; Untether sends them as Telegram documents on completion with 📎
captions. Config: outbox_enabled, outbox_dir, outbox_max_files,
outbox_cleanup. Deny-glob security, size limits, auto-cleanup.
Preamble updated for all 6 engines. Integration tested across
Claude, Codex, OpenCode, Pi, and Gemini.

AskUserQuestion fix (#144): _PENDING_ASK_REQUESTS and
_ASK_QUESTION_FLOWS were global dicts with no chat_id scoping — a
pending ask in one chat would steal the next message from any other
chat. Added channel_id contextvar and scoped all ask lookups by it.
Session cleanup now also clears stale pending asks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: v0.35.0 changelog completion + fix #123 updatedInput

- Complete v0.35.0 changelog: add missing entries for /continue (#135),
  /config UX overhaul (#132), resume line toggle (#128), cost budget
  (#129), model metadata, resume line formatting (#127), override
  preservation (#124), and updatedInput fix (#123)
- Fix #123: register input for system-level auto-approved control
  requests so updatedInput is included in the response
- Add parameterised test for all 5 auto-approve types input registration
- Remove unused OutboxResult import (ruff fix)

Issues closed: #115, #118, #123, #124, #126, #127, #134

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.35.0rc6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rc6 integration test fixes (#145, #146, #147, #148, #149)

- Reduce Telegram API timeout from 120s to 30s (#145)
- OpenCode error runs show error text instead of empty body (#146)
- Pi /continue captures session ID via allow_id_promotion (#147)
- Post-outline approval uses skip_reply to avoid "not found" (#148)
- Orphan progress message cleanup on restart (#149)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: post-outline notification reply + OpenCode empty body (#148, #150)

- #148: skip_reply callback results now bypass the executor's default
  reply_to fallback, sending directly via the transport with no
  reply_to_message_id. Previously, the executor treated reply_to=None
  as "use default" which pointed to the (deleted) outline message.

- #150: OpenCode normal completion with no Text events now falls back
  to last_tool_error. Added state.last_tool_error field populated on
  ToolUse error status. Covers both translate() and stream_end_events().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: suppress post-outline notification to avoid "message not found" (#148)

After outline approval/denial, the progress loop's _send_notify was
firing for the next tool approval, but the notification's reply_to
anchor could reference deleted state. Added _outline_just_resolved
flag to skip one notification cycle after outline cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: note OpenCode lacks auto-compaction — long sessions degrade (#150)

Added known limitation to OpenCode runner docs and integration testing
playbook. OpenCode sessions accumulate unbounded context (no compaction
events unlike Pi). Workaround: use /new before isolated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip_reply on regular approve path when outline was deleted (#148)

The "Approve Plan" button on outline messages uses the real
ExitPlanMode request_id, routing through the regular approve path
(not the da: synthetic path). When outline messages exist, set
skip_reply=True on the CommandResult to avoid replying to the
just-deleted outline message.

Also added reply_to_message_id and text_preview to transport.send.failed
warning for easier debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc6 integration test fixes (#145-#150)

Updated fix descriptions for #146/#150 (OpenCode last_tool_error
fallback) and #148 (regular approve path skip_reply). Added docs
section for OpenCode compaction limitation. Updated test counts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix formatting after merge resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review comments on PR #151

- bridge.py: replace text_preview with text_len in send failure warning
  to avoid logging raw message content (security)
- runner_bridge.py: move unregister_progress() after send_result_message()
  to avoid orphan window between ephemeral cleanup and final message send
- cross-environment-resume.md: add language spec to code block

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve /config "default" labels to effective on/off values (#152)

Sub-pages showed "Current: default" or "default (on/off)" while buttons
already showed the resolved value. Now all boolean-toggle settings show
the effective on/off value in both text and buttons.

Affected: verbose, ask mode, diff preview, API cost, subscription usage,
budget enabled/auto-cancel, resume line. Home page cost & resume labels
also resolved.

Plan mode, model, and reasoning keep "default" since they depend on CLI
settings and aren't simple on/off booleans.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update changelog for rc7 config default labels fix (#152)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update documentation for v0.35.0

- fix missing nav entries in zensical.toml (cross-env resume, Gemini/Amp runners)
- rewrite inline-settings.md for /config UX overhaul (2-column toggles, budget/resume toggles)
- update plan-mode.md with outline rendering, buttons-on-last-chunk, ephemeral cleanup
- update interactive-control tutorial with outline UX improvements
- add orphan progress cleanup section to operations.md
- add engine-specific approval policies to interactive-approval.md
- add per-chat budget overrides to cost-budgets.md
- update module-map.md with Gemini/Amp and new modules (outbox, progress persistence, proc_diag)
- update architecture.md mermaid diagrams with all 6 engines
- bump specification.md to v0.35.0, add progress persistence and outbox sections
- add v0.35.0 screenshot entries to CAPTURES.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: broaden frozen ring buffer stall escalation beyond MCP tools (#155)

Frozen ring buffer escalation was gated on `mcp_server is not None`,
so general stalls with cpu_active=True and no MCP tool running were
silently suppressed indefinitely. Broadened to fire for all stalls
after 3+ checks with no new JSONL events regardless of tool type.

New notification: "CPU active, no new events" for non-MCP frozen stalls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: tool approval buttons no longer suppressed after outline approval (#156)

After "Approve Plan" on an outline, the stale discuss_approve action
remained in ProgressTracker with completed=False. The renderer picked
up its stale "Approve Plan"/"Deny" buttons first, then the suppression
logic at line 994 stripped ALL buttons — including new Write/Edit/Bash
approval buttons. Claude blocked indefinitely waiting for approval.

Fix: after suppressing stale buttons, complete the discuss_approve
action(s) in the tracker, reset _outline_sent, and trigger a re-render
so subsequent tool requests get their own Approve/Deny buttons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add workflow mode indicator to startup message, fix startup crash on topics validation failure (#158, #159)

Features:
- Startup message now shows mode: assistant/workspace/handoff
- Derived from session_mode + topics.enabled config values
- _resolve_mode_label() helper in backend.py

Bug fixes:
- Fix UnboundLocalError crash when topics validation fails on startup (#158)
  - Moved import signal and shutdown imports before try block in loop.py
- Downgrade can_manage_topics check from fatal error to warning (#159)
  - Bot can now start without manage_topics admin right
  - Existing topics work fine; only topic creation/editing affected

Tests:
- 17 new unit tests for stateless/handoff mode (test_stateless_mode.py)
  - _should_show_resume_line, _chat_session_key, ResumeResolver, ResumeLineProxy
  - Integration-level: stateless shows resume lines, no auto-resume, chat hides lines
- 3 new tests for mode indicator in startup message (test_telegram_backend.py)

Docs:
- New docs/reference/modes.md — comprehensive reference for all 3 workflow modes
- Updated docs/reference/index.md and zensical.toml nav with modes page

* docs: comprehensive three-mode coverage across all documentation

New:
- docs/how-to/choose-a-mode.md — decision tree, mode comparison, mermaid
  sequence diagrams, configuration examples, switching guide, workspace
  prerequisites

Updated:
- README.md — improved three-mode description in features list
- docs/tutorials/install.md — added mode selection step (section 10)
- docs/tutorials/first-run.md — added 'What mode am I in?' tip
- docs/reference/config.md — cross-linked session_mode/show_resume_line to modes.md
- docs/reference/transports/telegram.md — added mode requirement callouts
  for forum topics and chat sessions sections
- docs/how-to/chat-sessions.md — added session persistence explanation
  (state files, auto-resume mechanics, handoff note)
- docs/how-to/topics.md — expanded prerequisites checklist with group
  privacy, can_manage_topics, and re-add steps
- docs/how-to/cross-environment-resume.md — added handoff mode terminal
  workflow with mermaid sequence diagram
- docs/how-to/index.md — added 'Getting started' section with choose-a-mode
- zensical.toml — added choose-a-mode to nav

* docs: add three-mode summary table to README Quick Start section

* feat: migrate to dev branch workflow — dev→TestPyPI, master→PyPI

Branch model:
- feature/* → PR → dev (TestPyPI auto-publish) → PR → master (PyPI)
- master always matches latest PyPI release
- dev is the integration/staging branch

CI changes:
- ci.yml: TestPyPI publish triggers on dev push (was master)
- ci.yml, codeql.yml: CI runs on both master and dev pushes
- dependabot.yml: PRs target dev branch

Hook changes:
- release-guard.sh: updated messages to mention dev branch
- release-guard-mcp.sh: updated messages to mention dev branch
- Both hooks already allow dev pushes (only block master/main)

Documentation:
- CLAUDE.md: updated 3-phase workflow, CI table, release guard docs
- dev-workflow.md: added branch model section
- release-discipline.md: added dev branch staging notes

* ci: retrigger CI for PR #160

* feat: allow Claude Code to merge PRs targeting dev branch only

Release guard hooks now check the PR's base branch:
- dev → allowed (TestPyPI/staging)
- master/main → blocked (PyPI releases remain Nathan-only)

Both Bash hook (gh pr merge) and MCP hook (merge_pull_request)
updated with base branch checking via gh pr view.

* docs: add workflow mode indicator and modes.md to CLAUDE.md

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: restore frozen ring buffer stall escalation (#155), staging 0.35.0rc8

* ci: add CODEOWNERS, update action SHA pins, add permission comments

- Create .github/CODEOWNERS requiring @littlebearapps/core review
- Pin setup-uv to v7.4.0 (6ee6290f), download-artifact to v8.0.1 (3e5f45b2)
- Add precise version comments on all action SHAs (codeql v3.32.6,
  pypi-publish v1.13.0, action-gh-release v2.5.0, fetch-metadata v2.5.0)
- Document write permissions with why-comments (OIDC, releases, auto-merge)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add release guard hooks and document protection in CLAUDE.md

Defence-in-depth hooks prevent Claude Code from pushing to master,
merging PRs, creating tags, or triggering releases. Feature branch
pushes and PR creation remain allowed.

- release-guard.sh: Bash hook blocking master push, tags, releases, PR merge
- release-guard-protect.sh: Edit/Write hook protecting guard files and hooks.json
- release-guard-mcp.sh: GitHub MCP hook blocking merge and master writes
- hooks.json: register all three hooks
- CLAUDE.md: document release guard, update workflow roles, CI pipeline notes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clarify /config default labels and remove redundant "Works with" lines

Default labels now explain what "default" means for each setting:
- Diff preview: "default (off)" — matches actual behaviour (was "default (on)")
- Model/Reasoning: "default (engine decides)"
- API cost: "default (on)", Subscription usage: "default (off)"
- Plan mode home hint: "agent decides"
- Diff preview home hint: "buttons only"

Added info lines to plan mode and reasoning sub-pages explaining
the default behaviour in more detail.

Removed all 9 "Works with: ..." lines from sub-pages — they're
redundant because engine visibility guards already hide settings
from unsupported engines.

Fixes #119

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress redundant cost footer on error runs

When a run fails (e.g. subscription limit hit), the diagnostic context
line from _extract_error() already shows cost, turns, and API time.
The 💰 cost footer was duplicating this same data in a different format.

Now the cost footer only appears on successful runs where it's the sole
source of cost information. Error runs still show cost in the diagnostic
line, and budget alerts still fire regardless.

Also adds usage field to mock Return dataclass (matching ErrorReturn)
so tests can verify cost footer behaviour on success runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: suppress stall notifications when CPU-active + heartbeat re-render

When cpu_active=True (extended thinking, background agents), suppress
Telegram stall warning notifications and instead trigger a heartbeat
re-render so the elapsed time counter keeps ticking. Notifications
still fire when cpu_active=False or None (no baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: staging 0.34.5rc2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: CI release-validation tomllib bytes/str mismatch

tomllib.loads() expects str but was receiving bytes from
sys.stdin.buffer.read() and open(...,'rb').read(). First
triggered when PR #122 changed the version (rc1 → rc2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: integrate screenshots into docs with correct JPG references

- Add 44 screenshots to docs/assets/screenshots/
- Fix all image refs from .png to .jpg across 25 doc files
- README uses absolute raw.githubusercontent.com URLs for PyPI rendering
- Fix 5 filename mismatches (session-auto-resume→chat-auto-resume, etc.)
- Comment out 11 missing screenshots with TODO markers
- Add CAPTURES.md checklist tracking capture status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: convert markdown images to HTML img tags for GitHub compatibility

Switch from MkDocs `![alt](src){ loading=lazy }` syntax to HTML `<img>`
tags with width="360" and loading="lazy". Fixes two GitHub rendering
issues: `{ loading=lazy }` appearing as visible text, and oversized
images with no width constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix 3 screenshot mismatches and replace 3 screenshots

- first-run.md: rewrite resume line text to match footer screenshot
- interactive-control.md: update planmode show admonition to match screenshot (auto not on)
- switch-engines.md: swap engine-footer.jpg for multi-engine-switch.jpg
- Replace startup-message.jpg with clean v0.34.4 capture (was rc/6-projects)
- Replace cooldown-auto-deny.jpg with post-outline approve/deny buttons
- Replace file-put.jpg with photo save confirmation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add iOS caption limitation note to file transfer guide

Telegram iOS doesn't show a caption field when sending documents via the
File picker, so /file put <path> captions aren't easily accessible.
Added a note with workarounds (use Desktop, send as photo, or let
auto-save handle it). Updated screenshot alt text to match actual
screenshot content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: temp swap README image URLs to feature branch for preview

Will revert to master before merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: lay out all 3 README screenshots in a single row

Reduce from 360px to 270px each and combine into one <p> block
so all three hero screenshots sit side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap 3rd hero screenshot for config-menu for visual variety

Replace plan-outline-approve (too similar to approval-diff-preview)
with config-menu showing the /config settings grid. The three hero
images now tell: voice input → approve changes → configure everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add captions under README hero screenshots

Small <sub> captions: "Send tasks by voice (Whisper transcription)",
"Approve changes remotely", "Configure from Telegram".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: use table layout for README hero screenshots with captions

Fixes stacking issue — <br> in a <p> broke inline flow. A table
keeps images side by side with captions underneath each one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace table layout with single hero collage image

Composite image scales proportionally on mobile instead of requiring
horizontal scroll. Captions baked into the image via ImageMagick.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap middle hero screenshot for full 3-button approval view

Replace approval-diff-preview with approval-buttons-howto showing
Approve / Deny / Pause & Outline Plan — more visually impressive.
Caption now reads "Approve changes remotely (Claude Code)".
Added footnote linking to engine compatibility table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: swap config-menu for parallel-projects in hero collage

Third hero screenshot now shows 10+ projects running simultaneously
across different repos — much more compelling than a settings menu.
New caption: "Run agents across projects in parallel".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: revert README image URL to master for merge

Swap hero-collage URL back from feature/github-hardening to master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc3

- fix: preserve all EngineOverrides fields when setting model/planmode/reasoning
  (was silently wiping ask_questions, diff_preview, show_api_cost, etc.)
- fix: /config home page resolves "default" to effective values
- feat: file upload auto-deduplication (append _1, _2 instead of requiring --force)
- feat: media groups without captions now auto-save instead of showing usage text
- feat: resume line visual separation (blank line + ↩️ prefix)
- fix: claude auto-approve echoes updatedInput in control response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: expand permission policies for Codex CLI and Gemini CLI in /config

Codex gets a new "Approval policy" page (full auto / safe) that passes
--ask-for-approval untrusted when safe mode is selected. Gemini's approval
mode expands from 2 to 3 tiers (read-only / edit files / full access) with
--approval-mode auto_edit for the middle tier. Both engines now show an
"Agent controls" section on the /config home page. Engine-specific model
default hints replace the generic "from CLI settings" text.

Also adds staging.sh helper, context-guard-stop hook, and docs updates.

Closes #131

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: staging 0.34.5rc4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /config UX overhaul, resume line toggle, cost budget settings, model metadata

/config UX cleanup:
- Convert all binary toggles from 3-column (on/off/clear) to 2-column
  (toggle + clear) for better mobile tap targets
- Merge Engine + Model into combined "Engine & model" page
- Reorganise home page to max 2 buttons per row across all engines
- Split plan mode 3-option rows (off/on/auto) into 2+1 layout
- Add _toggle_row() helper for consistent toggle button rendering

New features:
- #128: Resume line /config toggle — per-chat show_resume_line override
  via EngineOverrides with On/Off/Clear buttons, wired into executor
- #129: Cost budget /config settings — per-chat budget_enabled and
  budget_auto_cancel overrides on the Cost & Usage page, wired into
  _check_cost_budget() in runner_bridge.py

Model metadata improvements:
- Show Claude Code [1m] context window suffix: "opus 4.6 (1M)"
- Strip Gemini CLI "auto-" prefix: "auto-gemini-3" → "gemini-3"
- Future-proof: unknown suffixes default to .upper() (e.g. [500k] → 500K)

Bug fixes:
- #124: Standalone override commands (/planmode, /model, /reasoning) now
  preserve all EngineOverrides fields including new ones
- Error handling: control_response.write_failed catch-all in claude.py,
  ask_question.extraction_failed warning, model.override.failed logging

Hardening:
- Plan outline sent as separate ephemeral message (avoids 4096 char truncation)
- Added show_resume_line, budget_enabled, budget_auto_cancel to
  EngineOverrides, EngineRunOptions, normalize/merge, and all constructors

Tests: 1610 passed, 80.56% coverage, ruff clean.
Integration tested on @untether_dev_bot across all 6 engine chats.

Closes #128, closes #129, fixes #124

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI for PR #132

* fix: address 11 CodeRabbit review comments on PR #132

Bug fixes:
- claude.py: fix UnboundLocalError when factory.resume is falsy in
  ask_question.extraction_failed logging path
- ask_question.py: reject malformed option callbacks instead of
  silently falling back to option 0
- files.py: raise FileExistsError when deduplicate_target exhausts
  999 suffixes instead of returning the original (overwrite risk)
- config.py: disambiguate Codex "Full auto" (fa) vs Gemini "Full
  access" (ya) callback IDs and toast labels

Hardening:
- codex.py: add --ask-for-approval to _EXEC_ONLY_FLAGS guard
- model.py: add try/except to clear path (matching set path)
- reasoning.py: add try/except to clear path (matching set path)
- loop.py: notify user when media group upload fails instead of
  silently dropping
- export.py: log session count instead of identifiers at info level
- config.py: resolve resume-line default from config instead of
  hardcoding True
- staging.sh: pin PyPI index in rollback/reset with --pip-args

Skipped (not applicable):
- CHANGELOG.md: RC versions don't get changelog entries per release
  discipline
- docs/tutorials TODO screenshot: pre-existing, not introduced by PR
- .claude/hooks/context-guard-stop.sh: ContextDocs plugin hook, not
  Untether source

Tests: 1611 passed, 80.48% coverage, ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace bare pass with debug log to satisfy bandit B110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: setup wizard security + UX improvements

- Auto-set allowed_user_ids from captured Telegram user ID during
  onboarding (security: restricts bot to the setup user's account)
- Add "next steps" panel after wizard completion with pointers to
  /config, voice notes, projects, and account lock confirmation
- Update install.md: Python 3.12+ (not just 3.14), dynamic version
  string, /config mention for post-setup changes
- Update first-run.md: /config → Engine & model for default engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: plan outline UX — markdown rendering, buttons, cleanup (#139, #140, #141)

- Render outline messages as formatted text via render_markdown() +
  split_markdown_body() instead of raw markdown (#139)
- Add approve/deny buttons to last outline message so users don't
  have to scroll up past long outlines (#140)
- Delete outline messages on approve/deny via module-level
  _OUTLINE_REGISTRY callable from callback handler; suppress stale
  keyboard on progress message (#141)
- 8 new tests for outline rende…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/config diff preview label says "default (on)" but actual default is off

1 participant