diff --git a/README.md b/README.md index 227d07a..e2e6e03 100644 --- a/README.md +++ b/README.md @@ -71,19 +71,36 @@ input. It is intended for coding-agent interfaces that queue user prompts: tamq --mode pushy --command codex flex-auth audit-core ``` -The submitted line is sanitized and sender-labelled, then followed by exactly -one Enter key: +For newly created windows with an explicit command, pushy mode automatically +places that command behind tamq's transparent PTY tap. Inside the coding-agent +input, use `#repo:` to avoid interfaces that reserve `@` for file mentions or +templates: ```text -#flex-auth: please review the auth boundary [m-...] +#audit-core: Please review the authentication change. ``` -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. +The tap observes this complete line and queues the same durable route as +`@audit-core: ...`; it forwards the source input unchanged to its occupant. +`#repo:` is therefore an agent/tap convention, not a shell command—at an +ordinary shell it remains a comment. Existing windows are never respawned, so +recreate a session that was originally started without pushy observation. + +The submitted target line is sanitized, sender-labelled, deliberately +non-routable, and then followed by exactly one Enter key: + +```text +# from flex-auth: please review the auth boundary [m-...] +``` + +The `# from` shape cannot match the outbound `#repo:` grammar, preventing a +tapped recipient from bouncing the message back. Its leading `#` also 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 @@ -227,7 +244,7 @@ later phase. 2. the local broker assigns endpoint/source identity and durably queues intent; 3. Explicit `tamq tap` mode is a full-duplex PTY proxy around a command. It forwards bytes unchanged in raw terminal mode, propagates terminal resize and lifecycle - signals, and observes complete input lines for `@repo:` routing. + signals, and observes complete input lines for `@repo:`/`#repo:` routing. This keeps tmux-specific topology concerns separate from reusable terminal I/O observation and message identity. @@ -237,10 +254,11 @@ the target pane's PTY output and stays pending in the durable inbox until 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`. +as `injected`. When pushy mode creates explicitly commanded windows, the same +PTY tap observes outbound `@repo:` and `#repo:` lines without rewriting them. +The older pane-delivery experiment 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-`; each boot also receives an instance nonce so PID reuse cannot collide with prior leases or receipts. diff --git a/SCOPE.md b/SCOPE.md index 7ada7a9..45b1646 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -22,10 +22,11 @@ tamq does not choose or infer them. - 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. + Enter to target pane input for known queue-capable interactive programs, + with `#repo:` routing observed through the PTY tap. - 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. +- Exact `gita` repository validation and direct `@repo:`/`#repo:` routing. - Unix-socket operations for local clients and a future coordination-engine adapter. - Policy profiles, safety-gated retries, local diagnostics, tests, packaging, @@ -51,12 +52,12 @@ transport. | Intent capability | State | Evidence and remaining gap | | --- | --- | --- | -| Direct repository addressing | Implemented for local alpha | Exact `gita` validation, per-session `@repo`/`@repo:` commands, a bare `@` composer with latest-counterparty default and Tab recipient cycling, and long-form parsing are covered without modifying shell configuration. | +| Direct repository addressing | Implemented for local alpha | Exact `gita` validation, per-session `@repo`/`@repo:` shell commands, tapped `#repo:` agent-input routing, a bare `@` composer with latest-counterparty default and Tab recipient cycling, and long-form parsing are covered. | | 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 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. | +| Experimental pushy delivery | Explicit opt-in | `--mode pushy --command ...` observes outbound `#repo:`/`@repo:` lines and submits a non-routable `# from sender:` envelope 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. | @@ -70,8 +71,10 @@ shells at exact gita paths without sending initial keystrokes. The test proved per-window repository identity, stable reuse, shell-native addressing, 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. +mutation in safe modes, service restart, endpoint disappearance, explicit +initial-command startup, and cleanup. Isolated pushy-mode coverage additionally +proves one `#repo:` route, one non-routable target submission, and no feedback +message. Suitable today: @@ -95,7 +98,7 @@ Not yet suitable: and stronger process-supervision evidence. - Cross-host messaging or use as a general-purpose broker. -The suite currently has 117 passing tests and 77% statement coverage. Coverage +The suite currently has 120 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 diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 93ce8ce..778c123 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -19,7 +19,7 @@ | 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 | finished | — | workplans/TAMQ-WP-0010-experimental-pushy-delivery.md | -| workplan | TAMQ-WP-0011 | active | — | workplans/TAMQ-WP-0011-hash-routing-for-pushy-agents.md | +| workplan | TAMQ-WP-0011 | finished | — | workplans/TAMQ-WP-0011-hash-routing-for-pushy-agents.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 | @@ -60,6 +60,6 @@ | task | TAMQ-WP-0010-T01 | done | — | workplans/TAMQ-WP-0010-experimental-pushy-delivery.md | | task | TAMQ-WP-0010-T02 | done | — | workplans/TAMQ-WP-0010-experimental-pushy-delivery.md | | task | TAMQ-WP-0010-T03 | done | — | workplans/TAMQ-WP-0010-experimental-pushy-delivery.md | -| task | TAMQ-WP-0011-T01 | progress | — | workplans/TAMQ-WP-0011-hash-routing-for-pushy-agents.md | -| task | TAMQ-WP-0011-T02 | todo | — | workplans/TAMQ-WP-0011-hash-routing-for-pushy-agents.md | -| task | TAMQ-WP-0011-T03 | todo | — | workplans/TAMQ-WP-0011-hash-routing-for-pushy-agents.md | +| task | TAMQ-WP-0011-T01 | done | — | workplans/TAMQ-WP-0011-hash-routing-for-pushy-agents.md | +| task | TAMQ-WP-0011-T02 | done | — | workplans/TAMQ-WP-0011-hash-routing-for-pushy-agents.md | +| task | TAMQ-WP-0011-T03 | done | — | workplans/TAMQ-WP-0011-hash-routing-for-pushy-agents.md | diff --git a/src/tamq/cli.py b/src/tamq/cli.py index 5754171..b6f41a5 100644 --- a/src/tamq/cli.py +++ b/src/tamq/cli.py @@ -169,6 +169,7 @@ manual messaging from a managed shell: @TARGET: MESSAGE... @ compose with a visible, Tab-selectable recipient @ MESSAGE... fast reply (ordinary shell quoting applies) + #TARGET: MESSAGE... agent-input alias in a tapped pushy session tamq inbox [--filter COMMAND] """, ) @@ -336,6 +337,9 @@ def main(argv: list[str] | None = None) -> int: if args.no_service and args.mode is not None: print("tamq: --mode cannot be combined with --no-service", file=sys.stderr) return 2 + tap_enabled = args.tap or ( + selected_mode == "pushy" and args.initial_command is not None + ) manager = TmuxManager() try: if not args.no_service: @@ -356,7 +360,7 @@ def main(argv: list[str] | None = None) -> int: print("tamq service failed to start with the required delivery capability; use --no-service to open repos without messaging", file=sys.stderr) return 1 try: - endpoint = manager.ensure_plan(launch_plan, tap=args.tap) + endpoint = manager.ensure_plan(launch_plan, tap=tap_enabled) except (TmuxError, OSError) as exc: print(f"tamq: {exc}", file=sys.stderr) return 2 @@ -393,6 +397,7 @@ def main(argv: list[str] | None = None) -> int: "service": not args.no_service, "messaging": registered, "mode": "none" if args.no_service else ("tap" if args.tap else selected_mode), + "input_observation_requested": tap_enabled, "delivery_mode": "none" if args.no_service else ( "pane" if args.tap else endpoint_delivery_mode ), diff --git a/src/tamq/routing.py b/src/tamq/routing.py index 8d15fab..d8e25cb 100644 --- a/src/tamq/routing.py +++ b/src/tamq/routing.py @@ -3,7 +3,7 @@ from __future__ import annotations import re from dataclasses import dataclass -ADDRESS = re.compile(r"^@([a-z0-9][a-z0-9._-]*):(?:[ \t]*)(.+)$", re.IGNORECASE) +ADDRESS = re.compile(r"^[@#]([a-z0-9][a-z0-9._-]*):(?:[ \t]*)(.+)$", re.IGNORECASE) @dataclass(frozen=True) diff --git a/src/tamq/terminal.py b/src/tamq/terminal.py index ab97f02..95a0026 100644 --- a/src/tamq/terminal.py +++ b/src/tamq/terminal.py @@ -30,9 +30,9 @@ def format_comment(sender: str, body: str, message_id: str) -> str: def format_pushy_input(sender: str, body: str, message_id: str) -> str: - """Render one shell-safe input line for explicit pushy delivery.""" + """Render one shell-safe, non-routable line for pushy delivery.""" return ( - f"#{terminal_safe(sender)}: {terminal_safe(body)} " + f"# from {terminal_safe(sender)}: {terminal_safe(body)} " f"[{terminal_safe(message_id)}]" ) diff --git a/src/tamq/tmux.py b/src/tamq/tmux.py index 72d3359..f8424fe 100644 --- a/src/tamq/tmux.py +++ b/src/tamq/tmux.py @@ -246,7 +246,7 @@ class TmuxManager: command = ( [ *self.tamq_command, - "tap", "--repo", repo, "--endpoint", endpoint_id, "--", *plan.command, + "tap", "--repo", repo, "--endpoint", instance_id, "--", *plan.command, ] if tap else list(plan.command) diff --git a/tests/test_broker.py b/tests/test_broker.py index 327d478..b815d9a 100644 --- a/tests/test_broker.py +++ b/tests/test_broker.py @@ -9,3 +9,13 @@ def test_broker_preserves_identity(tmp_path): row = store.list()[0] assert row["sender_repo"] == "net-kingdom" assert row["endpoint_id"] == "tmux-amq-42" + + +def test_broker_accepts_hash_address_alias(tmp_path): + store = Store(tmp_path / "queue.sqlite3") + broker = InputBroker(store, BrokerIdentity("tmux-amq-42-boot", "net-kingdom")) + assert broker.inspect_line("#railiance-platform: hello") is not None + row = store.list()[0] + assert row["sender_repo"] == "net-kingdom" + assert row["target_repo"] == "railiance-platform" + assert row["endpoint_id"] == "tmux-amq-42-boot" diff --git a/tests/test_cli.py b/tests/test_cli.py index 781928c..e43b7d5 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -209,10 +209,11 @@ def test_pushy_mode_registers_explicit_delivery_mode(monkeypatch, capsys): class Manager: def preflight(self, repos, command): assert repos == ["a", "b"] + assert command == "codex" return "plan" def ensure_plan(self, plan, *, tap=True): - assert tap is False + assert tap is True return endpoint def rollback(self, value): @@ -231,10 +232,13 @@ def test_pushy_mode_registers_explicit_delivery_mode(monkeypatch, capsys): ) monkeypatch.setattr("tamq.cli.request", register) - assert main(["start", "--detach", "--mode", "pushy", "a", "b"]) == 0 + assert main([ + "start", "--detach", "--mode", "pushy", "--command", "codex", "a", "b" + ]) == 0 summary = json.loads(capsys.readouterr().out) assert summary["mode"] == "pushy" assert summary["delivery_mode"] == "pushy" + assert summary["input_observation_requested"] is True assert requests[0]["delivery_mode"] == "pushy" diff --git a/tests/test_ptytap.py b/tests/test_ptytap.py index 28742b3..8b99e91 100644 --- a/tests/test_ptytap.py +++ b/tests/test_ptytap.py @@ -13,7 +13,7 @@ class RecordingBroker: def inspect_line(self, line): self.lines.append(line) - return line.startswith("@") + return line.startswith(("@", "#")) def test_input_observer_accepts_raw_terminal_carriage_returns(): @@ -22,11 +22,11 @@ def test_input_observer_accepts_raw_terminal_carriage_returns(): tap = PtyTap(["true"], broker, on_line=observed.append) buffer = bytearray() - tap._observe_input(buffer, b"@activity-core: hel") + tap._observe_input(buffer, b"#activity-core: hel") tap._observe_input(buffer, b"lo\rplain line\n") - assert broker.lines == ["@activity-core: hello", "plain line"] - assert observed == ["@activity-core: hello"] + assert broker.lines == ["#activity-core: hello", "plain line"] + assert observed == ["#activity-core: hello"] def test_copy_winsize_preserves_rows_columns_and_pixels(): diff --git a/tests/test_routing.py b/tests/test_routing.py index 8df82f7..2d59355 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -1,11 +1,17 @@ +import pytest + from tamq.routing import parse_address_line -def test_direct_address(): - assert parse_address_line("@railiance-platform: do something!").body == "do something!" - assert parse_address_line("@railiance-platform: do something!").target_repo == "railiance-platform" +@pytest.mark.parametrize("prefix", ["@", "#"]) +def test_direct_address(prefix): + routed = parse_address_line(f"{prefix}railiance-platform: do something!") + assert routed.body == "do something!" + assert routed.target_repo == "railiance-platform" def test_ordinary_input_is_unchanged(): assert parse_address_line("hello @repo: not at start") is None assert parse_address_line("@repo:") is None + assert parse_address_line("# from repo: inbound envelope") is None + assert parse_address_line("##repo: not an address") is None diff --git a/tests/test_service_delivery.py b/tests/test_service_delivery.py index 8038743..593393e 100644 --- a/tests/test_service_delivery.py +++ b/tests/test_service_delivery.py @@ -74,7 +74,7 @@ def test_service_pushy_mode_submits_once_and_marks_injected(tmp_path, monkeypatc assert control.submitted == [ ( "tamq:repo-b", - f"#repo-a: first\\x0asecond [{message_id}]", + f"# from repo-a: first\\x0asecond [{message_id}]", ) ] assert store.list()[0]["state"] == "injected" diff --git a/tests/test_terminal_output.py b/tests/test_terminal_output.py index c0fb2f6..4d7d763 100644 --- a/tests/test_terminal_output.py +++ b/tests/test_terminal_output.py @@ -21,7 +21,7 @@ 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]" + "# from repo-a: first\\x0asecond\\x1b[31m [m-1]" ) diff --git a/tests/test_tmux_integration.py b/tests/test_tmux_integration.py index cbf1a6e..dee87b2 100644 --- a/tests/test_tmux_integration.py +++ b/tests/test_tmux_integration.py @@ -242,10 +242,10 @@ def test_real_tmux_pushy_mode_submits_one_shell_safe_input(tmp_path, monkeypatch service = Service(store=store) service._deliver_once() - expected = f"#flex-auth: What's next?\\x0asecond [{message_id}]" + expected = f"# from flex-auth: What's next?\\x0asecond [{message_id}]" deadline = time.monotonic() + 5 while time.monotonic() < deadline: - capture = manager._run("capture-pane", "-p", "-t", target) + capture = manager._run("capture-pane", "-p", "-J", "-t", target) if expected in capture: break time.sleep(0.05) @@ -254,8 +254,128 @@ def test_real_tmux_pushy_mode_submits_one_shell_safe_input(tmp_path, monkeypatch assert store.db.execute("SELECT COUNT(*) FROM leases").fetchone()[0] == 0 service._deliver_once() - repeated_capture = manager._run("capture-pane", "-p", "-t", target) + repeated_capture = manager._run("capture-pane", "-p", "-J", "-t", target) assert repeated_capture.count(expected) == 1 finally: store.close() manager._run("kill-server", check=False) + + +@pytest.mark.skipif(shutil.which("tmux") is None, reason="tmux is not installed") +def test_real_tmux_hash_route_pushes_once_without_feedback(tmp_path, monkeypatch): + repo_a = tmp_path / "railiance-platform" + repo_b = tmp_path / "activity-core" + bin_dir = tmp_path / "bin" + for path in (repo_a, repo_b, bin_dir): + path.mkdir() + gita = bin_dir / "gita" + gita.write_text( + "#!/bin/sh\n" + "if [ \"$1\" = ls ]; then printf 'railiance-platform activity-core\\n'; exit 0; fi\n" + "exit 2\n", + encoding="utf-8", + ) + gita.chmod(0o755) + fixture = tmp_path / "agent_fixture.py" + fixture.write_text( + "import sys\n" + "print('AGENT-READY', flush=True)\n" + "for line in sys.stdin:\n" + " print('AGENT:' + line.rstrip('\\r\\n'), flush=True)\n", + encoding="utf-8", + ) + project_src = str(Path(__file__).resolve().parents[1] / "src") + existing_pythonpath = os.environ.get("PYTHONPATH") + monkeypatch.setenv( + "PYTHONPATH", + project_src if not existing_pythonpath else f"{project_src}:{existing_pythonpath}", + ) + monkeypatch.setenv("PATH", f"{bin_dir}:{os.environ['PATH']}") + monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state")) + socket_name = f"tamq-hash-{os.getpid()}-{uuid4().hex[:8]}" + session = f"tamq-hash-{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", + ) + plan = LaunchPlan( + ("railiance-platform", "activity-core"), + { + "railiance-platform": str(repo_a), + "activity-core": str(repo_b), + }, + (sys.executable, str(fixture)), + ) + store = None + + try: + endpoint = manager.ensure_plan(plan, tap=True) + deadline = time.monotonic() + 5 + while time.monotonic() < deadline: + captures = { + repo: manager._run("capture-pane", "-p", "-t", f"{session}:{repo}") + for repo in plan.repos + } + if all("AGENT-READY" in output for output in captures.values()): + break + time.sleep(0.05) + assert all("AGENT-READY" in output for output in captures.values()) + + store = Store(tmp_path / "state" / "tamq.sqlite3") + store.register_endpoint( + endpoint.instance_key, + endpoint.pid, + endpoint.session, + endpoint.repos, + "pushy", + ) + manager._run( + "send-keys", + "-t", + f"{session}:railiance-platform", + "-l", + "--", + "#activity-core: Hello!", + ) + manager._run("send-keys", "-t", f"{session}:railiance-platform", "Enter") + + deadline = time.monotonic() + 5 + while time.monotonic() < deadline: + rows = store.list() + if rows: + break + time.sleep(0.05) + assert len(rows) == 1 + row = rows[0] + assert row["sender_repo"] == "railiance-platform" + assert row["target_repo"] == "activity-core" + assert row["body"] == "Hello!" + assert row["endpoint_id"] == endpoint.instance_key + + service = Service(store=store) + service._deliver_once() + expected = f"# from railiance-platform: Hello! [{row['message_id']}]" + deadline = time.monotonic() + 5 + while time.monotonic() < deadline: + target_capture = manager._run( + "capture-pane", "-p", "-J", "-t", f"{session}:activity-core" + ) + if f"AGENT:{expected}" in target_capture: + break + time.sleep(0.05) + assert f"AGENT:{expected}" in target_capture + assert store.list()[0]["state"] == "injected" + + service._deliver_once() + time.sleep(0.1) + assert len(store.list()) == 1 + assert manager._run( + "capture-pane", "-p", "-J", "-t", f"{session}:activity-core" + ).count(f"AGENT:{expected}") == 1 + finally: + if store is not None: + store.close() + manager._run("kill-server", check=False) diff --git a/tests/test_tmux_manager.py b/tests/test_tmux_manager.py index 3f0f093..9ed92a2 100644 --- a/tests/test_tmux_manager.py +++ b/tests/test_tmux_manager.py @@ -37,6 +37,7 @@ def test_tmux_manager_builds_tap_windows(tmp_path, monkeypatch): tap_calls = [call for call in calls if call and call[0] == "send-keys"] assert len(tap_calls) == 2 assert all("tamq tap" in " ".join(call) for call in tap_calls) + assert all(endpoint.instance_id in " ".join(call) for call in tap_calls) assert all("codex --quiet" in " ".join(call) for call in tap_calls) assert ("select-window", "-t", "tamq-test:a") in calls assert any(call and call[0] == "set-option" for call in calls) diff --git a/workplans/TAMQ-WP-0011-hash-routing-for-pushy-agents.md b/workplans/TAMQ-WP-0011-hash-routing-for-pushy-agents.md index 0b92a25..814eeff 100644 --- a/workplans/TAMQ-WP-0011-hash-routing-for-pushy-agents.md +++ b/workplans/TAMQ-WP-0011-hash-routing-for-pushy-agents.md @@ -4,13 +4,13 @@ type: workplan title: "Hash-prefixed routing for pushy agent sessions" domain: communication repo: tmux-amq -status: active +status: finished owner: codex topic_slug: coulomb-social planning_priority: P0 planning_order: 16 created: "2026-08-24" -updated: "2026-08-24" +updated: "2026-08-25" state_hub_workstream_id: "9a38f0c3-bd98-51ae-a6ad-502bef94a38f" --- @@ -35,7 +35,7 @@ comment and a clear coding-agent prompt. ```task id: TAMQ-WP-0011-T01 -status: progress +status: done priority: high state_hub_task_id: "d50b09b2-e4ad-5eb4-b833-2d9a62b5ff1f" ``` @@ -48,7 +48,7 @@ validation, sender identity, and ordinary-input pass-through. ```task id: TAMQ-WP-0011-T02 -status: todo +status: done priority: high state_hub_task_id: "1170ae19-a2ce-5f50-8a43-41b9625621e7" ``` @@ -61,7 +61,7 @@ command is supplied and keep legacy `--tap` behavior distinct. ```task id: TAMQ-WP-0011-T03 -status: todo +status: done priority: high state_hub_task_id: "2a355d88-5272-58c2-a4f8-82df6cac7c3e" ``` @@ -71,3 +71,20 @@ CLI, PTY, and real two-window tmux coverage proving one `#repo:` source line creates one durable message, one target submission, and no bounced message. Document the interaction, run full checks, install, and exercise a controlled live commanded pushy session if it can be isolated from user input. + +## Completion evidence + +- `make check`: 120 tests passed, including a real isolated two-window tmux + test proving one `#activity-core:` source line creates one durable message, + one submitted target prompt, an `injected` transition, and no feedback or + duplicate delivery on a second service pass. +- Coverage: 77% overall; routing is 100%, tmux management is 82%, terminal + framing is 85%, and the service is 77%. +- `make install`: installed `tmux-amq==0.1.0` with hash routing and automatic + PTY observation for newly created commanded pushy windows. +- The inbound `# from sender:` envelope is deliberately outside the outbound + route grammar, preventing tapped targets from bouncing injected messages. +- The operator's existing session was not recreated or modified. Existing + windows must be recreated to gain PTY observation; this preserves live input. +- Bounded retry and acknowledgement-policy residuals remain owned by + `TAMQ-WP-0003`; this workplan creates no additional residual record.