Skip to content

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

Merged
stranske merged 2 commits into
mainfrom
claude/advisor-reach
Aug 22, 2026
Merged

fix(advisor): the front door was a thin caller, not a structural dead end#20
stranske merged 2 commits into
mainfrom
claude/advisor-reach

Conversation

@stranske

Copy link
Copy Markdown
Owner

Replaces #19, which GitHub auto-closed when its base branch (#17's) was deleted on merge. Same commits, rebased onto main.

The claim this fixes — mine, and wrong

I reported that 35 of 41 capabilities were structurally unreachable from the advisor front door. That framed the central problem of this whole effort as a design limit to work around, when it was a thin caller.

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." advise() was handing it {repository, task_type, lane} and nothing else.

adversarial-review, matcher {kind: closer_gate, name: high_stakes_review}
  advisor trigger today   -> (False, ['closer_gate_not_in_trigger'])
  same matcher, one field -> (True, [])

Three changes

  1. One constant. DIRECT_ENTRY was {"offload": "offload"} under a comment claiming it mirrored dispatcher.TASK_TYPE_CAPABILITY. It did not — the dispatcher routed runtime_ac to runtime-ac-checks while the advisor named deliberate-break-verifier for the same work. Now direct_entry(), derived from the dispatcher's map.
  2. Context forwarded (context=, --context), confined to kinds the ledger declares. Absent, empty or wrong context still fails closed.
  3. The whole denominator. Every non-match now reports the reason _matches_trigger already returned, plus an entry_requirement derived from its own matcher — "entered at ci_workflow 'maint-87-docs-drift-fix-agent'" instead of nothing. Those reasons were computed and discarded; that discard was the silence.
before after
reachable, free text 6 7
reachable, real caller context 6 12
accounted for with a reason 6 of 41 41 of 41

No matcher rewritten, no task-signal widened — widening keywords to move the number would corrupt the associations this feeds.

What CI caught in the first version

The reach selftest asserted a floor against the live ledger (41 rows here, 14 on a clean runner). Green locally, red on CI — a machine-local assertion dressed as a correctness test. Fixed by isolation, not by skipping: mechanism assertions moved to a synthetic ledger so they run everywhere, and the drift guard became code-vs-code so it runs on the clean runner where drift would actually land unseen. No new skip is spent.

Reach shrinkage is deliberately not checked here — #18's capability_firing_monitor.advisor_reach already owns it with a declared baseline and an advisor_reach_regression defect. A second copy would be a parallel inventory. That's said in the code, at the spot someone would add it back.

Break→revert (three, all discriminating)

  • Emptying the dispatcher map → fails on the task-type disagreement.
  • Removing reason-reporting → "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 on the rebased branch: 365 passed, 0 failed, 0 skipped, 81/81 selftests, 5/5 gates.

🤖 Generated with Claude Code

Tim Stranske and others added 2 commits August 22, 2026 13:17
… 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>
…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>
@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: 9 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: 874a0a4d-e2e8-410a-b730-ce3acc3f43ac

📥 Commits

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

📒 Files selected for processing (1)
  • capability_advisor.py

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

@stranske
stranske merged commit 52acbd8 into main Aug 22, 2026
2 checks passed
@stranske
stranske deleted the claude/advisor-reach branch August 22, 2026 18:28
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