Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 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
d854488
fix: healthcheck.sh exits prematurely under set -e (#302)
nathanschram Apr 14, 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
18 changes: 13 additions & 5 deletions .claude/hooks/release-guard-mcp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,25 @@ set -euo pipefail

INPUT=$(cat)

# ── Always block merge_pull_request ───────────────────────────────
# ── merge_pull_request — allow dev, block master/main ────────────

TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
if [ "$TOOL_NAME" = "mcp__github__merge_pull_request" ]; then
echo '{"decision":"block","reason":"🛑 RELEASE GUARD: PR merging via GitHub MCP is blocked.\n\nPR merging must be done manually by Nathan in the GitHub UI."}'
PR_NUM=$(echo "$INPUT" | jq -r '.tool_input.pullNumber // .tool_input.pull_number // ""' 2>/dev/null)
if [ -n "$PR_NUM" ] && [ "$PR_NUM" != "null" ]; then
PR_BASE=$(gh pr view "$PR_NUM" --repo littlebearapps/untether --json baseRefName -q .baseRefName 2>/dev/null || echo "unknown")
if [ "$PR_BASE" = "dev" ]; then
echo '{}'
exit 0
fi
fi
echo '{"decision":"block","reason":"🛑 RELEASE GUARD: PR merging to master/main via GitHub MCP is blocked.\n\nOnly merges to dev are allowed via Claude Code. Master merges must be done manually by Nathan."}'
exit 0
fi

# Fallback: detect merge by input fields
# Fallback: detect merge by input fields (block if not already handled above)
if echo "$INPUT" | jq -e '.tool_input.pull_number // .tool_input.merge_method' > /dev/null 2>&1; then
echo '{"decision":"block","reason":"🛑 RELEASE GUARD: PR merging via GitHub MCP is blocked.\n\nPR merging must be done manually by Nathan in the GitHub UI."}'
echo '{"decision":"block","reason":"🛑 RELEASE GUARD: PR merging via GitHub MCP is blocked.\n\nUse gh pr merge <number> for dev-targeting PRs, or merge manually in GitHub UI."}'
exit 0
fi

Expand All @@ -29,7 +37,7 @@ BRANCH=$(echo "$INPUT" | jq -r '.tool_input.branch // ""' 2>/dev/null)

if [ "$BRANCH" = "master" ] || [ "$BRANCH" = "main" ] || [ -z "$BRANCH" ]; then
DISPLAY="${BRANCH:-default}"
jq -n --arg reason "🛑 RELEASE GUARD: GitHub MCP write to '${DISPLAY}' branch is blocked.\n\nSpecify a feature branch instead of master/main." \
jq -n --arg reason "🛑 RELEASE GUARD: GitHub MCP write to '${DISPLAY}' branch is blocked.\n\nSpecify a feature branch or 'dev' branch instead of master/main." \
'{"decision": "block", "reason": $reason}'
exit 0
fi
Expand Down
19 changes: 15 additions & 4 deletions .claude/hooks/release-guard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,22 @@ if echo "$COMMAND" | grep -qPi '\bgh\s+release\s+create\b'; then
REASON="gh release create is blocked. Releases must be created manually by Nathan."
fi

# ── gh pr merge ──────────────────────────────────────────────────
# ── gh pr merge — allow dev, block master/main ──────────────────

if echo "$COMMAND" | grep -qPi '\bgh\s+pr\s+merge\b'; then
BLOCKED=true
REASON="gh pr merge is blocked. PR merging must be done manually by Nathan."
PR_NUM=$(echo "$COMMAND" | grep -oP '\bgh\s+pr\s+merge\s+\K\d+')
if [ -n "$PR_NUM" ]; then
PR_BASE=$(gh pr view "$PR_NUM" --json baseRefName -q .baseRefName 2>/dev/null || echo "unknown")
if [ "$PR_BASE" = "dev" ]; then
: # Allow merges to dev (TestPyPI/staging)
else
BLOCKED=true
REASON="gh pr merge to '$PR_BASE' is blocked. Only merges to dev are allowed. Master merges must be done manually by Nathan."
fi
else
BLOCKED=true
REASON="gh pr merge without a PR number is blocked. Use: gh pr merge <number>"
fi
fi

# ── Self-protection ──────────────────────────────────────────────
Expand All @@ -92,7 +103,7 @@ fi
# ── Output ───────────────────────────────────────────────────────

if [ "$BLOCKED" = true ]; then
jq -n --arg reason "$(printf '🛑 RELEASE GUARD: %s\n\nFeature branch pushes are allowed. Only master/main, tags, releases, and PR merges are blocked.\n\nTo push a feature branch: git push -u origin <branch>\nTo create a PR: gh pr create --title "..." --body "..."\nFor master/tags/releases: Nathan runs these manually.' "$REASON")" \
jq -n --arg reason "$(printf '🛑 RELEASE GUARD: %s\n\nFeature branch and dev branch pushes are allowed. Only master/main, tags, releases, and PR merges are blocked.\n\nTo push a feature branch: git push -u origin <branch>\nTo create a PR to dev: gh pr create --base dev --title "..." --body "..."\nFor master/tags/releases: Nathan runs these manually.' "$REASON")" \
'{"decision": "block", "reason": $reason}'
else
echo '{}'
Expand Down
15 changes: 9 additions & 6 deletions .claude/rules/control-channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ _SESSION_STDIN: dict[str, anyio.abc.ByteSendStream] # session_id -> stdin
_REQUEST_TO_SESSION: dict[str, str] # request_id -> session_id
_DISCUSS_COOLDOWN: dict[str, tuple[float, int]] # session_id -> (timestamp, deny_count)
_DISCUSS_APPROVED: set[str] # sessions with post-outline approval
_PENDING_ASK_REQUESTS: dict[str, str] # request_id -> question text
_PENDING_ASK_REQUESTS: dict[str, tuple[int, str]] # request_id -> (channel_id, question)
```

- Register on first `system.init` event (when session_id is known)
Expand All @@ -29,10 +29,10 @@ _PENDING_ASK_REQUESTS: dict[str, str] # request_id -> question

## Auto-approve

Non-interactive tools are auto-approved without showing buttons:
- List maintained in `_AUTO_APPROVE_TOOLS` set
- `ControlInitializeRequest`: always auto-approved immediately
- Tool requests: check `tool_name in _AUTO_APPROVE_TOOLS`
Non-interactive requests are auto-approved without showing buttons:
- Request types in `_AUTO_APPROVE_TYPES` tuple: `ControlInitializeRequest`, `ControlHookCallbackRequest`, `ControlMcpMessageRequest`, `ControlRewindFilesRequest`, `ControlInterruptRequest`
- Tool requests: auto-approved UNLESS `tool_name in _TOOLS_REQUIRING_APPROVAL`
- `_TOOLS_REQUIRING_APPROVAL = {"ExitPlanMode", "AskUserQuestion"}`
- `ExitPlanMode`: NEVER auto-approved — always show Telegram buttons
- `AskUserQuestion`: NEVER auto-approved — shown in Telegram for user to reply with text

Expand Down Expand Up @@ -66,12 +66,15 @@ After "Pause & Outline Plan" click:

## Post-outline approval

After cooldown auto-deny, synthetic Approve/Deny buttons appear in Telegram:
After cooldown auto-deny, synthetic Approve/Deny/Let's discuss buttons (✅/❌/📋 emoji prefixes) appear in Telegram:
- User clicks "Approve Plan" → session added to `_DISCUSS_APPROVED`, cooldown cleared
- User clicks "Deny" → cooldown cleared, no auto-approve flag set
- User clicks "Let's discuss" → control request held open (never responded to) so Claude stays alive; 5-minute safety timeout (`CONTROL_REQUEST_TIMEOUT_SECONDS = 300.0`) cleans up stale held requests
- Next `ExitPlanMode` checks `_DISCUSS_APPROVED` → auto-approves if present
- Synthetic callback_data prefix: `da:` (fits 64-byte Telegram limit)
- Handled in `claude_control.py` before the normal approve/deny flow
- Outlines rendered as formatted text via `render_markdown()` + `split_markdown_body()` — approval buttons on last message
- Outline/notification cleanup via module-level `_OUTLINE_REGISTRY` on approve/deny

## Control request/response format

Expand Down
21 changes: 9 additions & 12 deletions .claude/rules/dev-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,27 +42,24 @@ scripts/staging.sh reset # or: pipx upgrade untether
systemctl --user restart untether
```

### Branch model

- **Feature branches** (`feature/*`, `fix/*`) — PR to `dev`
- **`dev` branch** — integration branch, auto-publishes to TestPyPI on merge
- **`master` branch** — release branch, always matches latest PyPI version
- Feature → `dev` → `master` (never feature → master directly)

### Testing before merge

1. Edit code in `src/`
2. `uv run pytest && uv run ruff check src/`
3. `systemctl --user restart untether-dev`
4. Test via `@untether_dev_bot` — follow `docs/reference/integration-testing.md`
5. When satisfied: commit, push, enter staging (see `docs/reference/dev-instance.md`)
5. When satisfied: commit, push feature branch, create PR to `dev`

### Integration testing before release (MANDATORY)

Before ANY version bump (patch, minor, or major), run the structured integration test suite against `@untether_dev_bot`. See `docs/reference/integration-testing.md` for the full playbook.

| Release type | Required tiers | Time |
|---|---|---|
| **Patch** | Tier 7 (smoke) + Tier 1 (affected engine + Claude) + relevant Tier 6 | ~30 min |
| **Minor** | Tier 7 + Tier 1 (all engines) + Tier 2 (Claude) + relevant Tier 3-4 + Tier 6 + upgrade path | ~75 min |
| **Major** | ALL tiers (1-7), ALL engines, full upgrade path | ~120 min |

**NEVER skip integration testing. NEVER test against staging (`@hetz_lba1_bot`).**

All integration test tiers are fully automatable by Claude Code via Telegram MCP tools (`send_message`, `get_history`, `list_inline_buttons`, `press_inline_button`, `reply_to_message`, `send_voice`, `send_file`) and the Bash tool (for `journalctl` log inspection, `kill -TERM` SIGTERM tests, FD/zombie checks). After testing, check dev bot logs for warnings/errors and create GitHub issues for any Untether bugs found. See `docs/reference/integration-testing.md` for chat IDs, workflow, and test details.
Before ANY version bump, run integration tests against `@untether_dev_bot`. See `docs/reference/integration-testing.md` for the full playbook and `.claude/rules/release-discipline.md` for tier requirements per release type. **NEVER skip integration testing. NEVER test against staging (`@hetz_lba1_bot`).**

## Staging workflow

Expand Down
4 changes: 3 additions & 1 deletion .claude/rules/release-discipline.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ Integration tests are automated via Telegram MCP tools (`send_message`, `get_his

Pre-release versions (`X.Y.ZrcN`) are used for staging on `@hetz_lba1_bot` before final release:

- rc versions live on the `dev` branch — merged via PR from feature branches
- rc versions do **NOT** require changelog entries — `validate_release.py` skips them
- rc versions are **NOT** git-tagged — no `v0.35.0rc1` tags (avoids triggering `release.yml`)
- Commit message convention: `chore: staging X.Y.ZrcN`
- Only final releases (`X.Y.Z`) get tagged and changelog entries
- Only final releases (`X.Y.Z`) get tagged and changelog entries on `master`
- `dev` → TestPyPI (auto on push), `master` → PyPI (tag + manual approval)
- See `docs/reference/dev-instance.md` for the full staging workflow

## Changelog format
Expand Down
12 changes: 12 additions & 0 deletions .claude/rules/runner-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ Every run MUST emit exactly this sequence:

After emitting `CompletedEvent`, drop all subsequent JSONL lines.

## Stream state tracking

`JsonlStreamState` (defined in `src/untether/runner.py`) captures subprocess lifecycle data including `proc_returncode`. Signal deaths (rc>128 or rc<0) are NOT auto-continued — see `_is_signal_death()` in `runner_bridge.py`.

## Auto-continue

When Claude Code exits with `last_event_type=user` (tool results sent but never processed), `runner_bridge.py` auto-resumes the session. Suppressed on signal deaths (rc=143/137) to prevent death spirals. Configure via `[auto_continue]` in `untether.toml` (`enabled`, `max_retries`).

## Event creation

Use `EventFactory` (from `src/untether/events.py`) for all event construction:
Expand All @@ -26,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
32 changes: 32 additions & 0 deletions .claude/rules/telegram-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,38 @@ Messages that should auto-delete when a run finishes:
- Approval buttons: detect transitions via keyboard length changes
- Push notification: sent separately (`notify=True`) when approval buttons appear

## Outbox file delivery

Agents write files to `.untether-outbox/` during a run. On completion, `outbox_delivery.py` scans, validates (deny-glob, size limit, file count cap), sends as Telegram documents with `📎` captions, and cleans up. Configure via `[transports.telegram.files]`: `outbox_enabled`, `outbox_dir`, `outbox_max_files`, `outbox_cleanup`.

## Progress persistence

`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`.

## /new command

`/new` cancels all running tasks for the chat via `_cancel_chat_tasks()` (in `commands/topics.py`) before clearing stored sessions. This prevents process leaks from orphaned Claude/engine subprocesses.

## After changes

If this change will be released, run integration tests T1-T10 (Telegram transport), S7 (rapid-fire), S8 (long prompt) via `@untether_dev_bot`. See `docs/reference/integration-testing.md` — the "Changed area" table maps `telegram/*.py` changes to required tests.
Expand Down
30 changes: 15 additions & 15 deletions .claude/rules/testing-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,28 +52,22 @@ assert all(isinstance(e, ActionEvent) for e in events[1:-1])

## Integration testing (MANDATORY before releases)

Unit tests cover code paths but NOT live Telegram interaction. Before every version bump, run the structured integration test suite against `@untether_dev_bot`. See `docs/reference/integration-testing.md` for the full playbook.

- **Patch**: Tier 7 (command smoke) + Tier 1 (affected engine + Claude) + relevant Tier 6
- **Minor**: Tier 7 + Tier 1 (all 6 engines) + Tier 2 (Claude interactive) + relevant Tier 3-4 + Tier 6 + upgrade path
- **Major**: ALL tiers (1-7), ALL engines, full upgrade path

**NEVER use `@hetz_lba1_bot` (staging) for initial dev testing. ALWAYS use `@untether_dev_bot` first.** Stage rc versions on `@hetz_lba1_bot` only after dev integration tests pass.
Unit tests cover code paths but NOT live Telegram interaction. Before every version bump, run integration tests against `@untether_dev_bot`. See `docs/reference/integration-testing.md` for the full playbook and `.claude/rules/release-discipline.md` for tier requirements per release type.

## Integration testing via Telegram MCP

Integration tests are automated via Telegram MCP tools by Claude Code during the release process. See `docs/reference/integration-testing.md` for the full playbook.

### Test chats

| Chat | Chat ID |
|------|---------|
| `ut-dev: claude` | 5284581592 |
| `ut-dev: codex` | 4929463515 |
| `ut-dev: opencode` | 5200822877 |
| `ut-dev: pi` | 5156256333 |
| `ut-dev: gemini` | 5207762142 |
| `ut-dev: amp` | 5230875989 |
| 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 @@ -119,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) |
4 changes: 3 additions & 1 deletion .claude/skills/claude-stream-json/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ AUTO_APPROVE_TOOLS = {"Grep", "Glob", "Read", "LS", "Bash", "BashOutput",
When Claude requests `ExitPlanMode`:
1. Inline keyboard shown: **Approve** / **Deny** / **Pause & Outline Plan**
2. "Pause & Outline Plan" sends a deny with a detailed message asking Claude to write a step-by-step plan
3. Progressive cooldown on rapid retries: 30s, 60s, 90s, 120s (capped)
3. After outline is written, post-outline buttons appear: **Approve Plan** / **Deny** / **Let's discuss**
4. "Let's discuss" sends a deny asking Claude to discuss the plan (action: `chat`)
5. Progressive cooldown on rapid retries: 30s, 60s, 90s, 120s (capped)

### Progressive cooldown

Expand Down
Loading
Loading