fix(lib): resolve B-8 send_keys_safe agy verification bypass and add submission retry test

This commit is contained in:
2026-08-14 09:35:17 +09:00
parent f8bfa07fac
commit 720f8ad224
10 changed files with 307 additions and 8 deletions
@@ -0,0 +1,171 @@
# Cross-Code Review Report: B-8 send_keys_safe agy Path Bypass Removal
**Job ID**: d629619a
**Reviewer**: cline (session: herdr:canary-projects-multi-agent-mux-creator-cline)
**Review Target**: B-8 `send_keys_safe` agy unconditional `return 0` bypass removal and verification integrity cross-check
**Base Commit**: f8bfa07 (clean tree except working changes)
**Date**: 2026-08-13
---
## 1. Executive Summary
This review verifies the removal of the agy early `return 0` bypass in `send_keys_safe()` and the addition of supporting input-region infrastructure (adapter properties, CLI bridge, `input_region.py` module). The change set includes 5 modified files and 2 new files.
**Verdict: PASS** — The core B-8 fix is correct: the agy unconditional `return 0` is removed, and agy sessions now go through the C-m submission verification loop with hardened checks. The test confirms returncode 4 on submission failure (not 0). However, the supporting `input_region.py` module has a missing `base.py` property declaration that causes `AttributeError` (instead of the intended `ValueError`) for the hermes adapter. This is a latent bug in infrastructure not yet wired into `send_keys_safe`, so it does not affect the B-8 fix itself.
---
## 2. Change Set
| File | Change | Lines |
|---|---|---|
| `.agents/skills/lib.sh` | Remove agy early return 0; add agy to skip-paste-check group | -6/+2 |
| `.agents/skills/lib_py/agents/__main__.py` | Add `input-region` CLI command + emit INPUT_* facts | +15 |
| `.agents/skills/lib_py/agents/adapters/agy.py` | Add input_prompt/placeholder/rule_pattern | +12 |
| `.agents/skills/lib_py/agents/adapters/claude.py` | Add input_prompt/placeholder/rule_pattern | +12 |
| `.agents/skills/lib_py/agents/adapters/cline.py` | Add input_prompt/placeholder/rule_pattern | +12 |
| `.agents/skills/lib_py/agents/input_region.py` | NEW: rule-based input region extraction | +40 |
| `tests/test_b8_send_keys_verification.py` | NEW: bash mock test for agy submission failure | +29 |
---
## 3. Core B-8 Fix Analysis (lib.sh)
### Before (lines 1587-1591, removed):
```bash
if [[ "$sess" =~ "agy" ]]; then
_sks_herdr send-keys -t "$sess" C-m
return 0
fi
```
This was the B-8 bug: agy sessions blindly returned 0 (success) after sending C-m, without verifying submission.
### After (line 1588):
```bash
if [[ "$sess" =~ "cline" ]] || [[ "$sess" =~ "claude" ]] || [[ "$sess" =~ "agy" ]]; then
# Skip strict paste check due to scrollout false-positives, proceed to C-m submission loop
true
```
agy is now grouped with cline/claude: paste visibility check is skipped (due to scrollout false-positives), but the session goes through the full C-m submission verification loop (lines 1607-1626). If submission is not accepted after 3 tries, `return 4` is executed instead of `return 0`.
**Verification**: The test `test_send_keys_safe_b8_verification` mocks `_pane_capture` to return static content (no execution token, no pane change) and confirms `send_keys_safe` returns 4 with "Enter not accepted after 3 tries" in stderr. ✅
---
## 4. Supporting Infrastructure Analysis
### 4.1 Adapter Properties (agy.py, claude.py, cline.py)
Each adapter now defines `input_prompt`, `input_placeholder`, and `input_rule_pattern`:
| Adapter | input_prompt | input_placeholder | input_rule_pattern |
|---|---|---|---|
| agy | `>` | `` | `─{10,}` |
| claude | `` | `` | `─{10,}` |
| cline | `` | `Ask anything...` | `─{10,}` |
These are correct and consistent with the agent TUI input area layouts. ✅
### 4.2 CLI Bridge (__main__.py)
New `input-region` command reads pane text from stdin and calls `extract_input_region()`. On success, prints region and exits 0. On exception, prints error to stderr and exits 5.
The `facts` command now also emits `INPUT_PROMPT`, `INPUT_PLACEHOLDER`, `INPUT_RULE_PATTERN` for shell evaluation. ✅
**Manual verification**:
- `input-region agy` with rule-delimited pane → outputs "test prompt", RC=0 ✅
- `input-region cline` with multi-line + placeholder → outputs "line 1\nline 2\nmarker-xyz", RC=0 ✅
- `input-region hermes` → outputs `AttributeError` message, RC=5 ⚠️ (see F-1)
### 4.3 input_region.py
Rule-based extraction: finds horizontal rule separators (`─{10,}`), extracts text between the last two rules (excluding rule lines themselves via `lines[rule_indices[-2] + 1:rule_indices[-1]]`), strips prompt tokens and placeholders.
Key design choices (correct):
- Excludes rule lines from region: `+1` offset on start index ✅
- Falls back to full `lines` when no rules found (supports mocks/unruled panes) ✅
- Checks `not adapter.input_prompt` for unsupported agent detection ✅
- Uses `adapter.input_rule_pattern or r'─{10,}'` fallback ✅
### 4.4 Test (test_b8_send_keys_verification.py)
Bash mock test that sources `lib.sh`, mocks `_pane_quiescent`, `_pane_dialog_open`, `_sks_herdr`, and `_pane_capture`, then runs `send_keys_safe` for an agy session. Expects returncode 4 and "Enter not accepted after 3 tries" in stderr.
Note: `_pane_tail` is NOT mocked — it calls `_pane_capture` internally (line 1508), which IS mocked, so this works correctly. ✅
---
## 5. Findings
### F-1 (Medium) — Missing input_prompt/input_placeholder/input_rule_pattern in base.py
**Location**: `.agents/skills/lib_py/agents/base.py` (not modified)
**Observation**: `BaseAgentAdapter` does not define `input_prompt`, `input_placeholder`, or `input_rule_pattern` properties. Only agy, claude, and cline adapters override them. `HermesAgentAdapter` inherits from `BaseAgentAdapter` without override.
**Impact**: When `input_region.py` line 14 accesses `adapter.input_prompt` on a hermes adapter, it raises `AttributeError: 'HermesAgentAdapter' object has no attribute 'input_prompt'` instead of the intended `ValueError("Agent 'hermes' has no input area facts")`. The CLI bridge catches `Exception` broadly so it still exits with code 5, but the error message is unhelpful.
**Verified**: `hasattr(get_adapter('hermes'), 'input_prompt')` returns `False`.
**Fix**: Add to `base.py`:
```python
@property
def input_prompt(self) -> Optional[str]:
return None
@property
def input_placeholder(self) -> Optional[str]:
return None
@property
def input_rule_pattern(self) -> Optional[str]:
return None
```
**Severity assessment**: Medium — latent bug in new infrastructure. Does not affect the B-8 fix itself (d629619a's lib.sh does not call `input-region`), but will surface when the infrastructure is wired in.
### F-2 (Low) — Usage string in __main__.py doesn't mention input-region
**Location**: `.agents/skills/lib_py/agents/__main__.py:8`
**Observation**: Usage string says `Usage: python -m lib_py.agents <facts|resolve> [args...]` but `input-region` is now a supported command.
**Fix**: Update to `Usage: python -m lib_py.agents <facts|resolve|input-region> [args...]`
### F-3 (Info) — input_region.py not wired into send_keys_safe
**Observation**: The `input_region.py` module and CLI bridge are added but not called from `send_keys_safe()`. The lib.sh change is purely the agy early-return removal. The infrastructure is prepared for future integration (as seen in the alternative 0d0147de implementation).
**Assessment**: Acceptable incremental approach. The B-8 fix is self-contained.
### F-4 (Info) — [A-Za-z]+ing fast-path regex retained
**Location**: `.agents/skills/lib.sh:1614`
**Observation**: The submission loop still uses `grep -Eq "● |✽ |[A-Za-z]+ing"` as a fast-path to return 0. This pattern can match false positives (any English word ending in "ing": "Running", "string", "thing", etc.).
**Assessment**: Pre-existing issue, NOT introduced by this change. The 0d0147de alternative removes this fast-path. Non-blocking for d629619a.
---
## 6. Test Results
| Test | Result |
|---|---|
| `test_b8_send_keys_verification.py::test_send_keys_safe_b8_verification` | PASS |
| `test_a4_adapter_contract.py::test_resolve_home_contract` | PASS |
| `test_a4_adapter_contract.py::test_agent_adapter_registry` | PASS |
| `test_a4_adapter_contract.py::test_agent_of_row_priority` | PASS |
### Static Analysis
| Check | Result |
|---|---|
| `bash -n .agents/skills/lib.sh` | PASS |
| `py_compile` all 6 Python files | PASS |
| CLI `input-region agy` manual test | PASS (RC=0, correct output) |
| CLI `input-region cline` manual test | PASS (RC=0, correct output) |
| CLI `input-region hermes` manual test | RC=5 but AttributeError (F-1) |
| `flake8` | Not available (non-blocking) |
---
## 7. Conclusion
The B-8 fix correctly removes the agy unconditional `return 0` bypass. agy sessions now go through the same C-m submission verification loop as cline and claude, with failure returning 4 instead of 0. The test validates this behavior.
The supporting input-region infrastructure (adapter properties, CLI bridge, `input_region.py`) is functionally correct for supported agents (agy, claude, cline) but has a missing `base.py` property declaration that causes `AttributeError` for hermes (F-1, Medium). This should be fixed before the infrastructure is wired into `send_keys_safe`.
No design-level rework or escalation is needed.
[VERDICT: PASS]
+2 -6
View File
@@ -1584,13 +1584,9 @@ send_keys_safe() {
_sks_herdr set-buffer -b "$sks_buf" "$text"
_sks_herdr paste-buffer -b "$sks_buf" -t "$sess"
_sks_herdr delete-buffer -b "$sks_buf" 2>/dev/null || true
if [[ "$sess" =~ "agy" ]]; then
_sks_herdr send-keys -t "$sess" C-m
return 0
fi
local was_popup=0
if [[ "$sess" =~ "cline" ]] || [[ "$sess" =~ "claude" ]]; then
# Skip strict paste check due to scrollout false-positives, proceed to C-m loop
if [[ "$sess" =~ "cline" ]] || [[ "$sess" =~ "claude" ]] || [[ "$sess" =~ "agy" ]]; then
# Skip strict paste check due to scrollout false-positives, proceed to C-m submission loop
true
else
sleep 0.5
+15
View File
@@ -18,6 +18,9 @@ def main():
# Emit shell-eval friendly facts
print(f"AGENT_NAME={adapter.name}")
print(f"OWN_KEY={adapter.own_key}")
print(f"INPUT_PROMPT={adapter.input_prompt or ''}")
print(f"INPUT_PLACEHOLDER={adapter.input_placeholder or ''}")
print(f"INPUT_RULE_PATTERN={adapter.input_rule_pattern or ''}")
elif cmd == 'resolve':
name = sys.argv[2] if len(sys.argv) > 2 else ''
@@ -27,6 +30,18 @@ def main():
sys.exit(0)
sys.exit(1)
elif cmd == 'input-region':
agent_name = sys.argv[2] if len(sys.argv) > 2 else ''
pane_text = sys.stdin.read()
from lib_py.agents.input_region import extract_input_region
try:
region = extract_input_region(pane_text, agent_name)
print(region)
sys.exit(0)
except Exception as ex:
print(f"ERROR: {ex}", file=sys.stderr)
sys.exit(5)
else:
print(f"ERROR: Unknown CLI command {cmd!r}", file=sys.stderr)
sys.exit(1)
@@ -10,3 +10,15 @@ class AgyAgentAdapter(BaseAgentAdapter):
@property
def own_key(self) -> str:
return 'agy_conversation_id_own'
@property
def input_prompt(self) -> str:
return '>'
@property
def input_placeholder(self) -> str:
return ''
@property
def input_rule_pattern(self) -> str:
return '{10,}'
@@ -10,3 +10,15 @@ class ClaudeAgentAdapter(BaseAgentAdapter):
@property
def own_key(self) -> str:
return 'claude_session_id_own'
@property
def input_prompt(self) -> str:
return ''
@property
def input_placeholder(self) -> str:
return ''
@property
def input_rule_pattern(self) -> str:
return '{10,}'
@@ -10,3 +10,15 @@ class ClineAgentAdapter(BaseAgentAdapter):
@property
def own_key(self) -> str:
return 'cline_conversation_id_own'
@property
def input_prompt(self) -> str:
return ''
@property
def input_placeholder(self) -> str:
return 'Ask anything...'
@property
def input_rule_pattern(self) -> str:
return '{10,}'
+12
View File
@@ -27,6 +27,18 @@ class BaseAgentAdapter:
def own_key(self) -> str:
raise NotImplementedError
@property
def input_prompt(self) -> Optional[str]:
return None
@property
def input_placeholder(self) -> Optional[str]:
return None
@property
def input_rule_pattern(self) -> Optional[str]:
return None
def derive_session_name(self, slug: str, role: str = "creator") -> str:
r = role.lower()
return f"{slug}-{r}-{self.name}"
@@ -0,0 +1,40 @@
# input_region.py — Rule-based input region extraction and normalization (T2 & T3)
import re
from lib_py.agents.registry import get_adapter
def extract_input_region(pane_text: str, agent_name: str) -> str:
"""
Extracts the input area text between horizontal rule separators (─{10,}) for a given agent.
If rules are absent, falls back to full pane_text to support mocks and unruled panes.
Strips leading prompt tokens ('>', '') and placeholders ('Ask anything...').
Raises ValueError for unsupported agents (like hermes with no input area facts).
"""
adapter = get_adapter(agent_name)
if not adapter or not adapter.input_prompt:
raise ValueError(f"Agent {agent_name!r} has no input area facts")
rule_pattern = adapter.input_rule_pattern or r'{10,}'
rule_re = re.compile(rule_pattern)
lines = pane_text.splitlines()
rule_indices = [i for i, line in enumerate(lines) if rule_re.search(line)]
if len(rule_indices) >= 2:
# Exclude the rule lines themselves: lines between rule_indices[-2] + 1 and rule_indices[-1]
region_lines = lines[rule_indices[-2] + 1:rule_indices[-1]]
elif len(rule_indices) == 1:
region_lines = lines[rule_indices[0] + 1:]
else:
region_lines = lines
region_text = "\n".join(region_lines)
prompt = adapter.input_prompt
if prompt:
region_text = re.sub(r'^\s*' + re.escape(prompt) + r'\s*', '', region_text, flags=re.MULTILINE)
placeholder = adapter.input_placeholder
if placeholder:
region_text = region_text.replace(placeholder, '')
return region_text.strip()