Skip to content

factory: a pass that cannot see says so, instead of reading as a quiet night - #495

Open
JulienMartel wants to merge 3 commits into
mainfrom
worktree-factory-blind-spot
Open

factory: a pass that cannot see says so, instead of reading as a quiet night#495
JulienMartel wants to merge 3 commits into
mainfrom
worktree-factory-blind-spot

Conversation

@JulienMartel

@JulienMartel JulienMartel commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

What

factory-shift runs unattended and its product is a log you read hours later, so silence in it is a claim — that something was looked at and was fine. Four steps could fail while making that claim on the shift's behalf:

step was is
gh repo list zero iterations, pass done: 0 merged non-zero pass ABORTED
gh pr list (process substitution, status unreachable) zero iterations, repo silently skipped prs-unknown: <repo>, and its CI is still checked
factory-tier returning neither 0 nor 3 queued: with stderr in the reason column tier-unknown: <repo>#<n>
gh run list empty conclusioncase falls through → that main unreported, including when red ci-unknown: <repo>

The two unknown lines carry the failing command's stderr, one line, truncated — telling a blip from a rate limit is the foreman's only judgement here and it had nothing to make it on.

tier-unknown is the expensive one. .agents/skills/nightshift/SKILL.md tells the foreman that queued rows need nothing, they are the morning's by design — so a PR nobody managed to sense was being filed as one nobody needs to look at.

Why

Not hypothetical, and not theorised from the code. Running the shift tonight, three consecutive passes hit api.github.com:

  • ~/.cache/hausfold-factory/shift-20260829.log:96queued: perch#120 … — Post "https://api.github.com/graphql": http2: client conn could not be established. A PR filed as judged-and-refused when nothing had judged it.
  • Two connection reset by peer lines that reached the terminal and never the log, from gh pr list. That repo was skipped; the pass reported success.

Nothing was red underneath — I checked all fourteen repos by hand. That is the point: the shift could not have told me either way, and the next person to run it at 3am will not be checking by hand.

Verify

shellcheck bench script/factory-lease script/factory-shift script/factory-tier
bats test/bench.bats test/factory-shift.bats

202/202, shellcheck clean. The factory scripts had neither before; the workflow now covers all three, which meant fixing two warnings already sitting there (notify done parsed as a loop terminator, factory-lease's third TSV field binding an unused name — both no-ops at runtime).

The tests stub gh, trill and factory-tier. Eight of the twelve fail against main — I verified that by checking out main's script under the new suite, because a test that passes before and after guards nothing. Three of the four that pass are declared controls: a green main still says nothing, a red one still says CI-RED, a judged refusal is still queued with its reason.

Also worth running once on a real org, which I did: ./script/factory-shift --dry-run prints exactly what it printed before, no false unknowns.

Watch out

  • Stubbing trill was a bug fix, not tidiness. setup() prepends to PATH, so notify's command -v trill found the real binary and three cases reach a notify fault. Running the earlier version of this suite drew real --source factory banners on this desktop. If you have a trill on PATH and pull an older revision of this branch, it will do it again.
  • factory-shift can now exit non-zero. Only on pass ABORTED. Its one caller is the /nightshift skill, updated here; nothing else invokes it and bench has no factory verb.
  • The twelfth control passes against main too (one unseeable repo does not card) — main does not card there either. It is a forward guard on the policy, not evidence of this change.
  • The gate's tool versions are not pinned, and mine were not the gate's. CI runs apt's shellcheck 0.9.0 and bats 1.10.0; I verified with nix's 0.11.0 and 1.14.0. 0.9.0 emits SC2015 on A && B || C where 0.11.0 does not, even at -S style, which is how this branch went green locally and red in CI on a line it never touched (script/factory-tier:25, now an if/then). Both steps print their version first now, so the next divergence is one line into the log instead of a bisect. Pinning them is a separate decision and yours.
  • A repo with Actions disabled would be ci-unknown on every pass forevergh run list exits 1 on its 404 rather than returning empty. None of hausfold's are today (verified: all fourteen exit 0, four of them with empty output). Noted in the code; the answer when it happens is a 404 arm, not a wider ||.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DMJ3jfaov6Q2wKZaTUqLAx

…t night

The shift's product is a log somebody reads hours later instead of having
watched it, so its worst failure is not a crash. It is a pass that sensed
nothing, said nothing about that, and ended on output identical to a genuinely
quiet night. Two paths did exactly that.

`gh run list` failing left `conclusion` empty, and empty is also what a
still-running job and a repo with no workflows look like, so the `case` fell
through and that repo's main went unreported — including when it was red.
Reporting a red main is the entire reason the block exists.

`gh repo list` failing left the loop with nothing to iterate, so every repo went
unchecked and the pass still printed `pass done: 0 merged`.

Neither is hypothetical. Measured 2026-08-29 across three consecutive passes on
one machine: four `connection reset by peer` and one `http2: client conn could
not be established` against api.github.com, one of them mid-pass. Nothing was
red underneath — checked by hand, every family repo green — which is the point:
the shift could not have told me either way.

`ci-unknown: <repo>` for the first, and a non-zero `pass ABORTED` for the
second. `ci-unknown` is a log line rather than a trill card because one blip is
noise and it is the foreman that decides whether a repeat is a story.

This is the rule the budget block already states about itself, applied to the
half that reports on other people's CI: degrade to a named unknown, never to an
answer that happens to parse.

`test/factory-shift.bats` is new and stubs `gh` to make those passes
observable. Four of its six cases fail against the previous script; the other
two are the controls that must not move — a green main still says nothing, and
a red one still says CI-RED. The workflow now shellchecks all three factory
scripts and runs both suites, which meant fixing two warnings that were already
there: `notify done` read as a loop terminator (quoted), and factory-lease's
third TSV field bound a name nothing used (`_`).
The first commit guarded the CI half and the org listing. An assurance pass
against a clean context pointed out that the one failure with a record in
tonight's log was in the MERGE half, which I had left alone.

`factory-tier` exits 0 tier-1 · 3 refused-with-a-reason · anything else means
`set -e` aborted inside it, usually on a `gh` call. `factory-shift` branched on
success/failure, so "refused" and "could not be judged" both became `queued:`
with whatever was on stderr in the reason column. The nightshift skill tells the
foreman that queued rows need nothing, being the morning's by design — so a PR
nobody managed to sense was filed as one nobody needs to look at. That is
shift-20260829.log:96, verbatim: `queued: perch#120 … — Post
"https://api.github.com/graphql": http2: client conn could not be established`.
It is now `tier-unknown`.

`gh pr list` ran in a process substitution, whose exit status is unreachable, so
a failed listing gave zero iterations and a repo whose PRs were never read was
indistinguishable from one with none open. That is where the `connection reset
by peer` lines went in the two passes before it: straight to the terminal,
never into the log, the repo silently skipped. Now `prs-unknown`, and it no
longer takes that repo's CI check down with it.

The dated measurement in the CI comment claimed counts the log does not support
and attributed them to the block they did not come from. Removed rather than
restated: the falsifiable evidence belongs in this message, where a reader
asking why already is.

Both unknown lines now carry the failing command's stderr, one line, truncated:
telling a blip from a rate limit is the foreman's only judgement here and it
had nothing to make it on. `pass ABORTED`'s card no longer claims "GitHub
unreachable" for what may be a token or a rate limit.

The tests stub `trill` now, and that is not tidiness. `setup()` prepends to
PATH, so `notify`'s `command -v trill` found the real one and three cases
reached a `notify fault` — running the suite drew real banners on this desktop.
The stub also records its calls, so the notify policy is pinned: one unseeable
repo must not card, a dead pass must.

docs/factory.md stated the change instead of the contract, which AGENTS.md
bans outright. It is now a table of what each line means and what the pass
exits, and the nightshift skill — the only consumer of any of this — learns the
four lines, that an unknown is never a fixer's cue, and that an unknown or an
abort is noop:false rather than another quiet tick.

Twelve cases now. Eight fail against main; three are the controls that must not
move, and one pins a card main does not draw either.
CI reddened on `script/factory-tier:25`, a line this PR does not touch: apt's
shellcheck emits SC2015 on `A && B || C` where 0.11.0 (the nix one I checked
with) does not, even at -S style. Putting the factory scripts under the gate is
what surfaced it, so it belongs here. The line is an if/then now, which reads
better than the guard it replaces and needs no directive.

Both steps print their tool's version first. The versions are not pinned and
this is the second-order failure that costs the most time: a step reddening on
a line nobody edited, with nothing in the log saying the checker moved.
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