feat: deliver installable local alpha sessions
Some checks failed
tamq-ci / test (push) Failing after 6s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a03397-4d51-7fd1-8ff2-946eb22ea2bc
This commit is contained in:
tegwick 2026-08-24 16:10:59 +02:00
parent d56bd3c79f
commit 5774fbd548
18 changed files with 890 additions and 94 deletions

View file

@ -11,6 +11,12 @@
# Install dependencies
uv sync --extra dev
# Install/upgrade the user command
make install
# Start the operator alpha session
tamq start --command codex railiance-platform activity-core
# Run tests
uv run pytest

View file

@ -12,6 +12,9 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: sudo apt-get update
- run: sudo apt-get install -y tmux
- run: python -m pip install uv
- run: python -m pip install -e '.[dev]'
- run: pytest -q
- run: python -m tamq.cli --help

View file

@ -266,6 +266,10 @@ To create a new workplan:
## Repository-specific workflow
- Install development dependencies with `uv sync --extra dev`.
- Install or upgrade the user command with `make install`.
- The operator alpha acceptance command is
`tamq start --command codex railiance-platform activity-core`; it must open
both exact gita paths and launch Codex once in each new window.
- Run the test suite with `uv run pytest` or `make test`.
- Before handoff, run `make check`; it combines tests, whitespace validation,
and Python bytecode compilation.

View file

@ -1,4 +1,13 @@
.PHONY: test check
.PHONY: install uninstall test check
install:
@command -v uv >/dev/null || { echo "tamq: uv is required; install it from https://docs.astral.sh/uv/" >&2; exit 1; }
uv tool install --force --reinstall --refresh .
@command -v tamq >/dev/null || { echo "tamq: installed, but the uv tool bin directory is not on PATH; run 'uv tool update-shell'" >&2; exit 1; }
tamq --version
uninstall:
uv tool uninstall tmux-amq
test:
uv run pytest

View file

@ -3,10 +3,62 @@
Tmux Agentic Message Queueing (`tamq`) is a local, durable message queue for
agent workers running in gita-registered repositories.
The initial implementation is being bootstrapped from the standalone queue and
CLI contract. It will provide tmux endpoint lifecycle, local SQLite history and
leases, direct `@repo:` routing, JSONL export/replay, and a Unix-socket attach
protocol for a later coordination-engine adapter.
The local alpha provides tmux endpoint lifecycle, local SQLite history and
leases, direct `@repo:` routing, JSONL export/replay, and a Unix-socket protocol
for a later coordination-engine adapter.
## Install and start a local session
Prerequisites must be available on `PATH`: Python 3.11+, [uv](https://docs.astral.sh/uv/),
tmux, gita, and the agent command you want to run (`codex` by default). Each
repository must already have an exact gita slug and path.
Install or upgrade `tamq` as an isolated user tool:
```bash
make install
command -v tamq
tamq --version
```
Start an attached two-repository Codex session:
```bash
tamq start --command codex railiance-platform activity-core
```
This creates or reuses the managed `tamq` session, opens same-named windows in
the exact gita paths for `railiance-platform` and `activity-core`, launches Codex
behind `tamq tap` in each new window, and attaches to the first window. Codex is
the default, so this is equivalent:
```bash
tamq start railiance-platform activity-core
```
Set up the session without attaching, then attach later:
```bash
tamq start --detach --command codex railiance-platform activity-core
tamq status
tamq attach
```
Repeated starts reuse existing windows and do not launch a second agent in
them. `--cmd` remains an alias for `--command`. A quoted command may include
arguments, for example `--command 'codex --profile local'`.
For degraded tmux-only use, `tamq start --no-service ...` skips socket endpoint
registration and message delivery. Stop the broker and remove the managed tmux
session explicitly when finished:
```bash
tamq stop
tmux kill-session -t tamq
```
Uninstall the user tool from the checkout with `make uninstall`, or from
anywhere with `uv tool uninstall tmux-amq`.
## Development
@ -18,7 +70,7 @@ uv run tamq start --cmd codex net-kingdom railiance-platform
uv run tamq attach
```
The bootstrap currently includes the local SQLite queue and a tmux endpoint
The alpha includes the local SQLite queue and a tmux endpoint
manager, strict gita target validation, and a control-mode client for tmux
operations/injection, and the PTY tap for full-duplex input observation. The
control-mode client alone does not expose arbitrary pane input; `tamq tap` is
@ -63,10 +115,13 @@ safety_gated_max_attempts = 2
delivery_ack_mode = "acknowledged"
```
Retries remain safety-gated and capped at nine attempts. Use `--policy-profile`
to select a profile; `--orwell` enables explicitly unsafe local diagnostics.
Policy profiles accept a safety-gated retry cap of at most nine attempts, but
the alpha delivery path does not enforce attempt counting yet. Explicit
acknowledgement exists, while `delivery_ack_mode` enforcement and bounded retry
state are tracked in `TAMQ-WP-0003`. Use `--policy-profile` to select a profile;
`--orwell` enables explicitly unsafe local diagnostics.
The Unix socket service now supports structured `ping`, `register`, `send`, and
`history` operations. Endpoint registrations and messages are persisted in the
same local SQLite database; delivery remains delegated to the control-mode
adapter.
The Unix socket service supports structured `ping`, `register`, `send`,
`history`, `ack`, `endpoints`, and `disconnect` operations. Endpoint
registrations and messages are persisted in the same local SQLite database;
delivery remains delegated to the control-mode adapter.

View file

@ -42,7 +42,7 @@ transport.
| Direct repository addressing | Implemented | Exact `gita` validation and `@repo:` parsing are covered by tests. |
| Durable, inspectable local queue | Implemented | SQLite history, 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. |
| Tmux endpoint lifecycle and delivery | Implemented, integration confidence limited | The manager, control client, and delivery loop exist, but tests use fakes and CI does not exercise real tmux panes or agent processes. |
| Tmux endpoint lifecycle and delivery | Implemented for local alpha | Real isolated-tmux and installed-package tests prove exact repository windows, stable reuse, control-mode injection, durable history transition, service stop, and bounded cleanup. |
| Full-duplex input observation | Implemented, lightly proven | `tamq tap` preserves PTY traffic and observes complete address lines; PTY code coverage is 23%. |
| 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`. |
@ -50,15 +50,19 @@ transport.
## Practical Usability
The following operator path was exercised successfully on 2026-08-24 with an
isolated state directory: start the socket service, ping it, inspect status,
queue a message to registered repo `tmux-amq`, inspect history, export one JSONL
record, and stop the service.
The installable alpha path was exercised successfully on 2026-08-24 with both
isolated fixtures and the operator's registered repositories. `make install`
produced a user-level `tamq` command; `tamq start --detach --command codex
railiance-platform activity-core` created two same-named live panes rooted at
their exact gita paths. Repeating the command retained the same session instance
and pane PIDs. The automated installed-package path also proved message
injection, history, status, service stop, and cleanup.
Suitable today:
- Local queue, history, export/replay, and diagnostic use.
- Controlled experiments with gita-registered repositories and tmux endpoints.
- Interactive local Codex sessions over one or more gita-registered repositories.
- Controlled message-routing experiments between managed tmux endpoints.
- Developing and testing the future coordination-engine adapter against the
local socket boundary.
@ -66,20 +70,20 @@ Not yet suitable:
- Unattended or high-confidence delivery where bounded retries and positive
recipient acknowledgement are required.
- Production-style operation without a real tmux/PTY integration test suite,
crash-recovery evidence, and stronger process supervision evidence.
- Production-style operation without full PTY lifecycle, crash-recovery, and
stronger process-supervision evidence.
- Cross-host messaging or use as a general-purpose broker.
The suite currently has 50 passing tests and 73% statement coverage. Coverage
The suite currently has 63 passing tests and 75% statement coverage. Coverage
is strongest in durable storage and registry handling, and weakest in the PTY
tap, control-mode process handling, and CLI orchestration. The Forgejo CI job
runs unit tests and CLI help/version smoke checks on Python 3.11, but does not
install or exercise tmux and gita end to end.
tap and CLI orchestration. The Forgejo CI job installs tmux and uv, runs the
real-tmux and isolated installed-package session tests with a deterministic gita
fixture, and retains CLI help/version smoke checks on Python 3.11.
## Next Usability Gates
- `TAMQ-WP-0003` owns bounded retry state, acknowledgement enforcement,
lifecycle recovery, and real tmux/PTY integration evidence.
- `TAMQ-WP-0003` owns bounded retry state, acknowledgement enforcement, crash
recovery, and deeper PTY lifecycle evidence.
- `TAMQ-WP-0002` owns the coordination-engine adapter after the local delivery
contract is sufficiently reliable.

View file

@ -12,7 +12,7 @@
| workplan | TAMQ-WP-0001 | finished | — | workplans/TAMQ-WP-0001-statehub-bootstrap.md |
| workplan | TAMQ-WP-0002 | active | — | workplans/TAMQ-WP-0002-coordination-engine-adapter.md |
| workplan | TAMQ-WP-0003 | active | — | workplans/TAMQ-WP-0003-delivery-reliability.md |
| workplan | TAMQ-WP-0004 | ready | — | workplans/TAMQ-WP-0004-operator-installable-local-alpha.md |
| workplan | TAMQ-WP-0004 | finished | — | workplans/TAMQ-WP-0004-operator-installable-local-alpha.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 |
@ -24,8 +24,8 @@
| task | TAMQ-WP-0003-T02 | wait | — | workplans/TAMQ-WP-0003-delivery-reliability.md |
| task | TAMQ-WP-0003-T03 | todo | — | workplans/TAMQ-WP-0003-delivery-reliability.md |
| task | TAMQ-WP-0003-T04 | wait | — | workplans/TAMQ-WP-0003-delivery-reliability.md |
| task | TAMQ-WP-0004-T01 | todo | — | workplans/TAMQ-WP-0004-operator-installable-local-alpha.md |
| task | TAMQ-WP-0004-T02 | todo | — | workplans/TAMQ-WP-0004-operator-installable-local-alpha.md |
| task | TAMQ-WP-0004-T03 | todo | — | workplans/TAMQ-WP-0004-operator-installable-local-alpha.md |
| task | TAMQ-WP-0004-T04 | wait | — | workplans/TAMQ-WP-0004-operator-installable-local-alpha.md |
| task | TAMQ-WP-0004-T05 | wait | — | workplans/TAMQ-WP-0004-operator-installable-local-alpha.md |
| task | TAMQ-WP-0004-T01 | done | — | workplans/TAMQ-WP-0004-operator-installable-local-alpha.md |
| task | TAMQ-WP-0004-T02 | done | — | workplans/TAMQ-WP-0004-operator-installable-local-alpha.md |
| task | TAMQ-WP-0004-T03 | done | — | workplans/TAMQ-WP-0004-operator-installable-local-alpha.md |
| task | TAMQ-WP-0004-T04 | done | — | workplans/TAMQ-WP-0004-operator-installable-local-alpha.md |
| task | TAMQ-WP-0004-T05 | done | — | workplans/TAMQ-WP-0004-operator-installable-local-alpha.md |

View file

@ -67,7 +67,21 @@ def ensure_service() -> bool:
if asyncio.run(ping()):
return True
time.sleep(0.05)
return False
return False
def preflight_runtime_paths() -> None:
"""Ensure local state/runtime parents are available before tmux mutation."""
parents = {db_path().parent, socket_path().parent, pid_path().parent, lock_path().parent}
for parent in parents:
parent.mkdir(parents=True, exist_ok=True)
if not os.access(parent, os.W_OK | os.X_OK):
raise OSError(f"runtime directory is not writable: {parent}")
def attach_session(session: str) -> int:
command = ["tmux", "switch-client" if os.environ.get("TMUX") else "attach-session", "-t", session]
return subprocess.run(command, check=False).returncode
def build_parser() -> argparse.ArgumentParser:
@ -83,9 +97,9 @@ def build_parser() -> argparse.ArgumentParser:
start = subparsers.add_parser("start", help="start or reuse a tamq endpoint")
start.add_argument("repos", nargs="*", help="gita-registered repository slugs")
start.add_argument("--cmd", default="codex", help="agent command for new windows")
start.add_argument("--command", "--cmd", dest="agent_command", default="codex", help="agent command for new windows (default: codex)")
start.add_argument("--detach", action="store_true", help="detach after startup")
start.add_argument("--no-service", action="store_true", help="skip service startup")
start.add_argument("--no-service", action="store_true", help="open tmux windows without service registration or messaging")
attach = subparsers.add_parser("attach", help="attach to the managed tmux session")
attach.add_argument("--session", default="tamq", help="tmux session name")
@ -168,8 +182,7 @@ def main(argv: list[str] | None = None) -> int:
except KeyboardInterrupt: return 0
return 0
if args.command == "attach":
result = subprocess.run(["tmux", "attach-session", "-t", args.session], check=False)
return result.returncode
return attach_session(args.session)
if args.command == "completion":
print(completion_script(args.shell), end="")
return 0
@ -189,30 +202,56 @@ def main(argv: list[str] | None = None) -> int:
finally:
store.close()
if args.command == "start":
manager = TmuxManager()
try:
preflight_runtime_paths()
launch_plan = manager.preflight(args.repos, args.agent_command)
except (TmuxError, OSError) as exc:
print(f"tamq: {exc}", file=sys.stderr)
return 2
if not args.no_service and not ensure_service():
print("tamq service failed to start; use --no-service to open repos without messaging", file=sys.stderr)
return 1
try:
endpoint = TmuxManager().ensure(args.repos, args.cmd)
endpoint = manager.ensure_plan(launch_plan)
except (TmuxError, OSError) as exc:
print(f"tamq: {exc}", file=sys.stderr)
return 2
registration = asyncio.run(request({"op": "register", "endpoint_id": endpoint.endpoint_id, "instance_id": endpoint.instance_key, "pid": endpoint.pid, "session": endpoint.session, "repos": endpoint.repos}))
if not registration.get("ok"):
print(f"tamq: endpoint registration failed: {registration.get('error', 'unknown error')}", file=sys.stderr)
return 1
control = ControlModeClient(endpoint.session)
queue_store = Store(db_path())
try:
control.start()
delivered = InputBroker(queue_store, BrokerIdentity(endpoint.instance_key, endpoint.repos[0])).deliver_pending(
control, window_for_repo=lambda repo: f"{endpoint.session}:{repo}"
)
finally:
control.close()
queue_store.close()
print(json.dumps({"endpoint_id": endpoint.endpoint_id, "instance_id": endpoint.instance_key, "session": endpoint.session, "repos": endpoint.repos, "delivered": delivered}))
return 0
delivered = 0
registered = False
if not args.no_service:
try:
registration = asyncio.run(request({"op": "register", "endpoint_id": endpoint.endpoint_id, "instance_id": endpoint.instance_key, "pid": endpoint.pid, "session": endpoint.session, "repos": endpoint.repos}))
if not registration.get("ok"):
raise RuntimeError(f"endpoint registration failed: {registration.get('error', 'unknown error')}")
registered = True
control = ControlModeClient(endpoint.session)
queue_store = Store(db_path())
try:
control.start()
delivered = InputBroker(queue_store, BrokerIdentity(endpoint.instance_key, endpoint.repos[0])).deliver_pending(
control, window_for_repo=lambda repo: f"{endpoint.session}:{repo}"
)
finally:
control.close()
queue_store.close()
except (OSError, RuntimeError, ValueError, json.JSONDecodeError) as exc:
manager.rollback(endpoint)
print(f"tamq: {exc}", file=sys.stderr)
return 1
summary = {
"endpoint_id": endpoint.endpoint_id,
"instance_id": endpoint.instance_key,
"session": endpoint.session,
"repos": endpoint.repos,
"delivered": delivered,
"service": not args.no_service,
"messaging": registered,
}
print(json.dumps(summary), flush=True)
if args.detach:
return 0
return attach_session(endpoint.session)
store = Store(db_path())
try:
advisory = history_advisory(store)

View file

@ -1,6 +1,8 @@
from __future__ import annotations
import os
import subprocess
from collections.abc import Sequence
from dataclasses import dataclass
@ -11,13 +13,19 @@ class ControlModeError(RuntimeError):
@dataclass
class ControlModeClient:
session: str
tmux_command: Sequence[str] | None = None
process: subprocess.Popen[str] | None = None
def start(self) -> None:
if self.process is not None:
return
socket_name = os.environ.get("TAMQ_TMUX_SOCKET")
command = tuple(
self.tmux_command
or (["tmux", "-L", socket_name] if socket_name else ["tmux"])
)
self.process = subprocess.Popen(
["tmux", "-C", "attach-session", "-t", self.session],
[*command, "-C", "attach-session", "-t", self.session],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
@ -37,7 +45,13 @@ class ControlModeClient:
def close(self) -> None:
if self.process is not None:
self.process.terminate()
process = self.process
process.terminate()
try:
process.wait(timeout=1)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=1)
self.process = None

View file

@ -62,7 +62,7 @@ class Service:
"""Continuously inject pending messages into registered live endpoints."""
while not stopped.is_set():
try:
await asyncio.to_thread(self._deliver_once)
self._deliver_once()
except Exception:
# A disappearing tmux session must not take down the queue.
pass

View file

@ -57,6 +57,11 @@ class Store:
def register_endpoint(self, endpoint_id: str, pid: int, session: str, repos: list[str]) -> None:
import json
self.db.execute(
"UPDATE endpoints SET disconnected_at=strftime('%s','now') "
"WHERE pid=? AND session=? AND endpoint_id<>? AND disconnected_at IS NULL",
(pid, session, endpoint_id),
)
self.db.execute(
"INSERT INTO endpoints(endpoint_id,pid,session,repos,connected_at,disconnected_at) VALUES(?,?,?,?,strftime('%s','now'),NULL) "
"ON CONFLICT(endpoint_id) DO UPDATE SET pid=excluded.pid, session=excluded.session, repos=excluded.repos, connected_at=excluded.connected_at, disconnected_at=NULL",
@ -73,17 +78,21 @@ class Store:
self.db.commit()
def endpoints(self) -> list[sqlite3.Row]:
return list(self.db.execute("SELECT * FROM endpoints ORDER BY endpoint_id"))
return list(
self.db.execute(
"SELECT * FROM endpoints WHERE disconnected_at IS NULL ORDER BY endpoint_id"
)
)
def endpoint(self, endpoint_id: str) -> sqlite3.Row | None:
row = self.db.execute("SELECT * FROM endpoints WHERE endpoint_id=? AND disconnected_at IS NULL", (endpoint_id,)).fetchone()
if row is not None:
return row
if endpoint_id.startswith("tmux-amq-"):
try:
pid = int(endpoint_id.removeprefix("tmux-amq-").split("-", 1)[0])
except ValueError:
visible_pid = endpoint_id.removeprefix("tmux-amq-")
if not visible_pid.isdigit():
return None
pid = int(visible_pid)
return self.db.execute("SELECT * FROM endpoints WHERE pid=? AND disconnected_at IS NULL ORDER BY connected_at DESC LIMIT 1", (pid,)).fetchone()
return None

View file

@ -2,9 +2,12 @@ from __future__ import annotations
import os
import shlex
import shutil
from uuid import uuid4
import subprocess
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
from .registry import repository_paths, validate_targets
@ -19,6 +22,8 @@ class Endpoint:
pid: int
repos: list[str]
instance_id: str = ""
created_session: bool = False
created_windows: tuple[str, ...] = ()
@property
def endpoint_id(self) -> str:
@ -29,48 +34,154 @@ class Endpoint:
return self.instance_id or self.endpoint_id
@dataclass(frozen=True)
class LaunchPlan:
repos: tuple[str, ...]
paths: dict[str, str]
command: tuple[str, ...]
class TmuxManager:
def __init__(self, session: str = "tamq"):
def __init__(
self,
session: str = "tamq",
*,
tmux_command: Sequence[str] | None = None,
tamq_command: Sequence[str] | 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"])
def _run(self, *args: str, check: bool = True) -> str:
result = subprocess.run(["tmux", *args], text=True, capture_output=True, check=False)
result = subprocess.run([*self.tmux_command, *args], text=True, capture_output=True, check=False)
if check and result.returncode:
raise TmuxError(result.stderr.strip() or "tmux command failed")
return result.stdout.strip()
def ensure(self, repos: list[str], command: str = "codex") -> Endpoint:
if not repos:
raise TmuxError("at least one repository is required")
try:
validate_targets(repos)
except Exception as exc:
raise TmuxError(str(exc)) from exc
result = subprocess.run(["tmux", "has-session", "-t", self.session], capture_output=True)
def _existing_session_identity(self) -> tuple[int, str] | None:
result = subprocess.run(
[*self.tmux_command, "has-session", "-t", self.session],
capture_output=True,
)
if result.returncode != 0:
self._run("new-session", "-d", "-s", self.session, "-n", "__tamq_boot", "-c", os.getcwd(), "sleep", "infinity")
return None
pid_text = self._run("display-message", "-p", "-t", self.session, "#{pid}")
try:
pid = int(pid_text)
except ValueError as exc:
raise TmuxError(f"unable to determine tmux PID: {pid_text!r}") from exc
endpoint = Endpoint(self.session, pid, list(dict.fromkeys(repos)), f"tmux-amq-{pid}-{uuid4().hex[:12]}")
instance_id = self._run(
"show-options", "-v", "-t", self.session, "@tamq_instance_id", check=False,
)
managed = self._run(
"show-options", "-v", "-t", self.session, "@tamq_managed", check=False,
)
if managed != "1" and not instance_id.startswith(f"tmux-amq-{pid}-"):
raise TmuxError(
f"tmux session already exists and is not managed by tamq: {self.session}"
)
return pid, instance_id
def preflight(self, repos: list[str], command: str = "codex") -> LaunchPlan:
if not repos:
raise TmuxError("at least one repository is required")
if not self.tmux_command or shutil.which(self.tmux_command[0]) is None:
raise TmuxError("tmux is required; install it and ensure 'tmux' is on PATH")
if shutil.which("gita") is None:
raise TmuxError("gita is required; install it and ensure 'gita' is on PATH")
try:
command_parts = tuple(shlex.split(command))
except ValueError as exc:
raise TmuxError(f"invalid agent command: {exc}") from exc
if not command_parts:
raise TmuxError("agent command must not be empty")
if shutil.which(command_parts[0]) is None:
raise TmuxError(f"agent command not found on PATH: {command_parts[0]}")
unique_repos = tuple(dict.fromkeys(repos))
try:
validate_targets(list(unique_repos))
except Exception as exc:
raise TmuxError(str(exc)) from exc
paths = repository_paths()
windows = self._run("list-windows", "-t", self.session, "-F", "#{window_name}").splitlines()
for index, repo in enumerate(endpoint.repos):
if repo not in windows:
selected_paths: dict[str, str] = {}
for repo in unique_repos:
try:
repo_path = paths[repo]
except KeyError as exc:
raise TmuxError(f"gita path missing for registered repository: {repo}") from exc
if not Path(repo_path).is_dir():
raise TmuxError(f"gita path is not a directory for registered repository {repo}: {repo_path}")
selected_paths[repo] = repo_path
self._existing_session_identity()
return LaunchPlan(unique_repos, selected_paths, command_parts)
def ensure(self, repos: list[str], command: str = "codex") -> Endpoint:
return self.ensure_plan(self.preflight(repos, command))
def ensure_plan(self, plan: LaunchPlan) -> Endpoint:
created_session = False
created_windows: list[str] = []
try:
identity = self._existing_session_identity()
if identity is None:
first_repo = plan.repos[0]
self._run(
"new-session", "-d", "-s", self.session, "-n", "__tamq_boot",
"-c", plan.paths[first_repo],
)
created_session = True
pid_text = self._run("display-message", "-p", "-t", self.session, "#{pid}")
try:
pid = int(pid_text)
except ValueError as exc:
raise TmuxError(f"unable to determine tmux PID: {pid_text!r}") from exc
instance_id = ""
else:
pid, instance_id = identity
endpoint_id = f"tmux-amq-{pid}"
if not instance_id.startswith(f"{endpoint_id}-"):
instance_id = f"{endpoint_id}-{uuid4().hex[:12]}"
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()
for index, repo in enumerate(plan.repos):
if repo in windows:
continue
if index == 0 and "__tamq_boot" in windows:
if not created_session:
self._run("respawn-pane", "-k", "-t", f"{self.session}:__tamq_boot", "-c", plan.paths[repo])
self._run("rename-window", "-t", f"{self.session}:__tamq_boot", repo)
windows[windows.index("__tamq_boot")] = repo
else:
try:
repo_path = paths[repo]
except KeyError as exc:
raise TmuxError(f"gita path missing for registered repository: {repo}") from exc
self._run("new-window", "-d", "-t", self.session, "-n", repo, "-c", repo_path, "sleep", "infinity")
tap = ["tamq", "tap", "--repo", repo, "--endpoint", endpoint.endpoint_id, "--", command]
self._run("new-window", "-d", "-t", self.session, "-n", repo, "-c", plan.paths[repo])
windows.append(repo)
created_windows.append(repo)
tap = [
*self.tamq_command,
"tap", "--repo", repo, "--endpoint", endpoint_id, "--", *plan.command,
]
self._run("send-keys", "-t", f"{self.session}:{repo}", shell_join(tap), "C-m")
return endpoint
self._run("select-window", "-t", f"{self.session}:{plan.repos[0]}")
return Endpoint(
self.session,
pid,
list(plan.repos),
instance_id,
created_session,
tuple(created_windows),
)
except Exception:
self.rollback(Endpoint(self.session, 0, list(plan.repos), created_session=created_session, created_windows=tuple(created_windows)))
raise
def rollback(self, endpoint: Endpoint) -> None:
if endpoint.created_session:
self._run("kill-session", "-t", endpoint.session, check=False)
return
for window in reversed(endpoint.created_windows):
self._run("kill-window", "-t", f"{endpoint.session}:{window}", check=False)
def shell_join(parts: list[str]) -> str:

View file

@ -1,4 +1,6 @@
from tamq.cli import main
import json
from tamq.cli import build_parser, main
import pytest
@ -13,6 +15,122 @@ def test_help_and_version(capsys):
def test_attach_delegates_to_tmux(monkeypatch):
calls = []
monkeypatch.delenv("TMUX", raising=False)
monkeypatch.setattr("tamq.cli.subprocess.run", lambda args, check=False: calls.append((args, check)) or type("R", (), {"returncode": 0})())
assert main(["attach", "--session", "demo"]) == 0
assert calls == [(["tmux", "attach-session", "-t", "demo"], False)]
def test_attach_switches_client_when_already_in_tmux(monkeypatch):
calls = []
monkeypatch.setenv("TMUX", "/tmp/tmux,1,0")
monkeypatch.setattr("tamq.cli.subprocess.run", lambda args, check=False: calls.append(args) or type("R", (), {"returncode": 0})())
assert main(["attach"]) == 0
assert calls == [["tmux", "switch-client", "-t", "tamq"]]
def test_start_parser_supports_command_and_cmd_alias():
parser = build_parser()
canonical = parser.parse_args(["start", "--command", "codex --quiet", "a", "b"])
compatibility = parser.parse_args(["start", "--cmd", "claude", "a"])
assert canonical.agent_command == "codex --quiet"
assert canonical.repos == ["a", "b"]
assert compatibility.agent_command == "claude"
def test_detached_no_service_start_skips_socket_registration(monkeypatch, capsys):
endpoint = type(
"Endpoint",
(),
{
"endpoint_id": "tmux-amq-42",
"instance_key": "tmux-amq-42-boot",
"pid": 42,
"session": "tamq",
"repos": ["a", "b"],
},
)()
class Manager:
def preflight(self, repos, command):
assert repos == ["a", "b"]
assert command == "codex"
return "plan"
def ensure_plan(self, plan):
assert plan == "plan"
return endpoint
monkeypatch.setattr("tamq.cli.TmuxManager", Manager)
monkeypatch.setattr("tamq.cli.preflight_runtime_paths", lambda: None)
monkeypatch.setattr("tamq.cli.request", lambda payload: pytest.fail("socket request must not run"))
monkeypatch.setattr("tamq.cli.attach_session", lambda session: pytest.fail("detached start must not attach"))
assert main(["start", "--detach", "--no-service", "a", "b"]) == 0
summary = json.loads(capsys.readouterr().out)
assert summary["service"] is False
assert summary["messaging"] is False
assert summary["repos"] == ["a", "b"]
def test_interactive_no_service_start_attaches(monkeypatch, capsys):
endpoint = type(
"Endpoint",
(),
{
"endpoint_id": "tmux-amq-42",
"instance_key": "tmux-amq-42-boot",
"session": "tamq",
"repos": ["a"],
},
)()
class Manager:
def preflight(self, repos, command):
return "plan"
def ensure_plan(self, plan):
return endpoint
attached = []
monkeypatch.setattr("tamq.cli.TmuxManager", Manager)
monkeypatch.setattr("tamq.cli.preflight_runtime_paths", lambda: None)
monkeypatch.setattr("tamq.cli.attach_session", lambda session: attached.append(session) or 0)
assert main(["start", "--no-service", "a"]) == 0
assert attached == ["tamq"]
assert json.loads(capsys.readouterr().out)["repos"] == ["a"]
def test_registration_failure_rolls_back_created_windows(monkeypatch, capsys):
endpoint = type(
"Endpoint",
(),
{
"endpoint_id": "tmux-amq-42",
"instance_key": "tmux-amq-42-boot",
"pid": 42,
"session": "tamq",
"repos": ["a"],
},
)()
rolled_back = []
class Manager:
def preflight(self, repos, command):
return "plan"
def ensure_plan(self, plan):
return endpoint
def rollback(self, value):
rolled_back.append(value)
async def failed_registration(payload):
return {"ok": False, "error": "denied"}
monkeypatch.setattr("tamq.cli.TmuxManager", Manager)
monkeypatch.setattr("tamq.cli.preflight_runtime_paths", lambda: None)
monkeypatch.setattr("tamq.cli.ensure_service", lambda: True)
monkeypatch.setattr("tamq.cli.request", failed_registration)
assert main(["start", "--detach", "a"]) == 1
assert rolled_back == [endpoint]
assert "endpoint registration failed: denied" in capsys.readouterr().err

View file

@ -13,3 +13,12 @@ def test_endpoint_resolves_visible_pid_id(tmp_path):
store = Store(tmp_path / "queue.sqlite3")
store.register_endpoint("tmux-amq-1-boot", 1, "tamq", ["a"])
assert store.endpoint("tmux-amq-1")["endpoint_id"] == "tmux-amq-1-boot"
def test_endpoint_registration_retires_previous_instance_for_session(tmp_path):
store = Store(tmp_path / "queue.sqlite3")
store.register_endpoint("tmux-amq-1-old", 1, "tamq", ["a"])
store.register_endpoint("tmux-amq-1-new", 1, "tamq", ["a", "b"])
assert store.endpoint("tmux-amq-1-old") is None
assert [row["endpoint_id"] for row in store.endpoints()] == ["tmux-amq-1-new"]

View file

@ -0,0 +1,180 @@
import json
import os
import shutil
import subprocess
import time
from pathlib import Path
import pytest
def test_make_install_uses_isolated_uv_tool_install():
makefile = Path("Makefile").read_text(encoding="utf-8")
assert "install:" in makefile
assert "uv tool install --force --reinstall --refresh ." in makefile
assert "tamq --version" in makefile
assert "uninstall:" in makefile
assert "uv tool uninstall tmux-amq" in makefile
@pytest.mark.skipif(
shutil.which("uv") is None or shutil.which("tmux") is None,
reason="installed-package smoke requires uv and tmux",
)
def test_isolated_installed_tool_session_smoke(tmp_path):
project = Path(__file__).resolve().parents[1]
tool_dir = tmp_path / "tools"
bin_dir = tmp_path / "bin"
runtime_dir = tmp_path / "runtime"
state_dir = tmp_path / "state"
repo_a = tmp_path / "railiance-platform"
repo_b = tmp_path / "activity-core"
for path in (bin_dir, runtime_dir, repo_a, repo_b):
path.mkdir(parents=True)
gita = bin_dir / "gita"
gita.write_text(
"#!/usr/bin/env python3\n"
"import sys\n"
f"paths = {{'railiance-platform': {str(repo_a)!r}, 'activity-core': {str(repo_b)!r}}}\n"
"if sys.argv[1:] == ['ls']:\n"
" print('railiance-platform activity-core')\n"
"elif sys.argv[1:] == ['freeze']:\n"
" for slug, path in paths.items(): print(f'local,{slug},{path}')\n"
"else:\n"
" raise SystemExit(2)\n",
encoding="utf-8",
)
agent = bin_dir / "alpha-agent"
agent.write_text(
"#!/bin/sh\nprintf 'installed-alpha-ready\\n'\nexec sleep 30\n",
encoding="utf-8",
)
gita.chmod(0o755)
agent.chmod(0o755)
socket_name = f"tamq-installed-{os.getpid()}"
env = os.environ.copy()
env.update(
{
"PATH": f"{bin_dir}:{env['PATH']}",
"UV_TOOL_DIR": str(tool_dir),
"UV_TOOL_BIN_DIR": str(bin_dir),
"UV_CACHE_DIR": str(tmp_path / "uv-cache"),
"XDG_RUNTIME_DIR": str(runtime_dir),
"TAMQ_STATE_DIR": str(state_dir),
"TAMQ_TMUX_SOCKET": socket_name,
}
)
tamq = bin_dir / "tamq"
tmux = ["tmux", "-L", socket_name]
def run(*args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
args,
cwd=tmp_path,
env=env,
text=True,
capture_output=True,
check=True,
timeout=30,
)
try:
subprocess.run(
["uv", "tool", "install", "--force", "--reinstall", "--refresh", str(project)],
cwd=project,
env=env,
text=True,
capture_output=True,
check=True,
timeout=60,
)
assert run(str(tamq), "--version").stdout.strip() == "0.1.0"
started = json.loads(
run(
str(tamq),
"start",
"--detach",
"--command",
"alpha-agent",
"railiance-platform",
"activity-core",
).stdout
)
assert started["repos"] == ["railiance-platform", "activity-core"]
rows = run(
*tmux,
"list-windows",
"-t",
"tamq",
"-F",
"#{window_name}|#{pane_current_path}|#{pane_pid}",
).stdout.splitlines()
parsed = {name: (path, pid) for name, path, pid in (row.split("|", 2) for row in rows)}
assert parsed["railiance-platform"][0] == str(repo_a)
assert parsed["activity-core"][0] == str(repo_b)
repeated = json.loads(
run(
str(tamq),
"start",
"--detach",
"--command",
"alpha-agent",
"railiance-platform",
"activity-core",
).stdout
)
assert repeated["instance_id"] == started["instance_id"]
repeated_rows = run(
*tmux,
"list-windows",
"-t",
"tamq",
"-F",
"#{window_name}|#{pane_current_path}|#{pane_pid}",
).stdout.splitlines()
assert repeated_rows == rows
pre_delivery_status = json.loads(run(str(tamq), "status").stdout)
assert pre_delivery_status["service"] is True
assert [item["endpoint_id"] for item in pre_delivery_status["endpoints"]] == [
started["instance_id"]
]
message_id = run(
str(tamq), "send", "@activity-core:", "installed-message"
).stdout.strip()
deadline = time.monotonic() + 5
history = []
while time.monotonic() < deadline:
history = [
json.loads(line)
for line in run(str(tamq), "history", "--repo", "activity-core").stdout.splitlines()
]
if history and history[-1]["state"] == "injected":
break
time.sleep(0.05)
assert history[-1]["message_id"] == message_id
assert history[-1]["state"] == "injected"
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"]]
assert run(str(tamq), "stop").returncode == 0
finally:
subprocess.run(
[str(tamq), "stop"],
cwd=tmp_path,
env=env,
text=True,
capture_output=True,
check=False,
timeout=5,
)
subprocess.run(
[*tmux, "kill-server"],
env=env,
text=True,
capture_output=True,
check=False,
)

View file

@ -0,0 +1,113 @@
import os
import shutil
import sys
import time
from pathlib import Path
from uuid import uuid4
import pytest
from tamq.broker import BrokerIdentity, InputBroker
from tamq.control import ControlModeClient
from tamq.store import Store
from tamq.tmux import LaunchPlan, TmuxManager
@pytest.mark.skipif(shutil.which("tmux") is None, reason="tmux is not installed")
def test_real_tmux_starts_two_repo_windows_and_reuses_them(tmp_path, monkeypatch):
repo_a = tmp_path / "railiance-platform"
repo_b = tmp_path / "activity-core"
repo_a.mkdir()
repo_b.mkdir()
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("TAMQ_STATE_DIR", str(tmp_path / "state"))
socket_name = f"tamq-pytest-{os.getpid()}-{uuid4().hex[:8]}"
session = f"tamq-test-{uuid4().hex[:8]}"
manager = TmuxManager(
session,
tmux_command=("tmux", "-L", socket_name),
tamq_command=(sys.executable, "-m", "tamq.cli"),
)
plan = LaunchPlan(
("railiance-platform", "activity-core"),
{"railiance-platform": str(repo_a), "activity-core": str(repo_b)},
("sh", "-c", "printf 'alpha-ready\\n'; exec sleep 30"),
)
try:
endpoint = manager.ensure_plan(plan)
assert endpoint.created_session is True
assert endpoint.created_windows == ("railiance-platform", "activity-core")
rows = manager._run(
"list-windows",
"-t",
session,
"-F",
"#{window_name}|#{pane_current_path}|#{pane_pid}",
).splitlines()
parsed = {name: (path, pid) for name, path, pid in (row.split("|", 2) for row in rows)}
assert set(parsed) == {"railiance-platform", "activity-core"}
assert parsed["railiance-platform"][0] == str(repo_a)
assert parsed["activity-core"][0] == str(repo_b)
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
captures = [
manager._run("capture-pane", "-p", "-t", f"{session}:{repo}")
for repo in plan.repos
]
if all("alpha-ready" in capture for capture in captures):
break
time.sleep(0.05)
assert all("alpha-ready" in capture for capture in captures)
reused = manager.ensure_plan(plan)
assert reused.created_session is False
assert reused.created_windows == ()
reused_rows = manager._run(
"list-windows",
"-t",
session,
"-F",
"#{window_name}|#{pane_current_path}|#{pane_pid}",
).splitlines()
reused_parsed = {name: (path, pid) for name, path, pid in (row.split("|", 2) for row in reused_rows)}
assert reused_parsed == parsed
assert reused.instance_id == endpoint.instance_id
store = Store(tmp_path / "state" / "tamq.sqlite3")
control = ControlModeClient(
session,
tmux_command=("tmux", "-L", socket_name),
)
try:
message_id = store.add("local", "activity-core", "integration-message")
control.start()
delivered = InputBroker(
store,
BrokerIdentity(endpoint.instance_id, "railiance-platform"),
).deliver_pending(
control,
window_for_repo=lambda repo: f"{session}:{repo}",
)
assert delivered == 1
assert store.list(state="injected")[0]["message_id"] == message_id
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
capture = manager._run(
"capture-pane", "-p", "-t", f"{session}:activity-core"
)
if "#local: integration-message" in capture:
break
time.sleep(0.05)
assert "#local: integration-message" in capture
finally:
control.close()
store.close()
finally:
manager._run("kill-server", check=False)

View file

@ -1,11 +1,17 @@
from tamq import tmux
import pytest
def test_tmux_manager_builds_tap_windows(monkeypatch):
def test_tmux_manager_builds_tap_windows(tmp_path, monkeypatch):
calls = []
manager = tmux.TmuxManager("tamq-test")
repo_a = tmp_path / "a"
repo_b = tmp_path / "b"
repo_a.mkdir()
repo_b.mkdir()
monkeypatch.setattr(tmux, "validate_targets", lambda repos: None)
monkeypatch.setattr(tmux, "repository_paths", lambda: {"a": "/tmp/a", "b": "/tmp/b"})
monkeypatch.setattr(tmux, "repository_paths", lambda: {"a": str(repo_a), "b": str(repo_b)})
monkeypatch.setattr(tmux.shutil, "which", lambda command: f"/bin/{command}")
def run(*args, check=True):
calls.append(args)
if args[:2] == ("list-windows", "-t"):
@ -15,6 +21,95 @@ def test_tmux_manager_builds_tap_windows(monkeypatch):
return ""
monkeypatch.setattr(manager, "_run", run)
monkeypatch.setattr(tmux.subprocess, "run", lambda *args, **kwargs: type("R", (), {"returncode": 1})())
endpoint = manager.ensure(["a", "b"], "codex")
endpoint = manager.ensure(["a", "b"], "codex --quiet")
assert endpoint.endpoint_id == "tmux-amq-42"
assert any("tamq tap" in " ".join(call) for call in calls if call and call[0] == "send-keys")
assert endpoint.instance_id.startswith("tmux-amq-42-")
assert endpoint.repos == ["a", "b"]
new_session = next(call for call in calls if call and call[0] == "new-session")
assert new_session[-2:] == ("-c", str(repo_a))
assert "sleep" not in new_session
new_window = next(call for call in calls if call and call[0] == "new-window")
assert new_window[-2:] == ("-c", str(repo_b))
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("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)
def test_tmux_manager_reuses_session_instance_id(tmp_path, monkeypatch):
repo = tmp_path / "a"
repo.mkdir()
manager = tmux.TmuxManager("tamq-test")
options = {"@tamq_managed": "1"}
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}")
def run(*args, check=True):
if args[:2] == ("display-message", "-p"):
return "42"
if args and args[0] == "show-options":
return options.get(args[-1], "")
if args and args[0] == "set-option":
options[args[-2]] = args[-1]
if args[:2] == ("list-windows", "-t"):
return "a"
return ""
monkeypatch.setattr(manager, "_run", run)
monkeypatch.setattr(
tmux.subprocess,
"run",
lambda *args, **kwargs: type("R", (), {"returncode": 0})(),
)
first = manager.ensure(["a"])
second = manager.ensure(["a"])
assert first.instance_id == second.instance_id
def test_preflight_rejects_foreign_session(tmp_path, monkeypatch):
repo = tmp_path / "a"
repo.mkdir()
manager = tmux.TmuxManager("tamq-test")
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}")
monkeypatch.setattr(
tmux.subprocess,
"run",
lambda *args, **kwargs: type("R", (), {"returncode": 0})(),
)
def run(*args, check=True):
if args[:2] == ("display-message", "-p"):
return "42"
return ""
monkeypatch.setattr(manager, "_run", run)
with pytest.raises(tmux.TmuxError, match="not managed by tamq"):
manager.preflight(["a"])
def test_preflight_rejects_missing_agent(tmp_path, monkeypatch):
repo = tmp_path / "a"
repo.mkdir()
manager = tmux.TmuxManager("tamq-test")
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}")
with pytest.raises(tmux.TmuxError, match="agent command not found"):
manager.preflight(["a"], "missing-agent")
def test_preflight_deduplicates_repositories(tmp_path, monkeypatch):
repo = tmp_path / "a"
repo.mkdir()
manager = tmux.TmuxManager("tamq-test")
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}")
plan = manager.preflight(["a", "a"], "codex")
assert plan.repos == ("a",)

View file

@ -4,7 +4,7 @@ type: workplan
title: "Operator-installable local alpha session"
domain: communication
repo: tmux-amq
status: active
status: finished
owner: codex
topic_slug: coulomb-social
planning_priority: P0
@ -100,7 +100,7 @@ tamq attach
```task
id: TAMQ-WP-0004-T01
status: todo
status: done
priority: high
state_hub_task_id: "017c4c30-3e1c-5939-b700-e73912e4a01e"
```
@ -111,11 +111,16 @@ installs. Verify `command -v tamq`, `tamq --version`, and invocation from outsid
the checkout. Document the corresponding uninstall command and executable-path
expectation.
Completed with a cache-refreshing `uv tool install` target and matching
uninstall target. The installed command resolves from the user tool bin, reports
version `0.1.0`, and is exercised from an isolated directory by the installed
package smoke test.
## Make session startup match the CLI contract
```task
id: TAMQ-WP-0004-T02
status: todo
status: done
priority: high
state_hub_task_id: "8e926f2b-b7b2-53f5-81e1-9bdd3c4b7d3e"
```
@ -130,11 +135,16 @@ new window and preserve idempotent reuse of existing windows. Define and test
`--no-service` as an explicitly degraded tmux-only mode or remove it from the
alpha surface.
Completed with canonical `--command`, compatible `--cmd`, default Codex,
attach-by-default, working detached and explicit no-service modes, exact first
and subsequent window paths, tmux-scoped endpoint identity, and idempotent
window/process reuse.
## Add installation and startup preflight
```task
id: TAMQ-WP-0004-T03
status: todo
status: done
priority: high
state_hub_task_id: "479dd94e-e21f-512d-80e7-2453cf163b67"
```
@ -145,11 +155,15 @@ and session conflicts. Return concise remediation for every failure and avoid
leaving boot windows, sockets, pidfiles, or processes behind after a failed
start.
Completed with command, gita target/path, runtime-path, agent, and managed-session
checks before mutation. Registration failures roll back newly created tmux
resources, and foreign same-named sessions are rejected rather than adopted.
## Prove the installed local session end to end
```task
id: TAMQ-WP-0004-T04
status: wait
status: done
priority: high
state_hub_task_id: "76990b5d-b0d7-562f-8316-1b9cc452cadf"
```
@ -162,11 +176,19 @@ same-named windows open in their registered paths with Codex running
automatically, along with attach/detach semantics, idempotent reuse, message
injection/history, shutdown, and cleanup. This task follows T01-T03.
Completed with both a real isolated-tmux integration test and an isolated uv
tool installation smoke test. The latter starts the two required repository
windows through the installed CLI, verifies exact paths and stable pane PIDs on
repeat, routes a message into `activity-core`, observes `injected` history,
checks status, stops the service, and bounds tmux cleanup. The local operator
acceptance used the same repository operands with Codex and left both live panes
ready for attachment.
## Publish the alpha quickstart and evidence
```task
id: TAMQ-WP-0004-T05
status: wait
status: done
priority: medium
state_hub_task_id: "e98eda1e-6200-5bfb-8873-c0a210484fc1"
```
@ -176,3 +198,8 @@ upgrade, start, attach, status, stop, and uninstall commands. Record the exact
local acceptance commands and results. Clearly retain the reliability caveats
owned by `TAMQ-WP-0003` and do not claim coordination-engine integration from
`TAMQ-WP-0002`. This task follows T04.
Completed in `README.md`, `AGENTS.md`, the stack command rules, and `SCOPE.md`.
The quickstart documents install/upgrade, exact Codex start, detached start,
attach, status, stop, tmux cleanup, and uninstall while retaining the bounded
retry, acknowledgement, and coordination-adapter caveats.