feat: add experimental pushy delivery mode
Some checks failed
tamq-ci / test (push) Has been cancelled

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:41:09 +02:00
parent 8186550c9a
commit 9ed8b62b45
16 changed files with 441 additions and 31 deletions

View file

@ -54,13 +54,37 @@ tamq start --command 'htop --tree' flex-auth audit-core
`--cmd` remains an alias for `--command`. Repeated starts reuse existing
windows and never run another initial command in them.
By default, a routed message is also written as sanitized output to its target
pane. Use `--no-display` when you want durable inbox-only delivery:
Select an endpoint delivery mode at startup. `output` is the safe default;
`inbox` keeps messages durable without displaying them:
```bash
tamq --no-display flex-auth audit-core
tamq --mode output flex-auth audit-core
tamq --mode inbox flex-auth audit-core
```
`--no-display` remains a compatibility alias for `--mode inbox`.
Experimental `pushy` mode submits each routed message to the target pane as
input. It is intended for coding-agent interfaces that queue user prompts:
```bash
tamq --mode pushy --command codex flex-auth audit-core
```
The submitted line is sanitized and sender-labelled, then followed by exactly
one Enter key:
```text
#flex-auth: please review the auth boundary [m-...]
```
This leading `#` makes an empty ordinary shell prompt treat the line as a
comment. Pushy mode cannot determine whether a pane is an agent, a shell, or
whether someone is already editing input: it can append to that input and
submit the combined line. Use it only for panes whose occupant is known to
accept or queue asynchronous prompts. Switching an existing endpoint to pushy
may also submit pending messages that have never been displayed.
## Exchange messages manually
Each managed shell exports its own repository slug as `TAMQ_REPO`. From the
@ -152,9 +176,11 @@ it and all later messages pending. Filters never run in the background and
cannot be combined with `--all` or `--json`.
Outside a managed window, use `tamq inbox --repo audit-core` and optionally
`--json`. Displayed messages remain durable and pending until acknowledged.
Neither normal output delivery nor `--no-display` injects terminal keystrokes,
so they cannot execute or alter a command being typed in the target pane.
`--json`. Output-displayed messages remain durable and pending until
acknowledged. Neither `output` nor `inbox` mode injects terminal keystrokes, so
they cannot execute or alter a command being typed in the target pane. Pushy
mode intentionally crosses that boundary and records accepted submissions as
`injected`.
After upgrading from an earlier alpha, recreate the managed session once so
existing panes inherit the neutral shell contract, repository command `PATH`,
@ -208,10 +234,13 @@ observation and message identity.
Normal endpoints use terminal-output delivery: each message is written once to
the target pane's PTY output and stays pending in the durable inbox until
acknowledged. `--no-display` selects inbox-only manual mode. Neither becomes
pane input. The legacy pane-input path is available only with the explicit
`tamq start --tap --command ...` opt-in; it remains subject to the retry and
acknowledgement limitations tracked by `TAMQ-WP-0003`.
acknowledged. `--mode inbox` selects inbox-only manual mode. Neither becomes
pane input. Experimental `--mode pushy` sends one checked tmux command list
containing sanitized literal input followed by Enter, then records the message
as `injected`. The older full-duplex pane-input path remains available only
with the explicit `tamq start --tap --command ...` opt-in. Both input paths
remain subject to the retry and acknowledgement limitations tracked by
`TAMQ-WP-0003`.
The visible endpoint label is `tmux-amq-<PID>`; each boot also receives an
instance nonce so PID reuse cannot collide with prior leases or receipts.

View file

@ -21,6 +21,8 @@ tamq does not choose or infer them.
display, a recipient-aware inline composer, 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 experimental pushy delivery that submits a sanitized comment and
Enter to target pane input for known queue-capable interactive programs.
- Explicit opt-in control-mode pane delivery and the full-duplex `tamq tap` PTY
broker for integration experiments.
- Exact `gita` repository validation and direct `@repo: message` routing.
@ -34,8 +36,9 @@ tamq does not choose or infer them.
- Owning goal planning, workflow scheduling, or cross-host coordination; those
belong to `coordination-engine` and its consumers.
- Acting as a network-accessible or multi-host message broker.
- Selecting a coding agent, starting one implicitly, or assuming a pane accepts
machine-generated terminal input.
- Selecting a coding agent, starting one implicitly, or silently assuming a
pane accepts machine-generated input; pushy delivery requires explicit mode
selection by the operator.
- Bypassing `gita` registration or injecting arbitrary pane input outside the
supported tap/control-mode boundaries.
- Owning tmux, Codex, State Hub, or adjacent repositories' lifecycle.
@ -53,6 +56,7 @@ transport.
| 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 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`. |
| Experimental pushy delivery | Explicit opt-in | `--mode pushy` submits one sanitized sender-labelled comment plus Enter through a checked tmux command. It marks successful delivery `injected`, but cannot identify pane occupants or protect input already being edited. |
| 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. |
@ -76,6 +80,8 @@ Suitable today:
gita-registered repositories.
- Durable manual message exchange between managed repository windows, including
explicit pull-time loggers and filters.
- Controlled experiments with pushy delivery to coding-agent interfaces known
to queue asynchronous user prompts.
- Developing and testing the future coordination-engine adapter against the
local socket boundary.
@ -83,11 +89,13 @@ Not yet suitable:
- Unattended pane injection where bounded retries and positive recipient
acknowledgement are required.
- Pushy delivery to arbitrary shells, editors, or panes with unknown input
state.
- Production-style operation without longer-running crash/terminal soak tests
and stronger process-supervision evidence.
- Cross-host messaging or use as a general-purpose broker.
The suite currently has 109 passing tests and 76% statement coverage. Coverage
The suite currently has 117 passing tests and 77% 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

@ -18,6 +18,7 @@
| workplan | TAMQ-WP-0007 | finished | — | workplans/TAMQ-WP-0007-terminal-output-notifications.md |
| workplan | TAMQ-WP-0008 | finished | — | workplans/TAMQ-WP-0008-reply-shorthand-stable-output.md |
| workplan | TAMQ-WP-0009 | finished | — | workplans/TAMQ-WP-0009-interactive-recipient-composer.md |
| workplan | TAMQ-WP-0010 | active | — | workplans/TAMQ-WP-0010-experimental-pushy-delivery.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 |
@ -55,3 +56,6 @@
| task | TAMQ-WP-0009-T01 | done | — | workplans/TAMQ-WP-0009-interactive-recipient-composer.md |
| task | TAMQ-WP-0009-T02 | done | — | workplans/TAMQ-WP-0009-interactive-recipient-composer.md |
| task | TAMQ-WP-0009-T03 | done | — | workplans/TAMQ-WP-0009-interactive-recipient-composer.md |
| task | TAMQ-WP-0010-T01 | progress | — | workplans/TAMQ-WP-0010-experimental-pushy-delivery.md |
| task | TAMQ-WP-0010-T02 | todo | — | workplans/TAMQ-WP-0010-experimental-pushy-delivery.md |
| task | TAMQ-WP-0010-T03 | todo | — | workplans/TAMQ-WP-0010-experimental-pushy-delivery.md |

View file

@ -34,7 +34,7 @@ from .terminal import format_comment
SUBCOMMANDS = frozenset(
"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"})
START_OPTIONS = frozenset({"--command", "--cmd", "--tap", "--detach", "--no-service", "--no-display", "--mode"})
GLOBAL_FLAGS = frozenset({"--orwell", "--verbose"})
@ -129,6 +129,10 @@ def ensure_output_service() -> bool:
return ensure_service_capabilities({"manual_delivery", "terminal_output"})
def ensure_pushy_service() -> bool:
return ensure_service_capabilities({"manual_delivery", "pushy_input"})
def preflight_runtime_paths() -> None:
"""Ensure local state/runtime parents are available before tmux mutation."""
parents = {db_path().parent, socket_path().parent, pid_path().parent, lock_path().parent}
@ -154,12 +158,12 @@ def build_parser() -> argparse.ArgumentParser:
description="Repository-aware tmux sessions with durable local messaging.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""session shorthand:
tamq [--detach] [--command COMMAND] [--no-display] REPO [REPO ...]
tamq [--detach] [--command COMMAND] [--mode MODE] REPO [REPO ...]
With no --command, tamq opens ordinary repository shells. --command is an
explicit initial command for newly created windows. Messages appear as
sanitized terminal output by default; --no-display keeps them inbox-only.
Neither mode injects message bytes into pane input.
sanitized terminal output by default. --mode inbox keeps messages durable
without display; experimental --mode pushy submits them to target input.
manual messaging from a managed shell:
@TARGET: MESSAGE...
@ -178,7 +182,8 @@ manual messaging from a managed shell:
start.add_argument("repos", nargs="*", help="gita-registered repository slugs")
start.add_argument("--command", "--cmd", dest="initial_command", default=None, help="explicit initial command for newly created windows (default: ordinary shell)")
start.add_argument("--tap", action="store_true", help="explicitly opt in to PTY input observation and pane message injection")
start.add_argument("--no-display", action="store_true", help="keep messages in the durable inbox without writing target terminal output")
start.add_argument("--mode", choices=("output", "inbox", "pushy"), help="message delivery mode (default: output; pushy is experimental)")
start.add_argument("--no-display", action="store_true", help="compatibility alias for --mode inbox")
start.add_argument("--detach", action="store_true", help="detach after startup")
start.add_argument("--no-service", action="store_true", help="open tmux windows without service registration or messaging")
attach = subparsers.add_parser("attach", help="attach to the managed tmux session")
@ -251,7 +256,7 @@ def normalize_argv(argv: list[str]) -> list[str]:
if index == len(argv):
return argv
candidate = argv[index]
if candidate in START_OPTIONS or (
if candidate in START_OPTIONS or candidate.startswith("--mode=") or (
not candidate.startswith("-") and candidate not in SUBCOMMANDS
):
return [*argv[:index], "start", *argv[index:]]
@ -307,6 +312,15 @@ def main(argv: list[str] | None = None) -> int:
finally:
store.close()
if args.command == "start":
if args.no_display and args.mode not in (None, "inbox"):
print("tamq: --no-display conflicts with the selected --mode", file=sys.stderr)
return 2
selected_mode = args.mode or ("inbox" if args.no_display else "output")
endpoint_delivery_mode = {
"inbox": "manual",
"output": "output",
"pushy": "pushy",
}[selected_mode]
if args.tap and args.initial_command is None:
print("tamq: --tap requires an explicit --command", file=sys.stderr)
return 2
@ -316,6 +330,12 @@ def main(argv: list[str] | None = None) -> int:
if args.tap and args.no_display:
print("tamq: --tap cannot be combined with --no-display", file=sys.stderr)
return 2
if args.tap and args.mode is not None:
print("tamq: --tap cannot be combined with --mode", file=sys.stderr)
return 2
if args.no_service and args.mode is not None:
print("tamq: --mode cannot be combined with --no-service", file=sys.stderr)
return 2
manager = TmuxManager()
try:
if not args.no_service:
@ -324,9 +344,14 @@ def main(argv: list[str] | None = None) -> int:
except (TmuxError, OSError) as exc:
print(f"tamq: {exc}", file=sys.stderr)
return 2
service_ready = (
ensure_manual_service() if args.no_display else ensure_output_service()
) if not args.no_service else True
if args.no_service:
service_ready = True
elif selected_mode == "inbox":
service_ready = ensure_manual_service()
elif selected_mode == "pushy":
service_ready = ensure_pushy_service()
else:
service_ready = ensure_output_service()
if not service_ready:
print("tamq service failed to start with the required delivery capability; use --no-service to open repos without messaging", file=sys.stderr)
return 1
@ -339,7 +364,7 @@ def main(argv: list[str] | None = None) -> int:
registered = False
if not args.no_service:
try:
delivery_mode = "pane" if args.tap else ("manual" if args.no_display else "output")
delivery_mode = "pane" if args.tap else endpoint_delivery_mode
registration = asyncio.run(request({"op": "register", "endpoint_id": endpoint.endpoint_id, "instance_id": endpoint.instance_key, "pid": endpoint.pid, "session": endpoint.session, "repos": endpoint.repos, "delivery_mode": delivery_mode}))
if not registration.get("ok"):
raise RuntimeError(f"endpoint registration failed: {registration.get('error', 'unknown error')}")
@ -367,7 +392,10 @@ def main(argv: list[str] | None = None) -> int:
"delivered": delivered,
"service": not args.no_service,
"messaging": registered,
"delivery_mode": "none" if args.no_service else ("pane" if args.tap else ("manual" if args.no_display else "output")),
"mode": "none" if args.no_service else ("tap" if args.tap else selected_mode),
"delivery_mode": "none" if args.no_service else (
"pane" if args.tap else endpoint_delivery_mode
),
}
print(json.dumps(summary), flush=True)
if args.detach:

View file

@ -93,6 +93,24 @@ class ControlModeClient:
# send-keys -l preserves punctuation and whitespace in the message.
self.command(f"send-keys -t {window} -l -- {shell_quote(text)}")
def submit(self, window: str, text: str) -> None:
"""Inject literal text and Enter as one checked tmux command list."""
result = subprocess.run(
[
*self._command(),
"send-keys", "-t", window, "-l", "--", text,
";",
"send-keys", "-t", window, "Enter",
],
text=True,
capture_output=True,
check=False,
)
if result.returncode:
raise ControlModeError(
result.stderr.strip() or f"cannot submit pane input: {window}"
)
def close(self) -> None:
if self.process is not None:
process = self.process

View file

@ -13,7 +13,7 @@ from .config import db_path
from .store import Store
from .registry import RegistryError, validate_targets
from .control import ControlModeClient
from .terminal import terminal_frame, write_terminal_output
from .terminal import format_pushy_input, terminal_frame, write_terminal_output
PROTOCOL_VERSION = "0.1"
@ -112,6 +112,15 @@ class Service:
alternate_on=display.alternate_on,
),
)
elif delivery_mode == "pushy":
control.submit(
target,
format_pushy_input(
row["sender_repo"],
row["body"],
row["message_id"],
),
)
else:
control.inject(target, f'#{row["sender_repo"]}: {row["body"]}')
except Exception:
@ -135,7 +144,7 @@ class Service:
if protocol.split(".")[0] != PROTOCOL_VERSION.split(".")[0]:
response = {"ok": False, "error": f"incompatible protocol: {protocol}", "protocol": PROTOCOL_VERSION}
elif op == "ping":
response = {"ok": True, "protocol": PROTOCOL_VERSION, "capabilities": ["register", "send", "history", "manual_delivery", "terminal_output"]}
response = {"ok": True, "protocol": PROTOCOL_VERSION, "capabilities": ["register", "send", "history", "manual_delivery", "terminal_output", "pushy_input"]}
elif op == "register":
required = ("endpoint_id", "pid", "session", "repos")
if any(key not in request for key in required):

View file

@ -82,7 +82,7 @@ class Store:
delivery_mode: str = "manual",
) -> None:
import json
if delivery_mode not in {"manual", "output", "pane"}:
if delivery_mode not in {"manual", "output", "pane", "pushy"}:
raise ValueError(f"invalid delivery mode: {delivery_mode}")
self.db.execute(
"UPDATE endpoints SET disconnected_at=strftime('%s','now') "

View file

@ -29,6 +29,14 @@ def format_comment(sender: str, body: str, message_id: str) -> str:
return "\n".join(rendered)
def format_pushy_input(sender: str, body: str, message_id: str) -> str:
"""Render one shell-safe input line for explicit pushy delivery."""
return (
f"#{terminal_safe(sender)}: {terminal_safe(body)} "
f"[{terminal_safe(message_id)}]"
)
def terminal_frame(
sender: str,
body: str,

View file

@ -18,7 +18,7 @@ def test_root_help_exposes_repository_shorthand_and_command(capsys):
main(["--help"])
assert exc.value.code == 0
output = capsys.readouterr().out
assert "tamq [--detach] [--command COMMAND] [--no-display] REPO" in output
assert "tamq [--detach] [--command COMMAND] [--mode MODE] REPO" in output
assert "With no --command" in output
assert "@TARGET: MESSAGE" in output
@ -45,11 +45,13 @@ def test_start_parser_supports_command_and_cmd_alias():
compatibility = parser.parse_args(["start", "--cmd", "claude", "a"])
neutral = parser.parse_args(["start", "a", "b"])
inbox_only = parser.parse_args(["start", "--no-display", "a"])
pushy = parser.parse_args(["start", "--mode", "pushy", "a"])
assert canonical.initial_command == "codex --quiet"
assert canonical.repos == ["a", "b"]
assert compatibility.initial_command == "claude"
assert neutral.initial_command is None
assert inbox_only.no_display is True
assert pushy.mode == "pushy"
def test_repository_first_and_command_first_start_shorthand():
@ -69,6 +71,17 @@ def test_repository_first_and_command_first_start_shorthand():
"--no-display",
"flex-auth",
]
assert normalize_argv(["--mode", "pushy", "flex-auth"]) == [
"start",
"--mode",
"pushy",
"flex-auth",
]
assert normalize_argv(["--mode=pushy", "flex-auth"]) == [
"start",
"--mode=pushy",
"flex-auth",
]
assert normalize_argv(["status"]) == ["status"]
assert normalize_argv(["--verbose", "flex-auth", "audit-core"]) == [
"--verbose",
@ -179,6 +192,52 @@ def test_registration_failure_rolls_back_created_windows(monkeypatch, capsys):
assert "endpoint registration failed: denied" in capsys.readouterr().err
def test_pushy_mode_registers_explicit_delivery_mode(monkeypatch, capsys):
endpoint = type(
"Endpoint",
(),
{
"endpoint_id": "tmux-amq-42",
"instance_key": "tmux-amq-42-boot",
"pid": 42,
"session": "tamq",
"repos": ["a", "b"],
},
)()
requests = []
class Manager:
def preflight(self, repos, command):
assert repos == ["a", "b"]
return "plan"
def ensure_plan(self, plan, *, tap=True):
assert tap is False
return endpoint
def rollback(self, value):
pytest.fail("successful registration must not roll back")
async def register(payload):
requests.append(payload)
return {"ok": True}
monkeypatch.setattr("tamq.cli.TmuxManager", Manager)
monkeypatch.setattr("tamq.cli.preflight_runtime_paths", lambda: None)
monkeypatch.setattr("tamq.cli.ensure_pushy_service", lambda: True)
monkeypatch.setattr(
"tamq.cli.ensure_output_service",
lambda: pytest.fail("output capability must not select pushy mode"),
)
monkeypatch.setattr("tamq.cli.request", register)
assert main(["start", "--detach", "--mode", "pushy", "a", "b"]) == 0
summary = json.loads(capsys.readouterr().out)
assert summary["mode"] == "pushy"
assert summary["delivery_mode"] == "pushy"
assert requests[0]["delivery_mode"] == "pushy"
def test_tap_requires_explicit_command_and_service(capsys):
assert main(["start", "--tap", "a"]) == 2
assert "--tap requires an explicit --command" in capsys.readouterr().err
@ -186,3 +245,9 @@ def test_tap_requires_explicit_command_and_service(capsys):
assert "--tap cannot be combined with --no-service" in capsys.readouterr().err
assert main(["start", "--tap", "--no-display", "--command", "sh", "a"]) == 2
assert "--tap cannot be combined with --no-display" in capsys.readouterr().err
assert main(["start", "--no-display", "--mode", "pushy", "a"]) == 2
assert "conflicts with the selected --mode" in capsys.readouterr().err
assert main(["start", "--tap", "--mode", "pushy", "--command", "sh", "a"]) == 2
assert "--tap cannot be combined with --mode" in capsys.readouterr().err
assert main(["start", "--no-service", "--mode", "pushy", "a"]) == 2
assert "--mode cannot be combined with --no-service" in capsys.readouterr().err

View file

@ -1,4 +1,9 @@
from tamq.cli import ensure_manual_service, ensure_output_service, parse_size
from tamq.cli import (
ensure_manual_service,
ensure_output_service,
ensure_pushy_service,
parse_size,
)
def test_parse_size():
@ -42,3 +47,23 @@ def test_output_service_requires_terminal_output_capability(monkeypatch):
assert ensure_output_service() is True
assert len(starts) == 2
assert stops == [True]
def test_pushy_service_requires_pushy_input_capability(monkeypatch):
capabilities = iter([
["manual_delivery", "terminal_output"],
["manual_delivery", "terminal_output", "pushy_input"],
])
starts = []
stops = []
async def request(payload):
return {"capabilities": next(capabilities)}
monkeypatch.setattr("tamq.cli.ensure_service", lambda: starts.append(True) or True)
monkeypatch.setattr("tamq.cli.stop_service_process", lambda: stops.append(True) or 42)
monkeypatch.setattr("tamq.cli.request", request)
assert ensure_pushy_service() is True
assert len(starts) == 2
assert stops == [True]

View file

@ -1,5 +1,7 @@
from tamq import control
from tamq.control import ControlModeClient, shell_quote
import pytest
from tamq.control import ControlModeClient, ControlModeError, shell_quote
from tamq.tmux import shell_join
@ -27,3 +29,39 @@ def test_pane_tty_is_resolved_through_tmux(monkeypatch):
"tmux", "-L", "test", "display-message", "-p", "-t",
"tamq:audit-core", "#{pane_tty}|#{cursor_x}|#{cursor_y}|#{pane_height}|#{alternate_on}",
]]
def test_submit_sends_literal_input_then_one_enter(monkeypatch):
calls = []
def run(command, **kwargs):
calls.append(command)
return type("Result", (), {"returncode": 0, "stderr": ""})()
monkeypatch.setattr(control.subprocess, "run", run)
client = ControlModeClient("tamq", tmux_command=("tmux", "-L", "test"))
client.submit("tamq:audit-core", "#flex-auth: What's up? [m-1]")
assert calls == [
[
"tmux", "-L", "test",
"send-keys", "-t", "tamq:audit-core", "-l", "--",
"#flex-auth: What's up? [m-1]",
";",
"send-keys", "-t", "tamq:audit-core", "Enter",
]
]
def test_submit_surfaces_tmux_rejection(monkeypatch):
monkeypatch.setattr(
control.subprocess,
"run",
lambda *args, **kwargs: type(
"Result", (), {"returncode": 1, "stderr": "missing pane"}
)(),
)
client = ControlModeClient("tamq")
with pytest.raises(ControlModeError, match="missing pane"):
client.submit("tamq:missing", "message")

View file

@ -92,7 +92,7 @@ def test_isolated_installed_tool_session_smoke(tmp_path):
)
assert run(str(tamq), "--version").stdout.strip() == "0.1.0"
root_help = run(str(tamq), "--help").stdout
assert "tamq [--detach] [--command COMMAND] [--no-display] REPO" in root_help
assert "tamq [--detach] [--command COMMAND] [--mode MODE] REPO" in root_help
started = json.loads(
run(
str(tamq),

View file

@ -9,6 +9,7 @@ class FakeControl:
def __init__(self, session):
self.session = session
self.injected = []
self.submitted = []
self.__class__.instances.append(self)
def start(self):
@ -20,6 +21,9 @@ class FakeControl:
def inject(self, window, text):
self.injected.append((window, text))
def submit(self, window, text):
self.submitted.append((window, text))
def pane_display(self, window):
return PaneDisplay(
tty_path=f"/dev/pts/{window.rsplit(':', 1)[-1]}",
@ -56,6 +60,41 @@ def test_service_never_injects_manual_endpoint_messages(tmp_path, monkeypatch):
assert store.list()[0]["state"] == "pending"
def test_service_pushy_mode_submits_once_and_marks_injected(tmp_path, monkeypatch):
store = Store(tmp_path / "queue.sqlite3")
store.register_endpoint("tmux-amq-9-boot", 9, "tamq", ["repo-b"], "pushy")
message_id = store.add("repo-a", "repo-b", "first\nsecond")
monkeypatch.setattr("tamq.service.ControlModeClient", FakeControl)
service = Service(store=store)
service._deliver_once()
control = FakeControl.instances[-1]
service._deliver_once()
assert control.submitted == [
(
"tamq:repo-b",
f"#repo-a: first\\x0asecond [{message_id}]",
)
]
assert store.list()[0]["state"] == "injected"
def test_failed_pushy_submission_remains_pending(tmp_path, monkeypatch):
store = Store(tmp_path / "queue.sqlite3")
store.register_endpoint("tmux-amq-9-boot", 9, "tamq", ["repo-b"], "pushy")
store.add("repo-a", "repo-b", "continue")
class FailingControl(FakeControl):
def submit(self, window, text):
raise RuntimeError("tmux rejected input")
monkeypatch.setattr("tamq.service.ControlModeClient", FailingControl)
Service(store=store)._deliver_once()
assert store.list()[0]["state"] == "pending"
def test_service_writes_output_once_and_retains_pending_ack(tmp_path, monkeypatch):
store = Store(tmp_path / "queue.sqlite3")
store.register_endpoint("tmux-amq-9-boot", 9, "tamq", ["repo-b"], "output")

View file

@ -7,6 +7,7 @@ import pytest
from tamq.terminal import (
TerminalOutputError,
format_comment,
format_pushy_input,
terminal_frame,
write_terminal_output,
)
@ -18,6 +19,12 @@ def test_comment_format_escapes_controls_and_prefixes_every_line():
)
def test_pushy_input_is_one_sanitized_shell_comment():
assert format_pushy_input("repo-a", "first\nsecond\x1b[31m", "m-1") == (
"#repo-a: first\\x0asecond\\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

View file

@ -9,6 +9,7 @@ import pytest
from tamq.broker import BrokerIdentity, InputBroker
from tamq.control import ControlModeClient
from tamq.service import Service
from tamq.store import Store
from tamq.terminal import terminal_frame, write_terminal_output
from tamq.tmux import LaunchPlan, TmuxManager
@ -198,3 +199,63 @@ def test_real_tmux_output_preserves_partial_input_line_and_cursor(tmp_path):
assert (after.cursor_x, after.cursor_y) == (before.cursor_x, before.cursor_y)
finally:
manager._run("kill-server", check=False)
@pytest.mark.skipif(shutil.which("tmux") is None, reason="tmux is not installed")
def test_real_tmux_pushy_mode_submits_one_shell_safe_input(tmp_path, monkeypatch):
repo = tmp_path / "audit-core"
repo.mkdir()
socket_name = f"tamq-pushy-{os.getpid()}-{uuid4().hex[:8]}"
session = f"tamq-pushy-{uuid4().hex[:8]}"
monkeypatch.setenv("TAMQ_TMUX_SOCKET", socket_name)
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"
store = Store(tmp_path / "queue.sqlite3")
try:
endpoint = manager.ensure_plan(
LaunchPlan(("audit-core",), {"audit-core": str(repo)}, ())
)
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"}
store.register_endpoint(
endpoint.instance_key,
endpoint.pid,
endpoint.session,
endpoint.repos,
"pushy",
)
message_id = store.add("flex-auth", "audit-core", "What's next?\nsecond")
service = Service(store=store)
service._deliver_once()
expected = f"#flex-auth: What's next?\\x0asecond [{message_id}]"
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
capture = manager._run("capture-pane", "-p", "-t", target)
if expected in capture:
break
time.sleep(0.05)
assert expected in capture
assert store.list()[0]["state"] == "injected"
assert store.db.execute("SELECT COUNT(*) FROM leases").fetchone()[0] == 0
service._deliver_once()
repeated_capture = manager._run("capture-pane", "-p", "-t", target)
assert repeated_capture.count(expected) == 1
finally:
store.close()
manager._run("kill-server", check=False)

View file

@ -0,0 +1,71 @@
---
id: TAMQ-WP-0010
type: workplan
title: "Experimental pushy input delivery mode"
domain: communication
repo: tmux-amq
status: active
owner: codex
topic_slug: coulomb-social
planning_priority: P0
planning_order: 15
created: "2026-08-24"
updated: "2026-08-24"
---
# Experimental pushy input delivery mode
Add an explicit, experimental mode that submits routed messages to the target
pane's input for interactive coding agents that manage queued user prompts.
## Required operator contract
`tamq --mode pushy REPO...` opts the whole managed endpoint into pushy
delivery. The default remains `--mode output`; `--mode inbox` retains durable
inbox-only behavior, and `--no-display` remains a compatibility alias for
inbox mode.
Pushy delivery injects exactly one sanitized, single-line comment of the form
`#sender: body [message-id]` and then one Enter key. This is deliberately safe
as a no-op when an ordinary shell has an empty prompt, while coding-agent TUIs
can receive it as submitted user input. The message becomes `injected` after
tmux accepts both operations. Pushy mode may append to input already being
edited and is therefore experimental and explicitly opt-in.
## Add explicit endpoint modes
```task
id: TAMQ-WP-0010-T01
status: progress
priority: high
```
Add `--mode output|inbox|pushy`, retain compatible `--no-display` behavior,
reject conflicting mode flags, register the selected endpoint mode, expose a
pushy service capability, and report the selected mode in startup status.
## Submit safely framed input
```task
id: TAMQ-WP-0010-T02
status: todo
priority: high
```
Sanitize all controls and collapse multiline messages into escaped text. Use
one checked tmux command list to send the literal comment and a distinct Enter
operation. Do not reuse the terminal-output path, and mark the durable record
`injected` only after tmux accepts both operations.
## Prove and document the experiment
```task
id: TAMQ-WP-0010-T03
status: todo
priority: high
```
Cover CLI conflicts and capability restart, store validation, exact framing,
one-time service delivery, and real tmux input submission. Update help, README,
and SCOPE with the risk boundary; run full checks, install, and exercise a
controlled live pushy session without altering user messages.