From ae83a8120d4a9beeb5dfd94315cfdc3b6a410551 Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Fri, 29 May 2026 03:10:12 +0100 Subject: [PATCH] watchdog: signal handoffs off claim()/review() commit prefixes (robust) + codify the convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- cc-ci-plan/launch.sh | 62 +++++++++++++++------------------ cc-ci-plan/plan.md | 16 ++++++--- cc-ci-plan/prompts/adversary.md | 1 + cc-ci-plan/prompts/builder.md | 1 + 4 files changed, 41 insertions(+), 39 deletions(-) diff --git a/cc-ci-plan/launch.sh b/cc-ci-plan/launch.sh index 02ac9b5..9157d07 100755 --- a/cc-ci-plan/launch.sh +++ b/cc-ci-plan/launch.sh @@ -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: … 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 diff --git a/cc-ci-plan/plan.md b/cc-ci-plan/plan.md index 169e0fb..e92d118 100644 --- a/cc-ci-plan/plan.md +++ b/cc-ci-plan/plan.md @@ -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: — 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` (`: PASS @` 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: — 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` (`: PASS @` 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 D1–D10. The Adversary can write `## VETO ` to `REVIEW.md` at any time, which forbids DONE until cleared. diff --git a/cc-ci-plan/prompts/adversary.md b/cc-ci-plan/prompts/adversary.md index 127b109..15577b2 100644 --- a/cc-ci-plan/prompts/adversary.md +++ b/cc-ci-plan/prompts/adversary.md @@ -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. diff --git a/cc-ci-plan/prompts/builder.md b/cc-ci-plan/prompts/builder.md index 68d4ed7..8a6ff68 100644 --- a/cc-ci-plan/prompts/builder.md +++ b/cc-ci-plan/prompts/builder.md @@ -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: CLAIMED, awaiting Adversary" in STATUS.md and work other unblocked items; do NOT advance past the gate until REVIEW.md shows its PASS.