Fold 5 substrate skills into their survivors (ao skills retire --into), after
re-litigating each keep-rationale:
- vibing-with-ntm -> ntm (operator layer over the same NTM tools)
- agy-headless-evidence -> agy-native (consumes agy-native; evidence is a
listed capability; mirrors agy-mcp/rules/sidecar folds)
- codex-approval -> codex-exec (codex-runtime driving-adapter; mirrors the
codex-* consolidation)
- dual-pane-atm -> using-atm (a specialization of the ATM substrate)
- orchestrate -> using-atm (route/preflight/verify instrument; the ao
orchestrate CLI survives, only the skill doc folds)
using-atm is the surviving out-of-session-substrate consolidator. Manual cleanups
ao skills retire does NOT reach: git rm .agy-plugin/skills/vibing-with-ntm +
drop it from validate-agy-plugin.sh core_skills; drop the dual-pane-atm/orchestrate
lines from scripts/.scenario-linkage-allow; drop vibing-with-ntm from
scripts/lint/codex-cross-runtime-skills.txt; repoint 26 broken cross-ref links to
survivors + fix frozen-twin generated_hash. scenario-linkage / skill-redirects
(47 resolve) / doc-skill-refs / manifests all green.
NOTE: validate-agy-plugin.sh has a PRE-EXISTING 9-bundle drift at HEAD (agent-mail,
beads-*, cass, cc-hooks, dcg, ntm, rch — stale hand-committed .agy-plugin bundle,
no sync tooling, not a routed gate); untouched here, flagged for a follow-up.
Demote compile, forge, flywheel, curate, operationalize, inject (the SKILL, not
the ao inject command) to the experimental tier (reusing the tier .21 created),
and DROP their Codex twins — the corpus/flywheel skill-surface cut.
Twin-drop mechanism: a new `excluded` catalog treatment. codex-sync.sh reads the
excluded set and skips generation (closing the .18 "missing twin regenerated"
escape hatch); validate-codex-override-coverage.sh accepts `excluded` and asserts
the twin+override dirs are truly absent (an excluded record can't mask an orphan).
Removed the two hard skills-codex/inject/* requirements from
validate-codex-rpi-contract.sh + check-hookless-cold-start.sh; pruned
forge/compile/flywheel/inject from images/codex/manifest.json CORE (else
verify.sh / ao codex health break). git rm'd the 6 twin dirs (53 files).
/forge, /compile, /flywheel still resolve (skills kept), with honest
"(experimental — kept, not primary)" notes in operating-loop move 7 / AGENTS.md /
SKILL-ROUTER. regen-check, coverage, parity-drift, cold-start, images verify.sh,
manifests all green; registry all 6 = experimental.
Highest-leverage maintenance move: editing an AMBIENT (non-spine) skill no
longer restains its Codex twin. Regeneration is now gated on the source skill's
`spine: true` frontmatter (the same signal check-spine-integrity.sh pins).
- codex-sync.sh: a non-spine skill whose twin is already complete + registered
is skipped in both --check and regen (frozen in place). A MISSING twin (new
skill) is still generated so validate-codex-override-coverage.sh stays satisfied;
`--force --only <slug>` still deliberately regenerates a frozen twin.
- regen-codex-hashes.sh: for non-spine skills the source_hash is NOT recomputed
from a changed source (frozen/authoritative); generated_hash is still recomputed
so a real twin-content change is still caught.
- validate-codex-lifecycle-guards.sh: partitioned entry/closeout/tracker
assertions into spine (hard require) vs frozen-ambient (assert-if-present), so
the guard survives the freeze and any later twin removal by .19.
NO twin deleted (count unchanged); coverage/parity-drift/lifecycle/cold-start/
manifest all green. Proven: editing account-rotation leaves its twin
byte-identical while editing spine skill `validate` still restains its twin.
Stamp `spine: true` on the 15 spine skills (membrane 9 + bookkeeper 6) and add
a BEHAVIORAL durability probe so the factory can't silently re-bury the spine.
- `spine: true` frontmatter on the 15; `spine` added to the tier schema's
properties. CRITICAL: added to schemas/skill-frontmatter.v1.schema.json (which
has additionalProperties:false and is the python-jsonschema path the blocking
skill.schema gate loads) — v2 + valid_keys alone would be inert.
- scripts/check-spine-integrity.sh: fail-closed probe pinning the 15 canonical
slugs; asserts each EXISTS, is FLAGGED (exactly 15, flagged-set==pinned-set
both directions), VALIDATES (its validate.sh where present + the schema), and
LEADS the router (inside new <!-- BEGIN/END:spine --> markers before the flow
sections). Negative-tested: removing spine:true from any spine skill FAILS it.
- GOALS.md: a `spine-integrity` fitness row so `ao goals measure` runs it.
Per the doc-instruction-is-inert lesson, this is a real probe of repo state, not
a content check. Full gate green; regen clean; ao goals measure passes it.
Heavy legacy RPI chains with no measured uplift. Add a new `experimental` tier
and demote the three. The tier value is registered at ALL FOUR enum sites — the
authoritative schemas/skill-frontmatter.v1.schema.json (enforced by the blocking
skill.manifests gate), validate-skill-schema.sh, sku_catalog.py, and
generate-registry.sh — plus a Tier Values row in SKILL-TIERS.md. Frontmatter
metadata.tier and the SKILL-TIERS row cell flipped in lockstep for all three.
acfs is demoted, NOT cut: its cut is external (~/acfs) and a deferred operator
one-way-door decision (noted in the disposition ledger). Codex twins refreshed
by regen (not dropped — .18 owns twin cuts). registry drift, manifests schema,
sku-catalog, sync-counts all green; go build + internal tests pass.
Escape fix for the ADR-0012 command archival (age-focus-membrane-bookkeeper-m1wg.13/.14):
archiving a command behind //go:build flywheel|legacy removed it from the
default `ao` binary, but skills legitimately document those commands (e.g.
skills/curate/SKILL.md + skills/flywheel/SKILL.md reference `ao harvest`).
validate-skill-cli-snippets.sh built a spine-only binary, so those snippets
false-failed the blocking skill.cli-snippets gate — a LATENT main breakage that
only surfaced when a later skills/** change routed the gate.
Build the validation binary with -tags "flywheel legacy" so snippets validate
against the full (spine + archived) command surface. A skill may reference any
command; the archive is a packaging concern, not a validation boundary.
The REFUTED/HOLD-path recording used `${vc:-REFUTED}`, so a timed-out pane (empty verdict, NO
vote) was written into refuters[] as a REFUTED it never emitted — fabricating refutations in
the membrane's OWN evidence, which feeds the escape corpus. metrics.jsonl already logged these
honestly as "timeout", so the two records disagreed (the .2/.10 verdict JSONs are the exhibit).
Extracted the pure _refuted_refuters helper: record ONLY panes that really voted (CONFIRMED/
REFUTED), mirroring the CONFIRMED path; a timed-out pane is OMITTED. The verdict schema requires
>=1 refuter, so the all-timeout case (REFUTED:insufficient — no reviewer voted) falls back to the
timed-out panes with a *-timeout context, keeping the membrane catch logged AND honest that it was
a timeout, not a refute. Fail-safe: REFUTED/HOLD path only, disposition unchanged (pawl_decide
already decided) — can't affect any CONFIRM/authorization.
5 new deterministic pawl-refuter-recording.bats cases; existing pawl suites green; go build OK.
NOTE: this is part 4 of the 4-part age-pawl-good-bar bead; the other parts are re-scoped (see the
bead close) — part 1's two-phase design was overtaken by the --converge calibrated pass and needs a
fresh decision, part 3 targets the evolve subsystem m1wg is demoting.
A stranger (or Bo on a fresh host) running the verb without its hard deps got a bare exit 1
that reads like a real REFUTE. Two fixes at the pawl-review.sh preflight:
- codex-missing now exits 2 (the script's documented "usage/precondition" code) instead of 1,
with an actionable message that explicitly says "This is NOT a review result" and to install
the codex CLI. exit 1 (hard error) / 3 (REFUTED) stay distinct.
- jq is ALSO hard-required (pawl-verdict.sh parses with it) but was only caught opaquely
mid-run — now checked upfront with the same exit-2 + actionable message. (bead CODEX WARN.)
Smoke-verified both guards fire with exit 2 + the right message (PATH with codex-absent, and
PATH with jq-absent-but-coreutils-present). embedded copy re-synced.
NOTE: the bead's other half — a stranger prebuilt-binary install one-liner — is DROPPED as
superseded: the a9iv stranger-onboarding epic was cut, and Bo's own install path is the plugin
installer. This lands the legible-dependency half, which is useful independent of the epic.
Two of the three warm-panel robustness fixes the age-yifo transport diagnosis pinned:
- DEGRADE-TO-READY-SUBSET (cmd_up): the readiness gate was all-or-nothing — a tri-spawn
with one family down (e.g. AGY) DIED instead of serving on the panes that came up. Now,
when the full enabled set never all becomes ready, cmd_up degrades to _ready_subset and
re-resolves the tier (reuse the existing tier door) rather than dying; only a fully-empty
ready subset is fatal. Fail-safe: _ready_subset runs each family's real fail-closed check
(no fabricated readiness); a degrade to one family honestly records TIER=fresh.
- ROUTE BUSY-GUARD (cmd_down / reap): down/reap killed the SHARED session with no busy-check,
so a concurrent lane or the idle reaper could tear down a MID-ROUTE pawl and lose its
verdict. cmd_route now writes a route.lock (RETURN-trap-cleaned); cmd_down refuses while the
lock is FRESH (return 3) unless --force. A STALE lock (crashed route) auto-cleans in
_route_in_progress, so it can never wedge teardown forever.
9 new deterministic pawl-yvrp-robustness.bats cases; existing pawl suites green; go build OK;
cold cross-family pawl to follow. RESIDUAL (Fix 2, deferred): stable private tmux socket +
detached server surviving harness reap — needs live multi-process verification and, per the
diagnosis, a launchd/systemd user service for true robustness (out of scope for this in-script pass).
cc_send/agy_send only checked "delivered":1 (keystrokes typed into the box), NOT that the
model started a turn — parity gap with cod_send's `atm codex wait-goal-engaged` gate. A
compacting or never-engaging opus/agy pane counted as delivered yet emitted no verdict in
the route window, mapping to <timeout> and fail-closing the route.
- Add _wait_engaged() over the existing `atm wait --until=generating --pane --type` primitive.
- cc_send/agy_send now loop (3x, like cod_send): send -> require delivered AND engaged; on
no-engagement respawn to a FRESH context + retry. The respawn is also the opus compaction-tax
fix — a pane that auto-compacts on send won't reach `generating` in the window, so it is
respawned to a fresh small context that engages promptly instead of burning the route window.
- PAWL_ENGAGE_WAIT (default 45s) bounds each per-send engagement poll.
Fail-safe: worst case a pane never engages -> no vote -> fail-closed REFUTED:insufficient
(no false-CONFIRM path). 6 new deterministic pawl-send-engagement.bats cases (mock atm+respawn);
existing pawl suites green. Live 5-route smoke deferred (no warm substrate); pairs with age-55qz.10.
The warm tri-family route loop fail-closed on nearly every route because pawl.sh:705
broke the wait loop the instant ANY pane REFUTED. codex is the only engagement-verified
+ fast family (~43s), so its verdict guillotined the still-pending opus/agy panes — they
mapped to <timeout> and the route recorded REFUTED:insufficient even when opus was
out-voting a codex false-positive (confirmed by live probe).
- Remove the break-on-any-REFUTED at :705. Disposition is UNCHANGED — the single-refute-
blocks rule lives in pawl_decide (refuted>=1 -> REFUTED), which runs AFTER the loop, so a
codex REFUTE still yields REFUTED; only WHEN we stop waiting changes. Fail-safe: no path to
a false-CONFIRM (the unchanged pawl_decide/tri-model tests prove the invariant).
- Add PAWL_ENGAGE_DEADLINE (default 240s < ROUTE_TIMEOUT): an absolute per-pane wall-clock
give-up that catches a compacting opus pane (re-renders every tick, so the cksum-stall
heuristic never fires) which would otherwise burn the full ROUTE_TIMEOUT.
Tests: 3 new deterministic _engage_over_deadline cases in pawl-prompt-stall.bats; embedded
copy re-synced; go build green. Live 5-route smoke deferred (no warm substrate); fix is
fail-safe by construction.
Audit finding: the published catalog (registry.json / docs/SKILLS.md / site)
presents product skills only — zero operator/personal-identity skills are in
the repo (athena, wealth-mentor, bo-* brand/voice live in ~/.claude/skills,
never committed). Substrate skills (ntm/atm/pawl/swarm) are legitimate product
skills, not operator-personal.
Make "gated out" durable instead of incidental: add scripts/check-no-operator-
skills.sh (fail-closed denylist guard), wire it as blocking gate
skill.no-operator-leakage in seed.go, with bats coverage and an audit doc.
(pre-commit legacy bd-shim hook has a REPO_ROOT path bug on cli/ commits;
build+vet+test+regen verified manually; authoritative pre-push gate runs full.)
dogfood-setup.sh --revert recursively replaces ~/.claude/skills with the
latest backup. A stale backup (few skills) silently wipes a healthy set —
the best-fit cause of the 26-vs-125 skills-drift incident. Refuse when the
backup has <50% of the live skill count; override with --force-revert.
Adds tests/scripts/dogfood-revert-guard.bats (3 cases: refuse / force-bypass / legit).
age-skills-coexist-harden-04h2.2
The agentops checkout runs hot; a parallel session can stage its files between
your git-add and git-commit, and a bare commit sweeps them in (happened live
2026-06-29 — GOALS/PRODUCT/.gitignore rode into a skill commit despite the
worktree footgun being documented). Advisory knowledge didn't prevent it; this
is the mechanical surface. Blocks (fail-closed, override AGENTOPS_COMMIT_SCOPE_OK=1)
a canonical-checkout commit whose staged set MIXES shared cross-lane files
(GOALS/PRODUCT/README/.gitignore/CLAUDE/AGENTS/registry.json) with other work.
Skips in linked worktrees (the safe path it steers toward). Tested: blocks the
contamination case, passes skill-only/docs-only/feature-only.
Stop defining AgentOps by rivals (the encoded sprawl trap): remove GOALS.md Directive 6 'Maintain competitive awareness' + its competitive-freshness gate, the PRODUCT.md 'positions against Compound Engineer' lineage, and the orphaned check/test scripts (unwired from ci-local-release). Keep the vs-* comparison docs as static analysis and the membrane strategy reasoning. gitignore graphify-out/ (one-shot audit artifact, stale on a hot repo).
When ao pawl review is launched detached (run_in_background/nohup &), its process
group can be reaped mid-codex-exec — killing the review before the existing retry +
fail-closed logic runs, leaving only the 'running…' line (a silent flatline that
looks like a codex stall). External reaping can't be prevented from inside, so emit
a self-explaining diagnostic BEFORE the vulnerable exec (survives the kill), pointing
at the foreground re-run. Fires only when non-interactive. Embedded pawl bundle synced.
The merge-to-main pawl was too strict — it spawned an infinite nitpick loop. The default refuter
posture was maximal-adversarial ("actively REFUTE anything plausible-but-wrong"), which has a
documented infinite false-alarm tail: on a multi-round landing it refuted ~9x and ~2/3 of findings
were cosmetic / pre-existing / theoretical / hallucinated, so an all-must-confirm fail-closed gate
never reached CONFIRM.
Define "good" and bind the gate to it. A finding now REFUTEs only if it clears ALL THREE filters:
INTRODUCED (by this diff, not pre-existing/adjacent) x REAL (survives deterministic ground truth; a
green go vet/test/bash -n/parity check that contradicts it kills it; no claims about unread files) x
BLOCKING (fail-open / data-loss / wrong-object / claimed-contract-false / ships-non-working).
Everything else is the ACCEPTED TAIL (noted, not blocked). "Would a senior engineer BLOCK this
merge?", not "can I find any imperfection?".
Anti-Goodhart preserved (council C): the calibrated bar is the MANDATORY default posture — no
author-selectable knob to route around. --converge stays the narrower real-safety convergence-tail
bar. Verified against the empirical refutation catalog: the new bar still REFUTEs every real defect
(round-trip data-loss, complexity-enum drop, duplicate registry key, the bash-n-only non-working
fix) and drops every nitpick (validation_lanes pre-existing, null-vs-absent pedantry, the TestLevels
+ source/twin hallucinations). Full definition: docs/contracts/pawls.md "What good means".
Deferred (follow-up bead): two-phase wiring (maximal as advisory surfacer + calibrated binding), the
auto-strip-on-green verifier, the nitpick-churn circuit breaker, the verdict-recording bug (timed-out
panes logged REFUTED).
(a) SAFETY: remove the matchable 'VERDICT: CONFIRMED' example from the refuter
prompt — an echoed/stalled codex run now fails CLOSED (no parseable verdict)
instead of mis-parsing the template as a false CONFIRMED.
(b) Raise MAX_INLINE_BYTES 24576->65536 so the common diff range stays inline
(self-contained, 'no tools') instead of read-files mode (which orders codex to
browse the filesystem + strips added lines — the wander cause).
Known limitation: codex still echoes LARGE (60KB+) prompts regardless of mode
(codex-CLI context pressure); those need the warm panel or a smaller unit.
S2 — decouple the guaranteed cross-family verb from the AgentOps source checkout
so a fresh external user can run the catch on their OWN non-AgentOps repo.
Before: ao pawl review died on a stranger's repo with 'locating AgentOps repo root'
before ever reaching codex (S0). Now: a GENUINE in-checkout run (the running ao binary
lives inside the resolved checkout — aoBinaryInside) uses the live scripts (EDGE 1);
everyone else (installed ao, or forged markers) extracts the embedded pawl bundle
(go:embed cli/embedded/pawl/, sibling scripts/+schemas/ for EDGE 2) and runs it against
the user's own git repo via AGENTOPS_REPO_ROOT + PAWL_NO_SERVICE. cwd stays the user's
repo so the read-only codex refuter can READ the changed files for large diffs.
SECURITY — the cross-family pawl caught a sequence of real defects on this commit:
- TRUSTED PATH: the stranger run's PATH drops every entry the repo could control — '',
'.', any relative entry, AND any absolute dir INSIDE the repo (e.g. $PWD/bin on PATH).
So a bare codex/git/jq/timeout can never resolve a repo-planted binary. Go-side bash
resolves on the same trusted PATH; repo-root discovery is PURE Go (.git walk, no binary)
so no planted git runs during discovery.
- resolve_ao no longer searches $REPO_ROOT/cli/bin/ao; live path gated on aoBinaryInside
(forged markers can't trigger it); pawl-verdict.sh emits + resolve_ao use ONLY the
absolute pinned AO_BIN when untrusted. In-checkout behavior unchanged (flags unset).
- INTEGRITY: the refuter runs codex with project_doc_max_bytes=0 so a repo-planted
AGENTS.md cannot hijack the reviewer into a fail-open VERDICT: CONFIRMED; the cold env
neutralizes BASH_ENV/ENV so non-interactive bash cannot source a repo file.
- ROOT PRE-RUN: ao's root PersistentPreRunE runs git in the cwd (RepairSharedCore
WorktreeConfig) before any command; that git is now resolved via worktreeconfig.
trustedGitBin — absolute, excluding '.'/relative/repo-internal PATH dirs — so a
repo-planted git can't run at startup on the stranger path (CLI-wide).
- GIT DIFF HELPERS: rendering an untrusted-repo diff (git show/diff) is hardened with
--no-ext-diff --no-textconv -c core.fsmonitor= (+ GIT_EXTERNAL_DIFF= in the cold env)
so a repo's diff.external / .gitattributes textconv driver can't run code pre-review.
Packaging/resolution + resolution-safety only — no review LOGIC/gate/daemon change.
Tests: embed-parity gate (+ validate-embedded-sync.sh extended); extract-layout (schema
sibling, EDGE 2); cold-env seam contract; trustedPATH unit test; gitToplevel-no-exec test;
an L2 e2e running the verb in a throwaway non-AgentOps repo with codex stubbed (no
resolveAgentsRepoRoot error, commit-bound verdict written into the USER's repo, schema-
validated check passes); RCE regression tests — planted cli/bin/ao + cli/ao + ./ao + ./codex
(with '.' first in PATH) + BASH_ENV that must never run, and a forged-markers repo that must
route to embedded. Verified on the real binary against a fresh stranger repo: CONFIRMED +
verdict written + merge authorized, exit 0.
Two defects the dogfood land caught against this very branch:
1. ship.sh's .agents/ write-surface contract gate FAILED: scripts/land-*.sh
write to .agents/land-queue/ but that surface was never documented in
docs/contracts/agents-write-surfaces.md. Add the Surfaces row + allowlist
entry (rolling, scripts, land-serialization-state).
2. pawl-review (codex, cross-family) REFUTED: scripts/land-queue-test.sh only
required 'bats', while the e2e acceptance suite skips when jq/go are absent
— so the 'complete' runner could exit 0 without running the integrated
acceptance proof (a fail-open acceptance gate). Require jq+go up front so a
green suite always EXECUTED the final proof.
Both surfaced by dogfooding the land lane on itself. All 24 land-queue tests
still green.
Closes age-landq-self
Implements: agentops-2pl.12
Dogfood land: this very branch (the land-queue lane) lands itself onto main
through its own serialized single-writer lane — the ultimate acceptance proof.
Closes age-landq-self
The runtime gh shim treated gh api as a safe read only when the method/body
flags appeared in the SPACE form (--field x=y). The EQUALS form bypassed the
guard: gh api --field=x=y / --raw-field=x=y / --input=f.json were delegated to
the real gh and could POST to a workflow/repository_dispatch endpoint = invoke
Actions. A cross-vendor reviewer flagged this as the remaining hole.
Fix (api_is_read_only): new is_body_flag helper matches each body flag if the
token EQUALS the flag OR STARTS WITH 'flag=', covering --field/--raw-field/
--input (long, both forms) and -f/-F (short, attached). Method override already
handled both forms. Endpoint denial (/dispatches, /actions/, *_dispatch) is
unchanged and still blocks regardless of method form. gh api is ALLOWED only
when method is absent/GET/HEAD, there are no body flags in any form, and the
endpoint is not an Actions/dispatch endpoint.
Tests: added BLOCKED cases for --field=/--raw-field=/--input=/--method= equals
forms and an actions/.../dispatches endpoint; added ALLOWED case for
--method GET. All prior cases (gh run rerun blocked, unknown verb blocked,
read-only delegated) retained.
Suites green: assert-no-actions.bats 11, land-lane.bats 5, flaky-retry.bats 3.
validate.yml byte-identical to HEAD (no .10 regression).
The runtime gh shim + static scan were a BLOCKLIST: any Actions-invoking gh
verb not enumerated slipped through. A cross-vendor reviewer caught the hole —
`gh run rerun 123` re-INVOKES a workflow run (running GitHub Actions) yet was
logged 'allowed' and delegated to the real gh, because only `gh workflow run` /
`gh pr create|merge` were enumerated. Any un-enumerated Actions verb was a hole.
Flip the runtime shim from blocklist to DEFAULT-DENY (allowlist):
- guard-gh now ALLOWS only an explicit allowlist of clearly read-only/safe
subcommands (gh run view|list|watch, gh pr view|list|checks|diff, gh api GET
on non-Actions endpoints, gh auth status, gh --version, workflow view|list,
repo/release/issue view|list, ...) and BLOCKS everything else. An UNKNOWN or
future `gh run *` / `gh workflow *` verb is denied by default, so a new verb
cannot reopen this hole. is_allowed_gh is the single source of truth;
is_forbidden_gh is now its negation. gh api is gated by method (only
GET/HEAD) and endpoint shape (no /dispatches, /actions/, repository_dispatch,
request-body fields).
- Static scan retained as defense-in-depth; added the now-known Actions verbs
(gh run rerun|cancel, gh workflow enable|disable). The runtime default-deny is
the real backstop.
Tests (tests/land-queue/assert-no-actions.bats):
- runtime shim BLOCKS `gh run rerun 123` from the land path (the exact hole);
- runtime shim BLOCKS an UNKNOWN `gh run <madeup>` (default-deny posture);
- static scan rejects gh run rerun planted in a land script;
- direct guard-gh allow/block matrix: read-only verbs delegated, every
Actions-invoking/mutating path blocked + logged, real gh never invoked.
- existing load-bearing cases kept (gh pr create / gh workflow run blocked).
validate.yml untouched (pull_request/merge_group triggers stay). Bead .10
flaky-retry code in land-lane-run.sh untouched.
The land-path no-actions guard conflated two different things: "the land lane
never invokes GitHub Actions" (the real invariant) and "the repo has no PR CI"
(wrong). validate.yml legitimately carries pull_request:[main] + merge_group for
ordinary contributor PR CI by other contributors; the lane simply never opens a
PR. The validate.yml trigger-assertion therefore (a) failed on the real
validate.yml and (b) made assert_no_actions_lane_start die, breaking all of
tests/land-queue/land-lane.bats.
Fix: remove the validate.yml config-assertion entirely (extract_on_block +
assert_validate_yml + the --validate-yml arg + its run_check call). The guard now
keeps only the two correct, sufficient mechanisms:
- STATIC scan: the lane scripts (land-lane-run.sh, land-submit.sh) must contain
no Actions-invoking gh verbs (gh pr create|merge, gh workflow run,
gh api .../dispatches, merge_group/merge_queue enqueue);
- RUNTIME gh shim: a PATH-shimmed gh that hard-errors + logs on any
Actions-invoking subcommand during a landing (read-only gh api GETs pass).
validate.yml is left untouched (its pull_request/merge_group triggers stay).
Tests: bats tests/land-queue/assert-no-actions.bats GREEN (6/6, incl. the
load-bearing runtime-shim "gh pr create from inside the land path"); the
config-assertion test is replaced by a static-scan gh-pr-create test + a
regression guard that the live validate.yml's pull_request/merge_group triggers
are tolerated. land-lane.bats GREEN again (5/5 — lane no longer dies at start);
flaky-retry.bats (.10) still 3/3.
A cross-vendor reviewer caught a single-writer violation: the INT/TERM trap
called release_lane_lock but did NOT exit. In --watch mode, after TERM the
first lane released its lane lock yet its loop KEPT RUNNING, so a second lane
could acquire the SAME lane lock while the first process was still alive —
two concurrent writers to main, breaking the singleton invariant under signal
handling.
Fix: split the trap. EXIT still releases the lock exactly once (release_lane_lock
is idempotent). INT/TERM get a dedicated on_signal handler that releases the lock
THEN exits with the conventional signal code (130 INT / 143 TERM), disarming the
traps first so exit does not re-recurse. The watch loop can no longer survive the
signal; the lock is genuinely gone only AFTER the process exits, so the next lane
starts cleanly. --drain/--once paths are unaffected.
Test: new bats case 'watch lane exits on TERM, frees the lock, and only then
admits a successor' — starts a --watch lane in the background, proves a second
lane started WHILE it is alive is REFUSED, sends TERM, asserts the process
actually exits within a short bound (not looping), the lock is freed, and a
successor started AFTER exit acquires the lock fine. Verified to FAIL on the old
release-only trap (process never exits within bound) and PASS on the fix.
Add scripts/land-lane-run.sh — the LAND LANE: one serialized writer that owns
main. It pops the oldest file-queue request (land-queue-next.sh), rebases its
branch onto current origin/main, runs the gate ONCE, lands via the .7-patched
pawl-land.sh (post-rebase pawl stamp + push HEAD:main), and loops — so the gate
runs once per landing, not once per racing agent, and there is exactly one
writer to main.
Agile pivot: built on the FILE-QUEUE + a file-based mkdir singleton lock
(reusing push-serial.sh's advisory-lock pattern + AGENTOPS_PUSH_LOCK_TIMEOUT),
NOT Agent-Mail (degraded_read_only). ADR-0009-compliant: thin foreground loop,
no bespoke daemon.
Design:
- LOCK: mkdir-atomic singleton lane lock; a 2nd lane blocks then refuses (exit 1)
— single-writer invariant. Released on exit/INT/TERM (holder file removed
before rmdir so the dir is empty and the lock always frees).
- CLAIM: claim_next pops the oldest unclaimed request under a short request-lock,
flips its requests.jsonl claimed flag + records claims.jsonl BEFORE processing
(crash-safe: a re-run never re-pops a claimed/done/dead-lettered bead).
- LAND: per bead, fetch + checkout -B land-work origin/land-queue/<bead> +
rebase origin/main, then gate-once + push (default path drives
ship -> pawl-review -> pawl-land; injectable via LAND_LANE_GATE_CMD).
- DEAD-LETTER: any failure (rebase conflict, gate/land fail) records
dead-letter.jsonl and continues — one bad bead never stops the lane.
- HOST: foreground loop for the always-on Mac (NEVER bushido). Modes: --drain
(default, process then exit), --once, --watch [--poll N].
Consumes (does not modify) the .8 queue scripts and the .7-patched pawl-land.sh.
Test tests/land-queue/land-lane.bats (e2e, all green): queues 3 independent-file
beads via the REAL land-submit.sh, drains, and asserts (a) main has all 3 in
queue order each rebased onto the prior, (b) gate ran exactly once per landing,
(c) no force-push (pre-lane main stays an ancestor), (d) a 2nd concurrent lane
refuses (singleton lock held), (e) a conflicting request dead-letters while the
lane lands the rest; plus a crash-safe re-drain that does not re-land.
S3 of the smart-membrane PROVE-FIRST plan (epic age-zpj5) — the CONSUMPTION side of membrane memory on
the S1/S2 foundation:
- recallCatchesByDomain (cmd/ao/membrane.go): DetectCatches filtered by domain, optionally narrowed to
catches whose affected_paths OVERLAP the given paths (set intersection). The abundant memory (escapes
are structurally rare; catches are not).
- 'ao membrane recall --include-catches [--paths f1,f2]': RECONCILES the shipped escape-keyed recall —
escapes (existing, unchanged when the flag is off) PLUS catch classes; JSON emits {escapes, catches}.
- pawl-review.sh: recall_prior_catches injects a 'MEMBRANE MEMORY — PRIOR CATCHES IN THIS AREA' block into
the reviewer prompt (domain + touched files), so the reviewer verifies the change doesn't reintroduce a
prior catch. FAIL-SAFE: prints nothing when ao absent / no domain / no catches.
- ADVISORY INVARIANT (TestRecall_IsAdvisoryNeverAGate): a static source scan asserts the recall functions
are called ONLY from runMembraneRecall — recall is advisory memory, NEVER a gate/verdict input; the test
fails if call-site count is 0 (non-vacuous). Plus TestRecallCatchesByDomain (domain + path-overlap filter).
- Verified end-to-end: a recorded catch surfaces in recall with overlapping paths, is filtered out with
non-overlapping paths; shellcheck -S warning clean; cmd/ao 7795 pass; go vet clean; back-compat preserved.
regen-all: COMMANDS.md / cli-surface / registry / codex twins updated for the new flags.
S2 of the smart-membrane PROVE-FIRST plan (epic age-zpj5) — the structured catch EMIT layer on the
S1 foundation:
- 'ao membrane catch --bead --domain --reason [--paths] [--detector-pattern --globs --detector-kind]
[--mode] [--head] [--run]': the structured catch recorder (cmd/ao/membrane.go). Records a REFUTED
gate-verdict carrying domain+reason+affected_paths (+ optional detector) via the production Writer;
class_key computed at emit. The manual twin of the pawl-review REFUTED branch. Judgment-class catches
(no detector) are path-recallable; detector-bearing catches become compile candidates (Axis-2, S4).
- pawl-review.sh REFUTED branch (cold + routed) now emits a structured catch via 'ao membrane catch'
(emit_pawl_catch): domain from the changed-files' top dir, reason from the REFUTED verdict text, paths
from the reviewed files. FAIL-SAFE + NON-BLOCKING (a missing ao or any error never blocks the REFUTED
exit — the catch is observability, not a gate).
- Tests: TestMembraneCatch_RecordsPathRecallableJudgmentClass + _DetectorMakesCompileCandidate (fixtures
round-trip through the production Writer -> DetectCatches/CompileCandidates). Smoke-verified end-to-end.
- regen-all: COMMANDS.md / cli-surface / registry / codex twins / surface-matrix updated for the new cmd.
cmd/ao: 7793 pass; go vet clean; shellcheck -S warning clean; regen-check green.
(1) README.md /validate mockup cited .agents/council/<run-id>/verdict.md — a
recurring pmf-evidence gate FAIL and operator inside-baseball on the most-viewed
public surface. Reworded to 'Recorded as a proof artifact — no verdict, not done'
(keeps the membrane thesis, drops the gitignored path). pmf-evidence gate now green.
(2) pawl.sh classified codex's update MENU ('› 1. Update now (runs brew upgrade) /
2. Skip / 3. Skip until next version') as plain codex-update -> dismissed with
Enter, which SELECTS the default 'Update now' and stalls the warm pane (observed
2026-06-26: spurious REFUTED, no review content). New codex-update-menu type
(matches 'update now|skip until next version', checked BEFORE codex-update since
the menu also says 'update available') dismissed with 'Down Enter' (navigate to
Skip). 2 new pawl-prompt-stall bats cases incl. the anti-'Update now' regression.
Caveat: codex is currently up-to-date (0.142.2) so the menu can't be reproduced
live; skip-keys reasoned from codex's arrow-select TUI. Worst case == prior
behavior (route degrades to the proven cold codex-exec path).
The literal 'harness these metrics' ask. GitHub's traffic API is a 14-day
rolling window, top-10 only — every read evaporates. Without a periodic capture,
'what do people use?' is forever a thin 14-day guess.
- scripts/capture-repo-metrics.sh: append one dated JSON snapshot (stars/forks/
watchers/open-issues, 14d views+clones total/unique, top-10 paths + referrers)
to docs/metrics/traffic.jsonl. --dry-run previews. Fails CLOSED (no partial
record) if gh is missing/unauthenticated or any endpoint fetch fails.
- docs/metrics/README.md: the instrument doc + the one trap baked into every
record (clones_note): clones is NOT users (CI/mirrors/scrapers — ~43k clones
vs ~1k views in one window); rank interest by unique path views + fork velocity.
- docs/metrics/traffic.jsonl: seeded baseline (399 stars, 40 forks, 4 watchers;
#1 viewed content = operating-loop.js).
- tests/scripts/capture-repo-metrics.bats: hermetic gh-stub tests — record shape,
append, fail-closed on unauth + on endpoint failure.
Scheduling is the operator's (cron/launchd/substrate); AgentOps ships no daemon
(ADR-0009). The script is the artifact, the cadence is yours.
operating-loop.js is the single most-viewed content artifact on the public repo
(GitHub traffic: 37 views/18 uniq, 14d) AND it was stale: its no-intent fallback
prompt told agents to run `bd ready` — bd/Dolt is retired (single-host SPOF, no
offline lane). An agent following it runs a tool that no longer exists. ship-beads.js
already used br in code; only 3 historical comments named bd.
- operating-loop.js: bd ready -> BEADS_DIR="$(ao beads dir)" br ready; bd bead id
-> br bead id.
- ship-beads.js: 3 historical comments bd -> br.
- scripts/check-workflow-no-retired-tracker.sh + bats: deterministic gate that
fails if any .claude/workflows/*.js references a retired bd COMMAND token
(bd ready|update|close|show|create|dep|list|ping|context|init|sync|--json|-);
does NOT false-positive on 'bead'/'bdd-foundry'/'embed' or any br command.
- seed.go: workflow.no-retired-tracker, Fast|Full, blocking, runs on workflow edits.
Run-proof: a full e2e run spawns ~15 agents (impractical as a gate). The
deterministic proof for a JS workflow in a Go repo is node --check parse +
check-workflow-governance.sh registration + this no-retired-tracker gate.
A folded skill (state: merged-into in skill-dispositions.yaml) is a redirect.
If a later rename/prune deletes the fold TARGET, the redirect silently points
at a skill that no longer exists and inbound traffic dead-ends. The metric that
surfaced this: skills-codex/brainstorm/SKILL.md was a top-10 inbound path
(10 views/4 uniq, 14d) but 404s after brainstorm folded into discovery.
GitHub cannot redirect a deleted blob URL, and tombstone DIRECTORIES under
skills/ break check-registry-drift.sh (it counts skills/ subdirs as skills). So
the durable invariant the repo CAN guarantee is redirect-VALIDITY: every
merged-into disposition must resolve, following the merged-into chain, to a
live, non-folded skills/<name>/SKILL.md, with no cycles.
- scripts/check-skill-redirects.sh: deterministic gate (42 merged, 1 cut, 0
broken today); fails on a dead/cyclic fold target. A folded skill is never a
valid resolution terminal even if a stale SKILL.md lingers (pawl/codex catch:
that short-circuit let a cycle a->b->a fail open when one node stayed live).
- tests/scripts/check-skill-redirects.bats: 6 cases incl. that cycle regression.
- seed.go: contract.skill-redirects, Fast|Full, blocking, runs on ledger/skill
changes.
Stale folded-skill refs in LIVE operator docs are tracked separately in age-gnrs.
Four scoped follow-ups to the capability-adaptive pawl (age-4o33), all in the pawl scripts:
age-mwhj (large-diff review): inlining the full diff timed the review out then failed CLOSED on
big diffs (a 41KB packet timed an opus pane out; a 62KB cold codex was killed) — unusable on the
large refactors where review matters most. Above PAWL_MAX_INLINE_BYTES (~24KB) the packet switches
to read-files-not-inline (size note + git --stat + changed-file ABSOLUTE paths); the reviewer reads
the files. Pure build_review_body + bats.
age-bb5l (tier transparency): the adaptive route can be tier=fresh (single family) but pawl-review.sh
hardcoded 'opus+codex duel' — misreporting a lone fresh-context refuter as a multi-model duel (false
reassurance). pawl_tier_note(mode) + reading the verdict's real mode; de-hardcoded the log strings;
the pre-push gate now prints mode= + a fresh-context NOTE.
age-djfo (prompt-dismiss + early-give-up): a warm pane can be alive yet stuck on a CLI prompt (codex
update dialog, agy survey) — falsely-green health + full-ROUTE_TIMEOUT burn. detect_blocking_prompt /
prompt_dismiss_key / clear_known_prompts (wired into the route poll, readiness gate, agy_ready); health
flags a 'stuck:<type>' cod pane; per-pane cksum stall give-up (PAWL_STALL_GIVEUP, 150s) degrades fast.
Folds in the probe-auth-depth finding (logged-out = one flavor of boots-but-not-ready).
age-mc3s (docs): operator pattern for scheduling 'ao pawl reap' (cron/launchd/NTM) — no in-repo
daemon (ADR-0009) — in scripts/pawl.sh usage + docs/contracts/pawls.md.
Tests: 72 pawl bats (build_review_body 4, pawl_tier_note 3, detect/dismiss/stall 9, + adaptive/verdict-
parse/session-state/tri-model back-compat). shellcheck clean across pawl.sh, pawl-review.sh, check-pawl-pre-push.sh.
Bead: age-mwhj
Spawn the strongest membrane the host's accounts can form, instead of tri-or-fail.
CAPABILITY-ADAPTIVE (age-4o33):
- up probes installed families (claude/codex/agy), spawns ONLY those (canonical order; pane
index = position in the enabled set), gates only those, stamps the session TIER: multi
(>=2 families => mode=multi-model) | fresh (1 family => mode=fresh-context). Fail-fast: a
host without codex/agy gets a working fresh-context membrane, not a hard failure.
- route operates over the ENABLED set; tier-aware min confirmers (2 multi / 1 fresh); any
REFUTE blocks; fail-closed on insufficient. A fresh-context verdict is recorded HONESTLY so
a high-irreversibility door (push-to-main) can still refuse it.
- pins (--dual/--tri/--models); idle-TTL reap + 'ao pawl reap' (substrate/cron-driven auto-down,
no in-repo daemon per ADR-0009). ao pawl: +reap; up/route shorts reflect adaptive/tier reality.
VERDICT + SESSION-STATE HARDENING (age-nomq) — BOTH caught by the cross-family review of this branch:
- verdict_of now requires the WHOLE LINE to be the verdict (only non-alnum TUI chrome before
PAWL, only whitespace after). A reviewer NARRATING the format ('... PAWL <nonce> CONFIRMED')
previously false-matched -> a fail-open. Anchored both ends; awk prints the last field.
- _touch_route_ts wrote session.json via python's spaced JSON, which the compact-format grep
readers (load_session / _session_idle) then failed to parse -> after the FIRST route a session
reverted to the 3-family default (mis-indexed panes) and the idle reaper died (-1 forever).
Writer now emits compact separators; readers tolerate any spacing.
Tests: 56 bats (adaptive 26, verdict-parse 9, session-state 4, tri-model 17 back-compat) + 5
cmd/ao Go tests. Pure helpers + the write->touch->reload round-trip + both verdict false-match
directions are all locked.
Adds the AGY/Antigravity pane (Gemini 3.5 Flash) as a 3rd cross-family reviewer
to the standing pawl-service, and makes pawl-review lazily stand the service up
so the membrane is never silently cold again — the gap that ran ~13 reviews cold
in a single session because the service was never started.
pawl.sh:
- AGY_PANE (pane 3) + agy lifecycle: agy_dead (foreground-process liveness,
mirrors cod_dead), agy_clear_trust_gate (accepts agy's 'trust this folder' gate
that --dangerously-skip-permissions does not skip), agy_ready, agy_send.
- up spawns --cc=1:opus --cod=1 --agy=1 and readiness-gates all 3.
- route sends to all 3, polls 3 nonce-tagged verdicts, applies pawl_decide_agreement
(extracted, pure, unit-tested): ALL-CONFIRM + recall-biased — any REFUTE blocks;
a pass needs every replier to CONFIRM and >=2 of them (any 2 of claude/gpt/gemini
are cross-family); a model unavailable after retries degrades to the remaining
>=2 (never <2, never fail-open). 3 refuters recorded; metrics gain agy + confirmed.
- health reports pane 3.
pawl-review.sh:
- lazy auto-up: when routing is eligible but the service is down, stand it up once
(warm for the whole session) instead of a cold codex-exec each review. Fail-safe
to the cold path; opt out via PAWL_NO_SERVICE / PAWL_NO_AUTOUP.
Depends on the ntm fork bump of the agy model pin 3.1 Pro -> 3.5 Flash.
Validated live: tri-model service stands up (all 3 ready), agy engages and emits a
parseable verdict, degrade-to-2-cross-family fired when codex was mid-self-upgrade.
bats: agy_dead x5 + the 6 pawl_decide_agreement branches.
Audit M-2: a new filepath.Join built from external input (CLI args, web request
fields, or a var explicitly named *untrusted/external/remote/requested/
userSupplied/userInput*Path) should route through the EvalSymlinks+filepath.Rel
containment that cli/internal/{worktree,resolver} use. scripts/check-path-containment.sh
flags every such Join for manual containment review; registered as the warn-only
go.path-containment gate.
FAIL-CLOSED by design: it does NOT auto-clear a flagged Join when containment
code is merely nearby — a proximity 'looks safe' check would fail open (an
unrelated nearby filepath.Rel/EvalSymlinks could hide a real escape). It reads
the WHOLE balanced-paren Join call, so a multi-line Join's tainted arg on a later
line is not missed. HEURISTIC forward-ratchet scoped to explicit taint markers
(the ~6k existing internal Join sites flag zero, by construction), NOT a sound
taint analysis — documented in the script header. bats covers
unsafe-flagged / contained-still-flagged / multiline-flagged / internal-not-flagged /
_test-excluded / warn-only-exit-0.
Closes age-0dq9.6
An uncaught INT/TERM does not run the EXIT trap in sh, so a killed push
stranded the mkdir-lock and made the next push spin the full
AGENTOPS_PUSH_LOCK_TIMEOUT (~300s) on a lock nobody owned. Add an INT/TERM
trap that runs cleanup then aborts; the EXIT trap re-runs cleanup idempotently.
cmd/ao/main_test.go TestMain now sets+restores TMUX_TMPDIR to isolate the
tmux socket (companion to 3ad929c5 — a test poisoned the dev's real tmux
server with HOME=tempdir). TestMain has no *testing.T so t.Setenv is
unavailable; these two raw os.Setenv are intentional, like the existing
HOME isolation. Ratchet 12->14.
The bead intent is 'ao pawl up / ao pawl route / ao pawl metrics', but those lived only
in scripts/pawl.sh ('bash scripts/pawl.sh <cmd>'); 'ao pawl' exposed only 'review'. Wire
the 5 service subcommands into the ao CLI (each forwards verbatim to scripts/pawl.sh and
propagates its exit code via pawlReviewExitError — so a REFUTED route exits non-zero
through ao). + 2 cmd/ao tests (exit-code propagation; subcommand registration) + 4 honest
cmdao-surface-allowlist rows (live-ATM service commands + bats-covered metrics). Completes
the ao pawl surface; the standing warm-duel service is now first-class on the CLI.
age-standing-pawl-service-ml8
cmd_route now records one SLO datapoint per route (latency, opus/codex verdicts,
agreement) to .agents/pawl/metrics.jsonl — non-blocking + fail-safe (metrics
never affect the verdict). 'pawl metrics [--json]' surfaces p50/p95 round-trip
latency + agreement rate (both-CONFIRMED vs disagreement) over the recorded
routes; a disagreement is counted + inspectable. 5 bats tests over synthetic
data (no live routes needed). age-standing-pawl-service-ml8.6.
pawl-review.sh routes the DEFAULT adversarial pawl through the warm pawl-service
(opus+codex DUEL) when a healthy service is up, deprecating the cold per-bead
'codex exec' subprocess (the hand-spinning anti-pattern). Stronger (multi-model
agreement) + warm. Fail-SAFE: falls back to codex-exec unless the route wrote a
verdict that PASSES the real pawl-verdict.sh check; never fail-open.
Hardened across THREE dogfood rounds where the warm duel reviewed this change
through itself and caught 5 real defects, all fixed:
R1: pass real $PR not hardcoded 0; routed path writes no --converge lineage;
use $SCRIPT_DIR/pawl.sh (PAWL_SERVICE_SCRIPT-overridable) not $REPO_ROOT.
R2: only route --scope head (staged is review-only — routing would write a
HEAD-bound verdict for an uncommitted diff); verify the routed verdict with
the REAL pawl-verdict.sh check (schema/PR/evidence/diversity), not a shallow
head+disposition jq that a malformed verdict could slip through.
+ 6 bats tests (valid-passes-check, invalid-fails-check->fallback, staged-not-
routed, REFUTED, no-verdict->fallback, opt-out). 23/23 pass.
age-standing-pawl-service-ml8.7.
The full-suite race gate ran 'go test ./... -race -shuffle=on' to the terminal;
on failure it only echoed a message, so the shuffle SEED + FAIL package + test —
the only handle on an order-dependent isolation flake (age-l45b) — scrolled past
and were LOST, leaving the flake undiagnosable. Now: capture the race output to
a timestamped TMPDIR log; on failure name the log + grep-surface the seed/FAIL
package with a ready 'go test ./<pkg> -race -shuffle=<seed>' repro hint; on
success show the summary + remove the log. Adds the first bats coverage of the
race-gate path (test 8). shellcheck clean, 8/8 green.
Four real 'find -printf' instances (age-iue5 #850, age-7jm6) shipped
undetected: no gate caught the class, and Linux CI succeeds at runtime
(only BSD/macOS find errors on it). Promote the arc's lesson to a durable
must-never-regress guard:
- scripts/check-shell-portability.sh — static grep over ALL tracked first-party
shell (git ls-files, so worktrees/vendored/gitignored are excluded), selecting
shell by .sh/.bash suffix OR shell shebang (covers extensionless hooks like
scripts/hooks/pre-push.local, .githooks/*, bin/*). tests/ excluded (fixtures
embed the pattern). Comment-excluded; '# portability-ok' must be a trailing
COMMENT (not smuggled in a string); self-excluded. ~0.35s over 601 files.
- tests/scripts/check-shell-portability.bats — 11 specs.
- gate 'always.shell-portability' (Fast|Full, blocking) in seed.go.
Hardened across a cross-family pawl refute of the first draft (3 defects):
(1) extensionless shell scripts slipped a *.sh-only scan -> shebang detection;
(2) the marker matched anywhere on the line -> require a '#'-led comment;
(3) Match scripts/** left holes outside scripts/ -> always-run over the whole
tracked tree. Catches reintroduction on EVERY platform.