Repository navigation
feat(skills): dispute wrong claims, align PR and evidence gates, refresh VM and roster guidance - #201
Conversation
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]>
`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]>
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]>
Automated PR review: Draft required獨立審查結論:BLOCK,2 個 introduced P2。 已讀取完整 60 檔 diff、受影響實作與 Codex/Claude/agy 角色設定;兩項反例均由獨立
R1 — [P2] 未解決的 needs_evidence claim 仍可關閉 Issuegate 只驗證「partial 必須有 dispute」,未驗證反向約束。將現有 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 真偽即可拒絕。 修正要求: 有尚未解決的 R2 — [P2] no-required-checks fallback 會吞掉有效的 required failure JSON
returncode = 1
stderr = ""
stdout = json.dumps([{
"name": "Build (no required checks reported)",
"state": "FAILURE",
"bucket": "fail"
}])
# gh_checks(..., required=True) -> [],應保留該 required check把此結果放入既有 修正要求: 先解析有效 JSON;僅在預期 nonzero status、stdout 為空、stderr 精確符合無 required checks 診斷時使用 fallback。加入「正常無 required 診斷」正向測試與「check name 含診斷字串但回傳有效 failure JSON」負向測試。新增外部 review regression 的實際結果為前者通過、後者 assertion 失敗。 驗證範圍與限制
下一步: 修正 R1/R2、將上述反例加入正式回歸測試、重跑相關測試與新 HEAD CI,再安排獨立審查。此次只發表審查證據並轉為 Draft,未修改 PR branch。 This is an automated technical review, not a human approval. |
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]>
|
兩項都成立,已在 R1(P2,introduced)→
R2 →
驗證
剩下兩個 P4 不在本 PR 處理(都不會讓 gate 通過),已寫進 PR 說明的 Limits。 |
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]>
Summary
Neither
pr-readynorissue-to-prcould say that the thing it was asked to act on was wrong.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.INVALID_OR_CLOSEDwas 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.pyrequiredFixes #Neven when part of the Issue stays open, and the documentedgh issue viewdid 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_evidenceornonblocking(P3/P4). issue-to-pr: each Issue claim (symptom, stated cause, proposed fix, stated acceptance criteria) isvalid,invalid,needs_evidenceorout_of_scope. Onlyvalidclaims 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_CLOSEDis split intoCLOSEDandINVALID;INVALIDandNEEDS_DECISIONmean no PR, a reply on the Issue, and the Issue left open for a person.issue_resolution: "partial"(needs at least one dispute) makescreate_pr.pyrequireRefs #Nwith no closing keyword;fixes(the default) requiresFixes #Nand is refused while a claim stays open: anyneeds_evidencedispute, or anout_of_scopeone without afollow_up_issue_url(same repository, not this Issue, linked in the reply, and confirmed bygh issue viewto be an Issue, open or closed).mark_ready.pyre-checks the PR body againstissue_resolutionbeforegh pr ready. The documentedgh issue viewnow fetchescomments.Role prompts (SA, Coder, Reviewer of both skills) updated identically on claude/codex/agy;
PROJECT-CHANGES.mdof each skill records the local edits.pr-ready gate on repositories without required checks.
gh pr checks --requiredexits 1 with no JSON andno required checks reported on the '<branch>' branchwhen nothing is required, andpr_gate.pyfailed 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'spr_ci.pyhad the same fallback with a substring match (pre-existing on base) and now uses the same rule;pr-ready/tests/test_required_checks_parity.pyruns one matrix through both helpers. Every other check must still pass and any other failure still fails closed. The fakeghin the tests always answered--requiredwith 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:nonblockingand resolved) and rejects the upstreamunresolved_nonblocking_threadsfield.WAITING, neverMERGE, for a check listed inunrelated_failing_checks(same shape as pr-ready's) and forCHANGES_REQUESTED(waiting for re-review); open objections still come back as reviewer findings.disputed_reply_urland drafts only withnew_evidence; otherwiseWAITINGfor a person, as on the pr-ready side.P4findings are accepted (shared rubric).pr-review-merge/tests/test_pr_ready_handoff.pyruns both gates over the same GitHub states and fails if a state pr-ready accepts isDRAFTin pr-review-merge, orMERGEwith a failing check.No
rm -rfin the shared VM image directory. Four vm-target-spec-testing passages said tosudo rm -rf /var/lib/libvirt/images/pilot/<name>afterdown, 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. Therm -rfwas never needed:downremoves 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 survivesdown; the clean-room step removes only this run's own or user-named VMs.TestDown_RemovesStatenow 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 whatstate: disabled/state: absentdo in FreeIPA (absentis a permanentipa user-del), that deleting a YAML entry revokes nothing, thatabsententries are tombstones that stay, andpilot roster remove-user|remove-groupfor 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 sameansible-vault viewcallroster migrateuses; 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;--upgradere-encrypts throughMigrateRosterFile. Without--upgradelint takes no lock and writes nothing.Schema v3 in the example and docs.
playbooks/apply/freeipa-identity.roster.example.yamlgetsschema_version: 3andgrants: [], exactly whatlint --upgradeandMigrateRosterV2ToV3produce, 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'sin [1, 2, 3]), with no checklist row changed;minimal-poc-configuration.mdand a trec reference no longer say the wizard writes v1.issue-to-pr
mark_ready.pyrefuses 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 refusesreviewDecisionCHANGES_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 mistypedissue_resolutionsuch asPartialis reported as such instead of as a missingFixes #N, and a follow-up Issue number over 10 digits is a format error instead of anint()traceback.pr-review-merge checks the mandated actual-run record before MERGE. The gate reads
.github/pr-evidence.jsonat the PR's base SHA (a PR cannot weaken its own requirement) andtarget_validation:blocked, a missing or malformed record, arequiredrecord whose artifact is not committed at HEAD, outsideartifact_globor not naming the tested revision, or a tested revision that is not HEAD or an ancestor with an evidence-only delta, all make itWAITING, neverMERGE(DRAFTstill needs the reviewer's P1 finding). The manifest gainsrequired_paths(playbooks/**,docs/verification/**,contracts/**), for which pr-review-merge refusesnot-neededand issue-to-pr's gate refusesnot-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'srepo_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-testPRM_STATE_DIR: they used~/.cache/pr-review-merge, so two concurrentmake skill-testruns 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) andtopology test, with--ephemeralfor fresh hosts and cross-playbook ordering; manualup/run/verifyis for debugging. Verify playbooks are no longer described asspec --generateoutput, and output goes to a candidate-bounddocs/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--helpand the code:--sandboxis notrun's default andtesthas none;runpasses--through as the playbook path; a container image without an ssh client;resize-diskinstead 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 afterc45ed559added 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 usespilot 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
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 wasWAITING, notDRAFT.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 onerequired_evidenceentry isWAITING.required_pathscovers:playbooks/apply/freeipa-identity.roster.example.yaml(data, not a playbook) anddocs/verification/freeipa-identity.md(prose, no row). The base manifest has norequired_paths, so the gates take thenot-applicablebelow; under the new rule such a change needs a run record or a person's decision.mark_ready.pychecks 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-completenow attests they are answered.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 checkvm-target list); AGENTS.md §1.3/§3 citeTestRegression_SpecAndInventoryAgree, which does not exist; trec references still cite deletedtui_multiselect.go/tui_confirm.go/tui_textinput.go;freeipa-identity-apply.yml:89andtopology-checkmode-test.sh:41comments still mention schema 1/2;minimal-poc-architecture.mdstill calls the SSSD flush the fix (that runbook has its own update skill).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_threadrelies ongh issue view --json commentsreturning 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 fullgo testofcmd/pilot/cmd,internal/inventoryandinternal/accessgrantspass locally;spec --lintoffreeipa-identity.md0 findings; the identity spec regressions andTestSpecPlaybookTagAlignmentpass; the example roster lintsok: 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,$TMPDIRorANSIBLE_LOCAL_TEMP,--upgradere-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 astested_revision.HOSTALIASESand showed the old tests fail and the new ones pass; ranunresolved_threads,file_atandcompareread-only against live GitHub; and its own mutants ofmark_readyand the gate were caught (two survivors are redundant with other checks). Its two P3s are fixed in e99c127: issue-to-pr now refusesnot-applicableforrequired_paths(5 mutants, each caught), and the pr-review-merge hand-off bullet no longer contradicts the reviewer's P1 for a missing record.gh issue view(Issue, PR number, missing number) and the no-required-checks stderr fixtures were re-captured from gh 2.103.3.create_pr.pymutation-checked: removing each one makes a test fail. For the hand-off: putting back pr-ready's thread exemption, drafting unrelated checks orCHANGES_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.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 fakeghoutput is copied from that capture. Searched everygh pr checkscaller in.agents/skills:pr_ci.pyalready handled it, andpr-review-mergedoes not use--required.vm-target up, a file written inside the VM,vm-target downon this host: the per-target directory, libvirt domain and state record were all gone.TestDown_RemovesStatefails if teardown stops removing the directory.--upgradecopy and an ansible-vault encrypted copy (throwaway password, deleted). State semantics read fromfreeipa-identity-apply.yml,roster_validate.goandroster remove-*.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 --checkclean.actual-runevidence (.github/pr-evidence.json): not applicable. The Go change isroster 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