watchdog: signal handoffs off claim()/review() commit prefixes (robust) + codify the convention

Replaces the brittle markdown prose-match ("Gate: … CLAIMED, awaiting Adversary") with detection of
the loops' conventional commit prefixes on origin/main: a new `claim(...)` commit pings the
Adversary; a new `review(...)` commit pings the Builder. Edge-triggered on the origin/main SHA
(append-only — no force-push), no file parsing, can't mis-route. The loops already use these
prefixes consistently; codified as a load-bearing contract in plan.md §6.1 + both prompts so it
stays reliable. INBOX detection unchanged (pushed-state, file-routed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 03:10:12 +01:00
parent e0e60bc2bc
commit ae83a8120d
4 changed files with 41 additions and 39 deletions

View File

@ -225,45 +225,39 @@ heal_orchestrator() {
_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
_wd_last_sha=""; _wd_adv_inbox_seen=""; _wd_builder_inbox_seen=""
handoff_reset() { _wd_last_sha=""; _wd_adv_inbox_seen=""; _wd_builder_inbox_seen=""; } # call on phase transition
# Signal handoffs off the loops' CONVENTIONAL COMMIT PREFIXES on origin/main — NOT by parsing
# free-form markdown prose (brittle). The loops consistently prefix every gate claim `claim(...)`
# and every verdict/finding `review(...)`. So: a new `claim(` commit pushed => ping the Adversary;
# a new `review(` commit => ping the Builder. Edge-triggered on the origin/main SHA (append-only —
# the loops never force-push), so it can't double-fire or mis-route. INBOX files are detected
# separately (which file changed routes the ping). All reads are of the PUSHED state (what the
# receiver pulls).
handoff_check() {
local idx status review adv_inbox builder_inbox now added ids cur h
idx="$(cur_idx)"
local head subjects adv_inbox builder_inbox h
_wd_fetch_origin "$BUILDER_DIR"
status="$(_wd_show_pushed "$BUILDER_DIR" "$(phase_status "$idx")")"
review="$(_wd_show_pushed "$BUILDER_DIR" "$(phase_review "$idx")")"
head="$(git -C "$BUILDER_DIR" rev-parse origin/main 2>/dev/null || true)"
if [[ -n "$head" ]]; then
if [[ -z "$_wd_last_sha" ]]; then
_wd_last_sha="$head" # baseline silently on first observation / restart
elif [[ "$head" != "$_wd_last_sha" ]]; then
subjects="$(git -C "$BUILDER_DIR" log --format='%s' "${_wd_last_sha}..origin/main" 2>/dev/null || true)"
if printf '%s\n' "$subjects" | grep -qiE '^claim'; then
log "handoff: new claim(...) commit on origin/main -> pinging Adversary"
ping_session "$ADV_SESSION" "watchdog ping: the Builder pushed a gate CLAIM (claim(...) commit). Pull and verify the claimed gate now."
fi
if printf '%s\n' "$subjects" | grep -qiE '^review'; then
log "handoff: new review(...) commit on origin/main -> pinging Builder"
ping_session "$BUILDER_SESSION" "watchdog ping: the Adversary pushed a verdict/finding (review(...) commit). Pull REVIEW and act — proceed if it PASSes your gate, address it if it's a finding."
fi
_wd_last_sha="$head"
fi
fi
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
fi
_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 (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), 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

View File

@ -637,11 +637,17 @@ its own pacing. To make concurrent writes conflict-free:
- **Git discipline (both loops, every write):** `git pull --rebase` before editing, make the
smallest change, commit, `git push`. On a rebase conflict, it will be inside the *other* agent's
file/section only if a rule was broken — re-pull and keep to your own files. Never `--force`.
- **Gate handshake via STATUS.md.** When the Builder believes a milestone gate is met, it sets in
`STATUS.md`: `Gate: <Mn> — CLAIMED, awaiting Adversary` and stops advancing past it. The
Adversary, on its next wake, sees the claim, runs the acceptance check cold, and writes the
verdict to `REVIEW.md` (`<Mn>: PASS @<ts>` with evidence, or `FAIL` + an `[adversary]` item).
The Builder only proceeds past the gate after seeing `PASS` in `REVIEW.md`.
- **Gate handshake via STATUS.md + commit-prefix signalling.** When the Builder believes a milestone
gate is met, it sets in `STATUS.md`: `Gate: <Mn> — CLAIMED, awaiting Adversary`, **commits it with a
`claim(...)` prefix**, and stops advancing past it. The Adversary runs the acceptance check cold and
writes the verdict to `REVIEW.md` (`<Mn>: PASS @<ts>` with evidence, or `FAIL` + an `[adversary]`
item), **committed with a `review(...)` prefix**. The Builder only proceeds past the gate after
seeing `PASS` in `REVIEW.md`.
- **The watchdog signals the handoff off these commit prefixes** (not by parsing prose): a new
`claim(...)` commit on origin/main pings the Adversary; a new `review(...)` commit pings the
Builder. So the prefixes are **load-bearing** — a gate claim MUST be a `claim(...)` commit and a
verdict MUST be a `review(...)` commit, or the counterpart isn't promptly woken (it falls back to
its slower self-poll). STATUS/REVIEW remain the durable source of truth; the prefix is the signal.
- **DONE handshake.** Builder may write `## DONE` to `STATUS.md` **only** when `REVIEW.md` shows a
PASS dated within 24h for every D1D10. The Adversary can write `## VETO <reason>` to
`REVIEW.md` at any time, which forbids DONE until cleared.

View File

@ -7,6 +7,7 @@ Credentials/access: §1.5 is the authoritative map. Provided creds are in /srv/c
You run as a SEPARATE process and coordinate ONLY through the git repo per §6.1:
- Keep your OWN clone at /srv/cc-ci/cc-ci-adv. If the repo doesn't exist yet, wait and retry on your next wake — the Builder creates it during §1 Bootstrap.
- git pull --rebase before every edit; commit; push; never --force.
- COMMIT-PREFIX CONVENTION (the watchdog depends on it for handoff signalling). Prefix every commit that records a **verdict or finding** with `review(...)` (e.g. `review(2w): ... PASS`/`... FAIL`). The watchdog watches origin/main and pings the Builder the moment a `review(...)` commit lands — that IS the handoff signal, so your verdict is only reliably picked up if its commit is prefixed `review(`. (The Builder's gate claims are `claim(...)`.) `review(` is load-bearing.
- Write ONLY your files: REVIEW.md and the "## Adversary findings" section of BACKLOG.md. Everything else (code, STATUS.md, JOURNAL.md, "## Build backlog") is read-only to you.
- INBOX side-channel (§6.1). For non-gate messages to the Builder (heads-up, "I'm running a break-it probe on X," request for clarification, etc.), write/append `machine-docs/BUILDER-INBOX.md` in your clone and push — the watchdog edge-pings the Builder on appearance. To receive a message from the Builder, look for `machine-docs/ADVERSARY-INBOX.md`; process it, then DELETE the file (commit + push) — deletion is the "consumed" signal. Do NOT use the inbox for formal verdicts — REVIEW.md still owns those.
- ISOLATION DISCIPLINE (anti-anchoring — critical). The Builder is REQUIRED to give you in STATUS.md the essential verification info you need: WHAT is claimed (gate id, DoD items), HOW to verify (the exact command/check), the EXPECTED outcome (hashes, fingerprints, status codes, file contents), WHERE the inputs live (commit shas, paths). **Read STATUS for that — you need all of it to verify.** What you must IGNORE — in STATUS, and NEVER read in JOURNAL.md before your verdict — is the Builder's REASONING / RATIONALISATIONS: "I think this passes because…", design narrative, dead-ends, justifications. Reading those anchors you. Form your verdict from (a) the phase plan = SSOT for what is being verified, (b) the code / git history, (c) the verification info the Builder passed you in STATUS, and (d) your own COLD acceptance run that re-executes the check against the expected outcomes. Only AFTER you have written your verdict may you consult JOURNAL.md (e.g. to contextualise a finding) — note in REVIEW.md that you did. Do not trust the Builder's narrative; trust observable behaviour, the plan, and your own re-run.

View File

@ -6,6 +6,7 @@ Start a self-paced loop now: invoke `/loop` with no interval so you re-wake your
You run as a SEPARATE process from the Adversary loop and coordinate ONLY through the git repo per §6.1:
- git pull --rebase before every edit; make the smallest change; commit; git push. Never --force.
- COMMIT-PREFIX CONVENTION (the watchdog depends on it for handoff signalling). Prefix every commit with its conventional type. CRITICALLY: prefix a commit that **claims a gate** with `claim(...)` (e.g. `claim(2w): ...`). The watchdog watches origin/main and pings the Adversary the moment a `claim(...)` commit lands — that IS the handoff signal, so a gate claim is only reliably picked up if its commit is prefixed `claim(`. (Verdicts from the Adversary are `review(...)`.) Keep using the other types too (`feat/fix/status/journal/decisions/chore/inbox(...)`), but `claim(` is load-bearing.
- Write ONLY your files: source/config, STATUS.md, JOURNAL.md, DECISIONS.md, and the "## Build backlog" section of BACKLOG.md. Treat REVIEW.md and "## Adversary findings" as read-only — the Adversary owns them.
- ARTIFACT-LAYER ISOLATION (facts in STATUS, reasoning in JOURNAL). STATUS.md **MUST** give the Adversary everything it needs to verify your claim — withholding verification context defeats the verification: **WHAT** is claimed (gate id, DoD items), **HOW** to verify it (the exact command/check the Adversary can re-run from its own clone), the **EXPECTED** outcome (build hashes, file contents, status codes, leaf fingerprints, command exit), and **WHERE** the inputs live (commit shas, paths). If something is essential for the Adversary to verify, put it in STATUS. STATUS **MUST NOT** include rationalisations / "I think this passes because…" / design narrative / dead-ends explored / design choices and their justification — those go in JOURNAL.md, which the Adversary is instructed not to read before forming its verdict (anti-anchoring), so keeping reasoning out of STATUS preserves that. The line: **WHAT + HOW + EXPECTED + WHERE = STATUS; WHY = JOURNAL.** DECISIONS.md is for SETTLED design decisions (joint authority), not in-the-moment rationale.
- At each milestone gate, set "Gate: <Mn> CLAIMED, awaiting Adversary" in STATUS.md and work other unblocked items; do NOT advance past the gate until REVIEW.md shows its PASS.