Skip to content

Publication prep: release gate, CLI, README split, fixture audit - #1

Merged
hchittanuru3 merged 10 commits into
mainfrom
chore/release-gate
Aug 14, 2026
Merged

Publication prep: release gate, CLI, README split, fixture audit#1
hchittanuru3 merged 10 commits into
mainfrom
chore/release-gate

Conversation

@hchittanuru3

Copy link
Copy Markdown
Contributor

Setting up the branch rules exposed a hole in the release design.

The plan was tag-push plus a required reviewer on the pypi environment — the environment being the real gate, since it pauses the run before the OIDC token is minted. Environment protection rules are unavailable on a private repo under the Team plan. Required reviewers, wait timers and deployment branch policies all reject with a billing error, so the environment exists and holds nothing.

Which means tag-push would have published to PyPI with no human in the loop.

The fix

Trigger is now release: published. A tag is cheap to create by accident and impossible to retract once it's on PyPI; creating a Release is deliberate and puts the generated notes in front of you first. Works on any plan.

The workflow no longer creates the Release — the human does, which is the whole point — so the third job attaches the built artifacts to it instead, keeping its own contents: write rather than lending it to the job holding the publishing identity.

Once the repo is public, environment protection unlocks for free. Add a required reviewer then and the two gates compose.

Repo rules now live

  • main: PR required, force-push and deletion blocked, all five checks (lint, test (3.11–3.14)) must pass. Approvals required: 0, so you can self-merge — any higher and a solo repo deadlocks.
  • Admin bypass is on, as asked. Worth knowing what that costs: your own direct pushes to main still succeed. The rule is enforced by habit for you and by construction for everyone else. Removing the bypass makes it airtight at the price of needing this escape hatch gone too.

This PR is the first thing to go through the rules.

hchittanuru3 and others added 2 commits August 13, 2026 21:00
Setting up the repo rules exposed a hole in the release design. The plan was tag-push
plus a required reviewer on the `pypi` environment, with the environment as the real
gate because it pauses before the OIDC token is minted. Environment protection rules
turn out to be unavailable on a PRIVATE repo under this plan -- required reviewers, wait
timers and deployment branch policies all rejected with a billing error -- so the
environment exists and holds nothing. Tag-push would therefore have published to PyPI
with no human in the loop at all.

Trigger is now `release: published`. A tag is cheap to create by accident and impossible
to retract once it has reached PyPI; creating a Release is deliberate and puts the notes
in front of you first. That gate works on any plan.

The workflow no longer creates the Release -- the human does, which is the point -- so
the third job attaches the built artifacts to it instead. It keeps its own
`contents: write` rather than lending it to the job that holds the publishing identity.

Environment protection becomes available for free once the repo is public, which has to
happen before publishing anyway. Add a required reviewer then and the two gates
compose.

Release steps written down in CONTRIBUTING, since they are now four and one of them is
easy to get wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README had become a manual. Four screens of configuration nuance sat between the
taxonomy and Status, answering questions a reader only has after deciding to adopt.
Split: README keeps pitch, taxonomy, install, use, what it prints, the Turn contract and
status. Everything else moved intact to docs/configuring.md behind one pointer.

Added a CLI, which was the real gap. Everyone wiring this into cron or CI had to write a
wrapper first. `python -m postflight --otel spans.jsonl` or `--langfuse --hours 24`,
exit 1 on a fault so a CI step can gate on it, `--coverage` to ask which detectors can
fire at all, `--json` for machines. It also prints inert detectors unprompted after the
findings, because a zero next to a detector that could not have fired is the failure
this package exists to prevent.

Fixture provenance was overstated. "A real OpenInference capture of a support agent"
reads as a production agent. It is a real capture of an INVENTED scenario: a throwaway
support desk written for the test, run for real through OpenInference. Audited every
attribute across all six spans including the input.value blobs the README never prints:
no names, phones, emails, addresses or identifiers of any kind, only the toy tools and
an order reference made up for the purpose. Wording now says which half is real.

Cut the temporal framing. "Portability is now demonstrated rather than asserted" and
"cache_read_tokens is now int | None" argue with a critic the reader never met, and
"now" is meaningless to someone seeing version one. State the property instead. The
OTel-adapter-found-a-bug narrative went with it: that is release-note content, and this
project keeps release notes on the Releases page.

Fixed section drift. refusal_exemptions sat under "What the other detectors depend on"
when it is a TOOL_REFUSAL concept, and the claim_rules material was orphaned after it.
Both now sit under their own headings. Deduplicated unknown-is-not-zero, the deny-list
argument, and order-matters, each of which was stated twice.

Moved the TOOL_REFUSAL caveat to a footnote so the row lands rather than trailing a
qualifier inside the table.

Em-dashes are gone from every public document, rewritten sentence by sentence rather
than swapped for commas, which would have produced splices. Code comments still use
them; that is a larger sweep and a separate call.

94 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hchittanuru3 hchittanuru3 changed the title ci: publishing a Release is the gate, not pushing a tag Publication prep: release gate, CLI, README split, fixture audit Aug 14, 2026
hchittanuru3 and others added 8 commits August 13, 2026 21:30
Applied README v3 with two corrections, both caught by checking the claims against the
code rather than reading them.

`detail` is keyed by "calls" and holds a list; the snippet showed a single element of
that list, so anyone indexing it as shown would get a KeyError. And the adapter section
promised "four notes" where docs/configuring.md carries three: the fourth was the
order-matters point, which now lives in the Turn contract section, and repeating it was
the duplication that got removed.

The output block was abridged while presented as literal terminal output, missing the
GATE_FILTERED line the command actually prints. It is now generated from a real run, so
what a reader sees matches what they get.

That surfaced the larger miss: coverage() reasons are CLI output, which is public copy,
and they were full of em-dashes. Rewritten, so the em-dash rule now covers what the tool
prints and not only what the docs say. Code comments still use them, which stays a
separate call.

94 tests, ruff clean, twine check --strict clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A refusal is not automatically a bug in the tool. Most in-body declines are expected
outcomes, and the reason is usually information the agent needs next, so "make it raise
instead" would be wrong more often than right: it puts expected branches in error
dashboards, flattens the structured reason into a stringified exception, and trades an
interesting signal for the boring one you already had. Followed to its conclusion it
would make TOOL_REFUSAL permanently inert, which is the failure coverage() exists to
catch.

What the section says instead is what to look at. A refusal on the same turn as an
UNVERIFIED_CLAIM is the pairing that matters, because the tool said no and the reply
said yes. A refusal the reply reports honestly is the system working, which is exactly
what the shipped fixture shows.

The one shape genuinely worth fixing at the source is a tool swallowing a real failure
into a result body, a 500 returned as {"ok": false}. That is an error wearing a
decline's clothes and belongs in TOOL_ERROR where existing alerting can see it.

Pinned the pairing, since the docs now send a reader looking for it: both codes fire on
the dishonest turn, and only TOOL_REFUSAL on the honest one.

Lives in docs/configuring.md rather than under the taxonomy table. The table is the
hook, and the restructure moved nuance out of it for a reason.

95 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The one idea in the README that is genuinely spatial: the failure is a relationship
between steps, and every step passes when scored alone. Everything else reads better as
text, so this is the only diagram.

Two constraints shaped the implementation more than the design did.

GitHub sanitises <style> out of SVGs in markdown, so a @media (prefers-color-scheme)
block never runs. Light and dark are separate files behind a <picture> source, generated
from one definition so they cannot drift.

A sanitised SVG also cannot load a webfont, which is why font-family="Inter" was
rendering as Inter only for whoever already had it installed. The Inter text is now
converted to outlines, so the shapes are in the file and everyone sees the same thing.
Code literals stay live text in the system monospace stack: that is what the design
system itself uses for them, and it resolves everywhere without embedding anything.

Palette is design-system tokens. There is no dark palette and no warning token, so the
dark set is derived to hold the warm cast, and the two findings are separated with the
existing primary/secondary pair rather than by inventing a hue. Weights stay at Inter
400 with tracking on the uppercase labels, per the type rules; the source used 700.

Kept the original emphasis model: the refusal is coloured throughout because it is the
fact being contradicted, while the claim carries colour only on its border and label so
the card still reads as an ordinary reply that happens to be false. Checkmarks are drawn
rather than typed, since Inter has no U+2713 and the glyph would have silently vanished.

docs/ stays out of the wheel; the generator takes the font path as an argument rather
than vendoring a font into the package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RUF001 flags the curly apostrophe as ambiguous, which is correct for source and
wrong for a string that becomes a picture. Inter draws them properly and this is
display copy, so the rule is silenced at the line with the reason rather than the
quotes degraded to ASCII.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tagline was bold body text, which is the same size as the paragraph under it. It is
a heading now, h3 so it does not pick up the horizontal rule GitHub draws under h1 and
h2. Blank lines collapse in rendered markdown and `style` is stripped, so <br> is the
only lever that actually produces vertical space: one above and below the figure, and
one before each section heading across the README, the configuring doc and CONTRIBUTING.

Also fixes the generator recipe, which did not work. The Inter URL I documented 404s,
and the recipe downloaded a 14-byte error page that fontTools rejected as "not a
TrueType font". So a script committed on the argument that it keeps the SVGs
reproducible was in fact not runnable by anyone, which is the failure it was meant to
prevent.

Now points at the Google Fonts copy, which exists and is served as a real TTF. That one
is the VARIABLE Inter, and a variable font's default master does not necessarily match
the static release of the same weight, so the axes are pinned to wght 400 / opsz 14 in
the generator rather than left to the default. Verified end to end: a clean venv, the
documented curl, and the documented command reproduce both committed SVGs byte for byte.

95 tests, ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Added additional line breaks for improved readability.
Removed an extra line break in the README file.
The generator is gitignored and kept locally. My argument for committing it was that
the SVGs are outlined path data and cannot be hand-edited, so the source has to live
somewhere. That is still true, but "somewhere" does not have to be a public package,
and the file also pulled a fonttools extra and a README into the repo to explain a
build step nobody downstream performs.

Removed with it: the `docs` optional-dependency group, which existed only to run it.
docs/img/README.md is now three lines saying the SVGs are generated and not editable,
which is the only part of that a reader here needs.

AGENTS.md carries the constraints an agent cannot see from the code and would plausibly
break: no dependencies ever, detector codes are the public API, unknown is not zero,
kind-keyed rules are deny-lists so they fail closed, vocabulary belongs in Config rather
than in the package, and a detector that fires on healthy behaviour is worse than one
that misses. Plus the writing rules, since public copy has a house style, and the note
that the SVGs are not editable markup.

It points at CONTRIBUTING rather than restating it. Two rulebooks that overlap drift,
and the human-facing one is already good. Every factual claim in it was checked against
the repo rather than written from memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hchittanuru3
hchittanuru3 merged commit ae17a9b into main Aug 14, 2026
5 checks passed
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