docs(853): workflow_dispatch can't be ref-restricted at 1.27.1 — and restricting it would close nothing #888

Merged
timothy merged 8 commits from fix/853-dispatch-ref-restriction into main 2026-08-30 03:18:05 +02:00
Owner

Closes the #853 probe-and-decide task. Docs-only: one new decision record, two updated records, docs/ci-cd.md, and a comment block in ci-image.yml.

What was probed

#853 warned explicitly: "Do NOT close this by asserting the boundary without checking option 3 first — an unprobed 'nothing can be done' is the shape this repo has been wrong about before." So every surface was checked, not just the endpoint the issue already knew 404s.

Surface Result
POST .../workflows/{id}/dispatches schema ref is required, free-form. No allow-list, no pattern, no filter field
Whole REST API (308 paths) 0 contain environment — protected environments do not exist at 1.27.1
Actions secrets org / repo / user scope only. No per-ref, no per-environment gate
/api/v1/settings/actions 404 (issue's claim reproduced)
Loaded config (/etc/gitea/app.ini, per the process's own --config) [actions] sets only ENABLED, DEFAULT_ACTIONS_URL
gitea CLI one Actions subcommand: actions generate-runner-token. Restricts nothing

Option 3 is unavailable. Recorded with stale-after: 2027-02-28 — these are claims about the outside world, due for re-confirmation after a Gitea upgrade.

Why it's accepted — and NOT for the reason the issue proposed

The issue's leading option was "anyone with repository write can already do worse." That argument is rejected here: it is unfalsifiable as stated, and it skips the question that decides the matter — whether dispatch is the cheapest route. It isn't.

docker-build.yml triggers on pull_request:, which Gitea resolves from the PR head. Its test job carries no event guard, is the required context Build ErsatzTV Image / Build & test (.NET) (pull_request), and logs into the registry with secrets.REGISTRY_PASSWORD (api-docs and format likewise). So "push a branch, open a PR" reaches the same credential with one fewer deliberate act than a dispatch. Restricting dispatch would close the more visible of two routes and change nothing.

Scoped, not generalised. renovate.yml reaches RENOVATE_TOKEN/GH_COM_TOKEN and has no pull_request: route — there a ref restriction would close something; it is unavailable, not pointless. Whether head-supplied YAML on the PR route could simply name an arbitrary repo secret is left unmeasured on purpose: the measurement prints a live credential into a run log.

Corrections to #853's own table, verified against the tree

  • dependency-scan.yml references no secrets. at all — its "scan credentials" row is wrong.
  • "four workflows carry an unrestricted workflow_dispatch" is correct (pr-checks.yml's three hits are comments; its trigger is pull_request: only).

Actor set (measured)

Exactly two write-capable accounts: timothy (admin) and renovate (write, not admin). The merge gate bounds what Renovate can merge; nothing bounds what it can run — a matched probe with anonymous and authenticated-non-writer controls shows renovate passes the dispatch authorization gate.

Deliberately not done here

tag_protections is empty, and a v* rule would close docker-build.yml's tag row against the bot. Not applied: that is protection-class config whose failure mode is a broken release cut, so it needs its own change and its own verification. Tracked with the pull_request: residual in #885.

Verification

  • scripts/tests full suite on the rebased base: 1250 passed, 2 skipped against 1252 collected — exact match, so nothing silently failed to collect. (Count rose from 1245 because #884 added a test file, confirming the post-rebase tree.)
  • decisions_validate.py: OK; catalog regenerated byte-identically; record is 70 prose lines, over the 60-line warning ceiling — 44/215 = 20.5%, inside the validator's 2–25% calibration band
  • ci-image.yml parses; its triggers are byte-for-byte unchanged (comment-only edit), so no CI behaviour delta and no ci-image-pin churn
  • No .cs touched, so the BOM/format gate does not apply
  • Rebased onto origin/main 94a3d1349 after #884 landed, and the PR-route job population was re-derived on the new base rather than carried over

Review

Four cold review rounds in isolated worktrees. Rounds 1–2 returned NOT-MERGEABLE (12 and 5 findings); rounds 3 and the delta pass returned MERGEABLE. Every repo-internal claim a reviewer made was verified against the tree before acting on it.

Deviation stated: Codex hit its usage limit, so all reviews are same-family rather than cross-family.

Notable, because it is the theme of the branch: every defect found was in the evidence, never the conclusion — a CLI probe that had exited 1 and printed nothing; a claim generalised past what was measured; an "invariant" written to replace a stale list that was itself stale on day one (it named 5 of 6 jobs); and finally the honesty paragraph itself borrowing authority for an inference. The decision has not moved since the first commit.

fixes #853

Closes the #853 probe-and-decide task. Docs-only: one new decision record, two updated records, `docs/ci-cd.md`, and a comment block in `ci-image.yml`. ## What was probed #853 warned explicitly: *"Do NOT close this by asserting the boundary without checking option 3 first — an unprobed 'nothing can be done' is the shape this repo has been wrong about before."* So every surface was checked, not just the endpoint the issue already knew 404s. | Surface | Result | |---|---| | `POST .../workflows/{id}/dispatches` schema | `ref` is **required, free-form**. No allow-list, no pattern, no filter field | | Whole REST API (308 paths) | **0** contain `environment` — protected environments do not exist at 1.27.1 | | Actions secrets | org / repo / user scope only. No per-ref, no per-environment gate | | `/api/v1/settings/actions` | 404 (issue's claim reproduced) | | Loaded config (`/etc/gitea/app.ini`, per the process's own `--config`) | `[actions]` sets only `ENABLED`, `DEFAULT_ACTIONS_URL` | | `gitea` CLI | one Actions subcommand: `actions generate-runner-token`. Restricts nothing | **Option 3 is unavailable.** Recorded with `stale-after: 2027-02-28` — these are claims about the outside world, due for re-confirmation after a Gitea upgrade. ## Why it's accepted — and NOT for the reason the issue proposed The issue's leading option was "anyone with repository write can already do worse." That argument is rejected here: it is unfalsifiable as stated, and it skips the question that decides the matter — whether dispatch is the *cheapest* route. It isn't. `docker-build.yml` triggers on `pull_request:`, which Gitea resolves from the **PR head**. Its `test` job carries no event guard, is the required context `Build ErsatzTV Image / Build & test (.NET) (pull_request)`, and logs into the registry with `secrets.REGISTRY_PASSWORD` (`api-docs` and `format` likewise). So **"push a branch, open a PR" reaches the same credential with one *fewer* deliberate act than a dispatch.** Restricting dispatch would close the more visible of two routes and change nothing. **Scoped, not generalised.** `renovate.yml` reaches `RENOVATE_TOKEN`/`GH_COM_TOKEN` and has no `pull_request:` route — there a ref restriction *would* close something; it is unavailable, not pointless. Whether head-supplied YAML on the PR route could simply *name* an arbitrary repo secret is left **unmeasured on purpose**: the measurement prints a live credential into a run log. ## Corrections to #853's own table, verified against the tree - `dependency-scan.yml` references **no** `secrets.` at all — its "scan credentials" row is wrong. - "four workflows carry an unrestricted `workflow_dispatch`" is **correct** (`pr-checks.yml`'s three hits are comments; its trigger is `pull_request:` only). ## Actor set (measured) Exactly two write-capable accounts: `timothy` (admin) and `renovate` (`write`, **not** admin). The merge gate bounds what Renovate can **merge**; nothing bounds what it can **run** — a matched probe with anonymous and authenticated-non-writer controls shows `renovate` passes the dispatch authorization gate. ## Deliberately not done here `tag_protections` is empty, and a `v*` rule would close `docker-build.yml`'s tag row against the bot. Not applied: that is protection-class config whose failure mode is a **broken release cut**, so it needs its own change and its own verification. Tracked with the `pull_request:` residual in **#885**. ## Verification - `scripts/tests` full suite on the rebased base: **1250 passed, 2 skipped** against **1252 collected** — exact match, so nothing silently failed to collect. (Count rose from 1245 because #884 added a test file, confirming the post-rebase tree.) - `decisions_validate.py`: OK; catalog regenerated byte-identically; record is 70 prose lines, over the 60-line *warning* ceiling — 44/215 = 20.5%, inside the validator's 2–25% calibration band - `ci-image.yml` parses; its triggers are byte-for-byte unchanged (comment-only edit), so no CI behaviour delta and no `ci-image-pin` churn - No `.cs` touched, so the BOM/format gate does not apply - Rebased onto `origin/main` 94a3d1349 after #884 landed, and the PR-route job population was **re-derived on the new base** rather than carried over ## Review Four cold review rounds in isolated worktrees. Rounds 1–2 returned NOT-MERGEABLE (12 and 5 findings); rounds 3 and the delta pass returned MERGEABLE. Every repo-internal claim a reviewer made was verified against the tree before acting on it. **Deviation stated:** Codex hit its usage limit, so all reviews are same-family rather than cross-family. Notable, because it is the theme of the branch: **every defect found was in the *evidence*, never the conclusion** — a CLI probe that had exited 1 and printed nothing; a claim generalised past what was measured; an "invariant" written to replace a stale list that was itself stale on day one (it named 5 of 6 jobs); and finally the honesty paragraph itself borrowing authority for an inference. The decision has not moved since the first commit. fixes #853
timothy added 8 commits 2026-08-30 02:46:07 +02:00
#853 asked whether Gitea 1.27.1 can restrict `workflow_dispatch` by ref, or gate a
secret behind a protected environment, and warned against closing the issue by
asserting the trust boundary without probing option 3 first.

Probed across every surface rather than stopping at the endpoint the issue already
knew 404s:
  - `CreateActionWorkflowDispatch` takes `ref` as a REQUIRED free-form string, with
    no allow-list or pattern field, and no repo/org/admin endpoint scopes it.
  - Protected environments do not exist at 1.27.1 — 0 of 308 documented API paths
    contain "environment"; secrets are org/repo/user-scoped only.
  - `/api/v1/settings/actions` 404 reproduced; host `[actions]` config carries only
    ENABLED + DEFAULT_ACTIONS_URL; no `gitea admin` subcommand touches actions,
    runners or secrets.

So option 3 is unavailable. But the probe also found why the answer does not rest on
that: dispatch is NOT the narrowest route. `docker-build.yml` triggers on
`pull_request:`, which Gitea resolves from the PR head, and its `test` job carries no
event guard, is the required context `Build & test (.NET) (pull_request)`, and logs
into the registry with REGISTRY_PASSWORD. "Push a branch, open a PR" therefore reaches
the same credential with one FEWER deliberate act than a dispatch. Restricting
dispatch alone would close the more visible of two routes and change nothing.

Accepted on that reasoning rather than on "repository write access is the boundary" —
the issue's own leading option, and the one to reject: it is unfalsifiable as stated
and it hides the cheaper route.

Also measured, because it is easy to misread the existing records the other way: the
write set is exactly `timothy` (admin) and `renovate` (write, NOT admin). The merge
gate bounds what Renovate can MERGE; nothing bounds what it can RUN. A matched probe
with anonymous and authenticated-non-writer controls shows `renovate` passes the
dispatch authorization gate — an authenticated non-writer short-circuits with
"not found", while `renovate` here reaches ref resolution like a known writer does.

Not applied here, deliberately: `tag_protections` is empty and a `v*` rule would close
docker-build.yml's tag row against the bot. That is live protection-class config whose
failure mode is a broken release cut, so it needs its own change and its own
verification. Tracked with the `pull_request:` residual in #885.

fixes #853

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UYNbVwgVszv6Pum7ZuGd75
Two defects in the previous commit, both in the EVIDENCE rather than the conclusion.

1. The claim "no `gitea admin` subcommand touches actions, runners or secrets" rested on
   `gitea --help` run as root — which exits 1 (`mustNotRunAsRoot`) and prints no help at
   all. The grep matched nothing because there was nothing to match, not because the
   surface is empty. Run as the service user, the CLI DOES list a `gitea actions`
   command. Its sole subcommand is `generate-runner-token`, so the conclusion stands —
   but "I checked the CLI" was not true as written. The record now states the trap
   (check the exit code, not the match count) since it is the reusable part.

   Also pinned the config claim to the file the running process actually names in its
   own `--config` (/etc/gitea/app.ini), rather than to a path that happened to exist.

2. "Restricting dispatch alone would be theatre / would close nothing" over-generalised
   from one credential to all four workflows. Verified against the tree: `renovate.yml`
   reaches RENOVATE_TOKEN and GH_COM_TOKEN and has NO `pull_request:` trigger, so for
   those a ref restriction WOULD close a route — it is unavailable, not pointless. The
   claim is now scoped to the registry credential everywhere it appears, and the limit
   is stated rather than smoothed over.

   Whether head-supplied YAML on the PR route could simply NAME an arbitrary repo secret
   is left explicitly unmeasured: measuring it means printing a live credential into a
   run log.

   Bonus correction, verified: `dependency-scan.yml` references no `secrets.` at all, so
   #853's own table row claiming it reaches "scan credentials" is wrong.

The decision itself is unchanged — accept, because the capability does not exist.

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UYNbVwgVszv6Pum7ZuGd75
Two defects from the read-through of the committed tree:

- `doesn''t` in the record BODY. The doubled apostrophe is YAML single-quoted-string
  escaping, correct in the `rule:` field and meaningless five lines later in Markdown,
  where it renders literally. Checked every edited record for the same leak; this was
  the only one.

- The prose named the required check `Build & test (.NET) (pull_request)`, but branch
  protection stores it prefixed by the workflow name — `Build ErsatzTV Image / Build &
  test (.NET) (pull_request)`. The short form is a substring so a grep still finds it,
  but a record that names a required context should name it exactly.

Verification: full `scripts/tests` suite green — 1243 passed, 2 skipped, matching the
1245 collected, so the run was real and not an empty-collection green.

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UYNbVwgVszv6Pum7ZuGd75
Cold review returned NOT-MERGEABLE with 12 findings. F2/F6/F8 were already fixed in
earlier commits of this branch. The rest:

F1 (HIGH) — the reason given accepted four dispatches but only covered the registry
  credential, leaving `renovate.yml` (RENOVATE_TOKEN, GH_COM_TOKEN, no `pull_request:`
  trigger) unaddressed by its own stated logic. I had made this WORSE two commits ago by
  narrowing the claim and marking the general step "unmeasured". It was never unmeasured:
  `ci.gate-trigger-base-resolved` already records it verbatim — "any PR-added workflow can
  reference `RENOVATE_TOKEN`, a `write:repository` bot PAT in the same store". Since the
  PR route runs attacker-authored YAML, it reaches EVERY secret in the store, which is
  precisely what makes the argument cover all four dispatches. Restored the general claim
  with that citation, in the record and in all three satellites I had narrowed.

F3 — "two further jobs (api-docs, format) do likewise" undercounts 6 as 3. Verified by
  parsing the jobs block: toolchain-preflight, test, migrations, functional-e2e, api-docs
  and format all hold REGISTRY_PASSWORD and run on the PR route; only `build` is excluded
  (`!= 'pull_request'`), and test + migrations are BOTH required contexts. Now stated as
  the invariant "every `container:` job on the PR route" rather than a list, because #885's
  remediation was scoped off that list and would have missed half the sites. #885 corrected
  to match, with a derive-the-population Done-when box.

F4 — my insertion into `ci.actions-credential-scoping` put four lines between "assert the
  verdict" and "A collaborator's own token always can", orphaning the elided verb onto
  "logs into the registry on every PR". Restored the original adjacency and moved the
  clause after it.

F5 — "That residual is RESOLVED" bound to a three-item list (tag push, pull_request,
  dispatch) of which only the dispatch third is resolved, and the catalog surfaces that
  rule text. Now says so explicitly.

F7 — the residual clause was exclusive ("is not dispatch — it is the PR route"), dropping
  the `v*` tag push, which is cheaper still at a single act. Both now listed.

F9 — `sources:` cited /admin/users for a `permission: write` fact only the collaborators
  endpoint can establish. Added it.

F10 — "one fewer deliberate act" reads false to anyone counting raw steps (2 vs 2). It is
  true in this repo's sense, so the sense is now stated rather than assumed.

F11 — the Renovate exemption predicate named one protected directory of six.

F12 — `stale-after` is a date but the real invalidator is a version bump past 1.27.1,
  which can land months earlier. Said so.

Verification: workflow guards 111 passed; decisions-validate OK; catalog regenerated.

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UYNbVwgVszv6Pum7ZuGd75
Two leftovers from un-narrowing across four files:

- `ci.toolchain-image-publish-is-a-dispatch`'s rule ended "...remain open in #885. See
  `ci.workflow-dispatch-ref-unrestricted` and do not re-derive it; the live residual is
  #885." — a first-round tail that both repeats the record pointer and says "the live
  residual" in the singular immediately after correctly naming two. The catalog surfaces
  rule text verbatim, so a skim got the singular.

- The ci-image.yml insertion collided with the original sentence and left a 120-char
  comment line in a block that wraps at ~100.

Swept for orphans the reversal could have left: no surviving "scoped to the registry
credential" or "unmeasured" phrasing anywhere in the touched files, and the general claim
now reads consistently in all five places that state it.

Verification: ci-image.yml parses; workflow guards 103 passed; decisions-validate OK.

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UYNbVwgVszv6Pum7ZuGd75
Second cold review, NOT-MERGEABLE, 5 findings. All five confirmed against the tree.

F1 (HIGH) — the replacement invariant "every `container:` job on the PR route" names
  FIVE of the six jobs it was written to cover. `toolchain-preflight` is deliberately
  container-free (`runs-on: small`, credential via `ETV_REGISTRY_AUTH` at :154), and
  docker-build.yml has exactly five `container:` blocks. So the predicate I introduced
  to stop a stale six-name list from being carried around reproduced that staleness
  immediately. Correct predicate: every job on the PR route that NAMES
  `secrets.REGISTRY_PASSWORD`. Re-derived after the rebase — still six, still five
  containerised. Fixed in the record (rule + body), ci-cd.md, and #885 (body + a
  correction comment), and the wrong predicate is now named explicitly as the one to
  resist, since it is the tempting one.

F2 — my own orphan sweep missed a fragment. Deleting the bolded scope from "for the
  **registry credential** restricting dispatch..." left "More to the point, for the
  restricting dispatch would close nothing". Grep finds phrases; only reading finds the
  residue a deletion leaves behind.

F3 — ci-cd.md:2093 was the one site of five still saying #853 "probed the class and
  settled it" unqualified. It settled the dispatch row; tag-push and pull_request are
  live in #885.

F4 — un-narrowing dropped the epistemic LABEL along with the narrowing. The record
  labels its other claims ("PROBED", "Measured rather than inferred"), so an unlabelled
  one reads as measured. The generalizing step is INFERRED from the repo-scoped secret
  model plus `ci.gate-trigger-base-resolved` — not measured, because measuring it prints
  a live credential into a run log. Said so in both rule and body.

F5 — "`workflow_dispatch` is self-supplied ... in the same way" lost its antecedent when
  six lines were inserted above it. Named the antecedent instead of relying on adjacency.

Rebased onto origin/main 94a3d1349 (#884 landed and touches docker-build.yml, ci-cd.md
and the catalog), so the job population was re-derived on the new base rather than
carried over from the pre-rebase measurement.

The record is now 64 prose lines, over the 60-line WARNING ceiling; 44/215 records = 20.5%,
inside the validator's 2-25% calibration band, and the content is distinct findings rather
than redundancy.

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UYNbVwgVszv6Pum7ZuGd75
Third cold review: MERGEABLE, converged — the round-2 invariant derives to exactly six
independently, so that fix did not repeat round 1's failure. Three non-blocking nits,
folded in because the branch is unpushed and they are one-word fixes.

The one worth naming is the third instance in this PR of the same defect: claiming more
evidence than I gathered. "PROBED across every surface" enumerated three surfaces and
omitted the web UI — and the sibling record edited in this very PR establishes that a
Gitea Actions control can exist with NO API surface at all: the owner-level default 404s
on the API and was nonetheless set, verified and reverted through the UI. So the omitted
surface is exactly the one that could falsify the claim.

Attempted to close it by sweeping the UI; the session login did not establish (Gitea
1.27.1's login page served no `_csrf` input to the scripted flow). Rather than report a
half-probe as a sweep, the record now states plainly which three surfaces were swept,
that the web UI was NOT, why the decision does not rest on it (the secret-scope model
carries the conclusion, and restricting dispatch closes nothing regardless), and — the
part that matters for the next reader — that the UI must be swept before citing this
record as proof that no such control CAN exist.

Also: `ci-image.yml:46` called the `branches:` key "the filter above" eleven lines after
the same block calls it "the filter below", and the key is below both. And three lines
added by earlier commits ran 141/196/139 columns in blocks that wrap at ~100 — the
ci-cd.md paragraph is rewrapped whole rather than per-line, which is what left it ragged
on the first attempt.

Verification on the rebased base (origin/main 94a3d1349): full scripts/tests suite
1250 passed, 2 skipped against 1252 collected; decisions-validate OK; ci-image.yml parses.

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UYNbVwgVszv6Pum7ZuGd75
docs(853): the honesty paragraph itself misattributed a claim, one indirection out
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 21s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 37s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 37s
PR Gates / Docs update reminder (pull_request) Successful in 37s
review-verdict/h10 Review-verdict: MERGEABLE @ 2f5ac5f (base: main)
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 18s
PR Gates / decisions lifecycle (pull_request) Successful in 28s
Review verdict / Set review-verdict status (pull_request_target) Successful in 38s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 8m39s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 13m13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 8m26s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 8m15s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 6s
2f5ac5f238
Delta review of the previous commit: MERGEABLE, one LOW finding, and it is the same
defect class the paragraph was written to fix.

The new paragraph said `ci.actions-credential-scoping` "records" that the owner-level
Actions default was set, verified and reverted THROUGH THE UI. Three of those four
elements are verbatim in that record. The fourth is not: the string `UI` appears zero
times in it (verified independently by the reviewer and again here — 0 standalone `UI`
tokens). The record says the default WAS set and that no API surface exists; it never
names the surface used.

A non-API surface is a sound inference by elimination — the API 404s, `app.ini` sets only
`ENABLED` and `DEFAULT_ACTIONS_URL`, and the CLI exposes only `generate-runner-token` —
but presenting it as something the sibling record RECORDS is exactly the overclaim the
paragraph exists to prevent. It now states the inference as an inference and gives its
grounds, rather than either dropping the point or borrowing authority for it.

Verification: decisions-validate OK; catalog regenerated; workflow-guard and
decisions-validator tests 201 passed.

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UYNbVwgVszv6Pum7ZuGd75
Author
Owner

Review-verdict: MERGEABLE @ 2f5ac5f

Four cold review rounds in isolated worktrees; rounds 1-2 NOT-MERGEABLE (12 and 5 findings), rounds 3 and the delta pass MERGEABLE. Every defect found was in the evidence, not the conclusion: a CLI probe that had exited 1 and printed nothing, a claim generalised past what was measured, an invariant that named 5 of 6 jobs, and a citation that borrowed authority for an inference. All fixed and re-verified against the tree. Cross-family review unavailable (Codex usage limit) so all rounds are same-family - stated as a deviation. Full scripts/tests suite 1250 passed 2 skipped against 1252 collected on the rebased base.

Review-verdict: MERGEABLE @ 2f5ac5f Four cold review rounds in isolated worktrees; rounds 1-2 NOT-MERGEABLE (12 and 5 findings), rounds 3 and the delta pass MERGEABLE. Every defect found was in the evidence, not the conclusion: a CLI probe that had exited 1 and printed nothing, a claim generalised past what was measured, an invariant that named 5 of 6 jobs, and a citation that borrowed authority for an inference. All fixed and re-verified against the tree. Cross-family review unavailable (Codex usage limit) so all rounds are same-family - stated as a deviation. Full scripts/tests suite 1250 passed 2 skipped against 1252 collected on the rebased base.
timothy merged commit 1d50241833 into main 2026-08-30 03:18:05 +02:00
timothy deleted branch fix/853-dispatch-ref-restriction 2026-08-30 03:18:07 +02:00
Sign in to join this conversation.