Skip to content

US-45.6 (v2): a thread merges only on trusted CI for the checkpoint's exact commit - #910

Merged
mkreyman merged 4 commits into
masterfrom
feature/us-45.6-ci-evidence-v2
Sep 27, 2026
Merged

mkreyman merged 4 commits into
masterfrom
feature/us-45.6-ci-evidence-v2

Conversation

@mkreyman

@mkreyman mkreyman commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

US-45.6 (Epic 45, change threads), v2: CI evidence by exact SHA. Rewrites #909, whose third review round still found material defects in its trust model. The review gate is still running.

What changed from #909, and why

  • Trust: only GitHub Actions check runs satisfy a required check (a status can be posted by the implementer), and a checkpoint touching .github/workflows/ or .github/actions/ is refused ci_definition_changed, because Actions runs the workflow files of the commit under test.
  • Per-suite judgement: the latest run of each check suite counts and every suite must pass, so a green workflow never hides a red one with the same job name.
  • Wait origin: 24 hours from the story's entry into ci (GitHub's self-hosted queue ceiling), not from the checkpoint's recording.
  • Policy read live: the required list is the source's current one, so a renamed job can be corrected for stories in flight (no placement binding, no runner_dispatches column).
  • Kept from US-45.6: a thread-mode merge requires green CI on the checkpoint's exact commit #909's rounds: null-safe names, time-bounded uncounted CI waits that clear the fault count, a wait holding back only an allow, fail-closed with nothing read, contention as unevaluated, superseded evidence as a wait, row-locked evidence writes with no rewrite of an unchanged judgement, one paged check-runs read, OpenAPI limits from Source, a default stub.

Operator notes

Required checks are GitHub Actions JOB names, and each must run on every push to the thread branches (no path filter or job-level if:); a check that never appears is refused after the wait. A CI that reports only commit statuses cannot satisfy thread mode. GITHUB_TOKEN needs checks: read and commit statuses: read. Migration 20260927100000 (additive).

Mutations

A01-A47 cover every assertion added (failed/pending/missing/timeout/unset/not-read reasons, the trusted-app and per-suite rules, the CI-definition refusal including renames, live policy, ci-entry origin, evidence write ordering, superseded/busy handling, counter clearing, adapter paging and parsing, intake validation, controller and MCP wiring). All exit 0; A31 (entered_at's stage filter) survived its first run, its test was strengthened, and A31b exits 0. Invocations are in the branch's scratch log.

Review rounds

  • Round 1 (10 findings): trust moved from check-run metadata to the JOBS of GitHub Actions workflow runs that a push of the thread branch at the exact commit triggered; only success passes (a skipped job fails); paged reads deduped and bounded; statuses best effort; contention on the ci-entry read is a retry; unreadable diff fails closed (ci_definition_unknown); a story with no ci entry falls back to its checkpoint time; padded names refused; docs reference the constants. Scope stated: scripts the jobs run are code under review (US-45.3).
  • Round 2 (9 findings, four introduced by round 1's fixes): every job carrying a name in the newest run must pass (matrix legs); the ci-entry time is read only on the judged path; identical later reads advance read_at; composite action.yml anywhere is a CI definition; shape before dedupe; concurrent jobs reads; migration names its manual step.
  • Round 3 (the ceiling, 9 findings, none in the trust model): fixed in place following US-45.1: the change-thread ledger (narrowed; replaces #901) #902's round 3, no round 4. Newest run per workflow taken from the runs (a jobless running run holds a name pending, a jobless dead run fails it), bound counts workflows; read_at stamped at read start; evidence copied only on allow/refuse; the gate's forge reads run concurrently; MCP validation matches the server; the counter-clearing decision kept and its bound documented.

Every assertion added in each round has a bin/mutate.sh proof (exit 0); the last round's set (60 mutations, cited ones retargeted) all exit 0. One fix has no falsifiable test and is stated as such: reading the ci-entry time only on the judged path (which path reads it is not observable from a test).

… exact commit

Rewrite of #909, whose third review round still found material defects in one design
element: which CI results the gate trusts and against which policy.

- intake_sources.required_checks (migration 20260927100000), read LIVE by the gate, so a
  renamed job can be corrected for stories already in flight. A thread source must name at
  least one; local-gate and non-string names are refused.
- Only a check run created by GitHub Actions satisfies a required name. Commit statuses are
  read and recorded, never trusted: the implementer's runner can post them.
- A checkpoint changing .github/workflows/ or .github/actions/ (renames included) is refused
  ci_definition_changed for a human: Actions runs the workflow files of the commit under test.
- Per name, the latest run of each check suite counts and every suite must pass.
- A check still running or not reported is a CI wait: unevaluated, Retry-After 300, never
  counted toward the unevaluated bound (and it clears the count), refused
  required_check_timed_out 24 hours after the story ENTERED ci (Stages.entered_at/3). A
  wait holds back only an allow; any other refusal is decided at once.
- Evidence (required runs and local-gate only) is copied onto the checkpoint's
  gate_evidence["ci"] under a row lock; a record read no later than the stored one is
  :superseded (the allow path waits and re-evaluates), an identical judgement is not
  rewritten. An allow whose copy met contention is unevaluated, never an escalation.
- Required checks with nothing read fail closed (ci_evidence_not_read).
- MCP 2.108.0: intake_source_enroll/update take required_checks; merge_precondition names
  the reasons.

Mutations A01-A47 (A31 re-run as A31b after strengthening its test), all exit 0.
1+2. Only a JOB of a GitHub Actions workflow run that a PUSH of the thread branch at the
     checkpoint's exact commit triggered satisfies a required check (Actions runs + jobs
     APIs, filtered by the API and again locally), and only by concluding success: a job
     skipped because a needs: failed, or neutral, fails. A check run created by any other
     workflow, or a status, is never trusted. check_evidence/3 now takes the branch.
3.   Jobs are de-duplicated by id and a short list is refused as truncated; more than 10
     workflow runs per push is refused rather than read.
4.   Commit statuses are read best effort ({:unread, reason}), feeding only local_gate.
5.   Stages.entered_at/3 runs under answering_busy with a bounded lock wait; contention is
     the ci_entry_unreadable fact, a retry.
6.   Scope stated in the CiEvidence moduledoc: scripts the jobs run are code under review,
     judged by the thread review (US-45.3), not by this gate.
7.   A diff that could not be listed refuses ci_definition_unknown (fail closed).
8.   A story with no recorded ci entry measures its wait from the checkpoint's recording.
9.   A required check name with surrounding whitespace is refused.
10.  OpenAPI interpolates ci_wait_retry_after/0 and ci_wait_limit_seconds/0; MCP, verdict
     and CHANGELOG no longer restate the numbers. Token scope is now actions: read.

Per-workflow judgement uses the highest job id (ids only grow); the separate newest-run
filter was redundant and removed (mutation B02 showed it could not fail).

Mutations A01-A47 (A14-A18, A20-A22, A25 superseded by the B set; A19 by B01; A30
retargeted) and B01, B03-B15, all exit 0.
1. Per workflow, only its newest run counts, and in it EVERY job carrying a required name
   must pass: matrix legs sharing a name are separate jobs (round 1 wrongly removed the
   newest-run step and judged the highest job id alone).
2. The ci entry time is read on exactly the path that reads CI, so a lock wait on it can no
   longer mask a merged or moved head's decision.
3. An identical evidence read that is later advances the stored read_at, so a slower read
   from in between is :superseded; an identical earlier read is :ok.
4. OpenAPI ci_evidence, the delivery-loop doc and the Source field comment describe the jobs
   design, not v1's check runs and statuses.
5. A composite action's action.yml anywhere in the diff is a CI definition change.
6. The jobs list's shape is judged before de-duplicating, so a malformed entry is
   unreadable_jobs, never a crash.
7. Per-run jobs reads run concurrently (4 at a time); the moduledoc restates the ceiling.
8. The migration names the manual step for thread sources enrolled before it; CHANGELOG
   gives the real column type.
9. ci_result/1 uses Map.fetch!: CI is judged once, in judge/1.

Mutations re-run: A01-A47 and B01-B15 as retargeted, plus C01, C02, C04-C10 (C10 and A40
re-run as C10b/A40b after a test for the identical-earlier case). All exit 0. Finding 2's
gating has no falsifiable test: which path reads the entry time is not observable from a
test.
…nce on decisions

Fixed in place (no round 4; every fix carries mutation proof), following #902's round 3:
none of the findings touched the trust model the rewrite settled.

- Runs (findings 2, 3, 4, 7): the adapter reduces the push runs to each workflow's NEWEST
  run before bounding and before any jobs read, and returns those runs. CiEvidence takes the
  newest run per workflow from the runs, once per judgement: a newest run with no jobs yet
  holds a name pending; one that ended with no jobs (startup_failure) fails a name no job
  carries (run_<conclusion>).
- Evidence (findings 1, 5): read_at is stamped when the read STARTS; evidence is copied onto
  the checkpoint only for a decision (allow, refuse), so CI-wait polls write nothing.
- Latency (finding 6): the gate's three forge reads (two trees, CI evidence) run
  concurrently.
- MCP (finding 9): required_checks refuses surrounding whitespace and duplicates locally,
  matching the server.
- Counter (finding 8): kept clearing on a pure CI wait (round 2's decision); documented why
  the wait is still bounded — every answered poll is judged against the CI wait limit.

Mutations: the cited set re-run (A06b, A40b, C10b retargeted) and D01-D05, D07-D09 (D03 re-proved as D03b after a complexity split), all 60
exit 0.
@mkreyman
mkreyman enabled auto-merge (squash) September 27, 2026 07:42
@mkreyman
mkreyman merged commit c95d021 into master Sep 27, 2026
16 checks passed
@mkreyman
mkreyman deleted the feature/us-45.6-ci-evidence-v2 branch September 27, 2026 07:47
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