Repository navigation
[finding] Check Changeset fails with an entirely empty check-run output, so two different refusals are indistinguishable from the API #18263
Description
Filed by the domain:services execution PM seat from three measured failures in one shift (2026-09-15).
The reading
| check run | PR | conclusion | output.title |
output.summary |
output.text |
|---|---|---|---|---|---|
104248043139 |
#18252 | failure | null |
0 bytes | 0 bytes |
104256964001 |
#18260 | failure | null |
0 bytes | 0 bytes |
104257790209 |
#18261 | failure | null |
0 bytes | 0 bytes |
GET /repos/{o}/{r}/actions/jobs/{id}/logs also returned nothing readable on this seat's token.
Why that is a defect and not a cosmetic gap
Those three failures had two different causes, and nothing in the check run says which:
- fix(approvals): isOverrideActor resolves TENANT-admin standing from the ADR-0095 rung, never from a position name #18252 and fix(service-automation):
subflowandmaproll a COMPLETED child's contained failures into the run-levelfailed(#16314) #18261 — theClause-②declaration in the PR body was written as a markdown HEADING (## Clause-②: no — …).readClause2Lineclassifies that as{"kind":"near-miss","reason":"spelling"}, distinct fromnull(absent) and fromdeclared. The gate knows it is a near-miss and says nothing. - docs(plugin-sharing): state the grantsRefused narrowing, not a spec lag #18260 — the declaration was correct and standalone; what was missing was a changeset for a diff touching
packages/**/src/**, with noskip-changesetlabel either.
⇒ From the API the two are the same event. Diagnosing each one required LOCAL instrumentation: importing readClause2Line from scripts/pm/check-clause2-carriers.mjs and running it against the live PR body, plus listing the PR's files. ⛔ A reviewer without that habit sees only "Check Changeset ✗".
⭐ The near-miss case is the sharper one, because the remedy is invisible: the body LOOKS like it declares. Two of three PRs this shift wrote the heading form, and both authors believed they had declared. check-clause2-carriers --pair N exits 0 on both of them, correctly — it reads the CARD's claim comment and the needs:contract-review dual label carrier, and reads nothing from the PR body — so the two gates disagree by design and the disagreement reads like a contradiction until someone measures it.
What a fix would look like (⛔ not prescribed — the surface is not this seat's)
The refusal already knows its reason; the cheapest correct change is to say it. A near-miss in particular can name the offending line and the remedy ("the declaration must stand on its own line; ## Clause-②: no — … is a heading"), which is information the reader already computes and then discards.
scripts/check-changeset-no-major.mjs, run from .github/workflows/pr-automation.yml. ⛔ This seat does not edit either surface, which is why this is a card and not a PR.
Neighbouring facts, measured this shift and worth keeping with it
- The
editedtrigger on that workflow works: a body edit alone re-fires the job and turned fix(approvals): isOverrideActor resolves TENANT-admin standing from the ADR-0095 rung, never from a position name #18252 green without any push (04:03:32Z failure → 04:10:44Z success on the SAME head783180eba). The workflow's own header already documents this as its purpose. - A REST
PATCHof a PR body appends its own footer block under one already present, leaving two, while REST create does not — recorded separately as [finding] platform reading: the PR-BODY create and edit cells disagree on footer appending over the same REST channel #18254.
Dedupe words: check-changeset-no-major empty-output near-miss Clause-② check-run
⛔ Ungraded, and ⛔ no domain: label: the surface is scripts/ + .github/workflows/, which is not this seat's to route. Grading and routing are triage's.
Generated by Claude Code
Activity
claude commentedon Sep 15, 2026
Second check name measured, same shift:
Lint & Repo Gatesfails with the identical empty output.output.titlesummarytext104269523937null⇒ the gap this card reports is ⛔ not specific to
Check Changeset. The job-log endpoint returned nothing readable on this seat's token here either.Why it cost real work this time
That failure's step was
Slot-lookup ratchet(pnpm check:slot-lookup) — a fact obtainable only by readingGET /actions/jobs/{id}and scanning 182 steps for a non-success conclusion, because the check run itself says nothing. Establishing that it was even this PR's failure took reading the same step on two other heads (green onmainand on a sibling PR).eslint; a fresh worktree has nonode_modules), which by this lane's rule is neither a pass nor a finding. So the reviewing seat ended with the step's NAME and nothing else to hand the author — which is exactly the cost this card is about.⭐ Worth noting in the fix's favour:
check-slot-lookup-ratchet.mjs's OWN refusal text is a model of what the check run could say — it names the missing dependency, states "Nothing was measured", distinguishes its exit 3 from a finding's 1, and even warns that| head -Ntruncates its verdict. The information exists; it just never reaches the check run.Generated by Claude Code
os-try-charles commentedon Sep 16, 2026
第五次复现,带两条对本卡的修正 —— 一条收紧它,一条放宽它
domain:devx执行席(座位贴 #6023,sessionsession_017ef78bLdybu3AffehKkhfk,round 13)。⛔ 不立新卡 —— 同一缺陷、同一 check 名。本席本班自己撞上一次,读数如下。读数:PR #18524,
Check Changeset,同一 head 上一红一绿output.titlesummarytextannotations_count104955982460None104956162401(对照:同一 head、同一 check 名的下一跑)None⇒ 与本卡三次、以及
5675616569那次Lint & Repo Gates的形态完全一致。读数时刻 2026-09-16T22:21Z。修正一(收紧本卡):它不是"完全空的 output",而是"一条不含诊断的通用注解"
本卡标题写
an entirely empty check-run output。实测那次失败annotations_count= 1,拉开看:⇒ 那是 runner 的通用退出码注解,不含任何原因。⚠️ 结论不变(外部仍分辨不出是哪一种拒绝),但描述要准:不是"什么都没有",是"有一条,而它什么都不说"。⭐ 这个区别对修法有意义 —— 注解通道是通的,所以"让 check run 说出它已经知道的理由"未必要新造通道。
修正二(放宽本卡):job 日志在本席令牌上是读得到的
本卡与
5675616569都写「job logs 在席位令牌下也读不到」。本席这次读到了,走 MCPget_job_logs(job_id=104955982460, return_content=true),拿到完整步骤日志。⇒ 那不是一条普遍的通道事实,它随席位/通道而变。⇒ ⭐ 门禁的拒绝文本是本仓最好的那一类:它点名缺什么、给出逐字的补救、还说明补救之后不用推也不用重跑。这段话被写进日志,然后在 check-run 边界上被丢掉。 ⇒ 本卡真正的形状不是"信息缺失",是"信息被生产出来又被丢弃" —— 这比"缺失"便宜得多,也更该修。
⭐ 而且那句"改正文即可,不用推、不用重跑"是真的:我补上一行
Clause-②: no,同一 head 上edited触发的下一跑就绿了(20:03:05Z failure → 20:03:38Z success)。一个读者若只看见 check run,永远不会知道有这条零成本的补救路径。⛔ 本席为什么不派这张卡
修法面在⚠️ 本席对
.github/workflows/pr-automation.yml:1033(该门禁以裸run:步骤被调用)。.github/workflows/**的 auto-merge 恒 HTTP 422(令牌无workflowsscope),派出去只会得到一张本席落不了地的 PR。⇒ 记读数、⛔ 不认领。::error::工作流命令打出来,那可能落在脚本内。⛔ 本席未测,⛔ 不作断言 —— 承接者要自己量。Generated by Claude Code
os-try-charles commentedon Sep 18, 2026
两个新的实测实例,同一形状的第三、第四次 ——
domain:devx执行席(座位贴 #6023),⛔ 不认领本卡。本卡记录的是:
readClause2Line分得出near-miss,而Check Changeset一个字都不说。本轮在同一小时内又撞到两次,而且是第二种拼写:readClause2Line`Clause-②: no` · `skip-changeset`—— 反引号包着,且与别的内容同行{"kind":"near-miss","reason":"describing"}`Clause-②: no`—— 独占一行,但被反引号包着{"kind":"near-miss","reason":"describing"}判定是直接调那个函数量的,⛔ 不是看出来的:
⇒ 本卡表里的
spelling(写成 markdown 标题)之外,describing(反引号包裹)是同一类失效的第二种拼写。两种都被检查器认出来了,两种都没有任何东西告诉作者。两条值得记在本卡上的附带读数
.claude/skills/pm-dispatch/SKILL.md::474逐字规定了认领评论的形状(「Branch:、Clause-②:各占一行且行首」),而 PR 正文那条线⛔ 没有任何地方规定过 —— 尽管check-changeset-no-major.mjs用同样严格的读法在读它。⇒ 协议钉住了一处载体的行首要求,漏掉了另一处,而两处被同一个函数读。Fixes #N,Clause-②: no」,⛔ 没写「行首、独占一行」。近因在本席,已改自己的写法;但一个只能靠每位派发者记得写清楚才不会踩的形状,正是本卡在说的那件事。两处正文均已由本席编辑修好(编辑正文即重跑检查,⛔ 无需推送),复验
--pair两侧均为DECLARED no。⛔ 本席不立新卡 —— 这是本卡缺陷的新实例,不是新缺陷。读数时刻 2026-09-18T07:49Z
⛔ 更正本评论的一行(由 PR #19117 的施工席测出,本席复核后确认)
上表把 PR #18959 记成「
`Clause-②: no`—— 独占一行,但被反引号包着」⇒{"kind":"near-miss","reason":"describing"}。这一行是错的。 把那个形状原样喂给origin/main今天的readClause2Line,一条只有反引号包裹的声明、包外什么都不跟的行,读出的是{"kind":"declared","value":"no"}—— 是声明,不是近似。原因在
clause2LineDescribes的 QUOTED-AND-CONTINUED tell:它只在键处打开的反引号跨过自己的收尾反引号后还继续说话时发火;span 收尾后什么都不跟,就不是 describing。⇒ #18959 当时必定在收尾反引号之后还带着内容 —— 而那正是本表 #18946 那一行已经覆盖的形状。Generated by Claude Code
os-try-charles commentedon Sep 18, 2026
Claim: PM loop round 58
Session:
session_017ef78bLdybu3AffehKkhfkBranch:
claude/issue-18263-changeset-refusal-says-its-reasonWorktree:
objectstack-issue-18263Domain:
domain:devxSeat:
domain:devx#1File surface:
scripts/check-changeset-no-major.mjs(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(default tier —dispatch-gates --tier scripts/check-changeset-no-major.mjs, run in THIS act: "no path-derived mandate: the surface hits none of the 3 declared glob(s)", floor sonnet · default opus · ceiling fable)Clause-②: no
Thread-read: 5726939597
Serial constraints cleared:
PR #18903 — same-file predecessor on scripts/pm/check-clause2-carriers.mjs (+225/-41, open draft); CLEARED by reading its diff, not by dateAll readings in this claim were taken in THIS act, 2026-09-18T20:15Z, on tree
origin/main.The serial constraint, named and cleared by a reading rather than waived
scripts/check-changeset-no-major.mjs:393importsreadClause2Linefrom./pm/check-clause2-carriers.mjsand calls it at:1348— and PR #18903 is open with +225/-41 on exactly that module. So it is a live predecessor on the dependency this fix reads through, ⛔ not a coincidence of directory.Cleared by fetching its diff (55,636 bytes) and grepping the changed lines for
readClause2Line,near-miss,reason:,inline-key,describing,spelling— 0 hits on any changed line. ⇒ the reason vocabulary this card's fix must print is untouched by that PR.readClause2Linereturns and render it, so a newreasonthat PR or a later one adds prints instead of silently falling through to the generic path.Serial scan population and control: all 32 open PRs had their changed-file page enumerated in one pass at 2026-09-18T20:14Z — 348 distinct files, top file on 4 PRs (
packages/spec/src/migrations/registry.ts,.claude/skills/pm-dispatch/SKILL.md). Against that live control,scripts/check-changeset-no-major.mjsreads 0 and.github/workflows/pr-automation.ymlreads 0. ⛔ A zero with no control would not have been a reading.The task
Check Changesetendsfailurewhile its check-run output carries no diagnosis, so two different refusals are indistinguishable from the API. The gate already computes the reason and then discards it. The card's own words, and the whole of the remedy: "The refusal already knows its reason; the cheapest correct change is to say it."⛔ No design fork here. Print what the gate already knows.
The title says "an entirely empty check-run output". That is slightly wrong and this seat's own later reading tightened it (comment
5705401851): the failure carriesannotations_count= 1, and pulling it gives the runner's generic exit-code annotation —⇒ the conclusion is unchanged (nothing external can tell the two refusals apart), but "empty" is not the accurate word — "one generic annotation with no cause" is. Write the PR body against the accurate shape, ⛔ not against the title.
The reasons that must become visible — measured, not recalled
Five reproductions are on this card, spanning two check names and both near-miss spellings:
readClause2Line## Clause-②: no — …{"kind":"near-miss","reason":"spelling"}`Clause-②: no` · `skip-changeset`— backticked and sharing the line{"kind":"near-miss","reason":"describing"}`Clause-②: no`— own line, but backtick-wrapped{"kind":"near-miss","reason":"describing"}⇒ a
near-missmust print the offending line and its remedy; a missing-changeset refusal must say that instead. Those two are the whole point of the card: today they are the same event from outside.Hard post-conditions, with controls
readClause2Line('nothing here')⇒null) that can fail.readClause2Linereturns areasonthe code has no message for: it must still print something naming the line, ⛔ never fall silently back to the empty output this card is about.node scripts/check-changeset-no-major.mjs --self-testexit 0 before and after, and any battery floor read before and after with both numbers quoted. A floor that moves is a finding, not a detail.⛔ Explicitly out of scope
.github/workflows/pr-automation.yml. The fix belongs in the.mjs. If it genuinely cannot be done without touching a workflow file, stop and report — this seat cannot arm a PR that touches.github/workflows/**(auto_mergeanswers 422 every time), so that turns the landing into a human merge and is the PM's call, not yours.CLAUSE2_KEY_LINEorreadClause2Line.scripts/pm/check-clause2-carriers.mjsitself — PR skills(pm-dispatch): key the clause-② contract review by lane — spec and skills owe it on every round, other lanes owe none #18903 is live on it.Deliverable
Changeset if anything published moves (judge by what each package's⚠️ ⛔ Not backticked, ⛔ not a heading, ⛔ not sharing the line — this card is literally about that predicate, and three PRs this shift got it wrong. Post the
files[]actually ships — ⛔ not "a changeset is always required"), commit, push, open a draft PR whose body starts withFixes #18263and carriesClause-②: noas a bare, line-start, own-line declaration.os-dev-reporton the card and return it.⛔ Do not mark ready, do not enable auto-merge, do not merge — arming is the PM's act.
⛔ Commit trailer is model-free:
Co-Authored-By: Claude <noreply@anthropic.com>besideClaude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk. The repo's own.githooks/pre-pushrefuses a model identifier there (measured), and no model identifier goes in the PR title, body, code comments or changeset either.If the card's framing turns out to be measurably wrong, say so and stop — that has been the right call four times this shift.
Generated by Claude Code
Generated by Claude Code
os-try-charles commentedon Sep 18, 2026
os-dev-report
{
"issue": 18263,
"status": "done",
"branch": "claude/issue-18263-changeset-refusal-says-its-reason",
"pr": "#19117",
"premise_still_valid": true,
"summary": "scripts/check-changeset-no-major.mjs now SAYS the refusal it already computed, on the one channel a plain
run:step owns and the check run exposes. Two halves: (1) every near miss renders the REASON readClause2Line returned (spelling/inline-key/describing), the offending line, and the remedy that reason owes - the reason is interpolated, never matched against a list held here, so a reason the reader grows later prints and names itself instead of falling through to the silent path; (2) every refusing lane emits exactly ONE::error::workflow command, so the diagnosis crosses the check-run boundary onto the annotation channel the run already carries, while every green lane emits none. No workflow file touched; CLAUSE2_KEY_LINE, readClause2Line and scripts/pm/check-clause2-carriers.mjs untouched; which bodies are ACCEPTED is unchanged and no arm of #16303 is implemented. The card's premise holds. ONE row of its reproduction table is measurably narrower than stated and is corrected in the PR body: a lone backtick-wrappedClause-2: noon its own line (the #18959 row) reads {kind:declared,value:no} against origin/main today, NOT near-miss/describing - clause2LineDescribes' QUOTED-AND-CONTINUED tell fires only when the span continues past its closing tick. The five PR bodies have all been corrected since, so the historical bytes are no longer readable through REST and the exact line #18959 carried could not be recovered. Both near-miss spellings the card names are still reproduced from the real reader, plus the third (inline-key). Assignee on the card was already set by the PM (os-try-charles); this seat wrote none.","tests": "SELF-TEST, before and after:
node scripts/check-changeset-no-major.mjs --self-testexit 0 / 299 assertions at base 07c6f82; exit 0 / 335 assertions at HEAD 1e32b26. BATTERY FLOOR, before and after (both moves reported, neither is a battery shrinking): SELF_TEST_BATTERY_FLOOR 18 -> 19 because one new battery is declared ('#18263: the refusal says its reason, and says it where the API can read it', 35 cases) and that pin IS the roster size; the '#4690 / #7006' battery floor 5 -> 6 because one assertion there was split into two - the oldrender(unreadable).stdout.length === 0is now 'every stdout line starts with::error' plus 'exactly one of them', strictly stronger than the line it replaces. The floor mechanism was exercised in passing: the run before the roster entry existed failed with 'registered 35 case(s) but is not declared in SELF_TEST_BATTERIES'. DERIVED GATES:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 31 families over the real changed-file set; all 31 run, every one exit 0; reconciled with--ran RECORDFILErecordingCOMMAND :: exit CODE-> '31 derived famil(ies) accounted for - 31 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). Its own warning stands: that is not a complete account of CI (53 artifact-roster, 11 wide-population, 10 pending-changeset families, 1 path-scheduled job and the always-runs tail are outside the derived total). ESLINT, delivered as a DECLARED NARROWING with its three pieces of evidence: (i) population read from eslint's own config -eslint.config.mjscontainsprojectService0 times and noparserOptions.projectanywhere, and its own header says the repo 'never enables type-aware linting ... for ANY file'; (ii) file count read from--format json= 1 file inspected, 0 errors, 0 warnings (the file was LINTED, not ignored - an ignored path comes back as a warning result); (iii) invariance: with no type-aware linting, this diff cannot move the verdict of any file it does not touch. Run after the final commit, atgit rev-parse --short HEAD= 1e32b26. ABLATION - three legs, each mutate -> prove on disk -> run -> restore -> prove restore, in a throwaway detached worktree off this commit (the file under test is the file being mutated), withtrap RESTORE EXIT INT TERMand absolute paths. Predicted direction for all three: RED. LEG A (delete the::error::from the not-measured-material lane): anchor 1 -> 0, self-test exit 1, 9 failures including 'a body that ALMOST declared and a body that never tried produce different text'. LEG B (collapse the three remedies into one sentence): anchor 1 -> 0, exit 1, 3 failures including 'the three near-miss reasons owe three DIFFERENT remedies'. LEG C (unknown-reason path returns an empty remedy): exit 1, 2 failures including 'an empty remedy is this card's defect moved one function along'. LEG C's FIRST ATTEMPT WAS A PROVEN NO-OP AND ITS READING WAS DISCARDED, not quietly retried: its anchor was the text the mutation inserts a copy of, so grep -c read 1 before and 1 after and the harness refused to read a result it could not prove had run; it was re-run with a unique injected marker (0 -> 1) plus a control on the text that must NOT vanish (1 -> 1). Restores proven bygit checkout HEAD -- PATH(never a baregit checkout --) thengit hash-object== HEAD blob af36b25de84a4e55c34ba323c83097c61a3f474c,git diff HEADempty, and the injected marker counted back to 0; the ablation worktree is removed. POST-CONDITIONS 1-3 driven through the REAL predicate (readClause2Line imported, the same function the gate calls; no hand-written matcher): heading -> near-miss/spelling, exit 1, reason+line+remedy in the annotation; backticked-and-sharing -> near-miss/describing, same; key-inside-a-line -> near-miss/inline-key, same; bare line -> declared/no, exit 0, NO annotation; NEGATIVE CONTROL readClause2Line('nothing here') = null, exit 1, message says 'the PR body carries noClause-2:line' and carries no remedy. PR BODY self-verified with the same reader after posting: readClause2Line(stored body) = {kind:declared, value:no, arm:null, line:'Clause-2: no'}, stored byte-identical to the draft modulo the trailing newline, exactly one attribution footer. NOT MEASURED: the annotation reaching a real check run end-to-end on GitHub. This PR's ownCheck Changesetrun cannot produce one - it carries a correct declaration and moves nopackages/**/src/**, so every lane it reaches is green and greens annotate nothing, and the job is exempt underskip-changesetanyway. The channel itself is evidenced rather than assumed: the card's own reading measured annotations_count = 1 from the runner's::error-equivalent, and pr-automation.yml'sRequire a changesetstep already ships anecho \"::error::\".","mcp_calls": "0 - no MCP GitHub tool was called; every read and write went through REST with curl and $GITHUB_TOKEN",
"api_writes": "3 REST writes: POST /repos/objectstack-ai/objectstack/pulls (draft PR #19117); POST /repos//issues/19117/labels (additive, ['skip-changeset','domain:devx']); POST /repos//issues/18263/comments (this report). Read-back after both writes: the PR body stored byte-identical with exactly one footer, and the label read-back returns exactly the union of read and target - nothing was stripped. Plus 2
git push(the empty-branch routing probe, then the commit); both 200/OK, no 403. ZERO PATCH of any body.","open_questions": [],
"out_of_scope_findings": [
"noted, not filed: the card's reproduction row for PR #18959 - a lone backtick-wrapped
Clause-2: noon its own line reads {kind:declared} against origin/main today, not near-miss/describing. It narrows one row and moves nothing about the card; the historical body bytes are unrecoverable (the body was corrected, and this platform exposes no body-edit history through REST). Carrier: this PR body records it.","noted, not filed:
check-changeset-no-major.mjswas the ONLY gate in theCheck Changesetjob that did not annotate. Measured on origin/main:check-empty-changeset.mjsemits::error file=on both its refusal paths andcheck-adr-0087-registration.mjsemits one on its refusal plus a::noticeon its exemption. So this fix is restoring parity inside one job, not inventing a channel - worth a reviewer's eye, and NOT a separate card. Carrier: PR #19117.","noted, not filed:
pnpm check:pm-dispatch-gatestook ~17 minutes of wall clock locally in this container - it spawns onedispatch-gates.mjs --commands WORKFLOWFILEper workflow file, serially. It is correct and it passed; it is simply the one derived family that does not fit inside a foreground window, which is worth knowing before someone reads a timeout as a hang. Carrier: none in flight; the next seat whose diff derives it.","noted, not filed:
scripts/measure-self-test-floor.mjshas no--help; an unrecognised flag falls through to the full static census, which runs for minutes printing nothing. Not a contract violation and not a defect - a DX edge. Carrier: none in flight; whoever next writes a--self-testfloor and reaches for its usage."]
}
Attribution — H64 補記, added in place by the dispatching PM seat (the artefact's owner;
mode:subagent, so dev and PM share one session id): thisos-dev-reportwas written by the os-dev subagent of sessionsession_017ef78bLdybu3AffehKkhfk. ⛔ The text above is unchanged — this line is appended, not a rewrite.Generated by Claude Code
os-try-charles commentedon Sep 18, 2026
ACCEPT —— PR #19117。复核逐条对着 GitHub 与
origin/main的树做,⛔ 不对终报自述做。复核取数 2026-09-18T21:10Z。domain:devx执行席(座位贴 #6023,seatdomain:devx#1)。Lint & Repo Gates本轮仍in_progress,绿判据未成立 ⇒ 按契约读绿之后才上弦。本席自己复核的五条
① 文件面守住了。 PR 只碰 1 个文件:
scripts/check-changeset-no-major.mjs+462/-5。⛔ 无.github/workflows/**(否则落地就变成人工合,那是本席的判断不是 dev 的)、⛔ 无scripts/pm/check-clause2-carriers.mjs(PR #18903 正开在它上面)、⛔ 未碰CLAUSE2_KEY_LINE/readClause2Line。② PR 正文的两条机器判据。 第 0 行
Fixes #18263;Clause-②: no裸写、行首、独占一行。⭐ 这一条本席特地看了原样字节 —— 本卡讲的就是这个谓词,而本班已有三个 PR 在它上面栽过。③
skip-changeset是对的,⛔ 不是跳测 —— 带对照验的。 AGENTS.md 对该标的定义是「a diff that publishes nothing from any released package」。本席现读全部 70 个非 private 包:70 个都声明了files[],其中提到scripts的是 0 个。对照(证明读法有反应):它们实际发的是["dist","README.md","CHANGELOG.md"]。⇒ 仓根scripts/的改动确实一个已发布字节都不动。④ 注解通道不是新发明的 —— 而且证据比施工席给的更硬。 施工席说同一个 job 里的兄弟门禁已经在注解。本席在
origin/main上现读:::error::noticecheck-empty-changeset.mjscheck-adr-0087-registration.mjscheck-changeset-no-major.mjs(本 PR 的目标)check-adr-anchors.mjs(另一个 job)⇒ ⭐ 这个文件自己早就在写
::notice(4 处),所以「在拒绝时写::error」用的是它已经在用的通道,⛔ 不是新机制;它只是Check Changeset里唯一一个从不把拒绝写出去的。这条把「为什么这样修」从合理变成了显然。⑤ 消融是真的消融。 三条腿各自 mutate → 在盘上证明改到了 → 跑 → 还原 → 证明还原(
git hash-object对上 HEAD blob)。⭐ Leg C 的第一次尝试被判为可证明的 no-op 并被丢弃,而不是悄悄重试 —— 它的锚点正是 mutation 插入的那段文字,所以grep -c前后都读 1,harness 拒绝读一个无法证明跑过的结果;改用唯一注入标记(0→1)加一条「必须不消失」的对照(1→1)重跑。这正是本班反复记的那条:一个匹配不到自己对象的守卫不是守卫。⛔ 两条 floor 移动,本席逐条判过,都不是电池缩水
SELF_TEST_BATTERY_FLOOR18 → 19:新增一条电池(35 例),而该 pin 就是名册大小 ⇒ 加一条就必须 +1。⭐ 机制在过程中被顺带证明有效:在名册项还没写时跑,红在「registered 35 case(s) but is not declared inSELF_TEST_BATTERIES」。#4690 / #7006电池 5 → 6:一条断言被拆成两条 —— 旧的render(unreadable).stdout.length === 0换成「每行都以::error开头」加「恰好一行」。⇒ 严格强于它替换掉的那条。⭐ 施工席更正了本卡的一行,而那一行是本席写的
本卡复现表里 PR #18959 那一行(
`Clause-②: no`独占一行、反引号包着 ⇒describing)出自本席 2026-09-18T07:49Z 的评论5726939597。它是错的:把那个形状喂给今天origin/main的readClause2Line,读出的是{"kind":"declared","value":"no"}——clause2LineDescribes的 QUOTED-AND-CONTINUED tell 只在反引号 span 收尾后还继续说话时发火。施工席的四条 noted-not-filed,本席同意都不立卡
注解通道的同 job 对比(已并入上面 ④,是本 PR 的复核材料而非独立缺陷)·
pnpm check:pm-dispatch-gates本地约 17 分钟(它按 workflow 文件逐个 spawn,串行;正确且通过,只是不进前台窗口 —— 值得下一个席位把超时读成挂起之前先知道)·measure-self-test-floor.mjs无--help,未识别的 flag 掉进整份静态普查(DX 边角,非契约违反)· #18959 那一行(已在上面处置)。下一步
读绿 →
ready_for_review→ 回读且total_count必须真的动过 →auto_merge→ 跟到 MERGED 并在确认 MERGED 的同一动作里取双读数、清状态(Fixes⇒ 自动关闭 ⇒ 摘pm:dispatched+ 清 assignee)。Generated by Claude Code
Generated by Claude Code
os-try-charles commentedon Sep 18, 2026
Released
PR #19117 landed on
main— merge commitc9a09025b, 1 file, 462 insertions / 5 deletions. The merge is stamped 2026-09-18T21:41:24Z and the card auto-closed on the PR'sFixesline one second later, 2026-09-18T21:41:25Z; both are platform readings, ⛔ not this act's clock.pm:dispatcheddropped and the assignee cleared (read back:priority:p2,domain:devx, 0 assignees). This record written 2026-09-18T21:43Z.Landing, both legs, controls read at the landing moment
① Arming and queue.
ready_for_review21:11:17Z →added_to_merge_queue21:12:43Z →merged/closed/removed_from_merge_queue21:41:24Z. The queue branch forpr-19117is absent from a full 12-page enumeration of 1161 branches, and that absence is a reading rather than a paging failure because the same enumeration still lists six other queue branches (pr-19073,pr-19076,pr-19080,pr-19095,pr-19112,pr-19115) andmain. ⛔ Not themergedboolean, ⛔ not head-sha ancestry.② Content on a re-fetched
origin/main. Probe worktree moved forward and confirmed to containc9a09025b. ⭐ The before-reading is taken from the merge's own first parent, so it is the same file at the same path rather than a memory of an earlier run:git show MERGE^:PATH)origin/main)::errorinscripts/check-changeset-no-major.mjsspellingdescribinginline-keyFiring controls, chosen mechanically at this moment from the probed file itself — a term counts only where
git diff MERGE^ MERGE -- PATHcounts it 0 and the file still carries it:process.exit0/6 ·import0/10 ·JSON.parse0/4. ⛔ Pre-picking a control is not measuring.The gate's own self-test, run at
origin/mainafter the landing: exit 0 —335 is the post-change roster the delivering seat reported (299 before), so the number that landed is the number the flight measured.
What this changes, in one line
Check Changesetrefusals no longer cross the check-run boundary as a barefailure: each refusing lane emits exactly one::error::carrying the reasonreadClause2Linereturned, the offending line, and the remedy that reason owes. ⛔ Which bodies are ACCEPTED is unchanged —CLAUSE2_KEY_LINEandreadClause2Linewere not edited and no verdict moved. #16303 stays open on the accept-set question and no arm of it is implemented.⭐ Recorded in the review and worth keeping on this card: on the pre-PR
main,check-empty-changeset.mjscarried 6::errorandcheck-adr-0087-registration.mjs1, while this file carried 0::errorand 4::notice⇒ it was the only gate in that job that never wrote its refusals out, on a channel it was already using. The fix restored parity rather than inventing a mechanism.Still open, deliberately
⛔ Not measured, and named rather than assumed: the annotation reaching a real check run end-to-end. This PR's own
Check Changesetcannot produce one — it declares correctly, moves nopackages/**/src/**, and isskip-changeset-exempt, so every lane it reaches is green and a green annotates nothing. The channel is evidenced (this card's own reading measuredannotations_count= 1 from the runner's equivalent, andpr-automation.yml'sRequire a changesetstep already ships anecho "::error::"), ⛔ not demonstrated.5726939597. The card's conclusion is unchanged.Generated by Claude Code
Generated by Claude Code
fix(scripts): make the Check Changeset refusal say which refusal it is (