Skip to content

fix: producers that ran before heartbeats were switched on, and three stored verdicts that had rotted - #18

Merged
stranske merged 1 commit into
mainfrom
claude/heartbeat-ordering-and-trigger-triage
Aug 22, 2026
Merged

fix: producers that ran before heartbeats were switched on, and three stored verdicts that had rotted#18
stranske merged 1 commit into
mainfrom
claude/heartbeat-ordering-and-trigger-triage

Conversation

@stranske

Copy link
Copy Markdown
Owner

What this is

Every capability already had a declared trigger, caller, heartbeat and recurrence fixture, and
capability_firing_monitor.py watched for silence. That proved the pathways exist. This PR is
what happened when 25 productive tasks were actually driven through them: four defects where the
pathway existed and still recorded nothing
, and four line-number citations pointing at the wrong
lines
.

Measured, not asserted: heartbeats were driven against an isolated copy of the ledger
(ORCH_CAPABILITIES_PATH) with the real Brain (ORCH_LOCAL_RUNTIME untouched), each task in its
own subprocess, ledger diffed after each. 38 of 41 capabilities are now confirmed to fire from
real work — 25 in production from the live hourly tick, 22 from tasks driven here, 13 of them with
zero prior production events. The 3 that never fired each carry a measured cause and a criterion.

A concurrent session was writing advisory match events into the same shared ledger. Every count
here filters metadata.source == "capability_advisor" and non-null metadata.skill; their 8-event
burst (epoch 1787417784–1787417795) is accounted for separately and counted as a firing nowhere.


1. Two producers ran above the heartbeat export, so their heartbeats were no-ops

production_heartbeat / daily_heartbeat return False unless ORCH_CAPABILITY_HEARTBEATS=1 is in
the child process's environment, and only orchestrate.sh exports it. The export sat below two
producers:

Producer Capability Effect
frontend_verify.py --doctor … frontend-verifier Its only tick caller. Flipping ORCH_FRONTEND_VERIFY_START_BROWSER=1 would have recorded nothing and read as "the switch didn't help".
capacity.py windowed-capacity-policy Its declared cadence — "every tick (capacity.build at the top of the tick)" — was false. Evidence came only from later in-process callers.

The active-mode block that validates lifecycle truth and exports the flag moves above all
producers, preserving the ordering invariant inside it (validate first, then enable — otherwise a
producer emits evidence outside a validated contract). Three grep-able anchors added:
ORCH-ANCHOR: heartbeat-export, heartbeat-producers, frontend-verify-doctor.

Durable countermeasure (in capability_activation_audit, extending the module that already owns
"could it fire?"):

  • emits_heartbeat(path) — a definition is not a call, which is why capabilities.py may
    legitimately stay above the export. Handled by mechanism, not an exemption list.
  • shell_heartbeat_gate(text) — pure; before / after / deferred. The third bucket exists
    because a one-line shell function (_gh_gate() { … gh_capacity.py …; }) is defined at the top and
    called throughout, so its position says nothing. My first implementation called it before and
    invented a defect; the bucket is the fix.
  • heartbeat_env_gate() — publishes suppressed_modules next to invocations_after, so a zero
    is readable. suppressed: [] with invocations_after: 0 is a broken parse, not a clean ordering.
  • New defect class heartbeat_env_suppressed, applied entry-class agnostically.
  • Suite gate: test_capabilities.test_no_tick_producer_runs_above_the_heartbeat_export, carrying its
    own positive control on synthetic text.

Verified both directions on real code. Against origin/main's orchestrate.sh the check reports
suppressed: ['capacity.py', 'frontend_verify.py']; against the fixed file, [] with 39 post-export
invocations.

Why this is not a duplicate of anything. heartbeat_reachable reported frontend-verifier
reachable — correctly, the call is reachable. The firing monitor reported never fired — also
correctly. Both were right and neither could see the cause. Reachability and enablement are different
questions and the second had no owner.

2. feature-scan had no producer at all

feature-scan and feature-reflection-cli both declare feature_scan.py:scan, and the module
credited only the second. So the daily cadence step ran, feature-reflection-cli accrued invocations,
and feature-scan read never fired foreverunrecorded, not dormant, and indistinguishable
from dormant in the monitor. Both rows are now credited at the path both declare.

Deliberately not fixed here: whether two rows should describe one code path. Merging or retiring
one is a lifecycle decision needing its own evidence, and inventing a heartbeat is the wrong way to
make it.

3. ORCH_RUNTIME_AC_ALLOW_COMMANDS was never holding what its record said

The stored criterion said the flag gated both agent-authored command/non_regression checks
and the template-built deliberate_break command, and that the actionable step was to split
the flag
. I ran a real deliberate_break spec through run_verification with
allow_command_checks False and True: identical resultslocal_verify executed, mutated a
candidate, ran the test, returned a real verdict, both times. The split already existed.

So deliberate-break-verifier sat in the "held switches" bucket on a frozen diagnosis, not a
blocker.

The split is now named: COMMAND_EXEC_GATED_TYPES + command_execution_gated() at
ORCH-ANCHOR: runtime-ac-command-exec-gate, consumed by both the runtime gate and the recurrence
fixture — one shared name, because a matching pair of literals drifts and a name cannot.

No new gate was added for deliberate_break's agent-authored test_cmd. A default-OFF gate over
a path that works today would create a latch, not remove one. What screens that payload is recorded
instead: local_verify runs it via shlex.split, shell=False, after _has_shell_marker has
rejected every shell control character. The flag correctly stays off for what it does hold.

switch_review.SWITCH_CAPABILITY remapped ORCH_RUNTIME_AC_ALLOW_COMMANDS → runtime-ac-checks;
pointing the "ON but silent" arm at a capability the switch cannot influence made that review unable
to say anything true about either one.

4. Line-number citations rot, so they are banned and the ban is enforced

Four found, all pointing somewhere wrong:

Citation Said Actually
SWITCH_ON_CRITERIA[…START_BROWSER]: orchestrate.sh:133 / :152 doctor before export rotted to 171 / 190
SWITCH_ON_CRITERIA[…ALLOW_COMMANDS]: runtime_ac.py:955 shlex.split(check['command']) off by one — and the claim built on it was false
capability_firing_monitor.py header: "(line ~152)" the export 190
ledger note on frontend-verifier: orchestrate.sh:133 the doctor 171

All replaced with anchors. Enforced in capability_recurrence_check._selftest: no
SWITCH_ON_CRITERIA value may contain a file.py:NNN citation
, and every cited
ORCH-ANCHOR: <name> must be defined on its own comment line in the tree, with a floor of ≥3
citations so the rule cannot pass vacuously.

That assertion's own first version was circular — a plain "ORCH-ANCHOR: x" in tree_text test
passed a citation to a nonexistent anchor, because the criteria table's own source is part of the
tree. The check satisfied itself, in three lines. Only the break→revert found it.

5. ORCH_EXPLORATION_MODE: a decision, not a hold

Its criterion correctly said the blocker was the review, not the data — and the review had never
been run. Ran it: keep_epsilon_greedy / epsilon_still_preferred. Its evidence gates are met
(727 instrumented runs, 65 direct exploration outcomes, ready_tasks 5/9, zero-cell 16.2%) and
Thompson-hybrid improves simulated challenger quality and direct exploration outcomes — but not
both. So ε-greedy is kept on merit.

_check_thompson now runs the review and reports its recommendation, so the prose criterion is
gone and the switch re-raises itself if that verdict ever flips. README updated (it still said
"pending the weekly evidence gate's recommendation").

Net re-triage: 5 held switches → 3 (range-lane genuinely data-gated — today's preview reports
dispatch_preview: [], 0 eligible candidates, read from the artifact its own criterion names, not a
calendar; strategy-experiments no demand; frontend operator policy), 1 decided, 1 never held.

6. Advisor reach is now measured against a declared floor

The advisor can name 6 of 41 capabilities from free text — 5 by declaration
({"field": "task_type", …}) plus offload through a hardcoded direct-entry map. The other 35 fail
closed by design, because _matches_trigger is handed {repository, task_type, lane} and a
kind-shaped matcher cannot match it.

Reach had already shrunk in silence: docs-drift-fix-agent and adversarial-review still hold
learned association observations from when they were reachable; each was later retuned to a
kind-shaped matcher and dropped out of the front door with nothing reporting it — a latched gate
inside the measuring instrument.

advisor_reach() publishes the reachable set against ADVISOR_REACH_BASELINE and raises
advisor_reach_regression on a shrink. Report-only, and it must stay that way. Widening
TASK_SIGNALS or loosening a matcher to move this number would corrupt the learned associations —
explicitly not done.

