Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8607271
chore(protection): retire the last exemption — the allow-list is now …
hyperpolymath Oct 24, 2026
37c45a4
docs: record the decision-layer fix everywhere a reader or agent woul…
hyperpolymath Oct 24, 2026
d9a78dd
fix(protection): the bypass removal missed three rulesets, one of the…
hyperpolymath Oct 24, 2026
da9073d
docs(ci): correct audit claims and clarify context exemption scope
coderabbitai[bot] Oct 24, 2026
8ed2f3a
Merge remote audit updates and preserve context review corrections
coderabbitai[bot] Oct 24, 2026
620060f
fix(protection): a failed read-back must not report success
hyperpolymath Oct 24, 2026
d4b3d28
fix(gate): a data-dependent rule blocks every PR and was invisible
hyperpolymath Oct 24, 2026
553bbd7
docs(gate): the failure message names both shapes of finding
hyperpolymath Oct 24, 2026
0aca273
docs(audit): the data-dependent rules are not repairable in-repo
hyperpolymath Oct 24, 2026
d49f8f7
fix(gate): a data-rule-only finding must fail strict mode
hyperpolymath Oct 24, 2026
88699f4
Update scripts/fix-required-context-name.sh
hyperpolymath Oct 25, 2026
0857173
docs(gate): clarify data-rule inspection and audit behavior
coderabbitai[bot] Oct 25, 2026
dc8caa8
fix(ci): Harden required-context audits and ruleset repair verification
coderabbitai[bot] Oct 25, 2026
477ff71
Merge remote branch updates and preserve required-context review fixes
coderabbitai[bot] Oct 25, 2026
97cd2e4
feat(ci): measure and publish real line coverage
hyperpolymath Oct 25, 2026
583101e
fix(ci): name the coverage context by slug, and publish the number
hyperpolymath Oct 25, 2026
57059c6
feat(ci): report the line counts, not just the percentage
hyperpolymath Oct 25, 2026
48fa3c9
docs(audit): correct the claim that coverage is unrepairable, with th…
hyperpolymath Oct 25, 2026
ffe1094
fix(gate): stop claiming nothing uploads while a workflow does
hyperpolymath Oct 25, 2026
faf2a1b
fix(ci): measure the commit the upload is attributed to
hyperpolymath Oct 25, 2026
64ada82
feat(gate): tell an unverified rule from an unsatisfiable one
hyperpolymath Oct 25, 2026
609deeb
Update scripts/check-required-contexts.sh
hyperpolymath Oct 25, 2026
0f433ca
Update scripts/check-required-contexts.sh
hyperpolymath Oct 25, 2026
98ebde4
fix(ci): Verify Code Quality evidence across all workflow pages
coderabbitai[bot] Oct 25, 2026
04acc6b
Merge remote head and preserve required-context evidence hardening
coderabbitai[bot] Oct 25, 2026
83afb5d
Update scripts/check-required-contexts.sh
hyperpolymath Oct 25, 2026
107d4c7
fix(ci): Match the correct GitHub Code Quality workflow path
coderabbitai[bot] Oct 25, 2026
c3e9fad
Merge commit '83afb5d2efb9b11d2b51d8d2706e2f21f34d52ca' into arena/94…
coderabbitai[bot] Oct 25, 2026
ddc6180
test(ci): Correct Code Quality workflow paths in required-context fix…
coderabbitai[bot] Oct 25, 2026
1d9ef56
Merge commit 'c3e9fad20d74964013bb91a29547a3bc60aa3579' into arena/94…
coderabbitai[bot] Oct 25, 2026
3dfb5fd
fix(gate): evidence only where a rule applies, and never a guessed br…
hyperpolymath Oct 25, 2026
95847d1
fix(gate): stop trusting a green coverage run that uploaded nothing
hyperpolymath Oct 25, 2026
d8d08ba
fix(ci): recognize exact canonical origins after standards owner rename
coderabbitai[bot] Oct 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .github/workflows/required-contexts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ jobs:
fi
# Findings = every required context that cannot be satisfied on a pull
# request. `app-owned` and `producible` are not findings.
jq -r '[.results[] | select(.verdict == "PR-UNPRODUCIBLE" or .verdict == "UNSATISFIABLE") | .context] | .[]' \
# Three verdicts are findings, and the third is easy to forget: a
# data-dependent RULE (`code_coverage`, `code_quality`) never appears
# as a status context, so it cannot be caught by looking at contexts,
# yet it blocks every pull request exactly like an unproducible one.
jq -r '[.results[] | select(.verdict == "PR-UNPRODUCIBLE" or .verdict == "UNSATISFIABLE" or .verdict == "UNSATISFIED-RULE") | .context] | .[]' \
"$JSON" > "$RUNNER_TEMP/findings.txt"
awk '{ sub(/#.*/, ""); gsub(/^[ \t]+|[ \t]+$/, ""); if ($0 != "") print }' \
"$ALLOW" > "$RUNNER_TEMP/allow.txt"
Expand Down Expand Up @@ -146,9 +150,9 @@ jobs:
echo "::warning::$n_stale declared exemption(s) no longer match anything — remove them from $ALLOW"
fi
if [ "$n_new" -gt 0 ]; then
echo "::error::$n_new required context(s) cannot be published on a pull request and are not declared in $ALLOW"
echo "::error::$n_new requirement(s) cannot be satisfied on a pull request and are not declared in $ALLOW — see the findings above; a `rule:` entry is a rule whose data nothing uploads, a bare name is a status context nothing publishes"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
sed 's/^/ /' "$RUNNER_TEMP/undeclared.txt"
echo "Every pull request waits for a check run that cannot happen. Either publish it on pull_request, require the name the PR-time workflow actually emits, or declare it in $ALLOW with a reason."
echo "For a status context: publish it on pull_request, or require the name the PR-time workflow actually emits. For a rule: upload the data it needs, or remove the rule — a coverage threshold on a repository that publishes no coverage blocks every pull request forever, and an undecidable gate is one people bypass. Declaring it in $ALLOW is the last resort, not the repair."
exit 1
fi
echo "All unsatisfiable required contexts are declared in $ALLOW ($n_find declared)."
echo "All unsatisfiable requirements are declared in $ALLOW ($n_find declared)."
78 changes: 50 additions & 28 deletions .machine_readable/required-contexts-allow.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
#
# Every line is a debt with a name and a reason: `context`
#
# THE LIST IS EMPTY, AND THAT IS THE POINT
# ----------------------------------------
# This file's preamble has always said that removing the last entry is the
# goal and not an edge case. The one entry has now been removed, because the
# defect it recorded was repaired at the decision layer rather than declared
# here. Empty is the correct resting state; if an entry comes back, it is a
# regression with a review trail, not a configuration.
#
# WHY THIS FILE EXISTS INSTEAD OF `continue-on-error: true`
# ---------------------------------------------------------
# The alternative was an always-green advisory job, and this repository has
Expand All @@ -15,33 +23,47 @@
# was never wired, so it never disagreed with anything.)
#
# So the job FAILS on any unsatisfiable required context that is not listed
# here, and passes only for the ones below, which are reported on every run.
# A new entry cannot be added quietly: this file is the review surface.
#
# Removing the last entry is the point, not an edge case.
#
# The known entry is a branch RULE, not a missing workflow. The scan it names
# does run on every pull request — as `Hypatia verdict grid / Hypatia
# Neurosymbolic Analysis`, the fork-governance lane. The required context names
# the OTHER call site of the same reusable workflow: hypatia-scan.yml, whose
# `scan` job is deliberately push/schedule-only so the same findings are not
# uploaded to code scanning twice per pull request. So the defect is a context
# name pointing at the post-merge call site of a scan that is gated pre-merge
# under a different name.
#
# Consequence, measured on #1336: 65 checks green, reviewDecision clear,
# mergeable, and still BLOCKED — because the required check can never appear.
# Every pull request in this repository is in that state, and the only way work
# lands is the ruleset's `always` bypass, which discards the other nineteen
# required checks in the same action. That is the whole failure mode this
# repository keeps re-learning: the bypass is not an escape hatch used
# occasionally, it is the actual merge path, and it makes the nineteen
# decorative.
#
# Repair (decision layer, needs repository admin — the ruleset, not this file):
# replace the context below with `Hypatia verdict grid / Hypatia Neurosymbolic
# Analysis` in ruleset 23787415, leaving the other nineteen untouched.
# here. A new entry cannot be added quietly: this file is the review surface,
# and the gate also warns when a listed context stops appearing as a finding, so
# this file cannot quietly rot into a museum of old exemptions either.
#
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# RECORD OF THE RETIRED ENTRY
# ---------------------------
# Context: `scan / Hypatia Neurosymbolic Analysis`
# Retired: 2026-10-24
#
# It named the second call site of hypatia-scan-reusable.yml. `hypatia-scan.yml`
# declares that job as push/schedule-only on purpose, so the same findings are
# not uploaded to code scanning twice per pull request; the pull-request call
# site is the fork-governance lane, which publishes
# `Hypatia verdict grid / Hypatia Neurosymbolic Analysis`. A required check is
# matched by name, and the names differ, so the required one could never be
# produced while a pull request was open.
#
# Measured consequence: #1336 reached 65 passing checks, a cleared
# reviewDecision and mergeable=MERGEABLE while mergeStateStatus stayed BLOCKED.
# #1337, which carried the repair, merged 79 minutes BEFORE the ruleset was
# corrected — so the fix provably did not enable its own merge. #1337's merge
# used the ruleset's `always` bypass, which discards every required check in the
# same action. This record does not establish that #1336 merged or used the bypass.
#
# Retired by the decision layer, as this file asked:
# ruleset 23787415, required_status_checks:
# scan / Hypatia Neurosymbolic Analysis
# -> Hypatia verdict grid / Hypatia Neurosymbolic Analysis
# bypass_actors: always on RepositoryRole:5 -> removed
# strict_required_status_checks_policy: false -> true
# + `Repo self-tests` required (21 contexts, all producible on a pull request)
#
# Doing it in this file instead would have required continuing to bypass, and a
# list of declared exceptions is only safer than a bypass while it stays short.
#
# Format: one context per line; `#` starts a comment; blank lines ignored.

scan / Hypatia Neurosymbolic Analysis
#
# An empty file (or one holding only comments) asserts that no repository-owned
# `PR-UNPRODUCIBLE` or `UNSATISFIABLE` finding is undeclared. It does
# NOT assert that every required context is satisfiable: the reconciliation step
# excludes `app-owned` contexts (those produced by a GitHub App other than
# Actions), and it only judges contexts that are findings. Both exclusions are
# deliberate — but they mean empty is a narrower claim than "all good", and
# this file should not be read as one.
3 changes: 2 additions & 1 deletion 0-AI-MANIFEST.deed
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
(rule :priority 10 :text "No A2ML/DEED duplication — root must NOT contain STATE.deed, META.deed, etc.")
(rule :priority 20 :text "The repo deed routes to state evidence; generated metadata does not confer normative authority.")
(rule :priority 30 :text "Initial licence assignment is permitted: agents may add the correct SPDX header to new files under the repository policy and set a brand-new repository's initial LICENSE. Changes to existing licences, LICENSE files or SPDX declarations are Manual-Only and restricted to registered codeowner-admins. Agents must flag licence drift for review; never sweep or relicense existing content. See LICENSE, .machine_readable/licensing-policy.toml, and .claude/CLAUDE.md.")
(rule :priority 40 :text "Author attribution — Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>"))
(rule :priority 40 :text "Author attribution — Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>")
(rule :priority 50 :text "Never merge through a branch-ruleset bypass to get past a required status check. A bypass discards every required check in that ruleset in a single action, so using one converts the rest of the decision layer into advice. If a pull request is green everywhere and still BLOCKED, the required context is not producible on a pull request: diagnose it with scripts/check-required-contexts.sh, repair the rule with scripts/fix-required-context-name.sh (report-only by default), and flag it for a human to apply. Downgrading the gate — continue-on-error, || true, or a new line in .machine_readable/required-contexts-allow.txt — is not a repair. See docs/audits/audit-ci-context-producibility-2026-09-20.adoc."))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

(startup-checklist
:steps ("Read THIS file, then README.adoc."
Expand Down
4 changes: 3 additions & 1 deletion CICD-WORKFLOW-CATALOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ These workflows **block** merges if they fail. They enforce critical estate-wide
| `governance-reusable.yml` | Reusable governance | standards | Yes |
| `hypatia-scan.yml` | Security scanning with Hypatia | rsr-template-repo | Yes |
| `hypatia-scan-reusable.yml` | Reusable Hypatia scan | standards | Yes |
| `fork-governance.yml` | The one PR comment (DEED · K9-DEED conformance · Hypatia verdict grid) plus the PR-time scanner. This is the lane that publishes `Hypatia verdict grid / Hypatia Neurosymbolic Analysis`, so a ruleset requiring that context depends on this workflow running on every PR | standards | `fork-governance-reusable.yml` |
| `required-contexts.yml` | Asserts every context the branch ruleset requires is actually publishable on a pull request. A required check nothing can publish makes every PR undecidable; this gate catches that before the ruleset has to be bypassed | standards | No |
| `main-estate-audit.yml` | Estate audit checks | rsr-template-repo | Yes |
| `scorecard.yml` | OSSF Scorecard | rsr-template-repo | Yes |
| `scorecard-reusable.yml` | Reusable Scorecard | standards | Yes |
Expand All @@ -51,7 +53,7 @@ These workflows **block** merges if they fail. They enforce critical estate-wide
| `guix-policy.yml` | Guix/Nix package policy | knot-rider (canonical) | No |
| `secret-scanner.yml` | Secrets detection | rsr-template-repo | Yes |
| `secret-scanner-reusable.yml` | Reusable secrets scanner | standards | Yes |
| `runtime-policy.yml` | Runtime security policies | rsr-template-repo | No |
| ~~`runtime-policy.yml`~~ | **RETIRED:** removed from `rsr-template-repo`; `tests/workflows/governance_policy_test.sh` asserts the file and its lock record must not exist. Listed here only so the retirement is discoverable rather than looking like an omission | — | No |
| `static-analysis-gate.yml` | Static analysis gate | rsr-template-repo | No |
| `wellknown-enforcement.yml` | .well-known file enforcement | rsr-template-repo | No |

Expand Down
6 changes: 6 additions & 0 deletions EXPLAINME.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ stick. Former satellite repos were absorbed as subdirectories on 2026-02-08.
| SHA-pinned GitHub Actions
| Prevents supply-chain attacks from mutable tags.

| A required check must be producible on a pull request
| A gate only means something if a pull request can satisfy it. A required context that only a post-merge run publishes makes every pull request undecidable, and forces the bypass that discards every other check. Producibility is therefore checked, not assumed (`scripts/check-required-contexts.sh`).

| No standing bypass on the default branch
| An `always` bypass actor turns 21 required checks into advice, because using it once discards all of them at once. The emergency path is disabling the ruleset, which is an auditable act, not a silent merge.

| Guix-first package management
| Reproducible builds via Guix.
| Elegance is the default arm (proposed)
Expand Down
40 changes: 40 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,46 @@ The link:.github/workflows/k9-contractile.yml[DEED–K9 workflow] publishes one
compact PR report, with expandable findings and links to dated evidence.
See link:1-formats/k9/tools/CONFORMANCE-REPORTING.adoc[its scope, colours and reproduction guide].

=== What actually blocks a merge

`main` is gated by the ruleset *main gate: append-only + required checks +
signatures + scanning*: 21 required status checks, branches required to be up to
date, and **no bypass actors**. Nothing merges without producing all 21.

That last clause is the one that is easy to lose, so it is worth stating what a
gate is *not*. A required check that no pull request can publish does not make
the gate stricter — it makes every pull request undecidable. On 2026-10-24 this
repository merged red repeatedly for exactly that reason, and not because a check
failed: pull request #1336 stood at 65 passing checks, a cleared review and
`mergeable=MERGEABLE` while `mergeStateStatus` stayed `BLOCKED`, because the
required context named the post-merge call site of a scan that runs pre-merge
under a different name. The only way through was the ruleset bypass, which
discards all 21 required checks in the same action.

Three things now keep that from recurring:

* *Producibility is a gate.*
link:.github/workflows/required-contexts.yml[`required-contexts.yml`] runs
link:scripts/check-required-contexts.sh[`scripts/check-required-contexts.sh`]
on every pull request and weekly. It derives which contexts the workflows can
actually publish on a PR and fails on any required context that is not among
them.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
* *Exemptions are declared, dated and shrink-only.*
link:.machine_readable/required-contexts-allow.txt[`required-contexts-allow.txt`]
lists known-unsatisfiable contexts with a reason. It is currently **empty**,
which is its intended resting state; the gate warns when a listed entry stops
being required, so it cannot rot into a museum of stale exemptions. Silence is
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
never achieved with `continue-on-error` or `|| true` — both were tried here and
both produced an always-green job that reported a gate it never ran.
* *The repair is scripted.*
link:scripts/fix-required-context-name.sh[`scripts/fix-required-context-name.sh`]
repoints a single named context at a single named ruleset, refuses every state
it cannot justify, and verifies the read-back. Branch protection is edited by
a narrow instrument or not at all.

The full measurement, including the 3h20m of red `main` that motivated it, is in
link:docs/audits/audit-ci-context-producibility-2026-09-20.adoc[the context-producibility audit].

=== CI/CD Workflows

Copy these workflows to `.github/workflows/` in your repository:
Expand Down
16 changes: 15 additions & 1 deletion coordination.k9
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ metadata:
schema: k9-coordination
schema_version: 1.2.0
generated_by: manual
last_updated: 2026-10-14
last_updated: 2026-10-24

project:
name: "standards"
Expand Down Expand Up @@ -88,6 +88,14 @@ invariants:
rule: "All GitHub Actions must be SHA-pinned, not version-tagged"
reason: "Version tags are mutable. SHA pins are immutable. This is a security invariant."
severity: high
- id: required-checks-are-producible
rule: "Every context required by a branch ruleset MUST be publishable on a pull request. A context only a post-merge run produces is a defect, never a stricter gate."
reason: "A required check nothing can publish makes every pull request undecidable, and the only way through is a bypass that discards every other required check at once. Measured 2026-10-24: #1336 had 65 passing checks, a cleared review and mergeable=MERGEABLE while BLOCKED. Checked by scripts/check-required-contexts.sh on every pull request and weekly."
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
severity: critical
- id: no-standing-bypass
rule: "No ruleset guarding the default branch may carry an `always` bypass actor. The emergency path is disabling the ruleset deliberately, which is auditable."
reason: "Bypass is all-or-nothing: one use discards every required check in the ruleset. A standing bypass therefore makes the whole decision layer advisory, and leaves no trace at merge time."
severity: critical

protected:
- path: component-readiness-grades/
Expand Down Expand Up @@ -182,6 +190,12 @@ innervation:
- trigger: "file-create"
guard: "No banned language files (*.ts, *.res, *.py, *.go, *.java, *.kt)"
action: "Reject with language policy link"
- trigger: "pull_request"
guard: "Every required status context is publishable on a pull request"
action: "Fail with the workflow and event list that could not publish it; never silence with continue-on-error"
- trigger: "ruleset-edit"
guard: "No `always` bypass actor on a ruleset guarding the default branch"
action: "Refuse the edit; the emergency path is disabling the ruleset deliberately"

do_not_create:
- pattern: **/*.ts
Expand Down
Loading
Loading