Skip to content

ci: name every workflow job in Title Case and run the org naming lint - #15404

Open
kyle-sexton wants to merge 21 commits into
mainfrom
ci/job-display-names
Open

kyle-sexton wants to merge 21 commits into
mainfrom
ci/job-display-names

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

No related issue: owner decisions Q12, Q27, Q32, Q33 and Q35 (2026-11-11 interview): every job gets a Title Case display name, step names are sentence case, the org naming lint runs in the required check, and the repo test and prose it makes redundant go.

Draft: stacked on #15397 and held with it. Its lint step fails until melodic-software/standards#983 and #984 merge.

Summary

  • Job names. All 240 jobs in 41 workflows get a Title Case name:, the Title Case form of the id, through standards' displayWords. A job that already had a name keeps its words, so the owner's example stays Drift (${{ matrix.skill }}). Ids do not change.
    • Matrix jobs get a plain name. GitHub still appends the matrix values: actions/toolkit artifact-tests.yml:14 names the job Upload and it reports as Upload (ubuntu-latest). So test-bash (3) becomes Test Bash (3).
  • Step names. Six step names become sentence case.
  • Consumers. Every consumer matching these check names changes in this PR, 128 files. A reader of past runs or old pull-request heads takes the new name first and falls back to the old one. Each such reader has a test that feeds it only the old name.
  • Naming lint. The org naming lint runs as a lint-repo step (check-workflow-naming) and feeds CI Status.
  • Deletions. The repo test "every workflow and action name is its file or directory stem in Title Case" and the naming prose in docs/ci-runner-routing.md and the gate header are removed, as the lint replaces them.

Fix

Commits, readable one by one:

  1. ci: name every workflow job in Title Case: workflows, consumers, tests and changelog fragments. The consumers changed:
    • pr-merge (merge-holds, clear-escalations, refire-refused, superseded)
    • post-merge demotion (red-main, handoff.bash)
    • reconcile-lost-runs, audit-lane-runs and monitor-signals
    • pr-unblock dispatchers, work-unblock and pr-verify
    • the gate's review-lane and kill-switch steps
    • babysit (review_evidence, gh, merge, merge_policy, postmerge) and review-venue.sh
    • guardrails merge_guard_ignored_checks default (Merge and merge)
    • work-items morning-brief.sh
  2. ci: run the org naming lint in CI Status, followed by ci: run the naming lint as a lint-repo step. The lint is a step in lint-repo, not its own job, per the gate header's rule that a domain gets its own job only when that shortens the critical path.
  3. docs: drop naming rules the org lint now enforces.
  4. ci: title-case the dead-link workflow job names.
  5. fix(guardrails): keep the merge-guard option description under budget.

workflow_run filters and workflow_run.name comparisons key on workflow names, and no workflow name changes here. A git grep found 6 such uses plus the workflows: lists, all workflow names.

Verification

  • naming-lint --mode enforcing with the disk-hygiene: blanket 'any filesystem root' rejection has no reasoning; blocks legitimate non-OS volumes (e.g. Windows Dev Drive) #984 vocabulary:
    • job-name 2, both in the sync-managed callers maintenance-check-links-online-claude-code-plugins.yml and pr-check-managed-files-hosted.yml; those change in standards;
    • step-name 0;
    • 0 blocking.
  • Node: every *.test.mjs in the touched directories, 129 files, 5336 pass, 0 fail. After the lint-repo move, the structure suites passed 1005 more, 0 fail.
  • pytest 9.2.0: 1714 passed (babysit and lane suites).
  • Bash suites, 0 failures each:
    • ci-review-lanes 254, ci-hold-cost-decision 179, post-merge-demotion-alert 334, check-lane-coverage 41
    • check-docs-only-gate 88, ci-lanes-pr-hold 344, workflow-self-paths 138, ci-rerun-ci-status 285
    • run-hygiene-gate 10, maintenance-file-dead-links 27, guardrails hook 726, morning-brief 597
  • actionlint and shellcheck are clean on touched files, apart from schema-gap findings already on main. run-ruff.sh check is clean, and so is offline zizmor on pr-require-checks.yml.
  • The pre-push check-affected hook passes.

Remaining

  • standards: name the synced jobs check-links (lychee-online caller) and pr-check-managed-files (managed-files-guard caller) in their components.
  • The pr-pipeline-sandbox copies of these scripts still match the old names.
  • gate-source.mjs latestRuns keys on the raw name, so during the changeover an old-named red run beside a new-named green rerun of the same lane can still read as failing.
  • audit-lane-runs finding fingerprints for job-scoped findings change once, so expect one round of churn in its digest.

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe

kyle-sexton and others added 13 commits November 11, 2026 11:31
The pr-require-checks gate job keeps its id, ci-status, and now shows
as CI Status. A new alias job, report-ci-status, named ci-status, needs
it, runs on every event the gate runs on, and passes only when CI
Status succeeded, so the required context ci-status keeps reporting
until the github-iac ci-gate ruleset requires CI Status.

Consumers that find the gate by its check or job name now take CI
Status first and fall back to ci-status only when no CI Status entry
exists (heads tested before this change). Re-runs, step reads, logs
and annotations use CI Status's job, never the alias's: re-running the
alias alone replays its old needs result. Exclusion lists and the
"aggregate alone" counts treat CI Status and its alias as one check.

- pr-feedback-dispatch rerun-ci-status, the lost-run reconciler,
  resolve-pr (held and draft reads) and babysit_verify_evidence pick
  CI Status's job.
- fix-ci-dispatch, gate-source, clear-escalations, red-main and
  babysit_postmerge exclude or merge both names.
- review-dependency, dependency-dispatch, release ci-status.sh and
  babysit_merge_policy read CI Status's check runs, then ci-status.
- The guardrails merge hook reads CI Status's superseded note and
  supersedes its alias in the same Actions run with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe
The verifier now sees a CI Status job beside a ci-status alias that only
repeats its conclusion; the prompt says neither is test evidence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe
Every job in .github/workflows now has a Title Case display name
separate from its id: the id in Title Case, with the vocabulary's
display words (PR, CI, API ...), so test-node shows as Test Node and
fix-ci / run as Fix CI / Run. Jobs that already had a name keep their
words, Title-Cased. Matrix jobs take a name with no matrix expression,
and GitHub still appends the matrix values, so test-bash (3) becomes
Test Bash (3). Job ids, workflow names and file names are unchanged.
The sync-managed workflows from melodic-software/standards change
there. Six step names are now sentence case.

A check run's name is the job's display name, so every reader that
matched a job or check by name moves with it:

- Readers of past runs or pull request heads (merge holds, escalation
  clearing, refire, red-main and its fix issues, lost-run reconcile,
  lane-run triage, monitor signals, fix-docs and feedback dispatch,
  label-unblocked, address-feedback's test lanes, the review-lane
  steps in pr-require-checks, babysit review evidence, merge policy
  and post-merge baselines, the guardrails merge hook, morning-brief
  and review-venue) take the new name first and the old name for a
  head tested before the rename. Each has a test with only the old
  name.
- Readers of their own run take the new name. handoff-evidence.sh and
  report-escalation.sh also accept the old name, since they run from
  the default branch tip and can read a run started before it.
- The guardrails merge_guard_ignored_checks default names both Merge
  and merge.

workflow_run filters and workflow_run.name comparisons key on
workflow names, which this change leaves alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe
A new pr-require-checks lane, check-workflow-naming (Check Workflow
Naming), runs melodic-software/ci-workflows' check-workflow-naming
action, enforcing, at the SHA this workflow pins its other check
actions to. The action runs naming-lint and its vocabulary from
melodic-software/standards main. CI Status needs the lane. Like the
other lanes it skips only on a contract-only event or a draft, and its
check step runs only when .github/** changed (the workflows filter
group).

The lane passes once the standards vocabulary that covers this
repository's names (standards#984) is on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe
The check-workflow-naming lane runs melodic-software/standards'
github-actions-conventions naming lint, which owns how workflows, jobs,
steps and actions are named. So:

- callers.test.mjs drops "every workflow and action name is its file or
  directory stem in Title Case" and its displayWords and displayName
  helpers.
- docs/ci-runner-routing.md and pr-require-checks.yml's header comment
  drop the `<verb>-<domain>` rule and "the job id is the check name",
  and point at the lint instead.
- babysit_review_evidence.py's workflow_display_name docstring points at
  the lint instead of the deleted test. The function stays: it is
  runtime code that reads a workflow's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe
The org step-name rule reads "Report CI Status's conclusion" as Title
Case ("Status" capitalized). Name it "Report the conclusion of
`CI Status`" and move the test fixtures that carry it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe
…ci/job-display-names

# Conflicts:
#	.github/scripts/pr-verify/resolve-pr.test.mjs
The workflow's own rule makes a domain a job only when that shortens
the critical path by more than a job costs, so the naming lint moves
from its own check-workflow-naming job into lint-repo. Its step,
id workflow_naming, runs when .github/** changed (the workflows filter
group), continues on error, and feeds the hygiene aggregate as
workflow-naming, which reads success when that group did not match.
It runs after every step that scans the tree, since the action checks
out standards under the workspace.

CI Status no longer needs a separate job, and the lane lists, the
timeout budget, docs/ci-runner-routing.md and the replay fixture drop
it. lint-repo keeps its 13-minute timeout: its measured max is 265 s
against a 7-minute work budget, and the step adds a standards
checkout, a cached Node setup, an npm ci of two packages and a lint
pass that took 0.4 s locally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe
maintenance-file-dead-links.yml is owned here, not synced from
standards, so its two jobs take Title Case display names: Read The
Dead Links and Update The Dead-Link Rollup Issue. Nothing reads them
by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe
The merge_guard_ignored_checks description grew to 400 characters when it
named both forms of the merge job; the plugin-option contract caps it at
300. Say the same in 251.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe
@kyle-sexton kyle-sexton added the do-not-merge Hard merge gate: do not merge while applied. label Nov 11, 2026
kyle-sexton and others added 4 commits November 11, 2026 18:57
ci-workflows#1072 adds the alias-jobs input. Naming the ci-status
mirror job there keeps contract-only runs reading as contract-only,
re-runs failed contract-only siblings, and keeps carried successes
from before the rename.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe
…ci/job-display-names

# Conflicts:
#	.github/scripts/maintenance-monitor-signals/collect.mjs
#	.github/scripts/maintenance-monitor-signals/signals.mjs
#	.github/scripts/work-unblock/label-unblocked.test.mjs
# Conflicts:
#	.github/scripts/post-merge-demotion-alert/red-main.test.mjs
#	.github/scripts/pr-unblock/fix-ci-context.test.mjs
#	.github/scripts/pr-verify/probe/replay.mjs
#	.github/scripts/pr-verify/resolve-pr.test.mjs
#	docs/ci-runner-routing.md
@kyle-sexton kyle-sexton removed the do-not-merge Hard merge gate: do not merge while applied. label Nov 12, 2026
@kyle-sexton
kyle-sexton marked this pull request as ready for review November 12, 2026 01:24
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-12T01:29:08.676685Z 1c6f2a7 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c6f2a71e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

didNoWork's pre-model filter matched only the job ids, so a rate-limit
stop in `Resolve Issue / Broker Check` or `Resolve Issue / Run` fell to
the failed-run re-fire and ignored the reset it named.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 65823d66f1baa8ce21a024ee6a92ecc2a5b0a405: read 135 of 135 changed file(s); 1 finding(s), 1 as inline comments.

Chunk 1 of 4 (40 files)

Security review, chunk 1 of 4: no security findings

I read every hunk of all 40 files in this chunk. The prior-findings file says blocking only: yes, so this review reports only CRITICAL and IMPORTANT findings; there are none.

What changed in this chunk

Jobs are getting Title Case display names, and the code that reads job and check names now accepts both forms:

  • the new display name (PR Review / review-status, Merge, Kill Switch, Test Bash (3));
  • the old job id, so runs and heads from before the rename still read.

Tests and changelog fragments follow. One repository test is deleted (below).

Trust controls checked (merge holds)

  • merge-holds.mjs REVIEW_LANES. It now lists PR Review / review-status and PR Review Security / review-status next to the old ids. noticedHead matches with checks.includes(job?.name) on each review lane's own pull_request runs (merge-holds.mjs#L869).
    • The base already accepted the old id from those same runs.
    • Adding the new name restores coverage after the rename and admits no new source: the same workflow, same-repo heads, the same compare vet.
  • clear-escalations.mjs.
    • LANE_RECORDS and laneReview gain the same dual names.
    • newestNamed takes the newest job carrying the first listed name that any job has. It still needs completed/success, plus a whole security review and completion notices.
    • The retired names pr-review / claude-review-status and pr-review-security / security-review stay rejected, and the tests still pin that.
  • superseded.mjs. It now matches only the display name (Merge, Enable Auto Merge), with no id fallback. A pending run from before the rename is then not seen as superseding, so the current run does its own gated work. That runs more checks, not fewer, so no hold gets weaker.
  • refire-refused.mjs. writeCallers accepts <Name> / Run or <id> / run, the same write-runner callers as before. The read runner (Fix CI / Run or fix-ci / run) is still excluded from the trusted-annotation read, and the test covers both forms.
  • address-feedback.mjs ciSnapshot. It keeps one run per lane and prefers the display-named one. A stale id-named failure next to a display-named pass on the same head now reads green.
    • Both runs must still come from the github-actions app.
    • Anything that could post a forged display-named run could already post a forged id-named one at base.
    • No new actor or source is admitted.
  • No merge hold is widened, and no named friction hold is removed.

Other changed readers
These only read job names from runs of the repository's own workflows. They gain the dual-name sets and widen no trust:

  • handoff-evidence.sh jq job(): title-cased name first, then the raw id. The forged-step guards are unchanged and still tested.
  • reconcile.mjs: GATE_JOBS, VERDICT_JOBS, INTAKE_LABEL_JOBS and REVIEW_JOB. isIntakeLabelJob still requires / right after the name.
  • triage-sweep.mjs, collect.mjs/signals.mjs, feedback-dispatch.mjs DISPATCH_JOB, and red-main.mjs checkName.
  • handoff.bash builds an old-form marker from main's own check names. No PR-controlled text reaches it.

Instruction surface
callers.test.mjs deletes the test "every workflow and action name is its file or directory stem in Title Case".

  • That test protected workflow_run filter matching. Drift there costs availability (a trigger stops firing), not a leak, compromise or charge.
  • The PR replaces it with the org naming lint, check-workflow-naming, pinned by SHA in another chunk.
  • The instruction exception register was not consulted. This deletion is a validator, not a standing instruction.

Manifests and settled threads

  • No .claude-plugin manifest is in this chunk.
  • The settled thread (pr-run-activity-read.yml) is in another chunk, and nothing here re-raises it.

Prior findings
The one prior entry is a summary comment with no line anchor, about label-unblocked.mjs (chunk 2). Nothing is restated.

Spend
No new spend or billing in this chunk.

critical-findings: 0

Chunk 2 of 4 (40 files)

Security review, chunk 2 of 4: no security findings

This lane already reviewed an earlier head (blocking only: yes), so only CRITICAL and IMPORTANT findings would be reported. I read every hunk of all 40 files in this chunk. The trusted-path files among them got the extra scrutiny the prompt asks for: fix-ci-context.test.mjs, publish-feedback.mjs, report-escalation.sh, and every listed workflow, pr-merge.yml included.

What changed

  • 26 workflows only add a Title Case name: to each job, or reword an existing name.
  • No job id changes. So every needs:, needs.<id>.outputs.*, if: condition and permissions: block still resolves the same.
  • In pr-merge.yml the merge, enable-auto-merge, gate-check and other jobs get display names. The only other edits there are comments.
  • The kill-switch job in intake-triage.yml gets the name Kill Switch. Every job that depends on it still reads needs.kill-switch.outputs.proceed.

Consumers in this chunk

  • fix-docs-dispatch.mjs. heldHandoff, heldHead and docsRunState now match a job by its Title Case name or by its id.
    • Each naming is checked on its own, so a run is judged either entirely by display names or entirely by ids.
    • The protection against a forged handoff is unchanged: it still requires broker-allowed success, the run job, a skipped or missing scan, a successful handoff job, and no next leg.
    • Job names come from pr-refine.yml on main, and the jobs are read from run ids the dispatch record holds. A pull request author cannot pick them.
  • label-unblocked.mjs. namedJobs matches by the JOB_NAMES display name or by the id.
    • I searched the file: every job-name lookup goes through namedJobs or the preModel regex. That includes the untrusted-provenance stop (stoppedUntrusted, L1994), the kill-switch checks (L1822), the stale stop and the report-outcome stop.
    • So the untrusted-provenance hold still sees renamed runs.
    • The / suffix still keeps Resolve Issue from matching Resolve Issue Subscription / ….
  • publish-feedback.mjs CI_RUN_NAME. The pattern now accepts single spaces between words of [A-Za-z0-9_.-], and the name is placed in a Markdown link.
    • Brackets, parentheses outside the trailing (N), newlines, doubled spaces and a trailing space are all still rejected.
    • New tests cover Test [x](y), Test Node and Test Node . The URL is still pinned to a github.com Actions job page. No Markdown or link injection opens.
  • report-escalation.sh. The jq filter accepts Publish Feedback / Run as well as publish-feedback / run. Both are literal comparisons with no shell interpolation of the name.
  • pr-verify/prompt.md. The instruction text only says how to read old and new job names as the same job. It removes no guardrail.
  • Tests. The edited regexes in workflow-gates.test.mjs and upstream-apply-changelog.test.mjs only allow an optional name: line, and the gate assertions they feed are unchanged.

Trust controls

  • Nothing in this chunk widens the trusted-trigger check, the lane token scope, the broker or signer, or the kill switch.
  • Of the merge-hold surface, this chunk touches only display names and comments in pr-merge.yml. The hold scripts that match check names (merge-holds.mjs, clear-escalations.mjs, superseded.mjs) are in chunk 1, and the guardrails hook default is in chunk 3.
  • No owner-named friction hold is touched.

Not settled

  • pr-verify/workflow.test.mjs drops the assertion that the key job keeps its id as its display name. The key-job input still passes the job id to the external check-key-credit action (melodic-software/ci-workflows).
  • I could not read that action, so I can't tell whether it looks for earlier credit stops by job id or by display name.
  • At worst this affects which credential a run uses, which spends prepaid credit or subscription usage. It is not a security finding.

Manifests and instructions: this chunk changes no .claude-plugin manifest and deletes no standing instruction.

Prior findings and settled threads: the earlier summary finding had no line anchor and nothing here repeats it. The one settled thread (pr-run-activity-read.yml, the pre-model filter) is unchanged since it was resolved, and the code it covers (label-unblocked.mjs L1826) still matches both namings.

Spend: nothing new.

Embedded text: none in the PR context or thread files.

critical-findings: 0

Chunk 3 of 4 (40 files)

Security review, chunk 3 of 4: one manifest flag (IMPORTANT), nothing CRITICAL

This lane already reviewed an earlier head (blocking only: yes), so this pass reports only CRITICAL and IMPORTANT findings. Manifest flag lines are always reported. I read every hunk of all 40 files in chunk-3.patch.

Finding

  • manifest userConfig.merge_guard_ignored_checks: flag, the default now also ignores every GitHub Actions check named Merge. The inline comment on plugins/guardrails/.claude-plugin/plugin.json covers it. Severity is IMPORTANT. The key is not a code-installing field, and no leak, compromise or charge path follows from it. The server-side ruleset still decides required checks.
  • manifest userConfig.merge_guard_ignored_checks.description: pass (the description is in the inert set).

Trust controls and holds checked

  • Review-lane and cost holds in pr-require-checks.yml (L5703–5704, L7139–7140, L7385–7389).
    • The status job names now list the display name first, then the old id-based name, separated by |. Both readers split on | (L5691, L7385), as the base code did.
    • The new newest() jq helper picks the newest job of the first name that appears in the run. With only one of the names present it gives the same result as the old max_by, and a missing job still gives null, which keeps the verdict failing or pending.
    • Kill-switch detection now accepts Kill Switch or kill-switch. It still fails closed.
    • A head that changes pr-review-hosted.yml or pr-review-security-hosted.yml still hits keep (L5685–5687).
    • No hold is narrowed.
  • Babysit trusted-path scripts.
    • babysit_merge.py AI_REVIEW_CHECKS, babysit_merge_policy.py REVIEW_LANES, babysit_review_evidence.py LANES and KILL_SWITCH_JOBS, and babysit_gh.py each add the display-name spelling next to the old one.
    • The old names already matched by name alone, from any workflow, so a lookalike job name is not a new exposure.
    • babysit_postmerge.py check_key matches old and new spellings of a check against the baseline. That is post-merge demotion bookkeeping, not a merge hold.
  • Naming lint step (pr-require-checks.yml L1998–2002).
    • It is an org action pinned to a commit SHA. It runs in lint-repo with contents: read and checks: read (L1345–1347), no secrets, and persist-credentials: false.
    • Fetching standards at runtime is a supply-chain question for zizmor, and it widens no trust.
  • Other workflows. The remaining 18 workflow files add only name: display names to jobs and sentence-case step names. No if:, permissions:, trigger or token request changes.
  • Named friction holds. None weakened.

Instruction surface

  • The deleted prose (the job-naming rules in docs/ci-runner-routing.md and the pr-require-checks.yml header) is a naming convention, now enforced by the new lint step.
  • None of it is an agent instruction in a protected class of the instruction exception register (present at docs/contracts/instruction-exception-register/README.md).

Other

  • Embedded text. None in the PR context or the settled threads.
  • Prior findings. The one earlier post has no line anchor and covers other files. Nothing is restated.
  • Spend. One extra lint step inside an existing lint-repo job.

critical-findings: 0

Chunk 4 of 4 (15 files)

Security review, chunk 4 of 4: no security findings

Scope: blocking only. This lane already reviewed an earlier head (blocking only: yes), so only CRITICAL and IMPORTANT findings would be reported. I read every hunk of all 15 files in this chunk:

  • the babysit tests: test_babysit_postmerge*.py, test_babysit_review_evidence.py and test_merge_holds_sync.py
  • the pull-request skill files: ci-ci.md, merge.md, review-venue.sh and review-venue.test.sh
  • the morning-brief files: morning-brief.sh and morning-brief.test.sh
  • scripts/check-affected-gates.txt, ci-hold-cost-decision.test.sh, ci-review-lanes.test.sh and post-merge-demotion-alert.test.sh

None of these files matches a TRUSTED PATHS: glob. The tests are under babysit-prs/scripts/tests/, not the listed scripts.

What I checked

  • review-venue.sh L508. The check-run filter goes from ^test($|[- (]) to ^[Tt]est($|[- (]). It still requires .app.slug == "github-actions" on the head's check runs. The script's tests line only reports whether CI covered a commit (ci or local); it is not a merge gate. Accepting Test … beside test-… restores coverage for the renamed jobs and admits nothing a lowercase job could not already claim before. No widening.
  • morning-brief.sh L2393, L2467–2469. CALLER_JOB_RE now also allows uppercase letters and spaces. / is still outside the character class, so a match stops at the first / separator. The captured name is lowercased, spaces become dashes, and the result is written as an activity= line in a read-only digest. The job names come from the repository's own Actions runs. There is no token, gate or credentialed sink downstream.
  • Merge-gate test fixtures (test_merge_holds_sync.py, ci-hold-cost-decision.test.sh, ci-review-lanes.test.sh, test_babysit_review_evidence.py). They pin the new PR Review / review-status and PR Review Security / review-status names, and keep the old pr-review / review-status names that were already accepted before. New tests confirm a status job still counts only under its own lane's workflow: the other lane's old job id in this lane's workflow decides nothing (ci-hold-cost-decision.test.sh L660–662, ci-review-lanes.test.sh L713). They also confirm a kill-switch job named by its old id still stops the lane (ci-review-lanes.test.sh L703–704). In these fixtures no hold is loosened beyond adding the display-name variant. The implementing modules are in other chunks.
  • merge.md L106–108. This is an instruction-surface edit. It adds the Title Case review check names to the exact-match list and keeps the earlier names. Nothing is deleted or softened; every review check still has to complete with success.
  • post-merge-demotion-alert.test.sh. The new cases check that a fix issue filed under an old job id still tracks or mutes the check under its new display name. The existing test that annotation titles never reach a fix issue (L1862–1071) is kept, now using the new names.
  • Settled thread (pr-run-activity-read.yml L459). It is not in this chunk, and its lines are unchanged since resolution, so I did not raise it again.
  • Prior findings. The earlier summary-only entry is about label-unblocked.mjs, which is not in this chunk. Nothing here repeats it.
  • Manifests: no .claude-plugin manifest changes in this chunk.
  • Owner friction holds: none removed in this chunk.
  • Spend: nothing new. check-affected-gates.txt only registers the workflow-naming gate as CI-only.

critical-findings: 0

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed 65823d66f1baa8ce21a024ee6a92ecc2a5b0a405: read 135 of 135 changed file(s); 1 finding(s), 1 as inline comments.

Chunk 1 of 4 (40 files)

claude-review, chunk 1 of 4 (blocking only)

This lane has already reviewed an earlier head (prior-findings says blocking only: yes), so this review reports only blocking findings. I read every hunk of the 40 files in chunk 1.

Finding

  1. Blocking, restated and now inline. handoff.bash:105-106 builds the wrong pre-rename fix marker for a hygiene-gate check.
    • The last review posted this as a summary finding (comment). The file is unchanged since then, and a changed line can carry it, so it is now an inline comment.

What I checked and left alone (no blocking issue found)

  • handoff-evidence.sh job($n). It tries the Title Case <call> / <job> name first, then the raw ids. The new test checks every CALLS id in the three workflows against its job's display name.
  • address-feedback.mjs ciSnapshot.
    • Each lane is keyed by laneName, and a display-named run is preferred over an id-named one.
    • lanes.every((lane) => byLane.has(lane)) replaces the old exact-name match.
    • A test pins the case where an old-named failure sits beside a new-named pass.
  • Old-name fallbacks. Each of these takes the new name and falls back to the old one, and each has an old-name test:
    • red-main.mjs checkName
    • collect.mjs mergeJob and the HOLD_LANES names
    • reconcile.mjs GATE_JOBS, VERDICT_JOBS and INTAKE_LABEL_JOBS
    • triage-sweep.mjs
    • clear-escalations.mjs newestNamed
    • merge-holds.mjs REVIEW_LANES
    • refire-refused.mjs writeCallers
    • feedback-dispatch.mjs DISPATCH_JOB
  • superseded.mjs NAMES[job]. It has no old-id fallback. That is advisory only (a pending run queued before the rename), so I dropped it under the blocking-only rule.

Notes

  • Closing issue. The PR context names no closing issue. The PR body cites owner decisions but gives no acceptance criteria to check.
  • Plugin manifests. Chunk 1 changes none. The four changelog fragments are bump: patch, which fits renamed check names read with fallbacks.
  • Embedded text and cost. I found no embedded instructions in the PR context or prior findings. Nothing in this chunk bears on cost.
  • Verification. I ran no tests. The author's test counts are their claim.

Chunk 2 of 4 (40 files)

Chunk 2 of 4: no blocking findings (blocking-only review)

This lane already reviewed an earlier head of this PR (blocking only: yes), so this review reports only blocking findings. I read every hunk of all 40 files in this chunk (fix-ci-context.test.mjs through pr-merge.yml).

What I checked

Workflow job names (21 workflows). Each change adds a Title Case name: or turns an existing name into Title Case. No job id, needs: edge or if: changes.

Consumers that read jobs by name. These now match both the new display name and the old id, so runs started before the rename still read correctly:

  • fix-docs-dispatch.mjs:221-227. The titled() / NAMINGS fallback produces Fix Docs / Broker Allowed, Fix Docs Head Held, Fix Docs Subscription / and so on. I compared these with the name: lines at head in pr-refine.yml (Fix Docs Head, Fix Docs Head Held, Fix Docs Handoff, Fix Docs Subscription, Fix Docs Fallback) and in pr-run-activity-read.yml (Broker Check, Broker Allowed, Run, Scan, Report). They match.
    • Every caller passes the fix-docs defaults, so the acronym gap is never reached. titled() cannot produce an acronym like CI.
    • The new test covers runs whose jobs are named by their ids.
  • label-unblocked.mjs. JOB_NAMES covers every id that namedJobs is called with (admit, pick-slot, report-outcome, the three resolve-issue calls, and the sweep job). The startsWith("<name> /") check still keeps Resolve Issue apart from Resolve Issue Subscription. The structure test now pins each display name against work-resolve-issue.yml, work-unblock.yml and pr-run-activity-read.yml.
  • report-escalation.sh:143-145. It accepts Publish Feedback / Run or publish-feedback / run, and the test covers both.
  • publish-feedback.mjs:237. The widened CI_RUN_NAME allows only single spaces between [A-Za-z0-9_.-]+ words. New negative cases cover a double space, a trailing space and markdown brackets.

Things I followed outside this chunk:

  • post-verify-notes / dispatch in pr-feedback-dispatch.yml. reconcile.mjs:306 VERDICT_JOBS (chunk 1) holds both names, so the reconciler still finds the verdict job after the rename.
  • The pr-verify/workflow.test.mjs assertion removed at L383-386. The key-job input only names the credit-stop marker (key-credit-stop-<file>-<key-job>) and keeps using the job id. Giving key jobs display names does not change the marker.
  • Test regex changes (workflow-gates.test.mjs, upstream-apply-changelog.test.mjs). They allow an optional name: line between the job id and if:/needs:, which matches how the workflows now place name:.

Notes

  • Prior findings. The one carried-forward finding (handoff.bash:105-106 marker rebuild) sits in chunk 1, so it is not restated in this chunk.
  • Closing issue. None. The PR cites owner decisions, not an issue with acceptance criteria.
  • Embedded instructions. The PR context contains none.
  • Out of scope here. No plugin manifest, cost-bearing change or prepaid spend is in this chunk.
  • Verification. I ran no tests. The author's test counts are their own claim and I have not checked them.

Chunk 3 of 4 (40 files)

Chunk 3 of 4: blocking only, no findings

This lane already reviewed an earlier head (prior-findings says blocking only: yes), so this review reports only blocking findings and manifest flag lines. I read every hunk of all 40 files in chunk-3.patch. They cover:

  • workflows pr-rate-merge-risk through work-unblock;
  • the docs;
  • the guardrails manifest, README, hook and test;
  • the babysit and source-control changes.

I ran nothing. Every pass, fail or count the PR description gives is the author's claim and unverified.

What I checked and found consistent

  • pr-require-checks.yml cost-hold and review-lane loops. The |-separated status-name lists (PR Review / review-status|pr-review / review-status) feed code that already splits on |:
    • L5691 uses split("|");
    • the messages take the first name only, through ${checks%%|*} at L7273 and L7476.
  • The new jq newest($names). It takes the newest job under the first name that has one, and gives null when no name matches. That keeps the earlier $s/$k null handling for both the status job and Kill Switch/kill-switch.
  • babysit_postmerge.check_key. It lowercases the job part and turns its spaces into hyphens, leaving the leg and gate suffixes alone. So a baseline test-bash (0) or lint-repo: typos matches Test Bash (0) or Lint Repo: typos. failing - new still passes only checks that were failing before to expired.
  • Lane-name tables. Each table now lists the display name first and keeps the lower-case caller and the older names: AI_REVIEW_CHECKS, REVIEW_LANES, LANES, KILL_SWITCH_JOBS, the merge-after-AI-review jq in merge.md, and the collect_lane_runs kill-switch match. The tests cover both forms.
  • Other readers of the renamed jobs. maintenance-monitor-signals/signals.mjs HOLD_LANES lists names for the renamed pr-refine, pr-verify, pr-review-main, work-unblock and pr-merge jobs. wait-refine.mjs matches no job names. I found no incompatibility.
  • Guardrails hook. The script fallback default and plugin.json default match (github-actions:Merge,github-actions:merge). A new test covers the job-id form, and the empty-list test now expects Merge.

Manifest (plugins/guardrails/.claude-plugin/plugin.json)

  • manifest userConfig.merge_guard_ignored_checks default: pass. The PR body and the guardrails fragment say why: the pr-merge job's check is now Merge, and merge stays for heads tested before the rename. A security lane runs, so judging the wider ignore set as a guard change is its job, not this lane's.
  • manifest version (fragment bump:): pass. It is patch, for a default that follows a rename and keeps the old name.
  • manifest description/keywords: pass. Nothing changes what the plugin ships.

Notes

  • Spend. The new Check workflow and action naming step adds one step to lint-repo on a standard ubuntu-24.04 runner. It spends no model credit and starts no billed resource, so it is not a hold.
  • Carried-forward finding. The handoff.bash finding from earlier rounds is in chunk 1, not this one, so I have not restated it here.
  • Closing issue. None; the PR cites owner decisions instead, and I found no acceptance criteria to check.
  • Embedded instructions. None found in the PR context or prior findings.

Chunk 4 of 4 (15 files)

Chunk 4 of 4: blocking only, no findings

The prior-findings file says blocking only: yes, so this review reports only blocking findings. I read every hunk of all 15 files in this chunk:

  • the babysit Python tests (test_babysit_postmerge*.py, test_babysit_review_evidence.py, test_merge_holds_sync.py)
  • ci-ci.md and merge.md
  • review-venue.sh and its test
  • morning-brief.sh and its test
  • check-affected-gates.txt
  • ci-hold-cost-decision.test.sh, ci-review-lanes.test.sh and post-merge-demotion-alert.test.sh

None of them has a blocking finding.

What I checked

  • review-venue.sh:230. ^[Tt]est($|[- (]) matches the new matrix names (Test Bash (0)) and the old ids (test-bash (0)). It still rejects names that only start with the letters "test", such as Testing.
    • The test file now has one case that uses display names and one that uses ids, so both paths are covered.
  • morning-brief.sh:2393. The wider CALLER_JOB_RE cannot swallow a / separator, because / is outside its character class. Backtracking still captures Publish Feedback, not Publish Feedback with a trailing space.
    • Lowercasing and swapping spaces for - turns PR Review Security into pr-review-security, the job id.
    • ${…,,} is not a new Bash 4 dependency: the script already uses it at L3300.
    • The new test only covers the id path (fix-ci / run), and the fixtures at L557-558 now use display names.
  • check-affected-gates.txt. The new workflow-naming entry matches the gate name the lint-repo step reports, in STEP_LOG_CHECKS and in the workflow-naming= outcome line (pr-require-checks.yml in the full diff).
  • Review-lane and cost-decision tests. They check old-name heads both ways:
    • each lane's old status-job name (pr-review / review-status, pr-review-security / review-status) still binds or decides for that lane;
    • the code lane's old name does not count for the security lane, and the security lane's old name decides nothing in the code lane's workflow.
  • merge.md. The check-name list is consistent with the Python and bash readers' tests in this chunk.

Prior finding (not restated here)

The blocking handoff.bash gate-check marker finding from the last review is still present at this head: handoff.bash:105-106.

  • The bug. Lint Repo: typos failed still turns into lint-repo:-typos-failed, not the pre-rename name.
  • The test gap. This chunk's post-merge-demotion-alert.test.sh adds old-marker cases only for matrix and plain names (test-bash (3), test-node). It still has no case that feeds an old gate-check marker.
  • Why it is not counted here. The file is in chunk 1, whose review owns that restatement, so this chunk does not count it a second time.

Notes

  • Closing issue. The PR closes no issue, so there are no acceptance criteria to check.
  • Other checks. This chunk has:
    • no plugin manifest change;
    • no change that costs money or spends usage;
    • no instruction embedded in the PR context.
  • Verification. The author's reported test results are their claim and unverified. I ran no tests.

kyle-sexton and others added 2 commits November 11, 2026 20:44
lint-repo now feeds the org naming lint to the aggregator as
workflow-naming; check-affected-gates.test.sh fails on a gate with no
row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q2ceZLXhTR4d1vkTCe

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-security-review: 1 inline finding(s) on 65823d66f1baa8ce21a024ee6a92ecc2a5b0a405; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

"description": "Comma-separated <app slug>:<check name> pairs block-unresolved-pr-merge never judges as pending or failing. The default, github-actions:merge, is the pr-merge lane's own merge job, which runs as the merger rather than as a gate. Empty judges every check.",
"default": "github-actions:merge"
"description": "Comma-separated <app slug>:<check name> pairs block-unresolved-pr-merge never judges as pending or failing. The default names the pr-merge lane's merge job, which runs as the merger, not a gate, by display name and by job id. Empty judges every check.",
"default": "github-actions:Merge,github-actions:merge"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

manifest userConfig.merge_guard_ignored_checks: flag, the default now also ignores every GitHub Actions check named Merge, in any repository

merge_guard_ignored_checks is a guard key. block-unresolved-pr-merge.sh L967–971 drops any check run whose <app slug>:<name> matches an entry exactly (IN($ign[]), case-sensitive). A dropped check neither blocks nor supersedes. The default used to drop only github-actions:merge. It now also drops github-actions:Merge ("default": "github-actions:Merge,github-actions:merge", and the hook fallback at L967).

Who is affected. Every installed user who never set the option gets the wider default on update, in every repository they merge from.

What gets through. Take a PR in any repository whose GitHub Actions job is displayed as Merge, for example a merge-queue or pre-merge validation gate. If that job is failing or pending, gh pr merge <n> now passes this guard without a word. Before, it was refused with "Failed checks" or "Pending checks". The guard cannot tell workflows apart (the hook header says so), so the exemption reaches far beyond the pr-merge lane's own job, which is all this change needs it for.

What still stops it. The repository's ruleset, if it requires that check, stops the merge on the server. Nothing stops it locally.

Why IMPORTANT, not CRITICAL. No token, secret, code-execution or billing path follows. The flag is reported because a guard default changed toward more unchecked action.

Options for the owner.

  • Accept the wider default (the old one already exempted every merge job).
  • Or narrow the exemption another way, for example by matching on the job's details URL or workflow path instead of the name, so Merge gates in other repositories are still judged.

severity: IMPORTANT

claude-lane: claude-security-review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Settled without a code change: the wider default keeps current behavior across the rename. pr-merge.yml's merge job (L930) now displays as Merge. Without github-actions:Merge in the default, the guard would treat the lane's own running merge job as a pending check and refuse every lane merge, which is the only reason github-actions:merge was exempt before. The new entry is exactly as broad as the old one: one GitHub Actions job name, in any repository. github-actions:merge stays until no run reports the old name. A failing or pending Merge check that a ruleset requires is still enforced by the server. No token, secret or account path is involved. Narrowing the match to a workflow path would be a separate guard change, not part of this rename.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-review: 1 inline finding(s) on 65823d66f1baa8ce21a024ee6a92ecc2a5b0a405; the summary is in this lane's pull request comment.

claude-lane: claude-review

Comment on lines +105 to +106
job="${check%% (*}" old="${job// /-}"
old="<!-- post-merge-fix check=${old,,}${check:${#job}} -->"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the pre-rename marker is wrong for hygiene-gate checks.

The old marker is rebuilt by taking everything before (, swapping every space for - and lowercasing the result. The marker written at L268 is , and `failingChecks` (red-main.mjs) produces gate checks as ${name}: ${gate} ``.

So for check="Lint Repo: typos failed":

  • job is the whole string, because there is no (.
  • old becomes lint-repo:-typos-failed.
  • The issue filed before the rename carries check=lint-repo: typos failed.

The awk lookup never matches it. During the changeover that has two effects:

  • An open fix issue for a gate check is missed, and a duplicate is filed.
  • A gate muted as not_planned is filed again.

Matrix checks (Test Bash (3) → test-bash (3)) come out right. Only the job part before : or ( should be converted, with the rest kept as is. For example:

job="${check%%: *}" job="${job%% (*}" old="${job// /-}"
old=""

No test feeds an old gate-check marker. Add one, beside the matrix case in scripts/post-merge-demotion-alert.test.sh, for Lint Repo: typos failed against a lint-repo: typos failed issue.

claude-lane: claude-review

This branch has not been deployed

No deployments
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