Skip to content

feat(channels): answer channels β€” ask a human from the loop without stopping it - #80

Open
caoergou wants to merge 1 commit into
mainfrom
claude/loop-external-task-integration-5vpst3
Open

caoergou wants to merge 1 commit into
mainfrom
claude/loop-external-task-integration-5vpst3

Conversation

@caoergou

@caoergou caoergou commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Implements the Layer 6 foundations designed in #53 (ship-order steps 1 + 2: notify-first, then the reply poller). The loop can now push a DECIDE question to your phone at 11 p.m., and your one-tap reply un-wedges the run instead of the whole night dying.

What's in

questions.py β€” the structured question protocol

  • Agents may precede <promise>DECIDE:…</promise> with a <question>{JSON}</question> block (title, context, options with one-line consequences, recommendation). The engine enriches it with mechanical facts it owns (spec, iteration, branch) β€” same trust boundary as feat(engine): deterministic verification gate β€” engine must own commit, push, and spec completionΒ #32.
  • Enforced quality contract (ensure_quality): β‰₯2 mutually exclusive options, exactly one recommended, mandatory no-answer default, context/evidence budgets. A bare old-format DECIDE:text is synthesized into the safe binary ("proceed as proposed" / "skip") and marked low-fidelity β€” never posted raw.
  • Files first, transport second: questions persist as JSON under .owloop/questions/ regardless of channel health; answers become attributed, fenced per-spec steering under .owloop/steering/<spec>.md, which the next iteration loads into its prompt (guidance data, never raw prompt splicing).

channels.py β€” transports for a dev machine with no public endpoint

  • Telegram (bidirectional): stdlib-urllib sendMessage with inline-keyboard buttons (callback_data kept ≀64 bytes), getUpdates long-poll, ack-by-edit via editMessageText, chat-id allowlist with silent drop of everyone else.
  • ntfy.sh (notify-only): questions, blockers, and the end-of-run summary as pushes.
  • Night mode (quiet hours 22:00–08:00): every question renders as the recommendation-first binary β€” πŸ‘ Go ahead / ⏭️ Skip β€” because both answers are safe (worktree branch only, nothing merges overnight), and the message says so. Day mode sends the full option list. Truncation only ever touches context; options and the default line are the decision and are never cut.
  • Reply correlation ladder: button callback β†’ question id in text β†’ bare option key only when exactly one question is pending. Ambiguity matches nothing β€” never guess. /skip and /steer <text> verbs supported; first valid reply wins.

Engine integration

  • DECIDE now raises a question, posts it, and waits a bounded --question-wait (default 30 min) for an answer. Answered β†’ the run continues: the next iteration retries the spec with the human's decision injected as steering (fresh-context makes resume free β€” the spec queue is the checkpointer). Skip/expiry β†’ the previous stop-for-review behavior, with the full question preserved for the morning.
  • BLOCKED and run-completion send notifications; the inter-iteration sleep doubles as the reply long-poll.
  • Non-interruption invariant: all channel I/O is best-effort β€” transport exceptions are caught, emitted as channel_error events, and the loop proceeds. A dead Telegram API costs delivery, never correctness (covered by an exploding-channel test).

CLI

export OWLOOP_TELEGRAM_BOT_TOKEN=... OWLOOP_TELEGRAM_CHAT_ID=...
owloop run --channels telegram --question-wait 30

Env validation happens before the loop starts, not at 2 a.m.

Deliberately out of scope

Verification

  • uv run pytest -q β€” 294 passed (36 new: quality contract, night/day rendering rules, correlation ladder, allowlist drop, Telegram transport against mocked urllib, engine answered/skip/expired/transport-failure paths)
  • uv run ruff check src/owloop tests β€” clean
  • uv run mypy src/owloop tests β€” clean

Closes nothing yet; advances #53 (Phase III.5).

πŸ€– Generated with Claude Code

https://claude.ai/code/session_016mrgfGebBKR3xGfywkqj3w


Generated by Claude Code

…topping it

Implements the Layer 6 foundations from #53 (ship-order steps 1+2):

- questions.py: structured <question> protocol with an enforced quality
  contract β€” >=2 mutually exclusive options with one-line consequences,
  exactly one recommendation, mandatory no-answer default. Bare
  DECIDE:text payloads are synthesized into a safe low-fidelity binary.
  Questions persist as JSON under .owloop/questions/ (files first,
  transport second); answers become attributed per-spec steering under
  .owloop/steering/ that the next iteration loads into its prompt.
- channels.py: AnswerChannel transports chosen for dev machines with no
  public endpoint. Telegram (stdlib urllib, getUpdates long-poll,
  inline-keyboard buttons, ack-by-edit, chat-id allowlist with silent
  drop) is bidirectional; ntfy.sh is notify-only. Night mode (quiet
  hours 22:00-08:00) renders every question as the recommendation-first
  binary so both possible answers are safe; day mode sends the full
  option list. Reply correlation ladder: button callback -> id in text
  -> bare key only when exactly one question is pending (never guess).
- engine: DECIDE now raises a question, posts it to channels, and waits
  a bounded --question-wait for an answer; an answer un-wedges the run
  (the next iteration retries the spec with the decision as steering)
  instead of stopping the whole night. BLOCKED and run-completion send
  notifications. The inter-iteration sleep doubles as the reply
  long-poll. All channel I/O is best-effort: a dead transport costs
  delivery, never the loop.
- CLI: owloop run --channels telegram,ntfy --question-wait N, with env
  validation (OWLOOP_TELEGRAM_BOT_TOKEN/CHAT_ID, OWLOOP_NTFY_TOPIC)
  before the loop starts.
- 36 new tests: quality contract, rendering rules (options never
  truncated), correlation, allowlist drop, transport-failure tolerance,
  and engine integration incl. answered/skip/expired paths.

Park-and-continue (one question costs one spec, not the night) is
deliberately deferred until #33's terminal-state taxonomy lands.
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