#!/usr/bin/env bash # Portable, append-only git relay for deliberate cross-agent communication. set -euo pipefail CONFIG_PATH="${MOS_COMMS_CONFIG:-$HOME/.config/mos-comms/mos-comms.config}" CONFIG_PRESENT=0 if [[ -f "$CONFIG_PATH" ]]; then # Configuration is local operator input. It must not be committed to the relay repo. # shellcheck source=/dev/null source "$CONFIG_PATH" CONFIG_PRESENT=1 fi AGENT_NAME="${AGENT_NAME:-}" COMMS_REMOTE="${COMMS_REMOTE:-}" COMMS_REPO_DIR="${COMMS_REPO_DIR:-$HOME/.local/state/mos-comms/repo}" COMMS_BRANCH="${COMMS_BRANCH:-mos-comms}" TMUX_SOCKET="${TMUX_SOCKET:-}" TMUX_SESSION="${TMUX_SESSION:-mos-claude}" STATE_DIR="${STATE_DIR:-$HOME/.local/state/mos-comms}" GIT_CRED_HELPER="${GIT_CRED_HELPER:-}" usage() { cat <<'EOF' Usage: mos-comms.sh setup | poll | read | send | selftest Messages are append-only files on the configured shared git branch. `poll` is mechanical: it only checks git and emits a fixed tmux notification. EOF } die() { printf 'mos-comms: %s\n' "$*" >&2 exit 1 } warn() { printf 'mos-comms: warning: %s\n' "$*" >&2 } gitc() { if [[ -n "$GIT_CRED_HELPER" ]]; then git -c credential.helper="$GIT_CRED_HELPER" "$@" else git "$@" fi } require_identity() { [[ -n "$AGENT_NAME" ]] || die 'AGENT_NAME is required in the local config' [[ "$AGENT_NAME" =~ ^[A-Za-z0-9.-]+$ ]] || die 'AGENT_NAME may contain only letters, numbers, dots, and hyphens' } require_remote() { [[ -n "$COMMS_REMOTE" ]] || die 'COMMS_REMOTE is required in the local config' } ensure_state_dir() { mkdir -p "$STATE_DIR" chmod 700 "$STATE_DIR" } remote_branch_exists() { gitc -C "$COMMS_REPO_DIR" show-ref --verify --quiet "refs/remotes/origin/$COMMS_BRANCH" } setup() { require_remote require_identity mkdir -p "$(dirname "$COMMS_REPO_DIR")" if [[ ! -d "$COMMS_REPO_DIR/.git" ]]; then gitc clone "$COMMS_REMOTE" "$COMMS_REPO_DIR" fi gitc -C "$COMMS_REPO_DIR" remote set-url origin "$COMMS_REMOTE" gitc -C "$COMMS_REPO_DIR" fetch origin if remote_branch_exists; then # Preserve any local append-only commit that is awaiting a retry; do not # reset a checked-out branch to origin here. if gitc -C "$COMMS_REPO_DIR" show-ref --verify --quiet "refs/heads/$COMMS_BRANCH"; then gitc -C "$COMMS_REPO_DIR" checkout "$COMMS_BRANCH" else gitc -C "$COMMS_REPO_DIR" checkout --track -b "$COMMS_BRANCH" "origin/$COMMS_BRANCH" fi gitc -C "$COMMS_REPO_DIR" branch --set-upstream-to="origin/$COMMS_BRANCH" "$COMMS_BRANCH" else gitc -C "$COMMS_REPO_DIR" checkout -B "$COMMS_BRANCH" fi mkdir -p "$COMMS_REPO_DIR/comms" ensure_state_dir } ensure_remote_branch() { if ! remote_branch_exists; then warn "origin branch '$COMMS_BRANCH' does not exist yet" return 1 fi } message_is_from_self() { local file="$1" [[ "$file" == *"__from-${AGENT_NAME}__"* ]] } changed_message_files() { local from_sha="$1" local to_sha="$2" if [[ -n "$from_sha" ]] && gitc -C "$COMMS_REPO_DIR" cat-file -e "$from_sha^{commit}" 2>/dev/null; then gitc -C "$COMMS_REPO_DIR" diff --name-only "$from_sha" "$to_sha" -- comms/ else gitc -C "$COMMS_REPO_DIR" ls-tree -r --name-only "$to_sha" -- comms/ fi } tmuxc() { if [[ -n "$TMUX_SOCKET" ]]; then tmux -L "$TMUX_SOCKET" "$@" else tmux "$@" fi } send() { require_identity require_remote [[ $# -ge 1 ]] || die 'send requires message text' setup local body="$*" local utc filename path utc="$(date -u +%Y%m%dT%H%M%SZ)" filename="${utc}__from-${AGENT_NAME}__${RANDOM}${RANDOM}.md" path="$COMMS_REPO_DIR/comms/$filename" umask 077 { printf '%s\n' '---' printf 'from: %s\n' "$AGENT_NAME" printf '%s\n' 'to: all' printf 'utc: %s\n' "$utc" printf '%s\n\n' '---' printf '%s\n' "$body" } >"$path" gitc -C "$COMMS_REPO_DIR" add -- "comms/$filename" if ! gitc -C "$COMMS_REPO_DIR" config user.name >/dev/null; then gitc -C "$COMMS_REPO_DIR" config user.name "$AGENT_NAME mos-comms" fi if ! gitc -C "$COMMS_REPO_DIR" config user.email >/dev/null; then gitc -C "$COMMS_REPO_DIR" config user.email "$AGENT_NAME@localhost" fi gitc -C "$COMMS_REPO_DIR" commit -m "comms: ${AGENT_NAME} ${utc}" local attempt for attempt in 1 2 3; do if remote_branch_exists; then gitc -C "$COMMS_REPO_DIR" pull --rebase origin "$COMMS_BRANCH" fi if gitc -C "$COMMS_REPO_DIR" push origin "HEAD:refs/heads/$COMMS_BRANCH"; then printf 'sent: %s\n' "$filename" return 0 fi if [[ "$attempt" -lt 3 ]]; then warn "push rejected; retrying ($attempt/3)" gitc -C "$COMMS_REPO_DIR" fetch origin fi done die 'push was rejected after 3 attempts; local message commit remains available for retry' } poll() { require_identity require_remote setup gitc -C "$COMMS_REPO_DIR" fetch origin ensure_remote_branch || return 0 local head marker last_notified head="$(gitc -C "$COMMS_REPO_DIR" rev-parse "origin/$COMMS_BRANCH")" marker="$STATE_DIR/last_notified_sha" if [[ ! -f "$marker" ]]; then printf '%s\n' "$head" >"$marker" printf 'poll: baseline recorded; existing history was not notified\n' return 0 fi last_notified="$(<"$marker")" if [[ "$last_notified" == "$head" ]]; then return 0 fi local file count=0 while IFS= read -r file; do [[ -n "$file" ]] || continue if ! message_is_from_self "$file"; then ((count += 1)) fi done < <(changed_message_files "$last_notified" "$head") if ((count > 0)); then # Intentionally static except for an integer: never inject peer data into tmux. tmuxc send-keys -t "$TMUX_SESSION" "[mos-comms] $count new peer message(s) — run: mos-comms.sh read" tmuxc send-keys -t "$TMUX_SESSION" Enter fi printf '%s\n' "$head" >"$marker" } read_messages() { require_identity require_remote setup gitc -C "$COMMS_REPO_DIR" fetch origin ensure_remote_branch || return 0 local head marker last_read file found=0 head="$(gitc -C "$COMMS_REPO_DIR" rev-parse "origin/$COMMS_BRANCH")" marker="$STATE_DIR/last_read_sha" last_read="" if [[ -f "$marker" ]]; then last_read="$(<"$marker")" fi while IFS= read -r file; do [[ -n "$file" ]] || continue if message_is_from_self "$file"; then continue fi found=1 printf '%s\n' "===== $file =====" # Peer content is printed as data. It is never evaluated or sent to tmux. gitc -C "$COMMS_REPO_DIR" show "origin/$COMMS_BRANCH:$file" printf '\n' done < <(changed_message_files "$last_read" "$head") printf '%s\n' "$head" >"$marker" if ((found == 0)); then printf 'No new peer messages.\n' fi } selftest() { local remote_ok=0 branch_ok=0 tmux_ok=0 config_ok=0 if ((CONFIG_PRESENT == 1)); then printf 'PASS config present: %s\n' "$CONFIG_PATH" config_ok=1 else printf 'FAIL config present: %s\n' "$CONFIG_PATH" fi if [[ -z "$COMMS_REMOTE" ]]; then printf 'WARN remote reachable: COMMS_REMOTE is not configured\n' elif gitc ls-remote "$COMMS_REMOTE" >/dev/null 2>&1; then printf 'PASS remote reachable\n' remote_ok=1 else printf 'FAIL remote reachable\n' fi if ((remote_ok == 1)) && gitc ls-remote --exit-code --heads "$COMMS_REMOTE" "$COMMS_BRANCH" >/dev/null 2>&1; then printf 'PASS branch resolves: %s\n' "$COMMS_BRANCH" branch_ok=1 elif [[ -z "$COMMS_REMOTE" ]]; then printf 'WARN branch resolves: skipped without a remote\n' else printf 'FAIL branch resolves: %s\n' "$COMMS_BRANCH" fi if tmuxc has-session -t "$TMUX_SESSION" 2>/dev/null; then printf 'PASS tmux session exists: %s\n' "$TMUX_SESSION" tmux_ok=1 else printf 'FAIL tmux session exists: %s\n' "$TMUX_SESSION" fi # Report all checks without making a no-remote first-run unusable. if ((config_ok == 1 && remote_ok == 1 && branch_ok == 1 && tmux_ok == 1)); then printf 'PASS selftest complete\n' else printf 'FAIL selftest incomplete (see checks above)\n' fi } case "${1:-}" in setup) [[ $# -eq 1 ]] || usage; setup ;; poll) [[ $# -eq 1 ]] || usage; poll ;; read) [[ $# -eq 1 ]] || usage; read_messages ;; send) shift; send "$@" ;; selftest) [[ $# -eq 1 ]] || usage; selftest ;; -h|--help|help) usage ;; *) usage; exit 2 ;; esac