diff --git a/cc-ci-plan/launch.sh b/cc-ci-plan/launch.sh index 7797302..02ac9b5 100755 --- a/cc-ci-plan/launch.sh +++ b/cc-ci-plan/launch.sh @@ -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: … 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