Skip to content

Telegram /plans: review, approve, decline & revise plans from chat - #324

Draft
serxa wants to merge 1 commit into
mainfrom
serxa/telegram-plans
Draft

Telegram /plans: review, approve, decline & revise plans from chat#324
serxa wants to merge 1 commit into
mainfrom
serxa/telegram-plans

Conversation

@serxa

@serxa serxa commented Aug 20, 2026

Copy link
Copy Markdown
Member

Problem

Plans (the propose → review → approve/decline/revise loop) could only be reviewed from the WebUI. On mobile / Telegram there was no way to see what's awaiting review or to act on it — you had to open the web panel. Meanwhile the approve/decline logic was duplicated between the HTTP route and the MCP tool handler (only revise was already shared via plan_service.py), so a third consumer would have meant a third copy.

Change

New Telegram /plans command — a chat-native review queue that mirrors the WebUI, following the /sessions conventions (PR #184 / #304):

  • /plans lists pending plans (⚙️ implementing shown after), paginated with ⬅️ Prev / ➡️ More and an always-present 🔄 Refresh. Empty queue says so.
  • Tap a plan → detail card: task title, status, version, proposed-at (user tz), and the plan body as an expandable blockquote (long plans collapse and expand in place; the card stays under Telegram's 4096 limit).
  • A pending plan gets ✅ Approve · ❌ Decline · ✍️ Revise:
    • Approve / Decline each show a one-tap confirm step (both are one-way: approve spawns an implementation session, decline closes the task).
    • Revise uses a native ForceReply prompt to collect feedback, then dispatches it to the planner — no copy-pasted ids anywhere; the plan id rides in callback_data.

Converge the three surfaces on one implementation. Extracted approve_plan and decline_plan into nerve/agent/plan_service.py (next to the existing request_plan_revision). The HTTP routes, the MCP plan_approve / plan_decline tools, and the new Telegram command are now thin adapters over the shared helpers — the same rationale the module's docstring already gives for revise ("keeping this in one place prevents the surfaces from drifting apart"). The impl session is now also registered with the engine on every surface, so /stop can cancel a stuck implementation (the MCP path previously couldn't).

Pure, unit-testable view builders (build_plans_view / build_plan_detail_view / build_plan_confirm_view) mirror build_sessions_view; the router gains thin list_plans / get_plan wrappers.

Behavior note (please confirm)

Routing PATCH /api/plans/{id} decline through the shared helper makes it refuse a non-pending plan with 409, matching every sibling surface (plan_decline tool, plan_approve, plan_update, POST …/approve). Previously that route was an unconditional field-updater. This is the same fix as the (policy-)closed #250; it changes no WebUI behavior (the UI only declines pending plans) and breaks no existing test. Happy to drop it back to a lenient path if you'd rather keep that route as-is.

Tests

  • tests/test_plan_actions.py (new) — the approve_plan / decline_plan contract: spawn + implementing + task→in_progress + engine registration; skill-prompt branch; refuses non-pending; not-found / task-not-found.
  • tests/test_telegram_plans.py (new) — the three view builders (labels, callback_data, HTML-escape, oversized-id skip, 4096 clamp, read-only non-pending) and _plans_view_for pending-first ordering + pagination.
  • Full suite: 3361 passed, 2 pre-existing unrelated failures (test_config_env scalar coercion, test_lockdown env flag) that reproduce on a clean main.
  • No frontend change (the WebUI already has plans); no npm run build needed.

Draft — behavior is best reviewed by driving /plans on the bot.

Adds a `/plans` command to the Telegram bot so plans can be reviewed
without the WebUI — the same actions the WebUI plan page offers, driven
by inline-keyboard taps (no copy-pasted plan ids).

Flow (mirrors the /sessions patterns):
- `/plans` lists pending plans (⚙️ implementing shown after), paginated
  ⬅️/➡️, always with a 🔄 Refresh button.
- Tap a plan → detail card (task, status, expandable plan body) with
  ✅ Approve · ❌ Decline · ✍️ Revise.
- Approve/Decline get a confirm step (both are one-way). Revise uses a
  native ForceReply prompt to collect feedback, then dispatches it to
  the planner.

Converge the three surfaces on one implementation. The approve/decline
logic was duplicated between the HTTP route and the MCP tool handler
(revise was already shared). Extracted `approve_plan` / `decline_plan`
into `nerve/agent/plan_service.py` alongside `request_plan_revision`;
the HTTP routes, MCP tools, and the new Telegram command are now thin
adapters over it. Also registers the spawned impl task with the engine
on every surface so `/stop` can cancel a stuck implementation (the MCP
path previously didn't).

Note: routing HTTP `PATCH /api/plans/{id}` decline through the shared
helper makes it refuse a non-pending plan with 409, matching every
sibling surface (previously the route was an unconditional field
updater). No frontend change — the WebUI only declines pending plans.

Tests: tests/test_plan_actions.py (approve/decline service contract) and
tests/test_telegram_plans.py (view builders + pagination). Full suite
3361 passed; 2 pre-existing unrelated failures (config-env scalar
coercion, lockdown env flag) reproduce on a clean main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant