1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
|
# cross-agent-send
**Purpose.** Send a cross-agent message file to a specific destination. Handles
peer-config lookup, GPG signing, atomic write (same-machine) or rsync push
(cross-machine), retry-with-backoff, and failure surfacing.
This is the canonical writer. The protocol spec defers all writer mechanics to
this script.
## Usage
```
cross-agent-send <destination> <message-file> [--no-sign] [--retries N]
```
### Positional arguments
| Position | Meaning | Example |
|---|---|---|
| 1 | Destination as `<machine>.<project>` | `homelab.career`, `velox.career` |
| 2 | Message file (already-formatted `.org`) | `/tmp/my-message.org` |
### Flags
| Flag | Default | Purpose |
|---|---|---|
| `--no-sign` | (signing on) | Skip GPG signing. Use only for testing; receivers reject unsigned messages by default. |
| `--retries N` | 3 | Override retry count for cross-machine sends. |
| `--key <key-id>` | (user's primary key) | GPG key to sign with. Resolution order: `--key` flag, `GPG_USER` env, `git config user.signingkey`, then the first secret key in the keyring. |
## Behavior
### Filename generation (script-controlled)
The script generates the canonical destination filename from the message's
frontmatter and sender context. The user's input filename is ignored — pass any
path, the script names the destination correctly:
```
<UTC-now>T<HHMMSS>Z-from-<sender-slug>-<short-conv-id>.org
```
`<sender-slug>` comes from the sender machine's project name (config or
hostname-based). `<short-conv-id>` is read from the message's
`#+CONVERSATION_ID` frontmatter field. UTC timestamp is generated at send time.
The script also performs the **sender-side max-seen scan** before writing: it
reads the receiver's `from-agents/` directory, finds the highest existing
sequence in this conversation across both sender prefixes, and (best-effort)
suggests `max(seen) + 1` for the next sequence. The user/agent is responsible
for setting `#+SEQUENCE` in the message body; the script only advises.
### Same-machine destinations
Resolved when the destination's machine matches the current hostname (or is
not in `peers.toml` as a remote). Steps:
1. Parse frontmatter; extract `CONVERSATION_ID` and `TIMESTAMP`. Validate per
the *Validation before send* section below.
2. Generate canonical filename per *Filename generation* above.
3. Sign: `gpg --detach-sign --armor --output <canonical>.asc --local-user <key> <input>`.
4. Compute target: read `peers.toml` for the project's `inbox_path`. If
missing, fall back to `~/projects/<project>/inbox/from-agents/`.
5. **Atomic write with strict ordering** (signature must precede message):
- Stage `.asc`: write to `<target>/.tmp.XXXXXX-<canonical>.asc`,
then `mv` to `<target>/<canonical>.asc`.
- **Then** stage `.org`: write to `<target>/.tmp.XXXXXX-<canonical>`,
then `mv` to `<target>/<canonical>`.
- Receivers only act on `.org` files; staging the `.asc` first guarantees
the signature is present when the receiver opens the message. Out-of-order
would race: receiver could read the `.org` before the `.asc` lands and
fail GPG verify even though the sender did everything right.
6. Exit 0 on success. Exit non-zero if any step fails.
### Cross-machine destinations
Steps:
1. Parse + generate canonical filename, as same-machine steps 1-2.
2. Sign locally to `<input>.asc` (or a tmp staging file).
3. rsync push **with the same .asc-first ordering**:
- `rsync -a <input>.asc <ssh-user>@<host>:<inbox_path>/<canonical>.asc`
- **Then** `rsync -a <input> <ssh-user>@<host>:<inbox_path>/<canonical>`
rsync writes to a hidden temp file then renames atomically by default
(`--inplace` would defeat this; do not pass it).
4. Retry on failure: 5s, 30s, 120s backoff, then surface error.
5. On persistent failure: write a marker file to
`~/.local/state/cross-agent-comms/failed-sends/<timestamp>-<dest>-<canonical>.json`
containing the destination, message path, error, and retry log. Exit non-zero.
### Validation before send
- Destination resolves via `peers.toml` (or local fallback). If neither, exit
immediately with `destination not found in peers.toml; available: <list>`.
- Message file must be readable, non-empty, and have valid org-mode frontmatter
with **all** of the following required fields:
- `#+TITLE`
- `#+CONVERSATION_ID`
- `#+MESSAGE_TYPE`
- `#+SEQUENCE`
- `#+TIMESTAMP`
- `#+PROTOCOL_VERSION` (must equal `5` for v5)
If any required field is missing or malformed, exit immediately with a parse
error naming the offending field.
- Optional fields the script recognizes and passes through (no special
handling beyond preservation):
- `#+REQUIRES_TOOLS` — comma-separated tool/MCP slugs the receiver needs.
- `#+RELEASE_STATUS` — valid only on `MESSAGE_TYPE: release`. Values per
spec: `complete`, `cancelled`, `withdrawn-after-pushback`,
`abandoned-after-escalation`.
- `#+WORKFLOW_VERSION` — sender's version of the cross-agent-comms workflow
file. Currently advisory; receiver may warn on mismatch but does not block.
## Configuration
Reads `~/.config/cross-agent-comms/peers.toml` for peer routing:
```toml
[peers.velox]
host = "velox.local"
ssh_user = "cjennings"
# Optional: per-project inbox-path overrides for non-default layouts.
[projects.work]
inbox_path = "~/projects/work/inbox/from-agents"
[projects.homelab]
inbox_path = "~/projects/homelab/inbox/from-agents"
```
If a project entry is omitted, defaults to `~/projects/<project>/inbox/from-agents`.
## Failure modes
| Symptom | Cause | Fix |
|---|---|---|
| `destination not found in peers.toml` | Misspelled destination, or peer not configured | Run `cross-agent-discover` to see available destinations. |
| `signing failed: no secret key` | GPG key missing or not in keyring | `gpg --list-secret-keys` to confirm. Override with `--key <id>`. |
| `signing failed: pinentry timed out` | Headless session, GUI pinentry unavailable | Confirm `pinentry-program` in `gpg-agent.conf` matches available pinentry. Per protocols.org, GUI pinentry works from Claude Code. |
| `rsync exit 255` | SSH unreachable | `cross-agent-discover --peer <name>` to confirm reachability. |
| `rsync exit 23` | Permission denied at destination | Check destination directory perms (`chmod 700`) and ownership. |
| Marker file written to `failed-sends/` | Persistent cross-machine failure | Inspect the marker's `error` field. After fixing, retry: `cross-agent-send <dest> <msg>` (the marker is for visibility; it does not auto-retry). |
| Receiver complains "unsigned message" | `--no-sign` was used in production | Don't use `--no-sign` outside testing. |
## HALT awareness
Checks `~/.config/cross-agent-comms/HALT` at the start of every send AND
between the `.asc` and `.org` rsync calls AND between each retry iteration.
On HALT exists, exits with code 5 ("halt active; remove
~/.config/cross-agent-comms/HALT to resume") without writing or pushing
further.
Worst case: one in-flight send completes its current rsync step within a few
seconds before halt kicks in for the next step. New sends are blocked
immediately. No `pkill` needed — the per-iteration check stops things
naturally.
If the HALT file exists but is unreadable (permissions wrong), fail-closed —
treat as if HALT is set. Safer than fail-open.
See `cross-agent-halt.md` for the full halt mechanism.
## Examples
```bash
# Same-machine send
cross-agent-send homelab.career /tmp/my-message.org
# Cross-machine send via Tailscale
cross-agent-send velox.career /tmp/my-message.org
# Test send without signing (receiver will reject)
cross-agent-send homelab.career /tmp/test.org --no-sign
# Override retry count for a flaky link
cross-agent-send velox.career /tmp/my-message.org --retries 10
# After a delivery failure, inspect the marker
cat ~/.local/state/cross-agent-comms/failed-sends/*.json | jq .
```
## Exit codes
| Code | Meaning |
|---|---|
| 0 | Sent successfully. |
| 1 | General error (parse failure, signing failure, etc.). |
| 2 | Destination not found in peers.toml. |
| 3 | Cross-machine delivery failed after retries. Marker file written. |
| 4 | Frontmatter validation failed. |
## See also
- `cross-agent-discover` — validate destinations before sending.
- `cross-agent-watch` — receiver-side notification.
- `cross-agent-status` — see what's queued.
- `cross-agent-comms.org` — protocol spec, the "what" the script implements.
|