fix(work): import hashlib and wire heal subparser into main CLI
This commit is contained in:
+82
-15
@@ -13,6 +13,7 @@ Supports:
|
||||
- A/B/C choice prompts -> "A"
|
||||
- Numbered menus -> "1"
|
||||
- y/n confirmation prompts -> "y"
|
||||
- Interview navigate+select menus (cursor on 1 -> Enter)
|
||||
- Press Enter prompts -> "Enter"
|
||||
- Safety guardrails (passwords, passkeys, destructive commands are never auto-approved)
|
||||
4. State persistence & audit logging:
|
||||
@@ -137,6 +138,21 @@ DEFAULT_RULES: List[MatchRule] = [
|
||||
description="Confirms y/n at end of terminal line",
|
||||
press_enter=True,
|
||||
),
|
||||
MatchRule(
|
||||
id="interview_select",
|
||||
name="Interview Menu (cursor on 1)",
|
||||
pattern=(r"\?\s*\n"
|
||||
r"(?:[^\n]*\n){0,8}"
|
||||
r"[ \t]*(?:›|>)[ \t]*1\.[ \t]+\S[^\n]*\n"
|
||||
r"(?:[^\n]*\n){0,10}"
|
||||
r"[ \t]*2\.[ \t]+\S"),
|
||||
response_key="Enter",
|
||||
category="enter",
|
||||
enabled=True,
|
||||
description=("Selects highlighted option 1 on navigate+select "
|
||||
"menus (cursor on 1. + 2. + ?-question above)"),
|
||||
press_enter=False,
|
||||
),
|
||||
MatchRule(
|
||||
id="enter_to_continue",
|
||||
name="Press Enter to Continue",
|
||||
@@ -188,9 +204,22 @@ class AutoApproverState:
|
||||
try:
|
||||
with open(STATE_FILE) as f:
|
||||
data = json.load(f)
|
||||
return cls(**data)
|
||||
st = cls(**data)
|
||||
except Exception:
|
||||
return cls()
|
||||
# Migrate: append built-in rules missing from stored state (a new
|
||||
# default must reach the daemon without wiping operator toggles).
|
||||
try:
|
||||
have = {r.get("id") for r in st.rules
|
||||
if isinstance(r, dict)}
|
||||
missing = [asdict(r) for r in DEFAULT_RULES
|
||||
if r.id not in have]
|
||||
if missing:
|
||||
st.rules.extend(missing)
|
||||
st.save()
|
||||
except Exception:
|
||||
pass
|
||||
return st
|
||||
|
||||
|
||||
# =====================================================================
|
||||
@@ -300,6 +329,23 @@ def capture_pane_text(socket_path: str, pane_id: str, lines: int = 30) -> str:
|
||||
|
||||
MUSE_COMMAND_HINTS = ("muse-bin", "muse-code")
|
||||
|
||||
# (socket, pane) ever observed running a muse runtime. pane_current_command
|
||||
# flickers to the child tool while the agent works, so a muse pane stays
|
||||
# muse-owned when its foreground reads "python3" (observed live: the hint
|
||||
# gate missed tool-running panes and both daemons stacked 'y' answers).
|
||||
_MUSE_PANES_SEEN = set()
|
||||
|
||||
# tmux rule category -> muse watcher kind for verified sends. Text-input
|
||||
# categories verify render + submit with one retry; single-key widgets
|
||||
# (and unknown categories) stay blind.
|
||||
_CATEGORY_KIND_MAP = {
|
||||
"choice": "letter",
|
||||
"menu": "numbered",
|
||||
"confirm": "yn",
|
||||
"muse_code": "muse-approval",
|
||||
"enter": None,
|
||||
}
|
||||
|
||||
|
||||
def should_defer_to_muse_watcher(socket_path: str, pane_id: str,
|
||||
current_command: str) -> bool:
|
||||
@@ -309,16 +355,25 @@ def should_defer_to_muse_watcher(socket_path: str, pane_id: str,
|
||||
panes (stability + re-verify + once-per-prompt + decided-block
|
||||
guard). When its daemon is alive for this socket:pane, tmux must
|
||||
skip the pane entirely, or both daemons answer the same prompt
|
||||
within the same second ('11' + stray keys, observed live). Never
|
||||
raises: import or liveness failures mean no owner, handle here.
|
||||
within the same second ('11' + stray keys, observed live; later the
|
||||
same hole stacked 'y' answers when the foreground flickered to a
|
||||
child tool mid-poll). Never raises: import or liveness failures
|
||||
mean no owner, handle here.
|
||||
"""
|
||||
try:
|
||||
cmd = current_command or ""
|
||||
if not any(h in cmd for h in MUSE_COMMAND_HINTS):
|
||||
return False
|
||||
import muse_choice_watcher as mcw
|
||||
alive = getattr(mcw, "watcher_alive", mcw.is_running)
|
||||
return alive(socket_path, pane_id) is not None
|
||||
cmd = current_command or ""
|
||||
key = (socket_path, pane_id)
|
||||
if any(h in cmd for h in MUSE_COMMAND_HINTS):
|
||||
_MUSE_PANES_SEEN.add(key)
|
||||
alive = getattr(mcw, "watcher_alive", mcw.is_running)
|
||||
return alive(socket_path, pane_id) is not None
|
||||
if key in _MUSE_PANES_SEEN:
|
||||
alive = getattr(mcw, "watcher_alive", mcw.is_running)
|
||||
return alive(socket_path, pane_id) is not None
|
||||
# Never observed as muse: cheap pidfile check only (covers a
|
||||
# watcher racing ahead of our first observation of the pane).
|
||||
return mcw.is_running(socket_path, pane_id) is not None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -568,15 +623,25 @@ class AutoApproverRunner:
|
||||
})
|
||||
continue
|
||||
|
||||
# Execute key dispatch
|
||||
# Execute key dispatch through the verified send path:
|
||||
# literal text paced apart from Enter (a single-call
|
||||
# burst arrives as paste and lands a newline in
|
||||
# composers instead of submitting, then re-fires past
|
||||
# dedup and stacks). Text-input categories also verify
|
||||
# render + submit with one retry; single-key widgets
|
||||
# stay blind.
|
||||
success = False
|
||||
detail = {"verified": None, "retried": False}
|
||||
if not self.dry_run:
|
||||
args = ["send-keys", "-t", p.pane_id, verdict.key]
|
||||
if verdict.press_enter or verdict.key == "Enter":
|
||||
if verdict.key != "Enter":
|
||||
args.append("Enter")
|
||||
rc, _, _ = run_tmux_cmd(p.socket, *args)
|
||||
success = (rc == 0)
|
||||
import muse_choice_watcher as mcw
|
||||
want_enter = (verdict.key != "Enter"
|
||||
and bool(verdict.press_enter))
|
||||
ok, detail = mcw.send_answer(
|
||||
p.socket, p.pane_id, verdict.key,
|
||||
enter=want_enter,
|
||||
kind=_CATEGORY_KIND_MAP.get(verdict.category),
|
||||
sig=sig)
|
||||
success = bool(ok)
|
||||
else:
|
||||
success = True # dry-run simulated
|
||||
|
||||
@@ -597,6 +662,8 @@ class AutoApproverRunner:
|
||||
"excerpt": verdict.excerpt,
|
||||
"dry_run": self.dry_run,
|
||||
"success": success,
|
||||
"verified": detail["verified"],
|
||||
"retried": detail["retried"],
|
||||
}
|
||||
self.record_audit(event)
|
||||
actions_taken.append(event)
|
||||
|
||||
Reference in New Issue
Block a user