Repository navigation
Description
A carried-content review pass over the canonical unit .agents/skills/merge-and-release/SKILL.md > The Procedure, run while retiring the publish-on-merge release trigger (#1467), read the unit whole and raised 14 findings, and three later passes over the same unit raised 19 more. None of the 33 is about that change, so all of them are pre-existing under local-strict-review "Disposing of Findings" and blocked nothing. Filing them here so they survive the session.
Four passes read this unit whole, each at a different digest as the one sentence the change touched was rewritten and then cut back. The findings below are the union, deduplicated, and each was verified against the tree or the live fleet by the pass that raised it. What the count implies about the unit is at the end.
Each was verified against the tree at cf7be2b (jq 1.8.2, git 2.47.3, gh 2.100.0). Grouped by what following the step literally does.
Loses a failure instead of reporting it
-
Step 5's release-trigger command exits 0 with empty output when its first stage fails, which is the outcome the same sentence forbids. The sentence requires "on a non-1 count exit non-zero rather than returning empty with success", but the guard lives inside
jq, so agit show origin/main:registry/repos.jsonfailure (an unfetchedorigin/main, a checkout with noregistry/, a differently named default branch) writes to stderr and handsjqempty stdin. Verified:git show origin/main:registry/no-such-file.jsonpiped into that same filter prints nothing and exits 0. The unit prescribes noset -o pipefailand no separate capture of thegit showresult. -
Step 5's
//fallback resolves tonullwith exit 0 when the registry declares no default, and the step then routes that into a dispatch.($m[0].releaseTrigger // .defaults.releaseTrigger)followed by "Two cases,noneversus anything else."registry/repos.schema.jsondoes not requiredefaults, andspec/validate.py:559-563errors only on an invalid value, so an absentdefaults.releaseTriggeris valid. Three entries (DiskSpeedTest,GEM-Echo-Server,GoogleTo1Password) carry noreleaseTriggerof their own and depend entirely on that fallback. Verified: with the key removed the filter printsnulland exits 0, andnullis notnone, so it falls into "anything else" and the procedure dispatches a release for a repository whose trigger could not be resolved.
Destroys work
-
Step 8's worktree sweep has no test for the property it says bounds it. It says "Never apply this sweep to
developormainthemselves, only to feature branches a drive-pr loop created", but the qualifying checks given are merged-with-baseRefNamedevelop, matching tips, and not beingdevelop/main. Nothing distinguishes a drive-pr branch from any other merged-to-developfeature branch, so a branch and worktree belonging to a different live task, merged but deliberately kept, passes every check and isgit worktree removed andgit branch -D'd. -
A bracketed placeholder reads as optional, and on step 3 it silently succeeds when omitted.
gh pr merge [number] --merge. Every other placeholder in the unit is angle-bracketed, and square brackets are the CLI convention for optional.gh pr mergewith no number resolves the current branch's PR, so the reading the brackets invite merges a different PR rather than erroring. (pr_review.py'snumberis a required positional, so step 2 fails loudly instead.) -
Step 7 does not say which checkout
git checkout mainruns in. "In the hub, when the chosen scope includes a release, bring this checkout to the merged content." "This checkout" is not resolved against step 5's "the hub checkout used for this procedure", and the session's own working tree is a third candidate. Following it in the session's tree switches that tree off its branch, and the preflight in the same step is scoped to.agents/skills/and.claude-plugin/only, so it does not catch that.
Cannot be followed as written
-
Step 6 matches on a value no prior step tells you to capture. "matched by
headShaagainst the dispatched ref's tip ... and bycreatedAtagainst the dispatch time". Step 5 issuesgh workflow runand never says to record the dispatch time, so thecreatedAthalf cannot be evaluated by a reader following the steps in order. TheheadShahalf is captured, in step 4. -
Step 6's polling bound is unspecified while the wait immediately after it is exact. "Poll the list itself, within a bounded interval" and "Report and stop rather than guessing once the interval elapses", in a paragraph that then specifies 2700 seconds for the subsequent watch. Incomplete rather than wrong.
-
Step 6 names no bounded-wait mechanism for two hosts it says must be covered. "or the equivalent bounded-wait mechanism enforcing the same bound on a host without it (macOS without coreutils, native Windows)". The instruction cannot be followed literally on either named host.
-
Step 2 requires a hub checkout and never says how to get one. "From a hub checkout,
scripts/is not carried into downstream repos". Steps 5 and 7 depend on that checkout too. A reader carrying this skill in a downstream repository, which is the case the clause exists for, gets the constraint and no way to satisfy it. -
Step 2 invokes a script bare where step 7 invokes one through
python3.scripts/pr_review.py status ...againstpython3 scripts/skills_install.py --report. Both files carry#!/usr/bin/env python3and mode 0755, so the bare form works on Linux and macOS but not on native Windows, which step 6 of this same unit names as a host the procedure must run on. -
Step 8's first bullet gives no commands and no branch for its own absent case. "The promotion PR's own worktree: fetch and prune, remove the worktree, then fast-forward the base clone to
develop." It is the only step prescribing actions with no command form, and it presumes a promotion worktree exists, where step 1 says "stop and report if none is open" for its analogous case.
States something false about the tree
-
Step 7's success criterion for the Skills refresh does not exist in the tool's output. "confirm
--reportnow reads current".scripts/skills_install.py's report path prints a JSON object ofstamp,currentCommitandstale, and returns1 if stale else 0. It never emits the word "current". Relatedly, the unit orders--reportbefore the install without noting that the first call is expected to exit 1, which a reader treating non-zero as failure stops on. -
Step 7's stated reason for
checkoutoverswitchis false against the tree. "checkoutrather thanswitchsince the fleet's owngitfloor is undeclared".docs/host-setup.md:19declaresgit's floor in the host-tool table, asnone. The conclusion survives, the premise does not. -
Step 8's rationale for the
headRepositorycheck describes somethinggh pr listcannot return. "a same-owner PR against an identically named branch in a different repository must never pass this check either". Agh pr listscoped to one repository returns only pull requests in that repository, so a different repository's PR can never appear regardless of the check. What the field actually rejects is a fork-head (cross-repository) PR. The check is worth keeping, the stated reason misleads about what it defends against.
Raised by the two later passes over the same unit
Two further passes ran over the unit after the publish-on-merge sentence was rewritten, and each read the whole text again. Four more pre-existing findings came out of them.
-
The Skills preflight's pathspecs are cwd-relative, so it passes vacuously from any subdirectory. "First assert
git status --porcelain --untracked-files=all --ignored -- .agents/skills/ .claude-plugin/is empty". The unit never says to run it at the checkout root or withgit -C <root>. Verified: run fromdocs/, the command prints nothing and exits 0 however dirty those two directories are, so a stray file rides into the install, which is the exact failure the same sentence citesskills_install.pyas guarding against. -
Step 7 and step 8 contradict each other when the target repository is the hub. Step 7 does
git checkout main,git merge --ff-only origin/main, and verifies the install stamp against that checkout, while step 8's first bullet fast-forwards "the base clone" todevelop. Those are the same clone in the hub, so step 8 moves it off themaintip whose stamp step 7 just verified, and the unit never says the two may be the same checkout. -
Step 6's final reporting instruction cannot be followed with the command the step supplies. "Report a timeout separately from a completed run's own conclusion, the tag or version it produced." The only command given is
gh run watch <run-id> --exit-status, which reports status and conclusion and nothing about a tag or version. -
--ref developis stated unconditionally and at least one fleet publisher refuses it by design. KiCadLibrary'ssetupjob fails fast when the event is aworkflow_dispatchand the ref is not the default branch, emitting "Dispatch publish-release from the default branch ...; the matrix builds both branches", and its dispatch path publishes both branches regardless of ref. A--ref developdispatch there produces a failed run rather than a prerelease. -
Step 6's mid-dispatch-ref-change report fires on the ordinary case. "A run whose
headShadoes not match the expected tip at all, rather than simply being absent, means the dispatched ref moved between step 4's confirmation and the dispatch itself, report that distinctly."gh run listreturns history at a default limit of 20, so the candidate list is normally full of older dispatch runs at other SHAs. Verified live: that exact query returned three runs with three differentheadShavalues from three different days. Every poll before the new run registers therefore has the shape "list non-empty, nothing matches", which this sentence says to report as a ref change, during the very window the preceding sentence tells the reader to keep polling through. -
Step 6's
developcorrelation value has no source and no moment. "ordevelop's current tip for a prerelease". Themaincase is pinned to a value step 4 confirmed.gh workflow run --ref developresolves the ref server-side, while a reader's naturalgit rev-parse developreads a local clone that can lagorigin/develop, and the stale value then yields a non-matchingheadShaand, per finding 19, a false ref-change report. -
The Skills preflight is scoped to two paths while the operations it guards are whole-tree. It asserts
.agents/skills/and.claude-plugin/are clean and then runsgit checkout mainandgit merge --ff-only origin/main. A modified tracked file anywhere else in the hub checkout passes the preflight and then either rides across the branch switch or aborts the checkout. The step names only two refusal modes on top of "what the preflight already ruled out", so the reader is told the preflight ruled out what would stop this step when it did not, and carrying another task's uncommitted work across branches is what the boundaries law exists to prevent. -
Step 5's registry query never defines its
<repo-name>placeholder, and it is not the spelling every other command in the step uses. It must be the registry's exact case-sensitive.name, where the surrounding commands take theowner/repoform thatghaccepts in several spellings. A reader substituting the value that works forgh, or the wrong case, gets zero matches, the deliberateerror(...)path, and a halted release step for a repository that is fine. The same step's "read this repo'sreleaseTrigger" compounds it, since the step runs in the hub checkout against a possibly different target repository, so "this repo" has two readings. -
The quoting-safety rule mislocates the arguments it binds. "Every branch or worktree-path placeholder below is the real value, substituted as its own quoted argument." The placeholders that rule must govern include the branch in
gh pr list --headand in thegit ls-remote refs/heads/guidance, both of which appear above that sentence in the same bullet. It is the one place the direction word matters. -
Step 8's exactly-one guard aborts on the case the same bullet calls normal. The
gh pr list --headquery errors unless exactly one merged PR matches, three lines before "a different merged pull request can share the same head branch name against a different base, and that is never this sweep's target". The command carries no--basefilter, so thebaseRefNamecheck it prescribes is unreachable whenever it would matter, and a branch that did finish againstdevelopcan never be cleaned once any second merged PR shares its head name. -
The
--exit-code2 diagnosis is backwards for the only cleanup path the unit defines. It reads "most likely a prior cleanup attempt got interrupted after the remote delete but before the local one", but the procedure deletes the local branch first and the remote last, so an interrupted run leaves the remote present and the local gone. -
Two tip-match preconditions state no disposition on failure. The remote and local tips "must match that exact pull request's
headRefOid" before each delete, and the unit never says whether a mismatch stops the procedure, stops the sweep, or skips that branch, in a unit that spells out "stop and report rather than guessing" for every other inconclusive case. -
Step 4 confirms a merge with no command, no source, and no fetch. "Confirm the merge landed,
mergedAtset,main's tip matching the merge commit." The firstgit fetch origin mainappears in step 5 and only when the scope includes a release, so read against an unfetchedmainthis confirmation fails on a merge that did land. Step 3'sgh pr mergeoutput is never captured, so the merge commit's SHA has no source either. -
Step 1's selection is the one the unit does not legislate. "Identify the open develop -> main promotion PR for this repo, stop and report if none is open" gives no command, while steps 2 and 3 immediately consume a PR number, and the unit elsewhere legislates exactly-one selection and fail-loud-on-a-non-1-count twice over.
-
Step 6 carries no scope guard, so a merge-only run polls for a dispatch nobody made. Steps 5 and 7 both gate on "when the chosen scope includes a release" and step 6 does not, so following the unit literally under a merge-only scope ends in step 6's bounded-interval "Report and stop" as though a release had failed.
-
Identical "stop and report" wording covers two different outcomes. Step 2's stop must end the procedure, while step 8 says steps 5 through 7's stops do not, listing "no release configured, a dispatch failure, an ambiguous run match, a timeout, a failed run, or a hub Skills refresh" as all still reaching cleanup. Step 7's own preflight stop is absent from that enumeration, so whether it ends the procedure is unresolved.
-
Step 6's concurrent-run warning names a collision its own query excludes. "A concurrent run of a different event on the same branch must never be mistaken for this one" guards a query that already carries
--event workflow_dispatch. The collision that actually produces more than one candidate is a second dispatch run on the same branch, which the sentence does not name. -
The
--end-of-options rule is applied everywhere except one command that supports it. The unit uses--ongit ls-remote,git branch -D, andgit push origin --delete, states an explicit exception forgit rev-parse, and then prescribesgit worktree remove "<worktree-path>"with neither, thoughgit worktree remove --is supported. The unit's own justification for the surrounding quoting rule is exactly the case quoting does not cover and--does. -
Step 8's sweep and step 4's tip check run against a checkout the unit never establishes. The unit establishes only "a hub checkout" in step 2, and step 8's second bullet then runs
git worktree list,git branch -vv,git ls-remote,git rev-parse --verify,git branch -D, andgit push origin --deleteagainst a clone of the target repository. For any target other than the hub,originin the established checkout is the hub, so the sweep reads, and would delete, against the wrong remote.
What the count suggests
Four passes read this unit whole at four digests and raised 33 distinct findings, none of them introduced by the change that occasioned the passes, and each pass found roughly ten the previous one had not. That distribution is the argument for restructuring the procedure rather than landing 33 point fixes on it: the failures cluster into a few classes (a selection the unit does not legislate, a precondition with no stated disposition, a checkout with no antecedent, and a rationale sentence that has drifted from the command beside it), and fixing the classes is what stops the next pass finding ten more.
Verified clean in the same pass
git ls-remote --heads --exit-code, git rev-parse -- resolving no SHA yet exiting 0, the gh pr list single-match jq guard, --timeout 2700 in scripts/pr_review.py, the watched list and copy mechanics in scripts/skills_install.py, the plain-porcelain-is-silent-on-ignored-paths claim, git status --porcelain --untracked-files=all --ignored over two paths, the releaseTrigger enum, the human-merge-never-auto-publishes gate in publish-plan-task.yml, delete_branch_on_merge: false in repo-config/settings.json, docs/host-setup.md "Fleet Skills Install", publish-release.yml's dispatch trigger and ref assertion, gh run list's flags and JSON fields, gh run watch --exit-status, and git push accepting --.
Activity
ptr727 commentedon Sep 9, 2026
Duplicate of #1347, which is this unit's tracker and was already the right home for these findings. Its body says so directly: "A new
pre-existingfinding on this unit gathers here perpr-review-conduct's Merge Gate, numbering continuing this list." Filing them as a standalone bundle was my error, and it is the shape #1315's roll-up exists to stop producing.All 33 are now on #1347, 31 of them as lines 10-40. Two were dropped as already tracked rather than re-listed: the
//fallback resolving tonullis its lines 5 and 8, and the unspecified polling bound is its lines 3, 6 and 7. That second defect now carries three lines and a fourth confirmation, which is its own argument for settling the unit.#1347 has also been retitled to the real unit key.
canonical_review.py listnames seven units in that file and the bare filename is not one of them, while all nine of the tracker's original lines are Procedure findings, so… SKILL.md > The Procedureis what it has been tracking all along.One line here that four passes missed and #1347 already had:
headRefOidis captured before merging and never passed togh pr merge, so a branch changed after the Merge Gate check can merge at a head nobody reviewed.