diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-06 21:59:52 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-06 21:59:52 -0500 |
| commit | d81b23ad6b6e437dfe3c338a00a4be39bc555146 (patch) | |
| tree | 2d4b0d7890fd1fc70d81282b81fed2808c28a106 /.ai/scripts/cross-agent-comms/cross-agent-send.md | |
| parent | 201377f57430ef28d02e703a2191434bbee55c75 (diff) | |
| download | rulesets-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.md | 199 |
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. |