Every audit row now also carries advisor_reachable, because "the advisor never recommended it" has
two causes with opposite fixes: no demand, versus the front door structurally cannot name it.


Verification

pytest:     354 passed, 0 failed, 0/24 max skipped (354 collected; floor 354)
selftests:  81 of 81 modules ran, 0/7 max skipped
activation audit   ok  CAN FIRE:   41 of 41  (100%)
recurrence replay  ok  WOULD FIRE:  42 of 46
set coverage       ok  all 6 checks over ALL 41 ledger capabilities
admission          ok  5 enforced, 3 legacy debt, commitments clean
ledger validate    ok  "valid": true
VERIFIED — 354 tests actually executed and passed, 81 selftests spoke, 5 of 5 gates green

Recurrence went 41/46 → 42/46 by replacing a fixture that asserted a blocker which does not
exist
, proven by running the code — not by bending one to match.

Path-independence instead of a mirror re-run. A concurrent session had deliberately left
~/.codex/orchestrator-mirror unsynced, and that gap is the owner's only circuit breaker between an
agent's change and the dispatcher that dispatches agents — so it was not touched. The same guarantee
was obtained by re-running the verdict from a copy of the tree at a different path: identical
354/354, 41/41, 42/46, 5/5.

Break→revert, all reverted byte-identical:

  1. COMMAND_EXEC_GATED_TYPES gaining deliberate_break → predicate assertion fails.
  2. The gate call site re-inlining a literal set that includes it (predicate still correct) → the
    runtime assertion fails with SKIP instead of UNSAFE. This is what the shared name buys.
  3. A SWITCH_ON_CRITERIA entry citing a nonexistent anchor → anchor rule fails (and caught its own
    circular first draft).
  4. The ordering check run against origin/main's real orchestrate.sh → reports both real
    suppressed producers.

.verify-floor.json 353 → 354: exactly one new test. No ceiling moved and nothing new is
skipped
— the new checks read source files rather than a populated ledger, so they run on any
machine. Measured on the merge result, per that file's own note. Hand-edited rather than via
--update-floor so the note history survives.

Not in this PR, on purpose

  • capability_advisor.py is untouched — PR fix(advisor): every association count names its population (FM5) #17 is open against that file alone. The reach
    measurement lives in the activation audit and imports the advisor read-only.
  • No switch was flipped. ORCH_FRONTEND_VERIFY_START_BROWSER stays the owner's call; this only
    makes flipping it measurable.
  • No new human touchpoint. The reach sweep and the ordering gate are FYI-only and cannot
    accumulate: neither has a queue, and neither can auto-open a gate or auto-widen a matcher.
  • No stage, component, rail/role classification, feedback surface or roles.py change, so
    ARCHITECTURE.md + orchestrator-loop.svg are untouched by design.

The 41-row expected-vs-actual table, with a named cause and a criterion for every non-firing row,
lives in the gitignored evidence record Code/Audits/Orchestrator/2026-08-22-trigger-pathways-end-to-end.md.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in: 2 minutes

Limit details: You’ve used the included review currently available. Your 66 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3f5c4254-f63f-4dc9-9c11-648998170853

📥 Commits

Reviewing files that changed from the base of the PR and between d58316c and d5fed55.

📒 Files selected for processing (10)
  • .verify-floor.json
  • README.md
  • capability_activation_audit.py
  • capability_firing_monitor.py
  • capability_recurrence_check.py
  • feature_scan.py
  • orchestrate.sh
  • runtime_ac.py
  • switch_review.py
  • test_capabilities.py

Comment @coderabbitai help to get the list of available commands.

stranske pushed a commit that referenced this pull request Aug 22, 2026
…icating #18

CI caught a real defect in the first version of this change: the reach selftest
asserted a floor against the LIVE ledger, which holds 41 rows here and 14 on a
clean runner. It passed locally and failed CI — a machine-local assertion wearing
the clothes of a correctness test, which is the shape this repo's skip discipline
exists to catch.

