Skip to content

fix(ci): say WHAT KIND of red ejected a pull request, not just which run (BACKLOG #1385) - #873

Open
wshallwshall wants to merge 5 commits into
mainfrom
worktree-agent-af15283fafbd1cc53
Open

fix(ci): say WHAT KIND of red ejected a pull request, not just which run (BACKLOG #1385)#873
wshallwshall wants to merge 5 commits into
mainfrom
worktree-agent-af15283fafbd1cc53

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

BACKLOG #1385 -- a merge-queue red now says WHAT KIND of red it is

The brief for this Builder named two deliverables. Both had already landed in #789 (8d16c0b2a,
an ancestor of origin/main): the test_api_request_timeout.py deadline is fixed, and
scripts/ci/report_ci_red.py reads the ci-red label back. Re-reading them first is what the brief
asked for, and it is what redirected this work.

What was still missing is one level further on. The reader named the RUN and stopped. A run does
not say what KIND of red it was, and the kinds need different answers.

The finding, measured today

Read from the Actions API on 2026-09-04: eight failing merge_group CI runs across seven pull
requests, in one day.
PR 669 was not an unlucky change.

run id PR failing job
10:18:36Z 33862605516 769 web console tests (windows-2025)
10:18:37Z 33862607354 770 test (windows-2025)
15:04:34Z 33887390485 760 sql server (store + connector) 2022
16:23:15Z 33894838123 810 test (windows-2025)
16:56:20Z 33897877945 810 sql server (store + connector) 2025
17:12:36Z 33899333774 780 test (windows-2022)
17:54:25Z 33903044674 801 web console tests (windows-2025)
21:24:24Z 33920943852 771 test (windows-2022)

PR 810 failed twice, with a different job each time -- 669's signature, on a different PR six
days later.

Two of the eight are not test failures. Both web console legs failed the step
Step margin -- web console suite while Web console tests (pytest) concluded SUCCESS with
405 passed, 3 skipped, 2 warnings in 276.30s. The brief called this a reporting defect. It is not
quite: step_margin.py already says exactly the right thing, in its own step log --

LOW: the 'Web console tests (pytest)' step CONCLUDED SUCCESS -- nothing it runs failed. This
check is a TIMING gate, and it is what reddened this leg: 4:46 of a 6:00 cap (79.3% of the
cap, margin 1.261x, floor 1.30x)

The instrument was right and nothing carried its answer to a reader. On a merge_group run the
PR page shows none of it, and the ci-red report named only the run, so the one sentence that
answers the question sat behind two clicks nobody knew to make.

What this changes

report_ci_red.py now names the failing job and step, marks a timing gate as one, and applies
two attribution rules that the naive read gets wrong:

  • The CI gate roll-up is never named as the cause. It fails in all eight runs above and its own
    failing step is Fail -- a gated leg FAILED, which points at a leg it does not name. If it is the
    only failing job it is still reported, because silence would render a red as unattributed.
  • TIMING GATE needs both halves -- a watchdog step name and a work step that concluded success.
    Every pytest step in ci.yml carries an if: on the change filter, so a SKIPPED suite can leave
    the margin step as the first failure, and "the suite PASSED" would then describe a suite that never
    ran.

It also now says when an UNATTRIBUTED line is truncation rather than absence. The runs query
asks for one page and does not paginate; run live it read scanned 100 run(s) and returned eight
UNATTRIBUTED lines blaming "aged out". That is #1385's own recorded trap one level down, and the
report was reproducing it while reporting on it.

Live proof, same afternoon

::warning::#828 build(deps): bump the python-deps group -- CI failed in web console tests
(windows-2025, py3.14) / Step margin -- web console suite [TIMING GATE -- the suite PASSED; a
watchdog reddened the leg, so a re-queue cannot help. Read the step log for the margin, not the
test list] https://github.com/.../runs/33906669992

Before this branch that line was CI failed and a URL.

The second cluster, verified rather than inherited

The Lander reported tests/test_worktree_gate_control_plane.py flaking across unrelated PRs. Read
from the job logs:

PR test files it changed
833 test_the_launch_timeout_diagnostic_does_not_fire_on_an_ordinary_denial messagefoundry/redaction.py, its test, BACKLOG.md
783 test_a_repository_token_owned_by_an_EARLIER_command_is_not_the_disarm_s_target review-gate.yml, CI.md, two test files, BACKLOG.md

Neither diff can reach what failed. A third red on that job is excluded: PR 799's was
test_dangling_citation_check, a real content check. Two verified beats three asserted. Whether
these two are #1304's pwsh-launch mechanism is not established here -- that diagnostic fires only
on TimeoutExpired and neither log was read far enough to say. Recorded in the item, unallocated,
named by subject.

What I did NOT fix, said plainly

  1. The web-console margin baseline has rotted. step_margin_baseline.toml records 3:59
    (2026-08-08, n=360, right-censored) and a passing run today took 4:46. step_margin.py
    says raising the cap is not the fix; re-sizing needs a run population one Builder cannot gather.
  2. The SQL Server arm -- two of today's eight, both OS legs again. Unobservable off the gated CI
    leg. No claim made.
  3. Nothing calls report_ci_red.py. The label is read, but only by someone who chooses to.

Correction carried into the item

The 2026-09-03 re-score says nothing in this tree reads the ci-red label. True when written, false
the same day -- #789. Corrected in both places it appears. CLAUDE.md:302 and docs/METHOD.md:365
are stale on the same point; named, not edited -- not this branch's file to move.

Checks

check result
ruff check pass, both files
ruff format --check pass, both files
mypy pass, no issues in 2 source files
pytest tests/test_ci_red_reader.py 30 passed
pytest backlog citation + status + hygiene + ci-red 95 passed
pytest tests/test_dangling_citation_check.py 45 passed
backlog_status_check.py OK -- 664 items, each declaring exactly one status
full suite NOT RUN -- deliberately, per the brief's contention warning
windows-service-smoke, the gated SQL Server leg hosted-runner only; never seen from here

Interpreter receipt, as asked:

$ .venv/Scripts/python.exe -c "import messagefoundry; print(messagefoundry.__file__)"
C:\Users\Scott\Code\MessageFoundry\.claude\worktrees\agent-af15283fafbd1cc53\messagefoundry\__init__.py

It read this worktree.

Mutation pairs, one variable each

mutation result
empty _ROLLUP_JOBS 2 failed, 22 passed
drop the work_step_passed half of is_timing_gate 24 passed -- INVISIBLE
...after adding the SKIPPED-suite arm 1 failed, 24 passed
narrow the work-step suffix to one name 1 failed, 29 passed

The second row changed the change, and is the honest part of this PR. The first version of that
test asserted the condition through a job where Tests (pytest) FAILED beside the margin step -- and
step ordering already handles that, because a failed test step sorts before the watchdog measuring
it. The arm proved nothing about the condition it was written for, and the docstring justified it
with a scenario that ordering makes unreachable. The reachable case is a SKIPPED suite. Both were
rewritten.

Open question for whoever picks this up

report_ci_red.py has no caller. Wiring it into a workflow is the obvious next step and is
deliberately not done here: the label is written by failure-signal.yml on a merge_group
failure, and where the read belongs -- that workflow, stalled-prs.yml's cron, or a new one -- is a
scheduling decision this Builder cannot make in one turn without a brief. Not allocated a number; a
citation to an unissued number resolves to nothing today and to unrelated work later.

Not labelled. Not enqueued.

🤖 Generated with Claude Code

wshallwshall and others added 5 commits September 4, 2026 17:43
…he run (BACKLOG #1385)

The reader landed in #789 and names the RUN. That does not say what KIND of red it was, and the
kinds need different answers: a failed assertion wants a fix, and a watchdog verdict on a suite
that passed cannot be helped by a re-queue at all.

MEASURED 2026-09-04 on this repository, read from the Actions API. EIGHT merge_group CI failures
across SEVEN pull requests in one day:

  10:18:36Z  33862605516  pr-769  web console tests (windows-2025)
  10:18:37Z  33862607354  pr-770  test (windows-2025)
  15:04:34Z  33887390485  pr-760  sql server (store + connector) 2022
  16:23:15Z  33894838123  pr-810  test (windows-2025)
  16:56:20Z  33897877945  pr-810  sql server (store + connector) 2025
  17:12:36Z  33899333774  pr-780  test (windows-2022)
  17:54:25Z  33903044674  pr-801  web console tests (windows-2025)
  21:24:24Z  33920943852  pr-771  test (windows-2022)

PR 810 failed TWICE with a DIFFERENT job each time, which is PR 669's signature reproduced on a
different pull request six days later.

TWO of the eight are not test failures at all. Both web console legs failed the step
`Step margin -- web console suite` while `Web console tests (pytest)` CONCLUDED SUCCESS with
`405 passed, 3 skipped, 2 warnings in 276.30s`. step_margin.py's own log says it plainly:

  LOW: the 'Web console tests (pytest)' step CONCLUDED SUCCESS -- nothing it runs failed. This
  check is a TIMING gate, and it is what reddened this leg: 4:46 of a 6:00 cap (79.3% of the cap,
  margin 1.261x, floor 1.30x)

The instrument was already right. Nothing carried it to a reader. On a merge_group run the PR page
shows none of it, and the ci-red report named only the run, so the one sentence that answers the
question sat in a step log behind two clicks nobody knew to make.

WHAT THIS ADDS: the failing job and step on every attributed line, plus a TIMING GATE marker when a
watchdog step reddened a job whose own work step passed.

TWO ATTRIBUTION RULES, both because the naive read misreports:

  - A roll-up job is never named as the cause. `CI gate` fails in every one of the eight runs above
    and its failing step is `Fail -- a gated leg FAILED`, which points at a leg it does not name.
    Naming it would send every reader to the one job whose log cannot hold the answer. If a roll-up
    is the ONLY failing job it is still reported, because silence would render as unattributed.
  - TIMING GATE requires BOTH a watchdog step name AND a work step that concluded success. A margin
    step can fire alongside a genuine failure, and there "nothing failed" is false.

The live job fetch is one request per labelled pull request, for the run the report will actually
name -- not one per scanned run. `--runs-json` suppresses it: joining saved run ids against the
live repository is not a smaller answer, it is a wrong one.

Not fixed here, and stated rather than implied: the margin baseline for this leg has rotted. The
recorded maximum is 3:59 and today's passing run took 4:46, so the cap is sized against a smaller
suite. step_margin.py says raising the cap is not the fix, and re-sizing it needs a run population
this Builder cannot gather. Also unobserved: the SQL Server arm, provable only on the gated CI leg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… #1385)

Eight new arms over `blame_job` and the CLI join. The payload in
`_REAL_TIMING_GATE_JOB` is transcribed from run 33903044674's
`web console tests (windows-2025, py3.14)` job as the Actions API reported it on 2026-09-04, not
invented: the pytest step concluded `success` and `Step margin -- web console suite` reddened the
leg.

THE MUTATION PAIR, one variable each.

  mutation A -- empty `_ROLLUP_JOBS`, so the roll-up can be blamed:
    2 failed, 22 passed. `test_the_rollup_job_is_not_named_as_the_cause` and
    `test_the_rollup_match_survives_a_matrix_suffix` both name it.

  mutation B -- drop the `work_step_passed` half of `is_timing_gate`:
    FIRST ATTEMPT: 24 passed. COMPLETELY INVISIBLE.

Mutation B is recorded because it changed the change. The first version of these tests asserted the
second condition through a job where `Tests (pytest)` FAILED alongside the margin step -- and step
ordering already handles that case, because a failed test step sorts before the watchdog measuring
it and is the step named. So the arm proved nothing about the condition it was written for, and the
justification in the docstring described a scenario that ordering makes unreachable.

The case that IS reachable is a SKIPPED work step. Both pytest steps in `ci.yml` carry an `if:` on
the change filter, so the suite can be skipped while the margin step still reds, and "the suite
PASSED" is then a claim about a suite that never ran. With that arm added, mutation B reds:
`1 failed, 24 passed`.

Restored: 25 passed. ruff check, ruff format --check and mypy clean on both files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sence (BACKLOG #1385)

Two defects the first live run of the new report exposed, both in its own prose.

ONE. The runs query asks for a single page and does not paginate. Run live against this repository
on 2026-09-04 it read `14 open pull request(s) carry ci-red; scanned 100 run(s)` and returned EIGHT
UNATTRIBUTED lines whose stated cause was "aged out, or the label outlived its run". The page had
FILLED, so the honest cause is that the window was cut off at an unknown depth. This is #1385's own
recorded trap one level down -- the item's notes name an unpaginated per_page=100 returning 100 of
total_count 190 and dropping half the population silently -- and the report was reproducing it while
reporting on it.

It now says so when the page fills, and only then. Rendering "I could not see that far" as "there is
nothing there" is the exact defect class this signal chain exists to close, and a report that does it
about itself has no standing to say it about anything else. The page size is a named constant the
fetch and the caveat share, asserted through the command the fetch actually builds -- a caveat
quoting a stale literal is worse than none, because it reads as measured.

TWO. `1 of these are TIMING GATES` -- the count had no singular form, and one is the common case.

LIVE RESULT after the change, same repository, same afternoon. PR #828 reads:

  CI failed in web console tests (windows-2025, py3.14) / Step margin -- web console suite
  [TIMING GATE -- the suite PASSED; a watchdog reddened the leg, so a re-queue cannot help.
  Read the step log for the margin, not the test list]

Before this branch that line was `CI failed` and a URL. Three further pull requests (#833, #799,
#783) now read as one cluster on `repo harness tests (windows-2025) / Harness tests (pytest)`, which
is the shape the Lander reported by hand this afternoon and which no instrument was grouping.

28 tests pass. Negative control included: a short run list carries no caveat, because a caveat that
fires unconditionally teaches a reader to skip it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ale silently (BACKLOG #1385)

The timing-gate classification asked whether a step that RUNS work passed, against an enumeration
of two names. `ci.yml` has THREE pytest steps -- `Tests (pytest)`, `Web console tests (pytest)` and
`Harness tests (pytest)` -- and the third was missing.

It cost nothing today, because only the first two are paired with a margin step. It would have cost
silence tomorrow. An unrecognised work step reads as "did not pass", so a margin verdict on a suite
the list had not heard of would simply stop being classified, with nothing reporting the gap. That
is a completeness claim degrading into a false negative, which is what SDS-3.6 says to prefer "at
least" over an enumeration for.

Matched by the `(pytest)` suffix instead. One rule, no list.

MUTATION, one variable: narrow the suffix back to a single step name.
  `1 failed, 29 passed` -- `test_every_pytest_step_in_ci_yml_counts_as_work` names the step it lost:
  `'Tests (pytest)' was not recognised as a work step`.

That test reads the step names out of `ci.yml` rather than restating them, so it cannot pass by
agreeing with a stale copy -- the pattern `test_the_watched_workflows_match_the_writer` already uses
against `failure-signal.yml`. Its floor is `>= 3`, not `== 3`, so adding a fourth suite does not red
the leg for an unrelated change.

Negative control added: a passing SETUP step must not count as work. Without it, every margin red
would be labelled "the suite PASSED", including one over a suite that never ran.

30 tests pass. ruff check, ruff format --check and mypy clean.

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

Today's evidence, read from the Actions API on 2026-09-04 rather than reported: EIGHT failing
merge_group CI runs across SEVEN pull requests in one day, each attributed from its own job list.
PR 810 failed twice with a different job each time, which is PR 669's signature reproduced on
another pull request six days later.

TWO OF THE EIGHT ARE NOT TEST FAILURES, and the item had no name for that kind. Both web console
legs failed `Step margin -- web console suite` while `Web console tests (pytest)` concluded SUCCESS
with `405 passed, 3 skipped`. A red with zero failing assertions wants a different answer from a
flaky test: re-queueing a timing verdict cannot help, and each re-queue costs a full CI cycle, which
is this row's own severity sentence.

A SECOND CLUSTER, verified rather than inherited. tests/test_worktree_gate_control_plane.py failed
on PRs 833 and 783 in TWO DIFFERENT tests, and neither diff touches the gate script or any
test_worktree_gate file. A third red on that same job is EXCLUDED and said so: PR 799's was
test_dangling_citation_check, a real content check. Two verified beats three asserted. Whether the
two are #1304's pwsh-launch mechanism is explicitly NOT established -- that diagnostic fires only on
TimeoutExpired and neither log was read far enough to say.

CORRECTED, in both places it appears: the 2026-09-03 re-score says nothing in this tree reads the
ci-red label. True when written, false the same day -- scripts/ci/report_ci_red.py landed in #789.
The prose at CLAUDE.md:302 and docs/METHOD.md:365 is stale on the same point and is named rather
than edited, since it is not this branch's file to move.

THREE THINGS NOT FIXED, stated rather than left implied:
  - the web-console margin baseline has rotted. The record is 3:59 (2026-08-08, n=360, censored) and
    a PASSING run today took 4:46. step_margin.py says raising the cap is not the fix, and re-sizing
    needs a run population one Builder cannot gather in a turn.
  - the SQL Server arm, two of today's eight, still unobservable off the gated CI leg.
  - nothing CALLS report_ci_red.py. The label is read, but only by someone who chooses to.

backlog_status_check: OK, 664 items, each declaring exactly one status. 95 tests pass across the
backlog citation, status, hygiene and ci-red suites.

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

Copy link
Copy Markdown
Collaborator Author

Reviewed: read the reader, its tests, and the ledger entry. Three claims checked rather than taken: nothing under .github/workflows/ invokes the reader, the timing-gate path names why a re-queue cannot help, and the truncation-versus-absence distinction is handled in the code.

It corrected my brief, and the correction is sharper than what I wrote

I briefed the 405-passed-zero-failures case as a reporting defect. That is not quite right, and this PR says why: step_margin.py already states the correct thing in its own step log, naming itself a timing gate with its margin. The instrument was right; nothing carried its answer to a reader. Those need different fixes — one would have had me changing a check that was already correct.

The self-caught vacuous test is the best thing here

One mutation changed the change: dropping the work_step_passed condition left all 24 tests green. So the test asserted the behaviour through a case that step ordering already handles, and the docstring justified it with an unreachable scenario. The reachable case is a skipped suite. Both were rewritten and the mutation now reds.

That is a test that would have shipped looking like evidence and proving nothing — the same class as the control that passed by luck earlier today, caught by the author on itself rather than by a reviewer. Mutation testing is what made it visible; without it the green was indistinguishable.

It verified my relayed evidence and pared it down

I passed on the Lander's report of three spurious test_worktree_gate_control_plane.py failures. This PR confirmed two — PRs 833 and 783, two different tests, neither diff touching the gate — and excluded the third, because PR 799's was a real content check. Two verified beats three asserted.

That is the right handling of a relayed conclusion. I handed down a reading, and it checked rather than inherited it. It also declines to claim these are #1304's mechanism, which is the honest stopping point.

The tool stopped reproducing the trap it reports on

_fetch_runs asks for one page, so an UNATTRIBUTED line could mean "nothing to attribute" or "I stopped looking" — the same "I could not tell" rendered as "fine" that this script exists to end. It now says which. A reporting tool that reproduced its own subject would have been a quiet, durable defect.

Named and not fixed, correctly

The web-console margin baseline has rotted — it records 3:59 while a passing run today took 4:46, so the gate is close to firing on healthy runs. The SQL Server arm stays unobservable off the gated leg and no claim is made about it. And nothing calls the reader, which I verified: wiring it is a scheduling decision, left as an open question rather than guessed at.


Labelling, and saying so on the pull request. The owner has disabled the reviewer role and its tag requirement; I am applying reviewed to clear the requirement, not to assert an independent read of the scope. I commissioned this diff — I wrote the brief, so I am not independent of what it was told to build or leave alone. I read the diff, which I did not write, and the review above says what I checked. A later auditor cannot tell those apart unless someone writes it down.

Not enqueued, auto-merge not armed. BEHIND is expected; main moved.

@wshallwshall wshallwshall added the reviewed A reviewer has read this. Removed automatically when new commits arrive. label Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

reviewed A reviewer has read this. Removed automatically when new commits arrive.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant