aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/self-inject.sh
blob: e7340c180657d3fc7cd47f4d9fdb9f4fae68e775 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#!/bin/sh
# self-inject.sh — type text into the tmux pane running this agent session.
#
# The building block for AUTO-FLUSH: an agent checkpoints its session-context,
# then has tmux type "/clear" and a resume prompt at its own idle prompt, so a
# session flushes with no human at the keyboard.
#
# Usage:
#   self-inject.sh -t %PANE <delay> <text> [<delay2> <text2> ...]
#   self-inject.sh <delay> <text> [...]          # derive pane from ancestry
#   self-inject.sh [-t %PANE]                    # no pairs: report the pane
#
# Each pair: sleep <delay> seconds, then type <text> literally and press Enter.
#
# TWO HARD-WON GOTCHAS (2026-07-02, archsetup session):
# 1. A detached child (setsid/nohup/&) of an agent tool call DIES when the
#    tool call ends — the harness cleans up the process group. The arm step
#    must run under the tmux SERVER instead:
#      tmux run-shell -b "self-inject.sh -t %1 25 '/clear' 15 'go — resume...'"
# 2. Under tmux run-shell the process is a child of the tmux server, so
#    ancestry-based pane detection CANNOT work there. Derive the pane FIRST,
#    synchronously from the agent's own shell (no -t), then pass it
#    explicitly with -t when arming.
#
# Collision hazard: if the user happens to be typing when the send fires, the
# injected text merges into their input line (a real /clear became "/clearto"
# mid-word). Auto-flush is for sessions running unattended; warn the user to
# keep hands off for the armed window if they're present.

PANE=""
if [ "$1" = "-t" ]; then
    PANE=$2; shift 2
fi

ppid_of() {
    # /proc/<pid>/stat: pid (comm) state ppid ... — comm may contain spaces,
    # so take the 2nd field after the LAST ')'.
    stat=$(cat "/proc/$1/stat" 2>/dev/null) || return 1
    # shellcheck disable=SC2086  # word-splitting the stat tail is the point
    set -- ${stat##*) }
    echo "$2"
}

find_pane() {
    anc=" "
    pid=$$
    while [ -n "$pid" ] && [ "$pid" -gt 1 ] 2>/dev/null; do
        anc="$anc$pid "
        pid=$(ppid_of "$pid") || break
    done
    tmux list-panes -a -F "#{pane_pid} #{pane_id}" 2>/dev/null | \
    while read -r ppid pane; do
        case "$anc" in *" $ppid "*) echo "$pane"; break;; esac
    done
}

[ -n "$PANE" ] || PANE=$(find_pane)
[ -n "$PANE" ] || { echo "self-inject: no owning pane found (pass -t %PANE)" >&2; exit 1; }

# With no delay/text pairs, just report the pane (the derive-first step).
[ $# -ge 2 ] || { echo "$PANE"; exit 0; }

while [ $# -ge 2 ]; do
    sleep "$1"
    tmux send-keys -t "$PANE" -l "$2"
    tmux send-keys -t "$PANE" Enter
    shift 2
done