Fixed by isolation rather than by skipping, per .verify-floor.json's own rule
("Moving a test back below the ceiling is the preferred way to lower it — fix
what made it machine-dependent, rather than agreeing to check less"):

* MECHANISM assertions now run on a SYNTHETIC ledger, so they execute on every
  machine: a kind-based capability is reported with its named reason and entry
  requirement rather than as absence; context forwarding reaches it; and absent,
  empty or WRONG context still fails closed.
* THE DRIFT GUARD is now code vs code. `DIRECT_ENTRY` is hoisted to
  `direct_entry()` so the dispatcher agreement is assertable with no ledger at
  all — drift would otherwise land unnoticed precisely on the clean runner where
  a ledger-dependent check can only skip.
* REACH SHRINKAGE IS DELIBERATELY NOT CHECKED HERE. #18 already added
  `capability_firing_monitor.advisor_reach` with a declared baseline and an
  `advisor_reach_regression` defect. A second copy would be a parallel
  inventory. Said so in the code, at the point someone would add it back.

Net: no new skip is spent (the one PREREQUISITE mark on a bare runner is the
pre-existing front-door section), and CI now checks MORE than before.

Also fixed: the synthetic-ledger assertion originally required not_applicable to
equal exactly the two fixtures. `capabilities.load` seeds KNOWN_DECLARATIONS into
any ledger it reads, so that was a test coupled to an unrelated constant —
containment plus a denominator computed from the ledger instead.

Break->revert, all three discriminating: emptying the dispatcher map fails on the
task-type disagreement; removing reason-reporting fails with "came back as
SILENCE, not as named non-matches"; letting context be assumed instead of
fail-closed fails on the wrong-value case.

verify.py: 353 passed, 0 failed, 0 skipped, 81/81 selftests, 5/5 gates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
stranske pushed a commit that referenced this pull request Aug 22, 2026
…icating #18

CI caught a real defect in the first version of this change: the reach selftest
asserted a floor against the LIVE ledger, which holds 41 rows here and 14 on a
clean runner. It passed locally and failed CI — a machine-local assertion wearing
the clothes of a correctness test, which is the shape this repo's skip discipline
exists to catch.

Fixed by isolation rather than by skipping, per .verify-floor.json's own rule
("Moving a test back below the ceiling is the preferred way to lower it — fix
what made it machine-dependent, rather than agreeing to check less"):

* MECHANISM assertions now run on a SYNTHETIC ledger, so they execute on every
  machine: a kind-based capability is reported with its named reason and entry
  requirement rather than as absence; context forwarding reaches it; and absent,
  empty or WRONG context still fails closed.
* THE DRIFT GUARD is now code vs code. `DIRECT_ENTRY` is hoisted to
  `direct_entry()` so the dispatcher agreement is assertable with no ledger at
  all — drift would otherwise land unnoticed precisely on the clean runner where
  a ledger-dependent check can only skip.
* REACH SHRINKAGE IS DELIBERATELY NOT CHECKED HERE. #18 already added
  `capability_firing_monitor.advisor_reach` with a declared baseline and an
  `advisor_reach_regression` defect. A second copy would be a parallel
  inventory. Said so in the code, at the point someone would add it back.

Net: no new skip is spent (the one PREREQUISITE mark on a bare runner is the
pre-existing front-door section), and CI now checks MORE than before.

Also fixed: the synthetic-ledger assertion originally required not_applicable to
equal exactly the two fixtures. `capabilities.load` seeds KNOWN_DECLARATIONS into
any ledger it reads, so that was a test coupled to an unrelated constant —
containment plus a denominator computed from the ledger instead.

Break->revert, all three discriminating: emptying the dispatcher map fails on the
task-type disagreement; removing reason-reporting fails with "came back as
SILENCE, not as named non-matches"; letting context be assumed instead of
fail-closed fails on the wrong-value case.

verify.py: 353 passed, 0 failed, 0 skipped, 81/81 selftests, 5/5 gates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… stored verdicts that had rotted

Every capability had a declared trigger, caller, heartbeat and recurrence fixture. That proved the
pathways exist. Driving 25 productive tasks through them found four defects where a pathway existed
and still recorded nothing, plus four line-number citations pointing at the wrong lines.

1. TWO PRODUCERS RAN ABOVE THE HEARTBEAT EXPORT, so their heartbeats were no-ops.

   `production_heartbeat` / `daily_heartbeat` return False unless ORCH_CAPABILITY_HEARTBEATS=1 is in
   the CHILD's environment, and only orchestrate.sh exports it. The export sat 19-22 lines BELOW:

     * `frontend_verify.py --doctor` -- the frontend-verifier capability's ONLY tick caller, so
       flipping ORCH_FRONTEND_VERIFY_START_BROWSER=1 would have recorded nothing and read as "the
       switch didn't help";
     * `capacity.py` -- making windowed-capacity-policy's declared cadence ("every tick,
       capacity.build at the top of the tick") false; its evidence came only from later in-process
       callers.

   The active-mode validate+export block moves above all producers, preserving the invariant inside
   it (validate lifecycle truth FIRST, then enable heartbeats -- otherwise a producer emits evidence
   outside a validated contract). Three grep-able anchors added.

   Durable countermeasure in capability_activation_audit: `emits_heartbeat` (a definition is not a
   call, which is why capabilities.py may stay above the export), `shell_heartbeat_gate` (pure;
   before / after / DEFERRED, the third because a one-line shell function is defined at the top and
   called throughout, so its position says nothing), `heartbeat_env_gate` (publishes suppressed
   COUNT next to invocations_after, so a zero is readable), and defect `heartbeat_env_suppressed`.
   Verified both directions on real code: against origin/main's orchestrate.sh it reports
   ['capacity.py', 'frontend_verify.py']; against the fixed file, [] with 39 post-export invocations.

   This is a new dimension, not a duplicate: `heartbeat_reachable` reported frontend-verifier
   REACHABLE (correctly -- the call is reachable) and the firing monitor reported NEVER FIRED (also
   correctly). Both were right and neither could see the cause.

2. `feature-scan` HAD NO PRODUCER AT ALL. It and feature-reflection-cli both declare
   feature_scan.py:scan, and the module credited only the second -- so the daily cadence step ran
   and feature-scan read "never fired" forever. Unrecorded, not dormant, and indistinguishable from
   dormant in the monitor. Both rows are now credited at the path both declare. Whether two rows
   should describe one code path is left open: merging or retiring one is a lifecycle decision
   needing its own evidence, not an invented heartbeat.

3. ORCH_RUNTIME_AC_ALLOW_COMMANDS WAS NEVER HOLDING WHAT ITS RECORD SAID. The stored criterion said
   it gated both agent-authored command/non_regression checks AND the template-built
   deliberate_break command, and that the flag had to be SPLIT before it could be turned on. Running
   a real deliberate_break spec both ways gave identical results -- local_verify executed and
   returned a real verdict with the flag OFF. The split already existed. So deliberate-break-verifier
   sat in the "held switches" bucket on a frozen diagnosis.

   The split is now NAMED: COMMAND_EXEC_GATED_TYPES + command_execution_gated(), consumed by both
   the runtime gate and the recurrence fixture -- one shared name, because a matching pair of
   literals drifts. No new gate was added for deliberate_break's agent-authored test_cmd: a
   default-OFF gate over a path that works today would CREATE a latch. What screens it is recorded
   instead (shlex.split, shell=False, shell-marker rejected). switch_review's flag -> capability map
   remapped to runtime-ac-checks; pointing the "ON but silent" arm at a capability the switch cannot
   influence made that review unable to say anything true about either.

4. LINE-NUMBER CITATIONS ROT, so they are banned and the ban is enforced. Four found, all wrong:
   orchestrate.sh:133/:152 (actually 171/190), runtime_ac.py:955 (off by one, and the claim built on
   it was false), the firing monitor's "line ~152", and a ledger note. All replaced with anchors.
   capability_recurrence_check._selftest now rejects any file.py:NNN citation in SWITCH_ON_CRITERIA
   and requires every cited `ORCH-ANCHOR: <name>` to be DEFINED on its own comment line, with a
   floor of >=3 citations so the rule cannot pass vacuously.

   Its own first version was circular -- `"ORCH-ANCHOR: x" in tree_text` passed a citation to a
   nonexistent anchor, because the criteria table's own source is part of the tree. Only the
   break->revert found that.

5. ORCH_EXPLORATION_MODE was correctly diagnosed as "waiting for the review, not the data", and the
   review had never been run. Ran it: keep_epsilon_greedy / epsilon_still_preferred -- gates ARE met
   (727 instrumented runs, 65 direct exploration outcomes, ready_tasks 5/9, zero-cell 16.2%) and
   Thompson-hybrid improves simulated challenger quality and direct exploration outcomes not BOTH.
   A DECISION, not a hold. `_check_thompson` now runs the review and reports its recommendation, so
   the prose criterion is gone and the switch re-raises itself if the verdict flips.

6. ADVISOR REACH is now measured against a declared floor. The advisor can name 6 of 41 capabilities
   from free text (5 by task_type-shaped matcher + offload via a hardcoded map); the other 35 fail
   closed by design. Reach had already SHRUNK in silence -- docs-drift-fix-agent and
   adversarial-review still hold learned association observations from when they were reachable.
   `advisor_reach` publishes the set against ADVISOR_REACH_BASELINE and raises
   `advisor_reach_regression` on a shrink. REPORT-ONLY, and it must stay that way: widening
   TASK_SIGNALS or loosening a matcher to move this number would corrupt the learned associations.

Every row also carries advisor_reachable now, because "the advisor never recommended it" has two
causes with opposite fixes -- no demand, versus the front door structurally cannot name it.

Recurrence replay 41/46 -> 42/46, by replacing a fixture that asserted a nonexistent blocker.
Activation audit 41/41 can fire. `python3 verify.py`: 354 passed, 0 skipped, 5 of 5 gates green,
and identical from a copy of the tree at a different path (path-independence, since the exec mirror
was deliberately left unsynced by a concurrent session -- that gap is the owner's circuit breaker).

Break->revert, all reverted byte-identical: the command-exec split at the predicate; the same split
drifting at the call site; the anchor-existence rule; and the ordering check against origin/main's
real orchestrate.sh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@stranske
stranske force-pushed the claude/heartbeat-ordering-and-trigger-triage branch from 3d8f90e to d5fed55 Compare August 22, 2026 18:26
@stranske
stranske merged commit 80f1619 into main Aug 22, 2026
2 checks passed
@stranske
stranske deleted the claude/heartbeat-ordering-and-trigger-triage branch August 22, 2026 18:28
stranske added a commit that referenced this pull request Aug 22, 2026
… end (#20)

* fix(advisor): the front door was a thin caller, not a structural dead end

`capability_advisor` could name 6 of 41 capabilities from free text, and the
other 35 came back as ABSENCE — no entry, no reason, indistinguishable from
"nothing else exists". That was read as a structural limit. It is not.

`capabilities._matches_trigger` matches a `{"kind": k, "name": n}` matcher
against a SAME-NAMED FIELD THE CALLER SUPPLIES, and says so itself: "Adding a
new trigger kind is then a caller-side change, not an edit here." The advisor
was passing a three-field trigger (repository/task_type/lane) and no kind
fields. Supply the context a caller actually has and those capabilities match
immediately — proven in the selftest for `closer_gate`/`adversarial-review`.

Three changes, no matcher rewritten and no task-signal widened (widening
keywords to move the number would corrupt the very associations this feeds):

1. ONE CONSTANT. `DIRECT_ENTRY` was a one-entry literal whose comment claimed
   it mirrored `dispatcher.TASK_TYPE_CAPABILITY`. It did not — so the dispatcher
   routed `runtime_ac` to `runtime-ac-checks` while the advisor named
   `deliberate-break-verifier` for the same work. The two halves of the system
   disagreed about one task type. Now derived from the dispatcher's map.

2. CONTEXT IS FORWARDED (`context=`, and `--context` on the CLI), confined to
   kinds the ledger actually declares. Absent context still FAILS CLOSED.

3. THE WHOLE DENOMINATOR. Every non-match now reports its named reason from
   `_matches_trigger` plus an `entry_requirement` derived from its own matcher
   ("entered at ci_workflow 'maint-87-docs-drift-fix-agent'"). Those reasons
   were already computed and thrown away; discarding them is what turned 35
   capabilities into silence.

Reach: 6 -> 7 from free text alone, 6 -> 12 with real caller context, 41 of 41
now accounted for with a reason. `_selftest_reach` pins it, because
`adversarial-review` and `docs-drift-fix-agent` both carry advisory match
history proving they were once reachable and dropped out unnoticed when their
matchers were tightened. Reach had never been measured, so it fell in silence.

Break->revert: reverting DIRECT_ENTRY fails on the dispatcher disagreement;
removing the reason-reporting fails with all 39 unaccounted capabilities named.
verify.py: 353 passed, 0 failed, 0 skipped, 81/81 selftests, 5/5 gates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(advisor): make the reach guard machine-independent, and stop duplicating #18

CI caught a real defect in the first version of this change: the reach selftest
asserted a floor against the LIVE ledger, which holds 41 rows here and 14 on a
clean runner. It passed locally and failed CI — a machine-local assertion wearing
the clothes of a correctness test, which is the shape this repo's skip discipline
exists to catch.

Fixed by isolation rather than by skipping, per .verify-floor.json's own rule
("Moving a test back below the ceiling is the preferred way to lower it — fix
what made it machine-dependent, rather than agreeing to check less"):

* MECHANISM assertions now run on a SYNTHETIC ledger, so they execute on every
  machine: a kind-based capability is reported with its named reason and entry
  requirement rather than as absence; context forwarding reaches it; and absent,
  empty or WRONG context still fails closed.
* THE DRIFT GUARD is now code vs code. `DIRECT_ENTRY` is hoisted to
  `direct_entry()` so the dispatcher agreement is assertable with no ledger at
  all — drift would otherwise land unnoticed precisely on the clean runner where
  a ledger-dependent check can only skip.
* REACH SHRINKAGE IS DELIBERATELY NOT CHECKED HERE. #18 already added
  `capability_firing_monitor.advisor_reach` with a declared baseline and an
  `advisor_reach_regression` defect. A second copy would be a parallel
  inventory. Said so in the code, at the point someone would add it back.

Net: no new skip is spent (the one PREREQUISITE mark on a bare runner is the
pre-existing front-door section), and CI now checks MORE than before.

Also fixed: the synthetic-ledger assertion originally required not_applicable to
equal exactly the two fixtures. `capabilities.load` seeds KNOWN_DECLARATIONS into
any ledger it reads, so that was a test coupled to an unrelated constant —
containment plus a denominator computed from the ledger instead.

Break->revert, all three discriminating: emptying the dispatcher map fails on the
task-type disagreement; removing reason-reporting fails with "came back as
SILENCE, not as named non-matches"; letting context be assumed instead of
fail-closed fails on the wrong-value case.

verify.py: 353 passed, 0 failed, 0 skipped, 81/81 selftests, 5/5 gates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Tim Stranske <tim@stranskemo.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
stranske added a commit that referenced this pull request Aug 22, 2026
… opened (#21)

Two modules were publishing different "advisor reach" figures for the same front
door — `capability_activation_audit.advisor_reach` said 5, `capability_advisor`
said 7 — with no statement of which population each covered. Two disagreeing
inventories of the same thing is how a parallel inventory starts, which is the
defect both modules were written to prevent.

They disagreed for a good reason that had just expired. #18 excluded `offload`
from the reach baseline with an explicit rationale: it was reachable only through
a HARDCODED map inside `advise()`, and "a hardcode cannot shrink quietly —
deleting it IS a diff." That was correct when written. #20, merged the same day,
replaced the hardcode with `capability_advisor.direct_entry()` DERIVED from
`dispatcher.TASK_TYPE_CAPABILITY` — so dropping an entry from the dispatcher's
map now narrows the front door with no diff in either module. The exemption that
was right for a literal is exactly wrong for a derivation.

* `advisor_reach` now reads the direct-entry map from `capability_advisor` — one
  source, consumed by both, instead of a second list here.
* The derived set gets its OWN baseline (`ADVISOR_DIRECT_ENTRY_BASELINE`), so it
  can grow freely and cannot shrink without appearing in a diff. Declared and
  derived reach stay separate, because #18's reasoning for separating them holds;
  only the "no baseline needed" part died.
* `total_reachable_count` publishes 7 next to declared 5 and direct-only 2, so
  the numbers reconcile in the output rather than in someone's head.
* Fixed a pointer in `capability_advisor.py` that named
  `capability_firing_monitor.advisor_reach`; it lives in
  `capability_activation_audit`. A comment pointing at a symbol that does not
  exist is the rot #18 spent four citations cleaning up.

Break->revert: emptying `direct_entry()` must regress the derived baseline (it
does, asserted with the map patched out); neutering `direct_entry_regressed`
fails the new assertion.

verify.py: 366 passed, 0 failed, 0 skipped, 81/81 selftests, 5/5 gates.

Co-authored-by: Tim Stranske <tim@stranskemo.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant