diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index c01f29238..669588374 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,6 +1,8 @@ {"id":"agent-relay-0bn","title":"Dashboard doesn't show real-time connection status","description":"Dashboard shows agent status from messages but doesn't show live connection status (connected/disconnected). The daemon tracks this in agents.json but dashboard could show online/offline indicators with last-seen timestamps.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-20T00:18:04.647965+01:00","updated_at":"2025-12-20T00:18:04.647965+01:00"} {"id":"agent-relay-0uh","title":"Add session discovery for better status output","description":"Auto-discover relay sessions via 'tmux list-sessions'. Filter to relay-* sessions and extract agent names. Enhance 'agent-relay status' command output. See docs/TMUX_IMPROVEMENTS.md for implementation details.","status":"closed","priority":2,"issue_type":"feature","assignee":"LeadDev","created_at":"2025-12-20T21:28:49.058578+01:00","updated_at":"2025-12-20T21:36:23.10406+01:00","closed_at":"2025-12-20T21:36:23.10406+01:00"} {"id":"agent-relay-1ek","title":"Add agent registry for persistence across restarts","description":"Store agent metadata in persistent registry (agents.json): id, name, cli, workingDirectory, firstSeen, lastSeen, messagesSent, messagesReceived. Enables agent history and stats. See docs/TMUX_IMPROVEMENTS.md for implementation details.","notes":"No progress today; agent registry idea remains open. Consider aligning with Swarm Mail 'agents' projection + last_active tracking.","status":"open","priority":3,"issue_type":"feature","created_at":"2025-12-20T21:28:50.235444+01:00","updated_at":"2025-12-20T22:00:47.791303+01:00"} +{"id":"agent-relay-1ie","title":"Fix Gemini CLI message injection and handling","description":"Investigate and fix issues where Gemini agent messages are not being correctly picked up by the tmux-wrapper. Ensure the special printf injection path is working and that standard LLM output is captured correctly.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-22T12:11:57.073531+01:00","updated_at":"2025-12-22T12:17:27.434451+01:00","closed_at":"2025-12-22T12:17:27.434456+01:00"} +{"id":"agent-relay-1le","title":"Agent name truncated in tmux pane/status bar","description":"Agent name gets cut off in tmux display. May need to adjust tmux status bar width or session naming convention. Check: tmux status-left-length, session name format (relay-{name}-{pid}).","status":"open","priority":3,"issue_type":"bug","created_at":"2025-12-20T22:11:15.795453+01:00","updated_at":"2025-12-20T22:11:15.795453+01:00"} {"id":"agent-relay-2lw","title":"Add agent metadata tracking","description":"Track program, model, task description in agents.json. Better agent discovery.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-20T21:36:19.741328+01:00","updated_at":"2025-12-20T21:36:19.741328+01:00"} {"id":"agent-relay-2sn","title":"Competitive Analysis: mcp_agent_mail vs agent-relay","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-20T21:34:44.49366+01:00","updated_at":"2025-12-20T21:36:26.362391+01:00","closed_at":"2025-12-20T21:36:26.362391+01:00"} {"id":"agent-relay-2uf","title":"Add message threading support","description":"@relay:Bob [thread:feature-123] pattern. Group related messages for better context.","notes":"Current state: thread parsing + protocol/storage support implemented (ParsedCommand.thread, SendPayload.thread, DB messages.thread + filter). Remaining: wire cmd.thread through tmux-wrapper sendRelayCommand -\u003e RelayClient.sendMessage(..., thread) and include thread in injected display/Inbox. See src/wrapper/tmux-wrapper.ts sendRelayCommand + handleIncomingMessage.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-20T21:36:19.631448+01:00","updated_at":"2025-12-20T21:59:53.973954+01:00"} @@ -23,6 +25,7 @@ {"id":"agent-relay-7tu","title":"Fix storage portability: better-sqlite3 native binding failures","description":"The read command fails in some Node versions (e.g., Node 25) due to missing better-sqlite3 bindings. Decide path: pin supported Node LTS + ensure rebuild, or replace with a non-native option (e.g., Node sqlite, wasm, or a pure JS adapter).","design":"Implement driver selection/fallback: try better-sqlite3, fallback to node:sqlite DatabaseSync when bindings missing. Add env override AGENT_RELAY_SQLITE_DRIVER=node|better-sqlite3. Convert queries to positional params so both drivers share code.","acceptance_criteria":"- dist CLI read works on supported Node versions\\n- CI/build docs reflect supported runtime or replacement adapter","notes":"Implemented sqlite portability: dynamic better-sqlite3 import + fallback to node:sqlite (env override AGENT_RELAY_SQLITE_DRIVER). Updated sqlite adapter to use positional params for compatibility; added/updated tests; npx vitest run passes (216).","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-20T21:44:57.868091+01:00","updated_at":"2025-12-20T21:56:13.384399+01:00","closed_at":"2025-12-20T21:56:13.3844+01:00","labels":["build","storage"]} {"id":"agent-relay-7yo","title":"Update CLAUDE.md with new CLI commands","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T22:00:02.661859+01:00","updated_at":"2025-12-19T22:07:27.766701+01:00","closed_at":"2025-12-19T22:07:27.766701+01:00","dependencies":[{"issue_id":"agent-relay-7yo","depends_on_id":"agent-relay-85z","type":"blocks","created_at":"2025-12-19T22:00:25.731921+01:00","created_by":"daemon"},{"issue_id":"agent-relay-7yo","depends_on_id":"agent-relay-4ft","type":"blocks","created_at":"2025-12-19T22:00:25.772241+01:00","created_by":"daemon"},{"issue_id":"agent-relay-7yo","depends_on_id":"agent-relay-bd0","type":"blocks","created_at":"2025-12-19T22:00:25.80776+01:00","created_by":"daemon"},{"issue_id":"agent-relay-7yo","depends_on_id":"agent-relay-f3q","type":"blocks","created_at":"2025-12-19T22:00:25.843131+01:00","created_by":"daemon"}]} {"id":"agent-relay-85z","title":"Merge dashboard into start command","description":"","status":"closed","priority":2,"issue_type":"task","assignee":"InterfaceManager","created_at":"2025-12-19T21:59:51.61716+01:00","updated_at":"2025-12-19T22:06:44.27487+01:00","closed_at":"2025-12-19T22:06:44.27487+01:00"} +{"id":"agent-relay-8ap","title":"CRITICAL: send command triggers infinite reconnection loop","description":"When sending relay messages to Gem agent, the send command triggers an infinite reconnection loop. Client repeatedly connects with RESUME_TOO_OLD errors, never actually delivers the message. Each connection gets a new session ID and immediately reconnects (~90-110ms apart). This is a critical client/daemon issue, not just Gemini-specific.","status":"in_progress","priority":1,"issue_type":"bug","created_at":"2025-12-22T12:18:13.272843+01:00","updated_at":"2025-12-22T12:24:05.415022+01:00"} {"id":"agent-relay-8ff","title":"Add agent list command to CLI","description":"CLI has up/down/status/read but no way to list connected agents or see message history from command line. Add: (1) agent-relay agents - list connected agents, (2) agent-relay history - show recent messages, (3) agent-relay send \u003cagent\u003e \u003cmsg\u003e - send from CLI.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-20T00:18:34.400432+01:00","updated_at":"2025-12-20T00:18:34.400432+01:00"} {"id":"agent-relay-8nx","title":"SIGINT/SIGTERM handlers don't await cleanup","description":"In cli/index.ts:74-77 and daemon setup, SIGINT handlers call stop() but process.exit(0) runs immediately. The stop() is async but not awaited, potentially leaving socket files or incomplete shutdown.","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-20T00:18:19.189514+01:00","updated_at":"2025-12-20T00:18:19.189514+01:00"} {"id":"agent-relay-8z1","title":"Add CLI tests for new command structure","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-19T22:00:04.561793+01:00","updated_at":"2025-12-20T21:57:43.401385+01:00","dependencies":[{"issue_id":"agent-relay-8z1","depends_on_id":"agent-relay-85z","type":"blocks","created_at":"2025-12-19T22:00:27.632396+01:00","created_by":"daemon"},{"issue_id":"agent-relay-8z1","depends_on_id":"agent-relay-4ft","type":"blocks","created_at":"2025-12-19T22:00:27.671868+01:00","created_by":"daemon"},{"issue_id":"agent-relay-8z1","depends_on_id":"agent-relay-bd0","type":"blocks","created_at":"2025-12-19T22:00:27.713889+01:00","created_by":"daemon"},{"issue_id":"agent-relay-8z1","depends_on_id":"agent-relay-f3q","type":"blocks","created_at":"2025-12-19T22:00:27.752052+01:00","created_by":"daemon"}]} @@ -35,12 +38,14 @@ {"id":"agent-relay-d2f","title":"Add optional Git audit trail","description":"Commit messages to .agent-relay/messages/ directory. Recoverable history, compliance-friendly.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-20T21:36:19.689795+01:00","updated_at":"2025-12-20T21:36:19.689795+01:00"} {"id":"agent-relay-dd8","title":"Add file reservation system (from mcp_agent_mail)","description":"Advisory locks with TTL, @relay:lock pattern, pre-commit hook integration. Critical for multi-agent file editing.","notes":"No progress today; added as future work from swarm-mail comparison: implement reservations/locks with TTL + conflict detection. See swarm-mail reservations + locks patterns.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-20T21:36:19.573504+01:00","updated_at":"2025-12-20T22:00:37.608017+01:00"} {"id":"agent-relay-dyr","title":"No authentication between agents","description":"Any process can connect to the daemon socket and impersonate any agent name. Consider: (1) Per-agent tokens/secrets, (2) Socket permission checks, (3) Optional TLS for non-localhost deployments.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-20T00:18:16.215889+01:00","updated_at":"2025-12-20T00:18:16.215889+01:00"} +{"id":"agent-relay-eek","title":"Change relay prefix from @relay to \u003e\u003erelay","description":"Unify the relay protocol prefix to \u003e\u003erelay (instead of @relay) so it works for all agents including Gemini. Update parser, documentation, and any hardcoded references.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-22T12:20:35.35848+01:00","updated_at":"2025-12-22T12:30:21.696239+01:00","closed_at":"2025-12-22T12:30:21.696239+01:00"} {"id":"agent-relay-f3q","title":"Make msg-read a subcommand of send or message","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T21:59:55.123772+01:00","updated_at":"2025-12-19T22:04:51.112763+01:00","closed_at":"2025-12-19T22:04:51.112763+01:00"} {"id":"agent-relay-fb3","title":"Add configurable relay prefix for Gemini compatibility","description":"Gemini CLI uses @ for file references, conflicting with @relay:. Add --prefix flag to CLI and relayPrefix config option. Auto-detect CLI type and use appropriate default (\u003e\u003e for Gemini, @relay: for Claude/Codex). Update parser to use dynamic prefix. See docs/TMUX_IMPROVEMENTS.md for full implementation plan.","status":"closed","priority":2,"issue_type":"feature","assignee":"Coordinator","created_at":"2025-12-20T21:28:52.685002+01:00","updated_at":"2025-12-20T21:40:13.066766+01:00","closed_at":"2025-12-20T21:40:13.066766+01:00"} {"id":"agent-relay-ghy","title":"Team config: auto-spawn agents or auto-assign names from teams.json","description":"When a project has a teams.json config file, agent-relay up should either:\n\n1. **Auto-spawn option**: Automatically kick off terminal sessions for each agent defined in teams.json\n2. **Auto-assign option**: When users manually start sessions with `agent-relay -n \u003cname\u003e claude`, validate the name against teams.json and auto-assign roles/permissions\n\n## teams.json format\n```json\n{\n \"team\": \"my-project\",\n \"agents\": [\n {\"name\": \"Coordinator\", \"cli\": \"claude\", \"role\": \"coordinator\"},\n {\"name\": \"LeadDev\", \"cli\": \"claude\", \"role\": \"developer\"},\n {\"name\": \"Reviewer\", \"cli\": \"claude\", \"role\": \"reviewer\"}\n ],\n \"autoSpawn\": false\n}\n```\n\n## Behavior\n- If `autoSpawn: true`, `agent-relay up` spawns tmux sessions for each agent\n- If `autoSpawn: false`, validate names against config when agents connect\n- Store teams.json in project root or .agent-relay/teams.json\n\n## Commands\n- `agent-relay up --spawn` - force spawn all agents\n- `agent-relay up --no-spawn` - just start daemon, manual agent starts","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-19T23:13:02.482971+01:00","updated_at":"2025-12-20T21:57:43.362401+01:00"} {"id":"agent-relay-go9","title":"PostgreSQL storage adapter not implemented","description":"In storage/adapter.ts:152-162, PostgreSQL is listed as a storage option but throws 'not yet implemented'. For production multi-node deployments, SQLite won't scale. Implement PostgreSQL adapter for distributed storage.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-20T00:17:45.065487+01:00","updated_at":"2025-12-20T00:17:45.065487+01:00"} {"id":"agent-relay-hgd","title":"Reliable delivery: resume/replay via cursor/checkpoint","description":"Make ACK/RESUME real: persist a per-agent delivery cursor (last delivered/acked seq) and on reconnect replay missed messages from storage. Use existing delivery.seq as stream position, similar to swarm-mail DurableCursor checkpointing.","acceptance_criteria":"- Client can reconnect and receive missed messages\\n- Duplicate suppression is deterministic (id/seq based)\\n- Cursor persists across daemon restarts","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-20T21:44:02.016428+01:00","updated_at":"2025-12-20T21:44:02.016428+01:00","labels":["durability","protocol"]} {"id":"agent-relay-hks","title":"Increase test coverage for daemon/server.ts, dashboard, and CLI","description":"Current coverage is only 39% overall. Key files with 0% coverage: daemon/server.ts, dashboard/server.ts, cli/index.ts, wrapper/client.ts, wrapper/tmux-wrapper.ts, utils/project-namespace.ts. Need integration tests for the daemon startup/shutdown lifecycle and CLI commands.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-20T00:17:29.603137+01:00","updated_at":"2025-12-20T00:17:29.603137+01:00"} +{"id":"agent-relay-i5f","title":"Fix rendering and display issues","description":"Relay messages display with garbled/overlapping text in terminal. Characters appear scattered and mixed with other terminal content. Example: message text like 'Ping me when you're ready' renders with letters displaced across the line. PR #10 added input detection (isInputClear, waitForClearInput, getCursorX) to prevent this, but it's not working effectively. Need to investigate why the input detection/wait logic isn't preventing message injection during active typing.","notes":"Ready for review; bd has no 'review' status","status":"in_progress","priority":1,"issue_type":"bug","created_at":"2025-12-22T11:59:53.254169+01:00","updated_at":"2025-12-22T12:19:10.062402+01:00","labels":["review"]} {"id":"agent-relay-j9z","title":"Message injection corrupts human input in progress","description":"When a relay message arrives while the human is actively typing, the injection (Esc + Ctrl-U + message + Enter) destroys their partial input. Need to detect active input state before injecting. Options: (1) Check tmux pane input buffer, (2) Queue messages until prompt detected, (3) Use tmux display-message instead of send-keys for non-intrusive notification, (4) Add visual indicator that message is pending.","design":"Mitigation ideas: 1) Prefer tmux non-intrusive notify: tmux display-message (and always write to inbox/storage) instead of send-keys when user is typing. 2) Gate send-keys injection on 'safe' state: pane idle AND prompt detected AND no partial input; otherwise queue. 3) If queued, show pending count via display-message.","status":"open","priority":1,"issue_type":"bug","created_at":"2025-12-20T21:48:03.280298+01:00","updated_at":"2025-12-20T22:00:04.196161+01:00","dependencies":[{"issue_id":"agent-relay-j9z","depends_on_id":"agent-relay-6rz","type":"blocks","created_at":"2025-12-20T21:48:10.118963+01:00","created_by":"daemon"}]} {"id":"agent-relay-kzw","title":"Project namespace uses /tmp which can be cleared on reboot","description":"In utils/project-namespace.ts:13, BASE_DIR is /tmp/agent-relay. On macOS/Linux, /tmp is cleared on reboot, losing all message history. Consider: (1) XDG_DATA_HOME fallback, (2) ~/.agent-relay option, (3) Per-project .agent-relay folder.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-20T00:18:33.224965+01:00","updated_at":"2025-12-20T00:18:33.224965+01:00"} {"id":"agent-relay-l99","title":"Add output filtering for cleaner logs","description":"Filter noisy patterns from logs: thinking indicators [1/418], empty ANSI-only lines, etc. Add config.filterLogs option. See docs/TMUX_IMPROVEMENTS.md for implementation details.","status":"open","priority":4,"issue_type":"feature","created_at":"2025-12-20T21:28:51.199736+01:00","updated_at":"2025-12-20T21:28:51.199736+01:00","dependencies":[{"issue_id":"agent-relay-l99","depends_on_id":"agent-relay-1ek","type":"blocks","created_at":"2025-12-20T21:29:26.987526+01:00","created_by":"daemon"}]} diff --git a/AGENTS.md b/AGENTS.md index fa0758372..c609b9a82 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,20 +30,20 @@ agent-relay -n Bob claude ## For Agents: How to Communicate -When wrapped with `agent-relay`, agents communicate by outputting `@relay:` patterns. +When wrapped with `agent-relay`, agents communicate by outputting `>>relay:` patterns. ### Send a Message Output this in your response (not in a bash command): ``` -@relay:AgentName Your message here +>>relay:AgentName Your message here ``` ### Broadcast to All ``` -@relay:* This message goes to everyone +>>relay:* This message goes to everyone ``` ### Receiving Messages @@ -87,48 +87,48 @@ agent-relay read abc12345 ### Status Updates ``` -@relay:* STATUS: Starting work on auth module -@relay:* DONE: Auth module complete +>>relay:* STATUS: Starting work on auth module +>>relay:* DONE: Auth module complete ``` ### Task Assignment ``` -@relay:Developer TASK: Implement /api/register endpoint +>>relay:Developer TASK: Implement /api/register endpoint ``` ### Questions ``` -@relay:Architect QUESTION: Should we use JWT or sessions? +>>relay:Architect QUESTION: Should we use JWT or sessions? ``` ### Review Requests ``` -@relay:Reviewer REVIEW: Please check src/auth/*.ts +>>relay:Reviewer REVIEW: Please check src/auth/*.ts ``` --- ## Pattern Rules -The `@relay:` pattern must be at the start of a line: +The `>>relay:` pattern must be at the start of a line: ``` -@relay:Name message # Works - @relay:Name message # Works (whitespace OK) -> @relay:Name message # Works (prompt OK) -- @relay:Name message # Works (list OK) -Some text @relay:Name msg # Won't work +>>relay:Name message # Works + >>relay:Name message # Works (whitespace OK) +> >>relay:Name message # Works (prompt OK) +- >>relay:Name message # Works (list OK) +Some text >>relay:Name msg # Won't work ``` ### Escape -To output literal `@relay:` without sending: +To output literal `>>relay:` without sending: ``` -\@relay: This won't be sent +\>>relay: This won't be sent ``` --- diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6c82e5b6d..c5b91038b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -6,7 +6,7 @@ Agent Relay is a real-time messaging system that enables autonomous agent-to-age The system works by: 1. Wrapping agent CLI processes in monitored tmux sessions -2. Parsing agent output for `@relay:` commands +2. Parsing agent output for `>>relay:` commands 3. Routing messages through a central daemon via Unix domain sockets 4. Injecting incoming messages directly into agent terminal input @@ -44,7 +44,7 @@ Agent Relay solves this by providing a communication layer that requires **zero ### 1.2 Core Principle: Output Parsing, Not API Integration -The fundamental insight is that AI agents already produce text output. By monitoring that output for specific patterns (`@relay:Target message`), we can extract communication intent without modifying the agent itself. +The fundamental insight is that AI agents already produce text output. By monitoring that output for specific patterns (`>>relay:Target message`), we can extract communication intent without modifying the agent itself. This approach: - Works with any CLI-based agent @@ -113,7 +113,7 @@ Web UI for monitoring. Shows connected agents, message flow, real-time updates. │ Layer 2: Wrapper │ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ │ TmuxWrapper │ │ OutputParser │ │ RelayClient │ │ -│ │ (PTY mgmt) │ │ (@relay:) │ │ (Socket I/O) │ │ +│ │ (PTY mgmt) │ │ (>>relay:) │ │ (Socket I/O) │ │ │ └───────────────┘ └───────────────┘ └───────────────┘ │ ├─────────────────────────────────────────────────────────────────┤ │ Layer 3: Daemon │ @@ -162,7 +162,7 @@ The TmuxWrapper is the most complex component. It bridges the gap between agent │ │ │ Agent Process (claude, etc.) │ │ │ │ │ │ │ │ │ │ │ │ Output: "I'll send a message to Bob" │ │ │ -│ │ │ Output: "@relay:Bob Can you review auth.ts?" │ │ │ +│ │ │ Output: ">>relay:Bob Can you review auth.ts?" │ │ │ │ │ │ │ │ │ │ │ └────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ │ @@ -173,7 +173,7 @@ The TmuxWrapper is the most complex component. It bridges the gap between agent │ │ OutputParser │ │ │ │ - Strip ANSI codes │ │ │ │ - Join continuation lines │ │ -│ │ - Extract @relay: commands │ │ +│ │ - Extract >>relay: commands │ │ │ │ - Deduplicate (hash-based) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ @@ -214,9 +214,9 @@ return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, ''); ``` **3. Continuation Line Joining** -When TUIs wrap long lines, `@relay:` commands can span multiple lines: +When TUIs wrap long lines, `>>relay:` commands can span multiple lines: ``` -@relay:Bob This is a very long message that gets +>>relay:Bob This is a very long message that gets wrapped by the terminal and continues here ``` The wrapper joins these back together. @@ -252,8 +252,8 @@ Extracts relay commands from agent output. **1. Inline Format (Primary)** ``` -@relay:AgentName Your message here -@relay:* Broadcast to everyone +>>relay:AgentName Your message here +>>relay:* Broadcast to everyone @thinking:AgentName Share reasoning (not displayed to user) ``` @@ -268,15 +268,15 @@ The parser handles real-world terminal output complexity: ```typescript // Allow common input prefixes: >, $, %, #, bullets, etc. -const INLINE_RELAY = /^(?:\s*(?:[>$%#→➜›»●•◦‣⁃\-*⏺◆◇○□■]\s*)*)?@relay:(\S+)\s+(.+)$/; +const INLINE_RELAY = /^(?:\s*(?:[>$%#→➜›»●•◦‣⁃\-*⏺◆◇○□■]\s*)*)?>>relay:(\S+)\s+(.+)$/; ``` This matches: -- `@relay:Bob hello` (plain) -- ` @relay:Bob hello` (indented) -- `> @relay:Bob hello` (quoted) -- `- @relay:Bob hello` (bullet point) -- `⏺ @relay:Bob hello` (Claude's bullet) +- `>>relay:Bob hello` (plain) +- ` >>relay:Bob hello` (indented) +- `> >>relay:Bob hello` (quoted) +- `- >>relay:Bob hello` (bullet point) +- `⏺ >>relay:Bob hello` (Claude's bullet) #### Code Fence Awareness @@ -601,7 +601,7 @@ Messages can have different semantic kinds: ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 1. AGENT OUTPUT │ -│ Agent (Claude) produces text: "@relay:Bob Can you review auth.ts?" │ +│ Agent (Claude) produces text: ">>relay:Bob Can you review auth.ts?" │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ @@ -617,7 +617,7 @@ Messages can have different semantic kinds: │ OutputParser: │ │ - Strips ANSI escape codes │ │ - Joins continuation lines │ -│ - Matches /^@relay:(\S+)\s+(.+)$/ │ +│ - Matches /^>>relay:(\S+)\s+(.+)$/ │ │ - Returns: { to: "Bob", body: "Can you review auth.ts?" } │ └─────────────────────────────────────────────────────────────────────────┘ │ @@ -700,7 +700,7 @@ Messages can have different semantic kinds: ### 5.2 Broadcast Flow -When sending to `@relay:*`: +When sending to `>>relay:*`: ``` Alice Daemon Bob, Carol, Dave @@ -800,7 +800,7 @@ Alice Daemon Bob, Carol, Dave │ └─────────────┘ │ │ │ │ State Tracking: │ -│ - inCodeFence: boolean - ignore @relay inside code fences │ +│ - inCodeFence: boolean - ignore >>relay inside code fences │ │ - inBlock: boolean - buffering [[RELAY]]...[[/RELAY]] │ │ - blockBuffer: string - accumulated block content │ │ │ @@ -958,7 +958,7 @@ For sensitive environments: ### 9.1 Why Output Parsing Instead of API Integration? -**Decision**: Parse agent stdout for `@relay:` patterns instead of modifying agent code. +**Decision**: Parse agent stdout for `>>relay:` patterns instead of modifying agent code. **Rationale**: - Works with any CLI agent without modification @@ -971,7 +971,7 @@ For sensitive environments: - ❌ Can miss messages in edge cases - ❌ No structured validation of message content - ✅ Zero changes to Claude, Codex, or other agents -- ✅ Transparent - users see `@relay:` in agent output +- ✅ Transparent - users see `>>relay:` in agent output ### 9.2 Why Tmux Instead of Direct PTY? @@ -1162,7 +1162,7 @@ agent-relay/ │ ├── wrapper/ │ │ ├── tmux-wrapper.ts # Tmux session management │ │ ├── client.ts # Daemon client connection -│ │ ├── parser.ts # Output parsing (@relay:) +│ │ ├── parser.ts # Output parsing (>>relay:) │ │ ├── inbox.ts # File-based inbox │ │ └── index.ts │ ├── protocol/ @@ -1217,10 +1217,10 @@ agent-relay -n Bob claude ``` # Direct message -@relay:Bob Please review the auth module +>>relay:Bob Please review the auth module # Broadcast -@relay:* I've finished the database migration +>>relay:* I've finished the database migration # Structured (block format) [[RELAY]]{"to":"Bob","type":"action","body":"Run tests"}[[/RELAY]] diff --git a/CLAUDE.md b/CLAUDE.md index 35deb264b..97aff8d29 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,20 +72,20 @@ agent-relay -n Bob claude ## For Agents: How to Communicate -When wrapped with `agent-relay`, agents communicate by outputting `@relay:` patterns. +When wrapped with `agent-relay`, agents communicate by outputting `>>relay:` patterns. ### Send a Message Output this in your response (not in a bash command): ``` -@relay:AgentName Your message here +>>relay:AgentName Your message here ``` ### Broadcast to All ``` -@relay:* This message goes to everyone +>>relay:* This message goes to everyone ``` ### Receiving Messages @@ -115,24 +115,24 @@ agent-relay read abc12345... ## Communication Patterns ``` -@relay:* STATUS: Starting work on auth module -@relay:* DONE: Auth module complete -@relay:Developer TASK: Implement /api/register -@relay:Reviewer REVIEW: Please check src/auth/*.ts -@relay:Architect QUESTION: JWT or sessions? +>>relay:* STATUS: Starting work on auth module +>>relay:* DONE: Auth module complete +>>relay:Developer TASK: Implement /api/register +>>relay:Reviewer REVIEW: Please check src/auth/*.ts +>>relay:Architect QUESTION: JWT or sessions? ``` --- ## Pattern Rules -`@relay:` must be at the start of a line: +`>>relay:` must be at the start of a line: ``` -@relay:Name message # Works - @relay:Name message # Works (whitespace OK) -- @relay:Name message # Works (list OK) -Some text @relay:Name msg # Won't work +>>relay:Name message # Works + >>relay:Name message # Works (whitespace OK) +- >>relay:Name message # Works (list OK) +Some text >>relay:Name msg # Won't work ``` -Escape with `\@relay:` to output literally. +Escape with `\>>relay:` to output literally. diff --git a/EXECUTIVE_SUMMARY.md b/EXECUTIVE_SUMMARY.md index 6c02636be..b53b2652a 100644 --- a/EXECUTIVE_SUMMARY.md +++ b/EXECUTIVE_SUMMARY.md @@ -34,7 +34,7 @@ Agent Relay enables AI coding assistants (Claude, Codex, Gemini) running in sepa **Agents already produce text.** By watching for a simple pattern in their output, we can extract communication intent: ``` -Agent Output: "I'll ask Bob for help. @relay:Bob Can you review auth.ts?" +Agent Output: "I'll ask Bob for help. >>relay:Bob Can you review auth.ts?" ▲ │ Relay captures this, routes to Bob @@ -54,7 +54,7 @@ No API integration. No agent modification. Just pattern matching on stdout. │ │ │ agent-relay Background Daemon │ │ -n Alice claude ───▶ polling ───▶ finds routes ───▶ │ -│ captures @relay: message │ +│ captures >>relay: message │ │ Agent runs in output Bob... to Bob │ │ tmux session │ │ │ @@ -67,7 +67,7 @@ No API integration. No agent modification. Just pattern matching on stdout. │ message terminal as input │ │ │ │ "Relay message Can reply with │ -│ from Alice: @relay:Alice │ +│ from Alice: >>relay:Alice │ │ Can you..." │ │ │ └──────────────────────────────────────────────────────────────────────────┘ @@ -88,7 +88,7 @@ agent-relay -n Alice claude agent-relay -n Bob claude ``` -Alice can now send: `@relay:Bob Can you help with the auth module?` +Alice can now send: `>>relay:Bob Can you help with the auth module?` Bob receives: `Relay message from Alice [abc123]: Can you help with the auth module?` @@ -117,7 +117,7 @@ Bob receives: `Relay message from Alice [abc123]: Can you help with the auth mod │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │TmuxWrapper │ │OutputParser │ │RelayClient │ │ │ │ │ │ │ │ │ │ │ │ │ -│ │ │• Runs agent │ │• @relay: │ │• Socket I/O │ │ │ +│ │ │• Runs agent │ │• >>relay: │ │• Socket I/O │ │ │ │ │ │ in tmux │ │ patterns │ │• Reconnect │ │ │ │ │ │• Captures │ │• ANSI strip │ │• Handshake │ │ │ │ │ │ output │ │• Dedup │ │ │ │ │ @@ -160,7 +160,7 @@ Bob receives: `Relay message from Alice [abc123]: Can you help with the auth mod Alice (Claude) Daemon Bob (Codex) │ │ │ │ Agent outputs: │ │ - │ "@relay:Bob review auth" │ │ + │ ">>relay:Bob review auth" │ │ │ │ │ ├──────────────────────────────┤ │ │ TmuxWrapper captures │ │ @@ -186,7 +186,7 @@ Bob receives: `Relay message from Alice [abc123]: Can you help with the auth mod │ │ └───────────────┤ │ │ │ │ │ Bob can respond: │ - │ │ "@relay:Alice Done!" │ + │ │ ">>relay:Alice Done!" │ │ │ │ │ │◀─────────────────────────────┤ │◀─────────────────────────────┤ │ @@ -261,12 +261,12 @@ Bob receives: `Relay message from Alice [abc123]: Can you help with the auth mod ### Direct Message ``` -@relay:Bob Please review the authentication module +>>relay:Bob Please review the authentication module ``` ### Broadcast ``` -@relay:* I've completed the database migration - all tests passing +>>relay:* I've completed the database migration - all tests passing ``` ### Structured (Block Format) @@ -276,10 +276,10 @@ Bob receives: `Relay message from Alice [abc123]: Can you help with the auth mod ### Common Conventions ``` -@relay:* STATUS: Starting work on auth module -@relay:* DONE: Auth module complete -@relay:Reviewer REVIEW: Please check src/auth/*.ts -@relay:Architect QUESTION: Should we use JWT or sessions? +>>relay:* STATUS: Starting work on auth module +>>relay:* DONE: Auth module complete +>>relay:Reviewer REVIEW: Please check src/auth/*.ts +>>relay:Architect QUESTION: Should we use JWT or sessions? ``` --- @@ -321,7 +321,7 @@ agent-relay/ │ ├── wrapper/ │ │ ├── tmux-wrapper.ts # Tmux session management │ │ ├── client.ts # Daemon client -│ │ └── parser.ts # @relay: pattern matching +│ │ └── parser.ts # >>relay: pattern matching │ ├── protocol/ │ │ ├── types.ts # Message types │ │ └── framing.ts # Wire format diff --git a/README.md b/README.md index b5834a5b0..47e06e770 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,11 @@ agent-relay -n Alice claude agent-relay -n Bob codex ``` -Agents communicate by outputting `@relay:` patterns: +Agents communicate by outputting `>>relay:` patterns: ``` -@relay:Bob Hey, can you review my changes? -@relay:* Broadcasting to everyone +>>relay:Bob Hey, can you review my changes? +>>relay:* Broadcasting to everyone ``` ## CLI @@ -44,7 +44,7 @@ Agents communicate by outputting `@relay:` patterns: ## How It Works 1. `agent-relay up` starts a daemon that routes messages via Unix socket -2. `agent-relay ` wraps your agent in tmux, parsing output for `@relay:` patterns +2. `agent-relay ` wraps your agent in tmux, parsing output for `>>relay:` patterns 3. Messages are injected into recipient terminals in real-time ``` @@ -67,13 +67,13 @@ Agents communicate by outputting `@relay:` patterns: ### Send Message ``` -@relay:AgentName Your message here +>>relay:AgentName Your message here ``` ### Broadcast ``` -@relay:* Message to all agents +>>relay:* Message to all agents ``` ### Receive diff --git a/docs/AGENTS.md b/docs/AGENTS.md index b98958430..574583446 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -74,7 +74,7 @@ relay wrap -n PlayerO claude ### Why tmux? - Real terminal: you attach directly to tmux (no double PTY). -- Background capture/parse of `@relay:` without touching stdout. +- Background capture/parse of `>>relay:` without touching stdout. - Reliable injection via `tmux send-keys`. --- @@ -107,8 +107,8 @@ relay team status **Inline format** (simple messages): ``` -@relay:AgentName Your message here -@relay:* Broadcast to all agents +>>relay:AgentName Your message here +>>relay:* Broadcast to all agents ``` **Block format** (structured data): @@ -137,11 +137,11 @@ Use this when you're wrapped with `relay wrap`. ### CRITICAL: How to Send Messages -**You (the AI agent) must OUTPUT the @relay pattern as part of your response.** Do not wait for user input. The pattern is detected from your terminal output. +**You (the AI agent) must OUTPUT the >>relay pattern as part of your response.** Do not wait for user input. The pattern is detected from your terminal output. **Correct - Output this directly:** ``` -@relay:PlayerO I've finished the API refactor. Ready for your review. +>>relay:PlayerO I've finished the API refactor. Ready for your review. ``` **Wrong - Don't use bash commands for real-time messaging:** @@ -152,28 +152,28 @@ relay team send -n MyName -t PlayerO -m "message" ### Pattern Requirements -The `@relay:` pattern must appear on its own line. It can have common terminal/markdown prefixes: +The `>>relay:` pattern must appear on its own line. It can have common terminal/markdown prefixes: ``` -@relay:AgentName message Works - @relay:AgentName message Works (leading whitespace OK) -> @relay:AgentName message Works (input prompt OK) -$ @relay:AgentName message Works (shell prompt OK) -- @relay:AgentName message Works (list items OK) -* @relay:AgentName message Works (asterisk lists OK) -Some text @relay:AgentName msg Won't work (not at line start) +>>relay:AgentName message Works + >>relay:AgentName message Works (leading whitespace OK) +> >>relay:AgentName message Works (input prompt OK) +$ >>relay:AgentName message Works (shell prompt OK) +- >>relay:AgentName message Works (list items OK) +* >>relay:AgentName message Works (asterisk lists OK) +Some text >>relay:AgentName msg Won't work (not at line start) ``` ### Examples **Direct message:** ``` -@relay:PlayerO Your turn! I played X at center. +>>relay:PlayerO Your turn! I played X at center. ``` **Broadcast to all agents:** ``` -@relay:* I've completed the authentication module. Ready for review. +>>relay:* I've completed the authentication module. Ready for review. ``` **Structured data:** @@ -190,7 +190,7 @@ When another agent sends you a message, it appears in your terminal as: Relay message from PlayerO: Their message content here ``` -Respond by outputting another `@relay:` pattern. +Respond by outputting another `>>relay:` pattern. ### IMPORTANT: Handling Truncated Messages @@ -217,9 +217,9 @@ This retrieves the complete message from the database. ### Escaping -To output literal `@relay:` without triggering the parser: +To output literal `>>relay:` without triggering the parser: ``` -\@relay: This won't be sent as a message +\>>relay: This won't be sent as a message ``` --- @@ -317,7 +317,7 @@ Just 4 commands: ### Task Handoff ``` -@relay:Developer TASK: Implement user registration endpoint +>>relay:Developer TASK: Implement user registration endpoint Requirements: - POST /api/register - Validate email format @@ -328,29 +328,29 @@ Requirements: ### Status Updates ``` -@relay:* STATUS: Starting work on authentication module -@relay:* DONE: Authentication module complete, ready for review -@relay:Reviewer REVIEW: Please review src/auth/*.ts +>>relay:* STATUS: Starting work on authentication module +>>relay:* DONE: Authentication module complete, ready for review +>>relay:Reviewer REVIEW: Please review src/auth/*.ts ``` ### Requesting Help ``` -@relay:Architect QUESTION: Should we use JWT or session-based auth? -@relay:* BLOCKED: Need database credentials to proceed +>>relay:Architect QUESTION: Should we use JWT or session-based auth? +>>relay:* BLOCKED: Need database credentials to proceed ``` ### Code Review Flow ``` # Developer requests review -@relay:Reviewer REVIEW: src/api/users.ts - Added pagination support +>>relay:Reviewer REVIEW: src/api/users.ts - Added pagination support # Reviewer provides feedback -@relay:Developer FEEDBACK: Line 45 - Consider using cursor-based pagination for better performance +>>relay:Developer FEEDBACK: Line 45 - Consider using cursor-based pagination for better performance # Developer confirms fix -@relay:Reviewer FIXED: Updated to cursor-based pagination, please re-review +>>relay:Reviewer FIXED: Updated to cursor-based pagination, please re-review ``` --- diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 877cded5b..fab7c512d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,7 +5,7 @@ Initial public release. - Local daemon + client protocol for low-latency agent messaging (Unix domain sockets). -- `agent-relay wrap` for intercepting `@relay:AgentName ...` and `[[RELAY]]...[[/RELAY]]` messages. +- `agent-relay wrap` for intercepting `>>relay:AgentName ...` and `[[RELAY]]...[[/RELAY]]` messages. - Inbox utilities (`inbox-write`, `inbox-poll`, etc.) for file-based coordination in shared workspaces. - Built-in demos/games (e.g., tic-tac-toe) to validate turn-based coordination. diff --git a/docs/DESIGN_V2.md b/docs/DESIGN_V2.md index b63d515d5..65141e484 100644 --- a/docs/DESIGN_V2.md +++ b/docs/DESIGN_V2.md @@ -4,7 +4,7 @@ This document outlines improvements to agent-relay while preserving its core philosophy: **simple, transparent agent-to-agent communication via terminal output patterns**. -The `@relay:` pattern is the killer feature. Agents communicate naturally by just printing text. No APIs, no SDKs, no special integrations. This must remain the foundation. +The `>>relay:` pattern is the killer feature. Agents communicate naturally by just printing text. No APIs, no SDKs, no special integrations. This must remain the foundation. --- @@ -38,7 +38,7 @@ The `@relay:` pattern is the killer feature. Agents communicate naturally by jus 1. **Keep it simple** - Every feature must justify its complexity 2. **Terminal-native** - Users stay in tmux, not a browser -3. **Pattern-based** - `@relay:` is the API +3. **Pattern-based** - `>>relay:` is the API 4. **Zero config** - Works out of the box 5. **Debuggable** - Easy to understand what's happening @@ -362,7 +362,7 @@ src/ │ └── router.ts # Message routing + queue ├── wrapper/ │ ├── tmux-wrapper.ts # Agent wrapper -│ ├── parser.ts # @relay: pattern parser +│ ├── parser.ts # >>relay: pattern parser │ └── client.ts # Relay client ├── protocol/ │ └── types.ts # Message types (reduced) @@ -410,11 +410,11 @@ Group agents for targeted messaging: } # Send to group -@relay:@backend We need to refactor the user service +>>relay:@backend We need to refactor the user service # → Message delivered to ApiDev, DbAdmin, AuthService # Broadcast to all -@relay:* Starting deployment in 5 minutes +>>relay:* Starting deployment in 5 minutes ``` **Implementation:** @@ -506,9 +506,9 @@ One agent orchestrates the others: ``` Coordinator - ├── @relay:ApiDev Implement /api/users endpoint - ├── @relay:DbAdmin Create users table - └── @relay:UiDev Build user profile page + ├── >>relay:ApiDev Implement /api/users endpoint + ├── >>relay:DbAdmin Create users table + └── >>relay:UiDev Build user profile page ApiDev → Coordinator: Done, endpoint at /api/users DbAdmin → Coordinator: Table created with schema... @@ -524,11 +524,11 @@ Agents pass work sequentially: ``` Developer → Reviewer → QA → Deployer -@relay:Reviewer PR #123 ready for review +>>relay:Reviewer PR #123 ready for review ↓ -@relay:QA Review passed, ready for testing +>>relay:QA Review passed, ready for testing ↓ -@relay:Deployer Tests passed, deploy when ready +>>relay:Deployer Tests passed, deploy when ready ``` #### Pattern 3: Pub/Sub Topics @@ -537,10 +537,10 @@ Agents subscribe to topics of interest: ```bash # Agent subscribes to topic -@relay:subscribe security-alerts +>>relay:subscribe security-alerts # Any agent can publish -@relay:topic:security-alerts Found SQL injection in auth.ts +>>relay:topic:security-alerts Found SQL injection in auth.ts # All subscribers receive the message ``` @@ -548,14 +548,14 @@ Agents subscribe to topics of interest: **Implementation:** ```typescript // Subscribe syntax -@relay:+topic-name # Subscribe -@relay:-topic-name # Unsubscribe -@relay:#topic-name msg # Publish to topic +>>relay:+topic-name # Subscribe +>>relay:-topic-name # Unsubscribe +>>relay:#topic-name msg # Publish to topic // In parser.ts -const TOPIC_SUBSCRIBE = /^@relay:\+(\S+)$/; -const TOPIC_UNSUBSCRIBE = /^@relay:-(\S+)$/; -const TOPIC_PUBLISH = /^@relay:#(\S+)\s+(.+)$/; +const TOPIC_SUBSCRIBE = /^>>relay:\+(\S+)$/; +const TOPIC_UNSUBSCRIBE = /^>>relay:-(\S+)$/; +const TOPIC_PUBLISH = /^>>relay:#(\S+)\s+(.+)$/; ``` ### 5.4 Tmux Layout Helper @@ -639,21 +639,21 @@ With more agents, message prioritization becomes important: ```bash # Urgent message (interrupts immediately) -@relay:!ApiDev Production is down, check auth service +>>relay:!ApiDev Production is down, check auth service # Normal message (waits for idle) -@relay:ApiDev When you have time, review this PR +>>relay:ApiDev When you have time, review this PR # Low priority (batched, delivered during quiet periods) -@relay:?ApiDev FYI: Updated the style guide +>>relay:?ApiDev FYI: Updated the style guide ``` **Injection behavior:** | Priority | Syntax | Behavior | |----------|--------|----------| -| Urgent | `@relay:!Name` | Inject immediately, even if busy | -| Normal | `@relay:Name` | Wait for idle (current behavior) | -| Low | `@relay:?Name` | Batch and deliver during long idle | +| Urgent | `>>relay:!Name` | Inject immediately, even if busy | +| Normal | `>>relay:Name` | Wait for idle (current behavior) | +| Low | `>>relay:?Name` | Batch and deliver during long idle | ### 5.7 Status Broadcasts @@ -661,9 +661,9 @@ Agents automatically announce state changes: ```typescript // Automatic status messages -@relay:* STATUS: ApiDev is now idle -@relay:* STATUS: Reviewer completed task (closed PR #123) -@relay:* STATUS: QA disconnected +>>relay:* STATUS: ApiDev is now idle +>>relay:* STATUS: Reviewer completed task (closed PR #123) +>>relay:* STATUS: QA disconnected // Agents can filter these // In wrapper config: @@ -755,7 +755,7 @@ The goal: **Single pane of glass, but in the terminal** │ │ NATIVE TMUX SESSION │ │ │ │ │ │ │ │ claude> Working on the API endpoint... │ │ -│ │ @relay:DbAdmin Need the users table schema │ │ +│ │ >>relay:DbAdmin Need the users table schema │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ @@ -1055,7 +1055,7 @@ Key changes: | Feature | Effort | Priority | |---------|--------|----------| -| Agent groups (`@relay:@groupname`) | 1 day | P1 | +| Agent groups (`>>relay:@groupname`) | 1 day | P1 | | TUI dashboard (`agent-relay watch`) | 2 days | P1 | | Tmux layout helper | 0.5 day | P2 | | Message priority (`!`, `?`) | 0.5 day | P2 | diff --git a/docs/INTEGRATION-GUIDE.md b/docs/INTEGRATION-GUIDE.md index d64c2fd4f..bb7cb55ea 100644 --- a/docs/INTEGRATION-GUIDE.md +++ b/docs/INTEGRATION-GUIDE.md @@ -17,7 +17,7 @@ How to integrate agent-relay, claude-mem, and agent-trajectories into a cohesive │ │ CLAUDE-MEM │ │ AGENT-RELAY │ │ │ │ (Observations) │ │ (Messaging) │ │ │ │ │ │ │ │ -│ │ • Tool calls │ │ • @relay:Agent │ │ +│ │ • Tool calls │ │ • >>relay:Agent │ │ │ │ • Concepts │ │ • Broadcasting │ │ │ │ • Sessions │ │ • Persistence │ │ │ └────────┬────────┘ └────────┬────────┘ │ @@ -863,7 +863,7 @@ trajectory export ENG-456 --format markdown │ ├──────────────────┬───────────────────┐ │ │ ▼ ▼ ▼ │ │ ┌─────────┐ ┌───────────┐ ┌────────────┐ │ -│ │ Tool │ │ @relay: │ │[[TRAJECTORY│ │ +│ │ Tool │ │ >>relay: │ │[[TRAJECTORY│ │ │ │ Calls │ │ messages │ │ :decision]]│ │ │ └────┬────┘ └─────┬─────┘ └──────┬─────┘ │ │ │ │ │ │ diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 31ef4f5ed..a4f5ef75e 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -290,15 +290,15 @@ Client should send fresh HELLO. ### Inline Format (single line only) ``` -@relay:codex-1 Your turn to play -@thinking:* Considering the Queen... +>>relay:codex-1 Your turn to play +>>thinking:* Considering the Queen... ``` ### Rules 1. Block: Only parse when `[[RELAY]]` at start of line 2. Inline: Only at start of line, not in code fences -3. Escape: `\@relay:` outputs literal `@relay:` +3. Escape: `\>>relay:` outputs literal `>>relay:` --- diff --git a/docs/SCALING_ANALYSIS.md b/docs/SCALING_ANALYSIS.md index afb204768..131f89197 100644 --- a/docs/SCALING_ANALYSIS.md +++ b/docs/SCALING_ANALYSIS.md @@ -145,7 +145,7 @@ Let's separate "nice to have" from "essential": 2. **Know who's busy vs idle** → Activity state in agents output 3. **Message history** → `agent-relay history` (already proposed) 4. **Quick attach** → `agent-relay attach Alice` -5. **Group messaging** → `@relay:@backend` (already proposed) +5. **Group messaging** → `>>relay:@backend` (already proposed) ### Nice to Have (but complex) @@ -228,7 +228,7 @@ The TUI dashboard is over-engineered. tmux already provides: ## Updated Design Recommendation ### Keep from Phase 5: -- Agent groups (`@relay:@backend`) +- Agent groups (`>>relay:@backend`) - Message priority (`!`, `?`) - `agent-relay agents` command - `agent-relay history` command @@ -262,7 +262,7 @@ watch -n2 agent-relay agents | See all agents | Open TUI, see list | `agent-relay agents` or `Ctrl+B w` | | Switch to Bob | Press 'a', select Bob | `Ctrl+B 1` or `agent-relay attach Bob` | | Check if Alice is busy | See "typing..." in TUI | Check status column in agents | -| Send message to group | Same | Same (`@relay:@backend`) | +| Send message to group | Same | Same (`>>relay:@backend`) | | View history | Press 'h' in TUI | `agent-relay history` | **The simpler approach does everything the TUI does, with 80% less code.** diff --git a/docs/TMUX_IMPLEMENTATION_NOTES.md b/docs/TMUX_IMPLEMENTATION_NOTES.md index 82a0614b7..7fcdb834c 100644 --- a/docs/TMUX_IMPLEMENTATION_NOTES.md +++ b/docs/TMUX_IMPLEMENTATION_NOTES.md @@ -47,7 +47,7 @@ The previous tmux implementation had these issues: │ TmuxWrapper (this process) │ │ - Polls capture-pane @ 100ms │ │ - Detects new output (diff) │ -│ - Parses @relay: commands │ +│ - Parses >>relay: commands │ │ - Writes to stdout for user │ │ - Injects messages via send-keys│ │ - Forwards user stdin to tmux │ @@ -103,12 +103,12 @@ process.stdin.on('data', (data) => { - Solution: Use -S - to get full scrollback, diff against last capture 2. **Binary/escape sequences**: Output might contain control characters - - Solution: Strip ANSI codes before parsing @relay: + - Solution: Strip ANSI codes before parsing >>relay: 3. **Session death**: tmux session might exit - Solution: Monitor with `tmux has-session -t session` -4. **Multiple messages**: Several @relay: in one capture +4. **Multiple messages**: Several >>relay: in one capture - Solution: Track last processed line/position 5. **Stdin race conditions**: User types while we inject @@ -119,7 +119,7 @@ process.stdin.on('data', (data) => { 1. **Basic session start**: Does claude actually launch in tmux? 2. **Output capture**: Can we see claude's output? 3. **Input injection**: Can we send text and get response? -4. **@relay detection**: Does parser find @relay: commands? +4. **>>relay detection**: Does parser find >>relay: commands? 5. **Full game**: Two agents playing tic-tac-toe ## Rollback Plan @@ -194,10 +194,10 @@ node dist/cli/index.js send -f Coordinator -t PlayerX -m "Hello from Coordinator node dist/cli/index.js start -f # Terminal 2: PlayerX -node dist/cli/index.js wrap --tmux2 -n PlayerX -- claude -p "You are PlayerX playing tic-tac-toe. Use @relay:PlayerO to send moves. Start with your first move." +node dist/cli/index.js wrap --tmux2 -n PlayerX -- claude -p "You are PlayerX playing tic-tac-toe. Use >>relay:PlayerO to send moves. Start with your first move." # Terminal 3: PlayerO -node dist/cli/index.js wrap --tmux2 -n PlayerO -- claude -p "You are PlayerO playing tic-tac-toe. Use @relay:PlayerX to send moves. Wait for PlayerX to start." +node dist/cli/index.js wrap --tmux2 -n PlayerO -- claude -p "You are PlayerO playing tic-tac-toe. Use >>relay:PlayerX to send moves. Wait for PlayerX to start." ``` ### Debugging @@ -253,7 +253,7 @@ The message was successfully typed into the bash terminal via tmux send-keys. 1. Test with Claude CLI (replace bash with claude) 2. Test full tic-tac-toe game between two agents -3. Verify @relay: command parsing works in both directions +3. Verify >>relay: command parsing works in both directions 4. Check if multi-round injection remains stable (previous implementations failed after 2-3 rounds) ### Critical Question @@ -306,10 +306,10 @@ The critical test is whether injection remains stable over multiple rounds: node dist/cli/index.js start -f # Terminal 2: PlayerX -node dist/cli/index.js wrap --tmux2 -n PlayerX -- claude -p "Play tic-tac-toe. Use @relay:PlayerO to send moves." +node dist/cli/index.js wrap --tmux2 -n PlayerX -- claude -p "Play tic-tac-toe. Use >>relay:PlayerO to send moves." # Terminal 3: PlayerO -node dist/cli/index.js wrap --tmux2 -n PlayerO -- claude -p "Play tic-tac-toe. Use @relay:PlayerX to send moves." +node dist/cli/index.js wrap --tmux2 -n PlayerO -- claude -p "Play tic-tac-toe. Use >>relay:PlayerX to send moves." ``` If this works for a full game (5-9 moves), we've solved the problem. diff --git a/docs/TMUX_IMPROVEMENTS.md b/docs/TMUX_IMPROVEMENTS.md index 21de5e271..9d9984743 100644 --- a/docs/TMUX_IMPROVEMENTS.md +++ b/docs/TMUX_IMPROVEMENTS.md @@ -22,7 +22,7 @@ │ ├─► setInterval(200ms) │ │ │ └─► execAsync('tmux capture-pane -p -J -S -') │ │ │ └─► parser.parse(output) │ -│ │ └─► detect @relay: patterns │ +│ │ └─► detect >>relay: patterns │ │ │ └─► send to daemon │ │ │ │ │ └─► onMessage from daemon │ @@ -234,7 +234,7 @@ tmux send-keys -t $SESSION -l "Message: $MSG" ```typescript // Parser handles real-world terminal mess -const INLINE_RELAY = /^(?:\s*(?:[>$%#→➜›»●•◦‣⁃\-*⏺◆◇○□■]\s*)*)?@relay:(\S+)\s+(.+)$/; +const INLINE_RELAY = /^(?:\s*(?:[>$%#→➜›»●•◦‣⁃\-*⏺◆◇○□■]\s*)*)?>>relay:(\S+)\s+(.+)$/; // Strip ANSI codes const ANSI_PATTERN = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b\].*?(?:\x07|\x1b\\)|\r/g; @@ -263,7 +263,7 @@ send-relay-message.sh Bob "Subject" "Body" | Aspect | Pattern Parsing (Ours) | API-based (Theirs) | |--------|------------------------|-------------------| | **Agent effort** | Just output text | Call external script | -| **Natural** | Yes (`@relay:Bob hi`) | No (shell command) | +| **Natural** | Yes (`>>relay:Bob hi`) | No (shell command) | | **Reliable** | ~95% (edge cases) | 100% (structured) | | **Multi-line** | Complex (continuation) | Easy (JSON body) | | **ANSI codes** | Must strip | N/A | @@ -430,7 +430,7 @@ async function injectSafe(text: string): Promise { Gemini CLI uses `@` for file references: ```bash gemini> @src/main.ts # References a file -gemini> @relay:Bob Hi # Gemini might try to open file "relay:Bob"! +gemini> >>relay:Bob Hi # Gemini might try to open file "relay:Bob"! ``` This could explain why Gemini agents have trouble sending relay messages - the CLI intercepts `@` before it reaches the terminal output. @@ -455,7 +455,7 @@ Support multiple prefixes with a default that works everywhere: ```typescript // In config { - "relayPrefix": "@relay:", // Default (works for Claude, Codex) + "relayPrefix": ">>relay:", // Default (works for Claude, Codex) // Alternatives: // "relayPrefix": ">>", // For Gemini // "relayPrefix": "/relay", // Slash command style @@ -463,7 +463,7 @@ Support multiple prefixes with a default that works everywhere: } // In parser.ts -const prefix = config.relayPrefix || '@relay:'; +const prefix = config.relayPrefix || '>>relay:'; const pattern = new RegExp(`^(?:\\s*)?${escapeRegex(prefix)}(\\S+)\\s+(.+)$`); ``` @@ -477,21 +477,21 @@ agent-relay -n GeminiAgent --prefix=">>" gemini >>Bob: Can you review this code? # Instead of: -@relay:Bob Can you review this code? +>>relay:Bob Can you review this code? ``` ### Implementation: CLI Flag ```typescript // In cli/index.ts -.option('--prefix ', 'Relay pattern prefix (default: @relay:)') +.option('--prefix ', 'Relay pattern prefix (default: >>relay:)') // In wrapper config const wrapperConfig: TmuxWrapperConfig = { name: options.name, command: mainCommand, args: commandArgs, - relayPrefix: options.prefix || '@relay:', + relayPrefix: options.prefix || '>>relay:', // ... }; ``` @@ -505,7 +505,7 @@ export class OutputParser { private inlinePattern: RegExp; constructor(options: ParserOptions = {}) { - this.prefix = options.prefix || '@relay:'; + this.prefix = options.prefix || '>>relay:'; // Build pattern dynamically const escaped = this.escapeRegex(this.prefix); @@ -535,7 +535,7 @@ function getDefaultPrefix(cliType: string): string { case 'claude': case 'codex': default: - return '@relay:'; // Original, works fine + return '>>relay:'; // Original, works fine } } ``` @@ -570,7 +570,7 @@ Our tmux wrapper uses an **attach-based polling architecture**: │ │ │ Background (every 200ms) │ │ └─ tmux capture-pane -p -J -S - │ -│ └─ Parse for @relay: patterns │ +│ └─ Parse for >>relay: patterns │ │ └─ Send detected commands to daemon │ │ │ │ Message Injection │ @@ -664,12 +664,12 @@ We only need tmux and Node.js. No native compilation (node-pty), no browser comp ### 3. Pattern-Based Communication -Agents just output `@relay:Name message`. No API calls, no special handling. +Agents just output `>>relay:Name message`. No API calls, no special handling. ``` # Our approach - agent outputs text naturally Claude: I'll ask Bob for help. -@relay:Bob Can you review the auth module? +>>relay:Bob Can you review the auth module? # Alternative - agent calls external script Claude: I'll ask Bob for help. diff --git a/docs/competitive-analysis-mcp-agent-mail.md b/docs/competitive-analysis-mcp-agent-mail.md index 4e97d5684..193d2bbb3 100644 --- a/docs/competitive-analysis-mcp-agent-mail.md +++ b/docs/competitive-analysis-mcp-agent-mail.md @@ -75,7 +75,7 @@ │ Tmux Session │ │ ├─ Agent Process (claude, codex, gemini) │ │ ├─ Silent Polling (capture-pane @ 200ms) │ -│ ├─ Pattern Detection (@relay:) │ +│ ├─ Pattern Detection (>>relay:) │ │ └─ Message Injection (send-keys) │ └────────────────────────────┬────────────────────────────────┘ │ Unix Socket IPC @@ -112,8 +112,8 @@ | Feature | mcp_agent_mail | agent-relay | |---------|----------------|-------------| -| **API** | `send_message()` tool call | `@relay:Name message` pattern | -| **Broadcast** | `to=["*"]` | `@relay:*` | +| **API** | `send_message()` tool call | `>>relay:Name message` pattern | +| **Broadcast** | `to=["*"]` | `>>relay:*` | | **Attachments** | Yes (images, files) | No | | **Threading** | Yes (`thread_id`) | No | | **Markdown** | Full GFM support | Plain text | @@ -182,7 +182,7 @@ |---------|----------------|-------------| | **Setup** | Python venv, pip install, .env | npm install, single binary | | **Integration** | MCP client required | Pattern in stdout | -| **Learning Curve** | Steep (40+ tools) | Minimal (@relay:) | +| **Learning Curve** | Steep (40+ tools) | Minimal (>>relay:) | | **Debugging** | Structured logging, metrics | Dashboard + logs | **Winner: agent-relay** - Much simpler to get started. @@ -272,12 +272,12 @@ 1. **Add File Reservation System** (from mcp_agent_mail) - Advisory locks with TTL - - `@relay:lock src/**/*.ts` pattern + - `>>relay:lock src/**/*.ts` pattern - Pre-commit hook integration - Critical for multi-agent file editing 2. **Add Message Threading** - - `@relay:Bob [thread:feature-123] message` + - `>>relay:Bob [thread:feature-123] message` - Group related messages - Better context tracking @@ -294,12 +294,12 @@ ### Medium Priority 5. **Add Message Attachments** - - `@relay:Bob [attach:path/to/file]` + - `>>relay:Bob [attach:path/to/file]` - Inline small files - Reference large files 6. **Add Cross-Project Messaging** - - `@relay:Bob@other-project message` + - `>>relay:Bob@other-project message` - Contact request system - Product grouping @@ -321,7 +321,7 @@ - Contact policies 10. **Add Workflow Macros** - - `@relay:macro:start-session` + - `>>relay:macro:start-session` - Bundle common patterns - Reduce boilerplate @@ -341,7 +341,7 @@ Consider a **hybrid architecture** that combines the best of both: │ ┌────────────────────────────▼────────────────────────────────┐ │ PTY Wrapper (fast path) │ -│ ├─ Pattern Detection (@relay:) │ +│ ├─ Pattern Detection (>>relay:) │ │ ├─ Message Injection │ │ └─ Direct daemon IPC (<5ms) │ └────────────────────────────┬────────────────────────────────┘ diff --git a/examples/basic-chat/README.md b/examples/basic-chat/README.md index 23d4411aa..11ddac953 100644 --- a/examples/basic-chat/README.md +++ b/examples/basic-chat/README.md @@ -23,7 +23,7 @@ npx agent-relay wrap -n Alice "claude" ``` Once Claude starts, you can tell it: -> "Your name is Alice. You're chatting with Bob via agent-relay. Say hello to Bob by typing: @relay:Bob Hello Bob! I'm Alice." +> "Your name is Alice. You're chatting with Bob via agent-relay. Say hello to Bob by typing: >>relay:Bob Hello Bob! I'm Alice." ### Terminal 3: Agent Bob @@ -37,13 +37,13 @@ Once Claude starts, you can tell it: ## How It Works 1. Each agent is wrapped with `agent-relay wrap`, which: - - Intercepts terminal output looking for `@relay:` patterns + - Intercepts terminal output looking for `>>relay:` patterns - Sends matched messages through the daemon to other agents - Injects received messages into the agent's terminal 2. Messages use the inline format: ``` - @relay:RecipientName Your message here + >>relay:RecipientName Your message here ``` 3. Received messages appear as: @@ -71,6 +71,6 @@ agent-relay inbox-write -t Alice -f Bob -m "Hi Alice!" -d /tmp/chat ## Tips -- Use `@relay:*` to broadcast to all connected agents -- Use `\@relay:` to output literal text without triggering the relay +- Use `>>relay:*` to broadcast to all connected agents +- Use `\>>relay:` to output literal text without triggering the relay - Check daemon status with `npx agent-relay status` diff --git a/examples/collaborative-task/README.md b/examples/collaborative-task/README.md index 9199741c9..d3fe491e6 100644 --- a/examples/collaborative-task/README.md +++ b/examples/collaborative-task/README.md @@ -27,7 +27,7 @@ npx agent-relay wrap -n Architect "claude" ``` Tell the agent: -> "You are the Architect. Your job is to design a solution for adding user authentication. Once you have a plan, message Developer with the design using @relay:Developer" +> "You are the Architect. Your job is to design a solution for adding user authentication. Once you have a plan, message Developer with the design using >>relay:Developer" ### Terminal 3: Developer ```bash @@ -74,21 +74,21 @@ Agents use structured communication: ```bash # Architect assigns task -@relay:Developer TASK: Implement user registration endpoint. +>>relay:Developer TASK: Implement user registration endpoint. Requirements: POST /api/register, validate email, hash password, return JWT. # Developer requests review -@relay:Reviewer REVIEW REQUEST: Please review src/api/register.ts +>>relay:Reviewer REVIEW REQUEST: Please review src/api/register.ts # Reviewer provides feedback -@relay:Developer FEEDBACK: Line 23: Use bcrypt instead of md5 for password hashing. +>>relay:Developer FEEDBACK: Line 23: Use bcrypt instead of md5 for password hashing. # Developer notifies completion -@relay:Architect DONE: Registration endpoint implemented and reviewed. +>>relay:Architect DONE: Registration endpoint implemented and reviewed. ``` ## Tips - Use clear prefixes (TASK:, REVIEW:, FEEDBACK:, DONE:) for structured communication -- Broadcast status updates with `@relay:*` +- Broadcast status updates with `>>relay:*` - Keep messages concise - agents can read files for details diff --git a/package.json b/package.json index fade1976b..61bf5b2d7 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,9 @@ "access": "public" }, "scripts": { + "postinstall": "npm rebuild better-sqlite3", "build": "npm run clean && tsc", - "postbuild": "cp -r src/dashboard/public dist/dashboard/", + "postbuild": "cp -r src/dashboard/public dist/dashboard/ && chmod +x dist/cli/index.js", "dev": "tsc -w", "dev:local": "npm run build && npm link && echo '✓ agent-relay linked globally'", "dev:unlink": "npm unlink -g agent-relay && echo '✓ agent-relay unlinked'", diff --git a/src/cli/index.ts b/src/cli/index.ts index 1b4be25b4..7df92444a 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -11,7 +11,7 @@ import { Command } from 'commander'; import { config as dotenvConfig } from 'dotenv'; -import { Daemon, DEFAULT_SOCKET_PATH } from '../daemon/server.js'; +import { Daemon } from '../daemon/server.js'; import { RelayClient } from '../wrapper/client.js'; import { generateAgentName } from '../utils/name-generator.js'; import fs from 'node:fs'; @@ -47,7 +47,7 @@ program program .option('-n, --name ', 'Agent name (auto-generated if not set)') .option('-q, --quiet', 'Disable debug output', false) - .option('--prefix ', 'Relay prefix pattern (default: @relay: or >> for Gemini)') + .option('--prefix ', 'Relay prefix pattern (default: >>relay:)') .argument('[command...]', 'Command to wrap (e.g., claude)') .action(async (commandParts, options) => { // If no command provided, show help diff --git a/src/wrapper/parser.test.ts b/src/wrapper/parser.test.ts index 7656a52f9..df9a298dd 100644 --- a/src/wrapper/parser.test.ts +++ b/src/wrapper/parser.test.ts @@ -12,22 +12,22 @@ describe('OutputParser', () => { parser = new OutputParser(); }); - describe('Inline format - @relay:target message', () => { + describe('Inline format - >>relay:target message', () => { it('parses basic inline relay command', () => { - const result = parser.parse('@relay:agent2 Hello there\n'); + const result = parser.parse('>>relay:agent2 Hello there\n'); expect(result.commands).toHaveLength(1); expect(result.commands[0]).toMatchObject({ to: 'agent2', kind: 'message', body: 'Hello there', - raw: '@relay:agent2 Hello there', + raw: '>>relay:agent2 Hello there', }); expect(result.output).toBe(''); }); it('extracts target and body correctly', () => { - const result = parser.parse('@relay:supervisor This is a longer message with multiple words\n'); + const result = parser.parse('>>relay:supervisor This is a longer message with multiple words\n'); expect(result.commands).toHaveLength(1); expect(result.commands[0].to).toBe('supervisor'); @@ -35,22 +35,22 @@ describe('OutputParser', () => { }); it('only matches at start of line (after whitespace)', () => { - const result = parser.parse(' @relay:agent2 Indented message\n'); + const result = parser.parse(' >>relay:agent2 Indented message\n'); expect(result.commands).toHaveLength(1); expect(result.commands[0].to).toBe('agent2'); expect(result.commands[0].body).toBe('Indented message'); }); - it('does not match @relay: in middle of line', () => { - const result = parser.parse('This is text @relay:agent2 should not match\n'); + it('does not match >>relay: in middle of line', () => { + const result = parser.parse('This is text >>relay:agent2 should not match\n'); expect(result.commands).toHaveLength(0); - expect(result.output).toBe('This is text @relay:agent2 should not match\n'); + expect(result.output).toBe('This is text >>relay:agent2 should not match\n'); }); - it('handles @thinking: variant', () => { - const result = parser.parse('@thinking:agent2 Considering the options\n'); + it('handles >>thinking: variant', () => { + const result = parser.parse('>>thinking:agent2 Considering the options\n'); expect(result.commands).toHaveLength(1); expect(result.commands[0]).toMatchObject({ @@ -61,7 +61,7 @@ describe('OutputParser', () => { }); it('parses multiple inline commands', () => { - const result = parser.parse('@relay:agent1 First message\n@relay:agent2 Second message\n'); + const result = parser.parse('>>relay:agent1 First message\n>>relay:agent2 Second message\n'); expect(result.commands).toHaveLength(2); expect(result.commands[0].to).toBe('agent1'); @@ -72,7 +72,7 @@ describe('OutputParser', () => { it('parses multi-line inline command with indented continuation', () => { // TUI wrapping indents continuation lines - const result = parser.parse('@relay:agent2 First line\n Second line\n'); + const result = parser.parse('>>relay:agent2 First line\n Second line\n'); expect(result.commands).toHaveLength(1); expect(result.commands[0].body).toBe('First line\n Second line'); @@ -80,7 +80,7 @@ describe('OutputParser', () => { }); it('does not swallow subsequent inline command after indented continuation', () => { - const result = parser.parse('@relay:agent1 First line\n Second line\n@relay:agent2 Next\n'); + const result = parser.parse('>>relay:agent1 First line\n Second line\n>>relay:agent2 Next\n'); expect(result.commands).toHaveLength(2); expect(result.commands[0].body).toBe('First line\n Second line'); @@ -89,8 +89,8 @@ describe('OutputParser', () => { }); it('does not treat non-indented lines as continuation', () => { - // Non-indented lines after @relay should be regular output - const result = parser.parse('@relay:agent2 Message\nRegular output\n'); + // Non-indented lines after >>relay should be regular output + const result = parser.parse('>>relay:agent2 Message\nRegular output\n'); expect(result.commands).toHaveLength(1); expect(result.commands[0].body).toBe('Message'); @@ -98,7 +98,7 @@ describe('OutputParser', () => { }); it('does not require spaces in target name', () => { - const result = parser.parse('@relay:agent-with-dashes Message here\n'); + const result = parser.parse('>>relay:agent-with-dashes Message here\n'); expect(result.commands).toHaveLength(1); expect(result.commands[0].to).toBe('agent-with-dashes'); @@ -194,25 +194,25 @@ describe('OutputParser', () => { }); describe('Code fence handling', () => { - it('ignores @relay: inside code fences', () => { - const input = '```\n@relay:agent2 This should be ignored\n```\n'; + it('ignores >>relay: inside code fences', () => { + const input = '```\n>>relay:agent2 This should be ignored\n```\n'; const result = parser.parse(input); expect(result.commands).toHaveLength(0); - expect(result.output).toBe('```\n@relay:agent2 This should be ignored\n```\n'); + expect(result.output).toBe('```\n>>relay:agent2 This should be ignored\n```\n'); }); it('tracks code fence state correctly', () => { - const input = 'Before fence\n```\n@relay:agent2 Inside fence\n```\nAfter fence\n@relay:agent3 Outside fence\n'; + const input = 'Before fence\n```\n>>relay:agent2 Inside fence\n```\nAfter fence\n>>relay:agent3 Outside fence\n'; const result = parser.parse(input); expect(result.commands).toHaveLength(1); expect(result.commands[0].to).toBe('agent3'); - expect(result.output).toContain('@relay:agent2 Inside fence'); + expect(result.output).toContain('>>relay:agent2 Inside fence'); }); it('handles multiple code fences', () => { - const input = '```\n@relay:a1 First fence\n```\nBetween\n```\n@relay:a2 Second fence\n```\n@relay:a3 Outside\n'; + const input = '```\n>>relay:a1 First fence\n```\nBetween\n```\n>>relay:a2 Second fence\n```\n>>relay:a3 Outside\n'; const result = parser.parse(input); expect(result.commands).toHaveLength(1); @@ -220,11 +220,11 @@ describe('OutputParser', () => { }); it('handles code fence with language specifier', () => { - const input = '```javascript\n@relay:agent2 Code example\n```\n'; + const input = '```javascript\n>>relay:agent2 Code example\n```\n'; const result = parser.parse(input); expect(result.commands).toHaveLength(0); - expect(result.output).toContain('@relay:agent2 Code example'); + expect(result.output).toContain('>>relay:agent2 Code example'); }); it('does not interfere with block format in code fence', () => { @@ -237,32 +237,32 @@ describe('OutputParser', () => { }); describe('Escaping', () => { - it('\\@relay: outputs as @relay: without triggering command', () => { - const result = parser.parse('\\@relay:agent2 This is escaped\n'); + it('\\>>relay: outputs as >>relay: without triggering command', () => { + const result = parser.parse('\\>>relay:agent2 This is escaped\n'); expect(result.commands).toHaveLength(0); - expect(result.output).toBe('@relay:agent2 This is escaped\n'); + expect(result.output).toBe('>>relay:agent2 This is escaped\n'); }); - it('\\@thinking: outputs as @thinking: without triggering command', () => { - const result = parser.parse('\\@thinking:agent2 This is escaped\n'); + it('\\>>thinking: outputs as >>thinking: without triggering command', () => { + const result = parser.parse('\\>>thinking:agent2 This is escaped\n'); expect(result.commands).toHaveLength(0); - expect(result.output).toBe('@thinking:agent2 This is escaped\n'); + expect(result.output).toBe('>>thinking:agent2 This is escaped\n'); }); it('escapes work with indentation', () => { - const result = parser.parse(' \\@relay:agent2 Indented escape\n'); + const result = parser.parse(' \\>>relay:agent2 Indented escape\n'); expect(result.commands).toHaveLength(0); - expect(result.output).toBe(' @relay:agent2 Indented escape\n'); + expect(result.output).toBe(' >>relay:agent2 Indented escape\n'); }); it('only escapes at line start', () => { - const result = parser.parse('Text \\@relay:agent2 Not escaped\n'); + const result = parser.parse('Text \\>>relay:agent2 Not escaped\n'); expect(result.commands).toHaveLength(0); - expect(result.output).toBe('Text \\@relay:agent2 Not escaped\n'); + expect(result.output).toBe('Text \\>>relay:agent2 Not escaped\n'); }); }); @@ -270,9 +270,9 @@ describe('OutputParser', () => { it('inline commands must be complete in single chunk (no cross-chunk buffering)', () => { // Inline relay commands split across chunks are NOT detected // This is intentional for minimal terminal interference - const result1 = parser.parse('@relay:agent2 Partial'); + const result1 = parser.parse('>>relay:agent2 Partial'); expect(result1.commands).toHaveLength(0); - expect(result1.output).toBe('@relay:agent2 Partial'); // Passed through + expect(result1.output).toBe('>>relay:agent2 Partial'); // Passed through const result2 = parser.parse(' line\n'); expect(result2.commands).toHaveLength(0); // Not detected @@ -290,18 +290,18 @@ describe('OutputParser', () => { it('flush() does not detect incomplete inline commands (no buffering)', () => { // Incomplete inline commands without newline are passed through, not buffered - const result1 = parser.parse('@relay:agent2 No newline'); - expect(result1.output).toBe('@relay:agent2 No newline'); // Passed through + const result1 = parser.parse('>>relay:agent2 No newline'); + expect(result1.output).toBe('>>relay:agent2 No newline'); // Passed through const result = parser.flush(); expect(result.commands).toHaveLength(0); // Not detected }); it('flush() clears all state', () => { - parser.parse('```\n@relay:agent2 In fence'); + parser.parse('```\n>>relay:agent2 In fence'); parser.flush(); - const result = parser.parse('@relay:agent3 After flush\n'); + const result = parser.parse('>>relay:agent3 After flush\n'); expect(result.commands).toHaveLength(1); expect(result.commands[0].to).toBe('agent3'); }); @@ -316,10 +316,10 @@ describe('OutputParser', () => { }); it('reset() clears code fence state', () => { - parser.parse('```\n@relay:agent2 test'); + parser.parse('```\n>>relay:agent2 test'); parser.reset(); - const result = parser.parse('@relay:agent3 After reset\n'); + const result = parser.parse('>>relay:agent3 After reset\n'); expect(result.commands).toHaveLength(1); expect(result.commands[0].to).toBe('agent3'); }); @@ -355,7 +355,7 @@ describe('OutputParser', () => { }); it('mixes relay commands with regular output', () => { - const input = 'Output 1\n@relay:agent2 Message\nOutput 2\n'; + const input = 'Output 1\n>>relay:agent2 Message\nOutput 2\n'; const result = parser.parse(input); expect(result.commands).toHaveLength(1); @@ -370,7 +370,7 @@ describe('OutputParser', () => { }); it('handles target with special characters', () => { - const result = parser.parse('@relay:agent_2-test.v1 Message\n'); + const result = parser.parse('>>relay:agent_2-test.v1 Message\n'); expect(result.commands).toHaveLength(1); expect(result.commands[0].to).toBe('agent_2-test.v1'); @@ -379,7 +379,7 @@ describe('OutputParser', () => { it('handles empty body in inline format', () => { // Note: The regex requires at least one character for the body (.+) // so this actually won't match as a command - const result = parser.parse('@relay:agent2 Test\n'); + const result = parser.parse('>>relay:agent2 Test\n'); expect(result.commands).toHaveLength(1); expect(result.commands[0].body).toBe('Test'); @@ -396,10 +396,10 @@ describe('OutputParser', () => { describe('Parser options', () => { it('disables inline format when enableInline is false', () => { const customParser = new OutputParser({ enableInline: false }); - const result = customParser.parse('@relay:agent2 Message\n'); + const result = customParser.parse('>>relay:agent2 Message\n'); expect(result.commands).toHaveLength(0); - expect(result.output).toBe('@relay:agent2 Message\n'); + expect(result.output).toBe('>>relay:agent2 Message\n'); }); it('disables block format when enableBlock is false', () => { @@ -424,9 +424,9 @@ describe('OutputParser', () => { describe('Complex scenarios', () => { it('handles multiple commands in one parse call', () => { - const input = `@relay:agent1 First + const input = `>>relay:agent1 First Regular output -@relay:agent2 Second +>>relay:agent2 Second [[RELAY]]{"to":"agent3","type":"message","body":"Third"}[[/RELAY]] More output `; @@ -442,9 +442,9 @@ More output it('handles incremental parsing with multiple parse calls', () => { parser.parse('Line 1\n'); - parser.parse('@relay:agent1 Message 1\n'); + parser.parse('>>relay:agent1 Message 1\n'); parser.parse('Line 2\n'); - const result = parser.parse('@relay:agent2 Message 2\n'); + const result = parser.parse('>>relay:agent2 Message 2\n'); // Only the last parse call returns commands from that call expect(result.commands).toHaveLength(1); @@ -468,9 +468,9 @@ More output it('preserves order of commands and output', () => { const input = `Out1 -@relay:agent1 Msg1 +>>relay:agent1 Msg1 Out2 -@relay:agent2 Msg2 +>>relay:agent2 Msg2 Out3 `; const result = parser.parse(input); @@ -483,11 +483,11 @@ Out3 }); describe('Configurable prefix', () => { - it('uses default @relay: prefix', () => { + it('uses default >>relay: prefix', () => { const defaultParser = new OutputParser(); - expect(defaultParser.prefix).toBe('@relay:'); + expect(defaultParser.prefix).toBe('>>relay:'); - const result = defaultParser.parse('@relay:agent2 Hello\n'); + const result = defaultParser.parse('>>relay:agent2 Hello\n'); expect(result.commands).toHaveLength(1); expect(result.commands[0].to).toBe('agent2'); }); @@ -502,12 +502,12 @@ Out3 expect(result.commands[0].body).toBe('Hello from Gemini'); }); - it('ignores @relay: when using >> prefix', () => { - const customParser = new OutputParser({ prefix: '>>' }); + it('ignores >>relay: when using @msg: prefix', () => { + const customParser = new OutputParser({ prefix: '@msg:' }); - const result = customParser.parse('@relay:agent2 Should not match\n'); + const result = customParser.parse('>>relay:agent2 Should not match\n'); expect(result.commands).toHaveLength(0); - expect(result.output).toBe('@relay:agent2 Should not match\n'); + expect(result.output).toBe('>>relay:agent2 Should not match\n'); }); it('uses custom prefix /relay', () => { diff --git a/src/wrapper/parser.ts b/src/wrapper/parser.ts index 2b9e2e625..ffa08de8d 100644 --- a/src/wrapper/parser.ts +++ b/src/wrapper/parser.ts @@ -3,13 +3,13 @@ * Extracts relay commands from agent terminal output. * * Supports two formats: - * 1. Inline: @relay: (single line, start of line only) + * 1. Inline: >>relay: (single line, start of line only) * 2. Block: [[RELAY]]{ json }[[/RELAY]] (multi-line, structured) * * Rules: * - Inline only matches at start of line (after whitespace) * - Ignores content inside code fences - * - Escape with \@relay: to output literal + * - Escape with \>>relay: to output literal * - Block format is preferred for structured data */ @@ -29,9 +29,9 @@ export interface ParserOptions { maxBlockBytes?: number; enableInline?: boolean; enableBlock?: boolean; - /** Relay prefix pattern (default: '@relay:') */ + /** Relay prefix pattern (default: '>>relay:') */ prefix?: string; - /** Thinking prefix pattern (default: '@thinking:') */ + /** Thinking prefix pattern (default: '>>thinking:') */ thinkingPrefix?: string; } @@ -39,8 +39,8 @@ const DEFAULT_OPTIONS: Required = { maxBlockBytes: 1024 * 1024, // 1 MiB enableInline: true, enableBlock: true, - prefix: '@relay:', - thinkingPrefix: '@thinking:', + prefix: '>>relay:', + thinkingPrefix: '>>thinking:', }; // Static patterns (not prefix-dependent) @@ -58,7 +58,7 @@ function escapeRegex(str: string): string { * Build inline pattern for a given prefix * Allow common input prefixes: >, $, %, #, →, ➜, bullets (●•◦‣⁃-*⏺◆◇○□■), and their variations * - * Supports optional thread syntax: @relay:Target [thread:id] message + * Supports optional thread syntax: >>relay:Target [thread:id] message * Thread IDs can contain alphanumeric chars, hyphens, underscores */ function buildInlinePattern(prefix: string): RegExp { @@ -68,7 +68,7 @@ function buildInlinePattern(prefix: string): RegExp { } /** - * Build escape pattern for a given prefix (e.g., \@relay: or \>>) + * Build escape pattern for a given prefix (e.g., \>>relay: or \>>) */ function buildEscapePattern(prefix: string, thinkingPrefix: string): RegExp { // Extract the first character(s) that would be escaped diff --git a/src/wrapper/tmux-wrapper.test.ts b/src/wrapper/tmux-wrapper.test.ts index dd9276ec0..0f5b5b72a 100644 --- a/src/wrapper/tmux-wrapper.test.ts +++ b/src/wrapper/tmux-wrapper.test.ts @@ -6,25 +6,22 @@ import { describe, it, expect } from 'vitest'; import { getDefaultPrefix } from './tmux-wrapper.js'; describe('TmuxWrapper constants', () => { - // Test that importing the module works and constants are defined - // Note: The constants are module-private, so we test their usage indirectly - // through the behaviors they control - + // Unified prefix across all CLI types describe('getDefaultPrefix', () => { - it('returns >> for gemini CLI type', () => { - expect(getDefaultPrefix('gemini')).toBe('>>'); + it('returns >>relay: for gemini CLI type', () => { + expect(getDefaultPrefix('gemini')).toBe('>>relay:'); }); - it('returns @relay: for claude CLI type', () => { - expect(getDefaultPrefix('claude')).toBe('@relay:'); + it('returns >>relay: for claude CLI type', () => { + expect(getDefaultPrefix('claude')).toBe('>>relay:'); }); - it('returns @relay: for codex CLI type', () => { - expect(getDefaultPrefix('codex')).toBe('@relay:'); + it('returns >>relay: for codex CLI type', () => { + expect(getDefaultPrefix('codex')).toBe('>>relay:'); }); - it('returns @relay: for other CLI type', () => { - expect(getDefaultPrefix('other')).toBe('@relay:'); + it('returns >>relay: for other CLI type', () => { + expect(getDefaultPrefix('other')).toBe('>>relay:'); }); }); }); diff --git a/src/wrapper/tmux-wrapper.ts b/src/wrapper/tmux-wrapper.ts index efb2a1141..6c801eece 100644 --- a/src/wrapper/tmux-wrapper.ts +++ b/src/wrapper/tmux-wrapper.ts @@ -5,7 +5,7 @@ * 1. Start agent in detached tmux session * 2. Attach user to tmux (they see real terminal) * 3. Background: poll capture-pane silently (no stdout writes) - * 4. Background: parse @relay commands, send to daemon + * 4. Background: parse >>relay commands, send to daemon * 5. Background: inject messages via send-keys * * The key insight: user sees the REAL tmux session, not a proxy. @@ -65,23 +65,17 @@ export interface TmuxWrapperConfig { cliType?: 'claude' | 'codex' | 'gemini' | 'other'; /** Enable tmux mouse mode for scroll passthrough (default: true) */ mouseMode?: boolean; - /** Relay prefix pattern (default: '@relay:' or '>>' for Gemini) */ + /** Relay prefix pattern (default: '>>relay:') */ relayPrefix?: string; } /** * Get the default relay prefix for a given CLI type. - * Gemini uses '>>' to avoid conflict with @ file references. + * All agents now use '>>relay:' as the unified prefix. */ export function getDefaultPrefix(cliType: 'claude' | 'codex' | 'gemini' | 'other'): string { - switch (cliType) { - case 'gemini': - return '>>'; // Avoid @ conflict with Gemini file references - case 'claude': - case 'codex': - default: - return '@relay:'; // Original, works fine - } + // Unified prefix for all agent types + return '>>relay:'; } export class TmuxWrapper { @@ -427,7 +421,7 @@ export class TmuxWrapper { } /** - * Start silent polling for @relay commands + * Start silent polling for >>relay commands * Does NOT write to stdout - just parses and sends to daemon */ private startSilentPolling(): void { @@ -439,7 +433,7 @@ export class TmuxWrapper { } /** - * Poll for @relay commands in output (silent) + * Poll for >>relay commands in output (silent) */ private async pollForRelayCommands(): Promise { if (!this.running) return; @@ -447,11 +441,11 @@ export class TmuxWrapper { try { // Capture scrollback const { stdout } = await execAsync( - // -J joins wrapped lines to avoid truncating @relay commands mid-line + // -J joins wrapped lines to avoid truncating >>relay commands mid-line `tmux capture-pane -t ${this.sessionName} -p -J -S - 2>/dev/null` ); - // Always parse the FULL capture for @relay commands + // Always parse the FULL capture for >>relay commands // This handles terminal UIs that rewrite content in place const cleanContent = this.stripAnsi(stdout); // Join continuation lines that TUIs split across multiple lines @@ -491,10 +485,10 @@ export class TmuxWrapper { } /** - * Join continuation lines after @relay commands. + * Join continuation lines after >>relay commands. * Claude Code and other TUIs insert real newlines in output, causing - * @relay messages to span multiple lines. This joins indented - * continuation lines back to the @relay line. + * >>relay messages to span multiple lines. This joins indented + * continuation lines back to the >>relay line. */ private joinContinuationLines(content: string): string { const lines = content.split('\n'); @@ -514,7 +508,7 @@ export class TmuxWrapper { while (i < lines.length) { const line = lines[i]; - // Check if this is a @relay line + // Check if this is a >>relay line if (relayPattern.test(line)) { let joined = line; let j = i + 1; @@ -665,17 +659,12 @@ export class TmuxWrapper { this.logStderr(`Injecting message from ${msg.from} (cli: ${this.cliType})`); try { - let sanitizedBody = msg.body.replace(/[\r\n]+/g, ' ').trim(); + const sanitizedBody = msg.body.replace(/[\r\n]+/g, ' ').trim(); // Short message ID for display (first 8 chars) const shortId = msg.messageId.substring(0, 8); - // Truncate very long messages to avoid display issues - const maxLen = 2000; - let wasTruncated = false; - if (sanitizedBody.length > maxLen) { - sanitizedBody = sanitizedBody.substring(0, maxLen) + '...'; - wasTruncated = true; - } + // Remove message truncation to allow full messages to pass through + const wasTruncated = false; // Always include message ID; add lookup hint if truncated const idTag = `[${shortId}]`; @@ -765,30 +754,56 @@ export class TmuxWrapper { } /** - * Check if the input line is clear (no user-typed text after the prompt). - * Returns true if the last visible line appears to be just a prompt. + * Get the prompt pattern for the current CLI type. + */ + private getPromptPattern(): RegExp { + const promptPatterns: Record = { + claude: /^[>›»]\s*$/, // Claude: "> " or similar + gemini: /^[>›»]\s*$/, // Gemini: "> " + codex: /^[>›»]\s*$/, // Codex: "> " + other: /^[>$%#➜›»]\s*$/, // Shell or other: "$ ", "> ", etc. + }; + + return promptPatterns[this.cliType] || promptPatterns.other; + } + + /** + * Capture the last non-empty line from the tmux pane. */ - private async isInputClear(): Promise { + private async getLastLine(): Promise { try { const { stdout } = await execAsync( `tmux capture-pane -t ${this.sessionName} -p -J 2>/dev/null` ); const lines = stdout.split('\n').filter(l => l.length > 0); - const lastLine = lines[lines.length - 1] || ''; + return lines[lines.length - 1] || ''; + } catch { + return ''; + } + } + + /** + * Detect if the provided line contains visible user input (beyond the prompt). + */ + private hasVisibleInput(line: string): boolean { + const cleanLine = this.stripAnsi(line).trimEnd(); + if (cleanLine === '') return false; - // CLI-specific prompt patterns (prompt char + optional whitespace, nothing else) - const promptPatterns: Record = { - claude: /^[>›»]\s*$/, // Claude: "> " or similar - gemini: /^[>›»]\s*$/, // Gemini: "> " - codex: /^[>›»]\s*$/, // Codex: "> " - other: /^[>$%#➜›»]\s*$/, // Shell or other: "$ ", "> ", etc. - }; + return !this.getPromptPattern().test(cleanLine); + } - const pattern = promptPatterns[this.cliType] || promptPatterns.other; - const isClear = pattern.test(lastLine); + /** + * Check if the input line is clear (no user-typed text after the prompt). + * Returns true if the last visible line appears to be just a prompt. + */ + private async isInputClear(lastLine?: string): Promise { + try { + const lineToCheck = lastLine ?? await this.getLastLine(); + const cleanLine = this.stripAnsi(lineToCheck).trimEnd(); + const isClear = this.getPromptPattern().test(cleanLine); if (this.config.debug) { - const truncatedLine = lastLine.substring(0, Math.min(DEBUG_LOG_TRUNCATE_LENGTH, lastLine.length)); + const truncatedLine = cleanLine.substring(0, Math.min(DEBUG_LOG_TRUNCATE_LENGTH, cleanLine.length)); this.logStderr(`isInputClear: lastLine="${truncatedLine}", clear=${isClear}`); } @@ -828,14 +843,18 @@ export class TmuxWrapper { let stableCursorCount = 0; while (Date.now() - startTime < maxWaitMs) { + const lastLine = await this.getLastLine(); + // Check if input line is just a prompt - if (await this.isInputClear()) { + if (await this.isInputClear(lastLine)) { return true; } + const hasInput = this.hasVisibleInput(lastLine); + // Also check cursor stability - if cursor is moving, agent is typing const cursorX = await this.getCursorX(); - if (cursorX === lastCursorX) { + if (!hasInput && cursorX === lastCursorX) { stableCursorCount++; // If cursor has been stable for enough polls and at typical prompt position, // the agent might be done but we just can't match the prompt pattern