What changed, and why
A standing command — "always categorize X as Y" — grants the strongest thing the agent can hold: standing autonomy to auto-apply a payee. handle_command granted it inline: it parsed the message and signalled the registry's bless straight away, minting a human_explicit auto-apply rule with no confirmation.
SPEC §0.6 and §5c are explicit that this is the wrong shape. High-impact verbs — standing rules, trust elevation — must be echoed back and confirmed with a one-word reply. And §5c notes the sharp edge: a command can arrive on a brand-new thread, where the allow-list is the only gate. A misread parse, or a mistaken send, must not silently grant auto-apply.
So the command now goes through a read-back: echo the interpretation, wait for a one-word yes, and only then bless.
The command opens a confirm, instead of blessing
handle_command no longer signals bless. It parses the command as before, then starts a one-shot confirm workflow keyed by (payee, category). The bless moves into that workflow, behind the owner's yes.
Parse → start CommandConfirmWorkflow. ALLOW_DUPLICATE + catching the already-started error makes a resend a no-op while one is pending.
src/ynab_agent/workflow/dispatch_activities.py · 239 lines
⋯ 180 lines hidden (lines 1–180)
⋯ 1 line hidden (lines 239–239)
The confirm workflow — a mirror of the proactive offer
The confirm workflow is deliberately the twin of the proactive AutonomyOfferWorkflow: open a read-back thread, stamp the shared OfferThreadId so W3 routes the reply back, then wait — a yes blesses, a no declines, an unclear reply keeps waiting, and silence past the window blesses nothing. The one difference from the offer is what a yes blesses: an explicit (payee, category) command rather than an already-eligible rule.
Open → stamp the shared routing attribute → wait → interpret → accept/decline. The reply interpreter is reused verbatim from the offer.
src/ynab_agent/workflow/command_workflow.py · 135 lines
⋯ 85 lines hidden (lines 1–85)
Blessing only on a yes
The accept activity is where the bless finally happens — the same registry bless signal handle_command used to send inline, now reached only after the owner confirmed. The read-back and the confirmation prose round out the flow.
open_command_thread echoes the command; accept_command signals the registry bless and confirms — the inline bless, now gated.
src/ynab_agent/workflow/command_activities.py · 149 lines
⋯ 49 lines hidden (lines 1–49)
⋯ 20 lines hidden (lines 61–80)
⋯ 37 lines hidden (lines 113–149)
The read-back: echoes the interpretation and asks for a one-word YES (SPEC §0.6).
src/ynab_agent/agentic/compose.py · 201 lines
⋯ 89 lines hidden (lines 1–89)
⋯ 97 lines hidden (lines 105–201)
Tests
The workflow is pinned end to end on the time-skipping server: a yes blesses, a no declines, an unclear-then-yes blesses, and silence blesses nothing. And the activity-level test proves the behavioural change directly — a bless command now starts the confirm workflow, never a direct registry bless.
Yes blesses; silence never does.
tests/workflow/test_command_workflow.py · 175 lines
⋯ 132 lines hidden (lines 1–132)
⋯ 17 lines hidden (lines 159–175)
handle_command opens a confirm, not an inline bless.