aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/cross-agent-comms/cross-agent-send.md
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-06 21:59:52 -0500
committerCraig Jennings <c@cjennings.net>2026-05-06 21:59:52 -0500
commitd81b23ad6b6e437dfe3c338a00a4be39bc555146 (patch)
tree2d4b0d7890fd1fc70d81282b81fed2808c28a106 /.ai/scripts/cross-agent-comms/cross-agent-send.md
parent201377f57430ef28d02e703a2191434bbee55c75 (diff)
downloadrulesets-d81b23ad6b6e437dfe3c338a00a4be39bc555146.tar.gz
rulesets-d81b23ad6b6e437dfe3c338a00a4be39bc555146.zip
chore(ai): initialize project notes and Claude tooling surfaces
Replace the seed notes.org with project-specific context (layout, install modes, task tracker location, recent inflection point). Bring in the synced template surfaces (protocols, workflows, scripts, references, retrospectives, someday-maybe) as tracked content for this content/documentation project.
Diffstat (limited to '.ai/scripts/cross-agent-comms/cross-agent-send.md')
-rw-r--r--.ai/scripts/cross-agent-comms/cross-agent-send.md199
1 files changed, 199 insertions, 0 deletions
diff --git a/.ai/scripts/cross-agent-comms/cross-agent-send.md b/.ai/scripts/cross-agent-comms/cross-agent-send.md
new file mode 100644
index 0000000..b06dbce
--- /dev/null
+++ b/.ai/scripts/cross-agent-comms/cross-agent-send.md
@@ -0,0 +1,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.career]
+inbox_path = "~/projects/career/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.