Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ data/ personal fleet records; LOCAL, gitignored as a whole
backlog.md task queue, dependencies, history
captain.md this home's domain-local captain preferences and working style; LOCAL, gitignored, canonical even if harness memory mirrors it, and updated with inspect-then-update
captain-shared.md main-authoritative shared captain preferences propagated read-only to secondmate homes; LOCAL, gitignored, owned by secondmate-provisioning
inbox-cards.md optional captain-private plain-English copy for the inbox board; see docs/inbox-board.md
learnings.md fleet-local operational facts and gotchas; LOCAL, gitignored; dated, evidence-backed, curated, and updated with inspect-then-update - rewrite and prune rather than append forever, the same contract as captain.md; created lazily, absent until this home has a learning to store
projects.md thin fleet navigation registry; firstmate-private, parsed by fm-project-mode.sh (section 6)
secondmates.md secondmate routing table; firstmate-private, maintained by fm-home-seed.sh (section 6)
Expand All @@ -101,6 +102,7 @@ state/ volatile runtime signals; gitignored
x-watch.check.sh generated X-mode relay poll shim; present only when opted in (section 14)
pending-replies/ parent-owned secondmate pending-reply records (correlation id, delivery vs reply, recovery, escalation); fm-pending-reply-lib.sh
x-inbox/ generated X-mode pending mention payloads; fmx-respond drains it (section 14)
inbox-answers/ generated captain-inbox Lavish feedback drops; inspect after the registered inbox check wakes firstmate (docs/inbox-board.md)
x-context/ generated X-mode durable per-request reply context and one-wake offer markers, keyed by request_id; survives inbox cleanup and expires within seven days (section 14; bin/fm-x-lib.sh)
x-outbox/ generated X-mode dry-run reply and dismiss previews; inspect it when FMX_DRY_RUN is set (section 14)
x-poll.error x-poll.claim-error generated X-mode relay and offer-claim diagnostic dedupe markers
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ Firstmate's skills live in two separate places with different audiences:
- [docs/architecture.md](docs/architecture.md) - maintainer architecture for the crew, supervision, worktrees, secondmates, and project modes.
- [docs/configuration.md](docs/configuration.md) - environment variables, `FM_HOME`, runtime backend selection, optional X mode, the files you set, and harness support.
- [docs/calm.md](docs/calm.md) - current Pi `/calm` behavior and supported presentation limits.
- [docs/inbox-board.md](docs/inbox-board.md) - the captain decision-and-review board, its answer relay, and current verification pointers.
- [docs/wedge-alarm.md](docs/wedge-alarm.md) - configure the active alert for an away-mode escalation delivery that gets stuck.
- [docs/tmux-backend.md](docs/tmux-backend.md) - current setup and limits for the tmux reference backend.
- [docs/herdr-backend.md](docs/herdr-backend.md) - current setup, safety boundaries, and limits for the experimental Herdr backend.
Expand Down
136 changes: 136 additions & 0 deletions bin/fm-inbox-arm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
# fm-inbox-arm.sh - write and register the captain-inbox answer relay.
#
# Writes a bounded watcher check at state/inbox.check.sh that polls the served
# board and wakes firstmate whenever the captain submits an answer, then binds
# its exact bytes with bin/fm-check-register.sh so the watcher will execute it.
# This is the fix for answers that looked silently eaten: once armed, the normal
# supervision cycle relays them without anyone remembering to poll.
#
# The generated check fails fast and stays silent when the board is not being
# served, so an armed-but-idle relay never floods. bin/fm-inbox-serve.sh calls
# this; run it directly only to (re)arm against an existing board.
#
# This writes firstmate's own private runtime state. Run it from the operating
# firstmate home.
#
# Usage:
# fm-inbox-arm.sh [--port <port>] <board.html>
# fm-inbox-arm.sh --help
set -u

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}"
STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"
PORT=${LAVISH_AXI_PORT:-4387}

usage() {
awk '
NR == 1 { next }
/^#/ { sub(/^# ?/, ""); print; next }
{ exit }
' "$0"
}

die() {
printf 'fm-inbox-arm: %s\n' "$1" >&2
exit "${2:-1}"
}

port_valid() {
local port=$1
case "$port" in
''|*[!0-9]*) return 1 ;;
esac
[ "$port" -ge 1 ] 2>/dev/null && [ "$port" -le 65535 ] 2>/dev/null
}

BOARD=
while [ "$#" -gt 0 ]; do
case "$1" in
-h|--help) usage; exit 0 ;;
--port)
[ "$#" -ge 2 ] || die "--port requires a port" 2
PORT=$2; shift 2 ;;
--) shift; break ;;
-*) usage >&2; exit 2 ;;
*)
[ -z "$BOARD" ] || die "expected exactly one board path" 2
BOARD=$1; shift ;;
esac
done
if [ "$#" -gt 0 ]; then
[ -z "$BOARD" ] || die "expected exactly one board path" 2
[ "$#" -eq 1 ] || die "expected exactly one board path" 2
BOARD=$1
fi
[ -n "$BOARD" ] || { usage >&2; exit 2; }
port_valid "$PORT" || die "invalid port: $PORT" 2
case "$BOARD" in
/*) ;;
*) BOARD="$PWD/$BOARD" ;;
esac

[ -d "$STATE" ] && [ ! -L "$STATE" ] || die "state directory is unavailable: $STATE"
[ -x "$SCRIPT_DIR/fm-check-register.sh" ] || die "fm-check-register.sh is unavailable"
[ -r "$SCRIPT_DIR/fm-pr-lib.sh" ] || die "fm-pr-lib.sh is unavailable"

# shellcheck source=bin/fm-pr-lib.sh
. "$SCRIPT_DIR/fm-pr-lib.sh"

ANSWERS="$STATE/inbox-answers"
CHECK="$STATE/inbox.check.sh"
STATE_DEVICE=$(fm_pr_file_device "$STATE") || die "state directory is unavailable: $STATE"
fm_pr_regular_destination_on_device_or_absent "$CHECK" "$STATE_DEVICE" \
|| die "relay path is unsafe: $CHECK"

printf -v BOARD_Q '%q' "$BOARD"
printf -v ANSWERS_Q '%q' "$ANSWERS"
printf -v PORT_Q '%q' "$PORT"
printf -v SCRIPT_DIR_Q '%q' "$SCRIPT_DIR"

umask 077
TMP_CHECK=$(mktemp "$STATE/.inbox.check.XXXXXX") || die "cannot stage the answer relay"
trap '[ -z "${TMP_CHECK:-}" ] || rm -f -- "$TMP_CHECK"' EXIT HUP INT TERM
cat > "$TMP_CHECK" <<SHIM || die "cannot write the answer relay"
#!/usr/bin/env bash
# Captain inbox relay - generated by fm-inbox-arm.sh; bound by fm-check-register.
# Bounded poll of the served board. ANY stdout wakes firstmate; silence keeps it
# asleep. If no local server is listening on the board port, the relay exits
# before polling and stays a cheap no-op until the board is being served.
set -u
BOARD=$BOARD_Q
ANSWERS=$ANSWERS_Q
SCRIPT_DIR=$SCRIPT_DIR_Q
export LAVISH_AXI_PORT=$PORT_Q
command -v lavish-axi >/dev/null 2>&1 || exit 0
command -v curl >/dev/null 2>&1 || exit 0
curl -s -o /dev/null --max-time 2 "http://127.0.0.1:\$LAVISH_AXI_PORT/" \
>/dev/null 2>&1 || exit 0
res=\$(lavish-axi poll "\$BOARD" --timeout-ms 5000 2>/dev/null) || exit 0
case "\$res" in *"status: feedback"*) ;; *) exit 0 ;; esac
FM_X_LIB=\$SCRIPT_DIR/fm-x-lib.sh
[ -r "\$FM_X_LIB" ] || { printf 'inbox board error: could not record captain answer in state/inbox-answers\n'; exit 0; }
. "\$FM_X_LIB" || { printf 'inbox board error: could not record captain answer in state/inbox-answers\n'; exit 0; }
stamp=\$(date -u +%Y%m%dT%H%M%SZ)
if ! (set -o pipefail; printf '%s\n' "\$res" | fmx_private_artifact_publish_stdin "\$ANSWERS" "\$stamp.txt" 600); then
printf 'inbox board error: could not record captain answer in state/inbox-answers\n'
exit 0
fi
printf 'inbox board: a captain answer is waiting in state/inbox-answers\n'
SHIM
chmod 0700 "$TMP_CHECK" || die "cannot set relay permissions"
fm_pr_private_file_valid "$TMP_CHECK" 700 "$STATE_DEVICE" \
|| die "staged relay is unsafe"
fm_pr_regular_destination_on_device_or_absent "$CHECK" "$STATE_DEVICE" \
|| die "relay path is unsafe: $CHECK"
mv -f -- "$TMP_CHECK" "$CHECK" || die "cannot publish the answer relay"
TMP_CHECK=
fm_pr_private_file_valid "$CHECK" 700 "$STATE_DEVICE" \
|| { rm -f -- "$CHECK"; die "published relay is unsafe"; }

"$SCRIPT_DIR/fm-check-register.sh" inbox >/dev/null \
|| { rm -f -- "$CHECK"; die "could not register the answer relay"; }

printf 'armed: state/inbox.check.sh relays answers for %s\n' "$BOARD"
Loading
Loading