Skip to content
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
3eb5e98
feat: three-mode support, startup mode indicator, dev branch workflow…
nathanschram Mar 20, 2026
4063d42
chore: staging 0.35.0rc7
nathanschram Mar 20, 2026
71cd772
docs: add workflow mode indicator to CLAUDE.md (#162)
nathanschram Mar 20, 2026
9fd2bf2
fix: restore frozen ring buffer stall escalation (#155), staging 0.35…
nathanschram Mar 21, 2026
696438a
docs: update CLAUDE.md test counts for v0.35.0rc8 (#165)
nathanschram Mar 21, 2026
d5c7445
fix: rc9 — engine headless hangs, auto-continue, sleeping-process sta…
nathanschram Mar 22, 2026
770a191
chore: staging 0.35.0rc9
nathanschram Mar 22, 2026
2ea05f7
docs: update CLAUDE.md test counts for v0.35.0rc9
nathanschram Mar 22, 2026
f050916
feat: rc10 UX improvements + stall warning fixes (#186, #187, #188) (…
nathanschram Mar 22, 2026
c12c140
chore: staging 0.35.0rc10
nathanschram Mar 22, 2026
670cb34
chore: staging 0.35.0rc11
nathanschram Mar 22, 2026
db94d8c
feat: add Let's discuss button to post-outline plan approval (#214)
nathanschram Mar 23, 2026
f4e34f0
fix: opencode model footer, engine command gates, gemini prompt injec…
nathanschram Mar 23, 2026
d6c006d
docs: add #215–#221 to v0.35.0 changelog
nathanschram Mar 23, 2026
98912b4
fix: /new command now cancels running processes (#222) (#223)
nathanschram Mar 23, 2026
4b135bc
docs: add topics.py and test file to CLAUDE.md key files
nathanschram Mar 23, 2026
198a7c9
fix: suppress auto-continue on signal deaths to prevent death spiral …
nathanschram Mar 23, 2026
2c2fcb4
chore: staging 0.35.0rc13
nathanschram Mar 24, 2026
b561b86
docs: update CLAUDE.md and rules for #222 fixes
nathanschram Mar 24, 2026
756182c
fix: prevent duplicate control response for already-handled requests …
nathanschram Mar 27, 2026
ec5458e
chore: staging 0.35.0rc14
nathanschram Mar 27, 2026
af73c40
chore: release v0.35.0 prep — docs, version bump, dep security (#240)
nathanschram Mar 29, 2026
603f6f9
docs: audit fixes for v0.35.0 release (#241)
nathanschram Mar 29, 2026
fc59ced
chore: staging 0.35.0rc15
nathanschram Mar 29, 2026
670320a
fix: /new command dispatch for all modes, not just topics (#236) (#242)
nathanschram Mar 29, 2026
0611a69
fix: Gemini runner defaults to yolo approval mode in headless mode (#…
nathanschram Mar 30, 2026
0d73661
feat: logging audit + CI lint expansion (v0.35.0rc16) (#256)
nathanschram Mar 31, 2026
778c418
docs: add orphaned workerd investigation to changelog (#257)
nathanschram Mar 31, 2026
f632dfb
docs: add missing changelog entries for #245, #246, #248
nathanschram Mar 31, 2026
f8ceeb3
chore: release v0.35.0
nathanschram Mar 31, 2026
840b739
docs: update context files for v0.35.0 release
nathanschram Mar 31, 2026
69e4766
fix: v0.35.1 — security hardening, stall false positive reduction, up…
nathanschram Apr 3, 2026
55bc762
chore: bump aiohttp 3.13.3 → 3.13.5 (10 CVE fixes)
nathanschram Apr 3, 2026
3542533
feat: timezone support for cron triggers (#270)
nathanschram Apr 8, 2026
8da66ea
docs: update context files for v0.35.1 release
nathanschram Apr 8, 2026
071aca4
feat: v0.35.1rc2 — max effort, SSRF, webhook actions, multipart, data…
nathanschram Apr 13, 2026
3039fd5
docs: update context files for v0.35.1rc2 trigger enhancements (#282)
nathanschram Apr 13, 2026
8c04904
fix: bypass diff_preview gate after ExitPlanMode approval (#283)
nathanschram Apr 13, 2026
721549b
ci: bump pypa/gh-action-pypi-publish from 1.13.0 to 1.14.0 (#290)
dependabot[bot] Apr 13, 2026
fe5548a
ci: bump softprops/action-gh-release from 2.5.0 to 3.0.0 (#291)
dependabot[bot] Apr 13, 2026
74ea778
feat: hot-reload trigger configuration without restart (#269) (#285)
nathanschram Apr 14, 2026
5951851
feat: v0.35.1rc4 — /at, hot-reload bridge, trigger visibility, restar…
nathanschram Apr 14, 2026
e2404b2
chore: staging 0.35.1rc5 — logging audit, docs, pytest CVE fix (#301)
nathanschram Apr 14, 2026
3caa2c3
chore: 0.35.1rc6 — doc audit gaps, healthcheck fix, update/uninstall …
nathanschram Apr 15, 2026
93c10b8
chore: release v0.35.1 (#308)
nathanschram Apr 15, 2026
74cf91b
chore: sync master into dev to unblock v0.35.1 release PR
nathanschram Apr 15, 2026
909fbf5
Merge pull request #310 from littlebearapps/sync/master-to-dev
nathanschram Apr 15, 2026
b979fec
fix: address CodeRabbit findings on PR #309 — 15 issues across trigge…
nathanschram Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .claude/rules/runner-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ factory.completed_ok(answer=..., resume=token, usage=...)

Do NOT construct `StartedEvent`, `ActionEvent`, `CompletedEvent` dataclasses directly.

## RunContext trigger_source (#271)

`RunContext` has a `trigger_source: str | None` field. Dispatchers set it to `"cron:<id>"` or `"webhook:<id>"`; `runner_bridge.handle_message` seeds `progress_tracker.meta["trigger"] = "<icon> <source>"`. Engine `StartedEvent.meta` merges over (not replaces) the trigger key via `ProgressTracker.note_event`. Runners themselves should NOT set `meta["trigger"]`; that's reserved for dispatchers.

## Session locking

- `SessionLockMixin` provides `lock_for(token) -> anyio.Semaphore`
Expand Down
16 changes: 16 additions & 0 deletions .claude/rules/telegram-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,22 @@ Agents write files to `.untether-outbox/` during a run. On completion, `outbox_d

`progress_persistence.py` tracks active progress messages in `active_progress.json`. On startup, orphan messages from a prior instance are edited to "⚠️ interrupted by restart" with keyboard removed.

## Telegram update_id persistence (#287)

`offset_persistence.py` persists the last confirmed Telegram `update_id` to `last_update_id.json` (sibling to config). On startup, `poll_updates` loads the saved offset and passes `offset=saved+1` to `getUpdates` so restarts don't drop or re-process updates within Telegram's 24h retention window. Writes are debounced (5s interval, 100-update cap) via `DebouncedOffsetWriter` — see its docstring for the crash/replay tradeoff. Flush happens automatically in the `poll_updates` finally block.

## TelegramBridgeConfig hot-reload (#286)

`TelegramBridgeConfig` is unfrozen (slots preserved) as of rc4. `update_from(settings)` applies a reloaded `TelegramTransportSettings` to the live config; `handle_reload()` in `loop.py` calls it and refreshes the two cached copies in `TelegramLoopState`. `route_update()` reads `cfg.allowed_user_ids` live so allowlist changes take effect on the next message. Restart-only keys (`bot_token`, `chat_id`, `session_mode`, `topics`, `message_overflow`) still warn with `restart_required=true`.

## sd_notify (#287)

`untether.sdnotify.notify(message)` sends `READY=1`/`STOPPING=1` to systemd's notify socket (stdlib only — no dependency). `NOTIFY_SOCKET` absent → no-op False. `poll_updates` sends `READY=1` after `_send_startup` succeeds; `_drain_and_exit` sends `STOPPING=1` at drain start. Requires `Type=notify` + `NotifyAccess=main` in the systemd unit (see `contrib/untether.service`).

## /at command (#288)

`telegram/at_scheduler.py` is a module-level holder for the task group + `run_job` closure; `install()` is called from `run_main_loop` once both are available. `AtCommand.handle` calls `schedule_delayed_run(chat_id, thread_id, delay_s, prompt)` which starts an anyio task that sleeps then dispatches. Pending delays tracked in `_PENDING`; `/cancel` drops them via `cancel_pending_for_chat(chat_id)`. Drain integration via `at_scheduler.active_count()`. No persistence — restart cancels all pending delays (documented in issue body).

## Plan outline rendering

Plan outlines render as formatted Telegram text via `render_markdown()` + `split_markdown_body()`. Approval buttons (✅/❌/📋) appear on the last outline message. Outline and notification messages are cleaned up on approve/deny via `_OUTLINE_REGISTRY`.
Expand Down
22 changes: 14 additions & 8 deletions .claude/rules/testing-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ Integration tests are automated via Telegram MCP tools by Claude Code during the

### Test chats

| Chat | Chat ID |
|------|---------|
| `ut-dev-hf: claude` | 5171122044 |
| `ut-dev-hf: codex` | 5116709786 |
| `ut-dev-hf: opencode` | 5020138767 |
| `ut-dev-hf: pi` | 5276373372 |
| `ut-dev-hf: gemini` | 5152406011 |
| `ut-dev-hf: amp` | 5064468679 |
| Chat | Chat ID | Bot API chat_id |
|------|---------|-----------------|
| Claude Code | `5284581592` | `-5284581592` |
| Codex CLI | `4929463515` | `-4929463515` |
| OpenCode | `5200822877` | `-5200822877` |
| Pi | `5156256333` | `-5156256333` |
| Gemini CLI | `5207762142` | `-5207762142` |
| AMP CLI | `5230875989` | `-5230875989` |

### Pattern

Expand Down Expand Up @@ -113,3 +113,9 @@ All integration test tiers are fully automatable by Claude Code.
| `test_loop_coverage.py` | Update loop edge cases, message routing, shutdown |
| `test_exec_runner.py` | Event tracking, ring buffer, PID in StartedEvent meta |
| `test_runner_utils.py` | Error formatting, drain_stderr, stderr sanitisation |
| `test_trigger_server.py` | Webhook HTTP server, multipart, rate limit burst, fire-and-forget dispatch |
| `test_trigger_actions.py` | file_write (multipart short-circuit), http_forward (SSRF), notify_only |
| `test_trigger_cron.py` | Cron expression matching, timezone conversion, step validation |
| `test_trigger_settings.py` | CronConfig/WebhookConfig/TriggersSettings validation, timezone |
| `test_trigger_ssrf.py` | SSRF blocking (IPv4/IPv6, DNS rebinding, allowlist) |
| `test_trigger_fetch.py` | Cron data-fetch (HTTP, file read, parse modes, failure) |
25 changes: 25 additions & 0 deletions .claude/skills/untether-architecture/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,31 @@ chat_id = -1001234567890 # optional per-project chat
- `/ctx set <project>` binds a chat context
- Project alias used as directive prefix: `/untether fix the bug`

## Trigger system

Triggers let external events or schedules start agent runs automatically. Opt-in via `[triggers] enabled = true`.

### Cron

`run_cron_scheduler()` ticks every minute, checking each `[[triggers.crons]]` entry against the current time via `cron_matches()` (5-field standard syntax). Per-cron `timezone` or global `default_timezone` converts UTC to local wall-clock time via `_resolve_now()` + `zoneinfo.ZoneInfo`. DST transitions handled automatically. `last_fired` dict prevents double-firing within the same minute.

### Webhooks

`run_webhook_server()` runs an aiohttp server. Each `[[triggers.webhooks]]` maps a URL path to auth (bearer/HMAC-SHA256/SHA1) + prompt template with `{{field.path}}` substitutions. Rate-limited per-webhook and globally.

### Dispatch

Both crons and webhooks feed into `TriggerDispatcher.dispatch_cron()`/`dispatch_webhook()` → sends a notification message to Telegram (`⏰`/`⚡`) → calls `run_job()` with the prompt, threading under the notification.
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

Dispatch flow docs are incomplete for webhook actions.

This line documents only dispatch_cron()/dispatch_webhook(), but the release scope also includes non-agent webhook actions (dispatch_action()). Please include that path to keep the architecture doc accurate.

As per coding guidelines "Maintain cross-file consistency, path verification, version accuracy, and command accuracy in AI context files".

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

In @.claude/skills/untether-architecture/SKILL.md at line 287, The doc line
omits the non-agent webhook path: update the sentence to include
TriggerDispatcher.dispatch_action() alongside dispatch_cron() and
dispatch_webhook(), stating that non-agent webhook actions also flow into
dispatch_action() → send the Telegram notification (⏰/⚡) → call run_job() with
the prompt, threading under the notification; ensure you reference
TriggerDispatcher.dispatch_action, dispatch_webhook, dispatch_cron and run_job
so cross-file/path verification and version/command accuracy are preserved.


### Key files

- `triggers/cron.py` — cron parser, timezone-aware scheduler
- `triggers/settings.py` — `CronConfig`, `WebhookConfig`, `TriggersSettings` (pydantic)
- `triggers/dispatcher.py` — notification + `run_job()` bridge
- `triggers/server.py` — aiohttp webhook server
- `triggers/auth.py` — bearer/HMAC verification
- `triggers/templating.py` — `{{field.path}}` prompt substitution

## Key conventions

- Python 3.12+, anyio for async, msgspec for JSONL parsing, structlog for logging
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ jobs:
path: dist/

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
repository-url: https://test.pypi.org/legacy/
packages-dir: dist/
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/notify-website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Trigger website rebuild
env:
TAG_NAME: ${{ github.event.release.tag_name }}
run: |
curl -s -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.WEBSITE_DISPATCH_TOKEN }}" \
https://api.github.com/repos/littlebearapps/littlebearapps.com/dispatches \
-d '{"event_type":"release-published","client_payload":{"repo":"untether","tag":"${{ github.event.release.tag_name }}"}}'
-d "$(jq -n --arg tag "$TAG_NAME" '{"event_type":"release-published","client_payload":{"repo":"untether","tag":$tag}}')"
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ jobs:
path: dist/

- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
packages-dir: dist/
skip-existing: true
Expand Down Expand Up @@ -119,7 +119,7 @@ jobs:
path: dist/

- name: Create GitHub release and upload artifacts
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
generate_release_notes: true
files: |
Expand Down
Loading
Loading