Skip to content

feat(tick): the tick consults the capability advisor and records whether one helped - #37

Merged
stranske merged 1 commit into
mainfrom
claude/tick-capability-evidence
Aug 23, 2026
Merged

feat(tick): the tick consults the capability advisor and records whether one helped#37
stranske merged 1 commit into
mainfrom
claude/tick-capability-evidence

Conversation

@stranske

Copy link
Copy Markdown
Owner

What this wires

21 of the 43 capabilities live on the hourly tick, four of them are bound to the tick surface in capability_advisor.SURFACE_BINDINGS — and orchestrate.sh had never called advise() and had never recorded an invocation/outcome edge against an advisory match. That is precisely why the capability-propensity cadence step printed PRIOR-ONLY on every run: the loop had a measurement and no producer.

New step, every tick, below ORCH-ANCHOR: heartbeat-export and below all four steps it grades:

if _step_disabled tick-capability-evidence; then :; else
  python3 "$ORCH/capability_propensity.py" tick-evidence --budget-seconds "${ORCH_TICK_EVIDENCE_BUDGET_S:-30}"
fi

Live dry-run against this instance's real artifacts:

  [tick-evidence] advisor consulted for surface 'tick': 4 bound capability(ies);
                  verdicts 0, gradable 3, awaiting regeneration 3

Dedup finding (CLAUDE.md §0)

Recorded in the module docstring and in the capability-propensity ledger notes, because a plan is not durable.

Checked Finding
capability_advisor.advise() EXISTS; the tick binding EXISTS; tree-wide grep found no caller outside the two modules' own selftests
record_trigger / record_usefulness EXIST; no production caller — only the CLI subcommands and selftests
capability_firing_monitor stores firing HISTORY (did it RUN); does not and should not ask whether the run said anything new
capability_effectiveness measures DELIVERY via influence_edges accepted-vs-counterfactual, which no observer can ever populate

Nothing was rebuilt. This is the missing caller. The one genuinely new part is the observer verdict, because no module compared an observer's own output across runs.

What "it helped" means for an observer

capabilities.is_observer() is the single source of the distinction — consumed, never re-declared, and OBSERVER_MATCHER_KINDS is deliberately not widened (capabilities.py says at the set itself why widening hides real linkage gaps).

  • USEFUL — its report's finding set changed since its own previous run: a defect newly reported, a regression flagged, a switch verdict that moved, a finding resolved.
  • NOT USEFUL — it ran and re-emitted an identical finding set. Silence is not usefulness, and an empty finding set that stays empty is explicitly not useful.
  • Never a delivery verdict. A bound capability the observer test does not confirm (capability-propensity, cli_subcommand) records that it ran and gets no verdict, so an output-change question and a delivery question never average into one rate.

It cannot manufacture evidence

Treated as a correctness requirement, not a nicety. Two independent bounds:

  1. Structural (holds even if bound 2 is buggy) — the experiment id is scoped to the UTC day, so advise()'s advice:<cap>:<digest> and the trigger:/useful: idempotency keys are day-unique. Ceiling 4 match + 4 trigger + 4 outcome events/day, whatever happens; ticks 2..24 of a day write nothing at all (one small state-file read and return).
  2. Substantive — a verdict additionally requires that capability's own cadence artifact to have been regenerated since the last evaluation, tying one verdict to one production.

Arithmetic, printed by the step itself in report["ceiling"]:

verdicts/day
naive, one per bound capability per tick (4 x 24) 96
structural ceiling (day-scoped experiment id) 4
graded ceiling (1/1 daily + 1/7 + 1/7 weekly) 1.29

A 74x reduction, and the realised rate is lower still. A first observation establishes a baseline and records nothing.

The finding projection is the whole safety mechanism. capability-firing-monitor's overdue rows carry silent_days, which rises every day on its own; hashing a row whole would score the monitor `useful` on every run it will ever make — a 100% rate that measures the calendar. The projection keeps identity and verdict fields and drops everything else, and a selftest re-feeds identical findings with moved counters and timestamps and asserts NOT useful. Artifact filenames come from `cadence_registry.STEP_BY_KEY`, not a second table.

Latched-gate answers for the freshness gate

  1. What decrements it? The capability's own cadence step regenerating its artifact — a mechanism already running on `_cadence_due`, not "time passes".
  2. Can the drain run while closed? Yes. The gate suppresses only recording; it never gates the cadence step, which runs on its own schedule regardless.
  3. Same window both ways? Yes, by construction — the measuring quantity and the draining quantity are the same artifact mtime from the same registry-declared step. No second literal to drift.

Runtime rule: every run reports `verdicts_recorded` beside `gradable` / `awaiting_regeneration`, so `0 verdicts` reads as cadence when 3 are gradable and as a deadlock when 0 are.

It cannot stall the tick

No gh, no network, no subprocess, no dispatch. Per-capability guards, a whole-run wrapper, and a SIGALRM budget over the one blocking wait in the path (the ledger flock; ledger writes are tmp+os.replace, so an interrupt cannot tear it). Handled failures exit 0 and the shell's if covers the rest.

Pre-existing defect fixed on the way

advise()'s binding-only branch returned real capabilities with useful: true and recorded no match events — so the fix that made a declared binding survive a classification miss covered the answer and not the evidence. A surface whose words never hit the keyword vocabulary (the tick's cadence pass, exactly) could never accumulate a candidate set: experiments() saw triggers and outcomes on trials with zero candidates, no control arm, and no attributable skill — which also meant the demotion drain could never run for such a surface.

Human involvement

Zero. Report-only, nothing queues, nothing accumulates, no owner touchpoint added.

Kill switch

  • ORCH_TICK_EVIDENCE_DISABLED=1 — module-side, works from any caller.
  • ORCH_DISABLE_STEPS=tick-capability-evidence — shell-side, the repo's one mechanism (registered in cadence_registry, so a typo still WARNs).

Either alone makes the tick behave exactly as it did before: no consult, no ledger event, no state file. Both are exercised by selftests, including a positive control that the same fresh artifact IS evaluated once the switch is off, so the assertion discriminates rather than describing an inert path.

No second store

Evidence goes to the existing capability ledger events via record_trigger/record_usefulness and the advisor's match heartbeat. The state file holds only the last-seen artifact mtime and finding fingerprint — what "did the output change" needs to compare against, the same shape and reason as capability_firing_monitor's history file.

Verification

python3 verify.py is byte-identical to the pre-change baseline on this machine: 365 passed, 3 failed, 368 collected, 82 of 82 selftest modules ran, 0/24 skipped. The 3 failures are the documented pre-existing ones, all traced to the evidence-acquisition ledger row whose module is not committed (another session's in-flight work) — confirmed by re-running against the pre-change tree.

No new pytest test, so .verify-floor.json is untouched; the guards live in capability_propensity --selftest, which verify.py runs and counts.

ARCHITECTURE.md gains prose only. No stage, component, rail/role classification or feedback surface changed, so orchestrator-loop.svg is correctly unchanged.

break -> revert, all 11 verified to discriminate

break assertion that failed
field projection removed (hash the row whole) identical report scored useful: True
experiment id no longer day-scoped five same-day evaluations added 5 verdicts
freshness gate > -> >= ticks 2..24 recorded
baseline-first removed (absent prior as empty) a FIRST observation produced a verdict
is_observer check removed a delivery-kind capability got an output verdict
shape change scored instead of reported unprojectable graded as unchanged
advisor binding-only record fix reverted "every trial triggered everything, so there is no control arm"
kill-switch early return removed "a disabled run still did work"
--budget-seconds 0 promoted to the default budget_exhausted never reported
shell caller removed "orchestrate.sh no longer invokes ... tick-evidence"
step moved above the heartbeat export "invoked ABOVE the ORCH_CAPABILITY_HEARTBEATS export"

Reverted clean afterwards; both modules' selftests green.

End-to-end proof against this instance's real artifacts (isolated ledger + state dir, nothing written to the live ledger)

RUN1 baselined: all 4        verdicts 0  triggers 4  matches 4
RUN2 nothing fresh           verdicts 0  triggers 0  (zero ledger touches)
RUN3 real 34-finding report, counters+generated_at moved
     -> useful=False  "ran and re-emitted an IDENTICAL finding set (34 finding(s) across 4 key(s))"
RUN4 one new overdue capability
     -> useful=True   "report changed since 2026-08-24 - overdue +1/-0"

capability-firing-monitor  cand=3 trig=3 useful=1 not=1 rate=0.5
propensity: 0.5, "1 of 2 resolved trials were useful"   <- no longer PRIOR-ONLY

🤖 Generated with Claude Code

…her one helped

21 of the 43 capabilities live on the hourly tick and four are bound to the
`tick` surface in `capability_advisor.SURFACE_BINDINGS`, yet `orchestrate.sh`
had never called `advise()` and nothing had ever recorded an
`invocation`/`outcome` edge against an advisory `match`. That is exactly why
the capability-propensity cadence step printed PRIOR-ONLY on every run: the
loop had a measurement and no producer. This adds the producer on the
highest-volume unattended surface in the system, so evidence accrues hourly
with no further human attention.

DEDUP (CLAUDE.md §0, also recorded in the `capability-propensity` ledger
notes): `advise()` and `record_trigger`/`record_usefulness` all EXIST and had
no production caller — a tree-wide grep found none outside the two modules'
own selftests. `capability_firing_monitor` stores firing HISTORY (did it RUN)
and `capability_effectiveness` measures DELIVERY via `influence_edges`, which
no observer can populate; neither answers "did the run say anything new".
Nothing was rebuilt: this is the missing caller. The one new part is the
observer verdict, because no module compared an observer's output across runs.

WHAT "IT HELPED" MEANS FOR AN OBSERVER. `capabilities.is_observer()` is the
single source of the distinction, consumed rather than re-declared. An
observer HELPED when its report's finding set changed since its own previous
run (a defect newly reported, a regression flagged, a switch verdict that
moved, a finding resolved) and did NOT help when it re-emitted an identical
set — silence is not usefulness, and an empty set that stays empty is
explicitly not useful. Never a delivery verdict: that is the category error
that parked eight capabilities in a measurement gap they could not leave. A
bound capability the observer test does not confirm records that it ran and
gets no verdict, so two different questions never average into one rate.

IT CANNOT MANUFACTURE EVIDENCE — treated as a correctness requirement, since
24 ticks/day over 4 capabilities is 96 potential data points and a per-run
verdict would make the ranking measure the cadence. Two independent bounds:
(1) STRUCTURAL — the experiment id is scoped to the UTC day, so the ledger
idempotency keys admit at most one match/trigger/verdict per capability per
day however many ticks run, and ticks 2..24 write nothing; (2) SUBSTANTIVE —
a verdict additionally requires that capability's own cadence artifact to have
been regenerated since the last evaluation, which ties one verdict to one
production. The declared cadences are daily and 6-daily, so the graded ceiling
is 1.29 verdicts/day against 96 unconditional — a 74x reduction, and the
step prints that arithmetic in its own report. A first observation establishes
a baseline and records nothing.

The finding projection is the whole safety mechanism: `overdue` rows carry
`silent_days`, which rises daily on its own, so hashing a row whole would
score the firing monitor "useful" on every run it will ever make. The
projection keeps identity and verdict fields and drops everything else.
Artifact filenames come from `cadence_registry.STEP_BY_KEY`, not a second
table, so they cannot drift from the step that writes them.

IT CANNOT STALL THE TICK. No gh, no network, no subprocess, no dispatch;
per-capability guards, a whole-run wrapper, and a SIGALRM budget over the one
blocking wait in the path (the ledger flock — writes are tmp+os.replace, so an
interrupt cannot tear it). Handled failures exit 0. Placed below
`ORCH-ANCHOR: heartbeat-export` (a producer above it records nothing) and
below all four steps it grades; a selftest asserts the caller exists and sits
in both of those positions, which is the guard against this becoming another
built-and-forgotten subsystem.

Also fixes a pre-existing defect found while wiring it: `advise()`'s
binding-only branch returned real capabilities with `useful: true` and recorded
NO match events, so the fix that made a declared binding survive a
classification miss covered the answer and not the evidence. A surface whose
words never hit the keyword vocabulary — the tick's cadence pass exactly —
could never accumulate a candidate set, leaving trials with no control arm and
no attributable skill, which also meant the demotion drain could never run for
such a surface.

Kill switches (both tested): `ORCH_TICK_EVIDENCE_DISABLED=1` module-side, and
`ORCH_DISABLE_STEPS=tick-capability-evidence` shell-side. Either alone makes
the tick behave exactly as before — no consult, no ledger event, no state file.

No new store: evidence goes to the existing ledger events. The state file holds
only the last-seen artifact mtime and finding fingerprint, which is what "did
the output change" needs to compare against — the same shape and reason as
`capability_firing_monitor`'s history file.

No new pytest test, so `.verify-floor.json` is untouched; the guards live in
`capability_propensity --selftest`, which `verify.py` already runs and counts.
`ARCHITECTURE.md` gains prose only — no stage, component, rail/role
classification or feedback surface changed, so `orchestrator-loop.svg` is
correctly unchanged.

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

coderabbitai Bot commented Aug 23, 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: 25 minutes

Limit details: You’ve used the included review currently available. Your 70 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: 3ccc54c4-ab2c-43f2-b6f2-c91ef39d5079

📥 Commits

Reviewing files that changed from the base of the PR and between 75d6788 and c0747a2.

📒 Files selected for processing (6)
  • ARCHITECTURE.md
  • README.md
  • cadence_registry.py
  • capability_advisor.py
  • capability_propensity.py
  • orchestrate.sh

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

@stranske
stranske merged commit 48121ed into main Aug 23, 2026
5 checks passed
stranske added a commit that referenced this pull request Aug 23, 2026
`.verify-floor.json` recorded 366 while main collects 368. The drift came from
#34 (evidence-acquisition landed) and #37 (tick capability evidence), each
measured against a branch cut before the other merged — the same mistake the
file's own note already documents from the 345->353 episode.

A floor BELOW reality is the permissive direction this file exists to close: two
tests could have stopped being collected and still cleared the check, which is
precisely the hole it guards. Measured on the merge result, not on a branch:
368 passed, 0 failed, 0 skipped, 83/83 selftests, 43/43 can-fire, 5/5 gates.

No ceiling moved and nothing new is skipped.

Co-authored-by: Tim Stranske <tim@stranskemo.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
stranske pushed a commit that referenced this pull request Aug 23, 2026
… silent

`.verify-floor.json` has been found BELOW reality four times -- 21 low at the worst, then 8,
then 1, then 2 -- and each was hand-raised after the fact by whoever happened to look. The
stale number was never the defect. `_floor_problems` fired only on `collected < floor`, so a
PR could add tests and never touch the file: silently green, with the floor left permissive
by exactly the number added. #34 and #37 each did precisely that, which is what PR #50 then
had to clean up by hand.

`collected` is now an equality. Too few tests still fails; too many fails as well, printing
the two integers to write and telling you to rebase first.

That also makes the concurrent case self-enforcing, which is the part discipline could not
fix. Once every test-adding branch must edit these same two lines, two concurrent branches
CONFLICT IN GIT -- the second cannot merge without rebasing onto the first and re-measuring
on the actual merge result. This change demonstrated that on itself: #42 merged underneath
it, moved the floor 368 -> 387 and the ceiling 24 -> 26, and the resulting conflict forced
the rebase that produced the 387 recorded here. Git's own conflict detection is what
enforces "measure on the merge result, not on the branch", the rule the note in that file
had to restate three times with nothing behind it.

`passed` deliberately stays a MINIMUM on passed+skipped. Only collection is
machine-invariant -- a skipped test is still collected, measured the same day at 368 on both
CI and this machine with pass/skip splits of 344/24 against 368/0. Making that one strict
too would fail a machine for honestly naming a missing prerequisite.

Two further fixes in the same change:

* `--update-floor` no longer REPLACES the note. It appends. The note is the only record of
  which prerequisite justifies each ceiling, so overwriting it destroyed the rationale on
  every use -- the file had to carry a warning about its own tool.
* Drift does not block `--update-floor`. The first draft made it a latched gate: a floor
  behind reality became a problem, and the guard was `not problems`, so the one remedy the
  error message named was refused for the existence of the very condition it clears.
  `_blocks_floor_update` lets drift through while real failures still block, sharing
  `DRIFT_PREFIX` so message and predicate cannot diverge.

Selftests cover both directions, skip-invariance, the unset-floor case, the unblock
predicate and note preservation, each with a deliberate-break demonstration. Two of those
tests were themselves defective and the break demo is what caught it: the latched-gate
assert exercised `_blocks_floor_update` in isolation and stayed green when the CALL SITE was
reverted (built-but-not-wired), and the source-text assert written to fix that searched for
a literal that appears in its own line, so it could never fail. The needle is now built from
two fragments and the wiring is asserted at the guard.

Verified: 387 passed, 0 failed, 0/26 skipped, 387 collected (floor 387), 83/83 selftests,
43/43 can-fire, 5/5 gates. Ceilings untouched at 26/7/2. black clean at line-length 100;
ruff at main's baseline of 6 pre-existing findings, none added.

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

`.verify-floor.json` has been found BELOW reality four times -- 21 low at the worst, then 8,
then 1, then 2 -- and each was hand-raised after the fact by whoever happened to look. The
stale number was never the defect. `_floor_problems` fired only on `collected < floor`, so a
PR could add tests and never touch the file: silently green, with the floor left permissive
by exactly the number added. #34 and #37 each did precisely that, which is what PR #50 then
had to clean up by hand.

`collected` is now an equality. Too few tests still fails; too many fails as well, printing
the two integers to write and telling you to rebase first.

That also makes the concurrent case self-enforcing, which is the part discipline could not
fix. Once every test-adding branch must edit these same two lines, two concurrent branches
CONFLICT IN GIT -- the second cannot merge without rebasing onto the first and re-measuring
on the actual merge result. This change demonstrated that on itself TWICE inside an hour:
#42 landed underneath it (floor 368 -> 387, ceiling 24 -> 26) and then #59 did (387 -> 391),
and each conflict forced a rebase and a fresh measurement. Under the old one-directional
rule both would have merged green with a floor below reality. Git's own conflict detection
is what enforces "measure on the merge result, not on the branch", the rule the note in that
file had to restate three times with nothing behind it.

`passed` deliberately stays a MINIMUM on passed+skipped. Only collection is
machine-invariant -- a skipped test is still collected, measured the same day at 368 on both
CI and this machine with pass/skip splits of 344/24 against 368/0. Making that one strict
too would fail a machine for honestly naming a missing prerequisite.

Two further fixes in the same change:

* `--update-floor` no longer REPLACES the note. It appends. The note is the only record of
  which prerequisite justifies each ceiling, so overwriting it destroyed the rationale on
  every use -- the file had to carry a warning about its own tool.
* Drift does not block `--update-floor`. The first draft made it a latched gate: a floor
  behind reality became a problem, and the guard was `not problems`, so the one remedy the
  error message named was refused for the existence of the very condition it clears.
  `_blocks_floor_update` lets drift through while real failures still block, sharing
  `DRIFT_PREFIX` so message and predicate cannot diverge.

Selftests cover both directions, skip-invariance, the unset-floor case, the unblock
predicate and note preservation, each with a deliberate-break demonstration. Two of those
tests were themselves defective and the break demo is what caught it: the latched-gate
assert exercised `_blocks_floor_update` in isolation and stayed green when the CALL SITE was
reverted (built-but-not-wired), and the source-text assert written to fix that searched for
a literal that appears in its own line, so it could never fail. The needle is now built from
two fragments and the wiring is asserted at the guard.

Verified on the merge result: 391 passed, 0 failed, 0/26 skipped, 391 collected (floor 391),
84/84 selftests, 43/43 can-fire, 5/5 gates. Ceilings untouched at 26/7/2; floor stays at
main's 391, since this adds selftest assertions rather than pytest tests. black clean at
line-length 100; ruff at main's baseline of 6 pre-existing findings, none added.

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

`.verify-floor.json` has been found BELOW reality four times -- 21 low at the worst, then 8,
then 1, then 2 -- and each was hand-raised after the fact by whoever happened to look. The
stale number was never the defect. `_floor_problems` fired only on `collected < floor`, so a
PR could add tests and never touch the file: silently green, with the floor left permissive
by exactly the number added. #34 and #37 each did precisely that, which is what PR #50 then
had to clean up by hand.

`collected` is now an equality. Too few tests still fails; too many fails as well, printing
the two integers to write and telling you to rebase first.

That also makes the concurrent case self-enforcing, which is the part discipline could not
fix. Once every test-adding branch must edit these same two lines, two concurrent branches
CONFLICT IN GIT -- the second cannot merge without rebasing onto the first and re-measuring
on the actual merge result. This change demonstrated that on itself repeatedly: #42 landed
underneath it (floor 368 -> 387, ceiling 24 -> 26, verify.py reformatted), then #59
(387 -> 391), then #61, all in one afternoon. Each conflict forced a rebase and a fresh
measurement; under the old one-directional rule each would have merged green with a floor
below reality. Git's own conflict detection is what enforces "measure on the merge result,
not on the branch", the rule the note in that file had to restate three times with nothing
behind it.

`passed` deliberately stays a MINIMUM on passed+skipped. Only collection is
machine-invariant, measured across machines at 391 collected on both, with pass/skip splits
of 365/26 on CI against 391/0 locally. Making that one strict too would fail a machine for
honestly naming a missing prerequisite.

Two further fixes in the same change:

* `--update-floor` no longer REPLACES the note. It appends. The note is the only record of
  which prerequisite justifies each ceiling, so overwriting it destroyed the rationale on
  every use -- the file had to carry a warning about its own tool.
* Drift does not block `--update-floor`. The first draft made it a latched gate: a floor
  behind reality became a problem, and the guard was `not problems`, so the one remedy the
  error message named was refused for the existence of the very condition it clears.
  `_blocks_floor_update` lets drift through while real failures still block, sharing
  `DRIFT_PREFIX` so message and predicate cannot diverge.

Selftests cover both directions, skip-invariance, the unset-floor case, the unblock
predicate and note preservation, each with a deliberate-break demonstration. Two of those
tests were themselves defective and only the break demo caught it: the latched-gate assert
exercised `_blocks_floor_update` in isolation and stayed green when the CALL SITE was
reverted (built-but-not-wired), and the source-text assert written to fix that searched for
a literal that appears in its own line, so it could never fail. The needle is now built from
two fragments and the wiring is asserted at the guard.

Verified on the merge result: 391 passed, 0 failed, 0/26 skipped, 391 collected (floor 391),
84/84 selftests, 43/43 can-fire, 5/5 gates. Ceilings untouched at 26/7/2; the floor stays at
main's 391, since this adds selftest assertions rather than pytest tests. black clean at
line-length 100; ruff at main's baseline of 6 pre-existing findings, none added.

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

`.verify-floor.json` has been found BELOW reality four times -- 21 low at the worst, then 8,
then 1, then 2 -- and each was hand-raised after the fact by whoever happened to look. The
stale number was never the defect. `_floor_problems` fired only on `collected < floor`, so a
PR could add tests and never touch the file: silently green, with the floor left permissive
by exactly the number added. #34 and #37 each did precisely that, which is what PR #50 then
had to clean up by hand.

`collected` is now an equality. Too few tests still fails; too many fails as well, printing
the two integers to write and telling you to rebase first.

That also makes the concurrent case self-enforcing, which is the part discipline could not
fix. Once every test-adding branch must edit these same two lines, two concurrent branches
CONFLICT IN GIT -- the second cannot merge without rebasing onto the first and re-measuring
on the actual merge result. This change demonstrated that on itself, at length: six merges
landed on main during the two hours it took to write, moving the floor 368 -> 387 -> 391 ->
402 and the ceiling 24 -> 26. Under the old one-directional rule every one of them would
have left the floor below reality and merged green. Git's own conflict detection is what
enforces "measure on the merge result, not on the branch", the rule the note in that file
had to restate three times with nothing behind it.

`passed` deliberately stays a MINIMUM on passed+skipped. Only collection is
machine-invariant, measured across machines at 391 collected on both, with pass/skip splits
of 365/26 on CI against 391/0 locally. Making that one strict too would fail a machine for
honestly naming a missing prerequisite.

Two further fixes in the same change:

* `--update-floor` no longer REPLACES the note. It appends. The note is the only record of
  which prerequisite justifies each ceiling, so overwriting it destroyed the rationale on
  every use -- the file had to carry a warning about its own tool.
* Drift does not block `--update-floor`. The first draft made it a latched gate: a floor
  behind reality became a problem, and the guard was `not problems`, so the one remedy the
  error message named was refused for the existence of the very condition it clears.
  `_blocks_floor_update` lets drift through while real failures still block, sharing
  `DRIFT_PREFIX` so message and predicate cannot diverge.

Selftests cover both directions, skip-invariance, the unset-floor case, the unblock
predicate and note preservation, each with a deliberate-break demonstration. Two of those
tests were themselves defective and only the break demo caught it: the latched-gate assert
exercised `_blocks_floor_update` in isolation and stayed green when the CALL SITE was
reverted (built-but-not-wired), and the source-text assert written to fix that searched for
a literal that appears in its own line, so it could never fail. The needle is now built from
two fragments and the wiring is asserted at the guard.

Verified on the merge result: 402 passed, 0 failed, 0/26 skipped, 402 collected (floor 402),
84/84 selftests, 43/43 can-fire, 5/5 gates. Ceilings untouched at 26/7/2; the floor stays at
main's 402, since this adds selftest assertions rather than pytest tests. black clean at
line-length 100.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
stranske pushed a commit that referenced this pull request Aug 23, 2026
22 of 43 capabilities were bound to NO surface, so nothing could ever offer
them and no amount of running produced evidence: the gate starving its own
drain, one layer below the concealment rule the table already guards.

DEDUP FINDING (CLAUDE.md §0). Searched "binding", "surface", "SURFACE_BINDINGS",
"consult", "NO_BINDING", "phase" across the tree and the improvement log. The
MECHANISM all exists — `binding_for()`'s prefix resolution already sub-surfaces
`repo-audit` by phase and dimension; `capability_propensity.tick_evidence`
(PR #37) is already a tick-surface consult; the skills already carry a uniform
consult stanza; `verify.py` already has the diagnostic-line pattern
(`absent_entrypoint_line`). So this is DATA + CALLERS: no second binding table,
no second event log, no second consult mechanism.

1. THE TICK IS SUB-SURFACED BY PHASE, and every phase has a caller. 18 of the 43
   capabilities live on the tick; one context holding 18 is the measured 13.62%
   selection condition. Five phases — `tick:capacity`, `tick:dispatch`,
   `tick:experiments`, `tick:redirect`, `tick:learning` — named from
   orchestrate.sh's own "capacity -> discover -> plan -> dispatch" line and its
   `--- Learning cadence ---` / `[cadence] redirect ...` /
   `[cadence] experiment follow-up` blocks, with most bound capabilities
   carrying a `{kind: tick_phase, name: ...}` matcher naming the phase they land
   in. Each resolves to 6-8 instead of 18.

   THE BARE `tick` SET DOES NOT MOVE. `capability_propensity.TICK_SURFACE` is
   "tick", `tick_evidence()` grades exactly `binding_for("tick")`, and its
   selftest requires every capability with a finding projection to be in it —
   moving those four into a phase would silently zero the only producer of
   layer-2 evidence. A selftest now pins that, and pins that each phase declares
   something the bare set does not.

   The caller is `capability_advisor.py --consult-tick-phases` at
   `ORCH-ANCHOR: tick-phase-consult`, immediately below the #37 step and below
   `ORCH-ANCHOR: heartbeat-export`. It writes NO verdicts — `tick_evidence`
   remains the only writer and reads an unchanged binding, so its ~1.3/day
   ceiling is untouched. Its own volume is bounded by construction: the consult
   text is stable per (surface, UTC day) and the match heartbeat is idempotent on
   its digest, so 34 events land on the first tick of a day and 0 on the other
   23. Fails open per phase, SIGALRM-bounded, always exits 0. Kill switch:
   ORCH_DISABLE_STEPS=tick-phase-consult (registered in cadence_registry.py, so
   the switch is real rather than a no-op that WARNs).

2. `verify.py` IS THE `ci` SURFACE AND NOW CONSULTS AS ONE. Three capabilities
   were declared for a surface no caller reached. verify.py runs on every PR and
   already executes the admission gate. The line reports both quantities — what
   the table DECLARES (identical on every machine) beside what this machine's
   ledger could OFFER — plus the findability pair (rows bound to some surface /
   rows bound to none). record=False: a verifier must not write to the ledger its
   own gates read. Never a skip (no PREREQ_ABSENT_MARK), never in `problems`, so
   exit semantics and every reported count are unchanged.

3. THE SURFACE WAS DROPPED ON THE BINDING-ONLY BRANCH. `advise()` passed only
   `skill` to `_record_matches` there, so a `--surface` consult that missed the
   classifier wrote surface:null AND skill:null — the candidate set recorded and
   belonging to nobody, so no drain could locate it. That is the branch an
   unclassifiable cadence consult ALWAYS takes.

4. AN UNSUBSTITUTED SURFACE TEMPLATE NOW SAYS SO. `binding_for` resolves by
   prefix, so the literal `repo-audit:phase-N` returned `repo-audit`'s
   surface-wide set — one capability where the phase declares four — with
   nothing to distinguish it from a correct answer. Three audits under identical
   instructions consulted 13, 9 and 2 distinct surfaces. `surface_template` is
   reported on all four answer paths and printed loudly; it changes neither the
   set nor the order.

Also bound: role-adjudicator → implementation-verification, role-prompt →
file-agent-issue, role-{redirect,prompt} + agy-runtime-isolation → orchestrate,
redirect-{policy,plan} → closer-lane (not the opener: the opener's own prompt
raises `drain_needed` and relays a stalled PR to the closer),
capability:reference-sync-hygiene-test-gate → ci.

Unbound is now 1: `local-model-profile-trial`, declared with `NO_BINDING` and its
quarantine reason rather than left silently absent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
stranske added a commit that referenced this pull request Aug 23, 2026
…77)

* feat(advisor): give every declared binding a caller — 22 unbound → 1

22 of 43 capabilities were bound to NO surface, so nothing could ever offer
them and no amount of running produced evidence: the gate starving its own
drain, one layer below the concealment rule the table already guards.

DEDUP FINDING (CLAUDE.md §0). Searched "binding", "surface", "SURFACE_BINDINGS",
"consult", "NO_BINDING", "phase" across the tree and the improvement log. The
MECHANISM all exists — `binding_for()`'s prefix resolution already sub-surfaces
`repo-audit` by phase and dimension; `capability_propensity.tick_evidence`
(PR #37) is already a tick-surface consult; the skills already carry a uniform
consult stanza; `verify.py` already has the diagnostic-line pattern
(`absent_entrypoint_line`). So this is DATA + CALLERS: no second binding table,
no second event log, no second consult mechanism.

1. THE TICK IS SUB-SURFACED BY PHASE, and every phase has a caller. 18 of the 43
   capabilities live on the tick; one context holding 18 is the measured 13.62%
   selection condition. Five phases — `tick:capacity`, `tick:dispatch`,
   `tick:experiments`, `tick:redirect`, `tick:learning` — named from
   orchestrate.sh's own "capacity -> discover -> plan -> dispatch" line and its
   `--- Learning cadence ---` / `[cadence] redirect ...` /
   `[cadence] experiment follow-up` blocks, with most bound capabilities
   carrying a `{kind: tick_phase, name: ...}` matcher naming the phase they land
   in. Each resolves to 6-8 instead of 18.

   THE BARE `tick` SET DOES NOT MOVE. `capability_propensity.TICK_SURFACE` is
   "tick", `tick_evidence()` grades exactly `binding_for("tick")`, and its
   selftest requires every capability with a finding projection to be in it —
   moving those four into a phase would silently zero the only producer of
   layer-2 evidence. A selftest now pins that, and pins that each phase declares
   something the bare set does not.

   The caller is `capability_advisor.py --consult-tick-phases` at
   `ORCH-ANCHOR: tick-phase-consult`, immediately below the #37 step and below
   `ORCH-ANCHOR: heartbeat-export`. It writes NO verdicts — `tick_evidence`
   remains the only writer and reads an unchanged binding, so its ~1.3/day
   ceiling is untouched. Its own volume is bounded by construction: the consult
   text is stable per (surface, UTC day) and the match heartbeat is idempotent on
   its digest, so 34 events land on the first tick of a day and 0 on the other
   23. Fails open per phase, SIGALRM-bounded, always exits 0. Kill switch:
   ORCH_DISABLE_STEPS=tick-phase-consult (registered in cadence_registry.py, so
   the switch is real rather than a no-op that WARNs).

2. `verify.py` IS THE `ci` SURFACE AND NOW CONSULTS AS ONE. Three capabilities
   were declared for a surface no caller reached. verify.py runs on every PR and
   already executes the admission gate. The line reports both quantities — what
   the table DECLARES (identical on every machine) beside what this machine's
   ledger could OFFER — plus the findability pair (rows bound to some surface /
   rows bound to none). record=False: a verifier must not write to the ledger its
   own gates read. Never a skip (no PREREQ_ABSENT_MARK), never in `problems`, so
   exit semantics and every reported count are unchanged.

3. THE SURFACE WAS DROPPED ON THE BINDING-ONLY BRANCH. `advise()` passed only
   `skill` to `_record_matches` there, so a `--surface` consult that missed the
   classifier wrote surface:null AND skill:null — the candidate set recorded and
   belonging to nobody, so no drain could locate it. That is the branch an
   unclassifiable cadence consult ALWAYS takes.

4. AN UNSUBSTITUTED SURFACE TEMPLATE NOW SAYS SO. `binding_for` resolves by
   prefix, so the literal `repo-audit:phase-N` returned `repo-audit`'s
   surface-wide set — one capability where the phase declares four — with
   nothing to distinguish it from a correct answer. Three audits under identical
   instructions consulted 13, 9 and 2 distinct surfaces. `surface_template` is
   reported on all four answer paths and printed loudly; it changes neither the
   set nor the order.

Also bound: role-adjudicator → implementation-verification, role-prompt →
file-agent-issue, role-{redirect,prompt} + agy-runtime-isolation → orchestrate,
redirect-{policy,plan} → closer-lane (not the opener: the opener's own prompt
raises `drain_needed` and relays a stalled PR to the closer),
capability:reference-sync-hygiene-test-gate → ci.

Unbound is now 1: `local-model-profile-trial`, declared with `NO_BINDING` and its
quarantine reason rather than left silently absent.

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

* fix(verify): bound the ci consult's ledger wait, and format at the pinned width

TWO REAL DEFECTS in the ci consult as first written.

1. IT COULD HANG THE VERDICT. The consult reads the ledger and
   `capabilities.load()` takes a BLOCKING flock. Unbounded, a stuck lock would
   hang `verify.py` — the project's verdict — indefinitely. Not hypothetical: it
   actually timed out during this session, with a dozen concurrent verify runs
   and the hourly tick contending for that lock. Now SIGALRM-bounded at 20s, the
   same shape and the same reason as `capability_propensity.tick_evidence_guarded`;
   a wait that expires becomes the `NOT CHECKED` line the guard already produced
   for an import failure, so a hang is a reported line and never a hung run. The
   line still carries no `PREREQ_ABSENT_MARK`, so a timeout is not a skip and
   cannot spend the zero-headroom ceiling. Selftested by substituting the inner
   call, with the healthy path re-asserted so the guard cannot swallow it.

2. `black -l 100` WOULD HAVE REFORMATTED IT. `ruff.toml` records that the Gate's
   format check and Autofix both run `black --line-length 100`; main is clean
   under it and these two files were not, so the Gate would have gone red and
   Autofix would have rewritten them. Reformatted at the pinned width — a bare
   `black` (default 88) disagrees with the Gate and is the wrong instrument here.

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

* fix(verify): the ci-consult selftest asserted against the live ledger lock

`assert "NOT CHECKED" not in ci_consult_line()` was meant to prove the guard
does not swallow a healthy answer. It asserted it through the REAL ledger, so it
went red the same afternoon it was written: a dozen concurrent verify runs and
the hourly tick were contending for the flock, the 20s budget expired, and the
guard did exactly its job — failing a test written to prove the guard works.

A machine-dependent assertion inside a check about robustness is this repo's
green-locally/red-on-CI defect one level up, and I was warned about it twice.

Both directions now go through a SUBSTITUTED inner call — a raising one for the
timeout branch, a known-good one for the healthy branch — so neither depends on
what the ledger lock is doing. The real call is still exercised, but only on the
SHAPE both branches satisfy (starts with the label, carries no
PREREQ_ABSENT_MARK, so a timeout can never be miscounted as a skip).

Break→revert, both discriminating: making the guard return NOT CHECKED for a
healthy inner call fails the new healthy assertion; deleting the except clause
lets the TimeoutError escape and the selftest dies on it.

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

* fix(advisor): drop the ci consult — #68 showed the surface has nothing to select

Two sessions reached opposite conclusions about `ci` and only one can ship.

I instructed the binding work to add a `surface: "ci"` consult to verify.py on the
reasoning that verify.py runs on every PR and therefore IS the CI surface. The
findability requirement (#68), landing concurrently, showed that reasoning is
wrong: `capability-admission-gate` and `docs-drift-fix-agent` are invoked
UNCONDITIONALLY by a rail. They are never OFFERED to a reasoning context, so no
binding could raise their selection odds and no consult could change what runs —
verify.py does not CHOOSE to run the admission gate. Both now declare
`findability_category: no_surface`, which is the honest statement, and `ci` is
NO_BINDING with that reason.

Shipping both would have left the merged tree self-contradictory: the `ci` reason
says "no caller anywhere consults a `ci` surface — not verify.py" on the same
commit that adds a verify.py consult.

So the consult is removed (185 lines) and #68's verdict kept. Two further reasons
to prefer that direction: a consult against a suppressed surface returns
`confidence: "suppressed"` and an empty set on every run, so it was a no-op in the
one script whose exit semantics and printed counts CI parses; and it had already
needed two follow-up commits to bound a ledger wait and to stop a selftest
asserting against the live ledger lock. That is real risk for no signal.

Kept from the binding work, unchanged: the five tick sub-surfaces
(tick:capacity/dispatch/experiments/learning/redirect) with their consult sites,
the lane `--surface` fix, and the NO_BINDING verdict for
`local-model-profile-trial`.

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

* fix(advisor): declare the tick phase consults, and retire the lane defect record

Integration between two concurrent sessions, and #68's ninth requirement caught it
within minutes of landing — which is the first thing it did.

The binding work added five tick sub-surfaces and a real in-tree consult
(`ORCH-ANCHOR: tick-phase-consult` iterating `tick_phase_surfaces()`, below the
heartbeat export). #68 added `CONSULT_SITES`, which requires every bound surface to
name a caller that can be VERIFIED — the caller file must actually contain the
declared literal. Neither branch knew about the other, so all five landed bound and
undeclared, and the findability selftest failed with them named.

Declared as a FAMILY, not five entries. `tick_phase_surfaces()` derives the phases
from `TICK_PHASE_PREFIX`, so no caller names them literally and a per-surface entry
could never be verified — that is exactly what `instances` exists for. One claim,
five instances, caller `capability_advisor.py` with literal `TICK_PHASE_PREFIX`.

Also retires both `KNOWN_UNCONSULTED` entries. #68 independently found the same
defect I did — the opener and closer TOMLs consulted with `--lane` and no
`--surface`, so `binding_for("")` returned {} and eleven bindings never reached the
two highest-volume surfaces — and recorded it rather than patching it, correctly,
since CLAUDE.md forbids a loop that edits lane prompts. The TOMLs now carry
`--surface`, re-rendered and verified (bound_count 0 -> 5 and 0 -> 6), so the record
is stale. The table is kept EMPTY rather than deleted, so the next stranded surface
has an obvious home and the record shows the defect existed.

Two mistakes of mine while resolving this, both caught before commit: a regex
scoped to the file rather than to `CONSULT_SITES` deleted the tick bindings out of
`SURFACE_BINDINGS` (caught by `tick_phase_surfaces()` returning `['tick:*']`), and
the first attempt declared five per-surface entries the verifier could never
confirm.

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

* style: black at the pinned width (100)

The  list was hand-wrapped; black wants one element per line. The Gate's
lint-format job runs black, not ruff format, and the two disagree on assert-style
wrapping — black is the authority here per ruff.toml's own note.

---------

Co-authored-by: Tim Stranske <tim@stranskemo.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@stranske
stranske deleted the claude/tick-capability-evidence branch August 23, 2026 22:37
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