fix(pty-runner): active-render gate veto in the floor check + honest periodic-wiring docs

Codex re-review P2s on the fix wave, both verified:

- A finding AUQ rendering within TAIL_SCAN_BYTES of the gate (model waiting,
  no further output) was vetoed by the blanket tail exclusion until timeout.
  The veto is now ACTIVE-RENDER-aware: parseNumberedOptions anchors the last
  cursor menu, so only a pending GATE menu vetoes; the judge fallback shares
  the same check. Residual (documented): prose gate + prose finding inside
  one tail — floors run the native-menu path in practice.
- The four demoted periodic tests are not in evals-periodic.yml's explicit
  matrix (a named instance of the pre-existing periodic-orphans TODO), so
  they run locally/manually until the PTY-capable periodic job lands.
  CHANGELOG claim softened accordingly; TODO filed with the wiring recipe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry TanandClaude Fable 5 committed 2026-08-12 09:19:00 -07:00
1 parent bd8c1f64b3
commit 3d2dd4a33f
4 files changed
+59 -7

No files matched your search

+19 -5
View File
@@ -2260,14 +2260,27 @@ export async function runPlanSkillFloorCheck(opts: {
// gate render has been seen, acceptance scans only the content APPENDED
// after it (positional anchor above) — the buffer is append-only, so a
// whole-buffer acceptance would keep matching the stale gate render
// forever. The tail exclusion additionally covers the window where the
// gate menu is still the active render.
// forever.
//
// The gate veto is ACTIVE-RENDER-aware, not blanket-tail: when a
// numbered menu is up, parseNumberedOptions anchors on the LAST cursor
// line, so we veto only when the pending menu IS the gate — a finding
// AUQ that renders within TAIL_SCAN_BYTES of the gate (model waiting,
// no further output) still satisfies the floor. Prose renders have no
// cursor anchor, so the prose path falls back to the tail check
// (accepted residual: prose gate + prose finding inside one tail can
// suppress until timeout; floors run the native-menu path in practice).
const tail = visible.slice(-TAIL_SCAN_BYTES);
const acceptWindow = gateSeenIdx === -1 ? visible : visible.slice(gateSeenIdx);
const activeMenu = parseNumberedOptions(visible);
const gateIsActiveRender =
activeMenu.length > 0
? activeMenu.some((o) => /current\s*branch\s*diff/i.test(o.label))
: isScopeGateQuestionVisible(tail);
if (
(isNumberedOptionListVisible(acceptWindow) || isProseAUQVisible(acceptWindow)) &&
!isPermissionDialogVisible(tail) &&
!isScopeGateQuestionVisible(tail)
!gateIsActiveRender
) {
return {
auqObserved: true,
@@ -2291,8 +2304,9 @@ export async function runPlanSkillFloorCheck(opts: {
lastJudgeVerdict = judgePtyState(visible, { testName: opts.skillName });
// The judge can't tell a scope-gate question from a finding question,
// so a 'waiting' verdict while the gate menu is the pending render
// must NOT satisfy the floor — same exclusion as the regex path.
if (lastJudgeVerdict.state === 'waiting' && !isScopeGateQuestionVisible(tail)) {
// must NOT satisfy the floor — same active-render exclusion as the
// regex path.
if (lastJudgeVerdict.state === 'waiting' && !gateIsActiveRender) {
return {
auqObserved: true,
outcome: 'auq_observed',