Skip to content

feat(skills): dispute wrong claims, align PR and evidence gates, refresh VM and roster guidance - #201

Merged
kjelly merged 16 commits into
mainfrom
chore/pr-ready-dispute-review-comments
Oct 31, 2026
Merged

kjelly merged 16 commits into
mainfrom
chore/pr-ready-dispute-review-comments

Conversation

@kjelly

@kjelly kjelly commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

Neither pr-ready nor issue-to-pr could say that the thing it was asked to act on was wrong.

  • pr-ready: the gate blocked on every unresolved thread unless it was rated P3/P4, nothing told the orchestrator to reply, and the shared rubric's "a finding that does not reproduce is returned, not fixed" only covered the skill's own pr-reviewer. The easiest way through the gate was to change code until the comment went away. The gate also never looked at most of this repository's reviews: the last 60 PRs have 0 inline review threads; every review is a top-level PR comment.
  • issue-to-pr: INVALID_OR_CLOSED was an undefined label. Dismissing an Issue needed no independent check and no reply on the Issue. A partly wrong Issue (real symptom, wrong cause, proposed fix or acceptance criterion) had no path: SA writes the acceptance criteria and the gate only checks the ones listed, so a stated criterion could be dropped silently. create_pr.py required Fixes #N even when part of the Issue stays open, and the documented gh issue view did not fetch comments.

What changed

Both skills now treat what they are given as claims to verify, with the same shape:

  • Triage first. pr-ready: each review comment is valid, invalid, needs_evidence or nonblocking (P3/P4). issue-to-pr: each Issue claim (symptom, stated cause, proposed fix, stated acceptance criteria) is valid, invalid, needs_evidence or out_of_scope. Only valid claims are implemented. Protocols: pr-ready/references/review-comments.md, issue-to-pr/references/issue-claims.md.

  • Independent verdict. The reviewer gets the claim and the code, not SA's or Root's conclusion, and returns holds / does_not_hold / undetermined. A dispute stands only if that verdict agrees; otherwise the claim counts as valid.

  • Answer where the author sees it. pr-ready: in the thread (then resolve it) or a PR comment containing the disputed URL. issue-to-pr: a comment on the Issue, containing the source comment's URL when the claim came from a comment. Recorded as disputed_review_comments / disputed_issue_claims.

  • No loop. A claim raised again without new evidence after a confirmed dispute goes to a person (DRAFT-BLOCKED / NEEDS_DECISION), not into another fix or reply round.

  • Gates check the replies on GitHub. pr_gate.py: both comments on the PR, reply later, inline reply in the same thread and still its newest comment, other replies contain the disputed URL. issue_gate.py: source is the Issue or one of its comments, reply is a later comment on the Issue and links the source comment. Both: entry HEAD-bound and reviewer-confirmed. Comments are fetched only when the evidence has disputes.

  • issue-to-pr outcomes. INVALID_OR_CLOSED is split into CLOSED and INVALID; INVALID and NEEDS_DECISION mean no PR, a reply on the Issue, and the Issue left open for a person. issue_resolution: "partial" (needs at least one dispute) makes create_pr.py require Refs #N with no closing keyword; fixes (the default) requires Fixes #N and is refused while a claim stays open: any needs_evidence dispute, or an out_of_scope one without a follow_up_issue_url (same repository, not this Issue, linked in the reply, and confirmed by gh issue view to be an Issue, open or closed). mark_ready.py re-checks the PR body against issue_resolution before gh pr ready. The documented gh issue view now fetches comments.

  • Role prompts (SA, Coder, Reviewer of both skills) updated identically on claude/codex/agy; PROJECT-CHANGES.md of each skill records the local edits.

  • pr-ready gate on repositories without required checks. gh pr checks --required exits 1 with no JSON and no required checks reported on the '<branch>' branch when nothing is required, and pr_gate.py failed on the missing JSON, so the gate could never pass in this repository. It now reads that as zero required checks only when gh exits 1 with empty stdout and stderr is exactly that message; valid JSON on stdout is always parsed first, so a failing required check whose name contains the phrase is kept. issue-to-pr's pr_ci.py had the same fallback with a substring match (pre-existing on base) and now uses the same rule; pr-ready/tests/test_required_checks_parity.py runs one matrix through both helpers. Every other check must still pass and any other failure still fails closed. The fake gh in the tests always answered --required with JSON, which hid this; it now reproduces the real output.

  • No Draft/Ready loop between pr-ready and pr-review-merge. pr-ready could mark a PR Ready in states pr-review-merge returns to Draft: an unresolved P3/P4 thread, a reviewer-confirmed unrelated failing check (every check here is non-required), and CHANGES_REQUESTED. pr-review-merge also never saw the disputes, so a fresh reviewer could repeat a disputed finding. Now:

    • pr-ready requires every thread resolved (P3/P4 threads are disputed as nonblocking and resolved) and rejects the upstream unresolved_nonblocking_threads field.
    • pr-review-merge decides WAITING, never MERGE, for a check listed in unrelated_failing_checks (same shape as pr-ready's) and for CHANGES_REQUESTED (waiting for re-review); open objections still come back as reviewer findings.
    • Root passes disputed findings and their replies to prm-reviewer. A repeated finding carries disputed_reply_url and drafts only with new_evidence; otherwise WAITING for a person, as on the pr-ready side. P4 findings are accepted (shared rubric).
    • pr-review-merge/tests/test_pr_ready_handoff.py runs both gates over the same GitHub states and fails if a state pr-ready accepts is DRAFT in pr-review-merge, or MERGE with a failing check.
  • No rm -rf in the shared VM image directory. Four vm-target-spec-testing passages said to sudo rm -rf /var/lib/libvirt/images/pilot/<name> after down, and the clean-room contract's first teardown step removed every VM "belonging to the runbook". Every clone and session on the host shares the pilot data dir and that directory, so both could delete another session's VM. The rm -rf was never needed: down removes the per-target directory, and a Docker bind mount inside the VM lives in the overlay it deletes (no host/VM shared filesystem). The docs now say to down only VMs this session created and to report a directory that survives down; the clean-room step removes only this run's own or user-named VMs. TestDown_RemovesState now asserts the directory is gone.

  • freeipa-roster-authoring caught up with the code. Schema v3 (it taught v2); the real lint output; lint had no vault option and exited 1 on an encrypted roster (the skill said it decrypts in memory), so the skill now uses the new roster lint --vault-password-file (below); and a new section on what state: disabled / state: absent do in FreeIPA (absent is a permanent ipa user-del), that deleting a YAML entry revokes nothing, that absent entries are tombstones that stay, and pilot roster remove-user|remove-group for never-applied entries.

  • pilot roster lint --vault-password-file (Go). Lint reads the roster once (inventory.ReadRosterPlaintext) and decrypts an ansible-vault roster in memory with the same ansible-vault view call roster migrate uses; no plaintext file is written, and the report is the same as for the plaintext roster. A YAML error in a decrypted roster keeps only the line numbers (yaml.v3 quotes part of the value). Without the flag an encrypted roster exits 1 with "rerun with --vault-password-file"; the flag is not read for a plaintext roster; --upgrade re-encrypts through MigrateRosterFile. Without --upgrade lint takes no lock and writes nothing.

  • Schema v3 in the example and docs. playbooks/apply/freeipa-identity.roster.example.yaml gets schema_version: 3 and grants: [], exactly what lint --upgrade and MigrateRosterV2ToV3 produce, so behaviour does not change. docs/runbooks/freeipa-identity.md §3 shows v3 and a new step 5 (lint an encrypted roster) with output from a local run; docs/verification/freeipa-identity.md §0/§1.6 say v3 is current and 1/2/3 are accepted (the apply gate's in [1, 2, 3]), with no checklist row changed; minimal-poc-configuration.md and a trec reference no longer say the wizard writes v1.

  • issue-to-pr mark_ready.py refuses open review feedback. It reads every page of the PR's review threads and refuses Ready while one is unresolved (pr-review-merge would return that PR to Draft), and refuses reviewDecision CHANGES_REQUESTED, pointing at pr-ready, which owns review feedback on a Draft. A failed or malformed lookup blocks. GraphQL query and answers captured from gh 2.103.3 (feat(skills): dispute wrong claims, align PR and evidence gates, refresh VM and roster guidance #201, fix: handle non-zipped artifact downloads cli/cli#13013). Also fixed, from the last review: a mistyped issue_resolution such as Partial is reported as such instead of as a missing Fixes #N, and a follow-up Issue number over 10 digits is a format error instead of an int() traceback.

  • pr-review-merge checks the mandated actual-run record before MERGE. The gate reads .github/pr-evidence.json at the PR's base SHA (a PR cannot weaken its own requirement) and target_validation: blocked, a missing or malformed record, a required record whose artifact is not committed at HEAD, outside artifact_glob or not naming the tested revision, or a tested revision that is not HEAD or an ancestor with an evidence-only delta, all make it WAITING, never MERGE (DRAFT still needs the reviewer's P1 finding). The manifest gains required_paths (playbooks/**, docs/verification/**, contracts/**), for which pr-review-merge refuses not-needed and issue-to-pr's gate refuses not-applicable (from the merge base, both names of a rename), so an Issue PR does not reach Ready only to wait at merge. The delta logic is issue-to-pr's repo_evidence.py, loaded by path so both gates share one rule; the data comes from GitHub's contents and compare APIs (captured shapes; a compare at GitHub's 300-file cap fails closed). The integration tests now lock a per-test PRM_STATE_DIR: they used ~/.cache/pr-review-merge, so two concurrent make skill-test runs failed each other.

  • vm-target-spec-testing rewritten (it was about three months behind). Main flow is vm-target test (syntax, check/diff, snapshot, apply, verify, idempotency) and topology test, with --ephemeral for fresh hosts and cross-playbook ordering; manual up/run/verify is for debugging. Verify playbooks are no longer described as spec --generate output, and output goes to a candidate-bound docs/evidence/ record instead of being pasted into runbooks. AGENTS.md rules are linked from a "your change touches X → read §Y" table, not copied. Other stale claims fixed against --help and the code: --sandbox is not run's default and test has none; run passes -- through as the playbook path; a container image without an ssh client; resize-disk instead of re-provisioning. SKILL.md 426 → 261 lines.

  • pilot-trec-verification no longer contradicts AGENTS.md. The sudo gotcha called sss_cache -E && systemctl restart sssd "the fix"; AGENTS.md §5.9.5 says a result that needs a cache flush is a bug symptom. It now records what is known (seen in minimal-poc rounds 13, 23 and 31 even after c45ed559 added a flush to the identity playbook; bug(fixtures): hostauthz/audit sim 的 sudo 檢查依賴 client SSSD 的 sudo refresh 時機,結果隨快取狀態變動 #95, bug(freeipa-identity): 撤銷 SSH key 後,enrolled 主機的 SSSD 快取仍讓該 key 登入一段不定的時間 #114), says the root cause is unknown, and says to capture facts, file or update an issue, and treat that run as a deviation, not passing evidence. Roster guidance uses pilot edit's roster editors (a table of screens from the code) and allows hand edits only for what no screen covers, each named. A formal run starts from a frozen candidate (AGENTS.md §1.5). Runbooks no longer carry driver scripts or cast paths.

  • Flaky drift-probe test. "templated ansible_host over ssh" used the bare password as a single-label host name; on CI run 41076444653 two subtests got "details withheld" instead of "Could not resolve hostname" (ssh prints that for every resolver error, so the name most likely resolved through the runner's search list; the raw message is withheld by design). Both copies of the test now append .invalid. (RFC 6761, absolute), so no resolver answers it; the sentinel still reaches ansible's raw output.

Limits

  • pr-review-merge's gate checks only the shape of disputed_reply_url (a comment on this PR), not that the comment exists or what it says; an unflagged repeat of a disputed finding is not detectable by the gate, only by Root and the reviewer reading the replies. Ran read-only against this PR live: a repeat without new evidence was WAITING, not DRAFT.
  • pr-review-merge's actual-run check: a PR that merges the base branch after its run has a non-evidence delta and waits for a person (strict, per docs/actual-run-evidence.md §4, and likely common); "names the tested revision" accepts any 7+ hex prefix in the record; a manifest with more than one required_evidence entry is WAITING.
  • This PR itself changes two files required_paths covers: playbooks/apply/freeipa-identity.roster.example.yaml (data, not a playbook) and docs/verification/freeipa-identity.md (prose, no row). The base manifest has no required_paths, so the gates take the not-applicable below; under the new rule such a change needs a run record or a person's decision.
  • mark_ready.py checks review threads and the review decision only. Review findings posted as review bodies or conversation comments (how this repository reviews) are not machine-checked there; --confirm-complete now attests they are answered.
  • The flaky test's cause is inferred, not observed: the probe withholds ansible's raw message.
  • Found and left alone: make poc-checkmode-test (scripts/topology-checkmode-test.sh) tears down every VM it adopted by name, including another session's (the skill now warns to check vm-target list); AGENTS.md §1.3/§3 cite TestRegression_SpecAndInventoryAgree, which does not exist; trec references still cite deleted tui_multiselect.go/tui_confirm.go/tui_textinput.go; freeipa-identity-apply.yml:89 and topology-checkmode-test.sh:41 comments still mention schema 1/2; minimal-poc-architecture.md still calls the SSSD flush the fix (that runbook has its own update skill).
  • The gates cannot judge whether a dispute is right; that is the reviewer's independent verdict.
  • For PR conversation comments and Issue comments the gates cannot see a later disagreement that does not quote the reply; only inline PR threads have the "reply is still the newest comment" check. Root and the reviewer read later comments, and the escalation rule applies.
  • The reply/resolve commands in the two protocol docs (addPullRequestReviewThreadReply, resolveReviewThread, POST issues/{n}/comments) were checked against the GraphQL schema/REST shape only, not executed, since that would post to GitHub.
  • gh_issue_thread relies on gh issue view --json comments returning every comment; no Issue here has more than 100 comments to confirm the paging.

Verification

  • make skill-test: issue-to-pr 200, pr-ready 51, pr-review-merge 80, all passing (the base had 119 / 16 / 42).
  • go build ./..., go vet, and full go test of cmd/pilot/cmd, internal/inventory and internal/accessgrants pass locally; spec --lint of freeipa-identity.md 0 findings; the identity spec regressions and TestSpecPlaybookTagAlignment pass; the example roster lints ok: schema v3.
  • roster lint --vault-password-file: real cobra command against rosters encrypted with ansible-vault: same report as plaintext for six fixtures including the example, wrong and missing password file, flag ignored on plaintext, no decrypted content left in the roster dir, $TMPDIR or ANSIBLE_LOCAL_TEMP, --upgrade re-encrypts. All seven fail against the previous lint.
  • mark_ready.py / P4 fixes: 10 mutants of the new checks all fail the tests; an unmutated copy passes. pr-review-merge: 24 of its 32 new tests fail against the 74eefe2 gate (the others are runtime-shape and lock-dir tests); a read-only live run accepted bug(os-patch-sla): target_group 寫死成 sandbox,staging/prod stage 下 play 選不到主機,patch SLA 從未套用 #177's real record and refused it with the baseline SHA as tested_revision.
  • Independent review of the code commits (af9189a, c66a5bf, 2f75964): PASS, no P0–P2. It made the old single-label host resolve with HOSTALIASES and showed the old tests fail and the new ones pass; ran unresolved_threads, file_at and compare read-only against live GitHub; and its own mutants of mark_ready and the gate were caught (two survivors are redundant with other checks). Its two P3s are fixed in e99c127: issue-to-pr now refuses not-applicable for required_paths (5 mutants, each caught), and the pr-review-merge hand-off bullet no longer contradicts the reviewer's P1 for a missing record.
  • Review round on e5f499e (comment): R1 and R2 fixed in fc54eab and 74eefe2. An independent reviewer confirmed both closed on 74eefe2: the new tests fail at e5f499e for the intended reason (50 failures), the counterexamples are rejected or kept at HEAD, and its own 17 mutants are all killed; the implementer's 35 mutants, 0 survivors. gh issue view (Issue, PR number, missing number) and the no-required-checks stderr fixtures were re-captured from gh 2.103.3.
  • Every new guard in the gates and in create_pr.py mutation-checked: removing each one makes a test fail. For the hand-off: putting back pr-ready's thread exemption, drafting unrelated checks or CHANGES_REQUESTED, dropping P4, drafting a repeat without new evidence, accepting short evidence or a reply on another PR, and unconfirmed or short exemptions each fail a test.
  • Required-checks fix: run read-only against this PR with gh 2.103.3. Before the fix the gate stopped at Invalid/missing JSON from gh (exit 1): no required checks reported .... After it, the gate evaluated every check with no CI error (only the deliberately minimal evidence file was reported). The fake gh output is copied from that capture. Searched every gh pr checks caller in .agents/skills: pr_ci.py already handled it, and pr-review-merge does not use --required.
  • VM teardown: real vm-target up, a file written inside the VM, vm-target down on this host: the per-target directory, libvirt domain and state record were all gone. TestDown_RemovesState fails if teardown stops removing the directory.
  • Roster skill: every lint line and failure quoted was captured from a pilot binary built from this branch, on the repository example, its --upgrade copy and an ansible-vault encrypted copy (throwaway password, deleted). State semantics read from freeipa-identity-apply.yml, roster_validate.go and roster remove-*.
  • Fixture shapes copied from real output: GraphQL PR threads/comments/reviews (fix(detection-engine): only back up state.db when this run will install the binary #190, fix(freeipa,dns): leave out replicas FreeIPA does not list as DNS servers; one forwarder rule (follow-up to #73) #92, Document extrepo installation on Debian cli/cli#14796) and gh issue view --json number,url,createdAt,comments (bug(os-patch-sla): target_group 寫死成 sandbox,staging/prod stage 下 play 選不到主機,patch SLA 從未套用 #177). Both dispute checks were also run read-only against real data (fix(freeipa,dns): leave out replicas FreeIPA does not list as DNS servers; one forwarder rule (follow-up to #73) #92, bug(os-patch-sla): target_group 寫死成 sandbox,staging/prod stage 下 play 選不到主機,patch SLA 從未套用 #177): a reply without the link or before the claim is rejected, a valid one passes.
  • go test ./cmd/pilot/cmd -run TestAgentSkill: layout/prompt consistency passes. py_compile, git diff --check clean.
  • actual-run evidence (.github/pr-evidence.json): not applicable. The Go change is roster lint, a read-only CLI path proven by native-tool tests with real ansible-vault; the example roster changes exactly as the loader's automatic v2→v3 migration does; the spec change is prose only (no row, command or playbook); everything else is skills, docs and tests.

🤖 Generated with Claude Code

kjelly and others added 2 commits October 30, 2026 17:26
pr-ready had no way to say a review comment was wrong. The gate blocked
on every unresolved thread unless it was rated P3/P4, nothing told the
orchestrator to reply, and the shared rubric's "a finding that does not
reproduce is returned, not fixed" only covered the skill's own reviewer.
The path of least resistance was to change code until the comment went
away. In this repository every review so far is a top-level PR comment
(0 inline threads in the last 60 PRs), which the gate did not look at.

Review comments are now claims to verify:

- Root triages each claim as valid / invalid / needs_evidence /
  nonblocking; only valid ones go to the Coder.
- pr-reviewer judges a disputed comment from the code without Root's
  conclusion; the dispute stands only if its verdict agrees.
- Root replies on GitHub (in the thread, or a PR comment linking the
  disputed URL) and records disputed_review_comments in the evidence.
- A claim raised again without new evidence goes to a person as
  DRAFT-BLOCKED instead of another fix/reply round.

pr_gate.py verifies each dispute against GitHub: both comments are on
the PR, the reply is later, an inline reply is in the same thread and
still its newest comment, any other reply contains the disputed URL, and
the entry is HEAD-bound and reviewer-confirmed. Conversation comments
and reviews are fetched only when the evidence has disputes.

Tests: 20 new cases (every new guard mutation-checked); URL/timestamp
shapes from real GraphQL output; the check was also run read-only
against #92. make skill-test and TestAgentSkillLayout pass.
The reply/resolve mutations were checked against the GraphQL schema
only, not executed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
issue-to-pr had no defined way to say an Issue was wrong. INVALID_OR_CLOSED
was an undefined label, dismissing an Issue needed no independent check
and no reply on the Issue, and a partly wrong Issue had no path at all:
SA writes the acceptance criteria and the gate only checks the listed
ones, so a stated criterion could be dropped silently (the relaxation
AGENTS.md §0.2 forbids). create_pr.py required "Fixes #N" even when part
of the Issue stays open, and the documented gh issue view did not fetch
comments.

Issue claims are now hypotheses, mirroring pr-ready's review comments:

- SA reports per claim (symptom, stated cause, proposed fix, stated
  acceptance criteria) whether it holds; Root classifies valid / invalid
  / needs_evidence / out_of_scope and only valid claims are implemented.
- issue-reviewer judges disputed claims from the code without SA's or
  Root's conclusion, and checks every stated criterion is met or
  disputed.
- Root answers each disputed claim on the Issue and records
  disputed_issue_claims; INVALID_OR_CLOSED is split into CLOSED and
  INVALID, and INVALID / NEEDS_DECISION mean no PR, a reply on the
  Issue, and the Issue left open for a person.
- issue_resolution "partial" (needs a dispute) makes create_pr.py require
  "Refs #N" without a closing keyword; "fixes" keeps "Fixes #N".

issue_gate.py reads the Issue's comments only when there are disputes and
checks: source is the Issue or one of its comments, the reply is a later
comment on the Issue and links the source comment, and the entry is
HEAD-bound (evidence-only delta allowed) and reviewer-confirmed. The
closing-keyword matcher is shared with the duplicate-PR screen.

Tests: 20 new cases plus the fixture's 22 base gate tests re-run with an
answered dispute; every new guard mutation-checked. Fixture shapes from
real gh issue view output (#177), and the check was run
read-only against that Issue. make skill-test and TestAgentSkillLayout
pass.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@kjelly kjelly changed the title feat(skills): pr-ready disputes review comments that do not hold feat(skills): pr-ready and issue-to-pr dispute claims that do not hold Oct 30, 2026
kjelly and others added 2 commits October 30, 2026 19:47
`gh pr checks --required` exits 1 with no JSON and "no required checks
reported on the '<branch>' branch" on stderr when nothing is required.
pr_gate.py parsed stdout as JSON and failed, so in kjelly/pilot (no
required checks) the gate could never pass. Treat that message as zero
required checks, as issue-to-pr's pr_ci.py already does; every other
check must still pass, and any other --required failure still fails
closed.

The fake gh in test_cli_mock.py always answered --required with JSON,
which hid this. It now reproduces gh 2.103.3's real output (captured on
#201), with end-to-end cases for a branch without required checks (a
failing non-required check still blocks) and for a failed lookup.
Mutation-checked: dropping the handling, accepting any --required
failure, or a wrong message each fails a test.

Searched every `gh pr checks` caller under .agents/skills:
issue-to-pr/pr_ci.py already handles it; pr-review-merge/gh_runtime.py
does not use --required.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
pr-ready could mark a PR Ready in states that pr-review-merge returns to
Draft, so each run of one undid the other:

- an unresolved P3/P4 thread (pr-ready's unresolved_nonblocking_threads
  exemption; pr-review-merge drafts any unresolved thread);
- a failing check the reviewer confirmed unrelated (pr-ready's
  unrelated_failing_checks; pr-review-merge drafts any fail/cancel, and
  every check in this repository is non-required);
- CHANGES_REQUESTED (pr-ready deliberately does not wait for
  re-approval; pr-review-merge drafted it).

pr-review-merge also never saw pr-ready's disputes, so a fresh reviewer
could repeat a disputed finding and draft the PR again.

Each case is fixed on the side that can act on it. pr-ready requires
every thread resolved (P3/P4 threads are disputed as nonblocking and
resolved, as review-comments.md already says) and rejects the upstream
exemption field. pr-review-merge decides WAITING, never MERGE, for a
reviewer-confirmed unrelated failing check (same evidence shape as
pr-ready) and for CHANGES_REQUESTED. Root passes disputed findings and
their replies to prm-reviewer; a repeated one carries disputed_reply_url
and drafts only with new_evidence, otherwise WAITING for a person. P4
findings are accepted (shared rubric).

tests/test_pr_ready_handoff.py runs both gates over the same GitHub
states (check buckets, thread state, reviewDecision, exemptions,
required or not) and fails if a state pr-ready accepts is DRAFT in
pr-review-merge or MERGE with a failing check. Mutation-checked: putting
back pr-ready's thread exemption, drafting unrelated checks or
CHANGES_REQUESTED, dropping P4, drafting a repeat without new evidence,
accepting short evidence or a reply on another PR, and unconfirmed or
short exemptions each fail a test.

prm-reviewer prompts updated identically on claude/codex/agy and the
.codex copy. issue-to-pr's mark_ready.py has no exemptions and needs no
change; it does not look at threads, which is not a loop (a drafted PR
goes to pr-ready).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@kjelly kjelly changed the title feat(skills): pr-ready and issue-to-pr dispute claims that do not hold feat(skills): dispute claims that do not hold; align pr-ready and pr-review-merge gates Oct 30, 2026
kjelly and others added 2 commits October 30, 2026 20:40
Four vm-target-spec-testing passages told agents to run
`sudo rm -rf /var/lib/libvirt/images/pilot/<name>` after `vm-target down`,
and the clean-room contract's first teardown step removed every VM
"belonging to the runbook" before saying not to touch VMs you did not
create. On this host every clone and session shares the pilot data dir
and that image directory (`vm-target list` here shows other sessions'
VMs, and the directory holds other runs' leftovers and the base images),
so both could delete another session's VM.

The rm -rf was also never needed: `down` already removes the per-target
directory (teardown -> os.RemoveAll(t.Dir)), and nothing is shared
between host and VM, so a Docker bind mount inside the VM lives in the
overlay that down deletes. The "bind-mount survives vm-target down" trap
is rewritten as what it is: state that survives a re-run on the same VM,
cleared by `vm-target reset` or `down`.

- vm-target-spec-testing SKILL.md §8, vm-target-basics.md, container-in-
  vmtesting.md Trap 3, case-study-freeipa.md: drop the rm -rf; down only
  VMs this session created; a directory surviving down is a teardown bug
  to report.
- clean-room contract §4 step 1: record VMs, remove only this run's own
  or user-named disposable ones, use fresh names for the rest.
- TestDown_RemovesState now asserts the directory is gone (fails if
  teardown stops removing it).

Checked on this host: `vm-target up`, a file written inside the VM,
`vm-target down` -> the per-target directory, libvirt domain and state
record are all gone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The skill still taught schema v2 (current is v3,
CurrentRosterSchemaVersion), required a lint line that a v3 tool no
longer prints, and said `pilot roster lint` decrypts an encrypted roster
in memory; lint has no vault option and exits 1 on one. It also said
nothing about what state: absent does, so an agent could delete YAML
entries believing that revokes access.

- schema_version 3; v2 -> v3 only sets the version and adds grants: [];
  the repository example is still v2, so upgrade a copy with
  `pilot roster lint --upgrade`; v3-only access-governance sections are
  written only on request, from their validators, preferably via
  `pilot edit` / `pilot access`; netgroups are v2 and later.
- Lint section uses the real output (v2 notice, v3 ok line, --upgrade's
  backup and lock files) and a private-directory flow for encrypted
  rosters, since a pipe or <(...) fails ("cannot resolve secret source
  path"); `roster migrate --vault-password-file` on a v3 roster validates
  nothing, so it is not a lint.
- New "Revoking access and removing objects": disabled is
  `ipa user-disable`; absent is a permanent `ipa user-del` (no
  --preserve), group-del behind a history marker, host/HBAC/sudo/netgroup
  deletes; deleting a YAML entry disables or deletes nothing; absent
  entries are tombstones that stay; never-applied entries are undone with
  `pilot roster remove-user|remove-group`.

Every lint line and failure above was captured from a pilot binary built
from this branch, on the repository example, its --upgrade copy and an
ansible-vault encrypted copy (throwaway password, deleted afterwards).
State semantics read from freeipa-identity-apply.yml, roster_validate.go
and roster remove-* sources.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@kjelly kjelly changed the title feat(skills): dispute claims that do not hold; align pr-ready and pr-review-merge gates feat(skills): dispute wrong claims, align PR gates, fix VM teardown and roster guidance Oct 30, 2026

kjelly commented Oct 30, 2026

Copy link
Copy Markdown
Owner Author

Automated PR review: Draft required

獨立審查結論:BLOCK,2 個 introduced P2。 已讀取完整 60 檔 diff、受影響實作與 Codex/Claude/agy 角色設定;兩項反例均由獨立 prm-reviewer 與 Root 分別重現。

R1 — [P2] 未解決的 needs_evidence claim 仍可關閉 Issue

位置:issue_gate.py:190–194

gate 只驗證「partial 必須有 dispute」,未驗證反向約束。將現有 DisputedIssueClaimTests fixture 的 dispute 設為 needs_evidence,並保留/設定 issue_resolution: fixes,eval_with_thread() 回傳 errors=[];同時:

create_pr.issue_reference_problem(
    "Fixes #123\n\nTests pass", "OWNER/REPO", 123, "fixes"
)
# None:允許 closing keyword

因此,已明確記錄仍需證據的 claim 可以隨 PR 合併被關閉,違反本 PR 新增的 partial-resolution 契約。這是結構化 evidence 內部矛盾,無須 gate 判斷 claim 真偽即可拒絕。

修正要求: 有尚未解決的 needs_evidence 時拒絕 fixes,要求 partial/Refs。若 out_of_scope 可藉 follow-up 視為已處理,需明確記錄及驗證其關係。加入三個對照:invalid+fixes 通過;needs_evidence+partial 通過;needs_evidence+fixes 拒絕。新增外部 review regression 的實際結果為前兩項通過、第三項 assertion 失敗。

R2 — [P2] no-required-checks fallback 會吞掉有效的 required failure JSON

位置:pr_gate.py:30–31

empty_if 在解析 JSON 前搜尋 stdout 與 stderr。用符合 gh failure 形狀的 subprocess 結果重現:

returncode = 1
stderr = ""
stdout = json.dumps([{
    "name": "Build (no required checks reported)",
    "state": "FAILURE",
    "bucket": "fail"
}])
# gh_checks(..., required=True) -> [],應保留該 required check

把此結果放入既有 good() fixture,full checks 保留同一個 failure,並為其提供 reviewer-confirmed unrelated exemption,evaluate() 回傳 errors=[]。保留 required 清單時,則正確產生 required-check 拒絕。獨立 Reviewer 也以相同輸入執行 base 版本,base 會保留 required failure,確認是本 PR 新增 fallback 引入的回歸。

修正要求: 先解析有效 JSON;僅在預期 nonzero status、stdout 為空、stderr 精確符合無 required checks 診斷時使用 fallback。加入「正常無 required 診斷」正向測試與「check name 含診斷字串但回傳有效 failure JSON」負向測試。新增外部 review regression 的實際結果為前者通過、後者 assertion 失敗。

驗證範圍與限制

  • 最新 HEAD CI run 41057150808:completed/success;6 個 job success,promote pilot-cli:latest skipped。
  • Root 實跑 make skill-test:161 issue-to-pr + 38 pr-ready + 48 pr-review-merge,共 247 通過;py_compile、diff whitespace check 通過。
  • 獨立 Reviewer 另實跑 152 個 gate 測試通過,仍確認上述兩個原測試未覆蓋的反例。額外 review-only regression 共 5 項:3 個正向對照通過、2 個安全性拒絕 assertion 失敗。
  • 審查前沒有 conversation comments、inline threads 或正式 reviews,不存在已被反駁又重複升級的舊 finding。
  • 官方 gh v2.103.3 原始碼確認 Issue JSON comments 會持續分頁;inline thread comments 超過 100 時現行 gate fail closed。未將缺乏 live >100 則案例直接當作 bug。
  • edited comment 語意及 reviewer attestation 真實性仍需 Root/獨立 Reviewer 判斷,未將這項明示 trust boundary 列為額外 blocker。
  • target_validation=not-needed:skill/Python gate 與 Go 測試 assertion 變更,沒有 production target 行為變更。未執行 VM;本機沒有 Go,Go 部分以來源審查及完整 HEAD CI 驗證。
  • 本機 gh 缺乏認證,未使用原生 actor;Root 以已連線 GitHub API 核對現況,並將其事實交給只讀 review_gate.evaluate 得到 DRAFT。未取得的 branch-rule/原生 checks enumeration 保留為未知,未假裝通過,也未進行 merge。

下一步: 修正 R1/R2、將上述反例加入正式回歸測試、重跑相關測試與新 HEAD CI,再安排獨立審查。此次只發表審查證據並轉為 Draft,未修改 PR branch。

This is an automated technical review, not a human approval.

@kjelly
kjelly marked this pull request as draft October 30, 2026 13:56
kjelly and others added 2 commits October 30, 2026 22:38
Trigger: issue_gate.check_evidence only enforced "partial needs a
dispute". With a `needs_evidence` dispute and issue_resolution `fixes`
(or the key absent, which defaults to `fixes`) it returned no errors and
create_pr.issue_reference_problem accepts `Fixes #N` for `fixes`, so
merging closed an Issue that still had a claim waiting for evidence.
references/issue-claims.md "Whole-Issue outcomes" already said such a
PR must be `partial` with `Refs #N`; only the converse was unchecked.

Invariant: a claim that stays open after merge never ships with a
closing keyword.
- any `needs_evidence` entry, or any `out_of_scope` entry without a
  verified follow-up, requires `issue_resolution: partial`; the absent
  key (default `fixes`) is rejected too; any(), not all(); `invalid`
  alone still allows `fixes`.
- an `out_of_scope` entry may instead carry the new optional
  `follow_up_issue_url`: it must fullmatch
  https://github.com/<this repo, case-insensitive>/issues/<M> with M
  different from the Issue, its reply comment (already fetched) must
  contain the URL, and evaluate() confirms with a read-only
  `gh issue view M --json number,url` that an Issue (not a PR) numbered
  M exists, open or closed. Any lookup failure blocks; gh is only called
  when the field is present. The field is rejected on other
  dispositions, where it would be silently ignored.
- mark_ready.py fetches the PR body and re-runs
  issue_reference_problem against the evidence's issue_resolution
  (handed over in the gate's info) before `gh pr ready`: the evidence
  and the body can both change after create_pr.py checked them.

Same-class sites: swept .agents/skills for other readers of
issue_resolution / closing keywords; create_pr.py (creation-time body
check, already enforced by the gate before the push) and mark_ready.py
(the Ready transition) are the only two, pr-ready and pr-review-merge
do not read it.

Fixtures for the follow-up lookup are the real `gh issue view N -R
kjelly/pilot --json number,url` output captured with gh 2.103.3: an
Issue (rc 0), a closed Issue (same shape), a PR number (rc 0, number
and /pull/ URL, so the URL, not the number, tells them apart), a missing
number (rc 1, empty stdout, "GraphQL: Could not resolve to an issue or
pull request ..." on stderr). The tests run the real command() against
a fake gh on PATH and assert the exact argv.

Mutation checks (each in a scratch copy, 21 mutants): removing the
needs_evidence rule, any->all, dropping the fixes default, out_of_scope
never open, skipping the lookup, accepting a PR URL, skipping the
number check, skipping the reply link, the reply link without a word
boundary, any repository, follow-up == this Issue, match instead of
fullmatch, leading zeros, case-sensitive repository, follow-up on other
dispositions, evaluate dropping check_follow_ups or issue_resolution,
and mark_ready skipping the body check, using the wrong resolution or
not fetching the body were all caught. One survivor (the reply link
compared case-sensitively) led to a test with a canonical-case reply;
it is caught now. The new tests also fail against the previous scripts.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Trigger: pr-ready's pr_gate.cmd(empty_if=) and issue-to-pr's
issue_gate.command(empty_if=) (used by pr_ci.py) returned [] whenever
the phrase "no required checks reported" appeared anywhere in
stdout+stderr, before the JSON was parsed. gh prints a failing required
check's JSON on stdout with exit 1, so a check named "Build (no required
checks reported)" was swallowed: required_checks became [], and in
pr-ready a reviewer-confirmed unrelated_failing_checks exemption for the
same name then let evaluate() return no errors (reproduced; with the
required list kept it errors). pr-ready introduced it on this branch;
the issue-to-pr copy predates it and only misreports required_checks
there, since the all-checks query still fails the PR.

Invariant: valid JSON on stdout is always parsed; the fallback to [] is
only for the one failure gh really reports, and nothing else.
- both helpers now fall back only when the exit status is exactly 1,
  stdout is empty or whitespace, and stderr fullmatches
  `no required checks reported on the '<branch>' branch` (case
  sensitive, optional final newline). A substring of other output, a
  different case, exit 0/2/8 with the message, unparseable stdout, or
  the non-required query still raise.
- the constants are compiled patterns now (pr_gate.NO_REQUIRED_CHECKS,
  pr_ci.NO_REQUIRED_CHECKS) instead of a lowercased substring.

Real capture (gh 2.103.3, `gh pr checks 201 -R kjelly/pilot --required
--json name,state,bucket,link`, a repository without required checks):
rc 1, stdout empty, stderr exactly
`no required checks reported on the 'chore/pr-ready-dispute-review-comments' branch\n`.

Same-class sites: swept .agents/skills/*/scripts for substring matching
on command output and for returncode handling. These two helpers are the
only tolerated-failure fallbacks (pr-review-merge's gh_runtime reads
`gh pr checks` without a fallback, create_pr.execute and run_check raise
on any nonzero status).

Tests: both skills (RequiredChecksQueryTests, RequiredChecksQueryTest)
cover the exact message, a failing required check named like the
message (kept, and in pr-ready evaluate() still errors on it with an
unrelated_failing_checks exemption, also end to end through the CLI
mock), JSON with the message on stderr, the message embedded in other
text, other cases, exit 0/2/8, unparseable stdout and the full query.
pr-ready/tests/test_required_checks_parity.py (AGENTS.md 5.7) runs one
matrix through both helpers and requires identical, stated outcomes and
identical patterns. The CLI mock's failing checks now exit 1 like gh.

Mutation checks (each in a scratch copy, 14 mutants): substring instead
of fullmatch, dropping the empty-stdout or the rc == 1 condition,
case-insensitive pattern and falling back for the full query, in both
skills, were all caught by that skill's own tests; four divergences
between the two skills (rc condition, substring match, optional newline,
stdout condition) were caught by the parity test alone. The new tests
also fail against the previous scripts.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@kjelly

kjelly commented Oct 30, 2026

Copy link
Copy Markdown
Owner Author

回覆 #201 (comment)

兩項都成立,已在 74eefe26 修正(PR HEAD)。

R1(P2,introduced)→ fc54eabe

  • issue_gate.py:issue_resolution 為 fixes(含未填時的預設)時,只要有任一 needs_evidence dispute,或任一沒有 follow_up_issue_url 的 out_of_scope dispute,就拒絕,要求 partial/Refs #N。invalid 單獨存在仍可 fixes。
  • follow_up_issue_url(新欄位,只用於 out_of_scope):必須是同 repo 的 /issues/<M>、M 不等於本 Issue、回覆內文要連到它,且 gh issue view M --json number,url 確認是 Issue(PR 編號或查不到都擋);不要求 open。
  • 同類入口:mark_ready.py 在 gh pr ready 前讀 PR body,依 evidence 的 issue_resolution 再檢查一次 closing keyword。
  • 回歸:DisputedIssueClaimTests(invalid+fixes 通過、needs_evidence+partial 通過、needs_evidence+fixes 與未填皆擋、invalid+needs_evidence+fixes 擋、out_of_scope 各情境)、test_create_and_ci.py(push 前 BLOCKED)、test_mark_ready.py(不呼叫 gh pr ready)。

R2 → 74eefe26

  • pr_gate.cmd 與 issue-to-pr 的 issue_gate.command(pr_ci.py 使用;substring 寫法在 base 就已存在)改為:只有 rc 1、stdout 為空、stderr 完整符合 gh 的 no required checks reported on the '<branch>' branch 才當作沒有 required checks;stdout 有合法 JSON 一律先解析,名稱含該字串的失敗 required check 會保留。
  • 回歸:兩個 skill 的 RequiredChecksQuery* 測試,加上 pr-ready/tests/test_required_checks_parity.py 用同一組矩陣跑兩個 helper 並要求結果相同;pr-ready 也驗證 evaluate() 對該 required failure 加 unrelated exemption 仍會拒絕。
  • 嚴重度:SA 認為觸發條件(管理者設定、名稱含該字串的 required check)偏邊角,評為 P3;因為是本 PR 引入且修正便宜,照修。

驗證

  • 獨立 Reviewer(74eefe26):R1、R2 皆確認關閉;新測試放到 e5f499e1 上有 50 個失敗,原因符合預期;自行 17 個 mutant 全數被擋。實作端 35 個 mutant、0 存活。
  • gh issue view(Issue、PR 編號、不存在的編號)與 no-required-checks stderr 的 fixture 都以 gh 2.103.3 重新實測擷取。
  • make skill-test:issue-to-pr 185、pr-ready 51、pr-review-merge 48 全過。新 HEAD 的 CI 進行中。

剩下兩個 P4 不在本 PR 處理(都不會讓 gate 通過),已寫進 PR 說明的 Limits。

@kjelly
kjelly marked this pull request as ready for review October 30, 2026 16:27
kjelly and others added 8 commits October 31, 2026 10:10
The "templated ansible_host over ssh" case in both drift-probe leak tests
set ansible_host to the bare password, a single-label name. On a CI runner
(run 41076444653, attempt 1) ssh got past name resolution for it in two of
the four CLI subtests, so the output said "details withheld" instead of
"Could not resolve hostname (check ansible_host)" and the test failed; a
re-run passed. ssh prints "Could not resolve hostname" for every resolver
error, so the name most likely resolved through the runner's search list
(the probe withholds the raw message, so this is inferred).

Append ".invalid." to the template: .invalid is reserved (RFC 6761) and
the trailing dot stops the search list, so no resolver answers it. The
expanded value still carries the sentinel, and accessgrants' raw-output
check still proves ansible echoes it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
mark_ready.py moved a Draft to Ready without looking at review threads or
the review decision. A PR with an unresolved thread is then returned to
Draft by pr-review-merge, and a CHANGES_REQUESTED review was simply
ignored, although Ready is meant to say nothing is left to do.

- mark_ready reads every page of the PR's review threads (GraphQL, query
  and answers captured from gh 2.103.3: #201 with none,
  cli/cli#13013 with one unresolved of three) and refuses Ready while
  any is unresolved; a lookup that fails or is malformed blocks.
- reviewDecision CHANGES_REQUESTED blocks and points at pr-ready, which
  owns review feedback on a Draft (fix, or dispute with a reply);
  mark_ready does not triage it. "" (no review required),
  REVIEW_REQUIRED and APPROVED do not block.

Two P4s from the #201 review, both already fail-closed:
- a mistyped issue_resolution such as "Partial" is now reported as such
  by create_pr.py, instead of as a missing "Fixes #N";
- a follow_up_issue_url number longer than 10 digits is a format error;
  over 4300 digits int() raised and the gate CLI died with a traceback.

10 mutants of the new checks all fail the tests; an unmutated copy passes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ter edits and candidates

known-gotchas called `sss_cache -E && systemctl restart sssd` the only fix
for a denied first sudo; AGENTS.md §5.9 point 5 makes that a bug symptom.
The entry now records the observed facts (rounds 13/23/31, #95/PR #107,
#114, c45ed55), says the root cause is unknown, and tells the agent to
capture facts, check issues, file or report, and not count the run as
passing evidence. filing-policy and a new rules-table row 31 match it;
the services= note now reflects the per-OS-family setting (dfb58a5).

pilot-edit-wizard told agents to hand-edit the roster. It now lists the
roster editor's screens from current source and allows hand edits only
for what no screen covers, each with its code evidence; the stale
ssh_keys hand-edit and schema_version 1 claims are gone.

SKILL.md gains §0a: formal runs start from a frozen candidate in a clean
isolated checkout (AGENTS.md §1.5, docs/actual-run-evidence.md), and §6
no longer appends verification output to the runbook.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
review_gate.py ignored target_validation, so a PR changing playbooks,
specs or contracts could merge with no actual-run record. The gate now
reads .github/pr-evidence.json at the PR's base SHA (GitHub contents
API, never the head) and checks target_validation with issue-to-pr's
repo_evidence.py, loaded by path; the evidence-only delta comes from
GitHub compare. Missing, malformed, blocked, uncovered or unverifiable
records are WAITING; DRAFT still comes only from the reviewer's P1.
The manifest gains required_paths (playbooks, verification specs,
contracts), for which not-needed is not accepted. GitHub answers in
the tests are read-only gh api captures from kjelly/pilot. Integration
tests lock a per-test PRM_STATE_DIR instead of ~/.cache.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The skill still taught manual up/run/verify, said verify playbooks come
from `pilot spec --generate`, and told agents to paste run output into
runbooks. Rebuild it around `vm-target test` and `vm-target topology test
--ephemeral` (L3 check mode, L6 idempotency), the candidate freeze and
evidence rules (linked to AGENTS.md and docs/actual-run-evidence.md, not
copied), and an AGENTS.md routing table. Every flag was checked against
--help and cmd/pilot/cmd/vm_target*.go.

References: fix the inventory contract (aliases are groups, ControlPath in
ansible_ssh_args), --sandbox is opt-in and needs an image with an ssh
client (pilot-cli:latest), no `--` before the run playbook, resize-disk,
topology name adoption, HA drill section numbers, host-specific subnet,
and evidence/promotion steps. Teardown keeps 549b32e's rules.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
`pilot roster lint` had no vault option: on an encrypted roster it exited 1
with "decrypt it first", and the freeipa-roster-authoring skill told agents
to decrypt into a private directory, lint, and delete it.

Add `--vault-password-file`. Lint now reads the roster once
(inventory.ReadRosterPlaintext) and, for an ansible-vault file, decrypts it
in memory with the same `ansible-vault view` call `pilot roster migrate`
uses. No plaintext file is written. Validation, warnings and the schema
version are computed from those bytes, so the report for an encrypted
roster is the same as for its plaintext. A parse error on a decrypted
roster keeps only the line numbers, because yaml.v3 quotes part of the
offending value. The flag is not read for a plaintext roster, as with
migrate and remove-user/remove-group. `--upgrade` passes the password file
to MigrateRosterFile, which re-encrypts the result and keeps the original
ciphertext as the backup. Without `--upgrade`, lint takes no lock and
writes nothing.

Tests run the real cobra command against rosters encrypted with
ansible-vault (skipped when it is not installed): the same stdout and error
as plaintext for six fixtures, including the repository example; a wrong
password; a missing password file; the flag ignored on a plaintext roster;
no decrypted content in the roster directory, $TMPDIR or
ANSIBLE_LOCAL_TEMP; and --upgrade re-encrypting. Against the previous
lint, all seven new tests fail ("unknown flag: --vault-password-file").

Skill step 5 now uses the flag, with output from a real run against an
encrypted copy of the example roster.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The example roster was still schema v2 and several docs still presented v2
(or v1) as the version to write.

Example roster: schema_version 3 plus an empty `grants: []`, which is
exactly what `pilot roster lint --upgrade` produced on a copy (compared
ignoring comments and whitespace; applied by hand so the comments stay).
MigrateRosterV2ToV3 makes the same two changes when pilot opens a v2
roster, and the apply playbooks treat v2 and v3 the same apart from the
allowed top-level keys, so the effective behaviour does not change. The
header, netgroup and v3.2-hardening comments no longer call the file v2.

freeipa-identity runbook: §3's schema sample and text are now v3, with a
new step 5, `pilot roster lint --vault-password-file`, and its output from
a local run of steps 1, 4 and 5 against a throwaway HOME and vault
password. §4.1 points at it; §0.5 and the change log record it. No target
run: nothing about apply changed.

Verification spec (doc-only, no row change): the auto-upgrade target is v3
and schema_version accepts 1, 2 or 3, matching the apply gate.
minimal-poc-configuration and pilot-trec-verification's
freeipa-identity-prompt reference (its pilot-edit-wizard reference was
already rewritten by the trec-skill commit) stated schema_version 1 where the code writes or accepts 3. Skill step 1
no longer tells agents to upgrade the example.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review of the previous commits (PASS, P3/P4 only):

- required_paths was read only by pr-review-merge. issue-to-pr accepted a
  reasoned not-applicable for a playbook change, so an Issue PR could
  reach Ready through mark_ready.py and then wait at the merge gate.
  repo_evidence.py now validates required_paths in validate_manifest
  (shared: review_gate.py drops its own copy) and refuses not-applicable
  when the change, from the merge base with the base branch to HEAD and
  with both names of a rename, touches one. A change that cannot be listed
  is refused. 5 mutants (no check, renames collapsed, failure read as no
  change, no validation, two-dot diff counting the base branch's own
  commits) each fail a test.
- pr-review-merge SKILL.md's hand-off bullet said a missing actual-run
  record is never a reason to draft, next to the rule that the reviewer
  files a blocking P1 for it. It now says the gate alone waits and the
  reviewer's P1 is the one route to DRAFT; the hand-off test covers the
  no-findings states.
- issue_gate.py: resolution_problem no longer sits between the two
  constants.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@kjelly kjelly changed the title feat(skills): dispute wrong claims, align PR gates, fix VM teardown and roster guidance feat(skills): dispute wrong claims, align PR and evidence gates, refresh VM and roster guidance Oct 31, 2026
@kjelly
kjelly merged commit 4609b6f into main Oct 31, 2026
7 checks passed
@kjelly
kjelly deleted the chore/pr-ready-dispute-review-comments branch October 31, 2026 04:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant