Repository navigation
ci(reaper): guard the MERGED bucket on base.ref === 'main', with its contract harness - #15144
Merged
Merged
Conversation
…s contract harness `merged_at` says a pull request landed; it does not say WHERE. A stacked PR merged into another dev branch reports `merged_at` exactly like one that landed on `main`, and its content reaches `main` only if that base branch itself later merged -- a second hop the reaper's deliberately flat criterion cannot see. Measured in the #13503 census (2026-09-02) over 678 `copilot/*` branches: of 575 MERGED, 48 merged into something other than `main`; 43 of those have a base that itself later merged, 4 have a base that no longer exists, and 1 -- `copilot/check-action-run-status`, based on a PR that closed WITHOUT merging -- has no confirmed path to `main` at all. Unguarded, the classifier calls that one safe to delete. `reapable` now additionally requires that one of the branch's merged pull requests reports `base.ref === 'main'`. Everything else that merged lands in a new `mergedElsewhere` bucket: rendered in full with the base it merged into, never reaped. A PR record carrying no `base` is counted as NOT main -- the fail-closed direction, because the cost of guessing here is a deleted branch. The guard only NARROWS, and that is asserted rather than promised. `scripts/check-merged-branch-reaper-outcome.mjs` extracts the shipped script from the YAML with a real parser, runs it under doubles the way actions/github-script runs it, and holds four invariants over every scenario -- `reaped => the branch has a merged PR based on main`, the held bucket really is merged-and-not-into-main, the buckets partition the candidates, and the held count is rendered. Its `--self-test` deletes the guard, inverts it, renames the base constant, flips the fail-closed direction and unrecords/unrenders the bucket, and requires the battery to redden for each; it mutates the decisions the guard sits beside too (open-PR precedence, the grace window, the defensive head-ref filter, the protected short-circuit), because a guard that cost something already there would not be a narrowing. No `is-ancestor` is introduced anywhere: the base ref is read off the PR record, which is the instrument the workflow header mandates. The grace window still runs off the newest merge of ALL merged PRs, so the guard cannot shorten it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
baozhoutao
marked this pull request as ready for review
September 4, 2026 03:36
baozhoutao
enabled auto-merge
September 4, 2026 03:37
baozhoutao
pushed a commit
that referenced
this pull request
Sep 4, 2026
…aude/ branches Flips .github/workflows/merged-branch-reaper.yml from report-only to the scheduled weekly deletion the maintainer ruled on 2026-09-04 (issue #12771, decision batch #30), reaffirming the 2026-08-31 ruling under the base-ref guard PR #15144 landed. Deletion is a SEPARATE job (`reap`), because `permissions:` is scoped per job. `sweep` keeps `contents: read` + `pull-requests: read` and remains structurally incapable of deleting a ref; `reap` holds the only `contents: write` in the file, consumes the `reapable` list `sweep` publishes as a job output, and computes no classification of its own. Fences: - `reap` never runs on `pull_request` — the self-exercising run stays a dry run — and its `if:` is an allowlist of `schedule` plus a `workflow_dispatch` on which the operator explicitly set `dry_run: false`. - the new `dry_run` workflow_dispatch input defaults to true, so the manual path is fail-closed. - the base-ref guard, `PREFIX`, `BASE_REF`, the grace window, the schedule and the `is-ancestor` prohibition are all untouched. - the whole deletion list is printed to the run log before the first delete. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
zhuangjianguo
pushed a commit
that referenced
this pull request
Sep 4, 2026
…aude/ branches (#15224) * ci(reaper): arm the merged-branch reaper for scheduled deletion of claude/ branches Flips .github/workflows/merged-branch-reaper.yml from report-only to the scheduled weekly deletion the maintainer ruled on 2026-09-04 (issue #12771, decision batch #30), reaffirming the 2026-08-31 ruling under the base-ref guard PR #15144 landed. Deletion is a SEPARATE job (`reap`), because `permissions:` is scoped per job. `sweep` keeps `contents: read` + `pull-requests: read` and remains structurally incapable of deleting a ref; `reap` holds the only `contents: write` in the file, consumes the `reapable` list `sweep` publishes as a job output, and computes no classification of its own. Fences: - `reap` never runs on `pull_request` — the self-exercising run stays a dry run — and its `if:` is an allowlist of `schedule` plus a `workflow_dispatch` on which the operator explicitly set `dry_run: false`. - the new `dry_run` workflow_dispatch input defaults to true, so the manual path is fail-closed. - the base-ref guard, `PREFIX`, `BASE_REF`, the grace window, the schedule and the `is-ancestor` prohibition are all untouched. - the whole deletion list is printed to the run log before the first delete. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * test(reaper): pin the deletion hand-off and fence the delete job structurally The contract harness drives the `sweep` classifier and can say nothing about the job that deletes — deletion deliberately lives outside the extracted script, so what the harness judges stays a classification rather than an action. Two additions close that gap. 1. The hand-off. `sweep` now publishes `reapable_branches`, the machine-readable half of the list it prints, and `reap` consumes that and nothing else. Scenarios G1/G2/R1 pin that the list EQUALS the reapable bucket — same members, same order — over a population carrying one branch in every bucket, and mutations M13/M14 drive both directions red (held branches leaking in; the list not published at all). 2. The fence. `reapFenceFailures()` parses the shipped YAML and asserts the delete job's structure: its `if:` excludes `pull_request` and gates `workflow_dispatch` on `inputs.dry_run == false`; it declares `contents: write` and is the ONLY job in the file that does; the top-level grant stays `contents: read`; it still `needs: sweep`. New self-test battery 6 drives six mutations of the workflow text to red, each asserting its anchor was present first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * ci(reaper): put every excluded bucket on the run-log audit line The maintainer's ruling names the run log as the audit trail, and the notice line named three of the seven buckets — reapable, mergedElsewhere, noPr. The other four (open, closedUnmerged, grace, protectedBranch) lived only in the step summary and the uploaded artifact, so the log alone could not answer "what did it hold back, and why". Also retires two strings that stopped being true when the reaper was armed: the summary heading said "DRY RUN. Nothing was deleted." of a run that may now delete in a later job, and the notice said "Nothing was deleted" of the whole run rather than of this job. Both now speak for the `sweep` job only, which is the thing they were ever really asserting — its token grant is `contents: read` and that has not changed. No classification changed: the buckets, the guard, the grace window and the step outputs are byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 9, 2026
…aude/ branches (objectstack-ai#15224) * ci(reaper): arm the merged-branch reaper for scheduled deletion of claude/ branches Flips .github/workflows/merged-branch-reaper.yml from report-only to the scheduled weekly deletion the maintainer ruled on 2026-09-04 (issue objectstack-ai#12771, decision batch objectstack-ai#30), reaffirming the 2026-08-31 ruling under the base-ref guard PR objectstack-ai#15144 landed. Deletion is a SEPARATE job (`reap`), because `permissions:` is scoped per job. `sweep` keeps `contents: read` + `pull-requests: read` and remains structurally incapable of deleting a ref; `reap` holds the only `contents: write` in the file, consumes the `reapable` list `sweep` publishes as a job output, and computes no classification of its own. Fences: - `reap` never runs on `pull_request` — the self-exercising run stays a dry run — and its `if:` is an allowlist of `schedule` plus a `workflow_dispatch` on which the operator explicitly set `dry_run: false`. - the new `dry_run` workflow_dispatch input defaults to true, so the manual path is fail-closed. - the base-ref guard, `PREFIX`, `BASE_REF`, the grace window, the schedule and the `is-ancestor` prohibition are all untouched. - the whole deletion list is printed to the run log before the first delete. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * test(reaper): pin the deletion hand-off and fence the delete job structurally The contract harness drives the `sweep` classifier and can say nothing about the job that deletes — deletion deliberately lives outside the extracted script, so what the harness judges stays a classification rather than an action. Two additions close that gap. 1. The hand-off. `sweep` now publishes `reapable_branches`, the machine-readable half of the list it prints, and `reap` consumes that and nothing else. Scenarios G1/G2/R1 pin that the list EQUALS the reapable bucket — same members, same order — over a population carrying one branch in every bucket, and mutations M13/M14 drive both directions red (held branches leaking in; the list not published at all). 2. The fence. `reapFenceFailures()` parses the shipped YAML and asserts the delete job's structure: its `if:` excludes `pull_request` and gates `workflow_dispatch` on `inputs.dry_run == false`; it declares `contents: write` and is the ONLY job in the file that does; the top-level grant stays `contents: read`; it still `needs: sweep`. New self-test battery 6 drives six mutations of the workflow text to red, each asserting its anchor was present first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * ci(reaper): put every excluded bucket on the run-log audit line The maintainer's ruling names the run log as the audit trail, and the notice line named three of the seven buckets — reapable, mergedElsewhere, noPr. The other four (open, closedUnmerged, grace, protectedBranch) lived only in the step summary and the uploaded artifact, so the log alone could not answer "what did it hold back, and why". Also retires two strings that stopped being true when the reaper was armed: the summary heading said "DRY RUN. Nothing was deleted." of a run that may now delete in a later job, and the notice said "Nothing was deleted" of the whole run rather than of this job. Both now speak for the `sweep` job only, which is the thing they were ever really asserting — its token grant is `contents: read` and that has not changed. No classification changed: the buckets, the guard, the grace window and the step outputs are byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 9, 2026
…e-delivery rejections (objectstack-ai#15276) * fix(runtime): type the packages-domain `protocol` service handle so undeclared request keys are compile errors (objectstack-ai#15215) * wip(runtime): type the packages-domain protocol service handle Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * wip(runtime): add the packages-domain protocol handle typing pin Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * chore(changeset): patch note for the packages-domain protocol handle typing Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * docs(permissions): re-anchor the system-context census rows moved by the typing block Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N * docs(permissions): regenerate the system-context census from the merged tree Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --------- Co-authored-by: Claude <noreply@anthropic.com> * fix(objectql): publish the record's organization on every DataEvent (objectstack-ai#15220) * fix(objectql): publish the record's organization on every DataEvent (objectstack-ai#14970) `DataEventSchema.organizationId` was declared and published by the spec half but populated by nothing, so every `data.record.*` event went out with the key absent — which the contract requires a consumer to read as "this record is behind no organization wall". `publishDataEvent` now resolves it from the row itself: the written record on `created`, the post-state on `updated`, and the by-id branch's already-read pre-image on `deleted`, so no per-event read is bought. The record's organization, never `ExecutionContext.tenantId` — that is the caller's active org, and the two diverge on exactly the system/unscoped write this key most needs to label correctly. Absence keeps one spelling: the key is omitted, never `''` (which the schema refuses outright, dropping the whole event) and never an explicit `undefined` (which survives `parse` as a present key). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ * docs(permissions): re-anchor the system-context census after the engine line shift Mechanical repair by `node scripts/check-system-context-census.mjs --fix`, the only correct writer for this table. Pure line rot: the `eventOrganizationId` helper and its threading shifted every later line in `packages/objectql/src/engine.ts`, so 14 anchors (15 citation sites — one source line is cited twice) pointed at the wrong lines. No population and no classification change: still 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read — the same figures as before the shift. `--fix` did not refuse, and the diff is digits and nothing else (12 lines added, 12 removed, identical once digits are stripped). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --------- Co-authored-by: Claude <noreply@anthropic.com> * ci(reaper): arm the merged-branch reaper for scheduled deletion of claude/ branches (objectstack-ai#15224) * ci(reaper): arm the merged-branch reaper for scheduled deletion of claude/ branches Flips .github/workflows/merged-branch-reaper.yml from report-only to the scheduled weekly deletion the maintainer ruled on 2026-09-04 (issue objectstack-ai#12771, decision batch objectstack-ai#30), reaffirming the 2026-08-31 ruling under the base-ref guard PR objectstack-ai#15144 landed. Deletion is a SEPARATE job (`reap`), because `permissions:` is scoped per job. `sweep` keeps `contents: read` + `pull-requests: read` and remains structurally incapable of deleting a ref; `reap` holds the only `contents: write` in the file, consumes the `reapable` list `sweep` publishes as a job output, and computes no classification of its own. Fences: - `reap` never runs on `pull_request` — the self-exercising run stays a dry run — and its `if:` is an allowlist of `schedule` plus a `workflow_dispatch` on which the operator explicitly set `dry_run: false`. - the new `dry_run` workflow_dispatch input defaults to true, so the manual path is fail-closed. - the base-ref guard, `PREFIX`, `BASE_REF`, the grace window, the schedule and the `is-ancestor` prohibition are all untouched. - the whole deletion list is printed to the run log before the first delete. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * test(reaper): pin the deletion hand-off and fence the delete job structurally The contract harness drives the `sweep` classifier and can say nothing about the job that deletes — deletion deliberately lives outside the extracted script, so what the harness judges stays a classification rather than an action. Two additions close that gap. 1. The hand-off. `sweep` now publishes `reapable_branches`, the machine-readable half of the list it prints, and `reap` consumes that and nothing else. Scenarios G1/G2/R1 pin that the list EQUALS the reapable bucket — same members, same order — over a population carrying one branch in every bucket, and mutations M13/M14 drive both directions red (held branches leaking in; the list not published at all). 2. The fence. `reapFenceFailures()` parses the shipped YAML and asserts the delete job's structure: its `if:` excludes `pull_request` and gates `workflow_dispatch` on `inputs.dry_run == false`; it declares `contents: write` and is the ONLY job in the file that does; the top-level grant stays `contents: read`; it still `needs: sweep`. New self-test battery 6 drives six mutations of the workflow text to red, each asserting its anchor was present first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * ci(reaper): put every excluded bucket on the run-log audit line The maintainer's ruling names the run log as the audit trail, and the notice line named three of the seven buckets — reapable, mergedElsewhere, noPr. The other four (open, closedUnmerged, grace, protectedBranch) lived only in the step summary and the uploaded artifact, so the log alone could not answer "what did it hold back, and why". Also retires two strings that stopped being true when the reaper was armed: the summary heading said "DRY RUN. Nothing was deleted." of a run that may now delete in a later job, and the notice said "Nothing was deleted" of the whole run rather than of this job. Both now speak for the `sweep` job only, which is the thing they were ever really asserting — its token grant is `contents: read` and that has not changed. No classification changed: the buckets, the guard, the grace window and the step outputs are byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --------- Co-authored-by: Claude <noreply@anthropic.com> * fix(service-automation): compile the test layer with tsc, and repair the TS2341 x3 it hid (objectstack-ai#15152) * wip: onboard service-automation typecheck, fix TS2341 residue * wip: onboarding gate registry entry + changeset * fix(scripts): re-measure this entry's provenance totals on the merged tree The `service-knowledge` onboarding landed on `main` between this entry's first reading and this merge, so every absolute in its provenance block (programs, pairs, packages, clean count) was a number about a tree that no longer exists. Re-taken with `--list` on the merge commit itself, all four rows plus the before/after pair, by varying only what the `typecheck` script names: no `typecheck` script absent 120 programs / 293 pairs names tsconfig.json absent 120 programs / 293 pairs names tsconfig.test PRESENT 121 programs / 302 pairs names both (the card) PRESENT 121 programs / 302 pairs before 59 of 78 packages, 120 programs, 293 pairs, 19 clean after 60 of 78 packages, 121 programs, 302 pairs, 18 clean The deltas this block actually claims (+1 package, +1 program, +9 pairs, one per dep) are unchanged; only the absolutes moved, and the block now says which merge moved them. The sibling entries' own blocks keep their own historical readings untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y --------- Co-authored-by: Claude <noreply@anthropic.com> * docs(platform-objects): widen sys_email.error description to cover pre-delivery rejections `sys_email.error` was declared as "Transport error message when status=failed", but since objectstack-ai#14371 EmailService.recordRejectedMessage also writes status=failed rows for messages rejected by normalizeMessage before they reach a transport (prefixed "rejected before delivery: ..."). The declared field help was narrower than what the column actually holds. Widen the description (wording settled in triage, issue comment 5504375428) and regenerate the platform-objects i18n bundle with its own tooling (node scripts/check-i18n-bundles.mjs --write) rather than hand-editing the generated file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ * chore(platform-objects): add changeset for sys_email.error description widening Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --------- Co-authored-by: Litant Ying <litant.dev@proton.me> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: os-sales <sales@objectstack.ai>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #13503
⛔ Not
Fixes— deliberately. This is PR 1 of the ruling only. Theworkflow_dispatchdry-run, the deletion list posted on the card, the maintainer's release line and the deletion run are all still ahead, and a closing keyword here would silently close a card that is still waiting on a human.What the ruling asked for
Ruling of 2026-09-03 on #13503 (comment
5529423421, decision batch #24, maintainer 「其他同意」 adopting option A):That is this PR, and nothing else from that ruling.
The defect
merged_atsays a pull request landed. It does not say where. A stacked PR whose base is another dev branch reportsmerged_atexactly like one that landed onmain, and its content reachesmainonly if that base branch itself later merged — a second hop the reaper's deliberately flat criterion cannot see and must not start guessing at.Measured in the #13503 census (2026-09-02, comment
5511828156) over the 678copilot/*branches: of 575 MERGED, 48 (8.3%) merged into something other thanmain. Walking all 48 — 43 have a base that itself later merged tomain(content lands, via a 2-hop chain), 4 have a base that no longer exists and could not be re-verified, and 1 —copilot/check-action-run-status, based on a PR that closed without merging — is a MERGED branch whose content has no confirmed path tomainat all. Against that one, the unguarded criterion says "safe to delete", and it is wrong.The change
.github/workflows/merged-branch-reaper.yml, in the standing classifier:reapablenow additionally requires that one of the branch's merged PRs reportsbase.ref === 'main'.mergedElsewherebucket — its own table row, its own listing naming the base each branch merged into, its own step output. Held for a human look, never reaped, including the 43 that are safe by chain. That asymmetry is deliberate and is argued in the header: an unreaped branch costs a line of noise, a wrongly reaped one costs work that exists nowhere else.baseat all is counted as NOT main — the fail-closed direction.⛔ It only narrows, and that is asserted, not promised
reapableunder this diff is a subset ofreapablebefore it, for every input. Newscripts/check-merged-branch-reaper-outcome.mjsholds it as an invariant over every scenario:stated as a consequence rather than as the presence of a line of code, so a later rewrite that keeps the behaviour passes and one that loses it cannot.
⛔ No
is-ancestor, anywhereThe workflow header's line 12 constraint is respected, not replaced. The base ref is read off the PR record, which is the instrument that section mandates; nothing here asks git whether a commit is reachable from anything. The MERGED-state criterion is unchanged — the guard sits on top of it.
The self-test row the ruling names
There was no existing self-test on this classifier to add a row to — the reaper's logic is inline
actions/github-scriptand nothing in the tree drove it. So the row lands in this repo's established harness shape for exactly that situation, the third of its kind:check-cross-repo-closer-outcome.mjsandcheck-merge-queue-triage-outcome.mjsare the two siblings, and this follows them method-for-method — the shipped script is extracted from the YAML with a real parser, never retyped, and run under doubles the way the action runs it, as oneAsyncFunctionbody.11 scenarios (
node scripts/check-merged-branch-reaper-outcome.mjs --list). The guard's own rows:main, 30d agobaseat allmainand later into a siblingmaininside the grace windowplus four invariants checked for every scenario (narrowing, the held bucket is really merged-and-not-into-main, the buckets partition the candidates, the held count is actually rendered), and the pre-existing decisions the guard sits beside: open-PR precedence, the grace window, the defensive head-ref filter, the protected short-circuit.
--self-testmutates the extracted source 12 ways and requires the battery to redden for each, naming the scenario that catches it: the guard deleted, the guard blinded to the base, the base constant renamed, the fail-closed direction flipped, the bucket unrecorded, its table row deleted, its listing deleted, its step output dropped — and one mutation per neighbouring decision. Each mutation asserts its own anchor was present first, so a rewrite of the workflow cannot leave the mutations silently matching nothing.Wiring
.github/workflows/lint.yml, beside its two sibling harnesses, directnodeform per that file's GATE INVOCATION IDIOM note:check:self-test-wiredandcheck:self-test-workflow-commandsboth pick it up from there (167 scripts, all green), and the self-test asserts its own wiring in lint.yml so removing the step reddens the gate.merged-branch-reaper.ymlkeeps itsdispatch-gates: no-check-familiesdeclaration. That marker answers "do this workflow's own steps discover a family", and the answer is still no — the gate runs in lint.yml, not here. A card touching the reaper still gets the gate scheduled, because the gate names that path in its own source, which is howdispatch-gatesderives a family. Verified:node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstacklistscheck-merged-branch-reaper-outcomefor this diff.⛔ What this PR does NOT do
workflow_dispatch, no dry run, no sweep.contents: read; the deleting mode is still ABSENT, not switched off.copilot/is not added to the reaper's standing prefixes. The population is frozen, so a standing scope buys nothing — the ruling says so explicitly.workflow_dispatchdry-run over thecopilot/prefix", andworkflow_dispatchcurrently has no prefix input —PREFIXis the hard-codedclaude/. Adding one is out of scope for PR 1 and is flagged rather than done here.Verification
All exit codes captured before any pipe. Run on the final commit
45ca05049.node scripts/check-merged-branch-reaper-outcome.mjs→ exit 0,OK (68 assertions over 11 scenarios, driving the 8980-char classifier extracted from .github/workflows/merged-branch-reaper.yml)node scripts/check-merged-branch-reaper-outcome.mjs --self-test→ exit 0,63 assertions, 12 mutations of the shipped script each driven to redcheck:self-test-wired(+--self-test),check:self-test-workflow-commands,check:step-collectors,check:required-contexts,check:workflow-status-functions,check:ci-filter-parity,check:pm-dispatch-gates,check:nul-bytes→ all exit 0node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, its own change-set derivation, not a hand-made list) → 119 families; the ones this diff genuinely touches were run:entry-guard,watch-hint-literal,declared-population-live,agent-test-spelling,single-claim-paths,published-list-mirrors,ratchet-remedy-authority,parse-guard,keyed-text-bounds,comment-mask-corpus,whole-set-label-write,aggregator-roster,closing-keyword-parity,pnpm-acquisition,pnpm-filter-targets,swallow-census-controls,stall-guard-budget,stall-guard-headroom→ all exit 0pnpm lint(repo-wideeslint . --no-inline-config, not a narrowed subset) → exit 0No changeset:
.github/workflows/**andscripts/**publish nothing from any package.skip-changeset.Generated by Claude Code