feat: add conversational replies and stable output
Some checks failed
tamq-ci / test (push) Failing after 6s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a03397-4d51-7fd1-8ff2-946eb22ea2bc
This commit is contained in:
tegwick 2026-08-24 23:03:30 +02:00
parent 704eeea5c1
commit a4ba6e32e5
17 changed files with 363 additions and 59 deletions

View file

@ -77,6 +77,18 @@ The spelling without the trailing colon is equivalent:
@audit-core please review the auth boundary
```
After receiving a message, use the bare `@` command to answer its sender:
```bash
@ Thanks, I will take a look.
```
Bare `@` selects the sender of the latest durable inbound message for the
current repository, including an already acknowledged message. Self-addressed
messages are ignored. If this window has no prior counterparty, the command
reports an error and queues nothing. The explicit equivalent is `tamq reply
MESSAGE...`.
These are tamq-owned executable commands beside the installed `tamq` command,
not shell-specific aliases. Set `TAMQ_COMMAND_DIR` before startup to select a
different writable command directory already present on your shell's `PATH`.
@ -94,10 +106,14 @@ With normal `output` delivery, the target pane visibly receives:
```
This uses the pane's tmux-reported `/dev/pts/<number>` device—the same Unix
terminal-output mechanism underlying tools such as `write(1)`. It does not use
`send-keys`, send Enter, or place bytes on the foreground process's stdin.
Output can visually interleave with a prompt and a full-screen program may
redraw over it, so the durable inbox remains authoritative.
terminal-output mechanism underlying tools such as `write(1)`. For an ordinary
shell with screen rows above its cursor, tamq confines scrolling to those rows,
writes the comment immediately above the input row, and restores the cursor.
Thus a partially typed command remains in place. If no safe row exists, tamq
uses ordinary line output; alternate-screen programs receive the conservative
fallback and may redraw over it. Neither path uses `send-keys`, sends Enter, or
places bytes on the foreground process's stdin, so the durable inbox remains
authoritative.
In the `audit-core` window, inspect and acknowledge it:

View file

@ -17,8 +17,8 @@ tamq does not choose or infer them.
acknowledgements, replay, export, and bounded purging.
- Managed neutral-shell tmux lifecycle and explicit initial commands.
- Durable manual send/inbox/acknowledgement with per-window repository identity,
shell-native address commands, comment-safe display, and explicit pull-time
filters.
shell-native direct and latest-counterparty reply commands, comment-safe
display, and explicit pull-time filters.
- Sanitized one-time output notifications through target tmux pane PTYs, with
inbox-only delivery as an explicit option and no foreground-process input.
- Explicit opt-in control-mode pane delivery and the full-duplex `tamq tap` PTY
@ -48,11 +48,11 @@ transport.
| Intent capability | State | Evidence and remaining gap |
| --- | --- | --- |
| Direct repository addressing | Implemented for local alpha | Exact `gita` validation, per-session `@repo`/`@repo:` executable commands, and long-form parsing are covered without modifying shell configuration. |
| Direct repository addressing | Implemented for local alpha | Exact `gita` validation, per-session `@repo`/`@repo:` commands, bare `@` latest-counterparty replies, and long-form parsing are covered without modifying shell configuration. |
| Durable, inspectable local queue | Implemented | SQLite history, manual inbox, leases, endpoint records, inspect/history, JSONL export/replay, acknowledgement, and purge are present. |
| Local socket service | Implemented | Peer-credential checks and structured ping/register/send/history/ack/endpoints/disconnect operations are tested. |
| Neutral tmux session lifecycle | Implemented for local alpha | Repository-first startup opens untouched shells at exact gita paths, exports per-window identity, and runs no initial command unless `--command` is explicit. Stable reuse, service restart, and cleanup are covered by the installed-package test. |
| Safe manual messaging | Implemented for local alpha | Normal endpoints write one sanitized comment to the target PTY output without injecting stdin; messages remain pending until acknowledgement. Inbox-only manual mode is explicit with `--no-display`, and legacy rows migrate to manual mode. |
| Safe manual messaging | Implemented for local alpha | Normal endpoints write one sanitized comment above a stable shell input row without injecting stdin; conservative fallback handles the first row and alternate screens. Messages remain pending until acknowledgement. Inbox-only manual mode is explicit with `--no-display`. |
| Full-duplex input observation | Explicit opt-in | `--tap --command ...` enables the PTY integration path. It is absent from neutral startup and remains covered for geometry, resize, raw mouse input, and lifecycle behavior. |
| Bounded retry behavior | Not enforced | Failed output or injection remains pending and becomes claimable after lease expiry, but no attempt counter or terminal failure state applies the configured cap. |
| Acknowledgement policy | Partially implemented | Terminal output remains pending until explicit acknowledgement, while legacy pane injection becomes `injected`; the configured `delivery_ack_mode` does not yet govern both paths. |
@ -64,7 +64,8 @@ The terminal-neutral alpha path was exercised successfully on 2026-08-24 with
an isolated installed tool. Repository-first startup created two ordinary
shells at exact gita paths without sending initial keystrokes. The test proved
per-window repository identity, stable reuse, shell-native addressing,
comment-safe target output and inbox/filter/ack exchange, zero target-input
latest-counterparty replies, comment-safe target output above an unchanged
partial input line and cursor, inbox/filter/ack exchange, zero target-input
mutation, service restart, endpoint disappearance, explicit initial-command
startup, and cleanup.
@ -86,7 +87,7 @@ Not yet suitable:
and stronger process-supervision evidence.
- Cross-host messaging or use as a general-purpose broker.
The suite currently has 93 passing tests and 76% statement coverage. Coverage
The suite currently has 100 passing tests and 76% statement coverage. Coverage
is strongest in durable storage and registry handling, and weakest in the PTY
tap and CLI orchestration; PTY statement coverage increased from 23% to 33%,
while subprocess behavior is primarily proven by the real-tmux test. The

View file

@ -16,7 +16,7 @@
| workplan | TAMQ-WP-0005 | finished | — | workplans/TAMQ-WP-0005-terminal-neutral-manual-messaging.md |
| workplan | TAMQ-WP-0006 | finished | — | workplans/TAMQ-WP-0006-shell-native-message-routing.md |
| workplan | TAMQ-WP-0007 | finished | — | workplans/TAMQ-WP-0007-terminal-output-notifications.md |
| workplan | TAMQ-WP-0008 | active | — | workplans/TAMQ-WP-0008-reply-shorthand-stable-output.md |
| workplan | TAMQ-WP-0008 | finished | — | workplans/TAMQ-WP-0008-reply-shorthand-stable-output.md |
| task | TAMQ-WP-ADHOC-2026-08-24-T01 | done | — | workplans/ADHOC-2026-08-24.md |
| task | TAMQ-WP-0001-T01 | done | — | workplans/TAMQ-WP-0001-statehub-bootstrap.md |
| task | TAMQ-WP-0001-T02 | done | — | workplans/TAMQ-WP-0001-statehub-bootstrap.md |
@ -47,7 +47,7 @@
| task | TAMQ-WP-0007-T03 | done | — | workplans/TAMQ-WP-0007-terminal-output-notifications.md |
| task | TAMQ-WP-0007-T04 | done | — | workplans/TAMQ-WP-0007-terminal-output-notifications.md |
| task | TAMQ-WP-0007-T05 | done | — | workplans/TAMQ-WP-0007-terminal-output-notifications.md |
| task | TAMQ-WP-0008-T01 | progress | — | workplans/TAMQ-WP-0008-reply-shorthand-stable-output.md |
| task | TAMQ-WP-0008-T02 | todo | — | workplans/TAMQ-WP-0008-reply-shorthand-stable-output.md |
| task | TAMQ-WP-0008-T03 | todo | — | workplans/TAMQ-WP-0008-reply-shorthand-stable-output.md |
| task | TAMQ-WP-0008-T04 | todo | — | workplans/TAMQ-WP-0008-reply-shorthand-stable-output.md |
| task | TAMQ-WP-0008-T01 | done | — | workplans/TAMQ-WP-0008-reply-shorthand-stable-output.md |
| task | TAMQ-WP-0008-T02 | done | — | workplans/TAMQ-WP-0008-reply-shorthand-stable-output.md |
| task | TAMQ-WP-0008-T03 | done | — | workplans/TAMQ-WP-0008-reply-shorthand-stable-output.md |
| task | TAMQ-WP-0008-T04 | done | — | workplans/TAMQ-WP-0008-reply-shorthand-stable-output.md |

View file

@ -31,7 +31,7 @@ from .terminal import format_comment
SUBCOMMANDS = frozenset(
"start attach serve stop status ping inbox history inspect ack send export replay purge completion db-version config tap".split()
"start attach serve stop status ping inbox history inspect ack send reply export replay purge completion db-version config tap".split()
)
START_OPTIONS = frozenset({"--command", "--cmd", "--tap", "--detach", "--no-service", "--no-display"})
GLOBAL_FLAGS = frozenset({"--orwell", "--verbose"})
@ -162,6 +162,7 @@ def build_parser() -> argparse.ArgumentParser:
manual messaging from a managed shell:
@TARGET: MESSAGE...
@ MESSAGE... reply to the latest sender for this window
tamq inbox [--filter COMMAND]
""",
)
@ -206,6 +207,8 @@ manual messaging from a managed shell:
send.add_argument("body", nargs="*", help="message body when address is a repo slug")
send.add_argument("--endpoint-id")
send.add_argument("--from", dest="sender_repo", help="sender repository (default: TAMQ_REPO or local)")
reply = subparsers.add_parser("reply", help="reply to the latest sender for the current repository")
reply.add_argument("body", nargs="+", help="message body")
export = subparsers.add_parser("export", help="export history as JSONL")
export.add_argument("--output", required=True)
export.add_argument("--repo", dest="target_repo")
@ -371,15 +374,31 @@ def main(argv: list[str] | None = None) -> int:
store = Store(db_path())
try:
advisory = history_advisory(store)
if advisory and args.command in {"start", "serve", "status", "history", "inbox", "send"}:
if advisory and args.command in {"start", "serve", "status", "history", "inbox", "send", "reply"}:
print(advisory, file=sys.stderr)
if args.command == "send":
text = " ".join([args.address, *args.body]).strip()
if not text.startswith("@") or ":" not in text:
print("tamq send expects @repo: message", file=sys.stderr); return 2
target, body = text[1:].split(":", 1); body = body.strip()
if not target or not body: print("tamq send requires a target and body", file=sys.stderr); return 2
sender = args.sender_repo or os.environ.get("TAMQ_REPO") or "local"
if args.command in {"send", "reply"}:
endpoint_id = None
if args.command == "reply":
sender = os.environ.get("TAMQ_REPO")
if not sender:
print("tamq reply requires a managed window with TAMQ_REPO", file=sys.stderr)
return 2
body = " ".join(args.body).strip()
target = store.latest_counterparty(sender)
if target is None:
print(f"tamq: no counterparty has sent a message to {sender}", file=sys.stderr)
return 2
else:
text = " ".join([args.address, *args.body]).strip()
if not text.startswith("@") or ":" not in text:
print("tamq send expects @repo: message", file=sys.stderr); return 2
target, body = text[1:].split(":", 1); body = body.strip()
if not target or not body: print("tamq send requires a target and body", file=sys.stderr); return 2
sender = args.sender_repo or os.environ.get("TAMQ_REPO") or "local"
endpoint_id = args.endpoint_id
if not body:
print("tamq reply requires a message body", file=sys.stderr)
return 2
try:
validate_targets([target])
if sender != "local":
@ -388,13 +407,13 @@ def main(argv: list[str] | None = None) -> int:
print(f"tamq: {exc}", file=sys.stderr); return 2
if asyncio.run(ping()):
payload = {"op": "send", "sender_repo": sender, "target_repo": target, "body": body}
if args.endpoint_id:
payload["endpoint_id"] = args.endpoint_id
if endpoint_id:
payload["endpoint_id"] = endpoint_id
response = asyncio.run(request(payload))
if not response.get("ok"):
print(f"tamq: {response.get('error', 'send failed')}", file=sys.stderr); return 1
print(response["message_id"]); return 0
if args.endpoint_id:
if endpoint_id:
print("tamq: service is not running", file=sys.stderr); return 1
try:
print(store.add(sender, target, body))

View file

@ -10,6 +10,15 @@ class ControlModeError(RuntimeError):
pass
@dataclass
class PaneDisplay:
tty_path: str
cursor_x: int
cursor_y: int
pane_height: int
alternate_on: bool
@dataclass
class ControlModeClient:
session: str
@ -35,16 +44,32 @@ class ControlModeClient:
return expected_pid is None or result.stdout.strip() == str(expected_pid)
def pane_tty(self, window: str) -> str:
return self.pane_display(window).tty_path
def pane_display(self, window: str) -> PaneDisplay:
result = subprocess.run(
[*self._command(), "display-message", "-p", "-t", window, "#{pane_tty}"],
[
*self._command(), "display-message", "-p", "-t", window,
"#{pane_tty}|#{cursor_x}|#{cursor_y}|#{pane_height}|#{alternate_on}",
],
text=True,
capture_output=True,
check=False,
)
tty_path = result.stdout.strip()
if result.returncode != 0 or not tty_path:
output = result.stdout.strip()
if result.returncode != 0 or not output:
raise ControlModeError(result.stderr.strip() or f"cannot resolve pane tty: {window}")
return tty_path
try:
tty_path, cursor_x, cursor_y, pane_height, alternate_on = output.split("|", 4)
return PaneDisplay(
tty_path=tty_path,
cursor_x=int(cursor_x),
cursor_y=int(cursor_y),
pane_height=int(pane_height),
alternate_on=alternate_on == "1",
)
except ValueError as exc:
raise ControlModeError(f"invalid pane display metadata: {output!r}") from exc
def start(self) -> None:
if self.process is not None:

View file

@ -100,11 +100,16 @@ class Service:
try:
target = f'{endpoint["session"]}:{row["target_repo"]}'
if delivery_mode == "output":
tty_path = control.pane_tty(target)
display = control.pane_display(target)
write_terminal_output(
tty_path,
display.tty_path,
terminal_frame(
row["sender_repo"], row["body"], row["message_id"]
row["sender_repo"],
row["body"],
row["message_id"],
cursor_y=display.cursor_y,
pane_height=display.pane_height,
alternate_on=display.alternate_on,
),
)
else:

View file

@ -153,6 +153,15 @@ class Store:
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
return list(self.db.execute(f"SELECT * FROM messages{where} ORDER BY created_at", values))
def latest_counterparty(self, target: str) -> str | None:
row = self.db.execute(
"SELECT sender_repo FROM messages "
"WHERE target_repo=? AND sender_repo<>? "
"ORDER BY created_at DESC, rowid DESC LIMIT 1",
(target, target),
).fetchone()
return None if row is None else str(row["sender_repo"])
def set_state(self, message_id: str, state: str) -> None:
column = {"injected": "injected_at", "acknowledged": "acknowledged_at"}.get(state)
if column:

View file

@ -29,9 +29,35 @@ def format_comment(sender: str, body: str, message_id: str) -> str:
return "\n".join(rendered)
def terminal_frame(sender: str, body: str, message_id: str) -> str:
"""Frame asynchronous output away from the current visual input line."""
def terminal_frame(
sender: str,
body: str,
message_id: str,
*,
cursor_y: int | None = None,
pane_height: int | None = None,
alternate_on: bool = False,
) -> str:
"""Frame output above the cursor when a safe scroll region is available."""
content = format_comment(sender, body, message_id).replace("\n", "\r\n")
line_count = content.count("\r\n") + 1
if (
cursor_y is not None
and pane_height is not None
and not alternate_on
and 0 < cursor_y < pane_height
and line_count <= cursor_y
):
lines = content.split("\r\n")
output = "".join(f"\n\r{line}" for line in lines)
return (
"\x1b7"
f"\x1b[1;{cursor_y}r"
f"\x1b[{cursor_y};1H"
f"{output}"
"\x1b[r"
"\x1b8"
)
return f"\r\n{content}\r\n"

View file

@ -78,6 +78,13 @@ class TmuxManager:
self.command_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
if not os.access(self.command_dir, os.W_OK | os.X_OK):
raise TmuxError(f"address command directory is not writable: {self.command_dir}")
scripts = {
"@": (
"#!/bin/sh\n"
"# tamq-address-command v1\n"
f"exec {shell_join(list(self.tamq_command))} reply -- \"$@\"\n"
)
}
for repo in repos:
if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", repo) is None:
raise TmuxError(
@ -90,20 +97,24 @@ class TmuxManager:
f"exec {shell_join(list(self.tamq_command))} send -- {shlex.quote(target)} \"$@\"\n"
)
for name in (f"@{repo}", target):
destination = self.command_dir / name
if destination.exists() or destination.is_symlink():
if destination.is_symlink() or not destination.is_file():
raise TmuxError(f"refusing to replace existing command: {destination}")
try:
existing = destination.read_text(encoding="utf-8")
except OSError as exc:
raise TmuxError(f"cannot inspect existing command: {destination}") from exc
if "# tamq-address-command v1" not in existing:
raise TmuxError(f"refusing to replace existing command: {destination}")
temporary = self.command_dir / f".{name}.{uuid4().hex}.tmp"
temporary.write_text(script, encoding="utf-8")
temporary.chmod(0o700)
temporary.replace(destination)
scripts[name] = script
for name in scripts:
destination = self.command_dir / name
if destination.exists() or destination.is_symlink():
if destination.is_symlink() or not destination.is_file():
raise TmuxError(f"refusing to replace existing command: {destination}")
try:
existing = destination.read_text(encoding="utf-8")
except OSError as exc:
raise TmuxError(f"cannot inspect existing command: {destination}") from exc
if "# tamq-address-command v1" not in existing:
raise TmuxError(f"refusing to replace existing command: {destination}")
for name, script in scripts.items():
destination = self.command_dir / name
temporary = self.command_dir / f".{name}.{uuid4().hex}.tmp"
temporary.write_text(script, encoding="utf-8")
temporary.chmod(0o700)
temporary.replace(destination)
def _window_environment(self, repo: str) -> tuple[str, ...]:
path = os.environ.get("PATH", "")

View file

@ -18,12 +18,12 @@ def test_pane_tty_is_resolved_through_tmux(monkeypatch):
def run(command, **kwargs):
calls.append(command)
return type("Result", (), {"returncode": 0, "stdout": "/dev/pts/7\n", "stderr": ""})()
return type("Result", (), {"returncode": 0, "stdout": "/dev/pts/7|12|8|24|0\n", "stderr": ""})()
monkeypatch.setattr(control.subprocess, "run", run)
client = ControlModeClient("tamq", tmux_command=("tmux", "-L", "test"))
assert client.pane_tty("tamq:audit-core") == "/dev/pts/7"
assert calls == [[
"tmux", "-L", "test", "display-message", "-p", "-t",
"tamq:audit-core", "#{pane_tty}",
"tamq:audit-core", "#{pane_tty}|#{cursor_x}|#{cursor_y}|#{pane_height}|#{alternate_on}",
]]

View file

@ -32,6 +32,43 @@ def test_manual_send_inbox_and_ack_use_window_repository_identity(tmp_path, monk
assert capsys.readouterr().out == ""
def test_bare_reply_targets_latest_inbound_sender(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
monkeypatch.setenv("TAMQ_REPO", "audit-core")
monkeypatch.setattr("tamq.cli.ping", service_is_down)
monkeypatch.setattr("tamq.cli.validate_targets", lambda repos: None)
store = Store(tmp_path / "state" / "tamq.sqlite3")
store.add("flex-auth", "audit-core", "older")
latest = store.add("railiance-platform", "audit-core", "latest")
store.acknowledge(latest)
store.add("audit-core", "audit-core", "self note")
store.close()
assert main(["reply", "--", "--looks-like-an-option", "thanks"]) == 0
message_id = capsys.readouterr().out.strip()
store = Store(tmp_path / "state" / "tamq.sqlite3")
row = next(row for row in store.list() if row["message_id"] == message_id)
assert row["sender_repo"] == "audit-core"
assert row["target_repo"] == "railiance-platform"
assert row["body"] == "--looks-like-an-option thanks"
store.close()
def test_bare_reply_requires_managed_identity_and_prior_sender(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
monkeypatch.setattr("tamq.cli.ping", service_is_down)
monkeypatch.setattr("tamq.cli.validate_targets", lambda repos: None)
monkeypatch.delenv("TAMQ_REPO", raising=False)
assert main(["reply", "hello"]) == 2
assert "managed window" in capsys.readouterr().err
monkeypatch.setenv("TAMQ_REPO", "audit-core")
assert main(["reply", "hello"]) == 2
assert "no counterparty" in capsys.readouterr().err
def test_inbox_requires_repository_outside_managed_window(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
monkeypatch.delenv("TAMQ_REPO", raising=False)

View file

@ -1,3 +1,4 @@
from tamq.control import PaneDisplay
from tamq.service import Service
from tamq.store import Store
@ -19,8 +20,14 @@ class FakeControl:
def inject(self, window, text):
self.injected.append((window, text))
def pane_tty(self, window):
return f"/dev/pts/{window.rsplit(':', 1)[-1]}"
def pane_display(self, window):
return PaneDisplay(
tty_path=f"/dev/pts/{window.rsplit(':', 1)[-1]}",
cursor_x=0,
cursor_y=0,
pane_height=24,
alternate_on=False,
)
def close(self):
return None

View file

@ -59,3 +59,15 @@ def test_mark_displayed_releases_lease_without_acknowledging(tmp_path):
assert row["displayed_at"] is not None
assert store.db.execute("SELECT COUNT(*) FROM leases").fetchone()[0] == 0
store.close()
def test_latest_counterparty_uses_latest_inbound_message_regardless_of_state(tmp_path):
store = Store(tmp_path / "queue.sqlite3")
store.add("audit-core", "audit-core", "self note")
older = store.add("flex-auth", "audit-core", "first")
store.acknowledge(older)
store.add("railiance-platform", "audit-core", "latest")
assert store.latest_counterparty("audit-core") == "railiance-platform"
assert store.latest_counterparty("unknown") is None
store.close()

View file

@ -16,6 +16,24 @@ def test_comment_format_escapes_controls_and_prefixes_every_line():
assert format_comment("repo-a", "first\nsecond\x1b[31m", "m-1") == (
"#repo-a: first\n# second\\x1b[31m [m-1]"
)
def test_terminal_frame_scrolls_only_rows_above_the_cursor():
assert terminal_frame(
"repo-a", "first\nsecond", "m-1", cursor_y=8, pane_height=24
) == (
"\x1b7\x1b[1;8r\x1b[8;1H"
"\n\r#repo-a: first\n\r# second [m-1]"
"\x1b[r\x1b8"
)
def test_terminal_frame_falls_back_when_stable_region_is_not_safe():
expected = "\r\n#repo-a: hello [m-1]\r\n"
assert terminal_frame("repo-a", "hello", "m-1", cursor_y=0, pane_height=24) == expected
assert terminal_frame(
"repo-a", "hello", "m-1", cursor_y=8, pane_height=24, alternate_on=True
) == expected
assert terminal_frame("repo-a", "hello", "m-1") == (
"\r\n#repo-a: hello [m-1]\r\n"
)

View file

@ -10,6 +10,7 @@ import pytest
from tamq.broker import BrokerIdentity, InputBroker
from tamq.control import ControlModeClient
from tamq.store import Store
from tamq.terminal import terminal_frame, write_terminal_output
from tamq.tmux import LaunchPlan, TmuxManager
@ -112,3 +113,88 @@ def test_real_tmux_starts_two_repo_windows_and_reuses_them(tmp_path, monkeypatch
store.close()
finally:
manager._run("kill-server", check=False)
@pytest.mark.skipif(shutil.which("tmux") is None, reason="tmux is not installed")
def test_real_tmux_output_preserves_partial_input_line_and_cursor(tmp_path):
repo = tmp_path / "audit-core"
repo.mkdir()
socket_name = f"tamq-output-{os.getpid()}-{uuid4().hex[:8]}"
session = f"tamq-output-{uuid4().hex[:8]}"
manager = TmuxManager(
session,
tmux_command=("tmux", "-L", socket_name),
tamq_command=(sys.executable, "-m", "tamq.cli"),
command_dir=tmp_path / "commands",
)
target = f"{session}:audit-core"
plan = LaunchPlan(("audit-core",), {"audit-core": str(repo)}, ())
try:
manager.ensure_plan(plan)
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
current_command = manager._run(
"display-message", "-p", "-t", target, "#{pane_current_command}"
)
if current_command in {"bash", "sh", "zsh", "fish"}:
break
time.sleep(0.05)
assert current_command in {"bash", "sh", "zsh", "fish"}
manager._run(
"send-keys", "-t", target, "-l", "--", "printf 'line-one\\nline-two\\n'"
)
manager._run("send-keys", "-t", target, "Enter")
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
initial_capture = manager._run("capture-pane", "-p", "-t", target)
initial_lines = initial_capture.splitlines()
if "line-one" in initial_lines and "line-two" in initial_lines:
break
time.sleep(0.05)
assert "line-one" in initial_lines and "line-two" in initial_lines
manager._run("send-keys", "-t", target, "-l", "--", "PARTIAL-INPUT")
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
before_capture = manager._run("capture-pane", "-p", "-t", target)
if "PARTIAL-INPUT" in before_capture:
break
time.sleep(0.05)
assert "PARTIAL-INPUT" in before_capture
control = ControlModeClient(
session,
tmux_command=("tmux", "-L", socket_name),
)
before = control.pane_display(target)
write_terminal_output(
before.tty_path,
terminal_frame(
"flex-auth",
"stable-message",
"m-stable",
cursor_y=before.cursor_y,
pane_height=before.pane_height,
alternate_on=before.alternate_on,
),
)
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
after_capture = manager._run("capture-pane", "-p", "-t", target)
if "#flex-auth: stable-message [m-stable]" in after_capture:
break
time.sleep(0.05)
after = control.pane_display(target)
assert "#flex-auth: stable-message [m-stable]" in after_capture
before_input = next(line for line in before_capture.splitlines() if "PARTIAL-INPUT" in line)
after_lines = after_capture.splitlines()
assert any("PARTIAL-INPUT" in line for line in after_lines), repr(after_capture)
after_input_index = next(
index for index, line in enumerate(after_lines) if "PARTIAL-INPUT" in line
)
assert after_lines[after_input_index] == before_input
assert after_lines[after_input_index - 1] == "#flex-auth: stable-message [m-stable]"
assert (after.cursor_x, after.cursor_y) == (before.cursor_x, before.cursor_y)
finally:
manager._run("kill-server", check=False)

View file

@ -155,6 +155,7 @@ def test_neutral_plan_starts_shell_without_sending_keystrokes(tmp_path, monkeypa
assert (command_dir / "@a:").is_file()
assert (command_dir / "@b").is_file()
assert (command_dir / "@b:").is_file()
assert (command_dir / "@").is_file()
def test_address_commands_preserve_message_arguments(tmp_path):
@ -173,6 +174,14 @@ def test_address_commands_preserve_message_arguments(tmp_path):
)
assert result.stdout == "send -- @audit-core: Some message! $value ; literal --from\n"
result = subprocess.run(
[str(command_dir / "@"), "Some reply!", "$value", "--literal"],
text=True,
capture_output=True,
check=True,
)
assert result.stdout == "reply -- Some reply! $value --literal\n"
def test_address_commands_reject_unsafe_repository_names(tmp_path):
manager = tmux.TmuxManager("tamq-test", command_dir=tmp_path / "commands")
@ -192,6 +201,18 @@ def test_address_commands_do_not_replace_unowned_commands(tmp_path):
assert existing.read_text(encoding="utf-8") == "#!/bin/sh\necho mine\n"
def test_address_commands_do_not_replace_unowned_bare_reply(tmp_path):
command_dir = tmp_path / "commands"
command_dir.mkdir()
existing = command_dir / "@"
existing.write_text("#!/bin/sh\necho mine\n", encoding="utf-8")
manager = tmux.TmuxManager("tamq-test", command_dir=command_dir)
with pytest.raises(tmux.TmuxError, match="refusing to replace"):
manager._install_address_commands(["audit-core"])
assert existing.read_text(encoding="utf-8") == "#!/bin/sh\necho mine\n"
def test_preflight_rejects_missing_agent(tmp_path, monkeypatch):
repo = tmp_path / "a"
repo.mkdir()

View file

@ -4,7 +4,7 @@ type: workplan
title: "Counterparty reply shorthand and stable terminal output"
domain: communication
repo: tmux-amq
status: active
status: finished
owner: codex
topic_slug: coulomb-social
planning_priority: P0
@ -42,7 +42,7 @@ restore the cursor. The current input row and cursor position remain stable.
```task
id: TAMQ-WP-0008-T01
status: progress
status: done
priority: high
state_hub_task_id: "aad3dea8-b5c9-5b07-97bb-92aba18137d8"
```
@ -57,7 +57,7 @@ transport, and refuse collision with an unrelated existing `@` command.
```task
id: TAMQ-WP-0008-T02
status: todo
status: done
priority: high
state_hub_task_id: "f9893aba-e2b4-50da-9632-0f2e7f3b8c45"
```
@ -72,7 +72,7 @@ row exists above the cursor. Never emit input bytes.
```task
id: TAMQ-WP-0008-T03
status: todo
status: done
priority: high
state_hub_task_id: "f81290ce-5bf6-5c08-a0b1-efb2fb8b5782"
```
@ -87,7 +87,7 @@ message becomes visible immediately above them.
```task
id: TAMQ-WP-0008-T04
status: todo
status: done
priority: high
state_hub_task_id: "944c7fea-2e7c-5fcc-9fc6-1abc6a8cc784"
```
@ -95,3 +95,14 @@ state_hub_task_id: "944c7fea-2e7c-5fcc-9fc6-1abc6a8cc784"
Update help, README, and SCOPE; run complete checks; install the build; exercise
`@ MESSAGE` in both directions in a live two-window session; and retain only
user messages pending after acknowledging controlled smoke records.
## Completion evidence
- `make check`: 100 tests passed, including a real tmux pane proving that a
partial input row and both cursor coordinates remain unchanged.
- Coverage: 76% overall; terminal framing is 84% and control handling is 81%.
- `make install`: installed `tmux-amq==0.1.0` and verified `tamq --version`.
- Live `flex-auth`/`audit-core`: bare `@` routed in both directions. Incoming
comments appeared immediately above `SECOND-PARTIAL` and `THIRD-PARTIAL`
without moving either cursor. Only the three controlled smoke messages were
acknowledged; all pre-existing user messages remain pending.