Repository navigation
Conversation
Subfleet build jobs keep their state in .job/, and #704 and #699 committed .job/HANDOFF.md and .job/PR_BODY.md on 23 October 2026. The new Agent notes workflow runs .github/check-agent-notes.sh on every pull request, with no path filter, and fails when the merged tree holds a .job/, .job-git/ or .diag/ folder at any depth, or HANDOFF.md, handoffs.jsonl, PR_BODY.md, PR-BODY.md, REPORT.md or PROGRESS.md at the root, in any capitalisation. It mirrors policyengine-uk's root-note guard. .gitignore now ignores the three folders. Fixes #712 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… check must reject Reverted in the next commit; the PR is squash-merged, so neither reaches main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Agent notes check rejected it (run 40000640876). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of #713 (GPT-6.1 Sol) found three gaps in the pull_request version. A "[skip ci]" head commit skipped it. Retargeting a stacked pull request to main did not re-run it. And a path holding the legacy "##[" command syntax could stop the runner's commands and swallow the error annotation. The workflow now runs main's guard on pull_request_target, which skip instructions do not affect and which also runs on conflicted pull requests. It fetches the head commit as data and checks that commit's tree through a temporary index, so the base, and so a retarget, cannot change the result. A pull request cannot turn the guard off by editing it. The script prints the paths between stop-commands and a random resume token, and percent-encodes the annotation data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
that referenced
this pull request
Oct 23, 2026
Round 2 of the review of #713 (GPT-6.1 Sol) found that GitHub blocks pull_request_target in public repositories without an Actions event policy from 2 December 2026, and this repository has none. Its pull request workflows also never use pull_request_target (docs/release-bundles.md). The guard goes back to pull_request but keeps checking the head commit's tree, fetched by SHA, so a retarget still cannot change the result. A "[skip ci]" head commit or a direct push can still get past a pull request run, so the workflow also checks main on every push and on a daily schedule, which skip instructions do not affect. The stop-commands hardening stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 2 of the review of #713 (GPT-6.1 Sol) found that GitHub blocks pull_request_target in public repositories without an Actions event policy from 2 December 2026, and this repository has none. Its pull request workflows also never use pull_request_target (docs/release-bundles.md). The guard goes back to pull_request but keeps checking the head commit's tree, fetched by SHA, so a retarget still cannot change the result. A skip instruction in a head commit's message, or a direct push, can still get past a pull request run, so the workflow also checks main on every push and on a daily schedule, which skip instructions do not affect. The stop-commands hardening stays. (The previous head's message quoted a skip instruction while describing this gap, and GitHub skipped every pull request workflow on it.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
force-pushed
the
ci/agent-notes-guard
branch
from
October 23, 2026 19:56
33b76b7 to
693d012
Compare
… check must reject Reverted in the next commit; the PR is squash-merged, so neither reaches main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Agent notes check rejected it (run 40015409561). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rget Round 3 of the review of #713 (GPT-6.1 Sol) found two gaps. A pull request could replace the script that judges it. The workflow ran the checked-out copy, so a head that added .job/HANDOFF.md and a pass-always script passed. On a pull request the job now fetches the default branch with the head, and runs the default branch's copy of the script. Only while the default branch has none, on the pull request that adds it, does the checked-out copy run. Without the default branch the step fails rather than fall back. A stacked pull request whose old base had no guard got its first run only on its next push. Retargeting emits "edited", which the workflow now handles. Every edit runs the full check on the head, so a title edit cannot replace a failure with a skipped run. The docs and changelog now say that a skip instruction can skip push runs as well as pull request runs; only the daily schedule cannot be skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 4 of the review of #713 (GPT-6.1 Sol) approved, with one nit: GitHub can delay or drop a scheduled run, so the daily check finds a note that reached main by the next scheduled run, if the note is still there. The concurrency comment now says that an edit, as well as a push, supersedes a run in progress. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #712
Draft until CI and the independent review pass.
What and why
Subfleet build jobs keep their state in a
.job/folder at the root of their worktree:HANDOFF.md,PR_BODY.md,env, aHEADfile, git bundles and sometimes a wholerepo.git. On 23 October 2026, #704 committed.job/HANDOFF.md, and #699 committed.job/HANDOFF.mdand.job/PR_BODY.md. Both were removed by hand. Nothing in CI would have stopped them.pr_code_changes.yamlhas apaths:filter, so a pull request that adds only such files runs no workflow at all.This PR adds an Agent notes workflow (
.github/workflows/pr_agent_notes.yaml). It runs.github/check-agent-notes.shon every pull request and fails when the PR's head commit contains:.job/,.job-git/or.diag/folder at any depth, or a file of one of those names..job-git/is a Subfleet job's git directory. A local clone of policyengine-uk has 674.job-git/files and 8.diag/files in its branch history, none ever on main.HANDOFF.md,handoffs.jsonl,PR_BODY.md,PR-BODY.md,REPORT.mdorPROGRESS.md, in any ASCII capitalisation, sopr-body.mdandpr_body.mdcount too. Four of these (REPORT.md,PROGRESS.md,HANDOFF.md,handoffs.jsonl) are policyengine-uk's list.It also ignores the three folders in
.gitignore, sogit add -Aleaves them out, and documents the check indocs/engineering/skills/github-prs.md.Design
policyengine-uk's "No agent working notes at the repository root" step (
pr_code_changes.yamlon its main) is the model, and its pieces carry over:git ls-fileswith:(top,icase)pathspecs, plus:(glob,icase)**/for the folders at any depth.shell: bashadds pipefail, and the script also setsset -euo pipefail, so a git failure fails the step instead of passing it. A failure is reported with::error::andexit 1.It departs from policyengine-uk's guard where pe.py differs, or where review found a gap:
editedhandled. policyengine-uk's guard is a step in Lint, which runs on every pull request there. pe.py's code workflow has a path filter, so the guard needs its own workflow.types: [opened, synchronize, reopened, edited]: retargeting emitsedited, so a stacked PR whose old base had no guard is checked when it is retargeted to main. Every edit runs the full check on the head, with no condition, so a title edit cannot replace a failure with a skipped run..github/check-agent-notes.shinto$RUNNER_TEMPand runs that copy, so a PR cannot pass by editing the script. Round 3 reproduced a false pass with a pass-always script. Only while the default branch has no guard, on this PR, does the checked-out copy run. A missing default-branch ref fails the step rather than falling back. Push, schedule and dispatch runs check out the commit they check. A PR that edits the workflow file itself is out of reach withoutpull_request_targetor a required-workflow setting; that is visible in review.github.event.pull_request.head.shaby exact SHA. The script reads<commit>^{tree}into a temporaryGIT_INDEX_FILEand lists it with the same pathspecs. The result does not depend on the base, so retargeting a stacked pull request cannot change it. Main has never had any of these paths (git log origin/mainfor them is empty), so a PR based on main fails exactly when it adds one.pull_requestandpushruns. GitHub's docs: "Skip instructions only apply to thepushandpull_requestevents." A merge that carries such a commit to main can skip the push run too. This PR showed it by accident: two of its own head commits quoted the instruction while describing this gap, and no pull request workflow ran on them. No commit message can skip the dailyschedulerun, so a note that reaches main that way is found by the next scheduled run, if it is still there. GitHub can delay or drop a scheduled run. Thepushrun catches an ordinary direct push at once. GitHub disables schedules in public repositories after 60 days without activity; this one is active.pull_request, notpull_request_target. Round 2 usedpull_request_target. That made skip instructions powerless, ran on conflicted PRs, and ran main's copy of the guard. Two things ruled it out. GitHub blockspull_request_targetin public repositories without an Actions event policy from 2 December 2026, and neither this repository nor the org has one. And pe.py's PR verification never uses it (docs/release-bundles.md). Allowing it is a settings decision (queued as d1933, with the five other PolicyEngine workflows that rely on it).::name::at the start of a line, and the legacy##[name]anywhere in a line. So the paths are printed between::stop-commands::<random 32-hex token>and::<token>::(the runner masks the token in the log), and%is percent-encoded in the annotation data. The log lists every path; the annotation gives the count and the first path. This handles a trackedrepo.git..github/check-changelog.sh, so pytest can drive it. It runs under the macOS bash 3.2 as well as Ubuntu's bash 5.permissions: contents: read, no secrets,ubuntu-latest. Let CI test and check jobs use another runner pool, set by repository variables #706's runner-variable test (invariant 2: every other job keepsubuntu-latest) still holds.Invariants
pull_request_target.Test plan
tests/test_check_agent_notes.py, 55 tests:.job/HANDOFF.mdfails, and a normal change passes;docs/HANDOFF.md,.jobs/,x.job/,my.job,REPORTS.mdand others);repo.gitis reported in full;##[stop-commands]…,##[add-mask]…,::warning::…, an indented::error::…) and newline names, run through an emulation of the runner's parsing written fromactions/runnersource (ActionCommand.csTryParseV2/TryParse,ActionCommandManager.csTryProcessCommand);%in the annotation;HEAD, and the.gitignorerules;.job/HANDOFF.mdstill fails. Before main has a guard, the checked-out copy runs. A missing default-branch ref fails closed. Push, schedule and dispatch events check their own commit;Mutation check. Fourteen mutants each fail the suite:
.diag;.jobmatched at the root only;PR-BODY.mddropped;set -eand pipefail removed;%encoding removed;editeddropped;The guard's steps, simulated against the head of every open PR. A fresh
git clone --depth=1of main, thengit fetch --no-tags --depth=1 origin <head sha>and the script on that SHA. Only Add behavior input binding contract #511 and Build and verify isolated wrapper release candidates #520 fail (bothPROGRESS.md); the other 27, Allow a UK local-area release on an accepted model mismatch (hold: needs María's routing call) #699, Support bundle-pinned local data release manifests #704 and this PR among them, pass.actionlint 1.7.12onpr_agent_notes.yaml: no findings. On all workflows, its one finding is the existingactions/configure-pages@v3inpush.yaml, which this PR does not touch.shellcheckon the script, andruff format --check .andruff check .: clean.This version on GitHub:
.job/HANDOFF.md(run 40015409561), which the next commit reverts. Its log shows::stop-commands::***(the token is masked), thenAgent or job note: .job/HANDOFF.md, then the annotation "1 agent or job note path(s) in the tree, starting with .job/HANDOFF.md". The.gitignoremade the demonstration needgit add -f.The round-1 version did the same (pass, fail). The PR is squash-merged, so no demonstration commit reaches main.
CI on
af2c21024: Agent notes (run 40015473244) and Docs passed. Code changes was superseded by the next push.CI on the head
8656ee484.After merging, with a squash message free of skip instructions (the repository builds squash messages from commit messages, and 0fe7a2d's quotes one):
.job/HANDOFF.mdand replaces the script with a pass-always one. The check must fail, which shows main's copy runs. Then close the PR and delete its branch.Review
GPT-6.1 Sol (
subfleet run --task review --tier hard):Round 1 (
pull_request, merge tree): REQUEST_CHANGES.##[stop-commands]tokensuppressed the error annotation.Fixed: the check now reads the head tree, and output goes through stop-commands. Finding 2 is narrowed by the push and schedule runs on main.
Round 2 (
pull_request_target): REQUEST_CHANGES. It found no bypass, and reproduced the retarget and stop-commands fixes, including with a compiled copy of the runner's parser. The one finding was the 2 Decemberpull_request_targetblock. Fixed by returning topull_requestas above.Round 3 (
pull_request, head tree, push and schedule on main): REQUEST_CHANGES. It found no path-matching or output bypass, and verified the three GitHub runs. Its findings:editedwas not handled.Fixed in 8656ee4: the job runs the default branch's guard, handles
edited, and the wording is corrected.Round 4 (8656ee4): APPROVE. No blocker or should-fix. It reproduced the closure of both round-3 findings in scratch repositories: main's copy runs over a PR's pass-always script, and an old stacked head retargeted to main is checked. It verified the shallow two-refspec fetch, fork-object fetch and concurrency, and it caught three workflow-step mutations. One nit: "found within a day" was too strong, because GitHub can delay or drop scheduled runs. The wording is fixed in the next commit, along with the concurrency comment it flagged.
Sweep of open pull requests
At 2026-10-23 18:00 UTC, the changed-file lists of all 28 open PRs (
gh api repos/PolicyEngine/policyengine.py/pulls/N/files) were matched against every path this guard rejects. The search also flagged any root-level.md,.jsonlor.txtfile and any path naming a hand-off, PR body, progress note, report or review. Shortly after 19:00 UTC the guard itself was run on every open PR's head commit, as in the test plan.feat/be-behavior-input-contract→ mainPROGRESS.mdmax/wrapper-release-candidates-20260914→max/spm-canonical-wrapper-release-20260910PROGRESS.md, which its stacked base tracksNo open PR has a
.job/,.job-git/or.diag/path, or a hand-off or PR-body file. #699 and #704 are clean at their current heads (c669bbd98,463c3a953), though earlier commits in their branches add.job/HANDOFF.mdand.job/PR_BODY.md. A squash merge keeps those commits off main. The other root-level matches are edits toREADME.md(#581, #596, #709, #710). Nothing was pushed to any of these branches.Not in this PR
.job/,.job-git/or.diag/, and nothing checks its main on a schedule. That is a separate change to policyengine-uk, after Budget week.gh pr checksmust pass) already block on it. If it were required, a skipped run would stay pending and block the merge (GitHub's skip docs). That needs a ruleset that lets the release bot's version commits through, which is a settings decision.axiom: n/a: CI infrastructure only
🤖 Generated with Claude Code