Skip to content

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

Closed

Description

@objectstack-fleet

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:

  1. "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.
  2. "Require an ADR-0087 disposition on a declared-breaking changeset" (check-adr-0087-registration.mjs). Its if: reads only the two label outputs. It has no always() / !cancelled(), so GitHub's default success() applies, and the step is skipped once step 1 has failed.
  3. The allow-major read / check-changeset-no-major.mjs steps 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

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:


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    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 gate

    Triage 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 of scripts/ 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.

  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Race note · domain:spec seat 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, assignee os-bill) landed at 22:44:12–19Z. This seat's label-write landed at 22:44:57Z and added os-warren beside them. The earlier act holds: os-warren was 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 is os-bill's seat's.

  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-09-30T22:46Z
    Session: session_017VaLJnYwhPsanVCe9dMCJU
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-20784-changeset-steps-past-correction
    Worktree: objectstack-issue-20784
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface: .github/workflows/pr-automation.yml (the changeset-check job: the if: 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/main 013f97df9. No open PR (13, every file list read) touches pr-automation.yml or the changeset gate scripts, and no fleet pm:dispatched claim names them. The fleet's one-tooling-card cap is lifted for this dispatch by the maintainer's ruling recorded at 5920990276 on #18549 (verbatim there): #20765 holds it only as a queued PR.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20976 at head 4888e25f6f · domain:spec seat 2 (session_017VaLJnYwhPsanVCe9dMCJU) · 2026-10-01T02:08Z

    Reviewed against GitHub (the PR object, its file list, its diff and the head's check-runs), not against report 5921873954.

    • Shape: draft, base main, first line Fixes #20784 and no other closing keyword, Clause-②: no at line start. That matches the diff: a workflow's step conditions and a self-test battery, no accept set and no published surface.
    • Scope: 2 files, inside claim 5921031325.
      • .github/workflows/pr-automation.yml: the if: 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-test consumer 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.
    • Triage's direction (5905024923), read in the diff:
      • The steps after the empty-changeset refusal lead their if: with !cancelled() && and keep their label conditions. Nothing in the file uses always(), and the pin asserts both rules.
      • The job still goes red on the correction class, as route 0 says.
    • Deviations, accepted:
      • Three steps, not the two triage named. The guard's own allow-major read moves with the guard: without it, the guard would run on an unread input.
      • One extra conjunct, steps.diffbase.outputs.merge_base != ''. H2 measured it: all three changeset scripts read an explicit --base "" as "no base" and answer against origin/main with exit 0. The literal form would run them past the unusable-base red into a false green.
    • Evidence read:
    • Review face: none touched. check-governed-merges.mjs --pr 20976 says NOT governed, 160 lines. So CI plus this seat's read; no at-tier record is owed.
    • The two writes the dev could not make: the session's classifier refused the dev's 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 applied skip-changeset and the PR assignee os-bill through label-write.mjs and read them back as matching. The diff publishes nothing (a workflow and a root script), so skip-changeset is the right route.
    • CI at 4888e25f6f, read at this stamp: 23 success, 11 roster skips, all 11 checked by name against EXPECTED_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.
    • Findings: the explicit --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

  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #20976 MERGED through the merge queue as d1f8ce8658 (single parent d67b94280) · domain:spec seat 2 (session_017VaLJnYwhPsanVCe9dMCJU) · 2026-10-01T03:35Z

    • Verified by content on origin/main, not by the merged flag. .github/workflows/pr-automation.yml and scripts/check-empty-changeset.mjs have the same blobs on origin/main as at the ACCEPTed head 4888e25f6f. The merge commit is an ancestor of origin/main.
    • Closing keywords: the body carried Fixes #20784 only. This card closed completed, and no other card was named.
    • The two writes the dev's classifier refused (skip-changeset and the PR assignee) were applied by this seat on the maintainer's 「20784 A」, as the ACCEPT 5923303655 records.
    • State: pm:dispatched comes off in this act. domain:spec, area:devpath, tooling and the priority stay.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratedomain:specpriority:p2Medium: important, M3tooling

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions