feat: add shell-native message routing
Some checks failed
tamq-ci / test (push) Failing after 5s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a03397-4d51-7fd1-8ff2-946eb22ea2bc
This commit is contained in:
tegwick 2026-08-24 20:42:35 +02:00
parent 398fe316b4
commit e995cab1b8
12 changed files with 391 additions and 42 deletions

View file

@ -57,7 +57,24 @@ windows and never run another initial command in them.
## Exchange messages manually
Each managed shell exports its own repository slug as `TAMQ_REPO`. From the
`flex-auth` window, queue a message:
`flex-auth` window, queue a message using the repository command installed for
the session:
```bash
@audit-core: please review the auth boundary
```
The spelling without the trailing colon is equivalent:
```bash
@audit-core please review the auth boundary
```
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`.
Tamq refuses to overwrite unrelated commands. The shims only use tamq's durable
send operation. The long form remains available:
```bash
tamq send '@audit-core: please review the auth boundary'
@ -70,14 +87,33 @@ tamq inbox
tamq ack <message-id>
```
Human inbox output is safe to paste into an ordinary shell because every line
is a comment. It includes the durable id needed by `ack`:
```text
#flex-auth: please review the auth boundary [m-...]
```
To explicitly consume pending messages through a command, use an inbox filter:
```bash
tamq inbox --filter 'cat >> msg.log'
```
The command runs once per pending message with the comment form on standard
input. `TAMQ_MESSAGE_ID`, `TAMQ_SENDER_REPO`, and `TAMQ_TARGET_REPO` are set in
its environment. A zero exit acknowledges that message; a non-zero exit leaves
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`. Manual messages remain durable and pending until acknowledged. They
are never injected as terminal keystrokes, so they cannot corrupt a command
being typed in the target pane.
After upgrading from the earlier Codex-default alpha, recreate the managed
session once so existing panes are replaced by neutral shells and the broker is
re-registered in manual delivery mode:
After upgrading from an earlier alpha, recreate the managed session once so
existing panes inherit the neutral shell contract, repository command `PATH`,
and manual broker registration:
```bash
tamq stop

View file

@ -16,7 +16,9 @@ tamq does not choose or infer them.
- Local SQLite message history, leases, endpoint registrations, delivery state,
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.
- Durable manual send/inbox/acknowledgement with per-window repository identity,
shell-native address commands, comment-safe display, and explicit pull-time
filters.
- 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.
@ -44,11 +46,11 @@ transport.
| Intent capability | State | Evidence and remaining gap |
| --- | --- | --- |
| Direct repository addressing | Implemented | Exact `gita` validation and `@repo:` parsing are covered by tests. |
| 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. |
| 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 | Manual endpoints never inject pending messages into panes. Installed testing proves send/inbox/ack exchange while the target pane remains unchanged; legacy rows migrate to manual mode. |
| Safe manual messaging | Implemented for local alpha | Manual endpoints never inject pending messages into panes. Installed testing proves shell-native send, comment-safe inbox, explicit filter/ack exchange, and an unchanged target pane; legacy rows migrate to manual mode. |
| 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 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 | Explicit acknowledgement and the configuration field exist; delivery always marks a successful tmux injection as `injected`, irrespective of `delivery_ack_mode`. |
@ -59,16 +61,17 @@ transport.
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, manual send/inbox/ack exchange,
zero target-pane mutation, service restart, endpoint disappearance, explicit
initial-command startup, and cleanup.
per-window repository identity, stable reuse, shell-native addressing,
comment-safe inbox/filter/ack exchange, zero target-pane mutation, service
restart, endpoint disappearance, explicit initial-command startup, and cleanup.
Suitable today:
- Local queue, history, export/replay, and diagnostic use.
- Interactive local shell or explicitly commanded sessions over one or more
gita-registered repositories.
- Durable manual message exchange between managed repository windows.
- Durable manual message exchange between managed repository windows, including
explicit pull-time loggers and filters.
- Developing and testing the future coordination-engine adapter against the
local socket boundary.
@ -80,7 +83,7 @@ Not yet suitable:
and stronger process-supervision evidence.
- Cross-host messaging or use as a general-purpose broker.
The suite currently has 77 passing tests and 74% statement coverage. Coverage
The suite currently has 85 passing tests and 75% 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

@ -14,7 +14,7 @@
| workplan | TAMQ-WP-0003 | active | — | workplans/TAMQ-WP-0003-delivery-reliability.md |
| workplan | TAMQ-WP-0004 | finished | — | workplans/TAMQ-WP-0004-operator-installable-local-alpha.md |
| workplan | TAMQ-WP-0005 | finished | — | workplans/TAMQ-WP-0005-terminal-neutral-manual-messaging.md |
| workplan | TAMQ-WP-0006 | active | — | workplans/TAMQ-WP-0006-shell-native-message-routing.md |
| workplan | TAMQ-WP-0006 | finished | — | workplans/TAMQ-WP-0006-shell-native-message-routing.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 |
@ -35,8 +35,8 @@
| task | TAMQ-WP-0005-T02 | done | — | workplans/TAMQ-WP-0005-terminal-neutral-manual-messaging.md |
| task | TAMQ-WP-0005-T03 | done | — | workplans/TAMQ-WP-0005-terminal-neutral-manual-messaging.md |
| task | TAMQ-WP-0005-T04 | done | — | workplans/TAMQ-WP-0005-terminal-neutral-manual-messaging.md |
| task | TAMQ-WP-0006-T01 | progress | — | workplans/TAMQ-WP-0006-shell-native-message-routing.md |
| task | TAMQ-WP-0006-T02 | todo | — | workplans/TAMQ-WP-0006-shell-native-message-routing.md |
| task | TAMQ-WP-0006-T03 | todo | — | workplans/TAMQ-WP-0006-shell-native-message-routing.md |
| task | TAMQ-WP-0006-T04 | todo | — | workplans/TAMQ-WP-0006-shell-native-message-routing.md |
| task | TAMQ-WP-0006-T05 | todo | — | workplans/TAMQ-WP-0006-shell-native-message-routing.md |
| task | TAMQ-WP-0006-T01 | done | — | workplans/TAMQ-WP-0006-shell-native-message-routing.md |
| task | TAMQ-WP-0006-T02 | done | — | workplans/TAMQ-WP-0006-shell-native-message-routing.md |
| task | TAMQ-WP-0006-T03 | done | — | workplans/TAMQ-WP-0006-shell-native-message-routing.md |
| task | TAMQ-WP-0006-T04 | done | — | workplans/TAMQ-WP-0006-shell-native-message-routing.md |
| task | TAMQ-WP-0006-T05 | done | — | workplans/TAMQ-WP-0006-shell-native-message-routing.md |

View file

@ -7,6 +7,7 @@ import asyncio
import json
import os
import signal
import sqlite3
import fcntl
from uuid import uuid4
import subprocess
@ -132,10 +133,42 @@ def attach_session(session: str) -> int:
return subprocess.run(command, check=False).returncode
def _terminal_safe(text: str) -> str:
return "".join(
character
if character.isprintable()
else f"\\x{ord(character):02x}"
for character in text
)
def format_comment_message(row: sqlite3.Row) -> str:
"""Render a durable message so every displayed line remains a shell comment."""
sender = _terminal_safe(str(row["sender_repo"]))
body = str(row["body"])
lines = body.splitlines() or [""]
rendered = [f"#{sender}: {_terminal_safe(lines[0])}"]
rendered.extend(f"# {_terminal_safe(line)}" for line in lines[1:])
rendered[-1] += f" [{_terminal_safe(str(row['message_id']))}]"
return "\n".join(rendered)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="tamq",
description="Repository-aware tmux sessions with durable local messaging.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""session shorthand:
tamq [--detach] [--command COMMAND] REPO [REPO ...]
With no --command, tamq opens ordinary repository shells. --command is an
explicit initial command for newly created windows; it does not opt in to
terminal observation or message injection.
manual messaging from a managed shell:
@TARGET: MESSAGE...
tamq inbox [--filter COMMAND]
""",
)
parser.add_argument("--version", "-V", action="version", version=__version__)
parser.add_argument("--orwell", action="store_true", help="enable unsafe local diagnostics")
@ -163,6 +196,11 @@ def build_parser() -> argparse.ArgumentParser:
inbox.add_argument("--repo", dest="target_repo", help="target repository (default: TAMQ_REPO in a managed window)")
inbox.add_argument("--all", action="store_true", help="include non-pending messages")
inbox.add_argument("--json", action="store_true", help="emit one JSON object per message")
inbox.add_argument(
"--filter",
dest="filter_command",
help="consume each pending comment through COMMAND and acknowledge it after a zero exit",
)
inspect = subparsers.add_parser("inspect", help="inspect one message")
inspect.add_argument("message_id")
ack = subparsers.add_parser("ack", help="acknowledge one durable message")
@ -370,12 +408,42 @@ def main(argv: list[str] | None = None) -> int:
validate_targets([target])
except RegistryError as exc:
print(f"tamq: {exc}", file=sys.stderr); return 2
if args.filter_command is not None and not args.filter_command.strip():
print("tamq: --filter command must not be empty", file=sys.stderr)
return 2
if args.filter_command and (args.all or args.json):
print("tamq: --filter cannot be combined with --all or --json", file=sys.stderr)
return 2
rows = store.list(target, None if args.all else "pending")
for row in rows:
if args.json:
print(json.dumps(dict(row), sort_keys=True))
elif args.filter_command:
environment = os.environ.copy()
environment.update(
{
"TAMQ_MESSAGE_ID": row["message_id"],
"TAMQ_SENDER_REPO": row["sender_repo"],
"TAMQ_TARGET_REPO": row["target_repo"],
}
)
result = subprocess.run(
args.filter_command,
shell=True,
input=format_comment_message(row) + "\n",
text=True,
env=environment,
check=False,
)
if result.returncode:
print(
f"tamq: filter failed for {row['message_id']} with exit status {result.returncode}; message remains pending",
file=sys.stderr,
)
return 1
store.acknowledge(row["message_id"])
else:
print(f"{row['message_id']} {row['sender_repo']} -> {row['target_repo']}: {row['body']}")
print(format_comment_message(row))
return 0
if args.command == "history":
for row in store.list(args.target_repo, args.state): print(json.dumps(dict(row), sort_keys=True))

View file

@ -1,6 +1,7 @@
from __future__ import annotations
import os
import re
import shlex
import shutil
from uuid import uuid4
@ -9,6 +10,7 @@ from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
from .config import state_dir
from .registry import repository_paths, validate_targets
@ -48,11 +50,65 @@ class TmuxManager:
*,
tmux_command: Sequence[str] | None = None,
tamq_command: Sequence[str] | None = None,
command_dir: Path | None = None,
):
self.session = session
socket_name = os.environ.get("TAMQ_TMUX_SOCKET")
self.tmux_command = tuple(tmux_command or (["tmux", "-L", socket_name] if socket_name else ["tmux"]))
self.tamq_command = tuple(tamq_command or ["tamq"])
if tamq_command is None:
self.tamq_command = (shutil.which("tamq") or "tamq",)
else:
self.tamq_command = tuple(tamq_command)
configured_command_dir = os.environ.get("TAMQ_COMMAND_DIR")
if command_dir is not None:
self.command_dir = command_dir
elif configured_command_dir:
self.command_dir = Path(configured_command_dir)
elif (
len(self.tamq_command) == 1
and Path(self.tamq_command[0]).is_absolute()
and os.access(Path(self.tamq_command[0]).parent, os.W_OK | os.X_OK)
):
self.command_dir = Path(self.tamq_command[0]).parent
else:
self.command_dir = state_dir() / "commands" / session
def _install_address_commands(self, repos: Sequence[str]) -> None:
"""Install tamq-owned commands without modifying a user's shell files."""
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}")
for repo in repos:
if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", repo) is None:
raise TmuxError(
f"repository name cannot be exposed as a shell command: {repo!r}"
)
target = f"@{repo}:"
script = (
"#!/bin/sh\n"
"# tamq-address-command v1\n"
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)
def _window_environment(self, repo: str) -> tuple[str, ...]:
path = os.environ.get("PATH", "")
command_path = f"{self.command_dir}{os.pathsep}{path}" if path else str(self.command_dir)
return "-e", f"TAMQ_REPO={repo}", "-e", f"PATH={command_path}"
def _run(self, *args: str, check: bool = True) -> str:
result = subprocess.run([*self.tmux_command, *args], text=True, capture_output=True, check=False)
@ -133,7 +189,7 @@ class TmuxManager:
first_repo = plan.repos[0]
self._run(
"new-session", "-d", "-s", self.session, "-n", "__tamq_boot",
"-e", f"TAMQ_REPO={first_repo}",
*self._window_environment(first_repo),
"-c", plan.paths[first_repo],
)
created_session = True
@ -151,6 +207,12 @@ class TmuxManager:
self._run("set-option", "-t", self.session, "@tamq_instance_id", instance_id)
self._run("set-option", "-t", self.session, "@tamq_managed", "1")
windows = self._run("list-windows", "-t", self.session, "-F", "#{window_name}").splitlines()
addressable_repos = [repo for repo in windows if repo != "__tamq_boot"]
addressable_repos.extend(repo for repo in plan.repos if repo not in addressable_repos)
self._install_address_commands(addressable_repos)
command_path = self._window_environment(plan.repos[0])[-1].removeprefix("PATH=")
self._run("set-environment", "-t", self.session, "PATH", command_path)
self._run("set-option", "-t", self.session, "@tamq_command_dir", str(self.command_dir))
for index, repo in enumerate(plan.repos):
if repo in windows:
continue
@ -162,7 +224,7 @@ class TmuxManager:
else:
self._run(
"new-window", "-d", "-t", self.session, "-n", repo,
"-e", f"TAMQ_REPO={repo}", "-c", plan.paths[repo],
*self._window_environment(repo), "-c", plan.paths[repo],
)
windows.append(repo)
created_windows.append(repo)

View file

@ -13,6 +13,16 @@ def test_help_and_version(capsys):
assert "usage:" in capsys.readouterr().out
def test_root_help_exposes_repository_shorthand_and_command(capsys):
with pytest.raises(SystemExit) as exc:
main(["--help"])
assert exc.value.code == 0
output = capsys.readouterr().out
assert "tamq [--detach] [--command COMMAND] REPO [REPO ...]" in output
assert "With no --command" in output
assert "@TARGET: MESSAGE" in output
def test_attach_delegates_to_tmux(monkeypatch):
calls = []
monkeypatch.delenv("TMUX", raising=False)

View file

@ -91,6 +91,8 @@ def test_isolated_installed_tool_session_smoke(tmp_path):
timeout=60,
)
assert run(str(tamq), "--version").stdout.strip() == "0.1.0"
root_help = run(str(tamq), "--help").stdout
assert "tamq [--detach] [--command COMMAND] REPO [REPO ...]" in root_help
started = json.loads(
run(
str(tamq),
@ -176,7 +178,7 @@ def test_isolated_installed_tool_session_smoke(tmp_path):
"send-keys",
"-t",
"tamq:railiance-platform",
f"{tamq} send '@activity-core: installed-message'",
"@activity-core: installed-message",
"C-m",
)
deadline = time.monotonic() + 5
@ -191,10 +193,16 @@ def test_isolated_installed_tool_session_smoke(tmp_path):
if inbox:
break
time.sleep(0.05)
assert inbox, run(
*tmux, "capture-pane", "-p", "-t", "tamq:railiance-platform"
).stdout
assert inbox[-1]["sender_repo"] == "railiance-platform"
assert inbox[-1]["body"] == "installed-message"
assert inbox[-1]["state"] == "pending"
message_id = inbox[-1]["message_id"]
assert run(str(tamq), "inbox", "--repo", "activity-core").stdout == (
f"#railiance-platform: installed-message [{message_id}]\n"
)
target_after = run(
*tmux, "capture-pane", "-p", "-t", "tamq:activity-core"
).stdout
@ -209,6 +217,32 @@ def test_isolated_installed_tool_session_smoke(tmp_path):
).stdout.splitlines()
]
assert all_messages[-1]["state"] == "acknowledged"
run(
*tmux,
"send-keys",
"-t",
"tamq:railiance-platform",
"@activity-core filtered-message",
"C-m",
)
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
pending = run(
str(tamq), "inbox", "--repo", "activity-core", "--json"
).stdout
if "filtered-message" in pending:
break
time.sleep(0.05)
message_log = tmp_path / "msg.log"
run(
str(tamq), "inbox", "--repo", "activity-core", "--filter",
f"cat >> {message_log}",
)
assert "#railiance-platform: filtered-message [m-" in message_log.read_text(
encoding="utf-8"
)
assert run(str(tamq), "inbox", "--repo", "activity-core").stdout == ""
status = json.loads(run(str(tamq), "status").stdout)
assert status["service"] is True
assert [item["endpoint_id"] for item in status["endpoints"]] == [started["instance_id"]]

View file

@ -1,6 +1,8 @@
import json
import shlex
from tamq.cli import main
from tamq.store import Store
async def service_is_down():
@ -35,3 +37,64 @@ def test_inbox_requires_repository_outside_managed_window(tmp_path, monkeypatch,
monkeypatch.delenv("TAMQ_REPO", raising=False)
assert main(["inbox"]) == 2
assert "requires --repo" in capsys.readouterr().err
def test_human_inbox_is_comment_safe_on_every_line(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
monkeypatch.setattr("tamq.cli.validate_targets", lambda repos: None)
store = Store(tmp_path / "state" / "tamq.sqlite3")
message_id = store.add("flex-auth", "audit-core", "first\nsecond\x1b[31m")
store.close()
assert main(["inbox", "--repo", "audit-core"]) == 0
assert capsys.readouterr().out == (
f"#flex-auth: first\n# second\\x1b[31m [{message_id}]\n"
)
def test_inbox_filter_logs_and_acknowledges_successful_messages(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
monkeypatch.setattr("tamq.cli.validate_targets", lambda repos: None)
store = Store(tmp_path / "state" / "tamq.sqlite3")
first_id = store.add("flex-auth", "audit-core", "review 'quoted' $value")
second_id = store.add("railiance-platform", "audit-core", "second")
store.close()
log = tmp_path / "messages.log"
assert main([
"inbox", "--repo", "audit-core", "--filter",
f"cat >> {shlex.quote(str(log))}",
]) == 0
assert log.read_text(encoding="utf-8") == (
f"#flex-auth: review 'quoted' $value [{first_id}]\n"
f"#railiance-platform: second [{second_id}]\n"
)
assert main(["inbox", "--repo", "audit-core"]) == 0
assert capsys.readouterr().out == ""
def test_failed_inbox_filter_leaves_current_and_later_messages_pending(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
monkeypatch.setattr("tamq.cli.validate_targets", lambda repos: None)
store = Store(tmp_path / "state" / "tamq.sqlite3")
first_id = store.add("a", "audit-core", "first")
second_id = store.add("b", "audit-core", "second")
store.close()
assert main(["inbox", "--repo", "audit-core", "--filter", "false"]) == 1
assert first_id in capsys.readouterr().err
assert main(["inbox", "--repo", "audit-core", "--json"]) == 0
rows = [json.loads(line) for line in capsys.readouterr().out.splitlines()]
assert [row["message_id"] for row in rows] == [first_id, second_id]
assert all(row["state"] == "pending" for row in rows)
def test_inbox_filter_rejects_non_pending_and_json_modes(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("TAMQ_STATE_DIR", str(tmp_path / "state"))
monkeypatch.setattr("tamq.cli.validate_targets", lambda repos: None)
assert main(["inbox", "--repo", "audit-core", "--all", "--filter", "cat"]) == 2
assert "cannot be combined" in capsys.readouterr().err
assert main(["inbox", "--repo", "audit-core", "--json", "--filter", "cat"]) == 2
assert "cannot be combined" in capsys.readouterr().err
assert main(["inbox", "--repo", "audit-core", "--filter", " "]) == 2
assert "must not be empty" in capsys.readouterr().err

View file

@ -46,6 +46,7 @@ def test_tap_propagates_terminal_size_resize_and_raw_mouse_input(tmp_path, monke
session,
tmux_command=("tmux", "-L", socket_name),
tamq_command=(sys.executable, "-m", "tamq.cli"),
command_dir=tmp_path / "commands",
)
plan = LaunchPlan(
("activity-core",),

View file

@ -30,6 +30,7 @@ def test_real_tmux_starts_two_repo_windows_and_reuses_them(tmp_path, monkeypatch
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"),

View file

@ -1,10 +1,13 @@
import os
import subprocess
from tamq import tmux
import pytest
def test_tmux_manager_builds_tap_windows(tmp_path, monkeypatch):
calls = []
manager = tmux.TmuxManager("tamq-test")
manager = tmux.TmuxManager("tamq-test", command_dir=tmp_path / "commands")
repo_a = tmp_path / "a"
repo_b = tmp_path / "b"
repo_a.mkdir()
@ -42,7 +45,7 @@ def test_tmux_manager_builds_tap_windows(tmp_path, monkeypatch):
def test_tmux_manager_reuses_session_instance_id(tmp_path, monkeypatch):
repo = tmp_path / "a"
repo.mkdir()
manager = tmux.TmuxManager("tamq-test")
manager = tmux.TmuxManager("tamq-test", command_dir=tmp_path / "commands")
options = {"@tamq_managed": "1"}
monkeypatch.setattr(tmux, "validate_targets", lambda repos: None)
monkeypatch.setattr(tmux, "repository_paths", lambda: {"a": str(repo)})
@ -74,7 +77,7 @@ def test_tmux_manager_reuses_session_instance_id(tmp_path, monkeypatch):
def test_preflight_rejects_foreign_session(tmp_path, monkeypatch):
repo = tmp_path / "a"
repo.mkdir()
manager = tmux.TmuxManager("tamq-test")
manager = tmux.TmuxManager("tamq-test", command_dir=tmp_path / "commands")
monkeypatch.setattr(tmux, "validate_targets", lambda repos: None)
monkeypatch.setattr(tmux, "repository_paths", lambda: {"a": str(repo)})
monkeypatch.setattr(tmux.shutil, "which", lambda command: f"/bin/{command}")
@ -95,7 +98,7 @@ def test_preflight_rejects_foreign_session(tmp_path, monkeypatch):
def test_tmux_only_plan_starts_agent_without_tap(tmp_path, monkeypatch):
manager = tmux.TmuxManager("tamq-test")
manager = tmux.TmuxManager("tamq-test", command_dir=tmp_path / "commands")
calls = []
plan = tmux.LaunchPlan(("a",), {"a": str(tmp_path)}, ("codex", "--quiet"))
@ -120,7 +123,8 @@ def test_tmux_only_plan_starts_agent_without_tap(tmp_path, monkeypatch):
def test_neutral_plan_starts_shell_without_sending_keystrokes(tmp_path, monkeypatch):
manager = tmux.TmuxManager("tamq-test")
command_dir = tmp_path / "commands"
manager = tmux.TmuxManager("tamq-test", command_dir=command_dir)
calls = []
plan = tmux.LaunchPlan(("a", "b"), {"a": str(tmp_path), "b": str(tmp_path)}, ())
@ -145,12 +149,53 @@ def test_neutral_plan_starts_shell_without_sending_keystrokes(tmp_path, monkeypa
new_window = next(call for call in calls if call and call[0] == "new-window")
assert "TAMQ_REPO=a" in new_session
assert "TAMQ_REPO=b" in new_window
assert f"PATH={command_dir}{os.pathsep}" in " ".join(new_session)
assert f"PATH={command_dir}{os.pathsep}" in " ".join(new_window)
assert (command_dir / "@a").is_file()
assert (command_dir / "@a:").is_file()
assert (command_dir / "@b").is_file()
assert (command_dir / "@b:").is_file()
def test_address_commands_preserve_message_arguments(tmp_path):
command_dir = tmp_path / "commands"
manager = tmux.TmuxManager(
"tamq-test", tamq_command=("echo",), command_dir=command_dir,
)
manager._install_address_commands(["audit-core"])
for name in ("@audit-core", "@audit-core:"):
result = subprocess.run(
[str(command_dir / name), "Some message!", "$value", "; literal", "--from"],
text=True,
capture_output=True,
check=True,
)
assert result.stdout == "send -- @audit-core: Some message! $value ; literal --from\n"
def test_address_commands_reject_unsafe_repository_names(tmp_path):
manager = tmux.TmuxManager("tamq-test", command_dir=tmp_path / "commands")
with pytest.raises(tmux.TmuxError, match="cannot be exposed"):
manager._install_address_commands(["../outside"])
def test_address_commands_do_not_replace_unowned_commands(tmp_path):
command_dir = tmp_path / "commands"
command_dir.mkdir()
existing = command_dir / "@audit-core"
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()
manager = tmux.TmuxManager("tamq-test")
manager = tmux.TmuxManager("tamq-test", command_dir=tmp_path / "commands")
monkeypatch.setattr(tmux, "validate_targets", lambda repos: None)
monkeypatch.setattr(tmux, "repository_paths", lambda: {"a": str(repo)})
monkeypatch.setattr(tmux.shutil, "which", lambda command: None if command == "missing-agent" else f"/bin/{command}")
@ -161,7 +206,7 @@ def test_preflight_rejects_missing_agent(tmp_path, monkeypatch):
def test_preflight_deduplicates_repositories(tmp_path, monkeypatch):
repo = tmp_path / "a"
repo.mkdir()
manager = tmux.TmuxManager("tamq-test")
manager = tmux.TmuxManager("tamq-test", command_dir=tmp_path / "commands")
monkeypatch.setattr(tmux, "validate_targets", lambda repos: None)
monkeypatch.setattr(tmux, "repository_paths", lambda: {"a": str(repo)})
monkeypatch.setattr(tmux.shutil, "which", lambda command: f"/bin/{command}")

View file

@ -4,7 +4,7 @@ type: workplan
title: "Shell-native message routing and explicit inbox filters"
domain: communication
repo: tmux-amq
status: active
status: finished
owner: codex
topic_slug: coulomb-social
planning_priority: P0
@ -39,7 +39,9 @@ tamq flex-auth audit-core
@flex-auth Another message
```
These are per-session executable command shims, not shell aliases. They enqueue
These are tamq-owned executable command shims, not shell aliases. They are
placed in the operator command directory that already exposes `tamq`, with an
explicit command-directory override and isolated fallback. They enqueue
messages and do nothing else by default. Reading a manual inbox produces
comment-safe text:
@ -62,7 +64,7 @@ background, modifies a pane, or becomes a startup default.
```task
id: TAMQ-WP-0006-T01
status: progress
status: done
priority: high
state_hub_task_id: "f6228d55-37f0-5be6-a868-f681f09d091b"
```
@ -70,26 +72,35 @@ state_hub_task_id: "f6228d55-37f0-5be6-a868-f681f09d091b"
Document repository-first invocation and `--command COMMAND` in `tamq --help`,
including the terminal-neutral default and exact opt-in nature of the command.
Completed with a root-help shorthand section that names `--command COMMAND`,
the ordinary-shell default, and the absence of implicit terminal observation or
message injection.
## Install shell-native repository address commands
```task
id: TAMQ-WP-0006-T02
status: todo
status: done
priority: high
state_hub_task_id: "432e737c-b34e-56aa-8003-3f61e8ef4faf"
```
Create a private command directory for the managed session, populate it with
safe `@repo` and `@repo:` entry points for every requested repository, and add
that directory to each newly created window's environment. Invocation must
route through tamq's existing validation and durable send path. Reject unsafe
repository names rather than creating ambiguous executable paths.
Populate the operator command directory that exposes `tamq` with safe `@repo`
and `@repo:` entry points for every requested repository; support an explicit
`TAMQ_COMMAND_DIR` and isolated state fallback, and also add the selected
directory to each newly created window's environment. Invocation must route
through tamq's existing validation and durable send path. Reject unsafe
repository names and refuse to overwrite commands not owned by tamq.
Completed with executable `@repo` and `@repo:` shims beside the installed tamq
command, `TAMQ_COMMAND_DIR` override and state fallback, tmux PATH propagation,
safe repository-name checks, tamq ownership markers, and collision refusal.
## Make manual inbox output comment-safe
```task
id: TAMQ-WP-0006-T03
status: todo
status: done
priority: high
state_hub_task_id: "b4ff430d-3b09-56de-9c1a-20afb018964c"
```
@ -98,11 +109,15 @@ Render human-readable messages as shell comments prefixed with `#sender:` and
retain the message id for acknowledgement. Keep structured JSON stable for
automation.
Completed with `#sender:` rendering on every body line, durable ids on the
final line, and escaping of non-printing terminal control characters. JSON
output retains the original structured record.
## Add explicit pull-time inbox filters
```task
id: TAMQ-WP-0006-T04
status: todo
status: done
priority: high
state_hub_task_id: "d413ad83-06ff-5de1-97aa-256b4f684007"
```
@ -113,11 +128,15 @@ metadata through environment variables, acknowledge only after a zero exit,
and leave failed and subsequent messages pending. Filters are explicit inbox
consumers and never background terminal interceptors.
Completed with sequential pull-time filters, bounded metadata environment,
comment-only standard input, success acknowledgement, fail-fast pending
retention, and rejection of ambiguous `--all`/`--json` combinations.
## Prove and document the ordinary-shell workflow
```task
id: TAMQ-WP-0006-T05
status: todo
status: done
priority: high
state_hub_task_id: "da957158-fb75-5232-9610-7684ebca45b5"
```
@ -126,3 +145,10 @@ Cover root help, both address spellings, spaces and shell metacharacters in
message bodies, unknown targets, comment-safe inbox output, successful and
failed filters, acknowledgement behavior, path isolation, and installed tmux
acceptance. Update the operator quickstart and practical command reference.
Completed with 85 passing tests and 75% statement coverage. The isolated
installed-package test proves root help, real ordinary-shell command lookup
after shell startup, both neutral panes, durable delivery, comment-safe output,
filter acknowledgement, target-pane immutability, restart, recovery, and exact
explicit-command startup. A live user-install smoke repeated shell-native
routing with no target-pane change and left zero pending messages.