tickmarkr 2.2.0 → 2.3.0
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.
- package/README.md +10 -9
- package/dist/adapters/model-lints.js +9 -0
- package/dist/adapters/pi.d.ts +1 -0
- package/dist/adapters/pi.js +15 -1
- package/dist/adapters/prompt.js +11 -3
- package/dist/adapters/registry.js +13 -4
- package/dist/adapters/types.d.ts +23 -1
- package/dist/adapters/types.js +43 -2
- package/dist/cli/commands/approve.d.ts +3 -7
- package/dist/cli/commands/approve.js +26 -20
- package/dist/cli/commands/beat.js +7 -4
- package/dist/cli/commands/doctor.d.ts +6 -2
- package/dist/cli/commands/doctor.js +79 -9
- package/dist/cli/commands/init.js +36 -21
- package/dist/cli/commands/plan.js +20 -3
- package/dist/cli/commands/report.js +37 -1
- package/dist/cli/commands/verify.d.ts +5 -0
- package/dist/cli/commands/verify.js +142 -25
- package/dist/cli/commands/version.d.ts +2 -1
- package/dist/cli/commands/version.js +25 -4
- package/dist/compile/collateral.js +15 -9
- package/dist/compile/native.js +5 -3
- package/dist/config/config.js +1 -1
- package/dist/drivers/index.d.ts +7 -0
- package/dist/drivers/index.js +40 -10
- package/dist/drivers/orca.d.ts +41 -1
- package/dist/drivers/orca.js +192 -15
- package/dist/drivers/subprocess.d.ts +3 -3
- package/dist/drivers/subprocess.js +16 -9
- package/dist/drivers/types.d.ts +2 -0
- package/dist/gates/baseline.d.ts +4 -0
- package/dist/gates/baseline.js +68 -16
- package/dist/gates/llm.d.ts +7 -1
- package/dist/gates/llm.js +66 -35
- package/dist/gates/review.d.ts +3 -1
- package/dist/gates/review.js +42 -12
- package/dist/gates/run-gates.d.ts +6 -0
- package/dist/gates/run-gates.js +25 -10
- package/dist/gates/verdict-cause.d.ts +6 -2
- package/dist/gates/verdict-cause.js +8 -4
- package/dist/run/consult.d.ts +7 -0
- package/dist/run/consult.js +21 -3
- package/dist/run/daemon.d.ts +14 -0
- package/dist/run/daemon.js +249 -27
- package/dist/run/git.d.ts +1 -0
- package/dist/run/git.js +4 -0
- package/dist/run/journal.d.ts +15 -2
- package/dist/run/journal.js +70 -12
- package/dist/run/supervision.d.ts +6 -0
- package/dist/run/supervision.js +29 -1
- package/dist/tui/ink/init-app.js +4 -4
- package/package.json +1 -1
- package/skills/tickmarkr-loop/SKILL.md +1 -0
- package/skills/tickmarkr-overseer/SKILL.md +88 -18
- package/skills/tickmarkr-overseer/scripts/seat-send.sh +88 -18
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +36 -2
- package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +36 -15
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +35 -9
- package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +32 -8
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Wake the overseer when spawned seats have actually DELIVERED.
|
|
3
3
|
#
|
|
4
|
-
# watch-artifacts.sh <marker> <cap-seconds> <poll-seconds> <file>...
|
|
4
|
+
# watch-artifacts.sh [--changed-from <sha|mtime|now>] <marker> <cap-seconds> <poll-seconds> <file>...
|
|
5
5
|
#
|
|
6
6
|
# Completion is the ARTIFACT plus its TERMINAL MARKER — never an agent's `done`, which is turn end and
|
|
7
7
|
# fires the moment a seat finishes acknowledging you. Never file existence alone either: a seat killed
|
|
@@ -26,12 +26,29 @@ set -u
|
|
|
26
26
|
# arm crashed before printing its reason — a watcher that dies without a wake reason is the exact failure
|
|
27
27
|
# this script exists to prevent.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
CHANGED_FROM=""
|
|
30
|
+
if [ "${1:-}" = "--changed-from" ]; then
|
|
31
|
+
CHANGED_FROM="${2:?--changed-from requires a sha, prior mtime, or 'now'}"
|
|
32
|
+
shift 2
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
MARKER="${1:?usage: watch-artifacts.sh [--changed-from <sha|mtime|now>] <marker> <cap-seconds> <poll-seconds> <file>...}"
|
|
30
36
|
CAP="${2:?}"
|
|
31
37
|
POLL="${3:?}"
|
|
32
38
|
shift 3
|
|
33
39
|
[ "$#" -gt 0 ] || { echo "watch-artifacts: no files given" >&2; exit 2; }
|
|
34
40
|
|
|
41
|
+
# OBS-848: ownership is a pid-file fact, never an argv/path pattern. Two tiers intentionally watch
|
|
42
|
+
# the same artifacts, so the journal/file argument cannot identify which watcher a seat may retire.
|
|
43
|
+
PID_DIR="${TKR_STATE_DIR:-.tickmarkr}/overseer/pids"
|
|
44
|
+
PID_SEAT=$(printf '%s' "${TKR_ARMING_SEAT:-unattributed}" | sed 's/[^A-Za-z0-9_-]/_/g')
|
|
45
|
+
PID_FILE="$PID_DIR/${PID_SEAT}-watch-artifacts-$$.pid"
|
|
46
|
+
mkdir -p "$PID_DIR" && (umask 077; printf '%s\n' "$$" > "$PID_FILE") || {
|
|
47
|
+
echo "watch-artifacts: cannot record pid under $PID_DIR" >&2; exit 73;
|
|
48
|
+
}
|
|
49
|
+
clear_pid() { rm -f "$PID_FILE"; }
|
|
50
|
+
trap clear_pid EXIT
|
|
51
|
+
|
|
35
52
|
# Cap BELOW the host's background-job kill so every arm ends by PRINTING something. A job killed at the
|
|
36
53
|
# limit carries no wake reason and is indistinguishable from a real wake until you read the output —
|
|
37
54
|
# OBS-325, re-earned by a third watcher that had not been capped because the fix was applied only to the
|
|
@@ -74,6 +91,22 @@ END=$((SECONDS + CAP))
|
|
|
74
91
|
# RUNTIME, where the arithmetic then errors, `done_file` returns 1 forever, and the watcher never wakes.
|
|
75
92
|
# Caught by the drill below, not by the syntax check. A syntax check is not a positive control.
|
|
76
93
|
PREV=()
|
|
94
|
+
BASELINE=()
|
|
95
|
+
i=0
|
|
96
|
+
for f in "$@"; do
|
|
97
|
+
if [ -n "$CHANGED_FROM" ]; then
|
|
98
|
+
# A SHA is an explicit single-artifact baseline. Historical callers recorded mtimes; retain that
|
|
99
|
+
# spelling as the arming signal but capture content now, because equal-length rewrites and touches
|
|
100
|
+
# prove that mtime is not the property this watcher claims to read.
|
|
101
|
+
if printf '%s' "$CHANGED_FROM" | grep -qE '^[0-9a-fA-F]{40}$' && [ "$#" -eq 1 ]; then
|
|
102
|
+
BASELINE[$i]=$(printf '%s' "$CHANGED_FROM" | tr 'A-F' 'a-f')
|
|
103
|
+
else
|
|
104
|
+
BASELINE[$i]=$(shasum -a 1 "$f" 2>/dev/null | cut -d' ' -f1)
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
i=$((i + 1))
|
|
108
|
+
done
|
|
109
|
+
|
|
77
110
|
done_file() { # $1 = index into "$@", $2 = path
|
|
78
111
|
local i="$1" f="$2" sig
|
|
79
112
|
[ -s "$f" ] || return 1
|
|
@@ -85,6 +118,7 @@ done_file() { # $1 = index into "$@", $2 = path
|
|
|
85
118
|
# catch. Hashing 48KB per poll costs nothing.
|
|
86
119
|
sig=$(shasum -a 1 "$f" 2>/dev/null | cut -d' ' -f1)
|
|
87
120
|
[ -n "$sig" ] || return 1
|
|
121
|
+
if [ -n "$CHANGED_FROM" ] && [ "${BASELINE[$i]:-}" = "$sig" ]; then return 1; fi
|
|
88
122
|
if [ "${PREV[$i]:-}" = "$sig" ]; then return 0; fi
|
|
89
123
|
PREV[$i]="$sig" # marked but still moving — hold it one more poll
|
|
90
124
|
return 1
|
|
@@ -21,8 +21,16 @@ LOAD_CEIL="${2:-24}"
|
|
|
21
21
|
POLL="${3:-45}"
|
|
22
22
|
CAP="${4:-14400}"
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
:
|
|
24
|
+
# OBS-848: the arming seat owns this pid, even when a partner watches the same journal. Never derive
|
|
25
|
+
# retirement ownership from the script or journal argv: both are deliberately shared across tiers.
|
|
26
|
+
PID_DIR="${TKR_STATE_DIR:-.tickmarkr}/overseer/pids"
|
|
27
|
+
PID_SEAT=$(printf '%s' "${TKR_ARMING_SEAT:-unattributed}" | sed 's/[^A-Za-z0-9_-]/_/g')
|
|
28
|
+
PID_FILE="$PID_DIR/${PID_SEAT}-watch-contamination-$$.pid"
|
|
29
|
+
mkdir -p "$PID_DIR" && (umask 077; printf '%s\n' "$$" > "$PID_FILE") || {
|
|
30
|
+
echo "watch-contamination: cannot record pid under $PID_DIR" >&2; exit 73;
|
|
31
|
+
}
|
|
32
|
+
clear_pid() { rm -f "$PID_FILE"; }
|
|
33
|
+
trap clear_pid EXIT
|
|
26
34
|
|
|
27
35
|
# Fingerprints of a harness collapse rather than a real regression. Deliberately narrow: a genuine
|
|
28
36
|
# assertion failure must NOT match, or the watcher launders real defects into "infra".
|
|
@@ -35,11 +43,8 @@ load1() { uptime | sed 's/.*load averages*: *//' | awk '{print $1}' | tr -d ',';
|
|
|
35
43
|
vitest_n() { ps -axo comm,command 2>/dev/null | grep -Ec 'node_modules/(\.bin/)?[v]itest|[v]itest/dist/|\([v]itest [0-9]+\)'; }
|
|
36
44
|
|
|
37
45
|
# Seed the seen-set so we wake on what happens NEXT, not on history already ruled on.
|
|
38
|
-
if [ -f "$J" ]; then
|
|
39
|
-
|
|
40
|
-
else
|
|
41
|
-
echo 0 > "$STATE"
|
|
42
|
-
fi
|
|
46
|
+
if [ -f "$J" ]; then seen=$(grep -c '' "$J" 2>/dev/null); else seen=0; fi
|
|
47
|
+
[ -n "$seen" ] || seen=0
|
|
43
48
|
|
|
44
49
|
elapsed=0
|
|
45
50
|
while [ "$elapsed" -lt "$CAP" ]; do
|
|
@@ -54,18 +59,34 @@ while [ "$elapsed" -lt "$CAP" ]; do
|
|
|
54
59
|
Li=$(printf %s "$L" | sed "s/[^0-9].*//"); [ -z "$Li" ] && Li=0
|
|
55
60
|
|
|
56
61
|
if [ -f "$J" ]; then
|
|
57
|
-
seen=$(cat "$STATE" 2>/dev/null); [ -z "$seen" ] && seen=0
|
|
58
62
|
now=$(grep -c '' "$J" 2>/dev/null); [ -z "$now" ] && now=0
|
|
59
63
|
if [ "$now" -gt "$seen" ]; then
|
|
60
64
|
newrows=$(tail -n "$((now - seen))" "$J" 2>/dev/null)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
seen="$now"
|
|
66
|
+
# A fingerprint in arbitrary JSON details is prose, not an occurrence. Parse the event shape,
|
|
67
|
+
# admit only runner gates, and stop before the baseline classifier's secondary echo list. Review
|
|
68
|
+
# findings and acceptance prose are deliberately outside this instrument's claim.
|
|
69
|
+
hit=$(printf '%s\n' "$newrows" | python3 -c '
|
|
70
|
+
import json, re, sys
|
|
71
|
+
infra = re.compile(sys.argv[1], re.I)
|
|
72
|
+
for raw in sys.stdin:
|
|
73
|
+
try: row = json.loads(raw)
|
|
74
|
+
except Exception: continue
|
|
75
|
+
data = row.get("data") or {}
|
|
76
|
+
gate = data.get("gate")
|
|
77
|
+
if row.get("event") != "gate-result" or data.get("pass") is not False or gate not in {"test", "build", "lint"}: continue
|
|
78
|
+
details = str(data.get("details") or "")
|
|
79
|
+
runner = re.split(r"(?im)^new failure fingerprints vs baseline \(secondary\):[ \t]*$", details, maxsplit=1)[0]
|
|
80
|
+
match = infra.search(runner)
|
|
81
|
+
if match:
|
|
82
|
+
end = runner.find("\n", match.end())
|
|
83
|
+
line = runner[runner.rfind("\n", 0, match.start()) + 1:end if end >= 0 else len(runner)]
|
|
84
|
+
print("%s\t%s\t%s" % (row.get("taskId", ""), gate, line))
|
|
85
|
+
break
|
|
86
|
+
' "$INFRA_RE")
|
|
66
87
|
if [ -n "$hit" ]; then
|
|
67
|
-
task=$(printf '%s' "$hit" |
|
|
68
|
-
gate=$(printf '%s' "$hit" |
|
|
88
|
+
task=$(printf '%s' "$hit" | cut -f1)
|
|
89
|
+
gate=$(printf '%s' "$hit" | cut -f2)
|
|
69
90
|
echo "CONTAMINATED_VERDICT task=$task gate=$gate load=$L vitest=$V"
|
|
70
91
|
echo " an infra fingerprint appeared in a FAILED gate â this red is not evidence about the diff"
|
|
71
92
|
echo " ruling owed: is the attempt chargeable? (OBS-426: infra failures are not)"
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
# usage: watch-context.sh <role-slug> <agent|pane> <warn-pct> <act-pct> [handoff-file] [poll-s] [cap-s]
|
|
34
34
|
# <role-slug> is ANY seat role — orchestrator, overseer, surgeon, consult — and names the tier
|
|
35
35
|
# `<role>-context`. It is deliberately NOT a closed set: see OBS-730 at the guard below.
|
|
36
|
-
# TKR_AUTO_CLEAR=1 at act-pct WITH a fresh handoff,
|
|
36
|
+
# TKR_AUTO_CLEAR=1 at act-pct WITH a fresh handoff, auto-clear orchestrator/overseer; other roles wake only.
|
|
37
37
|
# TKR_REBRIEF=<path> the file the re-briefed seat is told to read (defaults to the handoff).
|
|
38
38
|
# TKR_HANDOFF_MAX_AGE_S how fresh "fresh" is (default 900).
|
|
39
39
|
# TKR_CLEAR_SETTLE_S seconds to let a cleared seat settle before the re-brief (default 6).
|
|
@@ -63,6 +63,15 @@ case "$ROLE" in
|
|
|
63
63
|
esac
|
|
64
64
|
TIER="${ROLE}-context"
|
|
65
65
|
|
|
66
|
+
# A shared target does not identify its watcher owner. The arming seat records this exact pid under
|
|
67
|
+
# the repository state dir; retirement reads that file and never matches a script/target name pattern.
|
|
68
|
+
PID_DIR="${TKR_STATE_DIR:-.tickmarkr}/overseer/pids"
|
|
69
|
+
PID_SEAT=$(printf '%s' "${TKR_ARMING_SEAT:-unattributed}" | sed 's/[^A-Za-z0-9_-]/_/g')
|
|
70
|
+
PID_FILE="$PID_DIR/${PID_SEAT}-watch-context-$$.pid"
|
|
71
|
+
mkdir -p "$PID_DIR" && (umask 077; printf '%s\n' "$$" > "$PID_FILE") || {
|
|
72
|
+
echo "watch-context: cannot record pid under $PID_DIR" >&2; exit 73;
|
|
73
|
+
}
|
|
74
|
+
clear_pid() { rm -f "$PID_FILE"; }
|
|
66
75
|
|
|
67
76
|
# The supervision beat interval (SUPERVISION_BEAT_MS = 10s). The loop ticks at the beat cadence or the
|
|
68
77
|
# caller's poll, whichever is SHORTER. Every successful screen read beats; its result is either a
|
|
@@ -100,7 +109,8 @@ beat() {
|
|
|
100
109
|
stand_down() { tickmarkr beat "$TIER" --stand-down --seat "$SEAT" >/dev/null 2>&1; return 0; }
|
|
101
110
|
# EVERY terminal exit — act, unsafe-act, cap — leaves through here, so none of them can forget to
|
|
102
111
|
# record the hand-off. A killed watcher never runs it, which is the one case that must read STALE.
|
|
103
|
-
|
|
112
|
+
cleanup() { stand_down; clear_pid; }
|
|
113
|
+
trap cleanup EXIT
|
|
104
114
|
|
|
105
115
|
# ── WHERE THE NUMBER COMES FROM, and this is the whole 2.1.7-series lesson ────────────────────────────
|
|
106
116
|
# The shipped version read a TERMINAL RENDERING and nothing else. Every context-measurement failure of
|
|
@@ -189,15 +199,31 @@ print(int(round(fill * 100.0 / window)))
|
|
|
189
199
|
' "$CTX_JSONL" "$CTX_WINDOW" 2>/dev/null
|
|
190
200
|
}
|
|
191
201
|
|
|
192
|
-
# The banner path stays as the FALLBACK,
|
|
193
|
-
#
|
|
202
|
+
# The banner path stays as the FALLBACK, and it now selects by POSITION (OBS-865). A seat's statusline
|
|
203
|
+
# renders BELOW the input box's horizontal rule, so the only rows that can carry THIS seat's fill are the
|
|
204
|
+
# rows after the LAST rule line in the window. A tip, a transcript row, or a decoy quoting another seat's
|
|
205
|
+
# percentage sits ABOVE that rule and can no longer be chosen. The previous selector took the last
|
|
206
|
+
# vendor-word line ANYWHERE in the window: on a Fable seat that was `Tip: Ask Claude ...` (UNREADABLE,
|
|
207
|
+
# duty silently dead), and a quoted `opus 12%` would have been read as this seat's fill. The model
|
|
208
|
+
# vocabulary survives only as a SANITY CHECK on the already-chosen row, never as the selector (OBS-780).
|
|
194
209
|
banner_pct() {
|
|
195
210
|
local screen banner pct
|
|
196
211
|
screen=$(herdr agent read "$TARGET" --source visible --lines 8 2>/dev/null) || return 1
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
212
|
+
# The rule glyph is passed IN, never written as an escape: this host's awk (BWK 20200816) does not
|
|
213
|
+
# honour \xNN, and an unsupported escape matches nothing — the selector would find no rule line and
|
|
214
|
+
# report UNREADABLE on every real pane while staying green against any fixture that skipped awk.
|
|
215
|
+
banner=$(printf '%s\n' "$screen" | awk -v rule='─' '
|
|
216
|
+
{ line[NR] = $0
|
|
217
|
+
bare = $0; gsub(/[[:space:]]/, "", bare)
|
|
218
|
+
stripped = bare; gsub(rule, "", stripped)
|
|
219
|
+
if (bare != "" && stripped == "") last = NR }
|
|
220
|
+
END { if (last) for (i = last + 1; i <= NR; i++) if (line[i] ~ /[0-9]+%/) print line[i] }
|
|
221
|
+
' | tail -1)
|
|
222
|
+
# No rule line in the window, or no percentage below it: say so out loud rather than guess.
|
|
200
223
|
[ -n "$banner" ] || { printf 'UNREADABLE\n'; return 0; }
|
|
224
|
+
printf '%s\n' "$banner" |
|
|
225
|
+
grep -Eqi '(^|[^[:alnum:]])(claude|opus|sonnet|haiku|fable|gpt|gemini|glm|kimi|grok|composer|openai|zai)[[:alnum:]_./-]*([[:space:]]|$)' ||
|
|
226
|
+
{ printf 'UNREADABLE\n'; return 0; }
|
|
201
227
|
pct=$(printf '%s\n' "$banner" | grep -oE '[0-9]+%' | tail -1 | tr -d '%')
|
|
202
228
|
[ -n "$pct" ] && printf '%s\n' "$pct" || printf 'UNREADABLE\n'
|
|
203
229
|
}
|
|
@@ -221,10 +247,10 @@ handoff_fresh() {
|
|
|
221
247
|
act_on() {
|
|
222
248
|
local P="$1"
|
|
223
249
|
if handoff_fresh; then
|
|
224
|
-
if [ "${TKR_AUTO_CLEAR:-0}" = "1" ]; then
|
|
250
|
+
if [ "${TKR_AUTO_CLEAR:-0}" = "1" ] && { [ "$ROLE" = "orchestrator" ] || [ "$ROLE" = "overseer" ]; }; then
|
|
225
251
|
herdr agent prompt "$TARGET" "/clear" >/dev/null 2>&1
|
|
226
252
|
sleep "$SETTLE"
|
|
227
|
-
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
|
|
253
|
+
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'). The same-process clear kept every background task alive: kill only this seat's recorded watcher pids first, verify the process table twice, then re-arm. Do not reconstruct from memory — everything you need is on disk." >/dev/null 2>&1
|
|
228
254
|
echo "CONTEXT_CLEARED $TARGET at ${P}% — handoff fresh, re-briefed from ${REBRIEF}"
|
|
229
255
|
exit 0
|
|
230
256
|
fi
|
|
@@ -21,6 +21,17 @@ POLL="${2:-30}"
|
|
|
21
21
|
CAP="${3:-14400}"
|
|
22
22
|
CONFIRM="${4:-2}" # consecutive polls before waking — a draft mid-typing is not a stall
|
|
23
23
|
|
|
24
|
+
# The path/target is shared evidence, not ownership. Record the arming seat and this exact process so
|
|
25
|
+
# stand-down can retire only what that seat armed, without a pgrep pattern that catches its partner.
|
|
26
|
+
PID_DIR="${TKR_STATE_DIR:-.tickmarkr}/overseer/pids"
|
|
27
|
+
PID_SEAT=$(printf '%s' "${TKR_ARMING_SEAT:-unattributed}" | sed 's/[^A-Za-z0-9_-]/_/g')
|
|
28
|
+
PID_FILE="$PID_DIR/${PID_SEAT}-watch-pending-input-$$.pid"
|
|
29
|
+
mkdir -p "$PID_DIR" && (umask 077; printf '%s\n' "$$" > "$PID_FILE") || {
|
|
30
|
+
echo "watch-pending-input: cannot record pid under $PID_DIR" >&2; exit 73;
|
|
31
|
+
}
|
|
32
|
+
clear_pid() { rm -f "$PID_FILE"; }
|
|
33
|
+
trap clear_pid EXIT
|
|
34
|
+
|
|
24
35
|
status_of() {
|
|
25
36
|
herdr agent get "$TARGET" 2>/dev/null \
|
|
26
37
|
| sed -n 's/.*"agent_status":"\([a-z_]*\)".*/\1/p' | head -1
|
|
@@ -35,8 +46,11 @@ status_of() {
|
|
|
35
46
|
# no wake, no streak. Negative scope: sampled on one theme/version; a theme rendering typed input dim
|
|
36
47
|
# would blind this check TOWARD silence, so a wake is still authoritative but its absence is not proof.
|
|
37
48
|
is_ghost() {
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
(
|
|
50
|
+
export LC_ALL=C
|
|
51
|
+
herdr agent read "$TARGET" --source visible --lines 14 --format ansi 2>/dev/null \
|
|
52
|
+
| grep -F -- "❯" | grep -F -- "$1" | head -1 | grep -q "$(printf '\033')\[2m"
|
|
53
|
+
)
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
pending_text() {
|
|
@@ -175,8 +189,11 @@ while [ "$elapsed" -lt "$CAP" ]; do
|
|
|
175
189
|
# typed draft. Ghost text renders dim/grey; typed text renders default. Emit the styled
|
|
176
190
|
# bytes so the reader can discriminate before treating this as a draft.
|
|
177
191
|
echo " styling evidence (dim/grey SGR around the text = autosuggest ghost, NOT a draft):"
|
|
178
|
-
|
|
179
|
-
|
|
192
|
+
(
|
|
193
|
+
export LC_ALL=C
|
|
194
|
+
herdr agent read "$TARGET" --source visible --lines 14 --format ansi 2>/dev/null \
|
|
195
|
+
| grep -F -- "$T" | head -2 | cat -v | sed 's/^/ /'
|
|
196
|
+
)
|
|
180
197
|
echo " the seat is idle and holding live work in its prompt — supersede it, do not re-send:"
|
|
181
198
|
echo " herdr agent prompt $TARGET \" <-- disregard everything before this arrow (stale draft). ACTUAL: …\""
|
|
182
199
|
# Name the ACTUAL reason we fell through. Saying "budget exhausted" when the cause was the
|
|
@@ -208,10 +225,17 @@ done
|
|
|
208
225
|
echo "WATCH_CAP_REACHED $TARGET status=$(status_of) — no sustained pending input in ${CAP}s"
|
|
209
226
|
FINAL=$(pending_text)
|
|
210
227
|
if [ -n "$FINAL" ]; then
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
228
|
+
if is_ghost "$FINAL"; then
|
|
229
|
+
echo " final read: autosuggest ghost ignored (dim SGR), input box has no typed draft"
|
|
230
|
+
else
|
|
231
|
+
echo " final read (UNSUSTAINED — present at cap, not confirmed across polls): $FINAL"
|
|
232
|
+
echo " styling evidence (dim/grey SGR = autosuggest ghost, NOT a draft):"
|
|
233
|
+
(
|
|
234
|
+
export LC_ALL=C
|
|
235
|
+
herdr agent read "$TARGET" --source visible --lines 14 --format ansi 2>/dev/null \
|
|
236
|
+
| grep -F -- "$FINAL" | head -2 | cat -v | sed 's/^/ /'
|
|
237
|
+
)
|
|
238
|
+
fi
|
|
215
239
|
else
|
|
216
240
|
echo " final read: input box empty"
|
|
217
241
|
fi
|