Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a03397-4d51-7fd1-8ff2-946eb22ea2bc
This commit is contained in:
parent
00f04da5f6
commit
e284c9f63c
16 changed files with 233 additions and 49 deletions
46
README.md
46
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
|
tamq --mode pushy --command codex flex-auth audit-core
|
||||||
```
|
```
|
||||||
|
|
||||||
The submitted line is sanitized and sender-labelled, then followed by exactly
|
For newly created windows with an explicit command, pushy mode automatically
|
||||||
one Enter key:
|
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
|
```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
|
The tap observes this complete line and queues the same durable route as
|
||||||
comment. Pushy mode cannot determine whether a pane is an agent, a shell, or
|
`@audit-core: ...`; it forwards the source input unchanged to its occupant.
|
||||||
whether someone is already editing input: it can append to that input and
|
`#repo:` is therefore an agent/tap convention, not a shell command—at an
|
||||||
submit the combined line. Use it only for panes whose occupant is known to
|
ordinary shell it remains a comment. Existing windows are never respawned, so
|
||||||
accept or queue asynchronous prompts. Switching an existing endpoint to pushy
|
recreate a session that was originally started without pushy observation.
|
||||||
may also submit pending messages that have never been displayed.
|
|
||||||
|
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
|
## Exchange messages manually
|
||||||
|
|
||||||
|
|
@ -227,7 +244,7 @@ later phase.
|
||||||
2. the local broker assigns endpoint/source identity and durably queues intent;
|
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
|
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
|
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
|
This keeps tmux-specific topology concerns separate from reusable terminal I/O
|
||||||
observation and message identity.
|
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
|
acknowledged. `--mode inbox` selects inbox-only manual mode. Neither becomes
|
||||||
pane input. Experimental `--mode pushy` sends one checked tmux command list
|
pane input. Experimental `--mode pushy` sends one checked tmux command list
|
||||||
containing sanitized literal input followed by Enter, then records the message
|
containing sanitized literal input followed by Enter, then records the message
|
||||||
as `injected`. The older full-duplex pane-input path remains available only
|
as `injected`. When pushy mode creates explicitly commanded windows, the same
|
||||||
with the explicit `tamq start --tap --command ...` opt-in. Both input paths
|
PTY tap observes outbound `@repo:` and `#repo:` lines without rewriting them.
|
||||||
remain subject to the retry and acknowledgement limitations tracked by
|
The older pane-delivery experiment remains available only with the explicit
|
||||||
`TAMQ-WP-0003`.
|
`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
|
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.
|
instance nonce so PID reuse cannot collide with prior leases or receipts.
|
||||||
|
|
||||||
|
|
|
||||||
17
SCOPE.md
17
SCOPE.md
|
|
@ -22,10 +22,11 @@ tamq does not choose or infer them.
|
||||||
- Sanitized one-time output notifications through target tmux pane PTYs, with
|
- Sanitized one-time output notifications through target tmux pane PTYs, with
|
||||||
inbox-only delivery as an explicit option and no foreground-process input.
|
inbox-only delivery as an explicit option and no foreground-process input.
|
||||||
- Explicit experimental pushy delivery that submits a sanitized comment and
|
- 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
|
- Explicit opt-in control-mode pane delivery and the full-duplex `tamq tap` PTY
|
||||||
broker for integration experiments.
|
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
|
- Unix-socket operations for local clients and a future coordination-engine
|
||||||
adapter.
|
adapter.
|
||||||
- Policy profiles, safety-gated retries, local diagnostics, tests, packaging,
|
- Policy profiles, safety-gated retries, local diagnostics, tests, packaging,
|
||||||
|
|
@ -51,12 +52,12 @@ transport.
|
||||||
|
|
||||||
| Intent capability | State | Evidence and remaining gap |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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`. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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,
|
per-window repository identity, stable reuse, shell-native addressing,
|
||||||
latest-counterparty replies, comment-safe target output above an unchanged
|
latest-counterparty replies, comment-safe target output above an unchanged
|
||||||
partial input line and cursor, inbox/filter/ack exchange, zero target-input
|
partial input line and cursor, inbox/filter/ack exchange, zero target-input
|
||||||
mutation, service restart, endpoint disappearance, explicit initial-command
|
mutation in safe modes, service restart, endpoint disappearance, explicit
|
||||||
startup, and cleanup.
|
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:
|
Suitable today:
|
||||||
|
|
||||||
|
|
@ -95,7 +98,7 @@ Not yet suitable:
|
||||||
and stronger process-supervision evidence.
|
and stronger process-supervision evidence.
|
||||||
- Cross-host messaging or use as a general-purpose broker.
|
- 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
|
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%,
|
tap and CLI orchestration; PTY statement coverage increased from 23% to 33%,
|
||||||
while subprocess behavior is primarily proven by the real-tmux test. The
|
while subprocess behavior is primarily proven by the real-tmux test. The
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
| workplan | TAMQ-WP-0008 | finished | — | workplans/TAMQ-WP-0008-reply-shorthand-stable-output.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-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-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-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-T01 | done | — | workplans/TAMQ-WP-0001-statehub-bootstrap.md |
|
||||||
| task | TAMQ-WP-0001-T02 | 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-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-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-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-T01 | done | — | 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-T02 | done | — | 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-T03 | done | — | workplans/TAMQ-WP-0011-hash-routing-for-pushy-agents.md |
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,7 @@ manual messaging from a managed shell:
|
||||||
@TARGET: MESSAGE...
|
@TARGET: MESSAGE...
|
||||||
@ compose with a visible, Tab-selectable recipient
|
@ compose with a visible, Tab-selectable recipient
|
||||||
@ MESSAGE... fast reply (ordinary shell quoting applies)
|
@ MESSAGE... fast reply (ordinary shell quoting applies)
|
||||||
|
#TARGET: MESSAGE... agent-input alias in a tapped pushy session
|
||||||
tamq inbox [--filter COMMAND]
|
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:
|
if args.no_service and args.mode is not None:
|
||||||
print("tamq: --mode cannot be combined with --no-service", file=sys.stderr)
|
print("tamq: --mode cannot be combined with --no-service", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
|
tap_enabled = args.tap or (
|
||||||
|
selected_mode == "pushy" and args.initial_command is not None
|
||||||
|
)
|
||||||
manager = TmuxManager()
|
manager = TmuxManager()
|
||||||
try:
|
try:
|
||||||
if not args.no_service:
|
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)
|
print("tamq service failed to start with the required delivery capability; use --no-service to open repos without messaging", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
try:
|
try:
|
||||||
endpoint = manager.ensure_plan(launch_plan, tap=args.tap)
|
endpoint = manager.ensure_plan(launch_plan, tap=tap_enabled)
|
||||||
except (TmuxError, OSError) as exc:
|
except (TmuxError, OSError) as exc:
|
||||||
print(f"tamq: {exc}", file=sys.stderr)
|
print(f"tamq: {exc}", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
|
|
@ -393,6 +397,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
"service": not args.no_service,
|
"service": not args.no_service,
|
||||||
"messaging": registered,
|
"messaging": registered,
|
||||||
"mode": "none" if args.no_service else ("tap" if args.tap else selected_mode),
|
"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 (
|
"delivery_mode": "none" if args.no_service else (
|
||||||
"pane" if args.tap else endpoint_delivery_mode
|
"pane" if args.tap else endpoint_delivery_mode
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
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)
|
@dataclass(frozen=True)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
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 (
|
return (
|
||||||
f"#{terminal_safe(sender)}: {terminal_safe(body)} "
|
f"# from {terminal_safe(sender)}: {terminal_safe(body)} "
|
||||||
f"[{terminal_safe(message_id)}]"
|
f"[{terminal_safe(message_id)}]"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,7 @@ class TmuxManager:
|
||||||
command = (
|
command = (
|
||||||
[
|
[
|
||||||
*self.tamq_command,
|
*self.tamq_command,
|
||||||
"tap", "--repo", repo, "--endpoint", endpoint_id, "--", *plan.command,
|
"tap", "--repo", repo, "--endpoint", instance_id, "--", *plan.command,
|
||||||
]
|
]
|
||||||
if tap
|
if tap
|
||||||
else list(plan.command)
|
else list(plan.command)
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,13 @@ def test_broker_preserves_identity(tmp_path):
|
||||||
row = store.list()[0]
|
row = store.list()[0]
|
||||||
assert row["sender_repo"] == "net-kingdom"
|
assert row["sender_repo"] == "net-kingdom"
|
||||||
assert row["endpoint_id"] == "tmux-amq-42"
|
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"
|
||||||
|
|
|
||||||
|
|
@ -209,10 +209,11 @@ def test_pushy_mode_registers_explicit_delivery_mode(monkeypatch, capsys):
|
||||||
class Manager:
|
class Manager:
|
||||||
def preflight(self, repos, command):
|
def preflight(self, repos, command):
|
||||||
assert repos == ["a", "b"]
|
assert repos == ["a", "b"]
|
||||||
|
assert command == "codex"
|
||||||
return "plan"
|
return "plan"
|
||||||
|
|
||||||
def ensure_plan(self, plan, *, tap=True):
|
def ensure_plan(self, plan, *, tap=True):
|
||||||
assert tap is False
|
assert tap is True
|
||||||
return endpoint
|
return endpoint
|
||||||
|
|
||||||
def rollback(self, value):
|
def rollback(self, value):
|
||||||
|
|
@ -231,10 +232,13 @@ def test_pushy_mode_registers_explicit_delivery_mode(monkeypatch, capsys):
|
||||||
)
|
)
|
||||||
monkeypatch.setattr("tamq.cli.request", register)
|
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)
|
summary = json.loads(capsys.readouterr().out)
|
||||||
assert summary["mode"] == "pushy"
|
assert summary["mode"] == "pushy"
|
||||||
assert summary["delivery_mode"] == "pushy"
|
assert summary["delivery_mode"] == "pushy"
|
||||||
|
assert summary["input_observation_requested"] is True
|
||||||
assert requests[0]["delivery_mode"] == "pushy"
|
assert requests[0]["delivery_mode"] == "pushy"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class RecordingBroker:
|
||||||
|
|
||||||
def inspect_line(self, line):
|
def inspect_line(self, line):
|
||||||
self.lines.append(line)
|
self.lines.append(line)
|
||||||
return line.startswith("@")
|
return line.startswith(("@", "#"))
|
||||||
|
|
||||||
|
|
||||||
def test_input_observer_accepts_raw_terminal_carriage_returns():
|
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)
|
tap = PtyTap(["true"], broker, on_line=observed.append)
|
||||||
buffer = bytearray()
|
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")
|
tap._observe_input(buffer, b"lo\rplain line\n")
|
||||||
|
|
||||||
assert broker.lines == ["@activity-core: hello", "plain line"]
|
assert broker.lines == ["#activity-core: hello", "plain line"]
|
||||||
assert observed == ["@activity-core: hello"]
|
assert observed == ["#activity-core: hello"]
|
||||||
|
|
||||||
|
|
||||||
def test_copy_winsize_preserves_rows_columns_and_pixels():
|
def test_copy_winsize_preserves_rows_columns_and_pixels():
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
from tamq.routing import parse_address_line
|
from tamq.routing import parse_address_line
|
||||||
|
|
||||||
|
|
||||||
def test_direct_address():
|
@pytest.mark.parametrize("prefix", ["@", "#"])
|
||||||
assert parse_address_line("@railiance-platform: do something!").body == "do something!"
|
def test_direct_address(prefix):
|
||||||
assert parse_address_line("@railiance-platform: do something!").target_repo == "railiance-platform"
|
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():
|
def test_ordinary_input_is_unchanged():
|
||||||
assert parse_address_line("hello @repo: not at start") is None
|
assert parse_address_line("hello @repo: not at start") is None
|
||||||
assert parse_address_line("@repo:") 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
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ def test_service_pushy_mode_submits_once_and_marks_injected(tmp_path, monkeypatc
|
||||||
assert control.submitted == [
|
assert control.submitted == [
|
||||||
(
|
(
|
||||||
"tamq:repo-b",
|
"tamq:repo-b",
|
||||||
f"#repo-a: first\\x0asecond [{message_id}]",
|
f"# from repo-a: first\\x0asecond [{message_id}]",
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
assert store.list()[0]["state"] == "injected"
|
assert store.list()[0]["state"] == "injected"
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ def test_comment_format_escapes_controls_and_prefixes_every_line():
|
||||||
|
|
||||||
def test_pushy_input_is_one_sanitized_shell_comment():
|
def test_pushy_input_is_one_sanitized_shell_comment():
|
||||||
assert format_pushy_input("repo-a", "first\nsecond\x1b[31m", "m-1") == (
|
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]"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -242,10 +242,10 @@ def test_real_tmux_pushy_mode_submits_one_shell_safe_input(tmp_path, monkeypatch
|
||||||
service = Service(store=store)
|
service = Service(store=store)
|
||||||
service._deliver_once()
|
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
|
deadline = time.monotonic() + 5
|
||||||
while time.monotonic() < deadline:
|
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:
|
if expected in capture:
|
||||||
break
|
break
|
||||||
time.sleep(0.05)
|
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
|
assert store.db.execute("SELECT COUNT(*) FROM leases").fetchone()[0] == 0
|
||||||
|
|
||||||
service._deliver_once()
|
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
|
assert repeated_capture.count(expected) == 1
|
||||||
finally:
|
finally:
|
||||||
store.close()
|
store.close()
|
||||||
manager._run("kill-server", check=False)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
tap_calls = [call for call in calls if call and call[0] == "send-keys"]
|
||||||
assert len(tap_calls) == 2
|
assert len(tap_calls) == 2
|
||||||
assert all("tamq tap" in " ".join(call) for call in tap_calls)
|
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 all("codex --quiet" in " ".join(call) for call in tap_calls)
|
||||||
assert ("select-window", "-t", "tamq-test:a") in calls
|
assert ("select-window", "-t", "tamq-test:a") in calls
|
||||||
assert any(call and call[0] == "set-option" for call in calls)
|
assert any(call and call[0] == "set-option" for call in calls)
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ type: workplan
|
||||||
title: "Hash-prefixed routing for pushy agent sessions"
|
title: "Hash-prefixed routing for pushy agent sessions"
|
||||||
domain: communication
|
domain: communication
|
||||||
repo: tmux-amq
|
repo: tmux-amq
|
||||||
status: active
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: coulomb-social
|
topic_slug: coulomb-social
|
||||||
planning_priority: P0
|
planning_priority: P0
|
||||||
planning_order: 16
|
planning_order: 16
|
||||||
created: "2026-08-24"
|
created: "2026-08-24"
|
||||||
updated: "2026-08-24"
|
updated: "2026-08-25"
|
||||||
state_hub_workstream_id: "9a38f0c3-bd98-51ae-a6ad-502bef94a38f"
|
state_hub_workstream_id: "9a38f0c3-bd98-51ae-a6ad-502bef94a38f"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ comment and a clear coding-agent prompt.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: TAMQ-WP-0011-T01
|
id: TAMQ-WP-0011-T01
|
||||||
status: progress
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "d50b09b2-e4ad-5eb4-b833-2d9a62b5ff1f"
|
state_hub_task_id: "d50b09b2-e4ad-5eb4-b833-2d9a62b5ff1f"
|
||||||
```
|
```
|
||||||
|
|
@ -48,7 +48,7 @@ validation, sender identity, and ordinary-input pass-through.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: TAMQ-WP-0011-T02
|
id: TAMQ-WP-0011-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "1170ae19-a2ce-5f50-8a43-41b9625621e7"
|
state_hub_task_id: "1170ae19-a2ce-5f50-8a43-41b9625621e7"
|
||||||
```
|
```
|
||||||
|
|
@ -61,7 +61,7 @@ command is supplied and keep legacy `--tap` behavior distinct.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: TAMQ-WP-0011-T03
|
id: TAMQ-WP-0011-T03
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "2a355d88-5272-58c2-a4f8-82df6cac7c3e"
|
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.
|
creates one durable message, one target submission, and no bounced message.
|
||||||
Document the interaction, run full checks, install, and exercise a controlled
|
Document the interaction, run full checks, install, and exercise a controlled
|
||||||
live commanded pushy session if it can be isolated from user input.
|
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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue