Repository navigation
Description
Filed unassigned by the dev seat implementing #9420 (PR #9490). Observation-class: recording a measured gap in the pin's coverage, not a proposal to widen that PR.
The measurement
scripts/check-required-contexts.mjs pins the required-status-check literals as contract — a job's name: in a workflow file is the check-run name GitHub matches a required context by, so a rename that moves one without the registry is red. That pin is real and it works.
Its scan set is the workflow files only. It does not read .claude/ or AGENTS.md at all:
$ grep -n "claude\|skills\|SCAN\|ROOT" scripts/check-required-contexts.mjs
(no output)
But those same literals are operative text elsewhere. They tell a review seat which jobs to confirm before flipping a PR ready / arming auto-merge / enqueuing it:
AGENTS.md:336— the merge-queue required-set rule, naming both contexts.claude/skills/pm-dispatch/references/review-checklist.md:37— the review seat's gate-clearance step, naming both contexts
Nothing ties either to the registry. Measured on the checklist line, by replacing the job name in it with Nonexistent Job Name and running the full derived gate union for that path:
pnpm check:doc-authoring -> exit 0
pnpm check:pm-skill-id-lint -> exit 0
pnpm check:pm-skill-ratchet -> exit 0
pnpm check:skill-frame-sync -> exit 0
pnpm check:nul-bytes -> exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions -> exit 0
Green across the board on a checklist that names a job which has never existed.
The complement holds too, so the gap is not "the file is unscanned": writing an issue-ID citation into that same line turns check:pm-skill-id-lint red naming the exact line number. The gates do read the file. None of them reads the job name.
Why it matters
This is the failure that just happened. The rename in #9421 moved the gate-family context; lint.yml and the registry both moved with it because the pin forces them to, and the two instruction surfaces did not. AGENTS.md was carried along inside that PR by hand, and the checklist line was noticed by the dev seat reading it — not by any gate — and had to be filed as its own card because the tiering ruling put it out of that seat's reach.
The harm is the one instruction files have: a seat that looks for a check-run name, finds nothing, and reads "no such check" as anything other than "the name moved" will either stall or arm on an unverified gate family. The literal is contract in both places; only one place is guarded.
Shape of a fix, for triage, not a decision
Two directions, and the second is the stronger one:
- Assert each pinned context literal appears verbatim in the instruction files that state the required set. Cheap, but it pins prose shape and goes red on legitimate rewording.
- Invert it: no instruction file in the scan set may name a required-context-shaped literal that is not in the registry. That catches the stale name, which is the actual defect, and stays blind to how the sentence is written. It needs a way to recognize the literals (the registry supplies them) and an anti-vacuous guard so an instruction file that stops naming any context does not pass by saying nothing.
Direction 2 also covers a case direction 1 misses: an instruction file naming a context that was removed from the required set entirely.
Deliberately not resolved here. Note the sequencing constraint any implementation inherits: the registry and the instruction files can only agree at rest, and a rename lands its halves in separate PRs by necessity (the Settings entry is maintainer-only), so the gate must be satisfiable at every intermediate state or it blocks the very sitting it is meant to protect.
Re-check commands
grep -n "claude\|skills" scripts/check-required-contexts.mjs
grep -n "TypeScript Type Check" AGENTS.md .claude/skills/pm-dispatch/references/review-checklist.md
node scripts/pm/dispatch-gates.mjs .claude/skills/pm-dispatch/references/review-checklist.md
Context: #9420 is the card whose whole content was one stale name on one line; #9421 carries the rename. Neither is addressed by this finding, and this finding does not block either.
Generated by Claude Code
Activity
os-zhuang commentedon Aug 18, 2026
PM triage →
pm:queue. Direction 2 ruled, with one implementation constraint added below. PM sessionsession_01NYgmGheCzM6NrHZN436Cxf.This finding is the durable output of the whole #9325 sitting — worth more than the one-line fix in #9490 that produced it. Three things in it are sharper than the framing I gave the seat that found them, and they are why this is being queued rather than left as an observation:
review-checklist.md.AGENTS.md:336names the same literals and is equally unpinned — it was carried along inside ci(lint): rename theESLintrequired context toLint & Repo Gates(#9325) — DRAFT, needs a maintainer-present Settings swap #9421 by hand, which is exactly the mechanism that fails the next time nobody happens to be looking at it.check:pm-skill-id-lintgoes red on the same line for an issue-ID citation while the whole union stays green on a job name that has never existed. That separates "the file is unscanned" from "the assertion is missing", and only the second is true.ESLintrequired context toLint & Repo Gates(#9325) — DRAFT, needs a maintainer-present Settings swap #9421 itself. Any implementation inherits this.Ruled: direction 2's intent — but not by recognizing context-shaped literals in prose
Direction 2 is right that the defect is a stale name, not a reworded sentence, and direction 1 would go red on legitimate rewording. Queue direction 2.
Lint & Repo Gatesis three ordinary English words. Deciding whether a given span of prose is meant as a check-run name requires either marking them in the prose — which is pinning prose shape, i.e. direction 1 wearing a hat — or guessing.Implement the intent through a retired-names ledger beside the registry instead:
Both sides are closed-list lookups against strings the registry already owns, so there is no recognition problem and no prose-shape pinning. (b) is the assertion that catches tonight's exact failure: a name that used to be right and silently stopped being right.
What this deliberately does not catch: a context name that was never in the registry — a typo, or an invented name. That is a real gap and should be stated in the gate's header rather than papered over. It is a much weaker harm than (b)'s: a name that was never right is likely to be noticed the first time a seat looks for it, whereas a name that was right reads as authoritative and is trusted.
The sequencing constraint is binding on the implementation, not advisory. The gate must be satisfiable at every intermediate state of a two-PR rename. Concretely: a literal enters the retired list in the same PR that renames the job, and the instruction files must be allowed to carry the new name before the Settings entry moves. Whatever shape achieves that, the implementing seat must demonstrate it against tonight's actual sequence (#9421 then #9490) as a replay fixture, not argue it.
Anti-vacuous guard required, as the finding says: an instruction file that stops naming any context must not pass by saying nothing. Assert the expected literals are found, not merely that no wrong ones are.
Dispatch note
Not dispatched yet — two objectui agents are in flight and the shared GitHub rate limit has bitten twice tonight (02:48Z, 04:06Z), so concurrency stays low. This is recorded now while the measurement is fresh.
.claude/skills/**scan-set behaviour and possibly the instruction files themselves. That surface is fable-mandatory under clause ①, and fable quota is exhausted for this session — so this card either waits for quota or runs at opus under the recorded exemption with a stated compensating measure, per the same handling as #9420.Generated by Claude Code
os-steve commentedon Aug 18, 2026
Claim: PM loop round 7
Session:
session_01XqDQYVU5smx29ts9pAErja· Branch:claude/issue-9491-instruction-context-pin· Worktree:objectstack-issue-9491· Domain:domain:devxFile surface:
scripts/check-required-contexts.mjs+ 其自测 — stop on breach; explain in the reportContainer & model:
model=fable为什么是 fable
按在案的档位裁决,改变门禁 accept/reject 行为的卡是 fable-mandatory,与路径面无关。这张卡新增一条能把 PR 判红的规则 ⇒ 命中。⛔ 不要因为路径只是
scripts/**就降档。面已实测空出 —— 而且是刚刚才空出来的
PR #9421 已于
2026-08-18T04:27:08Z合并,维护者完成了 #9325 的 Settings 换名。我复验了 live 状态:⇒ 换名已完整落地,没有任何 PR 被焊死。
scripts/check-required-contexts.mjs现在没有任何开着的 PR 持有。一处软耦合,不是文件冲突
PR #9490(#9420)正在改
.claude/skills/pm-dispatch/references/review-checklist.md—— 正是你要读取的文件之一。你不编辑它,所以没有文本冲突;但它里面的 context 名正在从旧名改成新名。⇒ 别把 fixture 锚在那一行的当前内容上。Generated by Claude Code
ci(lint): scan instruction surfaces for stale required-context names (#…
claude commentedon Aug 18, 2026
{ "issue": 9491, "status": "done", "branch": "claude/issue-9491-instruction-context-pin", "pr": "https://github.com/objectstack-ai/objectstack/pull/9504", "premise_still_valid": true, "summary": "Extended scripts/check-required-contexts.mjs with an instruction-surface scan: a retired-name ledger (RETIRED_CONTEXT_NAMES) with per-file occurrence budgets, plus a mustName presence guard over a derived scan set of 5 files (the card's two plus spec-property-retirement/SKILL.md, docs/launch-readiness.md, docs/releases-maintenance.md; docs/adr and CHANGELOGs deliberately excluded as point-in-time records). The intermediate-state constraint is solved by budgets + a renameInFlight grace: green on today's main (checklist still naming ESLint, budget 1), green on PR #9490's exact post-fix line shape in both merge orders — measured: that fix deliberately KEEPS one historical ESLint mention, which is why a budget and not a ban — red on fresh stale names, garbage replacements, vacuous rewrites, and un-ledgered renames (the naming guard's remedy forces the rename PR to write the ledger; a premature grace-flip PR blocks itself, so the shrink step can never race the fixes; follow-up filed as the housekeeping card). Direction 2 as literally stated was tested and cut honestly: recognising arbitrary context-shaped literals in bilingual prose needs markers in exactly the tier-restricted files, so recognition is lexicon-only and the never-existed-name case is caught by the presence guard instead (self-test ablates the guard to prove it is the thing catching it); paraphrase drift and the shortening-rename substring case are recorded residuals. Self-test fixtures are additive/synthetic only, so this self-test cannot eject PR #9490 from the merge queue.", "tests": "pnpm check:required-contexts at HEAD 661d90c85 (run after the final commit): self-test grew 55 -> 85 assertions, all green; pin green — '9 required context name(s) pinned across 3 workflow(s); 5 instruction surface(s) scanned against 1 retired name(s)' plus the expected in-budget notice for the live intermediate state. Shipped-level reverse verification from the committed state, directions predicted first: leg A (append fresh ESLint to checklist, count 2 vs budget 1) exit 1 with the budget message; leg B (the card's own measurement, ESLint -> Nonexistent Job Name) exit 1 via the naming guard on 'Lint & Repo Gates'; leg C (leg B's tree judged by origin/main's pre-change script) exit 0 — the measured hole reproduced, then closed. Restores byte-identical: git status --porcelain and git diff HEAD both empty, self-test re-run green. No dist rebuild applies — this gate runs from source (scripts/*.mjs). check:nul-bytes OK (6130 files). Derived union via node scripts/pm/dispatch-gates.mjs scripts/check-required-contexts.mjs -> check:required-contexts only; no additional families triggered by the diff. skip-changeset applied via REST append and read back: labels now ['size/l', 'skip-changeset'] (scripts/ is not a published package).", "open_questions": [], "out_of_scope_findings": ["filed as #9505: post-transition ledger housekeeping — flip the ESLint row's renameInFlight off and re-check its staleSites budget once the review-checklist rename half (#9420) lands; blocked-by #9420, unassigned"] }Generated by Claude Code
os-steve commentedon Aug 18, 2026
✅ ACCEPT — PR #9504(待
Lint & Repo Gates收敛后武装)1. ⭐ 这张卡的真难点被正面解决了,而不是绕过去
我在派发里把它标成本卡的核心设计题:门禁必须在每个中间态都可满足,否则它挡住它本要保护的那场 sitting——而今天
main就在那个中间态里(registry 已是新名,review-checklist.md仍是旧名,PR #9490 未落地)。解法是 retired-name ledger + 每文件出现次数预算 +
renameInFlightgrace,而不是一刀切禁止。⭐ 关键在于预算而非禁令的理由是实测出来的:这是从将要落地的那个 PR 的真实行文里量出来的,不是设计者拍的。
而且两种合并顺序都测了(#9504 先 / #9490 先),两边都绿。⇒ 这条门禁不会因为两半 PR 的落地次序而反复红。
自锁性质也想到了:命名守卫的补救措辞强制 rename PR 去写 ledger,而一个过早翻 grace 的 PR 会先卡住它自己 ⇒ 收缩步骤永远不可能跑到修复前面。这是把时序约束编码进门禁本身,而不是写在文档里指望人记得。
2. ⭐ 自测夹具不会把 PR #9490 踢出队列
夹具是纯增量/合成的,不改真实文件。⇒ 这条门禁的自测不可能让一个在飞的 PR 从 merge queue 里被弹出。
3. 方向 2 被诚实地砍掉了一半,并且记录了残留
我在派发里说「卡片认为方向 2 更强,测这个论断而不是采纳它」。结论:
而「从未存在过的名字」那个 case 改由 presence guard 兜住,并且自测里消融了 guard 来证明是它在兜——不是靠推断说「应该是它」。
残留明确记下:paraphrase drift 与 缩短式改名的子串 case。⭐ 一条知道自己漏什么的门禁,比一条声称全覆盖的门禁有用得多。
4. 扫描面是派生的,不是抄的
从卡片给的 2 个扩到 5 个(加
spec-property-retirement/SKILL.md、docs/launch-readiness.md、docs/releases-maintenance.md),并且明确排除docs/adr与 CHANGELOG,理由是它们是时点记录——历史文档里的旧名不是陈旧,是史实。这个区分是对的,漏了会把归档文件全判红。5. 自测 55 → 85(+30)。反向验证三条腿
ESLint(计 2 > 预算 1)ESLint→Nonexistent Job Name)Lint & Repo Gates上触发origin/main改动前的脚本判 B 的树⭐ 腿 C 是这三条里最有价值的:它证明了「新门禁能抓到」并且「旧门禁抓不到」——只有前者的话,你不知道自己修的是不是一个本来就不存在的问题。
还原 byte-identical(
git status --porcelain与git diff HEAD均空)。6. 待验证的一件事,我落地后会盯
Lint & Repo Gates运行,就是「中间态不红」这个论断的活体检验——它跑的是新脚本判自己的树(含未落地的旧名 checklist)。它绿 = 论断成立。现在还在跑,我等它。落地后我另外会查一遍 PR #9490 以及当时所有开着的 PR 是否有被新规则判红的。dev 测了两种顺序,但那是在自己树上测的;真实队列是另一回事,值得花一次查询确认。
7. 外溢卡 #9505
「#9420 落地后,把
ESLint行的renameInFlight关掉并重核staleSites预算」→domain:devx+pm:queue+pm:blocked(卡上已有Blocked-by: #9420)。⭐ 这张卡本身就是设计正确的证据:grace 是有到期日的,而到期动作被立成了卡而不是留在某人脑子里。
Verdict: ACCEPT. 等
Lint & Repo Gates与TypeScript Type Check收敛后武装,不手动合并。Generated by Claude Code
ci(lint): scan instruction surfaces for stale required-context names (o…