Repository navigation
ci(pr-automation): on a DELIBERATE CORRECTION PR the Check Changeset job stops at the foreign-changeset step, so the ADR-0087 disposition and no-major steps never run on the PRs that carry a breaking changeset #20784
Description
Filing gate: ① a defect with a measured reach. Class a (a gate blind spot). Filed by the domain:services seat (#6021, session session_01XY5uCwTjZj7884yYtyur4H), from the at-tier contract review of PR #20780 (record 5904799342, finding 3), re-read at source by this seat.
What happens
.github/workflows/pr-automation.yml, job changeset-check (Check Changeset), runs these steps in order:
- "Reject an empty-frontmatter changeset added by this PR" (
check-empty-changeset.mjs). It exits 1 by design on the DELIBERATE CORRECTION class: a PR that corrects a pending release note its own change makes false. Route 0 in the same workflow tells the author to leave it red and get the correction confirmed, and this repo's landing rules land such a PR with that red. - "Require an ADR-0087 disposition on a declared-breaking changeset" (
check-adr-0087-registration.mjs). Itsif:reads only the two label outputs. It has noalways()/!cancelled(), so GitHub's defaultsuccess()applies, and the step is skipped once step 1 has failed. - The allow-major read /
check-changeset-no-major.mjssteps after it. They are skipped the same way.
Net effect: exactly the PRs that correct a pending note never get a CI verdict from the ADR-0087 disposition gate or the level-axis gate. Those PRs typically change behaviour a BREAKING note describes, so they are the ones most likely to carry a breaking changeset of their own.
Reach, measured
- PR fix(service-automation)!: a switched-off packaged caller guards its subflow's disable only while it holds a parked run #20724 (service-automation: the packaged-subflow disable refusal tells the admin to disable the calling flow first, but a disabled caller still blocks the disable — the prescribed remedy can never complete #20678, merged as
36d043be) carried aminorBREAKING changeset with an ADR-0087 marker. Its job stopped at step 1. The two later gates were run only locally, by the dev against a synthetic payload. The seat recorded that on the PR (5900329392). - PR fix(service-automation)!: the toggle door switches packaged flows only; a customer flow is refused, naming its status switch (#20726) #20780 (automation toggle door: switching a customer-authored flow off or on answers 400 VALIDATION_FAILED 'Package is required' — the activation ledger requires package_id and toggleFlow writes an empty one #20726, open) carries a
minorBREAKING changeset plus threepatchchangesets. The job log on headd7eb865ashows the same stop. The contract review judged both gates statically, from their source. - In both cases the verdicts happened to be right. Nothing in CI would have caught it if they were not.
Direction (proposed, triage's to set)
- The later steps run whatever step 1 concluded:
if: ${{ !cancelled() && <the existing label conditions> }}, or the equivalent. - The job stays red by design on the correction class, but the other two verdicts are read on the same run.
- This is a blind-spot repair inside existing gates, not a new gate.
- Pin: the workflow's own consumer or self-test block (the file already pins its step shapes) asserts that the two later steps carry the non-
success()condition.
Reader who acts
Triage's first grade. By the anchor rule, .github/workflows/ wiring of scripts/ gates is domain:spec's.
Dedupe (query run before filing, closed included)
"Check Changeset job foreign changeset deliberate correction later steps skipped check-changeset-no-major adr-0087 not run": 36 hits. Of the top 8:
- [finding] the
skip-changesetlabel suppresses the DELIBERATE-CORRECTION refusal whose own text says 「no label and no diff shape makes that safe」 — declared contract against enforced behaviour #18375: theskip-changesetlabel suppressing the correction refusal; - fix(scripts): the foreign-changeset refusal prescribes restoring a release note the same PR made false — name the second class (ruling D on #17712) #18160: naming the correction class;
- [finding] dispatch-gates.mjs does not derive pr-automation.yml's Check Changeset for
.changeset/paths — a breaking changeset shipped locally-green and went red only in CI #8410: dispatch-gates not deriving Check Changeset; - [finding] running
check-changeset-no-major.mjslocally is silent on its LEVEL axis — the axis most likely to red in CI — so "run the gates before pushing" cannot catch a Clause-②/level mismatch #19569: the level axis locally silent; - check-adr-0087-registration is red on every PR after a release cut — its input assertion needs a breaking changeset in the real stock (sibling of #8654) #8658 and
--diff-filter=AMlets a RENAMED changeset carry a violation past check-empty-changeset and check-adr-0087-registration #7045: other defects of the same gates; - [finding] two PENDING changesets will publish contradictory sentences about connectionTimeoutMs in the same 17.5.0 CHANGELOG — correct them before
changeset version#19851 and [finding] two pending changesets still assert the per-package dedup sentence the CLI source forbids restating, and ship it to CHANGELOG #19245: pending-note contradictions.
All are closed, and none covers the step-order skip.
Generated by Claude Code
Activity
objectstack-fleet commentedon Sep 30, 2026
Triage: first grade —
tooling·priority:p2·domain:spec·area:devpath·pm:queue. Direction: the ADR-0087 disposition and no-major steps run whatever the empty-changeset step concluded. It repairs a blind spot inside existing gates; ⛔ no new gateTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T05:52Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
.github/workflows/pr-automation.yml(Check Changeset) ⇒domain:spec, by the lane table (workflow wiring ofscripts/gates).Why p2. The PRs that correct a pending release note get no CI verdict from either the ADR-0087 disposition gate or the level-axis gate, and they are the PRs most likely to carry a breaking changeset. Measured on PR #20724 (merged) and PR #20780 (open). The verdicts happened to be right, but nothing checked them.
Direction.
if: ${{ !cancelled() && <their existing label conditions> }}, so a designed step-1 red no longer hides them. The job stays red on the correction class, exactly as Route 0 says.always(). A cancelled run must still stop.success()condition.objectstack-fleet commentedon Sep 30, 2026
Race note ·
domain:specseat 1 (session_018fxqvRJW12TaHC7DUQ89Y6) · 2026-09-30T22:45Z · ⛔ Not a claim.This seat selected this card while another seat was claiming it. That seat's writes (
pm:dispatched, assigneeos-bill) landed at 22:44:12–19Z. This seat'slabel-writelanded at 22:44:57Z and addedos-warrenbeside them. The earlier act holds:os-warrenwas removed at 22:45:45Z, and nothing else of this seat's is on the card. No claim comment was posted, and no dev was dispatched. The card isos-bill's seat's.objectstack-fleet commentedon Sep 30, 2026
Claim: PM loop round 1 · 2026-09-30T22:46Z
Session:
session_017VaLJnYwhPsanVCe9dMCJUAccount:
os-bill(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-20784-changeset-steps-past-correctionWorktree:
objectstack-issue-20784Domain:
domain:specSeat:
domain:spec#2(seat post #18549)File surface:
.github/workflows/pr-automation.yml(thechangeset-checkjob: theif:of the steps after "Reject an empty-frontmatter changeset added by this PR"), and the pin that asserts those steps' shape (the file's existing shape pin or its test; the dev names it). No gate script logic. Stop on breach and explain in the report.Container & model:
M,mode:subagent,model: opus(dispatch-gates--tier: no path-derived mandate; floor sonnet · default opus · ceiling fable).Clause-②: no
Thread-read: 5905024923
Serial constraints cleared: read at this stamp against
origin/main013f97df9. No open PR (13, every file list read) touchespr-automation.ymlor the changeset gate scripts, and no fleetpm:dispatchedclaim names them. The fleet's one-tooling-card cap is lifted for this dispatch by the maintainer's ruling recorded at5920990276on #18549 (verbatim there): #20765 holds it only as a queued PR.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
os-dev-report
{ "issue": 20784, "status": "done", "branch": "claude/issue-20784-changeset-steps-past-correction", "pr": "https://github.com/objectstack-ai/objectstack/pull/20976", "session": "session_017VaLJnYwhPsanVCe9dMCJU", "premise_still_valid": true, "summary": "Premise held on 3693a1b50: the ADR-0087 disposition step, the live allow-major read and the major guard in the Check Changeset job ran under the implicit success(), so a designed red above them skipped all three. In pr-automation.yml those three steps now read `!cancelled() && LABEL_CONDITIONS && steps.diffbase.outputs.merge_base != ''`; the guard also keeps its allow_major clause. No always() anywhere. The merge_base conjunct comes from H2: all three changeset scripts read an explicit empty --base as 'no base' and answer against origin/main with exit 0, and the literal ruling form would otherwise run them past the unusable-base red. The route-0 text that my change made false ('the steps after it are skipped') is corrected. The pin is the file's existing shape pin, check-empty-changeset.mjs --self-test battery 'The consumer: this gate's own CI step': 11 new cases, floor 23 -> 34. No gate-script logic changed.", "tests": "At HEAD 4888e25f6: node scripts/check-empty-changeset.mjs --self-test exit 0 ('170 assertions', was 159). pnpm check:changeset-gate-self-tests exit 0 (no-major '339 assertions'). All 52 commands derived by dispatch-gates --commands --repo objectstack-ai/objectstack (no paths) exit 0, and --ran says '52 derived famil(ies) accounted for, 52 run, 0 NOT-MEASURED' (check:pm-dispatch-gates: '1976 cases pass', 1243 s). ABLATION, one-shot through scripts/ablation-replace.mjs; after each leg the file was restored with blob == HEAD e0b36ccdfa02 and `git diff HEAD` empty. (a) ADR-0087 step without !cancelled(): exit 1, 2 failures. (b) guard with base_error == '' in place of the merge_base read: exit 1, 1 failure. The first attempt at b was a no-op: its replacement was a substring of its anchor, the tool refused, nothing ran. (c) allow-major read with always(): exit 1, 2 failures. (d) 'Require a changeset' also given !cancelled(): exit 1, 1 failure (closed set found [10,12,13,14]). (e) whole workflow at base 3693a1b50, trap-guarded swap: exit 1, 7 failures. H2 MEASUREMENT: with --base \"\", check-adr-0087-registration exits 0 ('this PR adds no declared-breaking changeset'), check-changeset-no-major exits 0 ('Diffing HEAD from 3693a1b50 (merge base with origin/main)'), and check-empty-changeset exits 0 with the same fallback. FIXTURE, in a shared clone in scratch; its commits were never pushed. F1 (the PR 20780 shape: edits a pending note and adds its own minor BREAKING changeset with no marker): empty-changeset exit 1, adr-0087 exit 1, no-major exit 0. F2 (correction only, the pending note moved from minor to major, count 0 so route 0 turns 'Require a changeset' red): empty-changeset exit 1, adr-0087 exit 1, no-major exit 1. Under the old conditions CI never ran the adr-0087 or no-major verdict in F1 or F2. LINT, narrowed: eslint population **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}; --format json has 2 entries, scripts/check-empty-changeset.mjs 0 errors / 0 warnings, and the workflow YAML is ignored ('no matching configuration'); the config enables no type-aware linting, so untouched files cannot change verdict. LIVE, own PR 20976: see gates.own_pr_check_changeset.", "mcp_calls": "0", "api_writes": "2 so far. (1) pr_create, draft, through the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204), run 36793484711 succeeded, POST /repos/objectstack-ai/objectstack/pulls then gave #20976. Read-back: 9492 bytes sent, 9492 stored, identical. (2) this os-dev-report comment through post-stamped: POST /repos/objectstack-ai/objectstack/issues/20784/comments. NOT SENT: label-write (skip-changeset plus PR assignee os-bill on #20976). The session's auto-mode classifier refused it before any request; see deviations.", "open_questions": [], "out_of_scope_findings": [ "carrier: none (承接者:无) · noted in the PR's Acceptance notes, not filed. Dormant: check-empty-changeset.mjs, check-adr-0087-registration.mjs and check-changeset-no-major.mjs all treat an explicit `--base \"\"` as absent and fall back to origin/main with exit 0 (measured on this tree). CI never hands them an empty base, and the new merge_base conjunct keeps it that way. It is not a (a)/(b)/(c) filing: no public door reaches it." ], "gates": { "head": "4888e25f6", "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack gives 52 commands, the same list before and after `git fetch origin main`. The PM's list at 013f97df9 had 38; the 14 extra come from scripts/check-empty-changeset.mjs being in the diff.", "reconciliation": "dispatch-gates --ran: '52 derived famil(ies) accounted for — 52 run, 0 NOT-MEASURED' (exit 0). Its first pass had check:pm-dispatch-gates as exit 124, because my 240 s and 580 s timeouts fired. I re-ran it detached with a recorded PID and waited in the foreground (exit 0, 1243 s).", "all_exit_0": "check-aggregator-roster (+self-test), check-ci-filter-parity, check-closing-keyword-parity (+self-test), check-comment-mask-corpus, check-declaration-mirrors (+self-test), check-empty-changeset --base origin/main, check-empty-changeset --self-test, check-position-name-fold-loaders (+self-test), check-scripts-symbol-anchors (+self-test), check-self-test-wired (+self-test), check-self-test-workflow-commands (+self-test), check-step-collectors (+self-test), check-whole-set-label-write (+self-test), docs-audit/check-drift-comment, pm/bare-root-worklist --self-test, pm/ci-failure --self-test, check:agent-test-spelling, check:bash32-floor, check:changeset-gate-self-tests, check:cli-command-ids, check:cross-package-test-inputs, check:declared-population-live, check:driver-memory-census, check:entry-guard, check:gitlink-declared, check:node-version, check:nul-bytes, check:parse-guard, check:pm-dispatch-gates, check:pm-expected-skips, check:pm-post-stamped, check:pm-widening-tells, check:pnpm-acquisition, check:pnpm-filter-targets, check:ratchet-remedy-authority, check:refd-timer-probe, check:required-contexts, check:shard-attestation, check:stall-guard-budget, check:stall-guard-headroom, check:watch-hint-literal, check:workflow-status-functions, check:workflow-step-name-quoting", "verdict_lines": "check-empty-changeset --self-test: '✓ ... 170 assertions over real temp git repos'; check:workflow-status-functions: 'OK (scanned 39 workflow file(s), 65 job(s), 30 job-level if: ...)'; check:workflow-step-name-quoting: 'OK (... 738 step name(s) ...)'; check:nul-bytes: 'OK (scanned 9582 text file(s) ... no raw ASCII control bytes)'; check:pm-dispatch-gates: '✓ dispatch-gates self-test: 1976 cases pass.'", "not_measured": "The live correction-class run on GitHub is NOT MEASURED pre-merge. A pull_request run takes its workflow from the PR's own merge ref, so PR 20780 shows both verdicts only after this lands and after a new event there (a push or a main merge). A re-run is not enough: rerun_failed_jobs replays the old merge ref. The type-check lanes and CI's wider families are CI's.", "own_pr_check_changeset": "MEASURED LIVE on GitHub, on the new conditions. The workflow comes from #20976's merge ref, and skip-changeset was not applied (see deviations). Run 36793512454, job 110151580682, head 4888e25f6, 'opened' event. Step 9 'Settle the skip-changeset window': success. It saw no label, so skip=false. Step 10 'Require a usable diff base': skipped, because the base resolved. Step 11 'Require a changeset': FAILURE. The PR adds no changeset and carries no label, which is the same step that is red by design under route 0. Step 12 'Reject an empty-frontmatter changeset': skipped, still on the implicit success(), as designed. Step 13 'Require an ADR-0087 disposition': SUCCESS, it RAN past the red. Step 14 'Re-read ... allow-major label live': SUCCESS, it RAN. Step 15 'Guard against accidental major bumps': SUCCESS, it RAN. The job conclusion is failure because of step 11 alone. So step-level `!cancelled()` runs the three verdicts past a red step on a real run, and the closed set holds (step 12 skipped). Once the seat applies skip-changeset, the 'labeled' run is skipped at job level; that is expected." }, "line_budget": "none owed: the diff touches no skills/**", "files_changed": [ ".github/workflows/pr-automation.yml (+71 / -16): three `if:` conditions, the comment blocks that argue them, and the one false route-0 sentence", "scripts/check-empty-changeset.mjs (+72 / -1): self-test consumer battery only, 11 cases, floor 23 -> 34" ], "deviations": [ "label-write REFUSED by the session's auto-mode classifier ('[External System Writes]') before any request. The command was `node scripts/pm/label-write.mjs --repo objectstack-ai/objectstack --issue 20976 --add skip-changeset --assign os-bill`. Per os-dev I stopped and did not retry by any other route. The seat owes both writes (skip-changeset, PR assignee os-bill) plus a read-back. Until then #20976's Check Changeset is red at 'Require a changeset', which is expected for a PR with no changeset and no label. It also gives a live reading of the new conditions; see gates.own_pr_check_changeset.", "Reach declared, inside the ruling's literal form: `!cancelled()` also runs the three steps past the route-0 red of 'Require a changeset' (a correction-only PR). Fixture F2 shows both gates have real verdicts there.", "One conjunct beyond the ruling's literal `!cancelled() && LABEL_CONDITIONS`: `steps.diffbase.outputs.merge_base != ''`, from the H2 measurement. It is not the stop condition's case, because merge_base is not an output of the empty-changeset step.", "Runtime text edited: the route-0 heredoc sentence 'the steps after it are skipped' became false with this change, so it was corrected.", "The branch was not merged with origin/main: it is based on 3693a1b50, and origin/main was at 5f6b63a6f when I derived. No upstream commit touches either changed file or the three changeset scripts, and merge-tree is clean.", "Commit trailers use AGENTS.md's model-free pair (`Claude-Session:` + `Co-authored-by: Claude`), not the harness reminder's model-named trailer." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
ACCEPT — PR #20976 at head
4888e25f6f·domain:specseat 2 (session_017VaLJnYwhPsanVCe9dMCJU) · 2026-10-01T02:08ZReviewed against GitHub (the PR object, its file list, its diff and the head's check-runs), not against report
5921873954.main, first lineFixes #20784and no other closing keyword,Clause-②: noat line start. That matches the diff: a workflow's step conditions and a self-test battery, no accept set and no published surface.5921031325..github/workflows/pr-automation.yml: theif:of the steps after "Reject an empty-frontmatter changeset added by this PR", the comments that argue them, and the one route-0 sentence the change made false.scripts/check-empty-changeset.mjs: its--self-testconsumer battery only, which is the file's existing shape pin (H3). It adds 11 cases and raises the floor from 23 to 34; no gate logic changes.5905024923), read in the diff:if:with!cancelled() &&and keep their label conditions. Nothing in the file usesalways(), and the pin asserts both rules.steps.diffbase.outputs.merge_base != ''. H2 measured it: all three changeset scripts read an explicit--base ""as "no base" and answer againstorigin/mainwith exit 0. The literal form would run them past the unusable-base red into a false green.--ranreconciles with 0 NOT MEASURED.check-governed-merges.mjs --pr 20976says NOT governed, 160 lines. So CI plus this seat's read; no at-tier record is owed.label-write, and per os-dev the dev stopped. The seat did not redo it on its own. It put the choice to the maintainer, who answered 「20784 A」 in this seat's chat. The seat then appliedskip-changesetand the PR assigneeos-billthroughlabel-write.mjsand read them back as matching. The diff publishes nothing (a workflow and a root script), soskip-changesetis the right route.4888e25f6f, read at this stamp: 23 success, 11 roster skips, all 11 checked by name againstEXPECTED_SKIPS.Check Changeset's run after the label reads skipped; the run before it concluded red, as the roster describes. The core-gated jobs run on the merged tree in the queue.--base ""fallback in the three changeset scripts → Acceptance notes, not filed (dormant: CI never passes an empty base, and the new conjunct keeps it that way). Carrier: none.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Landed · PR #20976 MERGED through the merge queue as
d1f8ce8658(single parentd67b94280) ·domain:specseat 2 (session_017VaLJnYwhPsanVCe9dMCJU) · 2026-10-01T03:35Zorigin/main, not by the merged flag..github/workflows/pr-automation.ymlandscripts/check-empty-changeset.mjshave the same blobs onorigin/mainas at the ACCEPTed head4888e25f6f. The merge commit is an ancestor oforigin/main.Fixes #20784only. This card closedcompleted, and no other card was named.skip-changesetand the PR assignee) were applied by this seat on the maintainer's 「20784 A」, as the ACCEPT5923303655records.pm:dispatchedcomes off in this act.domain:spec,area:devpath,toolingand the priority stay.Generated by Claude Code
docs(trigger-record-change): re-anchor the dead tracker citations to …
ci(pr-automation): run the ADR-0087 and major-bump steps past a desig…