diff --git a/.claude/source-control.md b/.claude/source-control.md index e6ac92a378..c63bbdac57 100644 --- a/.claude/source-control.md +++ b/.claude/source-control.md @@ -11,14 +11,17 @@ Conventional-Commits-shaped) per config-resolution.md's per-key fallthrough, so deliberately does not restate them. The `babysit_loop_*` keys below are the other key family this file carries, and they are set explicitly. -The `pr_body_required_sections` values below are the sections this repo's merge gate actually -requires, each non-empty, alongside a native closing keyword: `pr-issue-linkage`, defined in -`melodic-software/ci-workflows/.github/workflows/pr-issue-linkage.yml` and called by this repo's -`.github/workflows/pr-issue-linkage.yml` — which exempts `dependabot[bot]`, and no other author. The -gate is the authority; this key restates it so `/source-control:pull-request` drafts a body that -passes. Read the reusable at the SHA the caller pins, not at its default branch, since that pin is -what actually runs. Re-read it before changing either — an author or agent trusting a stale list -writes a PR body that fails CI. +The `pr_body_required_sections` values below are the sections this repo's pull-request contract +actually asks for, each non-empty, alongside a native closing keyword. The contract runs in the +`ci-status` job of `.github/workflows/ci.yml`, in the `pr-contract` composite step pinned to +`melodic-software/ci-workflows/.github/actions/pr-contract`, which exempts `dependabot[bot]` from +the linkage check and no other author. The linkage half is advisory: a body that misses a closing +keyword or a required section gets a warning, an upserted comment and the `needs-issue-linkage` +label, and `ci-status` still passes on that account. The composite is the authority; this key +restates it so `/source-control:pull-request` drafts a body that conforms. Read the composite at the +SHA `ci.yml` pins, not at its default branch, since that pin is what actually runs. Re-read it +before changing either: an author or agent trusting a stale list writes a PR body that draws the +advisory label. ## pr_body_required_sections diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 024d2d9599..0870dec822 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,10 +1,11 @@ Closes # diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efdb47f0f9..a48c4cc9fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -409,11 +409,8 @@ jobs: .github/workflows/claude-review.yml .github/workflows/claude-security-review.yml .github/workflows/dependabot-miro-bundle.yml - .github/workflows/do-not-merge.yml .github/workflows/hook-utils-timing.yml .github/workflows/link-check.yml - .github/workflows/pr-issue-linkage.yml - .github/workflows/pr-title.yml .github/workflows/silent-revert-canary.yml - name: Verify shebang files are executable diff --git a/.github/workflows/do-not-merge.yml b/.github/workflows/do-not-merge.yml deleted file mode 100644 index 1b3b1198ec..0000000000 --- a/.github/workflows/do-not-merge.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: do-not-merge - -# Blocks merging while the PR carries the `do-not-merge` label, via the shared -# do-not-merge-gate reusable from ci-workflows. Runs on pull_request_target so -# the base-branch definition evaluates (a head edit cannot neuter the gate); -# safe because the reusable reads PR label metadata via the API and runs no head -# code. `labeled`/`unlabeled` are required so applying or removing the label -# re-evaluates the gate — a status check binds to a SHA, so without them a check -# that passed before the label was applied stays green and the merge is never -# blocked. `merge_group` re-checks the label in the queue (inert without a -# queue). The emitted required-check context is `do-not-merge / do-not-merge`. -# Public repo: runs on the reusable's hosted default runner. -on: - # zizmor: ignore[dangerous-triggers] metadata-only gate; rationale in the header comment - pull_request_target: - types: [opened, reopened, synchronize, labeled, unlabeled] - merge_group: - -permissions: - pull-requests: read - -# pull_request_target runs the base-branch definition, so github.ref is the base -# branch: the PR number scopes cancellation and github.ref covers merge_group -# (which carries no pull_request object); the fallback is inert without a queue. -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - do-not-merge: - permissions: - pull-requests: read - uses: melodic-software/ci-workflows/.github/workflows/do-not-merge-gate.yml@90f1c54935203fa31b5b3d1f41531228be2c2b7f # 90f1c54 2026-07-18 gh-CLI-free label refetch - with: - runner: ubuntu-24.04 diff --git a/.github/workflows/pr-issue-linkage.yml b/.github/workflows/pr-issue-linkage.yml deleted file mode 100644 index 9d7e519e08..0000000000 --- a/.github/workflows/pr-issue-linkage.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: pr-issue-linkage - -# Validates the PR body carries a native closing keyword (Closes/Fixes/Resolves -# #N, including owner/repo#N, or a no-issue marker when the PR closes nothing — -# a case-insensitive regex matching the phrase "no linked issue" or "no related -# issue", not a literal string) and four non-empty contract sections — -# `## Summary`, `## Fix`, `## Verification`, `## Related` — via the shared -# pr-issue-linkage reusable from ci-workflows. Both body scans run with fenced -# code blocks and any 4-space- or tab-indented line blanked, inline code spans -# masked, and HTML-commented text discarded, so a marker inside a fence, an -# indented line, or a comment does not count. -# `pull_request_target` runs the base-branch definition, so a head-branch edit -# cannot bypass the gate — safe here because no head-branch code ever executes: -# the pinned reusable checks out nothing, holds read-only pull-request/actions -# permissions only, and passes the body into its script step through -# env/GITHUB_ENV rather than splicing it into script text. The reusable reads -# the body via a live `gh api` re-fetch, falling back to the event payload if -# that call fails; at the pinned SHA no token reaches that step, so the re-fetch -# fails auth every time and the payload fallback is the path that actually runs -# — payload-only in effect as a side effect of the failed re-fetch, not by -# design. `edited` re-validates on a body edit. `merge_group` reports the check -# green in the queue (the body was validated at PR time; inert without a queue). -# The emitted required-check context is `pr-issue-linkage / pr-issue-linkage`. -# Public repo: GitHub-hosted runners are free here, and the `with:` block pins -# `runner: ubuntu-24.04` explicitly, which coincides with the reusable's -# default. -on: - # zizmor: ignore[dangerous-triggers] metadata-only gate; rationale in the header comment - pull_request_target: - types: [opened, edited, reopened, synchronize] - merge_group: - -permissions: {} - -# pull_request_target runs the base-branch definition, so github.ref is the base -# branch: the PR number scopes cancellation and github.ref covers merge_group -# (which carries no pull_request object); the fallback is inert without a queue. -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - pr-issue-linkage: - permissions: - pull-requests: read - actions: read - uses: melodic-software/ci-workflows/.github/workflows/pr-issue-linkage.yml@7107b34832a7b6db5d08d3b132621c599fbe5e50 # v0.14.2 - with: - runner: ubuntu-24.04 - # dependabot PR bodies cannot carry the closing keyword + four contract - # sections this gate requires; exempt the login so its PRs are mergeable. - exempt-authors: 'dependabot[bot]' diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml deleted file mode 100644 index b967642ca3..0000000000 --- a/.github/workflows/pr-title.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: pr-title - -# Conventional Commits PR-title gate. Consumes the shared semantic-pr reusable -# workflow from ci-workflows. Repos are squash-only with the squash title set to -# PR_TITLE, so the PR title becomes the default-branch subject — this gates the -# Conventional-Commits history. `edited` re-validates on a re-title. The emitted -# required-check context is `pr-title / pr-title`. Public repo: runs on the -# reusable's hosted default runner. -on: - pull_request: - types: [opened, edited, reopened, synchronize] - -permissions: - pull-requests: read # Allows the called validator to read the PR title. - -# The PR number scopes cancellation to exactly one PR — head_ref would collide -# across same-named fork branches (this workflow only runs on pull_request -# events; run_id is a safety fallback that never cancels). -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true - -jobs: - pr-title: - permissions: - pull-requests: read # Reads the pull-request title for validation. - uses: melodic-software/ci-workflows/.github/workflows/semantic-pr.yml@90f1c54935203fa31b5b3d1f41531228be2c2b7f # 90f1c54 2026-07-18 - with: - runner: ubuntu-24.04 diff --git a/docs/CI-RUNNER-ROUTING.md b/docs/CI-RUNNER-ROUTING.md index c281a346d4..6ed1629eb2 100644 --- a/docs/CI-RUNNER-ROUTING.md +++ b/docs/CI-RUNNER-ROUTING.md @@ -28,11 +28,11 @@ reviewed pull request. The `ci-status` required check depends on every workload lane and requires each result to be `success`, failing closed through execution (`!cancelled()`, never a success-guard, so a skipped lane cannot report -success to branch protection). The metadata gates (`do-not-merge`, -`pr-issue-linkage`) run on `pull_request_target` so the base-branch definition -evaluates, and emit their required contexts from the reusable's hosted default -runner. Fork pull requests receive no secrets and no automated review, by -design. +success to branch protection). The metadata checks (Conventional Commits title, +`do-not-merge` label, issue linkage) run as the `pr-contract` composite step +inside the same `ci-status` job on the same hosted runner, so they no longer +carry status contexts of their own. Fork pull requests receive no secrets and +no automated review, by design. ## Toolchain integrity diff --git a/docs/adr/0002-default-on-ai-review-advisory-with-earned-promotion.md b/docs/adr/0002-default-on-ai-review-advisory-with-earned-promotion.md index 6960d5e25a..6ea3e4ccc3 100644 --- a/docs/adr/0002-default-on-ai-review-advisory-with-earned-promotion.md +++ b/docs/adr/0002-default-on-ai-review-advisory-with-earned-promotion.md @@ -206,6 +206,17 @@ The complete required STATUS-CHECK set on `main` is `pr-title / pr-title`, and unaffected by this analysis: the `signing` ruleset requires signed commits, and `base` requires linear history and squash-only merges. +> **Superseded 2026-09-05:** the `ci-gate` ruleset now requires `ci-status` alone. The +> `pr-title / pr-title` and `do-not-merge / do-not-merge` contexts no longer exist: the +> ci-perf program folded the title check, the `do-not-merge` label check and the issue-linkage +> check into the `pr-contract` composite step inside `ci.yml`'s `ci-status` job, and this +> repository's `pr-title.yml`, `do-not-merge.yml` and `pr-issue-linkage.yml` callers were +> deleted. The title and label checks still block merge, now under the name `ci-status`; the +> linkage check became advisory, a comment plus the `needs-issue-linkage` label. Read the live +> rules rather than this paragraph: +> `gh api repos/melodic-software/claude-code-plugins/rules/branches/main`. Tracked in +> melodic-software/github-iac#396. + ### Correction: the caller-tamper mitigation does not hold The 2026-07-21 addendum names "workflow-file diffs are themselves security-review surface and diff --git a/docs/conventions/loop-lane/README.md b/docs/conventions/loop-lane/README.md index d849a99669..37411da73c 100644 --- a/docs/conventions/loop-lane/README.md +++ b/docs/conventions/loop-lane/README.md @@ -166,10 +166,11 @@ clause: an escalating lane decided to hold, drafted its explanation first, and a ~30 minutes later — 3 minutes *after* the PR had merged). - **The `do-not-merge` label is the only cross-lane hold.** It is the one hold mechanism enforced - server-side: the org ruleset requires the `do-not-merge` status check, and the workflow behind it - re-evaluates on `labeled`/`unlabeled`, so applying the label flips a SHA-bound required check with - no bypass actors. A PR **comment is never a hold** — comments are advisory by construction; no - gate reads them, and an escalation comment on the PR obliges nothing until the label is on. + server-side: the org ruleset requires the `ci-status` check, whose `pr-contract` step fails on the + `do-not-merge` label and re-evaluates on `labeled`/`unlabeled`, so applying the label flips a + SHA-bound required check with no bypass actors. A PR **comment is never a hold** — comments are + advisory by construction; no gate reads them, and an escalation comment on the PR obliges + nothing until the label is on. - **Hold first, explain second.** The moment a lane decides a PR must not merge, it applies `do-not-merge` — before drafting the escalation comment, before assembling the supporting evidence. Explain-then-hold inverts the deadline: the drafting time is exactly the window a diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index 692dc1e27b..0ce469038d 100644 --- a/plugins/source-control/.claude-plugin/plugin.json +++ b/plugins/source-control/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "source-control", - "version": "0.55.51", + "version": "0.55.52", "description": "Git and GitHub delivery workflow: /commit (Conventional Commits + Co-authored-by trailer via safe heredoc mechanics), /pull-request (prep, create, CI monitoring, review-comment triage, merge, CI-log fetch), /babysit-prs (self-pacing fleet loop \u2014 safe by default; opt-in worker/autopilot tiers add gate-checked merge and thread resolution behind a deterministic Python engine), /babysit-loop (the loop-lane merge lane: a standing or drain loop that invokes babysit-prs per cycle, configured through repo-scoped babysit_loop_* keys on the layered source-control.md seam, with merge authority human-only until the target repo's tracked config adopts the lane, a gate-proven C2-mechanical baseline once adopted, and standing merge-rung raises binding from the team-tracked layer only \u2014 with one named exception, where an invocation line explicitly typing both the autopilot tier keyword and the dedicated raise argument --merge c3-this-run widens that single invocation's merge authority up to C3 behind a fresh independent frontier-tier resolver, while C4-structural and C5-untrusted-provenance stay unconditionally human-merge), /worktree (create, status, cleanup, audit for parallel-session isolation), /setup (check the effective commit-subject / PR-title convention merged across its config layers and the babysit-prs config, or apply \u2014 interview the repo and write the convention config to a chosen layer), and /resolve-conflicts (intent-first merge/rebase conflict resolution with a semantic-conflict sweep \u2014 never --abort). The commit-subject / PR-title convention is configurable via a source-control.md config written by a re-runnable setup skill, layered across a ~/.claude user-global file, the tracked team file, and a gitignored .claude/source-control.local.md personal overlay merged per key; Conventional Commits is the default when no convention is declared.", "author": { "name": "Melodic Software", diff --git a/plugins/source-control/CHANGELOG.md b/plugins/source-control/CHANGELOG.md index b9c23aec70..e9662e759c 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -3,6 +3,21 @@ All notable changes to the `source-control` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.55.52] + +### Changed + +- **Two reference files stop citing the marketplace repository's own deleted + `pull_request_target` lanes.** `babysit-prs/reference/stuck-checks.md` used + those lanes as its worked example of which checks survive a conflicted PR; + the repository folded its whole pull-request contract into one `ci-status` + job and deleted the callers, so the example now describes the lane split in + general terms instead of naming workflows that no longer exist. + `pull-request/reference/readiness.md` referred to `do-not-merge / + do-not-merge` as a required status context in the present tense, in a + past-tense pagination anecdote; it now reads "then a required status + context". Guidance unchanged in both files. + ## [0.55.51] ### Changed diff --git a/plugins/source-control/skills/babysit-prs/reference/stuck-checks.md b/plugins/source-control/skills/babysit-prs/reference/stuck-checks.md index 2924fc2447..949701d94b 100644 --- a/plugins/source-control/skills/babysit-prs/reference/stuck-checks.md +++ b/plugins/source-control/skills/babysit-prs/reference/stuck-checks.md @@ -59,12 +59,12 @@ scheduled — they are **absent**, not queued, not pending, not failing. Nothing reports them, because a check that was never created has no record to classify. What makes this actively misleading is that `pull_request_target` workflows run against the base -commit and are therefore unaffected, as are external apps posting commit statuses. A conflicted PR -in this repository still runs its `pull_request_target` lanes — `do-not-merge` and -`pr-issue-linkage` — while `ci.yml`, which carries the great majority of the gates, does not -schedule at all. The result is a short all-green check list with no failures anywhere: a PR that -reads as "passing" or "not started yet" while nearly every gate is simply missing. Resolving the -conflict makes the absent lanes appear and the count jumps by an order of magnitude. +commit and are therefore unaffected, as are external apps posting commit statuses and any +`schedule` or `push` lane. A conflicted PR in a repository that splits its lanes that way still +runs the base-anchored ones while the `pull_request` workflow carrying the great majority of the +gates does not schedule at all. The result is a short all-green check list with no failures +anywhere: a PR that reads as "passing" or "not started yet" while nearly every gate is simply +missing. Resolving the conflict makes the absent lanes appear and the count jumps. So read `mergeStateStatus` BEFORE reasoning about a check list that looks too short. `DIRTY` explains the absence completely, and the remedy is to merge the base branch or rebase, not to diff --git a/plugins/source-control/skills/pull-request/reference/readiness.md b/plugins/source-control/skills/pull-request/reference/readiness.md index 05465d5e42..415b73f918 100644 --- a/plugins/source-control/skills/pull-request/reference/readiness.md +++ b/plugins/source-control/skills/pull-request/reference/readiness.md @@ -52,7 +52,7 @@ When a security scanner or reviewer is added, replaced, or removed: Every gate below reads a GitHub list endpoint, and every one of those endpoints returns **30 items per page** by default and reports nothing when it truncates. A truncated read is not a visibly short answer — it is a confidently wrong one. Three rules, all absolute: -**1. Paginate every list read.** `--paginate` with `per_page=100`. Without it, "is X present?" answers a silent *no* for anything on a page you never fetched — indistinguishable from X not existing. This repo's own PR heads carry 33–37 check runs, so the unpaginated form dropped `do-not-merge / do-not-merge`, a required status context, on every head it was run against, and a reader concluded the context never attaches. It attached and was green every time. +**1. Paginate every list read.** `--paginate` with `per_page=100`. Without it, "is X present?" answers a silent *no* for anything on a page you never fetched — indistinguishable from X not existing. This repo's own PR heads carry 33–37 check runs, so the unpaginated form dropped `do-not-merge / do-not-merge`, then a required status context, on every head it was run against, and a reader concluded the context never attaches. It attached and was green every time. **2. Never pair a positional index with a list.** `.[-1]` on a truncated list is the 30th-oldest item, not the newest — the read returns a real item, plausibly shaped, and simply wrong. On issue #657 (33 comments) `.[-1]` unpaginated returned a comment 11.5 hours older than the actual latest. Select by the property you actually care about (an id, a SHA, an author, a timestamp) so the query states its own intent and cannot be silently satisfied by the wrong record. **Where the query is a control gate you will act on — "did my write land?" — one property is usually not enough.** Ask what else could satisfy this selector, and constrain that too: a SHA in a comment body proves the SHA was mentioned, not that *you* posted it, so a reviewer quoting it passes the gate while your failed write goes unnoticed. Pin the identity as well. diff --git a/scripts/check-hook-wiring-liveness.sh b/scripts/check-hook-wiring-liveness.sh index 7c2a893343..899ccef444 100755 --- a/scripts/check-hook-wiring-liveness.sh +++ b/scripts/check-hook-wiring-liveness.sh @@ -96,8 +96,8 @@ A repo-local hook script under .claude/hooks/ must be wired by HOOK_TELEMETRY_SINK). An unwired script is dead weight that can still claim enforcement in its header (#2959: pr-linkage-mcp-gate.sh, stripped in #2188 and never restored). Delete it, or wire it — do not re-add a hook without -ledger evidence (#2188). Policy enforcement for PR linkage already survives -via the source-control plugin hook plus required CI pr-issue-linkage. +ledger evidence (#2188). PR linkage is still reported in CI by the pr-contract +step of ci.yml's ci-status job, advisory since the contract folded into it. REMEDY exit 1 diff --git a/scripts/check-silent-revert.sh b/scripts/check-silent-revert.sh index 9f7e5d49a2..f31e0603a8 100755 --- a/scripts/check-silent-revert.sh +++ b/scripts/check-silent-revert.sh @@ -419,13 +419,13 @@ declares_removal() { # The Conventional-Commits revert type, and the ONLY revert spelling that can # reach main here (#2837). This repo is squash-only with # squash_merge_commit_title: PR_TITLE, so the squash subject is the PR title, - # and .github/workflows/pr-title.yml gates every title through a required - # Conventional-Commits check whose default type list is all-lowercase and - # contains `revert` but nothing a `Revert "…"` subject could match. Measured - # over every first-parent commit of main: `Revert "` 0, `revert:` 1. The - # corpus is deliberately named as "every" rather than as a total, because a - # total is stale the next time anything merges while the 0-and-1 result is - # what the pin is actually about. + # and the pr-contract step of .github/workflows/ci.yml's ci-status job gates + # every title through a required Conventional-Commits check whose type list is + # all-lowercase and contains `revert` but nothing a `Revert "…"` subject could + # match. Measured over every first-parent commit of main: `Revert "` 0, + # `revert:` 1. The corpus is deliberately named as "every" rather than as a + # total, because a total is stale the next time anything merges while the + # 0-and-1 result is what the pin is actually about. # # Kept exactly as constrained as the three forms around it: anchored at the # start of the SUBJECT, the literal lowercase type token, its optional