F4 stamped the embedded pawl bundle with a deterministic content hash; this surfaces it in
`ao pawl doctor` so the landed!=installed class (an installed ao silently running OLD pawl scripts —
it bit this very session) is diagnosable.
THE DESIGN IS THE LESSON. Two cross-family codex refutes killed the obvious implementation:
round 1 — I hashed any repo containing scripts/pawl-*.sh and reported STALE. A stranger repo with
same-named files would be falsely failed.
round 2 — I gated that on "identity markers" (the verdict schema + pawls contract). Codex refuted
again, correctly: FILE PRESENCE IS NOT IDENTITY. Every marker is attacker-controlled and
spoofable, so no marker list can ever make it safe.
The real fix is not a better marker list — it is a TRUST BOUNDARY. A staleness FAILURE must never be
asserted against a repo the binary does not trust. So:
- trusted dogfood path (untrustedRoot == "", i.e. the ao binary lives inside the checkout — the ONE
trust decision the whole pawl surface is built on): the LIVE scripts run in place, so there is
nothing to compare and nothing to warn about;
- untrusted/installed path: surface the stamp INFORMATIONALLY (so the operator can diagnose
landed!=installed themselves and rebuild), and assert NO verdict about a repo we cannot trust.
This is the honest feature, not a weaker one: the diagnostic signal is preserved while doctor cannot
be weaponized into failing on an untrusted tree.
Tests: TestPawlBundleStaleness — dogfood surfaces the stamp and never says STALE; an untrusted repo
is never reported STALE *whatever it contains*, including a repo that SPOOFS every identity marker a
naive check might have used.
Claude-Session: https://claude.ai/code/session_01RDWPYn4ZS2SyBkcBvAM18u
`ao pawl up --dry-run` planned-step hardcoded "atm spawn ...", contradicting the actual seam and
`ao pawl doctor`'s own output: the swarm binary resolves ntm-first (PAWL_SWARM_BIN -> ntm -> atm),
and doctor reports "ntm -> ...". The planned step now says "swarm spawn session <s> ... (ntm-first
seam; see `ao pawl doctor` swarm-bin)" — neutral and truthful regardless of which binary wins.
Split from F10 (age-pawl-intent-zhndq.11) to keep that a fast shell land; this is the Go half.
Claude-Session: https://claude.ai/code/session_01RDWPYn4ZS2SyBkcBvAM18u
landed!=installed is a recurring class (an installed ao carries whatever pawl scripts were
embedded at build time, silently running old logic — it bit this very session). F4 adds the
MECHANISM: `make sync-hooks` writes a DETERMINISTIC content-hash stamp (sha256 of the three
embedded review scripts) to cli/embedded/pawl/BUNDLE_STAMP — it changes ONLY when they change, in
the SAME sync-hooks run (no always-dirty). `ao pawl doctor` gains a bundle row: the installed-binary
path shows the embedded stamp, the in-checkout dogfood path says live scripts run in place. The row
deliberately does NOT read $ROOT/scripts (on the embedded/stranger path $ROOT is the UNTRUSTED
caller repo; the full race suite's security guard TestEmbeddedPawlBundleCarriesVerdictWriterSibling
correctly caught a first attempt that did). A go:embed drift test (TestEmbeddedBundleStampMatchesScripts)
catches an embedded-script edit that forgot to regenerate the stamp.
Scoped: the trust-aware live-vs-installed STALE comparison (Go doctor layer) + the review-time
staleness warning split to .19 — they need the Go trust split to safely read a checkout.
Claude-Session: https://claude.ai/code/session_01RDWPYn4ZS2SyBkcBvAM18u
F2 makes the verdict->edge emit fail-closed at WRITE time, but history + crash windows can
still leave a verdict FILE with no bound EDGE — a silent desync where 'ao done' (which reads
the LEDGER, not the file) refuses the close, discovered one-refused-close at a time. This
command scans .agents/pawl-verdicts/*.json and, for every CONFIRMED/REBOUND verdict, reports
whether a matching verdict->commit edge is bound, reusing the EXACT recognizer the push gate
and ao done use (hasConfirmedVerdictEdge — exact-token disposition, sha-bound, never substring).
--emit re-emits missing edges (idempotent — only the UNBOUND ones detected this pass, so a
second run is a no-op); --force allows a dirty ledger (default: refuse, age-7krl — never sweep
another lane's rows); --json for machine output. Exit 0 clean / 1 unbound-or-emit-failed / 2
usage. Report-only default is safe (no mutation).
Operational data (this repo, per the bead): 251 verdicts scanned, 1 unbound — and it is a
harmless orphan from this session's own messy F1 land (a dead sha 6cbcc0f5 reset away during
the concurrent-session recovery), NOT a systemic desync. Validates F2 (prevents new) + F3 (finds
existing). Tests: TestScanReconcileVerdicts (bound/unbound/REFUTED-skipped/malformed) + empty-dir.
Regenerated CLI surface (COMMANDS.md, cli-surface.{json,md}, eval matrix + smoke) for the new command.
Claude-Session: https://claude.ai/code/session_01RDWPYn4ZS2SyBkcBvAM18u