Skip to content

feat(distribution): add sync-managed claude review-lane caller components - #286

Merged
kyle-sexton merged 15 commits into
mainfrom
feat/claude-lane-caller-components
Jul 29, 2026
Merged

feat(distribution): add sync-managed claude review-lane caller components#286
kyle-sexton merged 15 commits into
mainfrom
feat/claude-lane-caller-components

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds two components — claude-review-caller and
claude-security-review-caller — sourcing thin workflow callers for the
ci-workflows reusable Claude review lanes at
.github/workflows/claude-review.yml and
.github/workflows/claude-security-review.yml, pinned at the v0.9.1
release SHA (c136b27f404dd32ce3873f39a6f3443891d1c16e). This is Phase 3a of
the ci-workflows claude-review-lanes plan
(docs/topics/claude-review-lanes/PLAN.md).

Sections below marked "REVISED" supersede the original text. Four
commits landed after this body was first written; where they conflict, the
REVISED text is authoritative.

REVISED — pin is v0.9.1, not v0.9.0

v0.9.0 (cf666f67) was tagged BEFORE paths-file merged. The security caller
passes paths-file, which v0.9.0's reusable does not declare — Actions
hard-fails a workflow_call that passes an undeclared input, and on the
security lane that wedges a required check permanently. Verified by reading
each reusable's workflow_call block at the pinned SHA (git show <sha>:<path>, not main, not the tag) and diffing the caller's with: /
secrets: keys against it in both directions, plus every
needs.<job>.outputs.<name> read against the reusable's declared outputs.
Result at v0.9.1: 0 undeclared inputs, 0 undeclared secrets, 0 omitted
required inputs, 0 undeclared output reads, across all four uses: including
both select-runner.yml calls. Negative control at v0.9.0 reproduces the
defect.

REVISED — these components are PRIVATE-ONLY; the security caller is PARKED

The callers resolve the runner through the governed select-runner
indirection, and runner-policy admits that selector only for a private
self-hosted consumer (routingEnabled = visibility === "private" && selfHostedCi). The !routingEnabled branch consults neither exceptions nor
localRoutingGrants, so a PUBLIC target has no configuration escape.

Auditing each component against a public consumer config using the shipped
policy.json yields four public-self-hosted-routing findings; the same
audit passes clean for a private self-hosted consumer. Consequences:

  • claude-review-caller is managed for the four PRIVATE targets that run
    the lane: dotfiles, github-iac, medley, provisioning.
  • melodic-software/claude-code-plugins is PUBLIC and is now locally-owned
    for both callers, keeping its hand-written hosted-only callers. Had the
    original targeting shipped, its own runner-policy lane — and with it
    ci-status — would have gone red, wedging the one repo whose ruleset
    requires security-review / security-review.
  • claude-security-review-caller therefore has no managed target and is
    recorded as PARKED, not scoped: both repos running a security lane today
    (claude-code-plugins, ci-workflows) are public. It is retained rather than
    deleted because its bytes are the reviewed shape for the one lane whose
    check can be a required context. It unparks when a private repo adopts the
    security lane, or when the runner indirection moves inside the ci-workflows
    reusable so one component serves both visibilities.

Three tests in components/runner-policy/runner-policy.test.mjs hold this and
were each proven non-vacuous by reintroducing the defect: a selector-routed
caller may not be managed for a public target; every caller component must
audit clean for a private self-hosted consumer; a selector-routed caller is
expected to be rejected outright on a public one.

REVISED — accepted loss: synchronize

All four managed targets' live callers carry synchronize; the component
drops it. That is the plan's deliberate cadence cut (review on
open/ready/reopen; re-run the job for a fresh review), not drift — but it is
the largest behavior change the component makes and is now recorded as an
accepted loss for every target, alongside medley's paths-ignore. The
security lane KEEPS synchronize: its check certifies execution against the
latest head.

REVISED — validation coverage gap closed

components/claude-lanes/ holds workflow bytes, but every workflow-shaped
lane in this repo discovers files under .github/workflows — so actionlint,
zizmor, concurrency-policy, and pin-comment-convention were all blind to
these components, and lint:runner-policy scans the repo's own workflows,
not components/. Nothing validated them.

Three lanes now cover them in CI, each proven non-vacuous:

  • pin-comment-convention takes an explicit file list and scans
    components/claude-lanes/*.yml (a mismatched SHA claim fails it).

  • zizmor names both components alongside .; its paths input is
    whitespace-separated. Verified that a bare . never reached them, that they
    are audited now, and that the finding total, severity breakdown, and exit
    code are identical to baseline — so what the lane gates on is unchanged.

  • actionlint, through a new materialization contract test
    (components/claude-lanes/claude-lanes.test.sh, wired into the actionlint
    job). It runs the entrypoint a consumer runs rather than linting the
    component in place: sync-manifest.sh apply into a scratch checkout carrying
    the target's origin identity, then actionlint over the result. Every managed
    target is covered, plus each component's bytes standalone at their
    destination path so the PARKED security caller — which no target manages, so
    no target loop reaches it — is linted too. A control run with the suppression
    config removed still reports the concurrency.queue message, so the case
    cannot go vacuous unnoticed when concurrency: Add support for queue key rhysd/actionlint#654 ships upstream.

    Scope of that coverage, stated precisely: the test is hermetic and makes
    no network call, so it lints each target's materialized bytes under THIS
    repository's canonical actionlint config — not under the target's own. For
    github-iac and medley, which own actionlint locally and so receive no
    config from the manifest, the canonical config always substitutes. If either
    deleted its own queue suppression, this test would still pass. What the test
    owns is that the shipped bytes lint clean under a conforming config at the
    destination path the manifest maps. That the four live configs actually
    conform is gate item 1 below — a live check, re-verified independently, and
    it is what covers this gap.

Still blind, deliberately: concurrency-policy fails both components on three
rules whose values are load-bearing (lane-scoped group names; the security
caller's deliberate cancel-in-progress: false). No target repo runs
concurrency-policy, so nothing fails today; the conflict is documented at the
component sources so a future adopter exempts rather than normalizes.

Caller shape derives from the reusables' canonical-caller headers (the SSOT):

  • Job ids review / security-review — required-check name continuity
    (security-review / security-review on claude-code-plugins' ruleset).
  • Review triggers [opened, ready_for_review, reopened]; security triggers
    add synchronize; no workflow-level path filtering on the security caller
    (a non-triggering required check wedges forever).
  • Permissions per the canonical headers (contents: read,
    pull-requests: write, id-token: write on the lane job).
  • Governed select-runner indirection (selector job, needs, runner input)
    with the runner-policy recovery fallback 'ubuntu-24.04' — never a
    self-hosted label, never vars.CI_HOSTED_RUNNER.
  • Security caller passes paths-file: .github/claude-security-paths
    (shipped in ci-workflows#282).
  • CLAUDE_CODE_OAUTH_TOKEN passed explicitly; never secrets: inherit.
  • Targets are the repos that run each lane today — SUPERSEDED by the
    private-only revision above; the shipped targeting is claude-review-caller
    in dotfiles, github-iac, medley, provisioning, with claude-code-plugins
    locally-owned for both callers and claude-security-review-caller
    parked. .github and ci-runner stay exempt (plan approval record item 1);
    knowledge-corpus and songwriting are commented follow-ups gated on the sync
    App access grant.

distribution/README.md's three workflow-caller-exclusion statements gain
carve-outs pointing at a new authoritative section: hand-written lane
callers empirically drifted (medley's missing reopened trigger, divergent
skip-actors lists, pin skew v0.6.1 / e295107). AGENTS.md and
governance-process.md were grepped for restatements of the exclusion — none
exist. Cross-doc reconciliation self-review performed per
distribution/governance-process.md: the three README statements plus the
new section are the complete reconciliation surface, and no other normative
doc was left contradicting the change.

DO NOT MERGE — gate status (REVISED)

  1. standards#284 (Phase 3c0 actionlint suppression) — SATISFIED. Merged
    2026-07-27T14:24Z. Every consumer lints its own workflows with pinned
    actionlint 1.7.12, which rejects the review caller's concurrency.queue
    key (concurrency: Add support for queue key rhysd/actionlint#654); without the distributed suppression, each
    caller sync PR would fail its consumer's required ci-status check.
    Verified live: all four managed targets' .github/actionlint.yaml
    suppress the message for claude-review.yml (dotfiles and provisioning by
    glob, github-iac and medley by explicit path).
  2. standards-sync App workflows: write (Phase 3a0) — SATISFIED. Verified
    live: gh api orgs/melodic-software/installations reports installation
    144867070 (melodic-standards-sync) holding
    {contents: write, issues: write, metadata: read, pull_requests: write, workflows: write}. Writing .github/workflows/ files in target repos needs
    that permission; without it every sync PR from this component would fail.
  3. ci(distribution): disarm fleet automerge for the Phase 3d rollout window #290 (automerge: false rollout window) — SATISFIED. Merged
    2026-07-29T13:05Z and merged into this branch, resolving the one conflict at
    the claude-code-plugins target.
  4. ci: re-pin the standards-sync engine at ac223bb #289 (re-pin the sync engine at ac223bb) — OPEN. ci(distribution): disarm fleet automerge for the Phase 3d rollout window #290's stated merge
    order is ci(distribution): disarm fleet automerge for the Phase 3d rollout window #290ci: re-pin the standards-sync engine at ac223bb #289feat(distribution): add sync-managed claude review-lane caller components #286. The App grant above is inert until the engine
    re-pin lands, because main still pins ci-workflows@0b45b9f, which
    predates the permission-workflows: write mint.

Beyond that ordering, the only failing check on this PR is
do-not-merge / do-not-merge — the intentional label gate, which the label
owner lifts.

Concurrency decision record

Shipped the 2e-documented shape from the reusables' headers: review caller =
workflow-level per-PR cancel group plus a separate job-level queue: max
repo-wide group; security caller = per-PR group with
cancel-in-progress: false and no queue. github-iac's live caller
deliberately omits caller-level concurrency, claiming (i) caller-level
cancel reintroduces skipped-actor cancellation and (ii) a group-name
collision with the reusable's job group. Inspection at v0.9.0:

  • Collision claim: disproven. The inner job group is
    claude-review-<PR>-<headSHA> (claude-review.yml:261 at v0.9.0); the
    caller groups are claude-review-<PR> (cancel) and
    claude-review-<owner/repo> (queue). No name equality, so the historical
    caller/inner deadlock — real when the inner group was
    claude-review-<pr-number> exactly; provisioning's caller comment
    documents the observed "deadlock was detected" error from that era —
    cannot recur. github-iac's and provisioning's comments describe a
    pre-v0.9.0 inner-group shape.
  • Skipped-actor cancellation: real but bounded, accepted. The caller
    workflow-level group does evaluate before any job if, so a skip-actor
    event on the same PR cancels an in-flight review. With no synchronize
    trigger the same-PR event surface is opened / ready_for_review /
    reopened — rare and human-driven. The reusable's 2e header documents
    this exact caller value as canonical and per-lane deliberate.

The component sources record both rationales inline; the 3c smoke exercises
this exact shape.

Paths-file seeding disposition

The manifest has no seed-once mechanism (schema v2: components are
unconditional source-to-dest maps), and .github/claude-security-paths is
repo-owned tuning that must NOT become managed bytes. Disposition:

  • Recorded the gap in the manifest comment and README section: a new
    adopter commits its starter list via a repo-local PR at adoption time.
  • claude-code-plugins migration ordering: MOOT. That repo is now
    locally-owned for both callers and receives no sync PR, so no migration
    ordering applies. (Its .github/claude-security-paths already exists —
    733 bytes, live — and its hand-written caller keeps its inline paths:
    list.)
  • Proposed mechanism if seeding is wanted later: a seed file class in
    schema v3 — materialized only when absent at the target, never
    reconciled — which preserves repo ownership after first sync.

Deviations / notes for the 3c smoke

  • skip-actors is not passed by the callers: the reusable's default at the
    pinned c136b27 is already the normalized self-trigger-ban list
    (dependabot[bot],claude[bot],melodic-ai[bot],melodic-standards-sync[bot],
    claude-review.yml:150); existing callers passed it only because the
    pre-v0.9.0 default was narrower. Ownership therefore moves from the caller
    to the reusable, and
    conventions/review/ai-review-bot-composition.md is reconciled to say so —
    it previously attributed the melodic-standards-sync[bot] exclusion to the
    caller's skip-actors input, wiring this component no longer has.
  • Runner fallback normalized to 'ubuntu-24.04' (dotfiles and github-iac
    currently use melodic-ubuntu-24.04-x64, which violates the runner-policy
    recovery contract in distribution/README.md and would queue forever on a
    public repo).
  • Selector-failure surfacing (class=runner marker) is a comment, not a
    TODO(#issue): no dedicated issue exists; the incident-aggregator
    acceptance test (ci-workflows#238) exercises it in Phase 4.
  • The 3c smoke must confirm the required-check shape when the security
    caller's selector job fails or skips (a caller-job skip is a new state for
    the security-review / security-review required context on
    claude-code-plugins; on that public repo the selector routes hosted-only,
    so the exposure is infra-failure only).
  • medley's paths-ignore tuning is an accepted loss (plan approval record
    item 10), noted at its manifest target entry.

Test plan

REVISED — as run at a4d7ee6:

  • npm run test:runner-policy — 242/242 pass, including four new gates (each
    proven non-vacuous by reintroducing its defect in a scratch copy): a
    selector-routed caller may not be managed for a public target; every
    caller component must audit clean for a private self-hosted consumer; a
    selector-routed caller is rejected outright on a public one; and every
    managed target of any caller must be one runner-policy admits (written as a
    property, so legitimate unparking via a private consumer passes).
  • npm run lint:runner-policy, npm run lint:md,
    bash distribution/sync-manifest.sh validate
    (Manifest valid: 34 components, 8 targets) all pass.
  • Scripted input-conformance proof, both directions, all four uses: read at
    the pinned SHA: 0 failures, 0 warnings. Negative control at v0.9.0
    reproduces the paths-file defect.
  • Governance simulation with the shipped policy.json: private self-hosted
    consumer passes clean; public consumer produces four
    public-self-hosted-routing findings. Negative control against the
    pre-fa2e7a5 policy produces eight findings, proving the new policy
    entries load-bearing.
  • Selector byte-identity claim confirmed by object hash: c136b27 and
    e77f0126 both resolve select-runner.yml to blob 6ba7d60c.
  • zizmor 1.26.1 over both components: no findings; totals identical to
    baseline. Extended pin-comment-convention scan: exit 0.
  • End-to-end regression test against each managed target's REAL live state —
    its .github/runner-policy.json and its full live workflow set fetched via
    the API, then the synced caller dropped in: all four (dotfiles, github-iac,
    medley, provisioning) report Runner policy passed. both before and after,
    so the caller introduces no finding at any target.
  • CI on the tip: 42 of 43 checks SUCCESS, including ci-status,
    Runner policy, distribution, actionlint, zizmor,
    concurrency-policy, and pin-comment-convention. The sole failure is
    do-not-merge / do-not-merge, which is the intentional label gate.

REVISED — after merging main (#290) and closing the five review threads:

  • Conflict with ci(distribution): disarm fleet automerge for the Phase 3d rollout window #290 resolved at the claude-code-plugins target, the one
    place both parents insert after - typos. Purely additive over each parent:
    git diff origin/main -- distribution/sync-manifest.yml | grep '^-' and the
    same against af85ae0 each emit only the --- header, zero deletion lines,
    so ci(distribution): disarm fleet automerge for the Phase 3d rollout window #290's comment above targets: and every component list survive.
  • yq: 8 of 8 targets carry automerge as a !!bool false; the count of
    targets whose value is not boolean false is 0. sync-manifest.sh matrix
    emits it as a JSON boolean for all 8 (a quoted string would be truthy).
  • Per-target managed / locally-owned lists byte-identical to af85ae0
    (95 entries); components block unchanged.
  • bash distribution/sync-manifest.sh validate
    Manifest valid: 34 components, 8 targets, exit 0.
  • bash distribution/sync-manifest.test.sh → 147 passed, 0 failed.
  • npm run test:runner-policy → 242/242 against the merged manifest;
    npm run lint:runner-policyRunner policy passed.
  • bash harness/shell/run-tests.sh components/claude-lanes/claude-lanes.test.sh components/actionlint/actionlint.test.sh → 2 files, 2 passed, 0 failed.
  • The lane-caller suite was mutation-tested rather than trusted for being
    green. Two independent ways it could have shrunk silently — the managed-list
    query returning nothing, and the per-component membership test ceasing to
    match — were each reintroduced in throwaway copies outside the worktree and
    both now surface as FAIL with a non-zero exit, where the second previously
    dropped four assertions and still exited 0. Per-target assertion counts are
    asserted, not assumed.
  • shellcheck (--rcfile .shellcheckrc -x), actionlint 1.7.12 over the repo,
    zizmor 1.26.1 over the repo plus both components (No findings to report),
    markdownlint, typos, and editorconfig-checker all clean on the changed set.

Original pre-revision plan:

  • bash distribution/sync-manifest.sh validate reported
    Manifest valid: 33 components, 8 targets.
  • yq eval -o=json distribution/sync-manifest.yml | node distribution/validate-sync-manifest.mjs
    passes.
  • sync-manifest.sh plan over all five affected targets shows exactly the
    intended additions (claude-review-caller in all five; the security caller
    in claude-code-plugins only; automerge unchanged).
  • actionlint 1.7.12 over both callers materialized at their destination
    layout WITH ci-workflows' approved queue-suppression config exits 0; the
    negative control without the config reports exactly the one expected
    concurrency.queue syntax-check finding on the review caller.
  • markdownlint plus lefthook pre-commit gates (typos, editorconfig,
    gitleaks, markdownlint) green on commit.

Related

  • Phase 3a of melodic-software/ci-workflows
    docs/topics/claude-review-lanes/PLAN.md (approval record items 1, 9, 10).
  • Gate 1: feat(distribution): add actionlint queue-suppression component for no-config consumers #284 (3c0 actionlint suppression).
  • Gate 2: Phase 3a0 sync-App workflows: write grant (org-owner action,
    lands via github-iac).
  • Canonical caller headers: ci-workflows claude-review.yml /
    claude-security-review.yml at v0.9.1
    (c136b27f404dd32ce3873f39a6f3443891d1c16e).
  • ci-workflows#282 (paths-file input), ci-workflows#278 (empty selector
    output — motivates the hosted fallback), ci-workflows#238 (Phase 4
    acceptance test covering selector-failure surfacing).

No linked issue.

🤖 Generated with Claude Code

…ents

Adds `claude-review-caller` and `claude-security-review-caller` managed
components sourcing thin callers for the ci-workflows reusable Claude review
lanes (dest `.github/workflows/claude-review.yml` /
`claude-security-review.yml`), pinned at the v0.9.0 release SHA
(cf666f672e0ec81cf876303840a3af1ffd4cb966). Caller shape derives from the
reusables' own canonical-caller headers: job ids `review` / `security-review`
(required-check name continuity), review triggers [opened, ready_for_review,
reopened], security triggers + `synchronize` with no workflow-level path
filtering, per-lane concurrency (per-PR cancel + repo-wide `queue: max` on
review; cancel false, no queue on security), governed select-runner
indirection with the hosted recovery fallback, `paths-file:
.github/claude-security-paths` on the security caller, and the one named
secret (never `secrets: inherit`).

Managed workflow callers are a recorded exception to the distribution
contract's workflow-caller exclusion — hand-written callers empirically
drifted (medley's missing `reopened` trigger, divergent skip-actors, pin
skew v0.6.1 <-> e295107) — so distribution/README.md's three exclusion
statements gain carve-outs pointing at a new authoritative section.

Targets are the repos that run each lane today: claude-review-caller in
claude-code-plugins, dotfiles, github-iac, medley, provisioning;
claude-security-review-caller in claude-code-plugins only. `.github` and
ci-runner stay exempt; knowledge-corpus and songwriting wait on the sync
App access grant; standards keeps its repo-local caller as manifest source.

Validation: sync-manifest.sh validate (33 components, 8 targets); JSON
Schema check via yq -o=json | validate-sync-manifest.mjs; plan over all five
affected targets; actionlint 1.7.12 with the approved queue-suppression
config exits 0 on both callers (without the config it reports exactly the
expected `concurrency.queue` syntax-check finding on the review caller).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 25s —— View job


Code Review

  • Load tools
  • Read REVIEW.md and CLAUDE.md for review criteria
  • Read changed files
  • Analyze components and manifest
  • Post review findings

Review: feat(distribution): add sync-managed claude review-lane caller components

The PR is well-structured and the reasoning is thoroughly documented. The concurrency decisions, permission grants, selector-failure guards, and exception justification are all sound. A few items worth resolving before merge:


🔴 Important — Workflow name: vs. ruleset required-check context

components/claude-lanes/claude-security-review.yml#L1

The component file declares name: claude-security-review, which means the GitHub check context it produces is claude-security-review / security-review. The PR body states the required-check on claude-code-plugins' ruleset is security-review / security-review, which implies the current live caller uses name: security-review as the workflow display name.

If the sync PR replaces that file with name: claude-security-review, the check context changes and the existing ruleset requirement (security-review / security-review) will never be satisfied — every PR on claude-code-plugins will be permanently wedged until the ruleset context is updated. This would be a Gate 3 that isn't called out in the "DO NOT MERGE" block.

Please verify: does the live caller at claude-code-plugins currently have name: security-review or name: claude-security-review? If it's the former, the ruleset must be updated atomically with the sync PR landing, and that coordination should be an explicit gate in the PR body.

The same question applies to the review caller — if existing targets have name: claude-review already, this is a non-issue; if they have name: review, the same wedge risk exists.


🟡 Nit — Migration ordering for paths-file seeding not surfaced in the README section

distribution/README.md#L248-L256

The new "Claude review-lane caller components" section correctly documents that an absent .github/claude-security-paths file fails open. However, it says "a new adopter commits its starter list in a repo-local PR alongside (or before) its caller materialization PR" — it doesn't surface the specific migration-ordering requirement for the existing claude-code-plugins adopter, whose inline paths: list must be committed to .github/claude-security-paths before (not just alongside) the caller sync PR merges. That ordering matters because the synced caller drops the inline list entirely: until the file exists, "fails open" isn't just a note, it's a silent loss of the tuned relevance gate.

The PR body covers this well, but the README section (which survives as the long-lived consumer-facing doc) doesn't make the ordering constraint as explicit. A one-sentence addition like "Existing callers that pass an inline paths: list must commit it to .github/claude-security-paths before the caller sync PR merges, not alongside — the synced caller passes only paths-file, and until the file exists the tuning is gone" would close that gap for future operators.


🟡 Nit — select-review / select-security job-name symmetry with name: Select runner

components/claude-lanes/claude-review.yml#L53, components/claude-lanes/claude-security-review.yml#L68

Both selector jobs carry name: Select runner. Since these live in separate workflows, there's no naming collision. Just confirming this is intentional — a reader landing on one file in isolation might wonder why the name doesn't reflect which lane it's selecting for. No change needed if this matches the canonical-caller headers in the reusable; noting it for awareness.


Confirmed correct — concurrency shape, permission ceiling, and selector-guard pattern

  • The workflow-level permissions: contents: read is correctly narrowed; both review and security-review jobs override it with explicit grants. Job-level permissions in GitHub Actions supersede the workflow-level default, so pull-requests: write and id-token: write are available to the called reusable.
  • if: ${{ !cancelled() && needs.<selector>.result == 'success' }} on both review jobs is the correct guard against skip propagation running a privileged job when the selector fails with no runner output.
  • cancel-in-progress: false on the security caller is the correct defensive choice for a required execution-evidence check.
  • The queue: max on the code-review job-level concurrency is correctly separated into its own block (GitHub doesn't allow cancel-in-progress and queue in the same block).
  • No workflow-level paths: on the security caller — correct; a required check that never triggers stays Pending and wedges PRs.
  • secrets: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} (no secrets: inherit) — correct least-privilege forwarding.
  • Three README carve-outs plus the new section cover the full cross-doc reconciliation surface; the cross-references are consistent.

@kyle-sexton
kyle-sexton marked this pull request as ready for review July 27, 2026 15:16
@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 5s —— View job


Code Review

  • Load tools
  • Read REVIEW.md and project guidelines
  • Read changed files
  • Analyze components and manifest
  • Post review findings

Review: feat(distribution): add sync-managed claude review-lane caller components

The PR is well-structured with thorough inline rationale. Most of the mechanics are correct and well-reasoned. One open question from the prior review remains unresolved and needs an answer before merge.


🔴 Important — Workflow name: vs. required-check context (unresolved from prior review)

components/claude-lanes/claude-security-review.yml#L1

The component declares name: claude-security-review. GitHub's check context is <workflow name> / <job id>, so the check this file produces is claude-security-review / security-review.

The PR body states the required-check on claude-code-plugins' ruleset is security-review / security-review — which would require the workflow name: to be security-review, not claude-security-review.

If the live caller at claude-code-plugins currently has name: security-review, replacing it with this component silently changes the check context and the ruleset required-check is never satisfied. Every PR on claude-code-plugins would be permanently wedged on the old context until the ruleset is updated. This is Gate 3 that isn't documented.

Two possible resolutions:

  1. The live caller already has name: claude-security-review (or the ruleset already matches that context) — in which case the PR body's parenthetical is a documentation error, not a code error, and just needs a correction.
  2. The live caller has name: security-review — in which case the component name needs to match, or the ruleset update must be a coordinated explicit gate.

Please confirm which state the live caller is in. The same question applies to the review caller (name: claude-review → context claude-review / review; if existing targets had name: review, same wedge risk).


🟡 Nit — README "alongside (or before)" understates the ordering constraint for existing adopters

distribution/README.md#L252

The new section reads: "a new adopter commits its starter list in a repo-local PR alongside (or before) its caller materialization PR." For a genuinely new adopter, "alongside" is fine — absent file fails open, meaning over-review, not under-review.

But for an existing adopter (specifically claude-code-plugins, which has a 26-entry inline paths: list in its current caller), the constraint is strictly "before," not "alongside." The synced caller drops the inline paths: entirely and only passes paths-file. Until the file exists, the tuned relevance gate is silently gone — a different operational outcome than the over-review of a new adopter.

The section doesn't distinguish the two cases. A one-sentence note for existing adopters would close the gap for future operators of this migration: "An existing adopter whose current caller passes an inline paths: list must commit it to .github/claude-security-paths before the caller sync PR merges — the synced caller replaces the inline list entirely, and until the file exists the tuning is lost."

Fix this →


Confirmed correct — no changes needed

The following were independently verified on this pass:

  • Permission ceiling: Workflow-level permissions: contents: read correctly constrains the default; both review and security-review job blocks explicitly override with pull-requests: write and id-token: write. Job-level permissions supersede the workflow default in GitHub Actions. ✓
  • Selector guard: if: ${{ !cancelled() && needs.<selector>.result == 'success' }} on both review jobs correctly prevents the fallback runner path from executing a privileged job when the selector hard-fails with no output. ✓
  • cancel-in-progress: false on security caller: Correct. A cancelled required-check run does not satisfy the ruleset context — it must complete. ✓
  • Separate concurrency blocks on review caller: queue: max in its own block is required; GitHub does not allow cancel-in-progress and queue in the same concurrency block. ✓
  • No workflow-level paths: on security caller: Correct per the inline comment. A required check whose workflow never triggers stays Pending and wedges every out-of-scope PR. ✓
  • secrets: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}: Least-privilege forwarding — correct. secrets: inherit is explicitly avoided. ✓
  • SHA pin + comment: Both callers pin at cf666f672e0ec81cf876303840a3af1ffd4cb966 # v0.9.0, which follows the pin-comment convention. ✓
  • permissions: {} on selector jobs: Correct; the selector needs no GITHUB_TOKEN scope. ✓
  • Fallback runner 'ubuntu-24.04' (not vars.CI_HOSTED_RUNNER): Correct per runner-policy recovery contract; vars.CI_HOSTED_RUNNER could be a self-hosted label and would queue forever on a public repo during infra failure. ✓
  • Manifest target assignments: claude-review-caller in five targets (claude-code-plugins, dotfiles, github-iac, medley, provisioning); claude-security-review-caller in claude-code-plugins only. Matches PR body. ✓
  • Component count: 33 components across 8 targets matches PR body test-plan validation output. ✓
  • Three README carve-outs + new section: Cross-doc reconciliation surface is complete; no other normative doc contradicts the change. ✓

@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: 029aea03f2

ℹ️ 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".

Comment thread components/claude-lanes/claude-review.yml Outdated
Comment thread distribution/sync-manifest.yml Outdated
Comment thread components/claude-lanes/claude-review.yml Outdated
Comment thread distribution/sync-manifest.yml
@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 6m 2s —— View job


Code Review

  • Load tools
  • Read REVIEW.md and project guidelines
  • Read changed files (PR diff against origin/main)
  • Verify prior open findings + check for new issues from merge
  • Post review findings

Review: feat(distribution): add sync-managed claude review-lane caller components

This pass reviews the synchronize event (the merge of origin/main into the branch). The feature content is unchanged since the prior two reviews. Two prior findings remain open; the merge from main resolves Gate 1 and surfaces a new blocking issue.


Gate 1 status: RESOLVED

The merge of origin/main brought in .github/actionlint.yaml with the approved concurrency.queue suppression (and the corresponding components/actionlint/fixtures/good/queue-concurrency.yml positive-control fixture). Gate 1 is cleared — actionlint 1.7.12 running at consumers will accept the queue: max key in the synced review caller once the suppression component is also materialized.

Gate 2 (sync App workflows: write grant) remains pending — org-owner action, no code change here.


🔴 Important — v0.9.0 SHA not registered in runner-policy

components/runner-policy/policy.json

Both callers pin all three reusable references at cf666f672e0ec81cf876303840a3af1ffd4cb966 (v0.9.0). None of those three are registered in policy.json:

  • select-runner.yml@cf666f... is absent from both approvedSelectorReferences and approvedSelectorReferencesByRepositoryOwner.
  • claude-review.yml@cf666f... is absent from approvedReusableWorkflowContracts.
  • claude-security-review.yml@cf666f... is absent from approvedReusableWorkflowContracts.

The runner-policy gate (runner-policy.mjs) runs on every consumer's materialization PR. Without the approved entries, the gate will reject both callers and every caller sync PR across all five targets will fail its required ci-status check before it can merge — a fleet-wide rollout wedge that isn't listed in the "DO NOT MERGE" block.

The v0.9.0 contracts also need to reflect the changed input shapes:

  • claude-review.yml@cf666f...: runner input only (no skip-actors, consistent with the existing runner-input routing entries at df54d0e... and 4dbb0df...).
  • claude-security-review.yml@cf666f...: runner and paths-file inputs (v0.9.0 adds paths-file; the existing entries at e295107... and 66073e... list paths, which is the pre-v0.9.0 shape).

This is a code gap in the PR — components/runner-policy/policy.json needs new entries for all three v0.9.0 references before or alongside the component manifest additions.

Fix this →


🔴 Important — Workflow name: vs. required-check context (third pass, still unresolved)

components/claude-lanes/claude-security-review.yml#L1

Unchanged since the prior two reviews. The component file declares name: claude-security-review, which means GitHub's check context is claude-security-review / security-review. The PR body states the required-check on claude-code-plugins' ruleset is security-review / security-review — which would require name: security-review.

If the live caller at claude-code-plugins has name: security-review, replacing it with this component silently changes the check context and the existing required-check context is never satisfied. This is Gate 3 that isn't in the "DO NOT MERGE" block.

Two pass resolutions, same as before: (1) the live caller and/or ruleset already use claude-security-review — a documentation error, not a code error; (2) the live caller uses security-review — the component name or the ruleset must be updated as a coordinated explicit gate. Please confirm which state the live caller is in.

The same question applies to the review caller (name: claude-review → context claude-review / review).


🟡 Nit — README ordering constraint for existing adopters still understated

distribution/README.md#L254

Unchanged since the prior two reviews. The section reads "a new adopter commits its starter list in a repo-local PR alongside (or before) its caller materialization PR." For claude-code-plugins (an existing adopter with a 26-entry inline paths: list), "alongside" is wrong — the synced caller drops the inline paths: entirely and only passes paths-file, so until the file exists the tuned relevance gate is gone. The constraint for existing adopters is strictly "before."

Fix this →


🟡 Nit — ai-review-bot-composition.md skip-actors statement is stale

conventions/review/ai-review-bot-composition.md#L9

The governed-class description states the melodic-standards-sync[bot] exclusion is wired "via the caller's skip-actors input." The new managed callers deliberately omit skip-actors — the v0.9.0 reusable's default already includes the normalized self-trigger-ban list, so the PR body notes the input was only passed before because the pre-v0.9.0 default was narrower. The file is accurate about the outcome (the exclusion still holds), but incorrect about how it's implemented after this change. This is the kind of implementation-wiring detail that drifts if left unreferenced. A phrase like "via the reusable's built-in default or the caller's skip-actors input" or just removing the implementation detail from the normative statement would keep the doc accurate post-merge.

Fix this →


Summary of open items

# Severity Finding Status
1 🔴 v0.9.0 SHA not in runner-policy/policy.json (3 entries needed) New this pass
2 🔴 Workflow name: vs required-check context — needs confirmation Open 3rd pass
3 🟡 README ordering "alongside (or before)" understates existing-adopter constraint Open 3rd pass
4 🟡 ai-review-bot-composition.md skip-actors wiring statement is stale New this pass

Gate 1 (actionlint suppression) resolved by the merge from main. Gate 2 (App workflows: write) still pending external action.

@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: 3e10a4062f

ℹ️ 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".

Comment thread distribution/sync-manifest.yml
@kyle-sexton kyle-sexton added the do-not-merge Hard merge gate: do not merge while applied. label Jul 29, 2026
kyle-sexton and others added 2 commits July 28, 2026 23:09
v0.9.0 predates the security lane's paths-file input, which these caller
components pass. A caller passing an input the pinned reusable does not
declare hard-fails the workflow_call, leaving the required security check
Pending forever.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013pLW2dybov9xvTFtx48Ueb
Work-in-progress output from an in-flight worker that died with its
session before reporting or being verified. Committed as-is to make it
durable and diffable for a fresh-context verifier; not yet reviewed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013pLW2dybov9xvTFtx48Ueb
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

The caller components resolve the runner through the governed select-runner
indirection. runner-policy admits that selector only for a private
self-hosted consumer (`routingEnabled` is `visibility === "private" &&
selfHostedCi`), and the `!routingEnabled` branch consults neither
`exceptions` nor `localRoutingGrants` — a public target has no configuration
escape.

melodic-software/claude-code-plugins is the org's one public caller target,
and it carried BOTH components, including the security caller whose
`security-review / security-review` context is a required check there.
Syncing them in would have failed that repo's own runner-policy lane with
four findings, turning `ci-status` red and wedging its PR pipeline. Verified
by auditing each component against a public consumer config using the shipped
policy.json; the same audit passes clean for a private self-hosted consumer.

claude-code-plugins moves to `locally-owned` for both callers — the manifest's
documented customization seam, and the same disposition actionlint already
carries for consumers whose local content is load-bearing. Its existing
hosted-only callers pass runner-policy today and are unchanged by this branch.

Three tests hold the constraint so it cannot regress silently: a
selector-routed caller component may not be `managed` for a public target,
every caller component must audit clean for a private self-hosted consumer,
and a selector-routed caller is expected to be rejected outright on a public
one. Standards' own `lint:runner-policy` scans `.github/workflows`, not
`components/`, so nothing previously validated these components at all.

Also records the two lane contracts registered at c136b27 (both additive, no
secret or routing change) and renames PATHS_FILE_SELECTOR_SHA to
PATHS_FILE_LANE_SHA: the revision changes the security reusable, and its
selector is byte-identical to e77f0126.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013pLW2dybov9xvTFtx48Ueb
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

Fixes an independent verifier found against 28b335c.

`claude-security-review-caller` has NO managed target and cannot gain one as
written: both repos running a security lane today (claude-code-plugins,
ci-workflows) are public, and a selector-routed caller is rejected outright on
a public repository. 28b335c's "managed for PRIVATE targets only" read as
correctly scoped when one component in fact reaches nobody. Both the manifest
and distribution/README.md now say PARKED, why it is retained rather than
deleted, and what unparks it. The consequence of the claude-code-plugins
`locally-owned` disposition — the one repo whose ruleset requires
`security-review / security-review` now re-pins by hand — is stated where a
reader will meet it rather than left implicit.

The component drops `synchronize`, which all four managed targets' live callers
carry. That is the plan's deliberate cadence cut, not drift, but it is the
largest behavior change the component makes and the manifest documented only
medley's lesser `paths-ignore` loss. Recorded as an accepted loss for all
targets, alongside why the security lane keeps the trigger.

The pin comment still claimed v0.9.0 two commits after b94b204 re-pinned to
v0.9.1 — corrected, with the reason the older pin cannot serve the security
caller.

The runner-policy README said the c136b27 contracts change no secret or
routing surface. True as stated, but it glossed the one privilege delta in the
diff: the security reusable's `changes` job gained `contents: read` to read the
repo-owned paths file. Inbound-safe, now stated rather than omitted.

Also closes the coverage gap 28b335c only half-named. components/claude-lanes/
holds workflow bytes, but every workflow-shaped lane in this repo discovers
files under .github/workflows — so actionlint, zizmor, concurrency-policy, and
pin-comment-convention were ALL blind to them, not just lint:runner-policy. The
pin-comment lane takes an explicit file list, so it now scans the components
too; verified non-vacuous (a mismatched SHA claim fails it). zizmor was run
against both components directly and reports no findings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013pLW2dybov9xvTFtx48Ueb
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

Both caller components fail standards' own `concurrency-policy` component on
three rules: each lane-scoped group name is not `${{ github.workflow }}`-
prefixed, and the security caller's `cancel-in-progress: false` is not the
literal `true` that component requires.

Nothing fails today — no target repo runs `concurrency-policy`, and standards'
own lane scans `.github/workflows`, not `components/`. But both values are
deliberate and load-bearing: the lane-scoped group name is what keeps a caller
group distinct from the reusable's inner per-(PR,head) group, and cancelling a
REQUIRED check is not a skip and does not read success. A future adopter of
both components would meet the failure with no record of which side is right,
and normalizing the callers to satisfy the linter is the wrong resolution.

Recorded at the component sources, where the conflicting values already carry
their rationale, rather than in a doc a reader would have to know to look for.
Comment-only; the callers' bytes are otherwise unchanged and conformance,
runner-policy, pin-comment, and zizmor all re-verified clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013pLW2dybov9xvTFtx48Ueb
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude encountered an error after 1s —— View job


I'll analyze this and get back to you.

The pin-comment lane was pointed at components/claude-lanes/ but zizmor was
not, leaving the fix half-applied: zizmor discovers workflow definitions by
their `.github/workflows` location, so `paths: .` audits this repo's own
workflows and skips the component bytes entirely — verified by running it and
watching the two files never appear in the completed list.

`paths` is whitespace-separated, so naming both components extends the same
lane rather than adding one. Verified: both files now appear in the audit,
and the finding total, severity breakdown, and exit code are identical to
baseline (45 findings, 1 informational, 0 high) — the components introduce
nothing, and the lane still gates on `fail-on-severity: high`.

Leaves actionlint and concurrency-policy still blind to these files. Both are
deliberate and now recorded: actionlint 1.7.12 rejects the review caller's
`concurrency.queue` key (rhysd/actionlint#654), which is exactly what the
fleet suppression distributed in Phase 3c0 exists to handle at the consumers
that lint the synced file; concurrency-policy's conflict is documented at the
component sources.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013pLW2dybov9xvTFtx48Ueb
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

The parked security caller was doc-only. Nothing failed if a later change
quietly gave it a managed target, and the three existing gates all pass on a
component that reaches nobody — none of them asserts anything about the
targets a caller DOES have.

Asserts the property rather than the current count: every managed target of
any caller component must be a target runner-policy actually admits. Unparking
legitimately, by adding a private consumer, passes; adding a public one fails.
That keeps the guard useful after the parked state ends instead of pinning
today's arrangement.

Verified non-vacuous: adding claude-security-review-caller back to the public
claude-code-plugins target fails this test and the existing public-target gate,
2 of 242.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013pLW2dybov9xvTFtx48Ueb
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude encountered an error after 1s —— View job


I'll analyze this and get back to you.

The verifier found the gates had the blind spot that mattered most. Both keyed
public-ness off a hardcoded one-element `PUBLIC_SYNC_TARGETS` list, so any
public repo NOT named in it was silently treated as private and admitted.
Adding `melodic-software/ci-runner` — public, and already an exempt repo the
plan discusses — as a managed target of the review caller passed all 242 tests
while describing exactly the fleet-wedging defect this branch exists to
prevent.

The allowlist inverted the safe default: it enumerated the danger and assumed
safety for everything else. Replaced with a visibility map covering all 11
non-archived org repos, looked up FAIL-CLOSED — a target absent from the map
asserts rather than defaulting to private. The public-target gate now iterates
the manifest's own targets instead of the list, so it sees every target the
manifest actually declares.

Checked in rather than read from the API because this suite runs offline and
hermetically. Verified against live GitHub at authoring time: the map matches
`gh repo list` exactly for all 11 repos, both directions, no missing/extra/
mismatched entry. A visibility flip therefore surfaces as a failing test rather
than a silent admission.

Probes: ci-runner as a managed target now fails 2 of 242; a target absent from
the map fails with "add it to TARGET_VISIBILITY (public until proven private)".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013pLW2dybov9xvTFtx48Ueb
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

kyle-sexton added a commit that referenced this pull request Jul 29, 2026
…dow (#290)

## Summary

Sets `automerge: false` on every one of the eight targets in
`distribution/sync-manifest.yml`, plus one comment above the `targets:`
block
recording that the window is temporary.

**This is a temporary rollout-window gate. It MUST be reverted after
Phase 3d
completes.** Merging it is a prerequisite for #289 and #286.

`automerge` is opt-out policy-as-data —
`distribution/sync-manifest.sh:428-431`
treats an absent key as `true` — so a target that omits the key keeps
the armed
default. Every target therefore has to carry the key explicitly for the
window
to hold. No target opts out today.

Why the window is needed: the sync engine at `ci-workflows@ac223bb` adds
a step
that arms squash auto-merge at PR creation, gated on
`steps.cpr.outputs.pull-request-operation == 'created' &&
matrix.automerge`
(`standards-sync.yml:475`). The same commit adds `permission-workflows:
write`
to the per-target token mint, and this repository's `base` ruleset
carries
`required_approving_review_count: 0`. Composed, one merge here would
write
workflow content into all eight targets and self-merge with no human
checkpoint. Disarming `automerge` restores the two-checkpoint shape: a
merge
here proposes the sync PR, and a human merges it in each target.

Only the `automerge` key is added. No target's `managed` or
`locally-owned`
component list changes.

## Test plan

- `bash distribution/sync-manifest.sh validate` → `Manifest valid: 32
  components, 8 targets`, exit 0 (both the bare CI form and the explicit
  `--source-root . --manifest distribution/sync-manifest.yml` form).
- `bash distribution/sync-manifest.test.sh` → engine suite green.
- `bash distribution/sync-manifest.sh matrix` → all eight entries emit
`"automerge":false` as a JSON boolean (verified with `jq
'.automerge|type'`
== `boolean`), so `fromJson` yields a real boolean and
`matrix.automerge`
  evaluates falsy. A quoted `"false"` string would have been truthy.
- `yq '.targets[].automerge | tag'` → `!!bool` for all eight;
  `sync-manifest.sh:433` requires that tag.
- `grep -c "automerge: false" distribution/sync-manifest.yml` → `8`,
matching
the target count. The comment deliberately avoids the literal string so
the
  post-restore sanity check (`grep -c ... == 0`) stays meaningful.

## Related

No linked issue — this is a pre-step of the claude-review-lanes rollout
plan
(`ci-workflows` `docs/topics/claude-review-lanes/PLAN.md`, Phase 3d
pre-step i),
tracked there rather than as a standalone issue.

Merge order: this PR first, then #289 (`ci: re-pin the standards-sync
engine at
ac223bb`), then #286 (`feat(distribution): add sync-managed claude
review-lane
caller components`). Merging this PR is itself a push to `main` and
fires a real
sync run; that run is safe because `main` still pins the sync engine at
`0b45b9f`, which has no arming step at all.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit to melodic-software/ci-workflows that referenced this pull request Jul 29, 2026
## Summary

Records Phase 5 close-out **evidence** in
`docs/topics/claude-review-lanes/PLAN.md`. One file, additive only — no
plan requirement is restated, reinterpreted, or amended, and the phase
tag stays `[TODO]` because three of the five bullets are not fully
closed.

Of Phase 5's five bullets: two are fully closed with evidence (trailer
note, do-not-break sweep), one is half-closed (equivalence — the medley
half done, the blob-hash half blocked), and two remain outstanding
(README/CLAUDE.md, and "tags advanced", which cannot close while the
others are open).

**The medley REVIEW.md equivalence result inverts the bullet's own
framing**, which is the substantive finding here. medley's `REVIEW.md`
is not a drifted copy of the managed source — it is an independently
authored 188-line document with its own slice taxonomy, first added in
`4a1d61a6`, never synced from standards. So the absent lane-scope
headings are not the finding. The restructure's mutual-exclusion rule is
*conditional* on a named file (standards `REVIEW.md:66-73`): it
suppresses security findings only on a repository that runs the security
lane, and otherwise directs "report security findings under this lane
too". medley has no `claude-security-review.yml` and no caller routing
to that reusable, so it sits in the else branch — folding security in is
correct, and adopting the mutual-exclusion text would have produced the
2f-addendum failure mode of **no lane reporting security findings**.

The real drift is the one always-check that is *not* security-gated:
multi-location atomicity (standards `REVIEW.md:82-86`) has no
universal-checklist line in medley, and its nearest coverage self-scopes
to "EF Core persistence in the modular monolith" while medley is
polyglot. Filed as melodic-software/medley#1671. medley's file was
**not** edited — `review-instructions` is `locally-owned` for medley,
which is the customization seam working as intended.

The standards repo-local caller blob-hash equivalence is recorded as
**NOT DONE and blocked** on standards#286 rather than quietly omitted —
a sanity block that hides an unrun check is worse than one that names
it.

## Test plan

No executable surface changes; this is a documentation-only diff to a
planning artifact. Verification was of the *claims recorded*, not of
code:

- Every citation in the evidence block was read from the live tree:
standards `REVIEW.md:66-73` and `:82-86`; medley `REVIEW.md:65`, `:66`,
`:67`, `:96`; `review/security.md:7`, `:11`, `:18`;
`review/logging.md:12`; `review/transactions-and-consistency.md` line 3
self-scope; `claude-review.yml:751`. Reading is not the same as
verifying, and the third verifier round proved it: an earlier revision
of this block mislabelled the security-lane always-checks as five when
there are four (`abeccc6`'s own commit message says four), and inverted
the `:65`/`:66` → `security.md` mapping. Both are corrected here.
- medley's lane inventory confirmed empirically: no
`claude-security-review.yml`, and `grep -rln "security-review"
.github/workflows/` returns nothing.
- Phase 1 sanity greps re-run against medley's copy: `grep -c "Do not
report"` == 0, `grep -c "Cap the nits"` == 0.
- Issue states re-read via `gh` at time of writing: #255/#258/#259 OPEN
with `updatedAt` unchanged at 2026-07-27T02:11Z; standards#286 OPEN,
`mergedAt: null`.
- All version claims verified against `git rev-list`: v0.6.1=`90f1c549`,
v0.8.0=`e77f0126`, v0.9.0=`cf666f67`, v0.9.1=`c136b27f`.
- lefthook pre-commit gates passed on commit.

**Verification honesty note.** Three fresh-context verifier rounds ran;
all three returned REJECT, and each caught a defect introduced while
fixing the previous one:

- Round 1 — deployed-state overreach on the #151 comment; false "uniform
across all three lane workflows" claim on #257.
- Round 2 — `create-github-app-token` call-site undercount (one
asserted, six actual), introduced while fixing round 1.
- Round 3 — on #257, an absolute "no other pin was assessed" disclaimer
contradicted by the comment's own coverage claims, and a substantively
false one: medley's `claude-code-action` pin was described as having no
Dependabot path when **medley#1668 was already open bumping it 1.0.174 →
1.0.180**, created ~15 hours before the comment. On this PR, the
four-vs-five security-check mislabel and the inverted citation mapping
above.

All are corrected in place with the error named rather than edited away.
This is recorded because the pattern — each fix round introducing a new
error — is the substantive risk in this work, not an incidental detail.

## Related

No linked issue — Phase 5 is tracked as a phase of the
claude-review-lanes Brief in `docs/topics/claude-review-lanes/PLAN.md`,
not as a ci-workflows issue.

Referenced, not closed by this PR:

- melodic-software/medley#1671 — the atomicity drift this evidence
records
- #256, #151, #257 — carry the record-only comments this block cites
- #285 — the remaining Phase 5 README/CLAUDE.md bullet
- melodic-software/standards#286 — blocks the outstanding blob-hash
equivalence

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
kyle-sexton and others added 3 commits July 29, 2026 09:58
Resolves the one conflict in distribution/sync-manifest.yml, at the
melodic-software/claude-code-plugins target where both parents insert after
`- typos`: main's #290 adds `automerge: false`, this branch adds the
`locally-owned` block for the two lane callers. Both survive, with
`automerge: false` last to match the key order #290 uses on every target that
already carries a `locally-owned` list.

Purely additive over each parent — `git diff <parent> -- distribution/sync-manifest.yml`
emits no deletion lines against either origin/main or af85ae0, so #290's
comment above the `targets:` block is preserved along with every component
list.

Verified after resolution:
- yq: 8 of 8 targets carry `automerge` as a `!!bool` false; the count of
  targets whose value is not boolean false is 0.
- `sync-manifest.sh matrix` emits `automerge` as a JSON boolean for all 8
  (a quoted string would be truthy in a GitHub expression).
- Per-target managed/locally-owned lists are byte-identical to af85ae0
  (95 entries), and the components block is unchanged.
- `bash distribution/sync-manifest.sh validate` -> `Manifest valid: 34
  components, 8 targets`, exit 0.
- `bash distribution/sync-manifest.test.sh` -> 147 passed, 0 failed.
- `npm run test:runner-policy` -> 242/242 against the merged manifest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The governed-class criterion said `melodic-standards-sync[bot]` is excluded
"via the caller's `skip-actors` input". The sync-managed caller this PR adds
passes only `runner`, so that sentence described wiring the caller no longer
has.

The exclusion itself is intact — ownership moved, it did not disappear. Read
at the pinned SHA rather than main, ci-workflows `claude-review.yml` at
`c136b27` declares:

  skip-actors:
    default: dependabot[bot],claude[bot],melodic-ai[bot],melodic-standards-sync[bot]

So this is a documentation defect, not a behavior regression. The criterion now
attributes the exclusion to the reusable's default and records what overriding
the input costs: a caller that passes it owns the entire list, self-trigger ban
included.

Cross-document reconciliation left the other two `skip-actors` references
unchanged, deliberately. distribution/README.md cites divergent skip-actors
lists as historical evidence of caller drift — a claim about hand-written
callers, still true. components/runner-policy/README.md describes the earlier
contract entries, which do permit the input and are retained for consumers
mid-rollout; the new `c136b27` contract tightens `allowedInputs` to
`["runner"]`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The lane callers are workflow bytes, but every workflow-shaped lane in this
repository discovers files under .github/workflows — so the repo's actionlint
job never saw them. pin-comment-convention and zizmor were already extended to
name them explicitly; actionlint could not be, because the file only takes its
real form at a consumer's destination path.

components/claude-lanes/claude-lanes.test.sh runs the entrypoint a consumer
runs instead of linting the component in place: sync-manifest.sh apply into a
scratch checkout carrying the target's origin identity, then actionlint over
the result. Wired into the actionlint job, which already installs the pinned
1.7.12 engine; that job gains the same pinned yq install the distribution and
pin-comment-convention jobs carry, because apply needs it.

Twenty-four assertions, all derived from the manifest rather than hardcoded so
the parked security caller gaining a target does not require editing a count:

- every managed target materializes, receives the file at its mapped
  destination, and lints clean (dotfiles, github-iac, medley, provisioning);
- every component's bytes also lint standalone at their destination path, so
  claude-security-review-caller — parked, no managed target, unreachable from
  the target loop — is covered rather than shipped unlinted;
- a control run with the suppression config removed still reports the
  concurrency.queue message, so the suite cannot go quietly vacuous when
  rhysd/actionlint#654 ships upstream.

A target owning actionlint locally receives no config from the manifest, so
the canonical one stands in for the local equivalent it must carry. What this
repository can own is that the shipped bytes lint clean under a conforming
config; medley and github-iac each scope their own ignore to
.github/workflows/claude-review.yml, the exact destination the manifest maps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 6s —— View job


Code Review (Pass 4)

  • Load tools
  • Read REVIEW.md and project guidelines
  • Read changed files at HEAD (c674496)
  • Verify prior open findings are addressed
  • Check new commits for new issues
  • Post review findings

Review: feat(distribution): add sync-managed claude review-lane caller components

Four commits landed since Pass 3. All four prior open findings are resolved. No new blocking issues.


Prior findings — all resolved

# Prior severity Finding Resolution
1 🔴 v0.9.0 SHA missing from runner-policy/policy.json Resolved. Callers re-pinned to v0.9.1 (c136b27f). All three v0.9.1 entries are present in policy.json: select-runner.yml@c136b27f in approvedSelectorReferencesByRepositoryOwner, claude-review.yml@c136b27f with allowedInputs: ["runner"], claude-security-review.yml@c136b27f with allowedInputs: ["runner", "paths-file"]. Inputs match what the callers pass.
2 🔴 Workflow name: vs required-check context Resolved by scope change. claude-code-plugins (the only repo with a security-review / security-review ruleset requirement) is now locally-owned for both callers and keeps its hand-written caller. The security caller is parked with no managed target. No sync PR will touch claude-code-plugins.
3 🟡 README "alongside (or before)" understates existing-adopter constraint Resolved by scope change. No existing adopter receives a sync PR — the constraint was specific to claude-code-plugins, which is now locally-owned. "Alongside (or before)" is correct for a new adopter where an absent file fails open (over-review, not under-review).
4 🟡 ai-review-bot-composition.md skip-actors wiring statement stale Resolved by 1ec21e3. The file now correctly attributes the exclusion to "the reusable lane's own skip-actors default, which the sync-managed caller inherits rather than overriding."

New commits — verified correct

af85ae0 test(runner-policy): fail closed on unrecorded target visibility

Adds TARGET_VISIBILITY map and four new tests at runner-policy.test.mjs#L7941.

  • The fail-closed design is correct: isPublicTarget() calls assert.ok(visibility, "... add it to TARGET_VISIBILITY") — a target added to the manifest without a corresponding TARGET_VISIBILITY entry fails the test immediately with a diagnostic, rather than silently passing. ✓
  • Tests call auditRepository directly (not the audit() fixture wrapper), so they exercise the real policy.json rather than the synthetic fixture policy. ✓
  • consumerCarrying pushes into temporaryRoots for cleanup — consistent with the existing cleanup pattern. ✓
  • Property-based assertion on "every managed target admits its caller" means a future legitimate unpark (adding a private target) passes automatically, while adding a public one fails — exactly the right invariant. ✓
  • PATHS_FILE_LANE_SHA is added to the "production selector allowlist contains only independently reviewed commits" allowlist, which verifies the v0.9.1 SHA is in the approved selector set. ✓

1ec21e3 docs(review): attribute the sync-bot exclusion to the reusable lane — Correct. See finding #4 above.

eee0d9c Merge branch 'main' into feat/claude-lane-caller-components — The merge brings in the Gate 1 content (actionlint suppression from #284). No conflicts or regressions from the merge.

c674496 test(claude-lanes): lint the caller components at the materialized shape

Adds claude-lanes.test.sh at components/claude-lanes/claude-lanes.test.sh. Several properties verified:

  • Derives lanes from manifest, not hardcoded — the test reads component/target information from sync-manifest.yml dynamically, so a new lane caller or new managed target is covered without editing the test. The comment explicitly notes that the security caller has no managed target today and a fixed count would require edits. ✓
  • Lints parked bytes too — the first loop (lines 64–75) lints every lane caller at its destination path regardless of whether any target manages it today. This ensures the parked security caller's bytes remain lintable even while it has no managed target. ✓
  • Materializes via the real sync-manifest.sh apply engine — the second loop materializes each managed target through the distribution engine, then runs actionlint over the result. The consumer stand-in correctly sets a remote to bypass apply's origin check. ✓
  • Control case is load-bearing — removing the config on one consumer and asserting the specific queue: max message appears (lines 107–114) proves the suppression is genuinely needed. ✓
  • CI wiring — the actionlint job gains the yq install step (needed by the distribution engine) and the Test claude lane caller components step; zizmor now names both component files explicitly alongside .; pin-comment-convention scan is extended to components/claude-lanes/*.yml. The PR body's claim that the finding total and exit code are identical to baseline is consistent with the callers having no zizmor findings. ✓

Confirmed correct — unchanged from prior passes

  • SHA pin + comment format at v0.9.1 on all three uses: lines (select-runner.yml, claude-review.yml, claude-security-review.yml). ✓
  • permissions: {} on both selector jobs. ✓
  • if: ${{ !cancelled() && needs.<selector>.result == 'success' }} guard on both review jobs. ✓
  • Separate concurrency blocks on the review caller (cancel-in-progress and queue: in separate blocks). ✓
  • cancel-in-progress: false on the security caller. ✓
  • No workflow-level paths: on the security caller. ✓
  • secrets: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} (no secrets: inherit). ✓
  • Fallback runner 'ubuntu-24.04' (quoted, never vars.CI_HOSTED_RUNNER). ✓
  • Gate 1 clearedaf85ae0 and the merge bring the actionlint suppression in.
  • Gate 2 (sync App workflows: write grant) still pending external org-owner action.

…pping

The per-target destination check queried the manifest inside the loop and
skipped on a failed membership test. Skipping is correct for a component the
target does not manage — most targets manage only one caller — but the same
`|| continue` swallowed the case where the query itself returned nothing,
which silently dropped every destination assertion and still reported green.
An independent verifier proved the channel live: breaking the yq path in a
scratch copy took the suite from 24 assertions to 20, exit 0 both times.

The list is now read once per target and asserted non-empty before membership
is tested, so an empty result fails rather than falls through. Probed
directly: the real path yields 21 entries and passes; `.managedX[]` yields 0
and fails the assertion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 39s —— View job


Code Review (Pass 5)

  • Load tools
  • Read REVIEW.md and project guidelines
  • Read the new commit (d0ef74b) and changed files
  • Verify pass 4 findings still hold
  • Post review findings

Review: feat(distribution): add sync-managed claude review-lane caller components

One commit landed since Pass 4. All pass 4 findings remain resolved. No new blocking issues.


Pass 4 findings — still resolved

All four findings from Pass 4 were addressed before that review posted. This commit does not touch policy.json, sync-manifest.yml, ai-review-bot-composition.md, or distribution/README.md, so their resolutions are stable.


New commit d0ef74b — verified correct

test(claude-lanes): fail on an unreadable managed list instead of skipping

components/claude-lanes/claude-lanes.test.sh#L94-L99

The vacuousness hazard was real. The original inner loop queried the manifest on each iteration and the || continue correctly skipped non-managed components — but it also swallowed a failed or empty yq result, silently dropping all four assert_file_exists calls per target while the suite reported green. The commit message's proof is reproducible: a bad yq path (managedX instead of managed) drops 4 assertions without changing the exit code.

The fix is correct on all relevant axes:

  • Read-once and fail-empty: mapfile -t managed < <(yq -r ".targets.\"$target\".managed[]" "$manifest") captures the list once; assert_nonzero "$target has a readable managed list" "${#managed[@]}" catches the empty case before any membership test runs. This is the same assert_nonzero-as-count-check pattern already established at lines 49 and 57.
  • Membership test unchanged in semantics: printf '%s\n' "${managed[@]}" | grep -qxF "$component" || continue is a faithful rewrite of the original yq | grep pipeline; -F (fixed string) and -x (exact match) eliminate any regex concerns.
  • set -uo pipefail compatibility: mapfile always initializes the array (avoiding the set -u unset-variable trap even when yq returns nothing). The || continue on the grep pipeline correctly handles the non-managed case without becoming an unhandled pipeline failure.
  • No interaction with the control case: The bottom-of-test control run (lines 114–121) uses lane_targets[0], which is populated before this function and unaffected.
  • Alignment with Pass 3 and Pass 4 coverage claims: Pass 4 confirmed the test "derives lanes from manifest, not hardcoded" and "materializes via the real sync-manifest.sh apply engine." Those properties still hold; this commit tightens the assertion density of the same loop rather than changing its structure.

No issues found.


Overall status

Gate Status
Gate 1 — actionlint suppression (#284) ✅ Resolved (merged 2026-07-27)
Gate 2 — sync App workflows: write ⏳ Pending external org-owner action
Gate 3 — workflow name: / ruleset context ✅ Resolved by scope change (claude-code-plugins → locally-owned)
Gate 4 — #289 re-pin sync engine at ac223bb ⏳ Still open per PR body; #290#289#286 ordering

The only failing check is do-not-merge / do-not-merge — the intentional label gate.

d0ef74b closed one way this suite could shrink silently. An independent
verifier found a second with the same signature and a reachable trigger: when
the per-component membership test stopped matching, all four destination
assertions vanished and the suite still exited 0 (24 PASS, 0 FAIL).

The trigger was an asymmetry between the two membership tests. Target
admission matched by SUBSTRING (`grep -F` over `\t<component>`), while the
per-target test matched the WHOLE LINE. A component named as an extension of
an existing one — `claude-review-caller-v2` — is admitted by the first and
rejected by the second, so its target entered the loop and asserted nothing.
Demonstrated: the substring form admits `melodic-software/future` for that
component; exact lookup does not. Not live today, and the manifest's own
unparking plan makes such a name plausible.

Two changes. Both membership tests are now exact associative-array lookups, so
the asymmetry cannot recur. And each target asserts that it actually RAN a
destination assertion, which is the invariant that fails closed regardless of
why the count reached zero — a broken query, a renamed component, or a
membership test that quietly stops matching.

Mutation-tested, three throwaway copies outside the worktree:

  intact                                30 PASS   0 FAIL   exit 0
  membership never matches              22 PASS   4 FAIL   exit 1
  managed-list query returns nothing    22 PASS   4 FAIL   exit 1

Both mutants now name the failing target instead of vanishing. Also asserts
the standalone loop's copy landed, rather than relying on actionlint's exit 3
for an empty workflows directory to catch it incidentally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 17s —— View job


Code Review (Pass 6)

  • Load tools
  • Read REVIEW.md and project guidelines
  • Read the new commit (9c02de8) and changed files
  • Verify pass 5 findings still hold
  • Post review findings

Review: feat(distribution): add sync-managed claude review-lane caller components

One commit landed since Pass 5. All Pass 5 findings remain resolved. No new blocking issues.


Pass 5 findings — still resolved

This commit touches only components/claude-lanes/claude-lanes.test.sh. No changes to policy.json, sync-manifest.yml, ai-review-bot-composition.md, distribution/README.md, or the component YML files — all prior resolutions are stable.


New commit 9c02de8 — verified correct

test(claude-lanes): assert each target ran a destination assertion

The commit closes a second vacuousness hazard, distinct from the one d0ef74b addressed. The commit message's proof is internally consistent and the mutation test counts match the implementation.

The asymmetry that triggered this. Target admission (outer loop, selecting which targets to iterate) used a substring match (grep -F over \t<component>), while the per-target membership check (inner loop, selecting which lane files to assert) used a whole-line match (grep -qxF). A future component named as an extension of an existing one — claude-review-caller-v2 — would be admitted by the substring test and rejected by the exact test, so that target's iteration would produce zero destination assertions while the suite reported green.

The two changes:

  1. Both membership tests are now exact associative-array lookups. is_lane_component is built once before the outer loop; manages is built fresh per target inside the inner loop. Both use ${assoc[$key]-} to provide an empty default under set -u, which is the same correct pattern as the existing test harness. declare -A manages=() inside the script-level for loop correctly reinitializes the array on each iteration — this is idiomatic bash. ✓

  2. destinations counter with assert_nonzero on the count. destinations=$((destinations + 1)) increments before each assert_file_exists, and assert_nonzero "$target ran a destination assertion" "$destinations" follows after the inner loop. This is the correct invariant: it fails closed regardless of the failure mode — broken query, renamed component, or membership test that stops matching — because the symptom in all cases is destinations=0. ✓

  3. Standalone loop assert_file_exists added at line 85. Previously the loop relied on actionlint's exit 3 for an empty workflows directory as an indirect signal; the explicit assertion documents the intent directly. ✓

Mutation test count verification. The claim is 30 PASS / 0 FAIL intact and 22 PASS / 4 FAIL with broken membership. Working through the assertion counts:

  • Preamble: 2 (assert_nonzero ×2)
  • Standalone loop (2 lane callers × 3 assertions each): 6
  • Target loop (4 targets × 5 assertions each): 20 — one assert_exit (materialize) + one assert_file_exists (destination) + one assert_nonzero (count) + one assert_exit (lint) + one assert_silent (findings)
  • Control case: 2

Total: 30. With broken membership: assert_file_exists (destination, line 118) is skipped and assert_nonzero (count, line 120) fails, so each target loses 1 PASS and gains 1 FAIL → 30 - 4 skipped + 4 FAIL = 22 PASS / 4 FAIL. Consistent. ✓

No issues found.


Overall gate status

Gate Status
Gate 1 — actionlint suppression (#284) ✅ Resolved (merged 2026-07-27)
Gate 2 — sync App workflows: write ⏳ Pending external org-owner action
Gate 3 — workflow name: / ruleset context ✅ Resolved by scope change (claude-code-plugins → locally-owned)
Gate 4 — #289 re-pin sync engine at ac223bb ⏳ Open per PR body (#290#289#286 ordering)

The only failing check is do-not-merge / do-not-merge — the intentional label gate.

@kyle-sexton kyle-sexton removed the do-not-merge Hard merge gate: do not merge while applied. label Jul 29, 2026
@kyle-sexton
kyle-sexton merged commit 31d81a6 into main Jul 29, 2026
43 of 46 checks passed
@kyle-sexton
kyle-sexton deleted the feat/claude-lane-caller-components branch July 29, 2026 16:12
kyle-sexton added a commit that referenced this pull request Jul 29, 2026
…true form (#294)

## Summary

`components/claude-lanes/claude-review.yml` justified its job-level
queue block
with a key-level claim:

> `queue:` cannot share a concurrency block with cancel-in-progress

GitHub's prohibition is **value-level**, not key-level. Verbatim, from
the
reusable that renders into both `#concurrency` and
`#jobsjob_idconcurrency`:

> The combination of `queue: max` and `cancel-in-progress: true` is not
allowed
> and will result in a workflow validation error.

Same file, restated in the example prose:

> Note that `queue: max` cannot be combined with `cancel-in-progress:
true`,
> because the two options describe conflicting behaviors for handling
> in-progress runs.

- Source pinned at the commit read: [`github/docs@336b7f5`

`data/reusables/actions/actions-group-concurrency.md`](https://github.com/github/docs/blob/336b7f546d9443dab4e1fa4f0f470e45448c7abc/data/reusables/actions/actions-group-concurrency.md)
  (lines 20 and 126).
- Rendered page carrying the same sentence, confirmed by fetch:

<https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency>

`queue: max` + `cancel-in-progress: false` is legal and runs — the
key-level
wording forbids a shape GitHub permits.

The correction narrows the claim to the documented pair and adds the
second,
independent reason the two blocks stay separate: this group is
job-scoped and
repo-wide, not per-PR. Without that clause a reader who sets
`cancel-in-progress: false` could conclude the blocks may now be merged,
which
would silently collapse the per-PR supersede group into the repo-wide
queue.

The authoritative URL is cited **inline, in the tracked bytes**
(`565559a`,
added after review). Because this component is materialized verbatim
downstream, a citation living only in a commit message never reaches the
maintainers who read the claim — a correct-but-uncited assertion would
repeat
this PR's own failure mode at lower severity. The link sits at the
sentence that
defers to it per
`conventions/engineering/documentation-and-citations.md`, and
it is a living URL rather than a pinned snapshot because the same
convention
prefers fetching at read time over storing a snapshot with no recheck
trigger.
The job-level anchor (`#jobsjob_idconcurrency`) is cited rather than the
workflow-level one because the comment annotates a
`jobs.<id>.concurrency`
block; both anchors were confirmed live to exist on the rendered page
and to
carry the rule sentence.

### Why this races the open sync PRs

This component is sync-managed: its bytes are copied verbatim into every
consumer's `.github/workflows/claude-review.yml`. The wording has not
landed
anywhere yet, and four open sync PRs are carrying it right now.

Regenerated from commands, not recalled:

```console
$ grep -n '^targets:' distribution/sync-manifest.yml
216:targets:
$ awk 'NR>=216' distribution/sync-manifest.yml | grep -c '^      - claude-review-caller$'
5
```

Managed targets: `claude-code-plugins`, `dotfiles`, `github-iac`,
`medley`,
`provisioning`.

Each consumer's live default-branch file was fetched and counted — the
file was
read, not grepped on a ref for text expected to be there:

| Repo | `queue: max` on default branch | old wording on default branch
| open sync PR head carries old wording |
| --- | --- | --- | --- |
| `claude-code-plugins` | 0 | 0 | no open sync PR |
| `dotfiles` | 0 | 0 | #361 (`df035f9`) — yes |
| `github-iac` | 0 | 0 | #244 (`0b8124d`) — yes |
| `medley` | 0 | 0 | #1676 (`d107862`) — yes |
| `provisioning` | 0 | 0 | #231 (`7b9d016`) — yes |

Landing this before those PRs merge means the correct text reaches every
consumer on first contact. Landing it after means a false statement
propagates
fleet-wide and needs a second sync to retract.

**Hold on the four sync PRs until this merges.** The rollout-window gate
is
intact — `grep -c '^ automerge: false$' distribution/sync-manifest.yml`
returns `8` against `8` total targets — and each of the four PRs was
queried
live (`gh pr view --json autoMergeRequest`): **none is armed**. So
nothing
merges them without a human, and a human merging any of them before #294
lands
is the only thing that defeats this PR.

Merging #294 does **not** require closing them. Verified in the engine
at the
SHA this repository pins (`ci-workflows@ac223bb`,
`.github/workflows/standards-sync.yml:454-461`): it uses
`peter-evans/create-pull-request` against a fixed `branch:
chore/standards-sync`,
which is the head branch on all four PRs — so a subsequent real run
refreshes
each existing PR in place rather than opening a new one, which is also
why the
engine guards auto-merge arming on `pull-request-operation ==
'created'`.
`sync.yml` runs on `push: branches: [main]`, so merging this PR is
itself the
refresh trigger.

### Sibling component: checked, no change

`components/claude-lanes/claude-security-review.yml` was read in full,
not
assumed to match. It makes no key-level claim. Its one queue-adjacent
statement — "a full queue CANCELS new arrivals" — is accurate:

> `max`: Up to 100 jobs or workflow runs can be `pending` in the
concurrency
> group. When the queue is full, any additional jobs or workflow runs
are
> canceled.

A repo-wide grep confirms the defect had exactly one site:

```console
$ grep -rn 'share a concurrency block\|cannot share' . | grep -v '^\./\.git/'
./components/claude-lanes/claude-review.yml:87: ...
```

This repository's own `.github/workflows/claude-review.yml` sets no
caller-level
concurrency and no `queue:`, so it never carried the claim.

### Deliberate non-change

Independent verification surfaced a separate omission, not a falsehood:
the
comment does not mention that `queue: max` caps at 100 pending and
cancels
arrivals beyond that. Left out on purpose — this PR narrows a false
claim and
should stay a one-hunk diff while it races the sync PRs.

The overflow-wedge argument that makes the cap load-bearing in the
security
lane does not transfer here, and that was checked rather than assumed:
every
ruleset on all five managed targets was enumerated and its
`required_status_checks` contexts read, and **no target requires any
`claude`-named context today**.

```console
$ # per target: enumerate rulesets, union their required contexts, count claude ones
claude-code-plugins: total_required=4 claude_required=0
dotfiles:            total_required=3 claude_required=0
github-iac:          total_required=3 claude_required=0
medley:              total_required=3 claude_required=0
provisioning:        total_required=3 claude_required=0
```

So overflow cancellation on this lane cannot void a required check — the
same
premise that already makes this lane's deliberate `cancel-in-progress:
true`
safe. If a consumer later promotes the code-review context to required,
the
cap becomes load-bearing and the comment should gain it. Worth a
follow-up on
its own merits, not a blocker for this one.

## Test plan

- `components/claude-lanes/claude-lanes.test.sh`, counted from the run
rather
  than eyeballed, and reproduced by two independent runs:

  ```console
$ bash components/claude-lanes/claude-lanes.test.sh > run.txt 2>&1; echo
"exit=$?"
  exit=0
  $ grep -cE '^PASS' run.txt; grep -cE '^FAIL' run.txt
  30
  0
  ```

  That includes `[29] a synced lane caller fails actionlint without the
suppression` and `[30] control run reports the suppressed message`. Note
for
anyone re-running: the harness materializes from the git **index**, so
the
change must be staged or every target reports `source worktree bytes
differ
  from the indexed object` and the suite fails for that reason alone.
- Comment-only change: no YAML key, value, group expression, or pin is
touched.
Confirmed by `git diff --stat origin/main...HEAD` — 6 insertions, 3
deletions,
  every line inside a `#` comment block.
- Independent verification by a fresh-context agent with the rationale
withheld
(given the final file text and asked whether every claim is true, and
whether
the constraint is stated at the correct level of generality). Verdict on
the
corrected wording: **ACCURATE** — "The comment restates the constraint
at
precisely the docs' generality — the docs name the literal `queue: max`
+
  `cancel-in-progress: true` pair and nothing wider. Neither broader nor
narrower. I would not reword this clause." It also independently
confirmed
  `queue:` is valid at job level, via
`data/reusables/actions/jobs/section-using-concurrency-jobs.md`
including the
  same reusable.
- Repository CI on this PR: every check in the `pass` bucket, zero
non-pass,
  verified by `gh pr checks 294 --json name,bucket` on each pushed head
  (`a2a8228` and `565559a`). That includes the Claude review lane
  (`review / review`) — run once per head, never re-run to chase green.

## Related

- #286 — the PR that introduced these caller components and the wording
  corrected here.
- Open sync PRs carrying the pre-correction text:
melodic-software/dotfiles#361,
  melodic-software/provisioning#231, melodic-software/github-iac#244,
  melodic-software/medley#1676.
- Two sibling sites for the same claim live in `ci-workflows` and are
tracked
there (ci-workflows#296 for the workflow; ci-workflows#302, merged, for
  `PLAN.md`). Deliberately untouched by this PR.

No linked issue.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Aug 13, 2026
)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary

Unparks `claude-security-review-caller` for private adopters (path (b)
from #377), with `melodic-software/provisioning` as the first managed
target.

- Move `cancel-in-progress: false` to job-level concurrency on
`security-review` so the caller can coexist with `concurrency-policy`
(`delegated-job-level` shape).
- Manage the component for `provisioning` in the sync manifest.
- Rewrite parking docs so the remaining public/shared-shape blockers
from #377 stay recorded (cross-repo routing kind, blanket public-target
test, plugins evidence/`skip-actors`) without filing a follow-up issue.
- Align the `claude-code-plugins` locally-owned removal trigger with
that same three-part bar.

Fixes #377

## Related

- Issue #377 scoping note (private unpark vs public/shared-shape
blockers)
- `distribution/README.md` Claude review-lane caller parking prose
- Prior parking introduction in #286

## Verification

- `node --test --test-name-pattern='claude lane|managed target of a
claude|selector-routed claude'
components/runner-policy/runner-policy.test.mjs` — 4/4 pass
- Standalone actionlint of materialized security caller — clean
- `bash distribution/sync-manifest.sh validate` — valid
- Clean-config apply to a provisioning stand-in materializes both lane
callers

## Follow-through (same session)

- Merge sync PR into provisioning after this lands
- Ship repo-owned `.github/claude-security-paths` starter in
provisioning
- Do **not** add `security-review / security-review` as a required check
yet (frontier consensus: observe advisory first)
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-3f079f31-8a5f-4063-9a7a-5e4dbe0563a5?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-3f079f31-8a5f-4063-9a7a-5e4dbe0563a5&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
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