Repository navigation
Description
Found while adopting validate-task.yml in ptr727/aiopurpleair, against hub main at 9d5a0c0 (2.0.606). Three preconditions an adopting repository meets are not stated in "Adopting the Gates", and each one is silent in a different way. Filed together because they are one class: the adoption section describes the caller stub thoroughly and the task's own contract not at all.
1. A bare uv run mypy needs the pyproject to declare a target, and nothing says so
validate-task.yml's "Type check Python step" runs uv run mypy with no path argument. Mypy resolves its target from files, packages or modules in [tool.mypy], and with none of them it does not check the tree, it exits 2:
mypy: error: Missing target module, package, files, or command.
Reproduced on aiopurpleair, whose [tool.mypy] declared strict, python_version and plugins but no files, because its own task passed the target on the command line as uv run mypy src. The lint job fails immediately on adoption.
"Adopting the Pure Functions" does discuss [tool.mypy], at docs/reusable-workflows.md line 430, but only along the interpreter dimension: "mypy and pyright each defaulting their target to the one they run under where the repo's own [tool.mypy] or [tool.pyright] section does not name a version". That sentence is about the Python version, and a reader checking their config against it comes away satisfied while the file target is still undeclared.
The fix in the adopting repository is one line (files = ["src"]), so this is a documentation gap rather than a task defect. What makes it worth filing is the failure mode: an adopter reads a mypy usage message, which reads as a broken task rather than as a missing declaration in their own config.
2. mypy and pyright are mutually exclusive, and only a comment in the task says so
The same step takes mypy wherever [tool.mypy] is present and reaches pyright only in its absence. The task comments this clearly:
Mypy takes precedence where a pyproject declares both sections, matching every repo that runs mypy in CI today. A repo that genuinely wants both enforced adds the second as its own validate hook step.
That is the right design and the right escape hatch. The problem is where it is written. "Adopting the Gates" contains no occurrence of mypy, pyright, or a pointer to the hook for this purpose, so a repository that runs both today and follows the adoption section will drop one without any signal: the job stays green, and the check is simply gone.
aiopurpleair runs both deliberately, mypy strict over the shipped surface and pyright over sources and tests, so adopting without a hook would have stopped type-checking its test suite while reporting success. It now carries a validate hook for pyright, which is exactly what the comment prescribes, but it got there by reading the task rather than the adoption doc.
3. The prose gate does not run on a push-triggered call, and a shipped stub is push-triggered
validate-task.yml's "Check prose step" carries:
if: github.event.pull_request != nullwith the rationale "A publish run's content is already gated by the pull request that introduced it." That holds for a publish run. It does not hold for a caller whose CI trigger is push, and the hub ships one: catalog/snippets/workflows/test-pull-request.yml, the no-build stub, triggers on push: branches: [develop] as well as pull_request, and "Adopting the Gates" describes that shape as intended, since a direct push to develop is the operational model's documented allowance.
On those push runs the prose gate silently does not execute. Meanwhile the rollout section at line 151 describes the lint job as hosting "the fleet doc-lint block, language lint by tree detection, the prose gate, and the repo gate" with no caveat, and the repo gate beside it has no such condition, so the two behave differently for the same caller with nothing saying which.
The visible consequence is that an operational repository's direct-to-develop commits, the ones that never pass through a pull request at all, are the exact commits the prose gate never sees. That may be the intended trade, since a diff-scoped gate needs a base to diff against and a push has no obvious one. If so it is worth a sentence, because as it stands the only way to learn it is to read the if:.
Suggested fix
A short "what the task expects of your repository" block in "Adopting the Gates" covering all three: declare a mypy target, expect one type checker with the hook as the route to a second, and know which gates are conditional on the event that reached them. Items 1 and 2 are documentation only. Item 3 may be documentation or may be a design question about whether the prose gate should resolve a base for a push event.
Environment
- Hub
mainat9d5a0c0, release2.0.606 - Adopting repository:
ptr727/aiopurpleair, apython+pypirelease-model repo running mypy and pyright both - Reproduced item 1 locally against the repository's committed
uv.lock
Activity
Adopt the hub validate task and switch CI to a pull_request trigger
Adopt the hub validate task and switch CI to a pull_request trigger (#…
ptr727 commentedon Sep 10, 2026
A fourth item of the same class, found completing the adoption in
ptr727/aiopurpleair(merged asc9d889e). This one is a defect in the shipped stub rather than a documentation gap, and it affects every repository that adopts the stub as written.The base-branch filter strands a retargeted pull request
Both caller stubs in "Adopting the Gates" filter the trigger by base branch:
Changing a pull request's base fires the
pull_requestactivity typeedited. That type is not in the default set (opened,synchronize,reopened), and neither stub declarestypes:. So no run follows a retarget.The consequence appears when a stacked pull request is retargeted, which is the ordinary way a stack is unwound:
feature/bis opened againstfeature/a. The base is notmainordevelop, so the filter excludes it and no run happens. Correct so far, since no ruleset targets a feature branch and nothing is blocked.feature/amerges intodevelop.feature/bis retargeted ontodevelop. Now a ruleset does apply and requires the aggregator's context, but the retarget firededited, so no run posts it.The pull request sits at "Expected, waiting for status to be reported". It is not permanently stuck, since a later push (
synchronize) or a close and reopen clears it, but nothing in the stub or the docs says so, and the state gives no hint that a no-op push is the remedy.What
aiopurpleairdid, and the tradeoff it makesDropped the filter, so the trigger is a bare
pull_request:. Every pull request then gets its run atopened, and the aggregator's context lands on the head SHA, which a retarget does not change, so the check carries across the retarget with no new run needed.Verified rather than assumed, since the whole fix rests on it: for a
pull_requestevent the check run associates withhead.shaand not withmerge_commit_sha. Confirmed against a live run in this repository, whosehead_shamatched the pull request'shead.shawhile itsmerge_commit_shadiffered, with the aggregator's context listed under the former.The tradeoff is worth stating plainly, and
aiopurpleairrecords it in its ownWORKFLOW.mdrather than leaving it implicit: the context carried across a retarget was earned against the old base, and with a non-strict required-status-checks policy nothing forces revalidation, so the check is green on a merge that was never attempted. That is a widening of the gap a non-strict policy already has rather than a new one, and a push clears it.Options for the fleet
Offered as alternatives rather than a recommendation, since the right answer depends on whether the fleet wants stacked pull requests to be a supported shape at all.
types: [opened, synchronize, reopened, edited]. Keeps the filter's savings, at the cost of a run on every title and body edit, sinceediteddoes not distinguish a base change from either.PhotoCleanercarries the filtered form today, so whichever option is taken reaches at least one more repository.ptr727 commentedon Oct 11, 2026
New evidence for item 3, read at hub
main50b6bc3(2.0.972). The condition is wider than the Check prose step..github/workflows/validate-task.yml:457(Read comments label step),:478(Resolve prose base step), and:492(Check prose step) all carryif: github.event.pull_request != null.push(for examplepushto**as its pull request check, with nopull_requesttrigger) therefore never runs the prose gate in CI, and itscommentslabel is never read.docs/reusable-workflows.mdand thelintjob summary atvalidate-task.yml:7say the lint job "also runs the prose gate" without the event condition.Suggested addition to the fix: state in "Adopting the Gates" that the prose gate needs a
pull_requesttrigger on the caller, or resolve a base for a push event (for example the previous head or the default branch merge base).Found while: adopting validate-task.yml in ptr727/homeassistant-purpleair
🤖 Generated with Claude Code
ptr727 commentedon Oct 29, 2026
Item 1 recurred on 2026-10-29, on the lint-only profile this time. It turned up while PlexCleaner adopted
validate-task.ymlat 2.0.1251 for a lint-only Python directory.When
config_direqualsdir, the type-check step leavestargetsempty (validate-task.ymlnear line 443). So theuvx mypy@latest ...invocation near line 460 runs with no path. A[tool.mypy]with nofileskey then exits 2 with "Missing target module, package, files, or command". The fix on the carrier side wasfiles = ["."].Neither "Adopting the Gates" nor the
python-codestylelint-only profile tells a carrier it owes that key. The hub's ownpyproject.tomlsetsfiles, which is why the hub never sees it. The fix for this issue should name the key in both places, or have the step pass.when no target is configured.ptr727 commentedon Nov 1, 2026
Decision triage split the undecided item out of this issue into #3744, which carries the
decisionlabel. Everything else here stays as ordinary work an agent can do without the maintainer.ptr727 commentedon Nov 1, 2026
Maintainer answer to the split decision #3744 (decision triage, 2026-11-01): Option 1: drop the pull_request base-branch filter in both catalog stubs, the docs/reusable-workflows.md example, and the hub's own test-pull-request.yml, updating tests/test_release_guards.py with them. The prose-gate-on-push sub-question follows #3334's answer.
ptr727 commentedon Nov 1, 2026
Released (2026-11-02): worktree and branch removed, so this claim holds no files. Its fix merged to develop in PR #3796.
Backlog burndown claim (round 2, group R2G3). Branch:
feature/burn-3334. Issues: #3334, #1516, #3487, #1485, #1938.Predicted file set:
.github/workflows/test-pull-request.yml,.github/workflows/validate-task.yml,.github/workflows/build-release-task.ymlcatalog/snippets/workflows/test-pull-request.yml,catalog/snippets/workflows/test-pull-request-smoke.ymldocs/reusable-workflows.md,WORKFLOW.md,spec/audit.py,spec/files.json,scripts/repo_gate.pyspec/project-types.json,spec/fidelity-model.md(added in adjudication)tests/test_audit.py,tests/test_audit_helpers.py,tests/test_release_guards.py,tests/test_repo_gate.py.agents/skills/workflow-ci-contract/and its generated trees (.github/skills/workflow-ci-contract/,.claude-plugin/fleet-skills/skills/workflow-ci-contract/,.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract)Gate the Required Check Name, Latest Flag, Lint Interpreter, and .NET…