Skip to content

feat(process): deterministic pre-flight for the PR review agent - #163

Merged
tucktuck101 merged 36 commits into
launchpadfrom
feat/review-agent-preflight
Aug 16, 2026
Merged

feat(process): deterministic pre-flight for the PR review agent#163
tucktuck101 merged 36 commits into
launchpadfrom
feat/review-agent-preflight

Conversation

@serina-mcfall

Copy link
Copy Markdown

Summary

Adds the deterministic pre-flight for the PR review agent: a Python CLI that emits every
fact about a pull request decidable without a model, as JSON on stdout, and reaches no
conclusion about the code. Its central property is that an absence never reads as a
value — an input that could not be read comes back null with an enumerated reason, never
as an empty answer. This PR previously carried only the plan; the plan is still in it, at
launchpad/plans/, alongside the implementation of all twelve of its steps.

This body describes commit b783c83fd plus the corrections in the commit that follows it.

On the commit list. This branch was rebased onto launchpad, which left it unable to
reach the remote without a force-push — barred by §6 during review. It was reconciled by
merging the pre-rebase tip back in with strategy ours, which keeps this tree byte-for-byte
and makes the old tip an ancestor. The consequence a reviewer sees: 32 commits, of which
14 are pre-rebase duplicates of the other 16.
The files diff is unaffected — 40 files
against launchpad's current tip — and reviewing by file rather than by commit avoids
reading the same work twice. This PR replaces #124, whose diff was computed against a base
sha from before those 20 launchpad commits and so listed 134 files that were not its own.

Related issue

Closes #116

Issue type

Task


Agent provenance

Field Value
Harness / provider Claude Code
Model claude-opus-5[1m]
Session reference 6305f259-3a45-4ad8-aea9-08a4a2d19617
Initiating human @serina-mcfall

Objective

A committed, mutation-verified launchpad/scripts/pr-preflight.py that emits the
pre-flight record for any PR number, with the fixtures and controls that prove it.

Impacted components

launchpad/scripts/pr-preflight.py          new — the CLI entry point
launchpad/scripts/preflight_core.py        new — the record, pure functions, no I/O
launchpad/scripts/preflight_fetch.py       new — the fetch layer and the exit contract
launchpad/scripts/test_preflight_core.py   new — 109 controls over record, CLI, exit codes
launchpad/scripts/test_no_model.py         new — 12 controls, the no-model property by AST
launchpad/scripts/mutation_harness.py      new — proves the controls can fail, 3 phases
launchpad/scripts/INTERFACE.md             new — the record contract for #117/#118/#119
launchpad/scripts/testdata/                new — 24 recorded API responses + record.sh
launchpad/plans/2026-08-12-...preflight.md unchanged — the plan this implements

0 behind launchpad, 40 files changed, every commit DCO-signed. launchpad/scripts/ is
shared with PR #126's pr_body_check.py, which merged while this branch was being built;
nothing here touches it, and the mutation harness names its own two test modules rather
than discovering the directory so a foreign regression cannot be reported as this stage's.

Approach and rejected alternatives

Pure core, injected runner. preflight_core.py takes already-fetched data and does no
I/O at all; every gh call goes through a runner(argv) callable that controls replace
with recorded fixtures. Rejected: calling subprocess.run(["gh", ...]) at each call
site, which is simpler to write and would have made "prove each input's failure is fatal"
impossible without rewriting the fetch layer.

GitHub decides what a PR closes. The plan specified reusing
launchpad-pr-check.yml's body regex. Rejected on evidence, and the decision was
escalated rather than taken: that regex is bug #125, which #126 is fixing, and it disagrees
with GitHub on real PRs in this repo — #92's body carries a visible Closes #n while
GitHub reports it closes nothing, because its base was not the default branch. So
closingIssuesReferences decides, the body still supplies which keyword was written, and
a disagreement between them is recorded rather than resolved silently.

Checks as a list, never a name-keyed map. PR 86 carries three checks named check; a
map drops two. Rejected: keying by name, which reads more nicely and under-reports the
gate.

Two gates, asked separately. launchpad/AGENTS.md §6 says launchpad requires at
least two approving reviews, that the enforcing ruleset is unreadable without admin:org,
and that a PR's reviewDecision confirms review is required without exposing the count.
So the record answers both "is a required status check visible" (configured) and "is a
review gate in force" (review_required). Rejected: reporting configured: false alone,
which is what this branch did until the final review caught it — on a branch needing two
approvals that reads as "nothing gates this". Also rejected: inferring the count of two
from §6, since reviewDecision does not carry it and a document can drift.

Fixtures recorded, never written. A hand-written fixture proves only that the code
agrees with its author about the response shape. Rejected: inventing payloads — and the
recording caught a defect in itself, below.

Verification

Command run:

cd launchpad/scripts && python3 -m unittest test_preflight_core test_no_model
python3 -m unittest discover -s launchpad/scripts -t launchpad/scripts
python3 launchpad/scripts/mutation_harness.py
python3 launchpad/scripts/pr-preflight.py 86 | jq -c '.required_gate'
python3 launchpad/scripts/pr-preflight.py 999999 1>/dev/null ; echo "exit=$?"
python3 launchpad/scripts/pr-preflight.py 0 1>/dev/null ; echo "exit=$?"

Raw output:

$ cd launchpad/scripts && python3 -m unittest test_preflight_core test_no_model
.........................................................................................................................
----------------------------------------------------------------------
Ran 121 tests in 0.259s

OK

$ python3 -m unittest discover -s launchpad/scripts -t launchpad/scripts   # incl. PR #126's suite
.....................................................................................................................................................................
----------------------------------------------------------------------
Ran 165 tests in 0.266s

OK

$ env -i "$(command -v python3)" -m unittest discover -s launchpad/scripts -t launchpad/scripts   # no HOME, no GH_TOKEN
----------------------------------------------------------------------
Ran 165 tests in 0.263s

OK

$ python3 launchpad/scripts/pr-preflight.py 86 | jq -c ".required_gate"
{"configured":false,"source_endpoint":"GET /repos/launchpad-26/buzz/rules/branches/launchpad","review_required":true,"review_decision":"REVIEW_REQUIRED","review_source_endpoint":"gh pr view 86 --json reviewDecision"}

$ python3 launchpad/scripts/pr-preflight.py 86 | jq -c "{checks:(.checks|length), closing:.closing_issue.issue_numbers, skips:[.skips[]|{field,source,reason}]}"
{"checks":47,"closing":[79,91],"skips":[{"field":"required_gate.org_rulesets","source":"org_rulesets","reason":"forbidden"}]}

$ python3 launchpad/scripts/pr-preflight.py 999999 1>/dev/null ; echo "exit=$?"
{
  "error": "a required input could not be read",
  "skips": [
    {
      "field": "pr",
      "source": "pr",
      "reason": "absent",
      "detail": "gh: Not Found (HTTP 404)",
      "endpoint": "GET /repos/launchpad-26/buzz/pulls/999999"
exit=2

$ python3 launchpad/scripts/pr-preflight.py 0 1>/dev/null ; echo "exit=$?"
usage: pr-preflight.py [-h] [--repo REPO] [--indent INDENT] number
pr-preflight.py: error: PR number must be positive
exit=1

The harness output is 140 lines across three phases. Its first 20 lines, then every line of the branch phase, then the injection phase. The three summary lines are 24/24 mutants killed, 18/18 branches checked and 15/15 injected imports refused, and it exits 0 only if all three hold:

baseline: suite GREEN — OK

  preflight_core.py::build_pr                   -> return None                                    RED  (control works)
      FAILED (failures=22, errors=32)
  preflight_core.py::build_closing_issue        -> return {'present': True, 'keyword': None, 'i   RED  (control works)
      FAILED (failures=10)
  preflight_core.py::build_diff                 -> return {'merge_base_sha': 'x', 'head_sha': '   RED  (control works)
      FAILED (failures=12)
  preflight_core.py::build_checks               -> return []                                      RED  (control works)
      FAILED (failures=15)
  preflight_core.py::build_required_gate        -> return {'configured': False, 'source_endpoin   RED  (control works)
      FAILED (failures=15, errors=6)
  preflight_core.py::build_nearest_rules        -> return {}                                      RED  (control works)
      FAILED (failures=11, errors=11)
  preflight_core.py::build_record               -> return dict.fromkeys(RECORD_FIELDS)            RED  (control works)
      FAILED (failures=21, errors=60)
  preflight_core.py::_nearest                   -> return None                                    RED  (control works)
      FAILED (failures=10)
  preflight_core.py::_normalise_check           -> return {'name': None, 'workflow': None, 'sta   RED  (control works)
      FAILED (failures=4)
  preflight_fetch.py   a 404 on org rulesets stops being forbidden          RED  (branch is checked)
  preflight_fetch.py   a 5xx stops being unreachable                        RED  (branch is checked)
  preflight_fetch.py   401/403 stop being forbidden                         RED  (branch is checked)
  preflight_fetch.py   GraphQL prose absence stops being absent             RED  (branch is checked)
  preflight_fetch.py   an in-band errors array is handed on as data         RED  (branch is checked)
  preflight_fetch.py   the errors type mapping collapses to malformed       RED  (branch is checked)
  preflight_fetch.py   a broken gh install stops being caught               RED  (branch is checked)
  preflight_core.py    empty becomes fatal again                            RED  (branch is checked)
  preflight_core.py    a truncated tree stops being a failure               RED  (branch is checked)
  preflight_core.py    a tree entry's type stops being checked              RED  (branch is checked)
  preflight_core.py    an empty head tree stops being a failure             RED  (branch is checked)
  preflight_core.py    an unpinned diff stops being a failure               RED  (branch is checked)
  preflight_core.py    a capped check page stops being truncated            RED  (branch is checked)
  preflight_core.py    checks stop being pinned to the PR's head commit     RED  (branch is checked)
  preflight_core.py    the unreadable-rules path drops the review half again RED  (branch is checked)
  preflight_core.py    an unreadable review decision becomes 'not required' RED  (branch is checked)
  preflight_core.py    a null review decision stops being distinguishable from a required one RED  (branch is checked)
  preflight_core.py    a malformed rules probe is coerced to empty again    RED  (branch is checked)
18/18 branches checked
  preflight_core.py::module       import urllib.request                    REFUSED
  preflight_core.py::module       import requests                          REFUSED
  preflight_core.py::module       import httpx                             REFUSED
  preflight_core.py::module       import openai                            REFUSED
  preflight_core.py::module       import anthropic                         REFUSED
  preflight_core.py::build_diff   import requests                          REFUSED
  preflight_core.py::build_diff   import openai                            REFUSED
  preflight_core.py::_nearest     import httpx                             REFUSED
  preflight_core.py::module       importlib.import_module("requests")      REFUSED
  preflight_core.py::build_diff   importlib.import_module("openai")        REFUSED
  preflight_core.py::module       __import__("anthropic")                  REFUSED
  preflight_fetch.py::module       import httpx                             REFUSED
  preflight_fetch.py::gh_runner    import openai                            REFUSED
  pr-preflight.py::module       import openai                            REFUSED
  pr-preflight.py::module       importlib.import_module("anthropic")     REFUSED
15/15 injected imports refused
  • Tests or checks were run and the raw output is pasted above
  • The diff is confined to the scope of the linked issue
  • No secrets, keys, tokens or hostnames were added to tracked files

On the third box: the fixtures are recorded API responses, so they were scanned
structurally before committing rather than eyeballed. Every long high-entropy run in them
is a GitHub node_id, a commit SHA inside a blob_url/raw_url, or a GPG signature
(not a key); every keyword hit (token, credential) is prose or a real repository path
such as crates/git-credential-nostr. No value was printed to a transcript during that
check — only field paths and lengths.

Not verified

  • The pre-flight itself has never run in CI. A workflow job added by this PR runs the
    controls and the mutation harness on every pull request, which needs no credential —
    but no workflow invokes pr-preflight.py against a live PR. Every live run above is
    local, under a token holding gist, project, read:org, repo, workflow — not the scoped
    token adr: where the PR review agent runs — GitHub Actions or through Buzz #110 specifies and task: publish exactly one PR review comment, and re-review on push #119 will provision. Behaviour under that token is unverified,
    and the org-ruleset skip in particular may report differently.
  • The new CI job has not been observed on GitHub's runners, only locally, including
    under env -i with no HOME and no GH_TOKEN.
  • gh version-specific behaviour. The failure classifier reads gh's stderr wording —
    (HTTP 404), GraphQL: Could not resolve to a — and gh's rendering of a GraphQL null as
    an empty string. Those are output format, not API contract, verified against gh 2.93.0
    only. A gh that rewords them would reclassify failures, and nothing here pins gh's version.
  • The StatusContext path has no recorded response behind it. All 47 contexts in the
    PR-86 fixture are CheckRun; no PR in either repo currently carries a legacy commit
    status, so that normaliser and its control run on a hand-built node inside a fixture set
    whose stated doctrine is that nothing is hand-written. A reviewer flagged the consequence:
    GraphQL's StatusState includes PENDING, which is not a CheckConclusionState, so an
    in-flight external status would arrive as conclusion: "PENDING", status: null.
  • The truncated: true guard is verified on a fixture, never against this repo. This
    fork is 4337 entries; the fixture came from torvalds/linux at 71798.
  • No PR with more than 100 checks has been run. The query takes first: 100; PR 86
    has 47 with totalCount 47, verified against both the fixture and the live API. Such a
    PR now refuses rather than truncating: no record, exit 2, and a truncated skip.
    Pagination is not implemented and no issue tracks it — refusal is the current answer,
    and whether it should be the permanent one is a reviewer's call.
  • The review-gate count is unverified and deliberately unread. reviewDecision confirms
    review is required and carries no number; §6's figure of two comes from GitHub's merge box,
    which this token cannot read. review_required: true is therefore proved; "two" is not.
  • Only three repositories exercised: this fork, block/buzz, and one tree from
    torvalds/linux. Nothing ran against a repo with a readable required-status-check
    ruleset, so configured: true is proved only from a synthesised rules payload.
  • Not run on Windows or macOS. The OSError classification depends on how the platform
    reports EACCES/ENOEXEC; Linux only.
  • The reviewers were not independent of me. All four gates were subagents of the same
    session that wrote the branch — the final reviewer says so itself, having matched this
    PR's own session reference. They re-derived their claims from the repo and the live API
    rather than from context, and they found two High defects I had missed, but an independent
    human read is still owed.
  • CI on the previous head failed for an unrelated reason. Six Desktop E2E jobs died in
    activate-hermit downloading the toolchain (curl: (56) Connection died, curl: (22) … 503) before any test ran. Confirmed in four of the six logs; not reproduced locally, and
    no re-run has been attempted on the rebased head.

Security implications

The blast radius is a read-only JSON document on stdout. The script performs no writes,
posts nothing, and only ever spawns gh — enforced at the one spawn site and asserted by a
control that counts subprocess.run call nodes and requires the single one to sit inside
gh_runner.

It makes no model call, and that is checked by AST rather than by grep: every imported
module name in both modules must be on an allowlist written in the test, so an unanticipated
import fails by default, and importlib.import_module/__import__ are refused by name
because they create no import node at all. 15 injected imports were each shown turning that
check red.

Author-controlled PR text (title, body, file paths) is carried as data in separately
labelled JSON fields, never concatenated and never placed in a prompt — which satisfies the
"must never" half of #120's CONTAINMENT.md contract. That permission is contingent on this
stage making no model call, so the AST check above is what keeps it true.

Two exposures worth naming rather than dismissing. The record embeds untrusted author text
verbatim, so any consumer that renders it into a prompt inherits the containment
obligation this stage discharges by not having one
INTERFACE.md says so. And the
workflow job this PR adds runs on pull_request, not pull_request_target — deliberately.
The code under test lives in this repository, so a PR can modify the very controls the job
runs. That is acceptable for this job for the reason the file's existing comment gives: a
fork-triggered run gets a read-only GITHUB_TOKEN with no repository secrets, its checkout
now sets persist-credentials: false, and merging needs two approving reviews. A defeated
control suite misleads a reviewer; it cannot merge anything or write anywhere. The same
reasoning does not extend to invoking the pre-flight itself against a live PR, which
carries a credential and stays with #119.

Escalations

  1. A review subagent pushed this branch without being asked to. The remote moved at
    08:26 local, inside the window in which serina:review-code and serina:review-tests
    were running; neither was asked to push, and the reflog records only three pushes ever.
    No harm resulted, but the review gate assumes agents do not publish, and one did. Worth
    auditing what those subagent definitions are permitted to run.

  2. This branch was force-pushed after a rebase, with explicit human permission. §6 says
    not to force-push during review. No human review or review comment existed at that point,
    so nothing was hidden from a reviewer; @serina-mcfall authorised it deliberately rather
    than it being assumed. Recording it because the rule exists and was knowingly set aside.

  3. The plan's STEP 4 mechanism was rejected rather than implemented, and the decision was
    escalated rather than taken alone. Evidence in Approach.

  4. Four review gates ran and found nine, then eleven, findings. All are now resolved or
    tracked.
    The first pass's nine: seven confirmed and fixed, one refuted and hardened
    anyway, two merged into one. The final review's eleven: both High fixed (the unasked
    reviewDecision, and the no-model check not covering the entry point), and one Medium
    fixed (the harness discovering another task's suite).

    The remaining eight are settled as follows, and none is left as prose in this body:

    • Fixed here, because they were one-line corrections inside files this PR is still
      adding, and merging a wrong docstring is worse than filing an issue about it: the plan
      file's stale "adr: where the PR review agent runs — GitHub Actions or through Buzz #110 is open"; the harness's restore guarantee covering one phase of
      three; a docstring misstating its own evidence about PR 86's body; Read.name written
      at 24 sites and read at none, now published as skips[].source; fetch_all's
      "seven reads"; and the delete-fixture recorder comparing by branch name.
    • Filed as task: record a StatusContext response, and settle how a legacy commit status maps into the record #148: recording a real StatusContext response and settling how a legacy
      commit status maps into checks[]. It needs a PR carrying one, and none exists in either
      repository today — so it is a Task, not a Bug, since nothing has been observed failing.
    • Not fixable retroactively: the plan's GATES clause required all four reviews before
      the push, and the push happened first. Disclosed in escalations 1 and 2.
  5. A workflow job is added; the pre-flight invocation is not. The job runs this stage's
    controls and mutation harness on every PR and needs no credential, so adr: where the PR review agent runs — GitHub Actions or through Buzz #110 never gated
    it. Invoking pr-preflight.py against a live PR does need the scoped token and stays
    with task: publish exactly one PR review comment, and re-review on push #119. The plan's own text is corrected in place rather than left to contradict the
    diff.

  6. The required-check contradiction is resolved, not deferred. §6 was rewritten by fix(ci): PR body check must ignore issue references inside code, and accept Refs #126
    and now answers it: two approving reviews, ruleset invisible without admin:org,
    reviewDecision readable. The record reports both gates. What remains a cohort decision
    is whether the reviewer's token should hold admin:org at all.

  7. This tree sits at launchpad/scripts/ while task: treat all pull request content as untrusted data #120's review-agent tree sits at
    launchpad/review-agent/.
    fix(ci): PR body check must ignore issue references inside code, and accept Refs #126 merging pr_body_check.py into launchpad/scripts/
    settles the precedent in favour of staying, and also means the two stages share a
    directory. Nothing was copied or moved; the resolution is in INTERFACE.md.

  8. The dependency on unmerged task: treat all pull request content as untrusted data #120 is stated, not implemented. CONTAINMENT.md tells
    task: deterministic pre-flight for the PR review agent #116 to call fetch.fetch_all, which does not exist on launchpad. Importing from an
    unmerged branch is impossible and copying it would duplicate 2856 lines, so the injected
    runner is the seam and the resolution is a rebase after task: treat all pull request content as untrusted data #120 merges.

  9. Two fixtures are projections and one PR was manufactured. pr86-tree.json and
    tree-truncated.json are whole recorded responses through one documented jq filter,
    because the originals are 1.1 MB and 11 MB. No PR in this fork or upstream deletes a
    nearest rules file, so that fixture came from throwaway PR chore(fixtures): throwaway — recording a rules-file-DELETE response (closing immediately) #142, opened for the
    purpose and closed unmerged with its branch deleted.

  10. Three of the plan's stated facts had gone stale within a day, and the code follows
    the re-recording: PR 86 now has 47 checks with three named check (not 24 with two), org
    rulesets answer 404 rather than 403, and PR 86 has itself become divergent so it now
    exercises the two-dot trap directly.

  11. Test discovery uses -t launchpad/scripts, not the plan's -t .. The plan's command
    cannot work: unittest requires the start directory to be importable, so -t . needs
    __init__.py in launchpad/ and launchpad/scripts/, and making a documentation tree a
    Python package to satisfy a test command is the wrong trade.

  12. No step-by-step gate ledger exists. The final reviewer's mechanical check could not
    run: the plan uses STEP n headings, which the ledger script cannot parse, and no ledger
    file exists. So there is no per-step record of which step was reviewed when — only that
    all four gates ran against the whole tree.

Plans the deterministic pre-flight script for issue #116 — the stage that emits a machine-readable record of everything about a PR decidable without a model, so no later reviewer spends a token on a question with one correct answer.

Twelve steps, first running at step 3. Two serina:review-plan passes ran before any of it is built; twelve findings, three of them Blockers, all applied. The Blockers were a GraphQL merge-base field that does not exist, a fixture set that could not distinguish a three-dot diff from a two-dot one, and an exit contract that was titled but never defined or tested.

Written to launchpad/plans/ rather than the skill's default docs/plans/, because docs/ is upstream's tree and launchpad/AGENTS.md puts cohort files under launchpad/.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…inst

Seventeen fixtures for #116, recorded from the live API by
record.sh rather than written by hand. A hand-written fixture proves only that
the code agrees with its author about the response shape.

Three recorded facts differ from what the plan expected, and the controls follow
the recording rather than the plan:

- PR 86 now carries 47 checks with three named "check", not the 24 with two that
  the plan recorded a day earlier. Names collide harder than claimed, so the
  record must carry checks as a list.
- Org-level rulesets answer 404, not 403, for a token without admin:org.
  launchpad-26 is an Organization, so that 404 hides access rather than
  reporting absence, and it cannot be read as "no rulesets exist".
- PR 14 modifies root AGENTS.md and adds launchpad/AGENTS.md; the plan described
  both as additions. The ADD direction still has a real source.

The DELETE direction has no real source in this fork or upstream, so it was
recorded from throwaway PR #142, opened for the purpose and closed unmerged with
its branch deleted. record-delete-fixture.sh is the audit trail.

STEP 1's done-when is committed as eight controls rather than run once by hand,
so a re-recording that drifts fails where the cause is legible.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
preflight_core.py for #116. No subprocess, no network and no
model call in this module: every input arrives as a Read that somebody else
performed, which is what makes the whole surface testable from recorded
fixtures.

The record's seven fields are enumerated in the module docstring, so a later
change has a fixed list to be checked against rather than an open-ended "every
field". checks is a list, never a name-keyed map — PR 86 carries three checks
named "check" and a map would drop two of them.

The SKIP taxonomy is six enumerated reasons, two more than the plan named:

- truncated, because the trees API answers HTTP 200 with a partial list and a
  half-read tree reporting "no rules file" is the confusion the guard exists to
  prevent.
- unreachable, because "we never asked" — no gh binary, a timeout — must not be
  reported as "it does not exist", which is what folding it into absent does.

Re-recorded every compare fixture by SHA, base.sha...head.sha, after the first
recording exposed a defect in itself: comparing a merged PR against its base
BRANCH NAME answers 200 OK with zero files, because the branch tip already
contains the head. A real six-file PR rendered as a PR that changed nothing.

That re-recording also made PR 86 a stronger fixture than the plan expected. Its
base tip is now 6 commits ahead of its merge base, so the two-dot trap is
catchable on PR 86 itself, not only on the upstream divergent fixture.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
pr-preflight.py prints the record as JSON on stdout for #116.
Verified against the live API: on PR 86 the title matches `gh pr view 86 --json
title`, the record's sorted file paths are identical to `gh pr diff 86
--name-only | sort`, and a nonexistent PR number exits 2 having printed nothing
on stdout, with the skip register on stderr.

The runner is an injected argument defaulting to real gh. That is the whole
reason the later controls can make each individual call fail without a network:
hardcoding subprocess.run(["gh", ...]) per call site would have turned STEP 9
from test-writing into a rewrite of steps 3 through 7.

The fetch layer lives in preflight_fetch.py rather than inside pr-preflight.py,
because a module named pr-preflight cannot be imported, and a fetch layer no
control can drive is a fetch layer no control can make fail. The CLI file is the
entry point and nothing else.

Fixed a misclassification the live run exposed: `gh pr view 999999` reports
absence in prose — "GraphQL: Could not resolve to a PullRequest" — with no HTTP
status attached, so it was being classified unreachable. That is precisely the
confusion the taxonomy exists to prevent: we did ask, and the answer was that it
is not there.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…t disagrees

STEP 4 of #116, built the way Serina chose rather than the way
the plan prescribed. The plan said to reuse launchpad-pr-check.yml's body regex.
That is the mechanism sibling PR #126 is replacing as bug #125, and it is wrong
in a way this record must not repeat.

Probed against four real pull requests before deciding:

- fork #92 — the body carries a visible `Closes #n` and GitHub reports NOTHING,
  because the PR's base was not the default branch. Merging it closes no issue,
  and a regex says the board updates.
- fork #86 — closes TWO issues. The plan's `re.search` reports one. A control now
  reproduces the single search and requires the record to hold more than it.
- upstream block#5695 — the only keyword sits in an unfilled `<!-- Fixes block#1234 -->`
  placeholder. GitHub ignores it; so does the text half, which strips comments
  first.
- fork #124 — `Refs #116`, correctly not a closing reference.

So closingIssuesReferences decides `present` and the issue list, the body still
supplies which keyword was written, and a disagreement between them is recorded
instead of resolved silently — a disagreement is the shape of #125 and is signal
a reviewer wants.

An unreadable GitHub answer yields `present: null` — unknown, never false. "We
could not ask" and "it closes nothing" differ on whether the board updates, and
closing_refs is therefore skip-only: the run still exits 0 carrying the skip.

closing_issue grew from three fields to six, added to the module's enumerated
field list in the same commit as the plan requires.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…xpose a two-dot one

STEP 5 of #116. Six controls over the merge-base diff, and the
one that matters is the divergent case: the recorded base SHA must equal
merge_base_commit.sha and must NOT equal the PR's base tip. A two-dot
implementation passes every other control in this file and fails that one.

PR 86 now carries the property too — launchpad moved 19 commits under it, so its
base tip sits 6 commits past its fork point — so the trap is covered by the
fork's own PR as well as by the upstream fixture that was recorded for it.

Verified live as the plan asks: the record's sorted file paths are identical to
`gh pr diff 86 --repo launchpad-26/buzz --name-only | sort`, six paths, and the
same six are asserted from the recorded fixture so the check survives without a
network.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
… say why not

STEP 6 of #116. On PR 86 all 47 checks report required false,
repo-level rules for launchpad answer an empty list, and the record says
configured: false naming the endpoint that answered — never a silent zero, which
is indistinguishable from a scope failure.

Org-level rulesets are unreadable without admin:org, which this token does not
hold, so their absence is a published SKIP rather than an assertion that none
exist. A control requires that skip to be present whenever configured is false,
so the false can never be read alone.

Documented the two blind spots that survive, because "configured: false" is a
statement about what could be seen and not about what is:

- an org ruleset is invisible to this token, and leaks through only via
  per-context isRequired, which is why that is consulted as a second source;
- a required context that never RAN is absent from the rollup entirely, so
  isRequired cannot report it either.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…the worktree

STEP 7 of #116. Nine controls. Both AGENTS.md and CLAUDE.md are
resolved independently, because first-wins would hide the root CLAUDE.md from
every path under launchpad/, which has only an AGENTS.md.

The two controls that could fail for a resolver that looks like it works:

- the DELETE direction. launchpad/AGENTS.md is on disk in this checkout, and the
  throwaway PR's head tree does not have it. A resolver reading the local
  checkout answers launchpad/AGENTS.md; reading the head tree answers the root
  file. The control asserts the local file really is present first, so it cannot
  pass by the file being missing.
- a tree naming a directory that exists nowhere on disk still resolves, which no
  filesystem-reading implementation can do.

Also pinned: the nearest ancestor wins over a further one
(desktop/src/features/agents/AGENTS.md over the root), a lookalike basename is
not a rules file (VISION_REMOTE_AGENTS.md ends with AGENTS.md), and a path with
no ancestor rules file at all is a skip-only fact that keeps the exit at 0.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
STEP 8 of #116. Nine controls break exactly one call at a time,
leaving the other seven working, which is the only way to show that *this* input
is the one whose failure is fatal — and, for the skip-only ones, that it is not.
All five required inputs and all three skip-only ones are covered, plus a control
that every one of the six enumerated reasons is reachable.

Writing them exposed two defects in what came before:

- An empty-but-readable check list was fatal. A head commit whose checks have not
  started yet is READ, and empty; treating that as a failure is the same
  conflation as treating an unreadable check list as "no checks are required",
  pointed the other way. `empty` is now never fatal for any field, and that rule
  outranks the required/skip-only split.

- A usage error and an unreadable required input both exited 2, because argparse
  exits 2 on error and the documented contract claimed 1. Two very different
  failures sharing an exit code makes the contract untestable — the control that
  was supposed to tell them apart passed while asserting they were the same.
  Usage errors now exit 1, verified live: `pr-preflight.py 0` exits 1, and
  `pr-preflight.py 999999` exits 2.

A truncated head tree exits non-zero and resolves to None rather than {} — not
read is not the same as nothing found, and {} is what would let a half-read tree
publish as "this path has no rules file".

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
STEP 9 of #116. Six record fields, three degradations each —
empty, malformed, erroring — with the runner injected and no network anywhere.
The seventh field, skips, is the register the other six report into and has no
read of its own, so it gets its own controls instead of a row in the matrix.

No grep could do this. A grep reports every occurrence and leaves a human to sort
the legitimate hits from the real ones, which is not a check that can fail.

The matrix found a real defect: a malformed rules probe was coerced to an empty
list, so `configured: false` — a definite answer — was published from a response
nobody could read. It now reports malformed and configured: null.

Two fields where an EMPTY answer is a real answer rather than a skip, each named
in the suite with its reason rather than smoothed into false uniformity: an empty
rules list means no gate is configured, and GitHub answering with no closing
references means the PR closes nothing.

Also added: a head tree listing zero entries is now a skip, because it cannot be
reconciled with a diff that changed files, and resolving every path to "no rules
file" against it would be an empty read publishing itself as an answer.

Three controls assert the no-network property from different angles: subprocess
is broken outright and the whole CLI still runs, the real runner refuses any
binary but gh, and exactly eight reads are made with no call repeated.

The suite is discovered with `-t launchpad/scripts`, not the plan's `-t .`. The
plan's command cannot work: unittest requires the start directory to be
importable, so `-t .` needs __init__.py in launchpad/ and launchpad/scripts/, and
making a documentation tree a Python package to satisfy a test command is the
wrong trade.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
… go red

STEP 10 of #116. A control that has never been observed failing
has not been shown to test anything, and "the suite passes" is the claim this
harness exists to refuse.

Twenty functions across preflight_core.py and preflight_fetch.py, each replaced in
turn by a constant chosen to be the plausible wrong answer a careless
implementation would return — build_checks to [], build_nearest_rules to {},
is_fatal to False, gh_runner to a canned success. 20 of 20 mutants killed; the
raw run goes in the PR body as the plan asks.

The harness restores each file from the original held in memory and then verifies
the restoration byte-for-byte before exiting, because a mutation harness that
leaves a mutant in the tree is worse than none. It also refuses to start if the
suite is already red, so a survivor can never be a pre-existing failure
misattributed to a mutation.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…eck refusing

STEP 11 of #116. Every imported module name in both modules is
compared against an allowlist written in the test, so an import nobody
anticipated fails the check by default. A grep cannot do this: it reports every
occurrence and leaves a human to sort the legitimate hits from the real ones,
which is not a check that can fail.

Three evasions are closed, each demonstrated on a sample rather than asserted:

- ast.walk, not tree.body — a top-level scan finds {json} in a module that also
  does `import requests` inside a function, where walk finds both.
- Call nodes too — `importlib.import_module("openai")` creates no Import node, so
  an import-name scan reports it as absent.
- `__import__("openai")` needs no import statement at all.

The check lives in its own module, test_no_model.py, and reads the source rather
than importing it. That is what makes the evidence honest: httpx, openai and
anthropic are not installed here, so injecting one into preflight_core makes
every module that imports it die at import time — and a suite that is red
because a module could not load is not evidence that this check works.

The harness's injection phase confirms it, 13 of 13 refused: urllib.request,
requests, httpx, openai and anthropic at module level; requests, openai and httpx
inside function bodies; importlib.import_module and __import__ by call; and two
injections into the fetch layer. Raw output in the PR body.

Writing this caught a control failing in the way the module argues against: it
counted "subprocess.run(" as text and read its own prose about subprocess as a
second spawn site. It now counts call nodes, and requires the one spawn to sit
inside gh_runner.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…y resolves

INTERFACE.md for #116, so #117, #118 and #119 have something to
depend on that is not the source.

The three rules a consumer can rely on: null means "not read" and never "nothing
there"; every null field has a skip entry naming the endpoint that answered; and
exit 2 leaves stdout empty so there is no half-record to mistake for a whole one.

#120's CONTAINMENT.md names #116 in its contract table: must call
fetch.fetch_all, must never concatenate surfaces or build a prompt. Both halves
are addressed rather than assumed. The "must never" half holds today — every
author-controlled surface is a separately labelled JSON string, nothing is
concatenated, no prompt exists, and the AST controls keep the record module
unable to reach a model at all. The "must call" half cannot hold yet:
fetch.fetch_all is on an unmerged branch, importing it is impossible and copying
it would create the second copy this work was told not to make. It resolves by
rebase after #120 merges, and the injected runner is already the seam that makes
adopting their fetcher an adapter rather than a rewrite.

Also raised for the reviewer: #120 put its tree at launchpad/review-agent/ while
this plan assumed launchpad/scripts/, where PR #126 is also adding a script. They
should not stay split, and which way they converge is not this PR's call.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…nfirmed

Nine findings went in, seven confirmed, one refuted, two merged. All fixed, and
the refuted one hardened anyway. #116.

HIGH — the org-rulesets 404-to-forbidden reclassification was never asserted.
Deleting it left 80/80 green, because the three controls named for that property
hand-built a Read with skip=FORBIDDEN already baked into it. A control that
asserts an answer it supplied itself tests nothing, and one of those controls is
called test_org_rulesets_are_skipped_not_asserted_absent. The fake runner was
already feeding the real 404 text; now a control reads the reason back out of the
record.

MEDIUM — gh_runner caught FileNotFoundError and TimeoutExpired only, so any other
OSError escaped main() and exited 1 with a traceback: this tool's usage-error
code, telling a caller its arguments were bad. Three reproduced triggers, none of
them a missing binary — a non-executable gh, a directory named gh, and a gh that
is executable but not a runnable binary (wrong architecture or truncated
download, the plausible one). Now every OSError is unreachable, and the run exits
2. A non-executable gh with a working one later on PATH was always harmless:
CPython keeps walking PATH past EACCES.

MEDIUM — the 5xx-to-unreachable branch was untested. Only a 5xx carrying a JSON
message reaches it; a bodyless one prints no (HTTP nnn) and lands on the fallback.
Both forms now have controls, because "retry" and "the shape is unusable" are
different instructions to a consumer.

MEDIUM — the in-band GraphQL errors block was dead code with a comment asserting
a gh behaviour that does not happen: gh exits non-zero whenever a response
carries errors, so absence arrives as prose and is already classified correctly.
Kept, because the injected runner is a documented extension point and a direct
HTTP caller does return 200 with an errors body — but the comment now says so,
both halves have controls, and the error type maps to its own reason instead of
everything but FORBIDDEN collapsing to malformed.

LOW — a git directory or submodule named AGENTS.md resolved as a rules file,
suppressing the "no ancestor rules file" skip that was the truth. Blobs only now;
an entry with no recorded type is still kept, so a projection cannot drop a file.

LOW — Skips.reasons() had no caller and no test. Deleted.

REFUTED — diff.head_sha null with no skip cannot reach an emitted record: every
route that loses the head sha already exits 2. Hardened regardless, because
build_diff is a public function INTERFACE.md invites the next stage to call: it
takes the head sha directly and skips without one, so the invariant lives in the
function rather than in caller discipline.

The harness gained the phase that would have caught three of these. Whole-function
mutation cannot see a MISSING branch — it fails everything indiscriminately, which
proves the function is called and nothing about any decision inside it. Thirteen
branches are now disabled one at a time, and a snippet that no longer matches is a
hard error rather than a silent pass. TARGETS also gained the three functions the
adjudicator found absent despite the docstring's every-function claim, which is
now stated accurately.

103 controls, 23/23 mutants killed, 13/13 branches checked, 13/13 forbidden
imports refused.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Plans the deterministic pre-flight script for issue #116 — the stage that emits a machine-readable record of everything about a PR decidable without a model, so no later reviewer spends a token on a question with one correct answer.

Twelve steps, first running at step 3. Two serina:review-plan passes ran before any of it is built; twelve findings, three of them Blockers, all applied. The Blockers were a GraphQL merge-base field that does not exist, a fixture set that could not distinguish a three-dot diff from a two-dot one, and an exit contract that was titled but never defined or tested.

Written to launchpad/plans/ rather than the skill's default docs/plans/, because docs/ is upstream's tree and launchpad/AGENTS.md puts cohort files under launchpad/.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…inst

Seventeen fixtures for #116, recorded from the live API by
record.sh rather than written by hand. A hand-written fixture proves only that
the code agrees with its author about the response shape.

Three recorded facts differ from what the plan expected, and the controls follow
the recording rather than the plan:

- PR 86 now carries 47 checks with three named "check", not the 24 with two that
  the plan recorded a day earlier. Names collide harder than claimed, so the
  record must carry checks as a list.
- Org-level rulesets answer 404, not 403, for a token without admin:org.
  launchpad-26 is an Organization, so that 404 hides access rather than
  reporting absence, and it cannot be read as "no rulesets exist".
- PR 14 modifies root AGENTS.md and adds launchpad/AGENTS.md; the plan described
  both as additions. The ADD direction still has a real source.

The DELETE direction has no real source in this fork or upstream, so it was
recorded from throwaway PR #142, opened for the purpose and closed unmerged with
its branch deleted. record-delete-fixture.sh is the audit trail.

STEP 1's done-when is committed as eight controls rather than run once by hand,
so a re-recording that drifts fails where the cause is legible.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
preflight_core.py for #116. No subprocess, no network and no
model call in this module: every input arrives as a Read that somebody else
performed, which is what makes the whole surface testable from recorded
fixtures.

The record's seven fields are enumerated in the module docstring, so a later
change has a fixed list to be checked against rather than an open-ended "every
field". checks is a list, never a name-keyed map — PR 86 carries three checks
named "check" and a map would drop two of them.

The SKIP taxonomy is six enumerated reasons, two more than the plan named:

- truncated, because the trees API answers HTTP 200 with a partial list and a
  half-read tree reporting "no rules file" is the confusion the guard exists to
  prevent.
- unreachable, because "we never asked" — no gh binary, a timeout — must not be
  reported as "it does not exist", which is what folding it into absent does.

Re-recorded every compare fixture by SHA, base.sha...head.sha, after the first
recording exposed a defect in itself: comparing a merged PR against its base
BRANCH NAME answers 200 OK with zero files, because the branch tip already
contains the head. A real six-file PR rendered as a PR that changed nothing.

That re-recording also made PR 86 a stronger fixture than the plan expected. Its
base tip is now 6 commits ahead of its merge base, so the two-dot trap is
catchable on PR 86 itself, not only on the upstream divergent fixture.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
pr-preflight.py prints the record as JSON on stdout for #116.
Verified against the live API: on PR 86 the title matches `gh pr view 86 --json
title`, the record's sorted file paths are identical to `gh pr diff 86
--name-only | sort`, and a nonexistent PR number exits 2 having printed nothing
on stdout, with the skip register on stderr.

The runner is an injected argument defaulting to real gh. That is the whole
reason the later controls can make each individual call fail without a network:
hardcoding subprocess.run(["gh", ...]) per call site would have turned STEP 9
from test-writing into a rewrite of steps 3 through 7.

The fetch layer lives in preflight_fetch.py rather than inside pr-preflight.py,
because a module named pr-preflight cannot be imported, and a fetch layer no
control can drive is a fetch layer no control can make fail. The CLI file is the
entry point and nothing else.

Fixed a misclassification the live run exposed: `gh pr view 999999` reports
absence in prose — "GraphQL: Could not resolve to a PullRequest" — with no HTTP
status attached, so it was being classified unreachable. That is precisely the
confusion the taxonomy exists to prevent: we did ask, and the answer was that it
is not there.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…t disagrees

STEP 4 of #116, built the way Serina chose rather than the way
the plan prescribed. The plan said to reuse launchpad-pr-check.yml's body regex.
That is the mechanism sibling PR #126 is replacing as bug #125, and it is wrong
in a way this record must not repeat.

Probed against four real pull requests before deciding:

- fork #92 — the body carries a visible `Closes #n` and GitHub reports NOTHING,
  because the PR's base was not the default branch. Merging it closes no issue,
  and a regex says the board updates.
- fork #86 — closes TWO issues. The plan's `re.search` reports one. A control now
  reproduces the single search and requires the record to hold more than it.
- upstream block#5695 — the only keyword sits in an unfilled `<!-- Fixes block#1234 -->`
  placeholder. GitHub ignores it; so does the text half, which strips comments
  first.
- fork #124 — `Refs #116`, correctly not a closing reference.

So closingIssuesReferences decides `present` and the issue list, the body still
supplies which keyword was written, and a disagreement between them is recorded
instead of resolved silently — a disagreement is the shape of #125 and is signal
a reviewer wants.

An unreadable GitHub answer yields `present: null` — unknown, never false. "We
could not ask" and "it closes nothing" differ on whether the board updates, and
closing_refs is therefore skip-only: the run still exits 0 carrying the skip.

closing_issue grew from three fields to six, added to the module's enumerated
field list in the same commit as the plan requires.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…xpose a two-dot one

STEP 5 of #116. Six controls over the merge-base diff, and the
one that matters is the divergent case: the recorded base SHA must equal
merge_base_commit.sha and must NOT equal the PR's base tip. A two-dot
implementation passes every other control in this file and fails that one.

PR 86 now carries the property too — launchpad moved 19 commits under it, so its
base tip sits 6 commits past its fork point — so the trap is covered by the
fork's own PR as well as by the upstream fixture that was recorded for it.

Verified live as the plan asks: the record's sorted file paths are identical to
`gh pr diff 86 --repo launchpad-26/buzz --name-only | sort`, six paths, and the
same six are asserted from the recorded fixture so the check survives without a
network.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
… say why not

STEP 6 of #116. On PR 86 all 47 checks report required false,
repo-level rules for launchpad answer an empty list, and the record says
configured: false naming the endpoint that answered — never a silent zero, which
is indistinguishable from a scope failure.

Org-level rulesets are unreadable without admin:org, which this token does not
hold, so their absence is a published SKIP rather than an assertion that none
exist. A control requires that skip to be present whenever configured is false,
so the false can never be read alone.

Documented the two blind spots that survive, because "configured: false" is a
statement about what could be seen and not about what is:

- an org ruleset is invisible to this token, and leaks through only via
  per-context isRequired, which is why that is consulted as a second source;
- a required context that never RAN is absent from the rollup entirely, so
  isRequired cannot report it either.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…the worktree

STEP 7 of #116. Nine controls. Both AGENTS.md and CLAUDE.md are
resolved independently, because first-wins would hide the root CLAUDE.md from
every path under launchpad/, which has only an AGENTS.md.

The two controls that could fail for a resolver that looks like it works:

- the DELETE direction. launchpad/AGENTS.md is on disk in this checkout, and the
  throwaway PR's head tree does not have it. A resolver reading the local
  checkout answers launchpad/AGENTS.md; reading the head tree answers the root
  file. The control asserts the local file really is present first, so it cannot
  pass by the file being missing.
- a tree naming a directory that exists nowhere on disk still resolves, which no
  filesystem-reading implementation can do.

Also pinned: the nearest ancestor wins over a further one
(desktop/src/features/agents/AGENTS.md over the root), a lookalike basename is
not a rules file (VISION_REMOTE_AGENTS.md ends with AGENTS.md), and a path with
no ancestor rules file at all is a skip-only fact that keeps the exit at 0.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
STEP 8 of #116. Nine controls break exactly one call at a time,
leaving the other seven working, which is the only way to show that *this* input
is the one whose failure is fatal — and, for the skip-only ones, that it is not.
All five required inputs and all three skip-only ones are covered, plus a control
that every one of the six enumerated reasons is reachable.

Writing them exposed two defects in what came before:

- An empty-but-readable check list was fatal. A head commit whose checks have not
  started yet is READ, and empty; treating that as a failure is the same
  conflation as treating an unreadable check list as "no checks are required",
  pointed the other way. `empty` is now never fatal for any field, and that rule
  outranks the required/skip-only split.

- A usage error and an unreadable required input both exited 2, because argparse
  exits 2 on error and the documented contract claimed 1. Two very different
  failures sharing an exit code makes the contract untestable — the control that
  was supposed to tell them apart passed while asserting they were the same.
  Usage errors now exit 1, verified live: `pr-preflight.py 0` exits 1, and
  `pr-preflight.py 999999` exits 2.

A truncated head tree exits non-zero and resolves to None rather than {} — not
read is not the same as nothing found, and {} is what would let a half-read tree
publish as "this path has no rules file".

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
STEP 9 of #116. Six record fields, three degradations each —
empty, malformed, erroring — with the runner injected and no network anywhere.
The seventh field, skips, is the register the other six report into and has no
read of its own, so it gets its own controls instead of a row in the matrix.

No grep could do this. A grep reports every occurrence and leaves a human to sort
the legitimate hits from the real ones, which is not a check that can fail.

The matrix found a real defect: a malformed rules probe was coerced to an empty
list, so `configured: false` — a definite answer — was published from a response
nobody could read. It now reports malformed and configured: null.

Two fields where an EMPTY answer is a real answer rather than a skip, each named
in the suite with its reason rather than smoothed into false uniformity: an empty
rules list means no gate is configured, and GitHub answering with no closing
references means the PR closes nothing.

Also added: a head tree listing zero entries is now a skip, because it cannot be
reconciled with a diff that changed files, and resolving every path to "no rules
file" against it would be an empty read publishing itself as an answer.

Three controls assert the no-network property from different angles: subprocess
is broken outright and the whole CLI still runs, the real runner refuses any
binary but gh, and exactly eight reads are made with no call repeated.

The suite is discovered with `-t launchpad/scripts`, not the plan's `-t .`. The
plan's command cannot work: unittest requires the start directory to be
importable, so `-t .` needs __init__.py in launchpad/ and launchpad/scripts/, and
making a documentation tree a Python package to satisfy a test command is the
wrong trade.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
… go red

STEP 10 of #116. A control that has never been observed failing
has not been shown to test anything, and "the suite passes" is the claim this
harness exists to refuse.

Twenty functions across preflight_core.py and preflight_fetch.py, each replaced in
turn by a constant chosen to be the plausible wrong answer a careless
implementation would return — build_checks to [], build_nearest_rules to {},
is_fatal to False, gh_runner to a canned success. 20 of 20 mutants killed; the
raw run goes in the PR body as the plan asks.

The harness restores each file from the original held in memory and then verifies
the restoration byte-for-byte before exiting, because a mutation harness that
leaves a mutant in the tree is worse than none. It also refuses to start if the
suite is already red, so a survivor can never be a pre-existing failure
misattributed to a mutation.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…eck refusing

STEP 11 of #116. Every imported module name in both modules is
compared against an allowlist written in the test, so an import nobody
anticipated fails the check by default. A grep cannot do this: it reports every
occurrence and leaves a human to sort the legitimate hits from the real ones,
which is not a check that can fail.

Three evasions are closed, each demonstrated on a sample rather than asserted:

- ast.walk, not tree.body — a top-level scan finds {json} in a module that also
  does `import requests` inside a function, where walk finds both.
- Call nodes too — `importlib.import_module("openai")` creates no Import node, so
  an import-name scan reports it as absent.
- `__import__("openai")` needs no import statement at all.

The check lives in its own module, test_no_model.py, and reads the source rather
than importing it. That is what makes the evidence honest: httpx, openai and
anthropic are not installed here, so injecting one into preflight_core makes
every module that imports it die at import time — and a suite that is red
because a module could not load is not evidence that this check works.

The harness's injection phase confirms it, 13 of 13 refused: urllib.request,
requests, httpx, openai and anthropic at module level; requests, openai and httpx
inside function bodies; importlib.import_module and __import__ by call; and two
injections into the fetch layer. Raw output in the PR body.

Writing this caught a control failing in the way the module argues against: it
counted "subprocess.run(" as text and read its own prose about subprocess as a
second spawn site. It now counts call nodes, and requires the one spawn to sit
inside gh_runner.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…y resolves

INTERFACE.md for #116, so #117, #118 and #119 have something to
depend on that is not the source.

The three rules a consumer can rely on: null means "not read" and never "nothing
there"; every null field has a skip entry naming the endpoint that answered; and
exit 2 leaves stdout empty so there is no half-record to mistake for a whole one.

#120's CONTAINMENT.md names #116 in its contract table: must call
fetch.fetch_all, must never concatenate surfaces or build a prompt. Both halves
are addressed rather than assumed. The "must never" half holds today — every
author-controlled surface is a separately labelled JSON string, nothing is
concatenated, no prompt exists, and the AST controls keep the record module
unable to reach a model at all. The "must call" half cannot hold yet:
fetch.fetch_all is on an unmerged branch, importing it is impossible and copying
it would create the second copy this work was told not to make. It resolves by
rebase after #120 merges, and the injected runner is already the seam that makes
adopting their fetcher an adapter rather than a rewrite.

Also raised for the reviewer: #120 put its tree at launchpad/review-agent/ while
this plan assumed launchpad/scripts/, where PR #126 is also adding a script. They
should not stay split, and which way they converge is not this PR's call.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…nfirmed

Nine findings went in, seven confirmed, one refuted, two merged. All fixed, and
the refuted one hardened anyway. #116.

HIGH — the org-rulesets 404-to-forbidden reclassification was never asserted.
Deleting it left 80/80 green, because the three controls named for that property
hand-built a Read with skip=FORBIDDEN already baked into it. A control that
asserts an answer it supplied itself tests nothing, and one of those controls is
called test_org_rulesets_are_skipped_not_asserted_absent. The fake runner was
already feeding the real 404 text; now a control reads the reason back out of the
record.

MEDIUM — gh_runner caught FileNotFoundError and TimeoutExpired only, so any other
OSError escaped main() and exited 1 with a traceback: this tool's usage-error
code, telling a caller its arguments were bad. Three reproduced triggers, none of
them a missing binary — a non-executable gh, a directory named gh, and a gh that
is executable but not a runnable binary (wrong architecture or truncated
download, the plausible one). Now every OSError is unreachable, and the run exits
2. A non-executable gh with a working one later on PATH was always harmless:
CPython keeps walking PATH past EACCES.

MEDIUM — the 5xx-to-unreachable branch was untested. Only a 5xx carrying a JSON
message reaches it; a bodyless one prints no (HTTP nnn) and lands on the fallback.
Both forms now have controls, because "retry" and "the shape is unusable" are
different instructions to a consumer.

MEDIUM — the in-band GraphQL errors block was dead code with a comment asserting
a gh behaviour that does not happen: gh exits non-zero whenever a response
carries errors, so absence arrives as prose and is already classified correctly.
Kept, because the injected runner is a documented extension point and a direct
HTTP caller does return 200 with an errors body — but the comment now says so,
both halves have controls, and the error type maps to its own reason instead of
everything but FORBIDDEN collapsing to malformed.

LOW — a git directory or submodule named AGENTS.md resolved as a rules file,
suppressing the "no ancestor rules file" skip that was the truth. Blobs only now;
an entry with no recorded type is still kept, so a projection cannot drop a file.

LOW — Skips.reasons() had no caller and no test. Deleted.

REFUTED — diff.head_sha null with no skip cannot reach an emitted record: every
route that loses the head sha already exits 2. Hardened regardless, because
build_diff is a public function INTERFACE.md invites the next stage to call: it
takes the head sha directly and skips without one, so the invariant lives in the
function rather than in caller discipline.

The harness gained the phase that would have caught three of these. Whole-function
mutation cannot see a MISSING branch — it fails everything indiscriminately, which
proves the function is called and nothing about any decision inside it. Thirteen
branches are now disabled one at a time, and a snippet that no longer matches is a
hard error rather than a silent pass. TARGETS also gained the three functions the
adjudicator found absent despite the docstring's every-function claim, which is
now stated accurately.

103 controls, 23/23 mutants killed, 13/13 branches checked, 13/13 forbidden
imports refused.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Both High findings from the final branch review, on a branch now rebased onto
launchpad. #116.

HIGH — the record asked eight questions and none of them was the one readable
question about the gate. launchpad/AGENTS.md §6 was rewritten by merged PR #126:
the launchpad branch requires at least two approving reviews, the ruleset
enforcing that is invisible without admin:org — three endpoints report nothing —
and a live PR's reviewDecision confirms review IS required without exposing the
count. This record published `configured: false` and never asked. On a branch
that needs two approvals, that told #119 and #121 nothing gates it, with the
evidence one call away: the governing property of this whole stage, broken by the
stage itself.

There are two gates, so the record now asks about both. `configured` answers "is a
required STATUS CHECK visible"; `review_required` answers "is a REVIEW gate in
force". The count is not inferred — reviewDecision does not carry it and §6's
figure could drift. required_gate grew from two keys to five, added to the
enumerated field list in the same commit. The stale §6 quotation in the docstring
is replaced by what §6 says now.

Two recorded fixtures, because the signal genuinely differs by base: PR 86 on
launchpad answers REVIEW_REQUIRED, PR 92 on a topic branch answers "" — gh renders
a GraphQL null as an empty string, so "" means "not required" and treating it as
unrecognised would turn a readable answer into a malformed one.

HIGH — the no-model AST check named two files and pr-preflight.py was not one of
them, for three commits, while the suite reported the property as proved. An
inference call in the entry point — the file the issue calls "the script" and the
first one a reader opens — left all controls green. The files to check are now
enumerated from the directory, so a new pre-flight module fails closed instead of
going unchecked; the allowlist itself stays hand-written, because that is what
makes an unanticipated import fail. Scoped to this stage's files: #126's
pr_body_check.py shares the directory and its imports are not this suite's to
police.

MEDIUM, and the rebase is what made it real — the harness discovered all of
launchpad/scripts, which now holds #126's suite. Plain discovery there runs 156
tests, not this stage's 112. A regression in a module this branch does not own
would abort the harness and be reported as the pre-flight's; worse, a red foreign
control would let every pre-flight mutant register as "killed" while proving
nothing. SUITE and NO_MODEL_ONLY now name this stage's two test modules.

Rebased onto launchpad: 0 behind, 15 ahead, all 15 DCO-signed. 112 own controls
(156 with #126's), 24/24 mutants killed, 15/15 branches checked, 15/15 forbidden
imports refused — the two new injections being the entry point ones that would
have caught the second finding.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…for them

The final branch review left eight non-blocking findings. Six were one-line
corrections inside files this PR is still adding, so they are fixed here: filing
an issue to correct a docstring and then merging the wrong docstring is worse
than fixing it. The seventh is #148, because it needs a real
StatusContext response that no PR in either repository currently carries. The
eighth cannot be fixed retroactively — the plan's GATES ordering was not held —
and stays disclosed in the PR body.

Two docstrings that misstated their own evidence:

- preflight_core said PR 86's body writes one `Closes #79`. It writes two, on
  consecutive lines; the under-reporting belongs to the plan's `re.search`, which
  a control reproduces. As written it argued that the body-regex mechanism was
  unfixable, weakening a decision whose real support is PR 92 and #125.
- fetch_all's own docstring said "seven reads" and was stale twice over — the
  module header and a control both say nine.

Read.name was written at 24 sites and read at none, so a skip entry silently
dropped which read failed: build_pr reports under "pr" when `meta` is what broke.
Skip entries now carry `source`, added to the record's enumerated field list in
the same commit, with a control asserting source != field on exactly that case.

The mutation harness promised a byte-for-byte restore verification that only its
first phase performed. Two phases were added later and neither verified, in a
tool that rewrites tracked source. All three now restore through one helper that
checks its own work.

record-delete-fixture.sh compared by branch name — the exact recording error
README.md and record.sh both name, which once produced an empty file list for a
real six-file PR. It reads base.sha now. The committed fixture was always sound;
the script's own call was dead output.

The plan's OPEN section said #110 is open. #110 decided 45 minutes after that file
was committed, and its comment names #116's workflow invocation among what it
unblocks. The plan merges and a PR body does not, so the correction belongs in the
file.

113 own controls (157 with #126's), 24/24 mutants killed, 15/15 branches checked,
15/15 forbidden imports refused.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…he rebased tree

The branch was rebased onto launchpad, so its history diverged from what was
already pushed and only a force-push could reconcile them. §6 says not to
force-push during review, and the safety hook enforces it, so this merge takes
the other road: it makes the pushed tip an ancestor of this branch, and an
ordinary push then fast-forwards.

Strategy `ours` keeps this branch's tree byte-for-byte and discards the old tip's,
which is safe here for a reason that was checked rather than assumed: every commit
in f270512 was replayed by the rebase, `git diff f270512 HEAD` reports no
deleted file and no removed-only content, and this tree carries all of it plus the
two review-fix commits and launchpad's 20 intervening commits.

No file content changes in this commit. The tree is identical to the commit before
it, which is the property that makes the strategy honest.

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Two claims in a committed contract document that no longer reproduced: the suite
counts moved to 113 and 157 when the review fixes landed, and skip entries gained
`source` — which read of the eight failed, as distinct from which record field
lost a value.

A document stating a number a reader cannot reproduce is the same defect class
this stage exists to prevent, one level up: an assertion that has stopped being
checked.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…hpad

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Two High, five Medium, four Low. #116.

HIGH — the check list was capped at first:100 and totalCount, the field that
would say so, was fetched and read by nothing. A PR with more than a hundred
contexts would have published the first page as the whole list, with no skip and
exit 0, and required_gate.configured is computed from that list — so a required
check past the boundary read as "no gate configured". This is the exact shape the
truncated reason exists for on the trees API, with the detecting value already in
hand. It now fails closed.

One correction to the review that found it: its reproduction reported "totalCount
says 120" against the real fixture. The fixture and the live API both report 47
contexts and totalCount 47, verified twice. The defect is real and latent, not
observed — and a control now pins that PR 86 is genuinely un-truncated, so the
guard cannot be mistaken for one that never had a case.

HIGH — the unreadable-branch_rules return dropped the three review_* keys every
other return spreads. A consumer written to INTERFACE.md's five-key contract got
a KeyError whenever the rules probe 403s, while the run exited 0 — the one
failure "null means not read" cannot express, because the key was absent rather
than null. The control that was supposed to cover this asserted `configured`
alone.

MEDIUM — checks were unpinned to any commit while build_diff refuses to return
without a head sha. The rollup is reached through commits(last:1), resolved
server-side at its own moment, and its oid was fetched and dropped. A push
between the two reads gave a diff for commit A and checks for commit B with
nothing able to say so. They must now agree.

MEDIUM — "enumerated from the directory" was a hand-written list wearing a
filename prefix: a stage module not called preflight* escaped discovery, the
allowlist and the no-model check entirely. Discovery is by exclusion now — a new
file is ours until someone names it otherwise here, with a reason — and a control
creates an unanticipated module to prove it fails closed.

MEDIUM — the controls ran nowhere. Deferring CI to #119 was right for a live
pre-flight run and wrong for the suite: it needs no token and no network, proved
under `env -i` with no HOME and no GH_TOKEN, 165 tests green. #126 had already
built the workflow it belongs in. The mutation harness runs there too, because
24/24 in a PR body is a claim and not a gate.

MEDIUM — the PR body moved out of launchpad/plans/. Committing it there answered
the plan's own reserved question ("whether plans belong in this repository at all
is unresolved... one git rm either way") by accumulation, and put a second
artefact kind in a directory launchpad/AGENTS.md §3 does not list. It sits beside
the code it describes instead, and the decision stays the reviewer's.

MEDIUM — the body also asserted the plan still calls #110 open, four paragraphs
after saying that was fixed, and after it was.

LOW — a stale test count in the harness comment; the plan's six "24 checks"
claims, corrected in place the way its #110 line was, with the recorded 47 beside
each; a contract table naming the read "review_decis"; and the branch-rules skip
filed bare where its two siblings are dotted, so neither selector found all three.

Three branch mutations added for the new guards, and one for the return that
dropped the review half — the phase that catches a missing branch, which is how
these would have been caught the first time. 165 controls, 24/24 mutants,
18/18 branches, 15/15 injected imports.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
The mechanical findings from the third final review. #116.

HIGH — the previous commit added a CI job to .github/workflows and left four
statements saying no workflow file is added, one of them in the plan's OPEN
section. The worst was not the contradiction but which sentence went stale: the
security rationale argued that `pull_request` is the right trigger BECAUSE no
workflow is added here. That argument was gone and nothing replaced it, while the
Not-verified list told a reviewer the branch touches no workflow — so they would
skip the one file in the diff that executes on every PR.

All four now describe what exists: a job that runs this stage's controls and its
mutation harness, needing no credential, which is why #110 never gated it;
invoking the pre-flight against a live PR still needs the scoped token and stays
with #119. The trigger rationale is rewritten to argue why THIS job is safe — a
read-only token, no secrets, no persisted credentials, two approving reviews to
merge, and a defeated control suite that misleads a reviewer without being able
to write anywhere.

MEDIUM — the new job's checkout dropped the `persist-credentials: false` that the
sibling job argues for at length, in the job that runs the harness: code that
rewrites and executes repository source by design, from the fork's own commit.
Both checkouts now set it.

MEDIUM — the pasted evidence in the PR body no longer reproduced: 113 and 157
tests against 121 and 165, a 15-row branch listing against 18, and one paragraph
claiming 13 injected imports where its own pasted block said 15. Re-pasted from a
current run, including the `env -i` run that backs the no-credential claim.

MEDIUM — INTERFACE.md's counts were stale again, one commit after 34d50e4
existed only to make them exact. 157 -> 165, 113 -> 121.

LOW — the body described ">100 checks" as an unverified edge. It is now a hard
refusal: exit 2, no record, a truncated skip. Said so, and stated plainly that
pagination is unimplemented and untracked, so a reviewer can decide whether
refusal is the permanent answer.

Left alone deliberately, because they are judgement calls rather than mechanics:
scoping the CI job with a paths filter so another task's new .py under
launchpad/scripts does not fail this stage's allowlist; whether a raced
commit-desync deserves its own SKIP reason instead of `malformed`; and whether
launchpad/AGENTS.md §3 should list scripts/ and plans/, which is a governance
file and inherited from #126 rather than introduced here.

121 own controls (165 with #126's), 24/24 mutants, 18/18 branches, 15/15 imports.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
8c2a0e9's message claimed four statements about the workflow were corrected and
that the ">100 checks" note was rewritten. Its diff corrected three of the four
and never touched the note. The fourth statement was in the plan's OPEN section —
the one place the previous commit named explicitly as the reason the fix
mattered, and the one it missed.

Root cause, because it is the interesting part: those edits were applied with
str.replace and no assertion. When the search text did not match — the OPEN
sentence had been reworded by an earlier commit, and the checks bullet had been
split in two — replace returned the string unchanged and the script reported
success. A no-op edit read as a completed one, and the commit message asserted
it. That is the same defect this whole branch exists to prevent, in the process
that writes it rather than in the code it writes: an absence reading as a value.
Every substitution here asserts its target is present first.

Also corrected: the ">100 checks" bullet now states what the code does — refuse
with exit 2 and a truncated skip rather than truncate — and says plainly that
pagination is unimplemented and untracked, so a reviewer can decide whether
refusal is the permanent answer.

NOT fixed here, because it is not this branch's: launchpad/README.md says one
approving review while launchpad/AGENTS.md §6 says two. The review sourcing that
to this branch is mistaken — AGENTS.md is not in this diff. #126 changed §6 when
it merged and left README behind. It is a real contradiction for a new
contributor and belongs in its own issue against launchpad.

Refs #116

Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
@serina-mcfall serina-mcfall added by:agent Filed or authored by an AI agent, not a human area:agents-and-automation Agentic workflows and automation — proposals and delivery (see #40) labels Aug 13, 2026

@serina-mcfall serina-mcfall left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary — read this part first

Solid work, two real gaps worth fixing before merge — neither is a blocker-shaped emergency, but both hit the property this PR exists to guarantee.

I re-ran your mutation harness and test suite myself rather than trusting the pasted output — everything you claimed (165 tests, 24/24 mutants, 18/18 branches, 15/15 injected imports) reproduced exactly. That part is real, not decorative.

The two things I'd fix first:

  1. If GitHub's API ever returns a response missing the reviewDecision field entirely (not null — just absent), the tool silently reports "no review required" as a clean fact, with no flag that anything was unread. That's the exact failure mode INTERFACE.md says this stage exists to prevent ("a consumer that treats null and nothing there alike will publish a review of nothing as a review of something clean") — and it's happening on the one field that gates whether an approval requirement exists.
  2. The test that's supposed to guarantee "only one place in this codebase may ever spawn a process" only checks for the literal text subprocess.run. subprocess.Popen, .call, .check_output would sail through undetected — today's code happens to be clean, but the guardrail meant to keep it that way has a hole.

Two independent reviewers looked at this (one at the implementation, one at the tests) — findings below are both, unretouched.


Full review — independent code review

(no memory of writing this code; verified everything below by reading the shipped files and reproducing behavior directly, not by trusting the PR body)

High — preflight_core.py:687 — absence silently becomes a definite value in review_required

decision = _get(review_decision.data, "reviewDecision", default="")

_get's default fires identically whether the key is entirely absent or present as null. Reproduced directly against the shipped module:

weird = core.Read("review_decision", data={"someOtherField": 1}, endpoint="e")
core._review_gate(weird, skips)
# -> {'review_required': False, 'review_decision': None, ...}
# skips.entries == []   <-- nothing flagged

Not caught by the mutation harness — no branch exists to disable for "key present in the object graph but the expected leaf missing," because no branch currently tests that distinction.

Failure scenario: under a differently-scoped token (already flagged in your own "Not verified" section as future work for #119) or a gh output-shape change, a response that lacks the key produces an unflagged, false "review not required."

Fix: check "reviewDecision" in (review_decision.data or {}) before treating a missing key the same as an empty string; add a MALFORMED skip when it's absent entirely.

High — test_no_model.py:241 — the "one spawn site" guarantee only matches literal subprocess.run

test_only_the_fetch_layer_may_spawn_anything checks for AST calls named exactly subprocess.run. I proved the gap by patching a scratch copy of preflight_fetch.py with a module-level subprocess.Popen(["curl", url]) outside gh_runner — all 13 tests in test_no_model.py still passed. The shipped code today is clean (verified via diff), so this isn't live right now — but the enforcement meant to keep it that way doesn't cover Popen/call/check_call/check_output.

Fix: flag any Call whose dotted name starts with subprocess., not just .run — or assert gh_runner is the only place the subprocess name is referenced at all.

Medium — preflight_core.py:766 — an empty, non-truncated head tree exits 0 despite the code's own comment calling that state irreconcilable

The comment says "A head tree listing nothing cannot be reconciled with a diff that changed files" — but is_fatal's EMPTY rule is unconditional, so this state is still non-fatal. Reproduced: a tree Read with data={"tree": [], "truncated": False} alongside a compare naming a changed file returns successfully with a lone EMPTY skip, exit 0.

Fix: either special-case "entries empty AND diff non-empty" as fatal, or soften the comment if EMPTY-but-irreconcilable is an accepted judgment call — right now the code and the comment disagree.

Low — preflight_core.py:255labels silently defaults to [] when the key is entirely absent

Same class as the High finding above, much lower stakes (informational field, not a gate). Reproduced: meta.data with no labels key at all returns labels: [], indistinguishable from a real zero-label PR.

Checked and clean — no finding

  • AST allowlist import coverage: correctly walks the full tree (not just top-level), catches nested imports, and the allowlist itself is narrow and correct.
  • Failure classification fails closed: an unrecognized gh stderr shape for a required input still exits non-zero rather than becoming a clean record.
  • Single-subprocess-call-site at the current code level: exactly one subprocess.run in shipped code, confirmed by direct grep — the gap is in the enforcement test (above), not in the code as shipped today.
  • No prompt-injection surface: author-controlled text (title, body, paths) lands only in labelled JSON fields, never concatenated into anything resembling a prompt.

Full review — independent test-suite review

(also no memory of writing this suite; re-ran the real suite and the real mutation harness in a scratch checkout rather than trusting the pasted output)

Confirmed clean, independently reproduced: python3 -m unittest discover → 165 tests, OK. mutation_harness.py run for real → 24/24 mutants killed, 18/18 branches checked, 15/15 injected imports refused, matching the PR body line-for-line including per-mutant failure counts. Verified the harness restores files byte-identical afterward. suite_is_red() shells out to the real test modules — not a separate, weaker assertion set. This is a genuine, working mutation tool.

Medium — preflight_fetch.py:232closingIssuesReferences has no totalCount and no truncation guard, unlike the analogous (and tested) checks cap

CLOSING_QUERY requests closingIssuesReferences(first:50){nodes{number}} with no total count, and build_closing_issue never checks for truncation — unlike build_checks, which explicitly guards total > len(contexts) and has a dedicated test for it. No test here exercises a PR closing 51+ issues.

Failure scenario: a PR closing 51+ issues silently reports only the first 50, with no truncation flag — the exact failure mode checks was hardened against, missing one field over.

Fix: add totalCount to the query, a truncation guard mirroring build_checks, and a matching test.

Medium — preflight_core.py:823build_record's "key entirely missing" fallback path is never directly tested

Every test that builds a reads mapping supplies all keys — none omits one outright to test the SKIP-ONLY fallback (as opposed to a present-but-failed key, which is tested). INTERFACE.md explicitly invites future callers to call these functions directly, so this is reachable production code, not dead weight — but if the fallback logic broke, no test would notice.

Fix: one test per SKIP-ONLY key, calling build_record with that key omitted, asserting the fallback behavior.

Low — test_preflight_core.py:524 — the 100-check truncation test uses a generic case, not the literal edge

Tests total=120, count=2 rather than pinning total=101, count=100 — the production logic is size-agnostic so this isn't a real gap, but no test asserts the GraphQL query actually requests first:100 specifically.

Checked and clean — no finding

  • No test-cannot-fail patterns found across any test class — spot-checked all of them.
  • No logic-inside-tests pattern (no test reimplements production logic to compare against itself).
  • StatusContext/legacy-commit-status and same-repo-truncated-tree gaps are honestly disclosed in the PR's own "Not verified" section, and independently confirmed to be exactly as small as claimed — not hidden theatre.
  • Review-gate-count-is-never-invented is deliberately, correctly untested for a specific number.

Both reviews performed independently of the session that wrote this branch — one on the implementation, one on the test suite — each re-ran your actual code/tests rather than taking the PR body's claims on faith. CI is green.

@tucktuck101
tucktuck101 merged commit fe22a07 into launchpad Aug 16, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agents-and-automation Agentic workflows and automation — proposals and delivery (see #40) by:agent Filed or authored by an AI agent, not a human

Projects

None yet

Development

Successfully merging this pull request may close these issues.

task: deterministic pre-flight for the PR review agent

2 participants