watchdog: fix handoff lag — detect on pushed origin/main + precise formal-claim match
The handoff pings fired on the writer's LOCAL working-tree write (before push), so the receiver pulled a stale origin/main, saw "no formal gate", and a clarifying inbox round-trip ensued (several minutes + wasted turns per handoff). And the gate-id parser read "WC1" as "C1" and could fire on prose mentions. Fix (1): handoff_check now `git fetch`es and reads origin/main (what the receiver will pull), via _wd_fetch_origin + _wd_show_pushed, for STATUS / REVIEW / both INBOXes — a ping only fires once the claim/verdict is actually pushed, so the receiver's pull always sees it. Eliminates the stale-pull "premature" dance. Fix (2): gate-claim detection matches ONLY a formal line (Gate: <id> … CLAIMED, awaiting Adversary) and edge-triggers on a genuinely-new such line compared whole — no firing on historical "CLAIMED detail" lines or prose; gate-id is a best-effort label only. Loops' clones have a credential helper (reads .testenv) so the watchdog's fetch works non-interactively. Verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -217,60 +217,70 @@ heal_orchestrator() {
|
||||
"$ORCH_LAUNCHER" start >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# Edge-triggered handoff signalling for the CURRENT phase. Reads the loops' local clones.
|
||||
# Ping the Adversary only when a gate id NEWLY appears on a "CLAIMED … awaiting" line (never on
|
||||
# the baseline / restart / a passed-but-kept line). Ping the Builder when the phase REVIEW changes.
|
||||
# Detect handoffs against the PUSHED origin/main — i.e. exactly what the RECEIVER will pull — NOT the
|
||||
# writer's local working tree. (Reading the working tree fired on a claim/verdict the writer hadn't
|
||||
# pushed yet; the receiver then pulled a stale remote, saw "no formal gate", and a clarifying
|
||||
# inbox round-trip ensued. Mirroring origin/main eliminates that race.) origin/main is the shared
|
||||
# branch, so all four files are read from one clone's origin/main after a single best-effort fetch.
|
||||
_wd_fetch_origin() { git -C "$1" fetch -q origin 2>/dev/null || true; }
|
||||
_wd_show_pushed() { git -C "$1" show "origin/main:machine-docs/$2" 2>/dev/null || git -C "$1" show "origin/main:$2" 2>/dev/null || true; }
|
||||
|
||||
_wd_awaiting=""; _wd_baselined=""; _wd_last_review=""
|
||||
_wd_adv_inbox_seen=""; _wd_builder_inbox_seen=""
|
||||
handoff_reset() { _wd_awaiting=""; _wd_baselined=""; _wd_last_review=""; _wd_adv_inbox_seen=""; _wd_builder_inbox_seen=""; } # call on phase transition
|
||||
handoff_check() {
|
||||
local idx sf rf cur now added
|
||||
local idx status review adv_inbox builder_inbox now added ids cur h
|
||||
idx="$(cur_idx)"
|
||||
sf="$(resolve_state "$BUILDER_DIR" "$(phase_status "$idx")")"; rf="$(resolve_state "$ADV_DIR" "$(phase_review "$idx")")"
|
||||
if [[ -f "$sf" ]]; then
|
||||
now="$(grep -iE 'CLAIMED.*awaiting' "$sf" 2>/dev/null | grep -oiE 'M[0-9]+(\.[0-9]+)?|[A-Z][0-9]+' | tr '[:lower:]' '[:upper:]' | sort -u || true)"
|
||||
if [[ -n "$_wd_baselined" ]]; then
|
||||
added="$(comm -13 <(printf '%s\n' "$_wd_awaiting" | sort -u) <(printf '%s\n' "$now" | sort -u) | grep -vE '^$' || true)"
|
||||
if [[ -n "$added" ]]; then
|
||||
log "handoff: gate(s) newly awaiting verification: $(echo $added) -> pinging Adversary"
|
||||
ping_session "$ADV_SESSION" "watchdog ping: the Builder CLAIMED gate(s) [$(echo $added)] in $(phase_status "$idx") and is awaiting your verification. Pull and verify now."
|
||||
fi
|
||||
_wd_fetch_origin "$BUILDER_DIR"
|
||||
status="$(_wd_show_pushed "$BUILDER_DIR" "$(phase_status "$idx")")"
|
||||
review="$(_wd_show_pushed "$BUILDER_DIR" "$(phase_review "$idx")")"
|
||||
adv_inbox="$(_wd_show_pushed "$BUILDER_DIR" "ADVERSARY-INBOX.md")"
|
||||
builder_inbox="$(_wd_show_pushed "$BUILDER_DIR" "BUILDER-INBOX.md")"
|
||||
|
||||
# Gate claims: match ONLY a FORMAL claim line ("Gate: <id> … CLAIMED, awaiting Adversary"), and
|
||||
# edge-trigger on a genuinely-NEW such line (compared whole, so prose mentions / historical
|
||||
# "CLAIMED detail" lines / id mis-parsing can't fire it). The id is just a best-effort label.
|
||||
now="$(printf '%s\n' "$status" | grep -iE 'gate[: ].*claimed.*awaiting[[:space:]]+adversary' | sort -u || true)"
|
||||
if [[ -n "$_wd_baselined" ]]; then
|
||||
added="$(comm -13 <(printf '%s\n' "$_wd_awaiting") <(printf '%s\n' "$now") | grep -vE '^$' || true)"
|
||||
if [[ -n "$added" ]]; then
|
||||
ids="$(printf '%s\n' "$added" | grep -oiE 'gate:?[[:space:]]*[A-Z]{1,3}[0-9]+(\.[0-9]+)*' | sed -E 's/^[Gg]ate:?[[:space:]]*//' | tr '\n' ' ' | tr '[:lower:]' '[:upper:]' || true)"
|
||||
[[ -z "$ids" ]] && ids="a gate"
|
||||
log "handoff: NEW pushed gate claim [$ids] -> pinging Adversary"
|
||||
ping_session "$ADV_SESSION" "watchdog ping: the Builder pushed a gate claim [$ids] in $(phase_status "$idx") — awaiting your verification. Pull and verify now."
|
||||
fi
|
||||
_wd_awaiting="$now"; _wd_baselined=1
|
||||
fi
|
||||
if [[ -f "$rf" ]]; then
|
||||
cur="$(md5sum "$rf" 2>/dev/null | awk '{print $1}' || true)"
|
||||
_wd_awaiting="$now"; _wd_baselined=1
|
||||
|
||||
# REVIEW change (pushed) -> ping Builder.
|
||||
if [[ -n "$review" ]]; then
|
||||
cur="$(printf '%s' "$review" | md5sum | awk '{print $1}')"
|
||||
if [[ -n "$cur" && "$cur" != "$_wd_last_review" ]]; then
|
||||
[[ -n "$_wd_last_review" ]] && {
|
||||
log "handoff: $(phase_review "$idx") changed -> pinging Builder"
|
||||
ping_session "$BUILDER_SESSION" "watchdog ping: the Adversary updated $(phase_review "$idx") (a verdict or finding). Pull and act now — if it PASSes your gate, proceed; if it's a finding, address it."
|
||||
log "handoff: $(phase_review "$idx") changed (pushed) -> pinging Builder"
|
||||
ping_session "$BUILDER_SESSION" "watchdog ping: the Adversary pushed an update to $(phase_review "$idx") (a verdict or finding). Pull and act now — if it PASSes your gate, proceed; if it's a finding, address it."
|
||||
}
|
||||
_wd_last_review="$cur"
|
||||
fi
|
||||
fi
|
||||
|
||||
# INBOX side-channel (§6.1). The sender writes the receiver's inbox in their OWN clone, so we
|
||||
# detect from the sender side. Edge-trigger on content hash so a fresh message (sender re-wrote
|
||||
# before receiver consumed) re-pings. Receiver deletes after processing => hash empty => next
|
||||
# write re-triggers.
|
||||
local adv_inbox builder_inbox h
|
||||
adv_inbox="$(resolve_state "$BUILDER_DIR" "ADVERSARY-INBOX.md")"
|
||||
if [[ -f "$adv_inbox" ]]; then
|
||||
h="$(md5sum "$adv_inbox" 2>/dev/null | awk '{print $1}' || true)"
|
||||
if [[ -n "$h" && "$h" != "$_wd_adv_inbox_seen" ]]; then
|
||||
log "handoff: ADVERSARY-INBOX.md new/changed -> pinging Adversary"
|
||||
ping_session "$ADV_SESSION" "watchdog ping: the Builder wrote machine-docs/ADVERSARY-INBOX.md — pull, read the message, act on it, then delete the file (commit + push) to mark it consumed."
|
||||
# INBOX side-channel (§6.1), detected on the pushed state. Receiver deletes after consuming =>
|
||||
# absent on origin/main => re-arm so the next write re-pings.
|
||||
if [[ -n "$adv_inbox" ]]; then
|
||||
h="$(printf '%s' "$adv_inbox" | md5sum | awk '{print $1}')"
|
||||
if [[ "$h" != "$_wd_adv_inbox_seen" ]]; then
|
||||
log "handoff: ADVERSARY-INBOX.md new/changed (pushed) -> pinging Adversary"
|
||||
ping_session "$ADV_SESSION" "watchdog ping: the Builder pushed machine-docs/ADVERSARY-INBOX.md — pull, read it, act, then delete the file (commit + push) to mark it consumed."
|
||||
_wd_adv_inbox_seen="$h"
|
||||
fi
|
||||
else
|
||||
_wd_adv_inbox_seen="" # consumed; ready for the next write
|
||||
_wd_adv_inbox_seen=""
|
||||
fi
|
||||
builder_inbox="$(resolve_state "$ADV_DIR" "BUILDER-INBOX.md")"
|
||||
if [[ -f "$builder_inbox" ]]; then
|
||||
h="$(md5sum "$builder_inbox" 2>/dev/null | awk '{print $1}' || true)"
|
||||
if [[ -n "$h" && "$h" != "$_wd_builder_inbox_seen" ]]; then
|
||||
log "handoff: BUILDER-INBOX.md new/changed -> pinging Builder"
|
||||
ping_session "$BUILDER_SESSION" "watchdog ping: the Adversary wrote machine-docs/BUILDER-INBOX.md — pull, read the message, act on it, then delete the file (commit + push) to mark it consumed."
|
||||
if [[ -n "$builder_inbox" ]]; then
|
||||
h="$(printf '%s' "$builder_inbox" | md5sum | awk '{print $1}')"
|
||||
if [[ "$h" != "$_wd_builder_inbox_seen" ]]; then
|
||||
log "handoff: BUILDER-INBOX.md new/changed (pushed) -> pinging Builder"
|
||||
ping_session "$BUILDER_SESSION" "watchdog ping: the Adversary pushed machine-docs/BUILDER-INBOX.md — pull, read it, act, then delete the file (commit + push) to mark it consumed."
|
||||
_wd_builder_inbox_seen="$h"
|
||||
fi
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user