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
4 changes: 4 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ If argument starts with "poke" (e.g. "poke reviewer status?"):
<!-- agmsg:slot mode -->
<!-- /agmsg:slot mode -->

If argument is "fix" (no further words):
1. Run: `~/.agents/skills/__SKILL_NAME__/scripts/fix.sh` — with NO arguments. `fix` repairs THIS session's own seat marks (placement record, pane label, agent key, session name) at the pane the seat PROVES it is in. It takes no location: the seat establishes where it is from its own process ancestry (and, when that cannot decide, by writing a token to its own screen and finding it), and when it cannot establish that, it writes nothing and says why. Whoever invokes it — a `poke` from another member, a person at the keyboard, or this skill — gets the same answer. Passing a pane, a `--pane`, or any word is refused by name: a location handed in from outside is exactly the mistake this exists to remove.
2. Show the output. Exit 0: every seat this session holds was written. Exit 2: at least one seat was left unwritten, with `state=` and `reason=` on its line. Exit 1: refused (an argument, no session id, or no seat held by this session).

If argument is "reset":
1. Run: `~/.agents/skills/__SKILL_NAME__/scripts/reset.sh "$(pwd)" __AGENT_TYPE__`
2. Tell the user the result.
Expand Down
2 changes: 1 addition & 1 deletion scripts/arrange.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ _placement() {
local agent="$1" rec ref terminal id project type
rec="$(agmsg_spawn_path "$TEAM" "$agent")"
[ -f "$rec" ] || { echo "arrange: no placement record for '$TEAM/$agent'" >&2; return 1; }
IFS=$'\t' read -r ref project type < "$rec" || true
IFS=$'\t' read -r ref project type _fence < "$rec" || true
terminal="$(agmsg_terminal_ref_terminal "$ref" 2>/dev/null)" || terminal=""
id="$(agmsg_terminal_ref_id "$ref" 2>/dev/null)" || id=""
[ -n "$terminal" ] && [ -n "$id" ] \
Expand Down
10 changes: 5 additions & 5 deletions scripts/despawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ SPAWN_REC="$(agmsg_spawn_path "$TEAM" "$NAME")"
# Asking with it would make a graceful teardown that WORKED report needs-force.
recorded_pane_state() {
[ -f "$SPAWN_REC" ] || { printf 'no-record'; return 0; }
local id _proj _type _term _bare _out _rc=0
IFS=$'\t' read -r id _proj _type < "$SPAWN_REC"
local id _proj _type _fence _term _bare _out _rc=0
IFS=$'\t' read -r id _proj _type _fence < "$SPAWN_REC"
[ -n "$id" ] || { printf 'unknown'; return 0; }
_term="$(agmsg_terminal_ref_terminal "$id")" || { printf 'unknown'; return 0; }
_bare="$(agmsg_terminal_ref_id "$id")" || { printf 'unknown'; return 0; }
Expand All @@ -99,8 +99,8 @@ recorded_pane_state() {

kill_recorded_placement() {
[ -f "$SPAWN_REC" ] || return 1
local id _proj _type _term _bare
IFS=$'\t' read -r id _proj _type < "$SPAWN_REC"
local id _proj _type _fence _term _bare
IFS=$'\t' read -r id _proj _type _fence < "$SPAWN_REC"
[ -n "$id" ] || return 1
_term="$(agmsg_terminal_ref_terminal "$id")" || return 1 # unknown/corrupt ref
_bare="$(agmsg_terminal_ref_id "$id")"
Expand All @@ -111,7 +111,7 @@ kill_recorded_placement() {

if [ "$FORCE" = "1" ]; then
[ -f "$SPAWN_REC" ] || die "no placement record for '$TEAM/$NAME' — nothing to force (was it launched via 'spawn'? graceful despawn does not need this)"
IFS=$'\t' read -r _id _proj _type < "$SPAWN_REC"
IFS=$'\t' read -r _id _proj _type _fence < "$SPAWN_REC"
if ! kill_recorded_placement; then
# Teardown NOT confirmed. Keep the record (the only retry authority), the
# registration and the lock, and say so — never claim a forced teardown that did
Expand Down
33 changes: 33 additions & 0 deletions scripts/drivers/terminals/herdr/ops.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1330,5 +1330,38 @@ _herdr_panes_of() { # <socket> <pane-list-json>
[ -n "$pane" ] || continue
printf '%s\t%s\n' "$sock" "$pane"
done
}

# Fence for a self-write (#1152). Prints "<instance>\t<terminal_id>" for one pane:
# the herdr instance this driver is talking to, named by its SOCKET PATH -- the
# same string the sweep's enumeration (terminal_enumerate_panes) uses for an
# instance, so a location the sweep hands a seat and the fence the seat stores
# compare as equal strings (pane ids repeat across instances -- w1:p2 exists in
# both `jugemu` and `oma`, measured 2026-09-11) -- and the pane's server-side
# terminal_id (unique across sessions, 52 panes / 0 crossings; CHANGES
# across a herdr restart, so a stored fence expires with the server and a later
# write refuses instead of landing in whatever now sits at that pane id).
# Each half is either a value or a namespaced reason; the caller compares both
# against the stored pair right before each mutation. This is a PREFLIGHT check,
# not an atomic fence: the read and the keystroke are separate calls, so a pane
# closed and reused between them is not caught (a real fence would need herdr
# to compare-and-type). What it removes is the day's actual accident -- a write
# resolved in one session landing in another's live pane.
terminal_fence() { # <id>
local id="$1" instance pane_json esc tid
instance="$(_herdr_sock_of "$id")"
[ -n "$instance" ] || instance="${HERDR_SOCKET_PATH:-}"
[ -n "$instance" ] || instance="unknown:no_socket_in_env"
case "$instance" in *:*|*[[:cntrl:]]*|*[[:space:]]*) instance="unknown:socket_path_malformed" ;; esac
command -v herdr >/dev/null 2>&1 || { printf '%s\tunknown:terminal_unreachable\n' "$instance"; return 2; }
_herdr_pane_id_ok "$id" || { printf '%s\tunknown:invalid_pane_id\n' "$instance"; return 2; }
pane_json="$(_herdr_cli "$id" pane get "$(_herdr_bare_of "$id")" 2>/dev/null)" || { printf '%s\tunknown:pane_query_failed\n' "$instance"; return 2; }
esc="$(printf '%s' "$pane_json" | sed "s/'/''/g")"
tid="$(sqlite3 :memory: "SELECT CASE WHEN json_type('$esc','\$.result.pane.terminal_id')='text' THEN json_extract('$esc','\$.result.pane.terminal_id') ELSE '' END" 2>/dev/null)" \
|| { printf '%s\tunknown:pane_response_invalid\n' "$instance"; return 2; }
[ -n "$tid" ] || { printf '%s\tunknown:terminal_id_missing\n' "$instance"; return 2; }
case "$tid" in *[[:cntrl:]]*|*[[:space:]]*) printf '%s\tunknown:terminal_id_malformed\n' "$instance"; return 2 ;; esac
printf '%s\t%s\n' "$instance" "$tid"
case "$instance" in unknown:*) return 2 ;; esac
return 0
}
37 changes: 37 additions & 0 deletions scripts/drivers/terminals/plain/ops.sh
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,43 @@ terminal_find_by_label() { _plain_unsupported "find_by_label"; }
terminal_label_of() { _plain_unsupported "label_of"; }
terminal_name() { _plain_unsupported "name"; }

# Fence for a self-write (#1152, #1149). A plain seat is record-only: it can
# write the placement record for the locator it was handed, and nothing else
# (no label, key or session op exists in this implementation -- see the
# capability hook, which says so in #1163's words). What it CAN verify before
# writing is that the locator's tty is the tty of the seat's own CLI process,
# observed through that process, never through the environment (launcher-
# inherited session ids were measured colliding across seats, so the emulator
# half of the locator is carried as delivered and treated as NO evidence).
#
# Prints "<emulator>\t<anchor>" where the anchor is the tty plus something that
# changes when the tty is reused -- /dev/ttysNNN is handed to the next session
# when this one ends -- namely the owning pid and its start time:
# iterm\ttty=/dev/ttys040,pid=12345,start=Sat_Sep_13_02:10:11_2026
# Every failure is a named unknown in the anchor half, and the writer writes
# nothing on any of them:
# unknown:no_seat_pid the caller gave no pid to observe
# unknown:tty_unobservable the process has no controlling tty (a daemon,
# a background job), or ps could not answer
# unknown:tty_mismatch:<observed> the process sits on a different tty than
# the locator names -- the leader typed into one
# window and this seat lives in another
# unknown:invalid_id the id is not <emulator>:<tty>
terminal_fence() { # <id> [<seat-pid>]
local id="$1" pid="${2:-}" observed start
_plain_parse_id "$id" || { printf 'unknown:invalid_id\tunknown:invalid_id\n'; return 2; }
local emulator="$_PLAIN_EMULATOR" tty="$_PLAIN_TTY"
case "$pid" in ''|*[!0-9]*) printf '%s\tunknown:no_seat_pid\n' "$emulator"; return 2 ;; esac
observed="$(ps -o tty= -p "$pid" 2>/dev/null | tr -d ' ')"
case "$observed" in ''|'??'|'-'|'?') printf '%s\tunknown:tty_unobservable\n' "$emulator"; return 2 ;; esac
case "$observed" in /dev/*) ;; *) observed="/dev/$observed" ;; esac
[ "$observed" = "$tty" ] || { printf '%s\tunknown:tty_mismatch:%s\n' "$emulator" "$observed"; return 2; }
start="$(ps -o lstart= -p "$pid" 2>/dev/null | sed 's/^ *//; s/ *$//' | tr ' ' '_')"
[ -n "$start" ] || { printf '%s\tunknown:tty_unobservable\n' "$emulator"; return 2; }
printf '%s\ttty=%s,pid=%s,start=%s\n' "$emulator" "$tty" "$pid" "$start"
return 0
}

# NO terminal_pane_process_observe HERE, deliberately.
#
# The plain driver has no pane and no process to bind to, so there is nothing for
Expand Down
17 changes: 17 additions & 0 deletions scripts/drivers/terminals/tmux/ops.sh
Original file line number Diff line number Diff line change
Expand Up @@ -759,5 +759,22 @@ terminal_enumerate_panes() {
done
done
[ -n "$err" ] && rm -f "$err"
}

# Fence for a self-write (#1152): "<instance>\t<terminal_id>". The instance is
# the socket the id names (tmux pane ids repeat across servers, one server per
# socket -- the #1051 shape); the terminal_id is the pane's shell pid, which a
# pane that was closed and recreated does not keep. Same contract and the same
# limit as the herdr op: a preflight check right before a mutation, not an atomic
# fence.
terminal_fence() { # <id>
local id="$1" sock bare pid
terminal_id_ok "$id" || { printf 'unknown:invalid_pane_id\tunknown:invalid_pane_id\n'; return 2; }
sock="$(_tmux_sock_of "$id")"; bare="$(_tmux_bare_of "$id")"
[ -n "$sock" ] || sock="default"
pid="$(_tmux_do "$id" display-message -p -t "$bare" '#{pane_pid}' 2>/dev/null)" \
|| { printf '%s\tunknown:pane_query_failed\n' "$sock"; return 2; }
case "$pid" in ''|*[!0-9]*) printf '%s\tunknown:pane_pid_missing\n' "$sock"; return 2 ;; esac
printf '%s\tpane_pid=%s\n' "$sock" "$pid"
return 0
}
16 changes: 16 additions & 0 deletions scripts/fix.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# fix -- a seat establishes where it is and repairs its own identity cells there.
# No arguments (see scripts/lib/self-fix.sh for why). Run it from the seat.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export SKILL_DIR="${SKILL_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
# shellcheck disable=SC1091
. "$SKILL_DIR/scripts/lib/self-fix.sh"
# The session this shell belongs to: what the CLI exports for its own session.
# It identifies the seat's LOCKS (which roles this session holds); it says
# nothing about where the seat is -- that is the proof's job.
export AGMSG_SESSION_ID="${AGMSG_SESSION_ID:-${CLAUDE_CODE_SESSION_ID:-${CODEX_THREAD_ID:-}}}"
[ -n "$AGMSG_SESSION_ID" ] || { echo "fix none:no_session_id (no AGMSG_SESSION_ID, CLAUDE_CODE_SESSION_ID or CODEX_THREAD_ID in this shell)" >&2; exit 1; }
rc=0
agmsg_fix_run "$@" || rc=$?
exit "$rc"
Loading
Loading