tickmarkr 2.1.2 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,26 +14,66 @@
14
14
  # handoff is stale, the seat is holding state that a clear would destroy, and the correct move is to wake
15
15
  # a supervisor — never to clear and hope.
16
16
  #
17
- # usage: watch-context.sh <agent|pane> <warn-pct> <act-pct> [handoff-file] [poll-s] [cap-s]
17
+ # THIS WATCHER IS SUPERVISED, and the tier is PER SEAT: `<role>-context`, one per supervising seat, so a
18
+ # live overseer watcher can never make a dead orchestrator one read as covered. Every beat declares the
19
+ # seat it watches (`--seat`), because a tier that is armed and seatless reads as coverage, which is worse
20
+ # than absent. Four rules the shipped version broke, each of which made the tier lie:
21
+ # 1. BEAT ON THE SUPERVISION CADENCE, NOT ON THE POLL. Beats gap by TICK (below), never by POLL, so a
22
+ # poll interval above the supervision beat interval cannot leave the tier stale half of every cycle.
23
+ # 2. BEAT ONLY AFTER A SUCCESSFUL READ. A watcher that cannot see its seat's percentage is not
24
+ # watching it; beating anyway reports coverage it is not providing, and the tier must age out.
25
+ # 3. WARN DOES NOT EXIT. Warn precedes act, so exiting at warn meant the act was never reached and the
26
+ # last beat aged into a permanent stale — gradual growth never reached the auto-clear path at all.
27
+ # 4. EVERY TERMINAL EXIT STANDS THE TIER DOWN, so a watcher that finished reads DISARMED, not dead.
28
+ # Only a killed watcher reads STALE, which is exactly what STALE means.
29
+ #
30
+ # usage: watch-context.sh <orchestrator|overseer> <agent|pane> <warn-pct> <act-pct> [handoff-file] [poll-s] [cap-s]
18
31
  # TKR_AUTO_CLEAR=1 at act-pct WITH a fresh handoff, send /clear and re-brief instead of waking.
19
32
  # TKR_REBRIEF=<path> the file the re-briefed seat is told to read (defaults to the handoff).
20
33
  # TKR_HANDOFF_MAX_AGE_S how fresh "fresh" is (default 900).
34
+ # TKR_CLEAR_SETTLE_S seconds to let a cleared seat settle before the re-brief (default 6).
21
35
 
22
36
  set -u
23
- TARGET="${1:?agent name or pane id required}"
24
- WARN="${2:-60}"
25
- ACT="${3:-75}"
26
- HANDOFF="${4:-}"
27
- POLL="${5:-120}"
28
- CAP="${6:-28800}"
37
+ ROLE="${1:?supervising seat role required: orchestrator|overseer}"
38
+ TARGET="${2:?agent name or pane id required}"
39
+ WARN="${3:-60}"
40
+ ACT="${4:-75}"
41
+ HANDOFF="${5:-}"
42
+ POLL="${6:-120}"
43
+ CAP="${7:-28800}"
29
44
  MAXAGE="${TKR_HANDOFF_MAX_AGE_S:-900}"
30
45
  REBRIEF="${TKR_REBRIEF:-$HANDOFF}"
46
+ SETTLE="${TKR_CLEAR_SETTLE_S:-6}"
47
+
48
+ case "$ROLE" in
49
+ orchestrator|overseer) TIER="${ROLE}-context" ;;
50
+ *) echo "watch-context.sh: unknown seat role '$ROLE' — expected orchestrator or overseer" >&2; exit 64 ;;
51
+ esac
52
+
53
+ # The supervision beat interval (SUPERVISION_BEAT_MS = 10s). The loop ticks at the beat cadence or the
54
+ # caller's poll, whichever is SHORTER: a beat may only follow a successful read (rule 2), so the read
55
+ # cadence is the floor on the beat cadence, and the tier's freshness is never the caller's to widen.
56
+ BEAT_EVERY=5
57
+ TICK=$(( POLL < BEAT_EVERY ? POLL : BEAT_EVERY ))
58
+ [ "$TICK" -ge 1 ] 2>/dev/null || TICK=1 # a zero or junk poll would spin, not watch
59
+ SEAT="$TARGET"
60
+
61
+ beat() { tickmarkr beat "$TIER" --seat "$SEAT" >/dev/null 2>&1; }
62
+ stand_down() { tickmarkr beat "$TIER" --stand-down --seat "$SEAT" >/dev/null 2>&1; }
63
+ # EVERY terminal exit — act, unsafe-act, cap — leaves through here, so none of them can forget to
64
+ # record the hand-off. A killed watcher never runs it, which is the one case that must read STALE.
65
+ trap stand_down EXIT
31
66
 
32
- # The seat's own rendered truth. Anchor on the model marker so a percentage elsewhere on screen — a
33
- # progress figure, a coverage number — cannot be mistaken for the context gauge.
67
+ # The seat's own rendered truth. Prefer a line carrying a context marker so a percentage elsewhere on
68
+ # screen cannot be mistaken for the gauge — but NEVER require one: the ✳ marker the shipped version
69
+ # anchored on is rendered by a single vendor, so every other seat read empty, beat anyway and slept to
70
+ # its cap while its tier claimed coverage. The last percentage in the statusline window is the fallback.
34
71
  context_pct() {
35
- herdr agent read "$TARGET" --source visible --lines 8 2>/dev/null \
36
- | grep '✳' | tail -1 | grep -oE '[0-9]+%' | tail -1 | tr -d '%'
72
+ local screen marked
73
+ screen=$(herdr agent read "$TARGET" --source visible --lines 8 2>/dev/null) || return 1
74
+ marked=$(printf '%s\n' "$screen" | grep -iE '✳|context' | grep -oE '[0-9]+%' | tail -1 | tr -d '%')
75
+ [ -n "$marked" ] && { printf '%s\n' "$marked"; return 0; }
76
+ printf '%s\n' "$screen" | grep -oE '[0-9]+%' | tail -1 | tr -d '%'
37
77
  }
38
78
 
39
79
  handoff_fresh() {
@@ -41,46 +81,55 @@ handoff_fresh() {
41
81
  [ -f "$HANDOFF" ] || return 1
42
82
  local age now mt
43
83
  now=$(date +%s)
44
- mt=$(stat -f %m "$HANDOFF" 2>/dev/null || stat -c %Y "$HANDOFF" 2>/dev/null) || return 1
84
+ mt=$(stat -c %Y "$HANDOFF" 2>/dev/null || stat -f %m "$HANDOFF" 2>/dev/null) || return 1
45
85
  age=$((now - mt))
46
86
  [ "$age" -le "$MAXAGE" ]
47
87
  }
48
88
 
89
+ act_on() {
90
+ local P="$1"
91
+ if handoff_fresh; then
92
+ if [ "${TKR_AUTO_CLEAR:-0}" = "1" ]; then
93
+ herdr agent prompt "$TARGET" "/clear" >/dev/null 2>&1
94
+ sleep "$SETTLE"
95
+ herdr agent prompt "$TARGET" "Read ${REBRIEF} and continue exactly where it says. Your context was cleared at ${P}% against that handoff; it is current as of $(date '+%H:%M'). Do not reconstruct from memory — everything you need is on disk." >/dev/null 2>&1
96
+ echo "CONTEXT_CLEARED $TARGET at ${P}% — handoff fresh, re-briefed from ${REBRIEF}"
97
+ exit 0
98
+ fi
99
+ echo "CONTEXT_ACT $TARGET ${P}% (>= ${ACT}) — handoff is FRESH, a clear is SAFE now"
100
+ echo " herdr agent prompt $TARGET \"/clear\" then re-brief from ${REBRIEF}"
101
+ exit 0
102
+ fi
103
+ echo "CONTEXT_ACT_UNSAFE $TARGET ${P}% (>= ${ACT}) — NO FRESH HANDOFF (${HANDOFF:-none})"
104
+ echo " the seat is holding state that exists only in its head; a clear would destroy it"
105
+ echo " make it write the handoff FIRST, then clear"
106
+ exit 0
107
+ }
108
+
49
109
  warned=0
50
110
  elapsed=0
51
111
  while [ "$elapsed" -lt "$CAP" ]; do
52
112
  P=$(context_pct)
53
113
  if [ -z "$P" ]; then
54
- sleep "$POLL"; elapsed=$((elapsed + POLL)); continue
114
+ # Rule 2: no reading, no beat. The tier ages to STALE and a supervisor comes looking, which is the
115
+ # truth about a watcher that cannot see the seat it was armed on.
116
+ sleep "$TICK"; elapsed=$((elapsed + TICK)); continue
55
117
  fi
118
+ beat
56
119
 
57
120
  if [ "$P" -ge "$ACT" ] 2>/dev/null; then
58
- if handoff_fresh; then
59
- if [ "${TKR_AUTO_CLEAR:-0}" = "1" ]; then
60
- herdr agent prompt "$TARGET" "/clear" >/dev/null 2>&1
61
- sleep 6
62
- herdr agent prompt "$TARGET" "Read ${REBRIEF} and continue exactly where it says. Your context was cleared at ${P}% against that handoff; it is current as of $(date '+%H:%M'). Do not reconstruct from memory — everything you need is on disk." >/dev/null 2>&1
63
- echo "CONTEXT_CLEARED $TARGET at ${P}% — handoff fresh, re-briefed from ${REBRIEF}"
64
- exit 0
65
- fi
66
- echo "CONTEXT_ACT $TARGET ${P}% (>= ${ACT}) — handoff is FRESH, a clear is SAFE now"
67
- echo " herdr agent prompt $TARGET \"/clear\" then re-brief from ${REBRIEF}"
68
- exit 0
69
- fi
70
- echo "CONTEXT_ACT_UNSAFE $TARGET ${P}% (>= ${ACT}) — NO FRESH HANDOFF (${HANDOFF:-none})"
71
- echo " the seat is holding state that exists only in its head; a clear would destroy it"
72
- echo " make it write the handoff FIRST, then clear"
73
- exit 0
121
+ act_on "$P"
74
122
  fi
75
123
 
76
124
  if [ "$P" -ge "$WARN" ] 2>/dev/null && [ "$warned" -eq 0 ]; then
125
+ # Rule 3: warn is a LINE, not an exit — the act is on the far side of it.
77
126
  warned=1
78
127
  echo "CONTEXT_WARN $TARGET ${P}% (>= ${WARN}) — write the handoff NOW, while judgement is still good"
128
+ echo " re-brief target when it acts: ${REBRIEF:-none}"
79
129
  echo " a handoff written at ${ACT}% is written by a seat already degraded; that is the wrong time"
80
- exit 0
81
130
  fi
82
131
 
83
- sleep "$POLL"; elapsed=$((elapsed + POLL))
132
+ sleep "$TICK"; elapsed=$((elapsed + TICK))
84
133
  done
85
134
 
86
135
  echo "WATCH_CAP_REACHED $TARGET context=$(context_pct)% — no threshold crossed in ${CAP}s"