Skip to content

ci: wire the issue-citation verdict (blocking) and its census (report-only), plus the merged-result probe - #19259

Merged
os-elon-musk merged 12 commits into
mainfrom
claude/issue-18224-wire-issue-citations-to-ci
Sep 20, 2026
Merged

os-elon-musk merged 12 commits into
mainfrom
claude/issue-18224-wire-issue-citations-to-ci

Conversation

@os-try-charles

@os-try-charles os-try-charles commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #18224

Two gates were registered in the root manifest and invoked by zero workflows:
scripts/check-issue-citations.mjs (delivered by #18223 / card #17512) and
scripts/check-merged-result.mjs (delivered by #18338 / card #16287). Both cards' declared
file surfaces excluded .github/workflows/**, so both devs correctly stopped and filed the
wiring rather than widening. This PR is that wiring.

The three entry points, and their postures

entry lane posture
the DIFF-scoped citation verdict lint.yml · Lint & Repo Gates blocking
check:merged-result self-test lint.yml · Lint & Repo Gates blocking
check-issue-citations --census half-state-patrol.yml (scheduled) report-only, never blocking

The census posture is a ruling, not a preference, and the card carries the measurement that
forces it: #16783, #16786 and #16787 were measured RESOLVING on 2026-09-10 and 404 on
2026-09-14, with no change to this tree.
A tree-wide blocking verdict would therefore red a
motionless repository because a third party deleted an issue. The diff-scoped half is the part
an author owns, it is small, and it is the only thing that stops 2,785 unresolvable sites
becoming 3,000.

The manifest alias is NOT the verdict

package.json maps check:issue-citations to node scripts/check-issue-citations.mjs --self-test
and nothing else -- the shape every credential-needing gate in this manifest uses
(check:pm-half-states, check:pm-closed-card-sweep), because a live mode needs a board and a
credential. Wiring that alias alone would have run the self-test twice and scanned nothing. So
the lint step holds two commands, self-test first:

pnpm check:issue-citations && node scripts/check-issue-citations.mjs

The second spelling is lint.yml's documented gate-invocation idiom (dispatch-gates.mjs's own
header names it), and the first is what check-self-test-wired requires of every script CI runs.

check:merged-result needed no such split: the manifest key already IS
node scripts/check-merged-result.mjs --self-test, which is the whole gate.

package.json is deliberately untouched

No new manifest key was added. Both keys already exist and both are now named by lint.yml, so
the wiring needs no manifest edit, and leaving the file alone keeps this PR textually disjoint
from PR #18414, which adds a key two lines from where a new one would have gone.

The non-step changes: issues: read AND pull-requests: read

⚠️ Heading corrected by the dispatching PM at 2026-09-20T07:25Z: this section was written when the block
gained ONE scope. It now adds TWO — pull-requests: read followed from a measurement taken after
the body was written (patrol run 35495222460 censused 3628 unresolvable sites on a token without it,
run 35496169133 on the fixed head censused 2167, the exact full-scope reading; the 1460 difference is
exactly the resolves-as-pull-request tally, because GET /issues omits pull requests without that scope).
⛔ Nothing else in this body was altered.

The Lint & Repo Gates job's permissions: block gains issues: read. An explicit
permissions: block sets every unnamed scope to none; this board is public today, but a
blocking gate whose transport depends on repository visibility is a gate that goes red on a
settings change no file in this repo can assert. Read-only, one scope wide: the gate never
writes an issue, a comment, a label or an assignee.

Acceptance 2 -- both directions measured, on the CI side

PR #18223 measured the red/green pair on the gate side. This card asks for the same pair on
the CI side. Two instruments, both here.

A. The commits on this branch ARE the CI-side ablation. test(ci): ABLATION LEG 1 of 2
adds one citation naming a number beyond this board's allocation frontier, in a declared surface
(packages/**/src/**/*.ts, comment-prose projection). ABLATION LEG 2 of 2 removes it and
restores the file to the byte. The run on the first head is the red reading; the run on the final
head is the green one. Both run identifiers are recorded in the dev report on the card.

B. The exact command the new step holds, ablated locally with disk evidence. Three legs, run
from a committed state, each restored with git checkout HEAD -- path and proven by hash:

target                packages/types/src/index.ts
HEAD blob             0235d4e7d2ebe529c7101b5e1e004597b30c4554

leg 1  unresolvable   marker 0 -> 1   blob 0235d4e7d -> 2ac9ba333   exit 2   RED
       finding: [never-issued] packages/types/src/index.ts:8  #999999
                beyond the allocation frontier (19258) -- this number was never minted

leg 2  restored       marker 1 -> 0   blob back to 0235d4e7d       exit 0   GREEN
       `git diff HEAD` empty after the restore

leg 3  resolvable     cites #17512 instead                          exit 0   GREEN
       board: probed (1 citations), frontier #19258, 2 numbers resolve

Leg 3 is the control on leg 2: a green produced by a live board read, not by a run that never
asked the board anything.

Acceptance 3 -- where the census reports, how often, who pays

Written into the step itself, and repeated here:

  • Where. The patrol run's step summary and job log, plus one ::warning:: carrying the
    site count. Deliberately not the anchor issue: that body is owned end to end by
    check-half-states.mjs's generator, and a second writer is how half a generated body goes stale.
  • How often. This workflow's schedule -- four times a day, six hours apart (37 1,7,13,19) --
    plus any workflow_dispatch, plus the pull_request runs the paths filter admits. A row for
    scripts/check-issue-citations.mjs was added to that filter for the reason the file already
    gives for its two siblings: a step whose script can change without the trigger firing is a step
    whose PR-time proof is a coincidence.
  • Who pays the 159 requests/run. This repository's own secrets.GITHUB_TOKEN core quota --
    the same 5,000/hour the job already draws the live sweep from. Four runs a day is roughly 636
    requests/day, under half a percent of a single hour's allowance. No PAT and no cross-repo
    credential, per this file's own standing rule.

The step is gated on github.repository == 'objectstack-ai/objectstack', exactly as the
closed-card sweep above it is, because half-state-patrol.yml is copied verbatim into sibling
repos that do not carry this script -- a copy must skip the step, not fail on a missing file. It
is placed after the anchor write, unlike the closed-card sweep: the anchor is this patrol's
product, the job has a 15-minute timeout, and a report-only reading must never be able to starve
it. It always exits 0.

Acceptance 4 -- the 422 wall

This diff touches .github/workflows/**, which is outside the PM seat's arming channel: the
seat's auto_merge answers HTTP 422 for this PR. It merges by a human. That is the same
wall as PR #18096 and #18341, it is not a tool fault, and it must not be retried. No seat should
undraft this PR or arm auto-merge on it.

Note that .github/workflows/** is not on the GOVERNED_SURFACES register in
scripts/pm/check-governed-merges.mjs, so the governed-merge machinery is not what holds this
one -- the 422 is.

Placement, and the three in-flight PRs on these files

Read before editing: #18414 (lint.yml + package.json, green, awaiting a human merge),
#19024 (lint.yml), #19225 (half-state-patrol.yml, draft and frozen). Only additions here; no
existing step was moved, renumbered or reformatted.

Measurement this PR does not relay

The card's prose carries three disagreeing counts for the manifest census. Re-measured on this
branch's base 0f42d36ff, with a firing control and a dark control:

root manifest check:* keys                 165
not named by any workflow (name or path)     2   -> check:merged-result, check:issue-citations
...and not named by any other script         2
firing control 'check:nul-bytes' in a wf   true
dark control   'check:zznotreal' in a wf   false

Both unreached keys are the two this PR wires, so the reading after this lands is 0.

Acceptance notes

  • check-self-test-wired admits a script when a workflow names it directly or through a root
    manifest alias, repo-wide rather than per workflow, so the patrol's census step needs no second
    self-test invocation: the lint step above already runs it.
  • No changeset: nothing any package publishes moves. Workflow files ship in no package's files[].

Generated by Claude Code


Generated by Claude Code

Two gates were registered in the root manifest and called by zero workflows:
`scripts/check-issue-citations.mjs` (delivered by #18223) and
`scripts/check-merged-result.mjs` (delivered by #18338). Both cards' file
surfaces excluded `.github/workflows/**`, so both devs correctly stopped and
filed the wiring instead of widening.

Three entry points, two lanes, two deliberately opposite postures:

- `lint.yml` / `Lint & Repo Gates`: the DIFF-SCOPED citation verdict, blocking.
  Two commands in one step -- the manifest alias (which is the checker's own
  `--self-test` and nothing else) and then the live diff run. Wiring the alias
  alone would run the self-test twice and scan nothing.
- `lint.yml` / `Lint & Repo Gates`: `check:merged-result`, blocking. Offline,
  no credential, sub-second.
- `half-state-patrol.yml`: `--census`, REPORT-ONLY, four times a day. The
  census verdict moves on a motionless tree -- three numbers in its own
  control set went 404 in four days with no change here -- so a tree-wide
  blocking verdict would red a repository nobody touched.

The lint job gains `issues: read`: an explicit `permissions:` block sets every
unnamed scope to `none`, and a blocking gate must not depend on repository
visibility for its transport.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
…st go red

Temporary, and reverted by the next commit on this branch. The card's
acceptance asks for the red/green measurement to be taken ON THE CI SIDE, not
only on the gate side (#18223 already did the gate-side pair with disk
evidence). This commit is the red leg: one citation naming a number beyond
this board's allocation frontier, inside a declared surface
(`packages/**/src/**/*.ts`, comment-prose projection).

Expected: the `Issue citations this change adds resolve on the board` step in
`Lint & Repo Gates` exits 2 and the job goes red. The next commit removes the
line and the same step must go green.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
@os-try-charles os-try-charles added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 20, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/types/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/types/src/index.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 0f42d36ffcf2dd094911eb1ee45f3cc0061754e3 → packageMentionDocs.

… real runner

The census step landed on a runner in this PR's own patrol run and reported
3,628 unresolvable citation sites. A full-scope read of the same tree reports
2,168. The difference is 1,460 — EXACTLY the `resolves-as-pull-request` tally,
so every citation naming a PR number was classified as a number the board
never had.

Cause: `GET /repos/{owner}/{repo}/issues` omits pull requests unless the token
holds `pull-requests`, and an explicit `permissions:` block sets every unnamed
scope to `none`. The same call is how the gate reads the allocation frontier
(`?per_page=1&sort=created&direction=desc`), so an understated frontier turns
later numbers into `never-issued` as well.

Both lanes take the row, read-only:

- `half-state-patrol.yml`: a report-only reading wrong by 67% is still a
  machine-readable surface telling a lie.
- `lint.yml`: on the BLOCKING step it is worse than a wrong number — it is a
  false red on a correct citation.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
…must go green

The reset half of the CI-side ablation the card asks for. `packages/types/src/
index.ts` returns to the blob it carried before leg 1
(`0235d4e7d2ebe529c7101b5e1e004597b30c4554`), marker count 1 back to 0, and
this branch's delta against `main` is again the two workflow files alone.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
@os-elon-musk
os-elon-musk marked this pull request as ready for review September 20, 2026 14:29
@os-elon-musk
os-elon-musk added this pull request to the merge queue Sep 20, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 20, 2026
… queue ahead of it

This step's first queue build ejected the PR that added it, and the reason was
neither the tree nor a crash: `exit 2` is this gate's own FINDINGS code, and the
findings were real citations written by somebody else.

MEASURED on queue entry `a7109d1f08`. A queue entry is built on the GROUP's
base, which carries the entries AHEAD of it in the queue and has not landed on
`main` yet. `merge-base origin/main HEAD` therefore lands at the PUBLISHED tip:
`231283a6e` at 14:30:14Z, while the group's base `8271c81425` reached `main`
only at 14:47:59Z. Everything between the two read as "added by this change" --
15 file(s) / 16 citations judged, 3 unresolvable, and all three written by the
two entries ahead: `#6361` twice from `ada701220` (#19364), `#18003` from
`8271c81425` (#19363). Against the group's own base the same tree judges 0
file(s). The Governed Surface Queue Guard, in the same build, read
`merge_group.base_sha` and correctly saw 1 commit and 178 changed lines.

Two halves, and the second is not cosmetic:

1. `lint.yml` declares the base -- `OS_GATE_MERGE_GROUP_BASE_SHA`, the name and
   the expression this file already uses for that fact. It renders empty on
   `pull_request` and `push`, where the ref guesses are CORRECT and are kept: a
   PR's merge ref already contains the main it was computed against.
   The step is not `if:`-skipped on `merge_group` -- this file asserts that
   every gate step here runs there.

2. The gate verifies the base resolves before handing it to `git diff`. It did
   not: an unresolvable `--base` threw `fatal: bad object` and exited 1, a
   failed read wearing a code that is neither the clean answer, the findings
   answer, nor the refusal. It now refuses with PREREQUISITE NOT MET (exit 3)
   and names every spelling tried and what to pass instead. Half 1 alone is
   inert -- the pre-change gate ignores the variable entirely -- and half 1 is
   what makes an unverified base reachable, so both are required.

Firing controls, on the real queue tree with `origin/main` pinned to the
published main of 14:30:14Z: ref guess -> exit 2 with the three findings,
reproducing the ejection; declared base -> exit 0; declared base absent from
the checkout -> exit 3; `--base` absent -> exit 3 (was: uncaught throw, exit 1).
`--self-test` covers all of it: 66 -> 73 cases, 7 batteries, and the
`diff-scope` battery floor moves 6 -> 13 so the new cases cannot stop running
unnoticed.

Also corrects the gate docblock sentence this wiring falsifies ("Neither is
installed here").

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
PR #19225 made the half-state patrol callable: the patrol's steps now live in
the composite action `.github/actions/half-state-patrol`, and the workflow
`uses:` it. This branch adds the report-only `--census` leg of the citation
gate to the OLD shape, so the two sides overlapped in two places.

Conflict 1 (`on.pull_request.paths` + the `permissions:` note) is additive in
both directions: the branch's `scripts/check-issue-citations.mjs` trigger row
and main's `.github/actions/half-state-patrol/**` glob name different files,
and main's rewritten `issues: write` paragraph is kept with this branch's
`pull-requests: read` paragraph appended under it.

Conflict 2 is not textual. Main emptied that region -- every step in it moved
into the composite action -- and the census step is the one thing there that
main did not relocate. It stays a step of the CALLER, and the placement is the
argument rather than an accident:

  - `scripts/check-issue-citations.mjs` is objectstack-only, so inside the
    action its "Locate the patrol sources" step would either have to name it
    and refuse to run in every sibling that adopted the action, or not name it
    and fail on a missing file there. The repo-name gate only works in the
    caller.
  - The action runs its scripts from `steps.sources.outputs.root` (the tree the
    action ships from) while the board's checkout is `github.workspace`. A
    census of the WORKSPACE tree run from inside the action would, in a
    sibling, census this repo's release pages and report the count under the
    sibling's name.
  - `pull-requests: read` is granted by this workflow's `permissions:` block,
    which a composite action cannot carry and the action's inputs do not name.

One behaviour had to be spelled rather than inherited: the step's guard is now
`${{ !cancelled() && github.repository == '...' }}`. In the old shape the
census sat above the only step that failed the job, so it ran whatever the
sweep returned; the patrol is now one `uses:` step that goes red itself, and a
default `success()` would have skipped the census on exactly the runs where the
patrol is down. This is the guard main gave the closed-card sweep one step up,
for the reason its own comment states.

Nothing else changed: the blocking diff-scoped step in `lint.yml` and the
`OS_GATE_MERGE_GROUP_BASE_SHA` declaration are untouched, and the census
command stays a bare literal path so the gate derivation keeps seeing it.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator

os-dev-report

{
"issue": 18224,
"status": "done",
"branch": "claude/issue-18224-wire-issue-citations-to-ci",
"pr": "#19259",
"session": "session_019srGWGCBBCBHqcDoRZpQRh",
"premise_still_valid": true,
"summary": "Conflict-resolution round only. Merged origin/main (43d492f, PR #19225) into the branch as a MERGE commit bfe3abc and resolved the two hunks in .github/workflows/half-state-patrol.yml. HUNK 1 (on.pull_request.paths plus the permissions note) is additive in both directions: kept this branch's scripts/check-issue-citations.mjs trigger row AND main's .github/actions/half-state-patrol/** glob, and kept main's rewritten issues-write paragraph with this branch's pull-requests-read paragraph appended under it; the pull-requests: read row itself auto-merged. HUNK 2 is not textual: main EMPTIED that whole region because #19225 moved every step in it into the composite action, and the census step is the one thing there that main did not relocate. It stays a step of the CALLER, after the uses: of the action and after the closed-card sweep, with three placement reasons written into the step. Exactly one behaviour changed, deliberately and declared: the guard is now the same shape main gave the closed-card sweep, not-cancelled AND the repository name, because in the old shape the census sat above the only step that failed the job and so ran whatever the sweep returned, whereas the patrol is now a single uses: step that goes red itself and a default success() would have skipped the census on exactly the runs where the patrol is down. Diff versus origin/main: 3 files, 374 insertions, 13 deletions, and ZERO lines of main's copy of the patrol workflow removed; lint.yml and scripts/check-issue-citations.mjs are byte-identical to the pre-merge tip 7d60a60, so both halves of the accepted merge_group fix survive untouched. PR head is bfe3abc and mergeable_state moved from dirty to blocked (mergeable: true).",
"tests": "ALL exit codes captured before any pipe. (1) node scripts/check-issue-citations.mjs --self-test: EXIT=0, 'grammar narrowed, four 404 causes kept apart, both board strategies agree, diff scope red AND green, scope contract pinned (73 cases, 7 batteries)'. 73 is unchanged. (2) node scripts/check-issue-citations.mjs: EXIT=0, 'no issue citations added against 43d492f (0 file(s) read)'. Zero files because this diff touches none of the gate's declared surfaces (release pages, package source docblocks). (3) DERIVATION after the resolution, the dispatched command with both paths: EXIT=0, and the census family is still derived. Verbatim row: 'node scripts/check-issue-citations.mjs --census [half-state-patrol.yml] matched via .github/workflows/half-state-patrol.yml, CI trigger in half-state-patrol.yml; and via .github/actions/half-state-patrol/action.yml, CI trigger in half-state-patrol.yml .github/actions/half-state-patrol/**'. It sits in the value-bearing-argv-or-env block (NOT RUNNABLE LOCALLY, 1 value from the workflow: env GITHUB_TOKEN), the identical classification it carried BEFORE the merge, so the resolution lost nothing. (4) RECONCILIATION on the final commit bfe3abc: 56 derived, 56 run, 0 NOT-MEASURED, 0 UNRUN; dispatch-gates --ran EXIT=0; every one of the 56 recorded an exit code and all 56 are 0. check:pm-dispatch-gates ran for real (837.1s, 1883 self-test cases pass). One gate needed two runs: pnpm check:type-check-debt first exited 3, PREREQUISITE NOT MET, because its --re-measure leg needs the built workspace closure; I built the closure through the shared verify lock (turbo run build over the packages, 72 of 72 tasks, lock VERDICT command-exit 0, 428s held) and re-ran it: EXIT=0, '4 ledger entries re-measured in 19.7s, 53 raw tsc errors total, none above its recorded number'. The recorded exit for that row is the re-run's 0. Re-derived after origin/main moved on to 0870fb5: the 56-command set is byte-identical, so no newly-derived family is owed. (5) YAML PARSE of the resolved workflow with the yaml package 2.9.0: parses; top-level keys name, on, permissions, concurrency, jobs; permissions equals contents read, issues write, pull-requests read; pull_request.paths equals the four script rows, then the action glob, then the workflow file; job patrol has 5 steps. [0] Checkout repository. [1] Setup Node.js. [2] Sweep the board and update the anchor, uses ./.github/actions/half-state-patrol with github-token and anchor-issue. [3] Sweep the closed cards, not-cancelled AND repo name. [4] Census the repo's issue citations, not-cancelled AND repo name, env GITHUB_TOKEN, a 31-line run whose only citation command line is the BARE LITERAL 'node scripts/check-issue-citations.mjs --census'. A comment-stripped diff of the census step before versus after the resolution shows exactly one changed line, the if:. No ablation was owed: this round adds no gate and changes no checker.",
"mcp_calls": "0 - no MCP GitHub tool was called, for reading or for writing.",
"api_writes": "1 git push (7d60a60..bfe3abc on claude/issue-18224-wire-issue-citations-to-ci; pre-push check:commit-card-trailers passed, no card relation and no model identifier in the trailer pair), 1 read of pull 19259, and 1 attempted comment on pull 19259 carrying this report. No label, body, title, draft, assignee, queue or auto-merge write, and no write of any kind on card #18224.",
"open_questions": [
{
"question": "Report channel: this dispatch forbids posting on card #18224 and says the seat carries the report, while the standing os-dev contract and AGENTS.md's dispatch-protocol paragraph both make the executor's own report COMMENT its record on GitHub, GitHub-first.",
"options": [
"A. Post the report as a comment on PR #19259: keeps a GitHub-side record and touches neither the card nor the PR body, title, labels or state.",
"B. Post nothing and rely on the seat relaying the handback: obeys the dispatch literally, but leaves no GitHub record if this session dies between the push and the handback.",
"C. Post on card #18224 as the standing contract literally says: breaches an explicit dispatch prohibition on a card the seat owns."
],
"recommendation": "A, because it satisfies both instruments at the cost of one additive comment, on the surface this round's reviewer reads. Flagged rather than silently chosen."
}
],
"out_of_scope_findings": [
"noted, not filed: THE DISPATCH'S DERIVATION CLAIM IS FALSE AS STATED. 'If you put the census invocation inside the composite action, the derivation stops seeing it' is measured false: followCompositeActions (#19229) reads local composite-action steps into the CALLING workflow's invocation set. Positive control: the check-half-states invocation exists ONLY in the action file and IS derived; and the counterfactual, my census step pasted verbatim into the action's runs block and driven through extractCheckInvocations, derives the family with viaAction equal to .github/actions/half-state-patrol/action.yml. What IS real is the path-spelling half: all four spellings the dispatch names derive ZERO. So the derivation was never a reason to prefer the caller; the three structural reasons written into the step are. Successor: the dispatching seat, this round.",
"to file (3 classes; dedupe words: extractCheckInvocations, quoted literal path, DIRECT_CHECK_INVOCATION, derivation blind spot): the quote alone defeats the matcher, no variable needed. A double-quoted but otherwise literal script path derives ZERO families, exactly like the dollar-bearing spellings. That is a fully static, paste-runnable command the derivation cannot see, which is class (c), a trap that makes a writer's metadata (the derived gate family) silently disappear. Card #19395 already owns the open class, so this is one more measured spelling to add ON #19395 rather than a new card; I wrote nothing to it.",
"noted, not filed: two dispatch or harness details measured false and harmless. (i) The probe worktrees /home/user/wt-conflict and /home/user/wt-v19259 do not exist in this container - the harness also declared /home/user/wt-conflict as this session's primary working directory and as a git worktree, and it was absent - so there was nothing to avoid reusing; I created /home/user/wt-18224-conflict as instructed. (ii) dispatch-gates reports its change set with THREE-dot semantics and says so in its own stderr, 'three-dot semantics, never origin/main..HEAD', so the sister-PR attribution hazard my standing instructions warn about does not apply to this tool. Successor for both: the seat, when it next writes a dispatch."
]
}


Generated by Claude Code

@os-elon-musk
os-elon-musk added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit 5a5e710 Sep 20, 2026
33 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-18224-wire-issue-citations-to-ci branch September 20, 2026 17:59
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…and refuses a foreign one (objectstack-ai#19288)

Fixes objectstack-ai#19191

Clause-②: no

`scripts/pm/check-half-states.mjs` took three readings off the LOCAL
checkout with plain
`execFileSync` git calls and no `cwd`, so each inherited `process.cwd()`
while the board it reports on
is named by `PM_SWEEP_REPO`. The filer measured both directions on the
same board with the same script
(objectstack-ai#19191, comment 5740947564): with `cwd` in objectstack, H17's
trigger-file index validated objectui's
candidate paths against **objectstack's 8888** tracked files (objectui
had **7945**) and H57 read
NOTHING for the whole run — and the row it never read was hiding a
scheduled lane dead through five
consecutive fires (objectstack-ai/objectui#10010). Internally
consistent, externally wrong.

## 维护者速读(草稿)

**改了什么** —— 给这个巡检工具加了一个环境变量 `PM_SWEEP_CHECKOUT`,用来回答「哪个本地检出服务于这块
board」。设了它,三处本地 git 读取(H17 的 tracked-file 判据、H57 的 workflow
文件、`origin`)就都在那棵
树上读;那棵树的 `origin` 不是被扫的仓时,整轮巡检响亮拒绝(退出码 3),⛔ 绝不静默换一棵树读。不设它,
行为与今天逐字一致 —— 只多了一句:H17 页脚现在写明它到底在哪棵树上读的(路径 + `origin`)。

**为什么改** —— 这不是「跑错目录」的操作失误,而是一个没有症状的读数缺陷。跨仓巡检是常态(工具住在
objectstack,board 可以是 objectui),而 H17 的页脚正是在告诉派发席位「派发前请拿你的文件面和这张清单求
交集」。清单是拿另一个仓的文件列表验过的,只在另一个仓存在的触发文件被静默丢弃 —— 席位读到的是一份干净
清单,而那份干净是假的。H57 那半反而是诚实的:它明说自己什么都没读,代价是那一轮真有红行没人看见 ——
本 PR 的实测里那是两行,其中一行连卡都没提到。

**风险与代价(含回滚)** —— 风险面很窄:变量不设时是逐字旧行为(自测里有一条专门盯这件事),巡检
workflow 本身不设它,所以线上那条 lane 的行为不变。新增的唯一失败模式是「变量设错路径」,而那正好是本卡
要的那个响亮拒绝。回滚 = revert 这两个 commit,没有数据迁移、没有已发布面(`scripts/pm/**` 不随任何包
发布,故无 changeset)。

**席位意见** ——

**你要做的** —— 无需维护者动手。若希望巡检 workflow 把这棵树写明(目前不需要,因为 runner 的检出就是被扫
的仓),那是 `.github/workflows/half-state-patrol.yml` 的一行 env,已写在下面的
Acceptance notes 里,留给
单独一个由人合的改动 —— 本 PR ⛔ 不碰 workflow(objectstack-ai#19259、objectstack-ai#19225 正在改它)。

## What changed, mechanically

- **`PM_SWEEP_CHECKOUT`** — one knob, beside `PM_SWEEP_REPO` and
`PM_SWEEP_CLOSED_FLOOR` in `--help`,
carrying the PATH of the checkout that serves the board.
`resolveSweepCheckout(env)` resolves and
  trims it; whitespace is unset.
- **One git read site.** All three readers (`readTrackedFiles`,
`readRepoRoot`, `readOriginUrl`) now go
through a single `gitRead(args, extra)` helper that passes that path as
`cwd`. That is the only
`execFileSync` git call left in the file, and the self-test pins the
count at one — so a fourth
reader cannot be added later without the `cwd`. That is the card's
mechanism assumption turned into a
  measured property instead of a belief.
- **The refusal.** `checkoutPrerequisite(sweepRepo, env, originUrl)` is
a pure verdict in the shape
`reportPrerequisiteNotMet` prints: `null` when the knob is unset
(today's behaviour is not a
prerequisite) or when the named tree really serves the board, otherwise
the file's own exit-3
PREREQUISITE NOT MET, named. It is answered FIRST in `sweep()` — ahead
of the transport probe — so a
  foreign checkout costs zero requests and reads nothing.
- **`localCheckoutServes` resolves the same way**: the knob leg first
(it names the tree, so the
`origin` read IN that tree is the authority), then `GITHUB_REPOSITORY`
(which names the RUNNER's
tree, i.e. exactly not the knob's), then the checkout's `origin`. H57
therefore judges when the knob
  names the right tree, instead of refusing for the whole run.
- **The H17 footer names the tree** it read — path plus `origin` —
beside its oracle size, in BOTH
oracle states (read, and EMPTY BY FAILURE), knob set or not. A
wrong-tree reading is internally
consistent; the tree it names is the only thing that tells it from a
right one.

## Readings

| reading | before | after |
|:--|--:|--:|
| `scripts/pm/check-half-states.mjs` lines | 36,176 | 36,256 — net
**+80**, budget +80 |
| `--self-test` cases | 5,063 | 5,081 (+18), exit 0 |
| battery roster | 6 batteries, each above its pin | unchanged, each
above its pin |

Derived gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at this
branch → 39 runnable commands, all run locally with the exit code
captured BEFORE any pipe, all 0. The
per-command verdicts are in this card's `os-dev-report` comment.
Control-character self-scan over the
changed file: no match. No package contains `scripts/pm/**`, so no
dependency-closure build and no
package test suite is owed; repo-wide scans stay CI's.

## The firing pair, live

Both legs ran at this branch's head against the objectui board, from the
objectstack tool.

**Knob = the tree that serves the board** —
`PM_SWEEP_REPO=objectstack-ai/objectui` with
`PM_SWEEP_CHECKOUT=/home/user/objectui`, detached under the shared
heavy-verify lock (held 398s, waited
0s — shared-box seconds, not idle-box figures): **exit 0**, 502
half-states over 374 open
pm-/p0-labeled issues. The H17 footer:

```text
(read on 75 of 75 open `pm:on-hold` card(s); 8142 tracked file(s) in the oracle.) Read in `/home/user/objectui` (`origin` `https://github.com/objectstack-ai/objectui`).
```

8142 is objectui's LIVE tracked count today (`git -C /home/user/objectui
ls-files | wc -l` = 8142); the
filer read 7945 on 2026-09-19 and the board has moved since. This
worktree reads 9031, so the two trees
are still 889 apart — the number that never used to appear is now the
one printed, with the tree it came
from beside it.

H57 is judged rather than UNRESOLVED: `10 workflow(s) on the swept repo
declare a schedule; 8 were
judged against their latest event=schedule run and 0 are UNJUDGED rather
than clean because that read
failed.` It filed **two** red rows, neither reachable by any wrong-cwd
sweep:

- `.github/workflows/changeset-release.yml` — latest scheduled run
`35493590744`, started
2026-09-20T06:11:39Z, concluded `failure`. This is the lane the card's
re-run traced to
  objectstack-ai/objectui#10010, still dead today.
- `.github/workflows/check-links.yml` — latest scheduled run
`35489310168`, started
2026-09-20T04:30:22Z, concluded `failure`. Not named anywhere in the
card.

**Knob = a foreign tree** — same board,
`PM_SWEEP_CHECKOUT=/home/user/objectstack`: **exit 3**, nothing
swept, nothing spent:

```text
check-half-states: PREREQUISITE NOT MET — PM_SWEEP_CHECKOUT="/home/user/objectstack" does not serve `objectstack-ai/objectui`

  this checkout's `origin` is `objectstack-ai/objectstack` while the sweep reads `objectstack-ai/objectui`.

  H17 validates every on-hold trigger path against that checkout and H57 classifies its workflow
  files there, so a foreign tree renders an index whose paths were checked against another repo.

  Fix:  point PM_SWEEP_CHECKOUT at the checkout whose `origin` IS the swept board,
        or unset it and run the sweep from inside that checkout.
```

## Reverse verification — both new pins can fail

Each ran from the COMMITTED state, mutated through
`scripts/ablation-replace.mjs` (which proves the
write landed on disk and restores byte-exactly), and each turned exactly
ONE case red out of 5,081.
The committed blob is `f49d566ed276`.

| ablation | mutation landed | case that went red | restore |
|:--|:--|:--|:--|
| strip the `cwd` from the one git-read site | anchor 1 to 0, blob
`f49d566ed276` to `994882b2fcfc` | `objectstack-ai#19191 knob: …and that one site
takes the knob as its cwd` — got false, want true | blob back to
`f49d566ed276` == HEAD, `git diff HEAD` empty |
| re-spell `readRepoRoot` with its own `execFileSync` git call — a
fourth unrouted reader | anchor 1 to 0, blob `f49d566ed276` to
`ecd24ab63593` | `objectstack-ai#19191 knob: ONE git read site in the file, so a
fourth reader cannot skip the cwd` — got 2, want 1 | same |

The first is the one that mattered most: before that pin existed,
stripping the `cwd` left the whole
5,080-case suite green, i.e. the fix's central wiring had no test that
could fail. The second is what
makes the card's "the three readers are the ONLY local-tree reads" a
property the suite enforces.

## Acceptance notes

- **Knob name**: `PM_SWEEP_CHECKOUT`. It joins the established 12-member
`PM_*` PM-tooling family
(`PM_SWEEP_REPO`, `PM_SWEEP_CLOSED_FLOOR`, `PM_STATE_LABELS`,
`PM_TOKEN`, …) rather than the
product's `OS_{DOMAIN}_{NAME}` runtime family of Prime Directive objectstack-ai#9 —
the card asked for a knob
"beside `PM_SWEEP_REPO`", and `--help` groups the three under one
heading where a reader looks.
Flagged because that directive's wording is categorical; a maintainer
who prefers
  `OS_PM_SWEEP_CHECKOUT` gets it for the price of one rename.
- **The patrol workflow needs no env line, and this PR writes none.** On
every real patrol fire the
runner's own checkout IS the swept board (`PM_SWEEP_REPO: ${{
github.repository }}`, per-repo
installs), so the knob would be a no-op there and `GITHUB_REPOSITORY`
already answers
`localCheckoutServes`. If it should be stated explicitly anyway, the
exact line for the sweep step's
  `env:` block in `.github/workflows/half-state-patrol.yml` is
`PM_SWEEP_CHECKOUT: ${{ github.workspace }}` — a separate, human-merged
change, since objectstack-ai#19259 and
  objectstack-ai#19225 are open on that file.
- **`origin/main` was NOT merged into this branch.** It moved to
`e6a03e6` while this ran and touched
`scripts/pm/check-half-states.mjs` in none of those commits (verified
against a fetch into a ref this
worktree owns, not against the shared `origin/main` pointer), so the
line budget's 36,176 baseline
still holds and no serial writer conflict exists. The merge queue
rebuilds the PR onto current `main`
and re-runs the required contexts there, which is where a jointly-wrong
merge would surface.
- **Noted, not filed**: `sweep()` and `sweepScheduledWorkflows` each
perform their own
`readOriginUrl()` / `readRepoRoot()` — two duplicate local git reads per
sweep (microseconds, zero
requests). Threading one reading through `sweepInto` would widen that
function's signature, which is
the kind of change this card was told to keep out of. Successor: whoever
lands objectstack-ai#19177 (H59) or objectstack-ai#19160
  (H52) is next on this writer.
- **Nothing else in the file was touched**: objectstack-ai#19230, objectstack-ai#19177, objectstack-ai#19160,
objectstack-ai#19203, objectstack-ai#19255 and objectstack-ai#19108 queue
  behind this card on the same writer.
- `scripts/pm/**` publishes nothing from any released package, so this
diff owes no changeset
  (`Clause-②: no`). No label writes.
- Tier S: this stops at the draft PR. The owning seat writes the `##
Contract review` record, reads
  `--pair`, and lands it.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… so a deferred table stops deriving as a scan surface (objectstack-ai#19308)

Fixes objectstack-ai#19260

Clause-②: no

`EXCLUSION_DECL_NAME` in `scripts/pm/dispatch-gates.mjs` listed no
`DEFERRED`, while
`scripts/check-issue-citations.mjs` declares its exclusion table under
exactly that word —
`DEFERRED_SURFACES` / `DEFERRED_GLOBS` — and applies it as an exclusion:
`surfaceFor` opens by
returning `null` for every deferred glob. So the derivation read an
**exclusion** table as a
**scan** surface, and a `.changeset/**` path was told it triggers a gate
that looks at nothing
there. That derivation is the one PR objectstack-ai#19259 (card objectstack-ai#18224) trips on, and
the assertion it reds —
「a changeset path alone reaches NO value-bearing family any more」 — is
correct and is untouched
here.

One `DEFERRED` branch in the predicate; the measured census in the
docblock above it re-measured
and rewritten in the same stroke; one self-test case beside the existing
named-spelling cases.

⛔ Nothing is renamed in `check-issue-citations.mjs` —
`DEFERRED_SURFACES` and `DEFERRED_GLOBS`
are exported and pinned by that gate's own self-test, and this docblock
says the fix belongs on
the derivation side: 「an author's next spelling should be met by this
predicate rather than by a
rediscovery of this card」.

## Acceptance notes

### Firing pair, both directions, on PR objectstack-ai#19259's head

The script derives from the tree it LIVES in — `ROOT` comes from
`import.meta.url`, and
`trackedFiles()` reads that root — so a `cwd` does **not** redirect it
at another tree. The fixed
file was therefore copied into a detached worktree of PR objectstack-ai#19259's head
(`6d1272b3850e5c0e756745dc6486d20aa330653d`, blob `5d78406d6` before,
`8216826a5` while mutated)
and run from there; the probe was restored with `git checkout HEAD --
scripts/pm/dispatch-gates.mjs`
back to blob `5d78406d6`, `git diff HEAD` empty.
`.changeset/test-abc.md` did not need to exist.

| probe tree `6d1272b385` | `--commands` lines | `pnpm
check:issue-citations` rows |
|---|---|---|
| subject `.changeset/test-abc.md`, before | 19 | **1** |
| subject `.changeset/test-abc.md`, after | 18 | **0** |
| control `packages/types/src/index.ts`, before | 44 | **1** |
| control `packages/types/src/index.ts`, after | 44 | **1** |

The subject's two command lists differ on exactly one line — `diff`
reports hunk `13d12`, deleting
the `pnpm check:issue-citations` line and nothing else —
the control's two lists are byte-identical. Command: `node
scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack PATH`, exit 0 on all four runs.

### The census: the method it never stated, and what it now reads

The block named its method only in prose and named no command, so the
method is now spelled out in
the block itself: enumerate `topLevelDecls` over every tracked JS/TS
file under `scripts/`, keep the
non-callable declarations, test each name against the predicate, and
price the arm by diffing
`extractWatchHints` against a build of this module whose predicate
matches nothing. Attribution is
the regex engine's own — leftmost position first, then alternation order
(the old text said
"first-match attribution" without saying which of the two, and they
disagree on a multi-word name
such as `EXCLUDED_SKIP`).

Measured over the objectstack-ai/objectstack tree at `e6a03e6491`, with
the export-visibility of
six internal bindings as the only difference from the shipped file (a
throwaway copy, never
committed):

| reading | docblock before | re-measured, no `DEFERRED` | re-measured,
with `DEFERRED` |
|---|---|---|---|
| tracked JS/TS files under `scripts/` | 230 | 287 | 287 |
| top-level VALUE declarations | 3,513 | 4,697 | 4,697 |
| identifiers matching the predicate | 64 | 73 | **75** |
| of those, carrying a string literal | 58 | 65 | **66** |
| hints the set moves | 14 | 16 | **24** |
| files it moves them on | 6 | 7 | **8** |
| change no derivation / really leave one | 8 / 6 | — | **13 / 11** |

Per-word tally, same ordering as the block: `SKIP 54, EXCLUDED 10,
EXCLUSION 4, NOISE 2, SKIPPED 2,
DEFERRED 2, EXCLUSIONS 1, and EXCLUDE / EXCLUDES / IGNORE / IGNORED 0` —
four zero-scoring arms now,
not three, because `EXCLUDES` went 1 → 0 on its own. ⚠️ **Every one of
those numbers was already
stale before this card**: the middle column is the same tree with the
OLD predicate, so the drift
from `230 / 3,513 / 64 / 58 / 14` is the tree moving, not the arm.
Nothing reds when it does, which
is why the block now carries the tree ref it was read on.

`DEFERRED` matches exactly two declarations on this tree, both in
`check-issue-citations.mjs`, and
both are that gate's own exclusion table. It moves 8 hints off that
gate, 5 of which cost nothing —
the test globs sit under the gate's own `packages/**` inclusion, so
`hintCovers` still reaches a
test path — and **3 of which were fabricated leads**: `.changeset/**`
(the one the card measured),
plus `scripts/**` and `docs/adr/**`, which nobody had named. The gate
stays reachable through
`packages/**`, `packages/**/src/**/*.ts` and its siblings, which is what
keeps the control lit.

Re-verified after the merge of `origin/main` `e6a03e6491`: every figure
above reproduces, and the
edit adds no top-level VALUE declaration to this file (112 before, 112
after), so the 4,697 holds
for the delivered tree.

### Gates — every exit code captured before any pipe

| command | exit | verdict line |
|---|---|---|
| `node scripts/pm/dispatch-gates.mjs --self-test` (this branch) | 0 |
`✓ dispatch-gates self-test: 1867 cases pass.` |
| `node scripts/pm/dispatch-gates.mjs --self-test` (pristine
`e6a03e6491`) | 0 | `✓ dispatch-gates self-test: 1866 cases pass.` |
| `pnpm check:pm-dispatch-gates` (detached, under the shared lock) | 0 |
`check:pm-dispatch-gates: the battery took 1012.6s on this box.` |
| `pnpm lint` (repo-wide, `eslint . --no-inline-config`) | 0 | no output
|
| the other 27 derived families | 0 each | listed below |

Case count 1866 → 1867: the one new case is the `DEFERRED` entry in the
named-spelling loop beside
the existing exclusion-vocabulary cases, ⛔ not at the tail of
`selfTest()` — the tail regions that
PR objectstack-ai#19162 (`:22173`–`:22203`, now landed as `e6a03e6`) and PR objectstack-ai#19024
(`:23763`) touch are untouched
here, and the merge of `origin/main` carrying objectstack-ai#19162 was clean. Inside
the battery the case
「⭐ a changeset path alone reaches NO value-bearing family any more」 —
the one red on PR objectstack-ai#19259's
head — now reads green.

Families derived with `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`
(28, recomputed post-merge and identical to the pre-merge list) and
reconciled with `--ran`:
`28 derived famil(ies) accounted for — 28 run, 0 NOT-MEASURED (a DERIVED
zero — all 28 recorded an
exit code and none of them is 3)`. The 27 besides the battery:
`check-ci-filter-parity`, `check-closing-keyword-parity` (+ self-test),
`check-comment-mask-corpus`,
`check-declaration-mirrors` (+ self-test),
`check-scripts-symbol-anchors` (+ self-test),
`check-self-test-wired` (+ self-test),
`check-self-test-workflow-commands` (+ self-test),
`check-whole-set-label-write` (+ self-test),
`check:agent-test-spelling`, `check:bash32-floor`,
`check:cli-command-ids`, `check:cross-package-test-inputs`,
`check:declared-population-live`,
`check:driver-memory-census`, `check:entry-guard`, `check:nul-bytes`,
`check:parse-guard`,
`check:pnpm-filter-targets`, `check:ratchet-remedy-authority`,
`check:refd-timer-probe`,
`check:watch-hint-literal` — all exit 0.

Control-byte self-scan beyond the gate, exit captured before any pipe:
`grep -naP` over the changed file for the C0 range plus DEL exits 1 —
none present.

### Line budget and shape

`scripts/pm/dispatch-gates.mjs` 28,345 → 28,371 lines against
`origin/main` `e6a03e6491`:
**net +26** (+64 / −38), inside the +40 the dispatch set. Measured
against the 28,337 the dispatch
quoted at `7d0f911`/`7ec8534`, the file reads +34, of which +8 are PR
objectstack-ai#19162's own. `git diff --stat`
shows one file.

No changeset: `scripts/` ships in no published package's `files[]`
(checked across every non-private
manifest in the tree), and `scripts/pm/**` is on the non-publishing fast
track. ⚠️ So `Check Changeset`
needs the `skip-changeset` label, and this dispatch forbids label writes
to the dev — **that one write
is the seat's**, not left undone by accident.

### Noted, not filed

**The census in this docblock was already stale before this card, and
nothing reds when it goes
stale.** The middle column of the census table above is the same tree
read with the OLD predicate:
`230 → 287` files, `3,513 → 4,697` declarations, `64 → 73` matches, `14
→ 16` moved hints, and the
tally's `EXCLUDES 1 → 0`. The sibling gate keeps its own census as code
with a `measuredOn` ref
(`CENSUS_17512` in `check-issue-citations.mjs`, marked 「⛔ Readings, not
a budget」); this one is
prose in a comment, so it rots silently. Not filed — it is an
observation about a missing guard,
not a reproducible defect, a broken declared contract or an authoring
trap. 承接者: the standing
queue on this same file (objectstack-ai#19070 → objectstack-ai#19104 → objectstack-ai#19105 → objectstack-ai#19106 → objectstack-ai#19172),
any of which reads this block.
The mitigation this PR does ship is the tree ref and the spelled-out
method, so the next reader can
tell a stale number from a current one.

## 维护者速读(草稿)

**改了什么** — 派单工具 `dispatch-gates` 里那张「哪些常量名代表『这个门禁不看这里』」的词表,补上了
`DEFERRED` 这个拼写。顺带把该处注释里那份实测普查重新测了一遍并改写,因为它早已过期。

**为什么改** — `check-issue-citations` 这个门禁用 `DEFERRED_SURFACES`
声明它**故意不看**的路径。
词表不认这个词,于是工具把「不看的清单」读成了「要看的清单」,反过来告诉开发者:改一个 changeset
文件会触发这个门禁 —— 而该门禁对 changeset 路径其实什么都不做。后果不止是一条假线索:它还让 PR objectstack-ai#19259
在 `lint.yml` 第 32 步整条中止,objectstack-ai#18224 自己新加的两步从未执行,卡住了一张 p2。实测这条修法还顺手消掉
另外两条没人发现的假线索(`scripts/**` 与 `docs/adr/**`)。

**风险与代价(含回滚)** — 风险很低:改动是一个正则分支加注释,只影响派单提示里「你该跑哪些门禁」这份
清单,不影响任何门禁自身的判定,也不改任何对外发布的包。方向上只会**少给**一条线索、不会多给,而这一侧
的失效代价是「一张卡多跑一轮 CI」,比反方向「每张卡都被塞一条假线索」便宜得多 —— 这个不对称是该文件自己
写下的判据。回滚 = revert 这一个提交,单文件、无生成物、无迁移。

**席位意见**

**你要做的** — ① 这是 draft PR,按席位流程补 `## Contract review` 记录后再走 ready +
auto-merge;
② `skip-changeset` 标签需要席位来打(本单禁止开发侧写标签),否则 `Check Changeset` 会红;
③ 落地后 PR objectstack-ai#19259 / 卡 objectstack-ai#18224 即可重跑,它那条断言本身是对的、本 PR 未动。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants