ShippedSecurity · CI/CD
Workflow SAST — zizmor
boost's four workflows embed github-script JavaScript and
shell and drive a Trusted-Publisher release — a rich attack surface.
zizmor statically flags template injection, unpinned
actions, over-broad GITHUB_TOKEN permissions and dangerous
triggers. The one tool that audits the automation that ships every
other fix.
Complexity S
Impact High
Wow ★★★★★
pipx-run
ShippedQuality · CI/CD
Workflow linting — actionlint
Catches GitHub Actions YAML bugs before they fail a live release:
malformed ${{ }} expressions, deprecated syntax, and
shellcheck run over every run: block. Cheap insurance for a
repo whose release is fully automated — a broken workflow is a broken
publish.
Complexity S
Impact Med
Wow ★★★
shellcheck built in
ShippedSecurity · Supply chain
Build provenance — SLSA attestations
actions/attest-build-provenance emits a cryptographically
signed, verifiable record of which workflow built which wheel from
which commit, layered on top of the existing PyPI Trusted
Publishing. Consumers can gh attestation verify the
artifact they install — provenance without a paid signing service.
Complexity S
Impact Med
Wow ★★★★
SLSA build L2
ShippedSecurity · Supply chain
SBOM on every release — CycloneDX / Syft
Generate a CycloneDX SBOM with the free anchore/sbom-action
and attach it to each GitHub Release, so downstreams can inventory and
scan exactly what boost is built from. Modest today (boost is close to
stdlib-only) but it grows in value as the optional [rag]
extras pull in real dependencies.
Complexity S
Impact Med
Wow ★★★
feeds osv-scanner
ShippedSecurity · Vuln
SBOM-aware scanning — osv-scanner
Google's osv-scanner cross-checks a lockfile or the SBOM
above against the OSV database, with broader ecosystem coverage than a
single-language audit. The SBOM-driven complement to pip-audit:
one scans the resolved env, the other scans the published manifest.
Complexity S
Impact Med
Wow ★★
OSV-backed
ShippedTesting · Type
Second type checker — pyright
Run Microsoft's pyright alongside mypy. Its independent
inference catches None-flow and narrowing bugs the current
mypy config lets slide, and it's the same engine most editors use — so
CI enforces what contributors already see. Two type checkers rarely
agree on nothing.
Complexity M
Impact Med
Wow ★★★
editor-parity
ShippedQuality · Smell
Widen the ruff rule surface — B·SIM·C4·PERF·RUF
Beyond the S security family (round 1), enable
flake8-bugbear (B), simplify (SIM),
comprehensions (C4), perflint (PERF) and
Ruff-native (RUF) rules — bug, readability and performance
smells caught at zero new-tool cost since ruff already runs in the lint
gate.
Complexity S
Impact Med
Wow ★★★
0 new deps
ShippedTesting · Gap
Patch-coverage gate — diff-cover
Enforce coverage on the changed lines of each PR — no external
service needed, it reads the same coverage.xml the suite
already emits. Pairs with the 80% project gate so new code can't quietly
ride in under-tested behind an already-high overall number.
Complexity S
Impact Med
Wow ★★
self-hosted
ShippedCI/CD
publish.yml ignores the pip-audit / metadata gates
publish.yml triggers on workflow_run: [ci] and gates only on
github.event.workflow_run.conclusion == 'success' — it never checks the
independently-triggered pip-audit.yml or package-metadata.yml workflows
that run on the same push. A merge that fails the live-CVE gate or the twine/metadata check still
auto-publishes to PyPI as long as ci itself is green.
Shipped as the second option: scripts/release_preflight.py runs as the release job's
first step — before release-drafter creates the tag, so a blocked release leaves no
published Release behind — and waits for each required workflow to conclude for
git rev-parse HEAD, the commit actually checked out and about to be built.
Folding them into ci.yml was rejected: pip-audit also runs weekly on a
cron, which inside ci.yml would drag the whole 9-cell test matrix along with it.
The gate fails closed — red, cancelled, skipped, never started, still
running at the deadline and an unreadable API reply are all refusals. Silence is the dangerous
case: a gate that never ran leaves nothing red to see, so it must never read as consent.
Complexity S
Impact High
Wow ★★★
gates PyPI
ShippedCI/CD
No timeout-minutes on any CI job
A hung step — a stalled tap clone, a wedged smoke-test subprocess — runs until GitHub's default
job timeout instead of failing fast, costliest across the 3 OS × 3 Python tests
matrix. (The card said 8 workflow files; there are 24 now, of which only fuzz.yml
and sonarcloud.yml had a timeout.) Shipped: 24 jobs across 22 files, with the
numbers taken from measured durations over recent runs rather than guessed —
mutation's slowest observed run was 24.8m so it gets 60, the tests
matrix peaked at 8.3m so it gets 30, publish's release job gets 45 because the
new release preflight can legitimately wait on a sibling gate, and everything else ran under
2m so it gets 15. A timeout that trips on a normal run is worse than no timeout.
osv-scanner.yml's scan-pr is the one job left without one: it
delegates to a reusable workflow via a job-level uses:, and GitHub rejects
timeout-minutes there outright — the bound has to live in the called workflow.
A unit test holds the line so a job added next month cannot quietly arrive without one, and
it refuses to pass vacuously if its own parser stops finding jobs.
Complexity S
Impact Low
Wow ★
ShippedSupply chain
Dependabot's pip entry misses pyproject.toml's extras
.github/dependabot.yml declares pip for
directory: /requirements, so the hash-pinned dev/CI toolchain does get weekly
bump PRs. What it does not cover is pyproject.toml at the repo root:
Dependabot only scans manifests under the declared directory, so the optional
extras — [rag] (sqlite-vec), [eval] (ranx, ragas and the pinned
langchain 0.3 stack), [bdd] (behave), [perf] (pytest-benchmark) —
never get proactive version-bump PRs, only reactive pip-audit CVE flags.
Low impact by design: [project].dependencies is empty, so none of this reaches
anyone who installs boost-skill-cli — it is contributor-facing only. The fix is
a second pip entry with directory: /. Weigh it against the noise:
the [eval] stack is deliberately held at langchain 0.3 because ragas
0.2.x breaks against langchain ≥1.0, so that one will raise PRs that must be closed
unmerged until ragas catches up.
Complexity S
Impact Low
Wow ★
extras only — /requirements is covered
ShippedSupply chain
License-compliance scanning of the dependency closure
Nothing in CI checks SPDX license compatibility of resolved dependencies — pip-audit
gates known CVEs, not license terms, and the [eval]/[rag] extras pull in a
nontrivial transitive closure (the langchain stack, ragas, sqlite-vec) whose licenses are never
verified against boost's own. Add a pip-licenses --fail-on (or reuse lint)
step to package-metadata.yml to catch an incompatible transitive dependency before an
extra ships it.
Complexity S
Impact Med
Wow ★★
PlannedSupply chain
The pytest tmpdir CVE has no fix at our Python floor
CVE-2025-71176 — pytest through 9.0.2 relies on the predictable
/tmp/pytest-of-{user} path, letting a local user cause a denial of
service or possibly gain privileges. Dependabot alert #1 (medium), raised against
requirements/test-tools.txt.
It cannot be closed by a version bump. The lock carries two markered pins, and only
one of them is exposed: pytest==9.1.1 ; python_full_version ≥ '3.10' is
already past the 9.0.3 patch, while
pytest==8.4.2 ; python_full_version < '3.10' is not — and cannot be, because
pytest 9.x declares requires-python ≥3.10 while this project declares
requires-python = ">=3.9". The advisory marks every 8.x release
vulnerable, so there is no backport to move to either. The fix is gated on dropping the 3.9
floor, which is a deliberate compatibility promise, not an oversight.
Residual exposure is contributor-facing only: test-tools.txt is the dev/CI
toolchain and [project].dependencies is empty, so nobody installing
boost-skill-cli ever receives pytest, and CI runners are single-tenant
ephemeral VMs where the local-attacker precondition does not hold. Dismissed as
tolerable_risk rather than left open to rot the alert list.
Revisit when either the 3.9 floor is dropped or a patched 8.x lands. If the floor must stay
and the alert must clear, the real mitigation is pointing pytest at a private base
temp dir (--basetemp / PYTEST_DEBUG_TEMPROOT) so the predictable
path is never used — that fixes the behaviour without satisfying Dependabot's version check.
Complexity M
Impact Low
Wow ★★
dismissed as tolerable risk, 2026-07-25
PlannedSupply chain
OpenSSF Scorecard's 15 findings, triaged into three piles
Scorecard files into the same code-scanning inbox as CodeQL, so its findings read like
code defects and get ignored alongside the false positives. They are not defects — they
are posture metrics, and they must not be dismissed as "false positive" the way the
serve.py traversal alerts legitimately were, because that misrepresents the
repo's security posture. Triaged 2026-07-25:
Fixed — TokenPermissions #26/#27/#28: ci,
codeql and adapter-conformance had no top-level
permissions: block at all, scoring 0. Added contents: read as the
least-privilege default.
Intentional, leave — TokenPermissions #39/#37/#25/#24:
publish.yml needs contents: write to create the release and tag,
osv-scanner needs security-events: write to upload SARIF, and
ci.yml's tests job needs contents: write for the
coverage-badge push. "Fixing" these breaks the release pipeline. They should be
dismissed as used-in-tests/acceptable-risk with a reason, not left open to rot the list.
Not code-fixable — MaintainedID is repository age; CodeReviewID
counts approved changesets and is structurally 0 while loops self-merge;
CIIBestPracticesID is an opt-in badge programme; the four
PinnedDependencies hits are npx and pip invocations
inside steps, not actions, so SHA-pinning does not apply to them as written.
BranchProtectionID is tracked separately in
its own card — it is a workflow decision, not a
config oversight.
Complexity M
Impact Med
Wow ★★
15 open · 3 fixed, 4 intentional, 5 not code-fixable
ShippedRelease safety
main has no branch protection, so the release rules are honour-system
GET /branches/main/protection returns "Branch not protected". Scorecard
flags it (BranchProtectionID, score 0), but the sharper way to see it is this:
CLAUDE.md says "never merge onto a red release" and "every merge to main cuts a
PyPI release" — and nothing enforces either. A merge with a red gate succeeds
silently, and the release workflow then ships that commit to PyPI. The rule exists only in
the head of whoever is merging.
This is a decision, not a bug, which is why it is filed rather than fixed. Requiring
status checks would have stopped the exact situation that occurred on
2026-07-25, when three PRs sat red on a lock_toolchain --check failure that
belonged to none of them. But protection cuts both ways here: the repo's whole working model
is parallel loop/* branches opening and merging their own PRs, so
"require pull request reviews" would deadlock every loop (there is no second
reviewer), and that same absence is what keeps CodeReviewID pinned at 0/30.
The shape that fits: require status checks to pass (at minimum lint
and the tests matrix) and require branches to be up to date, but do not
require reviews. That enforces the release rule that actually matters while leaving
self-merge intact. Decide before adding more concurrent loops, not after.
Update — the blocker is gone. This prescription was not implementable as written: lint named three different jobs (ci, markdownlint, theme-lint), and GitHub matches required checks by name alone. Those collisions are now renamed, the required list is checked-in at .github/required-checks.txt and gated against drift, and python3 scripts/check_required_checks.py --print-api emits the exact payload — with required_pull_request_reviews: null, so self-merging loops keep working. Still a decision, but now a one-command one.
Shipped. The decision was taken as prescribed — status checks yes, reviews no.
GET /branches/main/protection now returns strict: true with
required_pull_request_reviews absent, so a red gate blocks the merge that would
have cut the release, and loops still self-merge. One correction to the prescription above:
requiring "at minimum lint and the tests matrix" is right, but the
list must contain only checks that run on every PR — the first version also
required four path-filtered docs checks, which report on some PRs and not others and would
have hung any PR that touched no matching file. See
required-checks-can-declare-a-check-that-deadlocks-prs.
Complexity S
Impact High
Wow ★★★
decided, applied, and now gated against deadlock
ShippedBug
adapter-conformance's LangGraph leg never passed — a quoted matrix value
The install step ran
pip -q install "${{ matrix.pip }}". Quoting is correct for the two legs
whose matrix value is a single token (crewai[anthropic],
openai-agents[litellm]) — the brackets would otherwise be shell globs.
But the LangGraph leg's value is a requirement list,
langgraph langchain-anthropic, so the quotes handed pip one bogus
requirement and it refused before installing anything:
ERROR: Invalid requirement: 'langgraph langchain-anthropic'.
The leg landed 2026-07-22 and the workflow was schedule/workflow_dispatch
only, so nothing exercised it on its own pull request — the first Monday cron was
its first execution ever, five days later, and it has a 0% pass rate. It read as
upstream framework drift; it was never that. Reproduced locally, and with the
step fixed the whole leg passes end to end against live PyPI: langgraph 1.2.9 +
langchain-anthropic 1.5.2 resolve clean, boost adapt --to langgraph
renders, and the factory builds a real
langgraph.graph.state.CompiledStateGraph.
Fix: pass the spec through env: (rather than inlining a
${{ }} expansion into run:, the shape zizmor flags) and
let it word-split under set -f, which keeps the [extra]
brackets literal — without it, crewai[anthropic] glob-expands against
any matching filename. Also added a narrow pull_request trigger on
adapters.py and this workflow, so an edit to the thing under test is
tested on the PR that makes it. That trigger is path-filtered, so it must stay out
of the required-check list: a required context that does not report on a PR leaves
it pending forever.
Complexity S
Impact Med
Wow ★★★
shipped broken, first run exposed it
ShippedRelease safety
The required-check gate could not see paths:, so it green-lit a list that deadlocks every PR
.github/required-checks.txt declared 21 contexts and
scripts/check_required_checks.py reported
"21 required, 22 PR check names, no ambiguity" and exited 0. Four of those 21 —
validate, markdown-lint, theme-lint and
vale — are produced by path-filtered workflows.
When a pull request touches none of a workflow's paths:, GitHub does not
create a pending check run that later resolves; it creates no check run at all.
Branch protection then waits forever for a status that is never coming, and the pull
request can never merge. Applying that list would have bricked the repository — and
--print-api would have emitted the payload to do it.
The cause is one regex. The gate decided "runs on pull_request" with
re.search(r"^\s{2}pull_request:", header), which is true for any workflow
that mentions the trigger, whether or not paths:,
paths-ignore: or types: [labeled] narrows it — precisely the
distinction that decides whether a required check deadlocks.
Demonstrated live rather than argued: the PR that fixed the
adapter-conformance install touches no style/** file, and its 30
check runs contain validate, markdown-lint, vale,
check and sweep — but no theme-lint. Requiring
theme-lint would have hung that PR permanently.
Fix: classify the trigger as none/always/filtered and refuse any required name that
resolves only to a filtered workflow, with an error that names the culprit file. The four
docs checks move to a commented block explaining why they are useful but not requireable.
The list is 17 contexts, all verified to report on every PR. Ten new tests cover the rule
and all ten fail against the previous script.
Complexity S
Impact High
Wow ★★★★
the gate green-lit a list that bricks every PR
ShippedBug
ci.yml's job summary could exit 1 on its own, under the always() it was given
The job summary step is marked if: always() so a run still reports
when something upstream died. Its last line was
[ -n "$SMOKE" ] && echo "- **Smoke suite:** ${SMOKE}", which is the final command
of the { ... } >> "$GITHUB_STEP_SUMMARY" group, and that group is the final
command of the step. GitHub runs shell: bash as
bash --noprofile --norc -eo pipefail, so with $SMOKE empty the
AND-list's status of 1 becomes the group's status and then the step's exit code.
$SMOKE is empty precisely when smoke.sh never printed its
== results: line — an earlier step died first, or smoke never ran. That is the
exact scenario always() exists for, so the reporting step failed in the one case
it was written to survive. Reproduced directly: exit 1 with no match, exit 0 with one.
It never masked a real failure (the job is already red when this happens), but it planted a
second misleading red step, and would have silently reddened the job if that step were ever
moved into a green context. Replaced with an if block.
Complexity S
Impact Low
Wow ★★★
an always() step that exits 1 on its own
ShippedBug
post-deploy.yml's always() destroyed the second signal it existed to preserve
The console-health step carries if: always() with the comment
"an HTTP failure above must not hide a console failure here — they are different breakages
and both belong in one report". But the two steps it depends on —
actions/setup-node and the npm install that fetches puppeteer — had
no if: at all. A failure in the HTTP smoke skips both, so the console
check then runs with no node_modules and produces an environment error rather
than a console verdict.
The run history separates the two cleanly: an environment error exits 2, a genuine
console failure exits 1. Run 30214061945 has
HTTP smoke = failure, setup-node = skipped, npm = skipped, console = failure (exit 2),
while the four runs where the npm install actually happened all exit 1. So in exactly the
case the always() was written for, the second signal was destroyed instead of
preserved — and the report said "console check failed" when it meant "the console check
never ran".
Fixed by putting always() on the setup steps too, so the dependency chain the
final step needs survives the failure it is meant to outlive.
Complexity S
Impact Med
Wow ★★★★
the always() destroyed the signal it was written to preserve
ShippedBug
markdownlint linted the fuzzer's corpus, so shipping a crash reproducer would redden a prose gate
.markdownlint-cli2.jsonc globs **/*.md and its ignores
list carved out .claude/** and docs/roadmap/items/** but not
tests/fuzz/corpus/**. So 12 of the 21 files being prose-linted were
deliberately-malformed YAML-frontmatter seeds for the atheris fuzzer —
07-unterminated.md, 09-unbalanced-quote.md, 12-bom.md
and friends. Malformed is their entire purpose, and they passed the gate only by luck.
That luck was going to run out by design. fuzz.yml exists to mine new
malformed inputs and ship them as reproducers
(-artifact_prefix=artifacts/, uploaded as fuzz-crash-<target>),
and one such crash artifact already exists from a real run. The moment anyone committed it into
tests/fuzz/corpus/frontmatter/ — exactly what the workflow is for — an unrelated
prose-style gate would have gone red for a reason with nothing to do with prose, on a file
whose whole point is to be invalid.
Fixed by adding tests/fuzz/corpus/** to ignores. Verified with the
pinned markdownlint-cli2@0.18.1: 21 → 9 files linted, 0 errors, and a planted
crash reproducer no longer reddens the gate.
Complexity S
Impact Med
Wow ★★★
12 of 21 linted files were malformed on purpose
ShippedRelease safety
Enabling a merge queue would deadlock every required check except ci.yml's
Only ci.yml declares merge_group among its triggers. The other
workflows producing required contexts — codeql.yml,
pip-audit.yml, package-metadata.yml, osv-scanner.yml —
run on pull_request only.
GitHub's merge queue evaluates the required checks against the temporary
merge_group ref, not against the pull request. So the moment a merge queue is
turned on, codeql-analyze, pip-audit, metadata and
scan-pr / osv-scan would never report on the queued ref and every enqueued PR
would sit forever — the same never-reports deadlock as
required-checks-can-declare-a-check-that-deadlocks-prs, arriving through a
different door.
Nothing is broken today; no merge queue is configured. Filed because the trap is invisible
until the switch is flipped, and because the pressure to flip it is real: with
strict: true and several concurrent loops, every merge forces every other open
PR to rebase and re-run its full matrix, which is exactly the problem merge queues solve.
Fix before enabling, not after: add merge_group: to each workflow that produces
a required context, and extend scripts/check_required_checks.py to fail when a
required name comes from a workflow lacking that trigger — the same gate that now catches
paths: filters.
Complexity S
Impact Med
Wow ★★★
a latent trap, only if someone enables the queue
ShippedCI speed
The mutation gate was CI — 26 minutes, three times the next-longest job
Measured across twelve consecutive runs, the mutation job took
20.6–39.2 minutes (median ~27) while the next-longest job in the same run,
tests (windows-latest, 3.12), took 8.6. One job was the critical path,
and it was charged twice per change: once blocking the pull request, then again on
main, because publish.yml fires on workflow_run after
CI completes — so the PyPI release waited on it too. PR #278 merged at 20:52 and the
release landed at ~21:20, essentially all of it this gate.
The cost is ~10,502 mutants over 45 files in boost_cli/core, run 4-way parallel
(max_children defaults to os.cpu_count(); ubuntu-latest
is 4 vCPU). Generating them takes 5.3 seconds — the entire cost is executing the unit
suite ten thousand times. A measured local baseline: 1600s wall, 6.70 mutations/second,
8850/10502 killed (84.3%).
Two independent wins, because they apply to different pull requests.
Skip what cannot have changed. Sampling the last 40 merged PRs, 38% touch neither
boost_cli/ nor tests/ nor the mutation configuration — docs,
roadmap cards, other workflows. Their score is identical to the base commit's by construction,
and they were each paying 26 minutes to prove it. The relevance rule is deliberately wider than
boost_cli/core/: setup.cfg's also_copy ships the whole
package into mutants/ and the tests import it, so a change in
commands/ can flip a core mutant. It also fails safe — no resolvable
base commit means run the full gate.
Shard the rest. mutmut run accepts fnmatch patterns over mutant names and
each source file owns a disjoint mutants/<path>.meta, so splitting by file
yields results that merge without any per-mutant reconciliation. Verified live:
mutmut run 'boost_cli.core.nethttp.*' ran 16/16 of that file's mutants and left
store.py at 0/1931. Six shards reach 5.44x — and no further, because
store.py alone is 18% of all mutants and cannot be split. That ceiling is printed
by plan --explain rather than left to be rediscovered.
The dead end, recorded so nobody repeats it. Caching mutants/ between runs
is the obvious first idea and it does not work in mutmut 3.6 — it actively destroys the
data it would need. copy_src_dir skips targets that already exist, so on a warm
tree create_mutants_for_file takes its source_mtime < mutant_mtime
branch and resets every exit code to None. Demonstrated rather than
inferred: a file with 235/235 recorded results came back with 185 wiped and the run
re-testing from zero. It would have looked like it was working while saving 5.3 seconds.
What adversarial review caught, which local testing did not. The first working
version sharded by glob("*.py") — non-recursive — while mutmut walks
source_paths with os.walk. A subpackage
(core/rag/bm25.py) would therefore be assigned to no shard; and because
export-cicd-stats skips a path with no .meta rather than
counting it unkilled, those mutants would drop out of total entirely and the
required check would report PASS over a subset. Fail-open — precisely inverting
the property the file's own docstring claimed. Two further finds: no fnmatch pattern can
address __init__.py, because mutmut rewrites .__init__. out of mutant
names (boost_cli.core.__init__.* matches nothing, and
boost_cli.core.* would swallow the whole package); and
git diff --name-only reports only a rename's destination, so moving
core/x.py to docs/x.py looked like a docs-only change and skipped the
gate. All three are now covered by tests that fail against the pre-fix code, and merge asserts
that every mutatable file was actually accounted for — so future layout drift reddens the
gate instead of quietly narrowing it.
The trap this had to avoid. mutation is a required status check
(.github/required-checks.txt). Path-filtering the job — the natural way to
implement "skip" — means GitHub creates no check run at all, and branch protection waits
for a status that never comes. This repository has already deadlocked that way twice. So the
required job always runs (if: always()) and inspects what the upstream jobs
actually did; always() on its own would have reported a failed shard as a green
required check. Merging likewise fails closed: mutmut counts an unrun mutant as "not checked"
inside total, and the gate divides by it, so a partial merge would quietly depress
the score instead of erroring.
What actually landed, measured on main. The eleven CI runs before this
merged took 20.7–39.4 minutes (median ~28.4); the merge commit's own run took
12.1. This pull request touches tests/, so it ran the full six-shard
gate — the worst case, not the docs-only path.
It also missed its own estimate, in an instructive way. The prediction was ~6 minutes for a code
change; the slowest shard took 9.9. Packing by mutant count is near-perfect — the six
shards carry 1703–1931 mutants each, within 1.10x of ideal — but measured
throughput ran 3.39 to 14.19 mutants/second, a 4.2x spread. Mutant count is simply a
poor proxy for time: a store.py mutant re-runs a far larger covering test set than an
ed25519.py one, and survivors run their tests to completion where kills exit early.
Re-weighting the planner by seconds instead of count would even out the billed minutes but
not the wall clock, because shard 0 is store.py by itself and its 9.5 minutes
is the floor. Going below it means splitting a single file — feasible, since mutant names are
addressable per function (boost_cli.core.store.install_*), which would bring the floor
toward the 5.1-minute even split of the 30.8 minutes of total work.
Complexity M
Impact High
Wow ★★★★
28 min of CI became 12; the new floor is one file, store.py
PlannedCI speed
The mutation gate's floor is a single file — shard 0 is store.py
Sharding took the mutation gate from ~28 minutes to ~12 (see
mutation-gate-was-the-whole-critical-path), but it cannot go lower without
splitting one file. Measured across four consecutive six-shard runs:
638bffeb ran 1.9, 3.9, 5.7, 6.7, 7.2, 11.7 minutes;
02c18bd6 ran 2.7, 5.0, 5.2, 5.5, 5.9, 11.7; fc427839 ran 3.1,
4.8, 5.3, 6.4, 7.3, 9.7; and f42e617b ran 2.9, 4.9, 5.5, 6.6, 7.3,
9.3.
mutation-shard (0) was the slowest leg in all four. Total work is
36–37 job-minutes, so a perfectly even split would be 6.1; the actual critical
path is 9.3–11.7, or 1.5–1.9× that. The gap is not bad packing —
the planner balances mutant count to within 1.10× of ideal. It is that count is
a poor proxy for time: measured throughput spans 3.39 to 14.19 mutants/second, because a
store.py mutant re-runs a far larger covering test set than an
ed25519.py one, and survivors run their tests to completion where kills exit early.
Re-weighting the planner by seconds would even out the billed minutes but not the wall
clock, because store.py is 1931 mutants (18.4% of the repo) in one file and
longest-processing-time packing cannot place a single file across two bins. The floor is that
file.
The way under it: mutant names are addressable per function, so
boost_cli.core.store.install_* can be its own shard. That would bring the critical
path toward the 6.1-minute even split. Two things must survive the change —
pattern_for() still has to emit patterns that match nothing else, and
cmd_merge's completeness assertion has to keep failing closed, since mutmut counts
an unrun mutant inside total and a partial merge would silently depress the score
rather than error.
Complexity M
Impact Med
Wow ★★★
shard 0 is store.py alone — 9.3-11.7 min against a 6.1 even split
PlannedCI speed
A third of CI job time is spent waiting for a runner, not running
With the mutation gate no longer dominating, the largest remaining term in CI wall clock is
queueing. Measured over six consecutive successful ci runs: 104
job-minutes queued against 236 job-minutes executing — 31% of all job
time spent waiting for a runner.
The obvious metric lies. A run's run_started_at minus
created_at is 0.0 for every run, which reads as "no queueing anywhere".
Queueing happens per job, not per run, so it is only visible as
job.started_at - job.created_at. Anything measuring this at run level will
conclude, wrongly, that there is nothing to fix.
The per-run spread shows what drives it — median job queue by run:
c36c53a7 0.1 min, c15ed9da 0.1, 903b704f 0.2,
638bffeb 0.7, ad71ba99 2.5, 9e190537 2.9.
That is a 29× swing driven not by the change under test but by how many sibling
loop/* branches and Dependabot PRs happen to be live at the same moment. It is why
one merge took 41 minutes wall clock with a max shard of only 11.4.
Sharding the mutation gate made this worse on purpose: it traded one runner slot for six. That
is the right trade for latency in isolation, and the wrong one to keep making blindly while a
dozen branches are in flight, because every concurrent PR now multiplies its footprint. Options,
roughly in increasing order of intrusiveness: a concurrency group that cancels
superseded PR runs; capping the shard matrix with max-parallel; or making the shard
count adaptive. Worth measuring before choosing — this item is the measurement, not the
fix.
Complexity M
Impact High
Wow ★★★★
104 job-min queued vs 236 executing — 31% of CI is waiting for a runner
PlannedSecurity · CI/CD
The required lint job pins zizmor==1.27.0 — a yanked release
ci.yml runs the workflow SAST step as
pipx run zizmor==1.27.0 .github/workflows. PyPI marks 1.27.0 as yanked, with
yanked_reason: GHSA-f42p-wjw5-97qh. It is the only yanked release out of 66,
and 1.28.0 is the successor.
Exact-pinning the security scanner is correct — an unpinned linter is how
ruff-defaults-broke-boost-lint happened. Pinning to a version its maintainer has
withdrawn for a security advisory is the failure mode on the other side, and it is
quieter: a yank does not stop an exact pin from resolving. pip installs
1.27.0 and emits a warning nobody reads in CI logs, so the required lint
gate goes on passing while running a withdrawn build of the tool that audits the workflows
driving a Trusted-Publisher release.
Two things worth doing together. Bump to 1.28.0 and confirm the four documented
dangerous-triggers exemptions in .github/zizmor.yml still suppress
cleanly — the line-anchored ignore entries (publish.yml:19,
sbom.yml:29, …) are exactly the kind of thing a version bump can invalidate.
And sweep the other exact pins for yanks: nothing currently checks for this. pip-audit
catches CVEs in the resolved dependency closure, but a yanked pinned tool is neither a
CVE in the closure nor a manifest diff, so osv-scanner does not see it either. This
was found by accident, while installing zizmor locally to verify an unrelated change.
Complexity S
Impact Med
Wow ★★★★
the only yanked release of 66 — and the required lint job pins exactly it
PlannedRelease safety
The release verifies one commit and ships another
publish.yml fires on workflow_run when ci completes, gates
on that run's conclusion == 'success' (plus event == 'push' and
head_repository == this repo), and then checks out ref: main. Those are
not the same commit. Observed 2026-07-28: ci for ecfe38bc
completed at 20:19:05Z; the release run was created at 20:19:07Z; it tagged and shipped
c1727bac as v1.0.271 — while c1727bac's own
ci, started 20:18:40Z, was still in_progress. One commit's green
verdict authorised a different commit's release.
Most of this is already mitigated, and the card should not be read as "untested code ships".
A strict "require branches to be up to date" policy means the shipped commit was green as a pull
request before it could merge, so its tree was gated. And
release_preflight.py is already pointed at the right commit —
--sha "$(git rev-parse HEAD)", with an explicit comment that this is "not the trigger
sha". It polls each required workflow for that exact sha and fails closed on red, cancelled,
skipped, never-ran or still-running-at-deadline. The machinery is correct.
Its coverage is the gap. .github/required-checks.txt lists 18 required
contexts. publish.yml passes preflight exactly two of them —
--require pip-audit.yml and --require package-metadata.yml. The other
16 are never re-checked against the commit being shipped, including all 14 that come from
ci.yml and codeql-analyze. The clearest evidence this is an oversight
rather than a decision is in-tree: release_preflight.py's own docstring says it waits
for every required workflow for the exact commit being released, and the caller hands it
two.
A required context that can never report on main at all. While checking the
other 16: osv-scanner.yml declares only pull_request and
merge_group triggers — no push. So scan-pr / osv-scan
is green as a PR gate and produces nothing for any commit on main, race or
no race. Adding it to preflight without first giving that workflow a
push: branches: [main] trigger would fail the release closed every time.
The fix is not to change the checkout ref. Pinning to
github.event.workflow_run.head_sha would build the older commit while release-drafter
still tags the default branch's head — artifact and tag would disagree. The lower-risk
change is to widen preflight's --require set to the contexts that actually run on
main, and optionally fail the job when git rev-parse HEAD differs from
the triggering run's head_sha. This is the third defect found in the same few lines
— see publish-trigger-was-reachable-from-a-fork (who can fire it) and
publish-gate-ignores-pip-audit-and-metadata (which gates are consulted); this one is
which commit they were consulted about.
Complexity S
Impact High
Wow ★★★★
2 of 18 required contexts are re-checked against the sha actually shipped
PlannedCI reporting
demo.yml still fails on every push — and the fix is not in the workflow
demo-gif-workflow-has-never-succeeded (shipped, PR 294) fixed the recorder:
charmbracelet/vhs-action's broken ffmpeg installer was replaced with apt plus
ttyd/vhs pinned by release-asset id. That worked. The recording step now succeeds on both event
paths. The workflow still fails on every push to main — observed
2026-07-28 at 20:56, 20:04 and 09:27, and it is the only failing workflow in the last 60 runs.
The failure moved one step later, to open a PR if the recording changed. The run
annotation gives it verbatim:
GitHub Actions is not permitted to create or approve pull requests.
Nothing in demo.yml is wrong, so nothing in demo.yml will fix it.
The job already declares contents: write and pull-requests: write,
which is exactly what peter-evans/create-pull-request documents as required. The
workflow uses no secrets. persist-credentials: false on the checkout is correct and
is the repo-wide convention across all 31 checkout steps — the action supplies its own git
auth and explicitly unsets any persisted credential. Every one of those is a plausible-looking
false lead. The actual blocker is a repository setting: Actions → General
→ Workflow permissions → "Allow GitHub Actions to create and
approve pull requests", which the API reports as
can_approve_pull_request_reviews: false. This is the same shape as
sbom.yml: a workflow that is correct in the file and inert in reality.
Deciding it is a real decision, not a formality. Turning that toggle on grants every
workflow in the repo the ability to open pull requests, which is a genuine widening of what a
compromised action can do. The alternative is to accept that this step cannot work and change
what the push path does — upload the GIF as an artifact on both paths, as the pull-request
path already does, and drop the PR-opening step.
A second, independent defect in the same file. Its concurrency group is
group: demo, not ref-scoped. Every other per-ref workflow scopes it —
sonarcloud-${{ github.ref }}, fuzz-${{ github.ref }},
eval-stats-${{ github.ref }}, eval-explain-${{ github.ref }}; only the
deliberately-singleton release and post-deploy are global. With
cancel-in-progress: true, a pull-request run and a main run therefore
cancel each other, which is where demo's cancelled runs come from. Fix is
group: demo-${{ github.ref }} regardless of what is decided about the toggle.
Worth reconciling while here: the tree disagrees with itself on the history.
demo-gif-workflow-has-never-succeeded says "3 runs, 3 failures", while
demo.yml and ci-failure-issue.yml both say six of six.
Complexity S
Impact Med
Wow ★★★★
a repo setting blocks it — no edit to demo.yml can fix this one
PlannedHygiene · DX
68 of the repo's 77 branches are merged loop/* branches nobody deletes
The repository carries 77 branches, 70 of them loop/*. Classifying each by the
state of its pull request: 68 have a merged PR and are safe to delete, 1 has an open
PR (loop/dependabot-findings), and 1 has no PR on record. The default branch list is
almost entirely finished work.
The obvious measurement is wrong here, which is worth recording. Testing "is it merged?"
with compare(main...branch).ahead_by == 0 reports 0 of 70 as merged —
because this repo squash-merges, and a squash leaves the branch's original commits outside
main's history forever. Every merged branch looks permanently ahead. The state has to
come from the pull request, not from commit reachability.
The cause is a single repository setting: delete_branch_on_merge is false, so
nothing removes a head branch when its PR merges. No workflow prunes them either — none of
the 25 workflow files triggers on create/delete or touches
loop/*, and no scripts/ entry or Makefile target does branch cleanup.
The one branch-deletion automation in the tree is demo.yml's
delete-branch: true, which is create-pull-request tidying its own
bot/demo-gif branch and has nothing to do with loop/*. No documentation
asks anyone to clean up either: CONTRIBUTING.md, CLAUDE.md and
.claude/commands/roadmap-loop.md all describe branching and squash-merging, and none
of the three mentions deleting the branch afterwards.
Not a claim that branches are never deleted. The item files name 144 distinct
owner: loop/<topic> values against 70 surviving branches, so roughly 98 have
gone at some point — by hand, or by topic reuse. The accurate statement is narrower:
nothing in the repo causes cleanup, so merged branches accumulate until someone does it
manually.
Low impact and near-zero risk to fix — flip delete_branch_on_merge to true and
the backlog can be pruned by listing merged PR head refs. The reason it is worth doing at all is
that CLAUDE.md tells every agent to run git worktree list and inspect branch state
before touching a tree, and 68 stale entries make that check noisier for every concurrent loop.
Complexity S
Impact Low
Wow ★★
68 of 70 loop/* branches have a merged PR; one setting fixes it