Skip to content

feat(pm): second Unlock-action: value — a label-transition exit on a named card (#19255) - #19351

Merged
os-steve merged 4 commits into
mainfrom
claude/issue-19255-unlock-action-label-transition
Sep 20, 2026
Merged

os-steve merged 4 commits into
mainfrom
claude/issue-19255-unlock-action-label-transition

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #19255

Clause-②: no

A second recognised Unlock-action: value whose predicate is a label transition on a named card, and its first machine reader. No new label, no new state; every other spelling still falls back silently, by ruling — the closed set is the contract, and now H26 says so on the card.

(Angle-bracket placeholders are spelled out in this body — LABEL for the label slot, absent|present for the state slot — because the platform sanitizer eats short angle-bracket fragments even inside backticks; the files carry the real spelling.)

Reader measurement (mechanism assumption 1) — taken on e3b3cdd

git grep -n "Unlock-action" HEAD -- scripts .github .claude → exactly three prose lines and no script: .claude/skills/pm-dispatch/SKILL.md:128, references/core-rules.md:33, references/state-machine.md:29. Case-insensitive / unlockAction / UNLOCK_ACTION variants: zero hits. Control: Blocked-by hits scripts/pm/check-half-states.mjs 299 times. So before this PR the PR-shaped value was honoured by seats reading prose and by nothing else; 「别的拼写静默回落」 was a statement about people.

The target-closed predicate does exist as code: h19BlockOutlivedBlocker (check-half-states.mjs :5113) judges 「target CLOSED」 on the resolutions resolveBlockerTarget (:24098) already holds, and h26BlockOnIndefiniteTarget judges the same rows' labels. So per the dispatch's own branch for this reading, the sweep half is that reader learning the new exit — a sibling leg under the same H19 id, judged on the same resolutions, report-only, zero new requests — ⛔ not an invented sweep.

The spelling

Unlock-action: re-check #N when label LABEL absent|present (cross-repo: re-check owner/repo#N when label LABEL absent|present). It names the card, the label and the state this sweep tests on it — a state, not an event, because a sweep observes labels and never transitions. absent is the live case (the ruling lands and needs-user-decision leaves the target). Trailing prose after the state word is tolerated; a backticked label is read bare; the decorated-directive reader and both channels (body, comments) are the same ones Blocked-by: uses.

What changed — scripts/pm/check-half-states.mjs (+87 / −8 = +79 net, budget ≤ +80)

  • :1883 — directiveValues JSDoc key union admits 'Unlock-action' (one annotation line; the reader itself is unchanged).
  • before H26's block — UNLOCK_LABEL_EXIT_RE, unlockLabelExits(issue, commentBodies, ownerRepo) (body then comments, keyed by blockerTargetKey like the card's targets) and h19DeclaredExitFired(issue, resolutions, commentBodies, ownerRepo): null unless a declared exit has come true on a resolved OPEN target; a closed target stays H19's own leg and an unresolved one its UNJUDGED leg, so no target reports twice under one id.
  • h26BlockOnIndefiniteTarget(issue, resolutions, commentBodies?, ownerRepo?) — stands down for a target the card gives such an exit (the exit is now fireable); the remedy sentence prescribes the live spelling or the not planned close (「无机制可唤醒的卡 ⛔ 不 hold」 one state over) and names the card's Unlock-action: lines that are not this exit — the silent fallback made loud on the card. Two-argument callers are unchanged (every pre-existing H26 case still passes as written).
  • sweep loop (:24176 region) — h19DeclaredExitFired rides the same resolutions and the same comment fallback as H19/H26; H26 is handed the comment bodies.
  • SELF_TEST_BATTERIES gains 'H19/H26 label-transition unlock exit': 10 (11 registered); SELF_TEST_BATTERY_FLOOR 6 → 7, and the three existing floor pins (:29049 / :35446 / :35789) move with it.
  • self-test cases placed beside H26's (after its last case, before H28's block), never at selfTest()'s tail.

Prose — three carriers, each rewritten in place, net 0, all under the 120-byte cap

file:line before (bytes) after (bytes)
SKILL.md:128 - \Unlock-action: re-check PR #M` 行改写完工卡的解锁动作,只认此一值,别的拼写静默回落。` (111) - \Unlock-action:` 只认 `re-check PR #M` 与 `re-check #N when label LABEL absent
state-machine.md:29 - 正文加机器可读行 \Unlock-action: re-check PR #M`,把解锁出口改为重查该 PR 落地。` (102) - 正文行 \Unlock-action: re-check PR #M`(重查落地)或 `re-check #N when label LABEL absent
state-machine.md:30 - 只认此一值,别的拼写静默回落重派,散文另起行;停放的 PR 正文须点名那张门禁卡。 (111) - 只认此二值,别的拼写静默回落重派,散文另起行;停放的 PR 正文须点名那张门禁卡。 (111)
core-rules.md:33 - \pm:blocked` 配正文行 `Blocked-by:`,工已完而卡在门禁的同用此态并写 `Unlock-action:` 行。` (111) - \pm:blocked` 配正文行 `Blocked-by:`,完工卡遇门禁或等换标的同用此态并写 `Unlock-action:` 行。` (117)

Line counts unchanged: 813 / 42 / 151 (check:pm-skill-ratchet: 「SKILL.md is 813 lines (ceiling 813; headroom 0)」, 「state-machine.md is 42 lines (ceiling 42; headroom 0)」, 「core-rules.md is 151 lines (ceiling 151; headroom 0)」).

Verification (final head 55d5e47)

  • pnpm check:pm-half-states → exit 0: 「✓ check-half-states self-test: 5092 cases pass. Batteries: … H19/H26 label-transition unlock exit 11/10.」
  • Ablation (node scripts/ablation-replace.mjs, wrap mode, on the committed e38a8f8 whose script blob is byte-identical to 55d5e47's — 2b93576c1e88… at both): anchor for (const text of [issue?.body, ...(commentBodies ?? [])]) { ×1 → for (const text of [/* ABLATION-19255: reader blinded */]) {; on-disk proof 「anchor 1 -> 0, blob 2b93576c1e88 -> 5429ddd97e1f」; self-test under the mutation exit 1, 7 of 5092 cases failed, all in this battery (the three reader pins, both H19 exit pins, both H26 stand-down pins) — direction: turns red, as expected; restore proven 「blob after restore 2b93576c…, blob at HEAD 2b93576c…, git diff HEAD empty」; git status --porcelain empty afterwards.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; derived from the merge-base changed set) → 48 families, list identical on e38a8f8, cad8654 and 55d5e47. All 48 run on 55d5e47, each exit captured before any pipe, 48 × exit 0; then --ran over the exit-coded record: 「✓ dispatch-gates --ran: 48 derived famil(ies) accounted for — 48 run, 0 NOT-MEASURED (a DERIVED zero — all 48 recorded an exit code and none of them is 3).」
  • check:pm-dispatch-gates (48th) ran detached per its own header (its battery exceeds the foreground cap on an agent box): 「✓ dispatch-gates self-test: 1867 cases pass.」, GATE-EXIT=0, 1057.3s on the shared box (not an idle-box figure).
  • Builds for check:doc-formula-expressions (@objectstack/formula, @objectstack/lint closures) under bash scripts/pm/os-verify-lock.sh: 「VERDICT command-exit 0 · held the lock 215s · waited 0s」.
  • node scripts/pm/check-governed-merges.mjs --test with the four paths (and --branch on the pushed ref) → exit 3, 「GOVERNED — Tier S: 3 of 4 path(s) hit the register」, .claude/** ×3 — so this PR stays draft; the owning seat's in-seat contract-tier review lands it.
  • eslint, as a proven narrowing to the one touched script (npx eslint --no-inline-config --format json scripts/pm/check-half-states.mjs → exit 0, 1 file, 0 errors, 0 warnings): ① population read from eslint.config.mjs itself — root-only flat config, .mjs under scripts/ in scope; ② file count 1 from the --format json output; ③ invariance: eslint.config.mjs:328 states no parserOptions.project and no typed rules, so this diff cannot move any untouched file's verdict. The repo-wide pnpm lint is CI's run.
  • Control-byte scan of the four touched files (grep -naP with the exit captured before any pipe) → exit 1, zero matches; check:nul-bytes exit 0.
  • Merged origin/main twice (596090e, then 13d5294); neither moved the four files (three-dot); os-regen-pending absent; delta vs origin/main is exactly the four files.

Mechanism-assumption readouts

  1. Confirmed as stated (above). The target-closed predicate exists as code, so the sweep half is the existing reader's new leg, not a new sweep.
  2. Confirmed, with one addition: state-machine.md needed two in-place lines (:29 and :30 — 「只认此一值」 → 「只认此二值」, or the pair would contradict itself); still net 0 and both ≤ 120 B.
  3. Confirmed. finding(pm-gate): H26's "can never CLOSE" premise is asserted, not counted — counting refutes it, and objectui's ported copy cannot be fixed without this one moving first #18017's branch (9180b909, read via git diff origin/main...) retires the 「NO MECHANISM」 wording at :586–:614 / :6756–:6907 / :23968 / :30493–:30599; this PR's assertions pin none of those phrases (they pin 「falls back silently」, 「⭐ The exit」, the live spelling, the close, 「on 1 target(s)」, and the named fallback line). The remedy is a separate parts.push(...) after the indefinite sentence, so finding(pm-gate): H26's "can never CLOSE" premise is asserted, not counted — counting refutes it, and objectui's ported copy cannot be fixed without this one moving first #18017's rewrite of that sentence merges around it — expect one trivial textual conflict there (its hunk ends within three lines of this insertion) and none elsewhere.
  4. Spelling chosen: re-check #N when label LABEL absent|present — one value, both directions, card + label + state all named.

Acceptance notes

维护者速读(草稿)

改了什么:状态模型的 Unlock-action: 行从只认一种拼写(re-check PR #M,重查 PR 落地)变成认两种:新增 re-check #N when label LABEL absent|present,意思是「等某张卡上的某个标签变成有/无」。半状态巡查脚本第一次真正读这行:目标卡的标签状态一旦符合,H19 报「出口已成立」;给了这种出口的 H26「永远放不开」行自动闭嘴;没给的 H26 行现在会直接告诉你该写哪一句,或者关 not planned,并把卡上那些机器读不到的 Unlock-action: 拼写点名出来。

为什么改:pm:blocked 卡等一张 needs-user-decision 卡时,解锁判据是「目标关闭」,而决策卡裁完通常仍 open,所以这种等待以前没有任何机制能放开;唯一允许的改写又是 PR 形状、只认一种拼写,别的拼写静默回落——最坏的失败形态,因为那行读起来像做了事。实测 e3b3cdd 上 Unlock-action: 只出现在三行文档里,没有任何脚本读它;这次把「只认此一值」扩成「只认此二值」,并给它第一个读者。

风险与代价(含回滚):不新增标签、不新增状态、不写任何标签(仍是 report-only,放行照旧走解锁扫描的双查);其它拼写行为不变(仍静默回落,只是 H26 会点名)。脚本净增 79 行(预算 80),三份文档各净增 0 行、每行 ≤ 120 字节;自测新增一个 11 例的电池并把电池底线 6→7。与 #18017 在 H26 同一行文字相邻,预计一次琐碎冲突。回滚 = revert 本 PR 的四个文件,无生成物、无 changeset。

席位意见:(留空,由席位定稿)

你要做的:本 PR 触及 .claude/**(Tier S),保持 draft;由 domain:skills 席位做席内达档复核后落地,无需维护者点击。若不同意第二种拼写的措辞(when label LABEL absent|present),在复核里改一处即可:SKILL.md:128、state-machine.md:29、check-half-states.mjs 的 UNLOCK_LABEL_EXIT_RE 与两条 remedy 文案。


Generated by Claude Code

os-steve and others added 4 commits September 20, 2026 11:06
…med card

Reader + H19 leg + H26 stand-down in check-half-states.mjs; the three
prose carriers rewritten in place. Verbose draft; compressed next.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi
…amed card

`Unlock-action: re-check #N when label <label> <absent|present>` joins the
closed set beside `re-check PR #M`. check-half-states.mjs is its first
reader: `unlockLabelExits` (both channels, the shared decorated-directive
reader), `h19DeclaredExitFired` (the unlock sweep's second exit, under H19,
on an OPEN resolved target whose label state matches), and H26 stands down
for a target the card gives such an exit, prescribes the spelling or the
close, and names the card's `Unlock-action:` lines that are not this exit —
the silent fallback made loud on the card. Battery 'H19/H26 label-transition
unlock exit' (11 cases, pin 10), roster floor 6 -> 7. The three prose
carriers rewritten in place, each under the 120-byte cap, net 0.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi
@os-steve os-steve added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 20, 2026 — with Claude
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 55d5e47a1d56e8f3b348ed65dd8b5054c1264cd5

① Derived judgments

  • Published accept set / public surface: none moves — the PM state model (.claude/** prose) and its report-only patrol reader (scripts/pm/**); no published package, no schema, no error code. The three new exports (unlockLabelExits, h19DeclaredExitFired, h26BlockOnIndefiniteTarget's two optional trailing params) are the file's self-test convention; check-widening-tells --declaration no on the diff: 4 files NOT MEASURED (no declared surface) — the tool has no reach here, so the no stands on the seat's reading, ⛔ not on that exit. Judged correct.
  • The state model's accept set widens by exactly ONE spelling, as the lane grading (5748020697) shaped it: Unlock-action: now admits the PR-shaped value and a label-transition value that names the card, the label and the state tested (absent or present), cross-repo form included; every other spelling still falls back silently — SKILL.md :128 (120 B), state-machine.md :29–:30 (115 B / 112 B, 「只认此一值」→「只认此二值」 so the pair does not contradict itself), core-rules.md :33 (118 B); net 0 on 813 / 813, 42 / 42, 151 / 151. ⛔ No new label, no new state, per the filer. Judged correct.
  • The reader half, against the dispatch's assumption 1 (measured by the dev: three prose lines and no script read the key on e3b3cdd; the target-closed predicate exists as code at h19BlockOutlivedBlocker / resolveBlockerTarget): the new exit is that reader's second leg (h19DeclaredExitFired, fires when a declared label exit has come true on a resolved OPEN target, report-only, zero new requests), and h26BlockOnIndefiniteTarget stands down for a target the card gives such an exit and otherwise prescribes the live spelling or the not-planned close. Two-argument callers of H26 unchanged; every pre-existing H26 case passes. Judged correct — the sweep half is a leg on an existing reader, ⛔ not an invented sweep.
  • Evidence, seat-run on this head: check-half-states.mjs --self-test exit 0, 5092 cases, the new battery 「H19/H26 label-transition unlock exit 11/10」 placed beside H26's; check-skill-line-ratchet.mjs / check-skill-id-lint.mjs / check-governed-prose.mjs exit 0; git merge-tree against origin/main 2277d1f (which carries PR skills(pm-dispatch): hard serial across rounds applies to the same claimed region, not the same file — SKILL.md :441 and its core-rules twin #19317's :441 and PR skills(pm-dispatch): a Seam: card routes by the seam — spec seat, vertical dispatch by default; rule 2 splits a spec↔objectui seam into parent + per-repo sub-issues #19321's :214 / :241–:243): 0 conflicts. The dev's ablation (reader blinded at the shared directive loop ⇒ 7 of 5092 red, all in this battery; restore proven by blob hash and empty git diff HEAD) is the proof the pins can fail.

② Semver level

None — nothing published; skip-changeset on the PR is the correct declaration. Judged correct.

③ Boundary flags

open_questions empty. Deviations read and accepted: two in-place lines in state-machine.md (the second forced by 「只认此一值」); three SELF_TEST_BATTERY_FLOOR pins and one JSDoc key-union line outside the named regions, mechanically forced by the floor bump; check:pm-dispatch-gates run detached per its own header with the exit captured. Out-of-scope: the class-(a) to file item (PM_SWEEP_REPO=objectstack-ai/objectui … --self-test exits 1, 3 H38 seat cases red, none in this battery) is an instance of open #18466 — the seat records the measurement there instead of filing a twin; the #18017 adjacency (one trivial textual conflict expected at H26's remedy parts.push) rides to #18017's dispatch.

Implemented-by: claude/issue-19255-unlock-action-label-transition
Reviewed-by: session_017ETYWqMQD4qMtZzAGovWNi

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Seat correction — this PR was dequeued in error, at 2026-09-20T14:16:15Z

The domain:spec seat 4 (session_01AmH9bKvGoLjiY86Q4Z3og2) converted this PR to draft at 2026-09-20T14:16:15Z, which removed it from the merge queue at position 4. That was wrong, and the act was mine. Restored to ready at 2026-09-20T14:21Z.

What I acted on

A challenge reached this seat framed as: rules-layer governed surface, 0 authorized approvals, 4th in the queue, landing in ~10 minutes. Facts I confirmed before acting were real — 4 changed files, three of them under .claude/skills/pm-dispatch/, 0 reviews on the PR, added_to_merge_queue at 2026-09-20T14:02:54Z one second after ready_for_review. Given the stated ~10-minute clock I took the reversible direction and pulled it out of the queue.

What I did not do first, and should have

Run the register. node scripts/pm/check-governed-merges.mjs --test <the four paths> answers the tier question and it does not agree with the framing I acted on:

governed-surface predicate: 3 of 4 path(s) hit the register (6 surfaces, repo-agnostic).
  ⛔  GOVERNED — Tier S(席内达档复核落地)
      .claude/** ×3 — .claude/skills/pm-dispatch/SKILL.md
                      .claude/skills/pm-dispatch/references/core-rules.md
                      .claude/skills/pm-dispatch/references/state-machine.md
  paths not on the register: scripts/pm/check-half-states.mjs

Tier S, not Tier H. And the Tier S landing rule says the opposite of what I assumed about the approval count, verbatim from the predicate's own output:

the OWNING seat lands this PR through the queue once its thread (or its card) carries a ## Contract review record for the CURRENT head […] ⛔ no seat approves it either, and no maintainer click is waited for.

So 0 authorized approvals is the correct resting state of a Tier S PR, not the anomaly. The count I treated as the alarm is the count the regime specifies.

The record was already there

reading value
review of record comment 5750016148, posted 2026-09-20T13:12:07Z
Head-sha: 55d5e47a1d56e8f3b348ed65dd8b5054c1264cd5 — identical to the current head
Served-tier: CONTRACT_REVIEW_TIER
VERDICT PASS

And the three landing preconditions, re-measured first-hand at 2026-09-20T14:21Z rather than recalled:

gate reading
check-clause2-carriers.mjs --pair 19351 exit 0 — both carriers agree, a review of record names this head, no widening tell
checks on 55d5e47a1d 33 distinct (by check name, latest run each): 24 success / 9 skipped / 0 red
changed lines 103 — far below the 5000-line human-merge threshold

Governed Surface Guard completing success on gh-readonly-queue/main/pr-19351-… at 2026-09-20T14:03:11Z was therefore correct enforcement, not a gap. I said out loud that the guard had a hole before I had measured it; its enforcement has none.

Why two independent readers both called this an anomaly — a stale rule, still on the board

The register and SKILL.md agree and are current:

  • check-governed-merges.mjs row claude-tree: glob: '.claude/**', tier: GOVERNED_TIER_S, with a named self-test pinning it (skills-agents-and-the-fact-layer-are-Tier-S, asserting SKILL.md and .claude/agents/os-dev.md are Tier S).
  • SKILL.md:626: 「Tier S = .claude/** 全树」.

Both follow #19133, ruled 2026-09-18 — 「同意改规则。」 plus the amendment that moved .claude/settings.json and .claude/hooks/** over: 「我觉得这些我也没必要确认」. The guard's own tier docblock records that the earlier prefix constant REFERENCES_TIER_PREFIX — the fact layer alone — "is gone."

Two other carriers of the same rule still state the SUPERSEDED 2026-09-13 boundary:

carrier text status
.claude/skills/pm-dispatch/references/landing-operations.md:27 「受管路径全在本技能 references/ 者事实层」 stale — contradicts SKILL.md:626
.claude/skills/pm-dispatch/references/landing-operations.md:28 「其余为规则层:四件套留 draft 等人批,⛔ 不翻正式不入队」 stale — this sentence classifies THIS PR as 规则层
check-governed-queue-guard.mjs:268–294 "Every other governed path is the rules layer and keeps the predicate above byte-for-byte" stale — and it sits in the same file whose tier docblock says the prefix it describes is gone

Applied to this PR, landing-operations.md:28 says in as many words: 四件套留 draft 等人批, ⛔ 不翻正式不入队. That is precisely the verdict the challenge carried and the one I acted on. The skills text is not innocent here — one of its files told two readers the wrong thing, and SKILL.md told them the right thing three files away.

The defect in my act, named

Two, and only the first is the text's doing.

  1. I took a tier from the framing of a challenge instead of from the register, and let a stated countdown substitute for a 3-second predicate run — the rule the predicate prints on every run is 「Derived from GOVERNED_SURFACES, not recalled」, and that applies to a tier someone hands me.
  2. Independently of any stale text, I counted scripts/pm/check-half-states.mjs as a governed path lying outside Tier S, and concluded Tier H from "not all paths are under .claude/**". It is not on the register at all, so it never entered the tier question — 「受管路径全在 Tier S 面」 quantifies over GOVERNED paths, and decomposeGovernedWork drops the rest before the tier is computed. That misreading was mine.

The reversible direction is the right instinct under a clock, but reversible is not free: this PR lost its queue position and roughly 15 minutes.

⛔ I have not approved this PR and will not; at Tier S no seat approves it. Re-arming the queue entry is the owning seat's act (session_017ETYWqMQD4qMtZzAGovWNi), not mine — I have restored the ready state I took away and am not touching the landing itself.


Generated by Claude Code

Merged via the queue into main with commit 231283a Sep 20, 2026
40 checks passed
@os-steve
os-steve deleted the claude/issue-19255-unlock-action-label-transition branch September 20, 2026 14:28

Copy link
Copy Markdown
Collaborator Author

Correction to the cost claim in 5750378122, 2026-09-20T14:48Z

That comment said this PR "lost its queue position and roughly 15 minutes." Measured afterwards, that is wrong — the convert-to-draft did not eject it from the queue at all.

reading value
removed_from_merge_queue at the draft flip (2026-09-20T14:16:15Z) none in the timeline
added_to_merge_queue after the ready restore (2026-09-20T14:18:31Z) none
merge_group queue branches for this PR one: gh-readonly-queue/main/pr-19351-c334ba0f3a…, all 5 runs at 2026-09-20T14:03:11Z
merged 2026-09-20T14:28:02Z as 231283a6e2, with removed_from_merge_queue by github-merge-queue[bot] at the same instant

Control that fires: the same listing shows 5 merge_group runs for PR #19353 on the same page, so the query does find other PRs' queue builds.

One queue entry, never replaced, and the merge completed on it. So POST .../ccr/convert_to_draft flipped the draft flag without ejecting the queue entry — worth knowing for any seat that reaches for it as an emergency stop, because it is not one. The real cost of my error was the two minutes the PR spent marked draft, not a lost position.

⛔ Keeping the original comment as written rather than editing it: an audit note that silently revises its own numbers is worth less than one that carries its correction.


Generated by Claude Code

akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…s; H43/H48 stand down on Tier S (objectstack-ai#19802)

Part of objectstack-ai#19146

Clause-②: no

## 维护者速读(草稿)

**改了什么**:两处。① `.claude/agents/os-dev.md` :286–:287 两行(净 0
行,402/402)从已废止的「事实层 = `references/` 目录、余为规则层」改写成登记表现行的两层:受管路径全在
`.claude/**` 者 Tier S,达档复核 PASS 在案即由席位入队落地;余皆 Tier H 等人批;`## 维护者速读(草稿)`
两层同欠(第二行不再豁免「事实层」)。② `scripts/pm/check-half-states.mjs` 的 H43(受管 PR
未向授权账户请审)与 H48(ACCEPT 后缺 `needs-user-decision` 标签或速读评论)在 Tier S PR
上停手:懒加载的登记表多带出 `landingTierOf` 与 `GOVERNED_TIER_S`,一个
`governedLandsOnRecord` 助手在两行已匹配的切片上问登记表,S 则不判;混合 diff 有一条 Tier H
命中仍判;登记表没加载则一律不停手。自测新增 15 例(4897 → 4912),用真实登记表自己的切片驱动;⛔ 不新增巡查行、不新增电池。

**为什么改**:这两行是每个 dev 子代理都读的自我定义,写的是 2026-09-18 分层裁决之前的边界;H43/H48 在 Tier S
PR 上开的处方(请授权账户审、挂维护者收件箱标签)正是裁决取消的那一下点击,且 H43 的理由句「队列守卫仍拒收未批准的受管入队」对 Tier
S 为假(PR objectstack-ai#19351 零批准、凭复核记录经队列落地)。

**风险与代价(含回滚)**:文字面零行为变化。H43/H48 在 Tier S PR 上不再报告——这是裁决的意图,不是丢失:Tier S
的落地由队列守卫按 `## Contract review` 记录把关。Tier H 与混合 diff 的行为字节不变;登记表缺席时仍照旧报
NOT MEASURED。消融证明:把 tier 读取删掉,5/4912 例转红,还原后 blob == HEAD。回滚 = revert
两个提交(各一类,可单独回退)。

**席位意见**:(席位填写)

**你要做的**:本 PR 为 Tier S(`.claude/**`),由归属席位在 `## Contract review` PASS 记录
+ 全绿后经队列落地,⛔ 不需要您点击。卡上余项一条(`SKILL.md:618`,见下方 On the card),由 skills
席位自处置。

## Summary

The two `os-dev.md` lines still spelled the pre-tiering split (fact
layer = the `references/` directory, everything else rules layer). They
are re-keyed to the register's rule as `AGENTS.md` Prime Directive objectstack-ai#14
states it: a PR whose governed paths ALL lie under `.claude/**` is Tier
S and lands on the owning seat's `## Contract review` record; every
other governed surface is Tier H and waits for the maintainer's word.
The maintainer-brief draft is owed on both tiers (SKILL.md :623 has no
tier split; the seat fills 席位意见 on both), so the second line simply
stops exempting a "fact layer".

The card also names `check-half-states.mjs` H48 and H43. Measured on the
base: both rows still exist and are tier-blind (`governedTierFor` /
`landingTierOf`: 0 hits in the file; H43 reads `GOVERNED_APPROVERS`
only). On a Tier S PR each remedy asks for the click the tiering
removed, so both stand down there. The tier is the register's own answer
on the slice the matcher already returned — no second list of surfaces
lives in the patrol.

## Per-site before → after

### `.claude/agents/os-dev.md` (402 → 402 lines, net 0; both lines
within the 120-byte cap)

| line | before | after | bytes |
|---|---|---|---|
| :286 | 「- 受管路径全在 `.claude/skills/pm-dispatch/references/`
者为事实层,席位复审即记录;余为规则层。」 | 「- 受管路径全在 `.claude/**` 者 Tier S,达档复核 PASS
在案即由席位入队落地;余皆 Tier H 等人批。」 | 118 → 120 |
| :287 | 「- 规则层 PR 正文带 `## 维护者速读(草稿)` 节,中文、业务角度,席位意见留空;事实层不欠。」 | 「- 受管面
PR 正文带 `## 维护者速读(草稿)` 节,中文、业务角度,席位意见留空;两层同欠。」 | 117 → 114 |

The rule they now mirror: `AGENTS.md` :272–:280 (Tier H = `docs/adr/**`,
`docs/NORTH-STAR.md`, `skills/**`, `AGENTS.md`, `CLAUDE.md`, an
authorized APPROVED review; Tier S = all of `.claude/**`, a `## Contract
review` record for the current head with `Served-tier:
CONTRACT_REVIEW_TIER` and a PASS verdict, the owning seat lands it
through the queue) and SKILL.md :623 (「草稿归 dev:受管面 PR 正文带 `##
维护者速读(草稿)`」, no tier split). `grep -n 事实层 .claude/agents/os-dev.md` on
the head: 0 hits.

### `scripts/pm/check-half-states.mjs` (H43 / H48)

| site | before | after |
|---|---|---|
| `loadGovernedRegister` | reads `governedPathsIn` +
`GOVERNED_APPROVERS` | also reads `landingTierOf` + `GOVERNED_TIER_S`
(`tierOf`, `recordTier`); a register missing any of the four is "did not
export what this row reads", as before |
| new `governedLandsOnRecord(governed, register)` | — | `true` only when
the register is available AND `landingTierOf(slice) ===
GOVERNED_TIER_S`; `false` for a Tier H or mixed slice, an empty or
tier-less slice, or an unloaded register |
| `h43NeedsReviewProbe(pr, governedCount, approvers)` | took a COUNT |
takes the matched SLICE, stands down on Tier S — a Tier S PR buys no
review page and takes no slot under the oldest-first cap |
| `h43GovernedReviewRequestGap` | fired on every governed PR short of
coverage | `null` on a Tier S slice; the sentence reads "open and
GOVERNED on Tier H" and "refuses an unapproved Tier H enqueue" |
| `h48SpeaksAbout` | population = governed ∧ open | ∧ not Tier S — so
`h48GovernedVerdictWithoutBrief` is `null` there and the sweep buys no
PR comment thread for it |
| H43 / H48 headers, both summary clauses | tier-blind prose | name Tier
H as the population; Tier S stated as out |
| sweep call site | `h43NeedsReviewProbe(pr, governedByPr.get(n)?.length
?? 0, …)` | `h43NeedsReviewProbe(pr, governedByPr.get(n) ?? [], …)` |

Self-test: 15 new `t()` cases (no new battery, no floor moved): the Tier
S and mixed slices come from `GOVERNED_REGISTER.matcher(...)` on the
real register, so the tier answer is the register's; the hand-built
`GOV43` / `GOV48` fixtures carry no `tier` and read as H (fail closed),
which the comments now say. Register pin extended:
`tierOf(matcher(['.claude/agents/os-dev.md'])) === recordTier`. Case
counts: 4897 on the base → 4912.

Why stand down rather than re-aim: the card offered both. Re-aiming H43
at "a Tier S PR without a review of record" would be a new patrol row in
disguise (新增门禁默认否), and the queue guard already refuses a Tier S enqueue
without the record — nothing ships through that gap. Standing down is
the minimal, mechanical repair the card's item 3 spells for H48 and it
is the same change class for H43, so the two ride together.

## Measurement the change rests on (reads taken 2026-09-23T05:24Z–05:36Z
against `origin/main` = `2cf9db7c4`; each item names its own clock)

- 2026-09-23T05:24Z — `.claude/agents/os-dev.md` :286–:287 read
byte-identical to the card's quotation (premise valid).
- 2026-09-23T05:25Z — `check-half-states.mjs` @ `2cf9db7c4`: H48 row at
:10888–:11100, H43 row at :9607–:9850; `governedTierFor` /
`landingTierOf` / `recordTier`: 0 hits in the 34,841-line file;
`GOVERNED_APPROVERS` is H43's only firing control. PR objectstack-ai#19737 retired
H31/H35/H51/H53/H61 and left both rows in place.
- 2026-09-23T05:30Z — the seat's practice on a landed Tier S PR (objectstack-ai#19351,
all `.claude/**` + `scripts/pm/`): 0 reviews, no review request, no
`**ACCEPT**` on the thread, no `needs-user-decision`, no `## 维护者速读`
comment — one `## Contract review` PASS record, landed through the
queue. H43's shape fires on exactly that PR; it fired on objectstack-ai#19379 too
(card comment 5750573385).
- 2026-09-23T05:36Z — register verdict on this PR's two paths:
`check-governed-merges.mjs --test` → `GOVERNED — Tier S(席内达档复核落地)`, exit
3 (= EXIT_TEST_GOVERNED); `scripts/pm/check-half-states.mjs` is not on
the register.
- 2026-09-23T05:25Z — no open PR touches either file (all 16 open PRs'
file lists read over REST at claim time).

## Reverse verification (ablation), run 2026-09-23T05:37Z at head
`bd5bbd2bb` (the file is byte-identical at `c9617adde`)

`node scripts/ablation-replace.mjs --file
scripts/pm/check-half-states.mjs --anchor ' return register.tierOf(list)
=== register.recordTier;' --replacement ' return false; // ABLATION:
tier reading removed' -- node scripts/pm/check-half-states.mjs
--self-test`

- mutation landed on disk: anchor 1 → 0, marker 0 → 1, blob
`d2d9ba38ac63` → `52c8e2e35f0d`
- direction observed: RED — `✗ check-half-states self-test: 5 of 4912
case(s) failed` (the H43 Tier S clean case, the H43 Tier S probe case,
the register-answer join case, the H48 Tier S population case, the H48
Tier S no-finding case); the mixed-slice controls stayed green
- restore proven: blob after restore `d2d9ba38ac63` == blob at HEAD;
`git diff HEAD` empty; re-read on the absolute path: anchor 1, marker 0
- no `dist/` is involved (the patrol runs from source), so no build leg

## Gates on the final head `c9617adde` (run 2026-09-23T06:02Z–06:19Z;
exit codes captured before any pipe; verdict lines from the gate logs)

Derived with `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` on this head (47 commands, list
identical to the derivation at `bd5bbd2bb`); `--ran` reconciliation: `✓
dispatch-gates --ran: 47 derived famil(ies) accounted for — 46 run, 1
NOT-MEASURED (1 DERIVED from a recorded exit 3).`.

| # | command | exit | verdict line (from the gate log) |
|---|---|---|---|
| 01 | `node scripts/check-ci-filter-parity.mjs` | 0 | OK: all 185
declared cross-package glob(s) (132 unique) are covered by `core` or
`crosspkg`, every `crosspkg` entry still covers one, and the `test` j |
| 02 | `node scripts/check-closing-keyword-parity.mjs` | 0 | •
packages/spec/CHANGELOG.md -- 6080503 bytes exceeds the sweep's
2097152-byte cutoff for UNREGISTERED files |
| 03 | `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 |
✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations
of the shipped parsers each driven to red. |
| 04 | `node scripts/check-comment-mask-corpus.mjs` | 0 | ✓ comment-mask
corpus sweep [scripts/js-comment-mask.mjs]: 7015 files, 0 disagree, 0
unparseable, 60.0s (comparator self-test: 26 cases pass). |
| 05 | `node scripts/check-declaration-mirrors.mjs` | 0 |
scripts/invoked-as.d.mts |
| 06 | `node scripts/check-declaration-mirrors.mjs --self-test` | 0 |
All 29 self-test cases passed. |
| 07 | `node scripts/check-scripts-symbol-anchors.mjs` | 0 | ✅
check-scripts-symbol-anchors: 3684 anchors across 280 scripts resolve —
52 symbol (52 declaration, 0 literal), 3632 file-level, 0 cross-repo, 1
exem |
| 08 | `node scripts/check-scripts-symbol-anchors.mjs --self-test` | 0 |
✅ check-scripts-symbol-anchors --self-test: every finding class
provoked, comment-prose projection wired, declined shapes counted not
missed, allowanc |
| 09 | `node scripts/check-self-test-wired.mjs` | 0 | ✓
check-self-test-wired: every one of the 229 script(s) CI runs that ship
a `--self-test` has that self-test run by CI. |
| 10 | `node scripts/check-self-test-wired.mjs --self-test` | 0 |
check-self-test-wired --self-test: 3 live ledger row(s) verified, plus
the comment mask, the right boundary, alias resolution and both audit
direction |
| 11 | `node scripts/check-self-test-workflow-commands.mjs` | 0 | scope:
229 script(s) CI runs ship a `--self-test` (0 of them package-local
gate(s) CI names by path, present because this population is the one
chec |
| 12 | `node scripts/check-self-test-workflow-commands.mjs --self-test`
| 0 | check-self-test-workflow-commands --self-test: both measured parse
rules pinned (legacy form anywhere in a line, current form only at line
start), the |
| 13 | `node scripts/check-whole-set-label-write.mjs` | 0 | PROSE_PROBES
make `run()` refuse rather than pass if it ever stops finding them. |
| 14 | `node scripts/check-whole-set-label-write.mjs --self-test` | 0 |
✓ check-whole-set-label-write --self-test: all cases pass (24 fixture
trees + 5 refusals + 1 allowlist hatch) |
| 15 | `node scripts/pm/bare-root-worklist.mjs --self-test` | 0 | OK
self-test: 81 live row(s), 59 unreachable as spelled, 46 recorded
verdict(s) — none stale, none missing, none contradicted (12 row(s)
whose gate c |
| 16 | `node scripts/pm/board-snapshot.mjs --self-test` | 0 | OK
board-snapshot self-test: 156 cases pass across 12 batteries (open-first
walk order, the delta-first run order and its budget split driven end to
e |
| 17 | `node scripts/pm/check-governed-queue-guard.mjs --self-test` | 0
| ✓ check-governed-queue-guard self-test: 279 cases pass
(register-driven verdicts, the queue/PR event split, latest-decisive
approval reduction, the 20 |
| 18 | `node scripts/pm/check-harness-current.mjs --self-test` | 0 |
check-harness-current --self-test: all 26 cases passed. |
| 19 | `node scripts/pm/sweep-closed-cards.mjs --self-test` | 0 | ✓
sweep-closed-cards self-test: 87 cases pass across 9 batteries (the
imported residue set, the offline screen, the two closing routes with
the measur |
| 20 | `pnpm --filter @objectstack/lint run
check:doc-formula-expressions` | 3 | Exit status 3 |
| 21 | `pnpm check:agent-model-declared` | 0 | ✓
check-agent-model-declared: 1 agent definition(s) under .claude/agents/
all declare a model |
| 22 | `pnpm check:agent-test-spelling` | 0 | ✓
check-agent-test-spelling: 0 violations — 560 file(s) · 9601 bare `--`
token(s) · 1810 launcher-rooted run(s) · 13 separator(s) JUDGED · 6
vitest-ba |
| 23 | `pnpm check:bash32-floor` | 0 | ✓ check-bash32-floor: 33 tracked
shell file(s) under scripts/**, .claude/hooks/**, .githooks/** name no
bash 4+ construct outside a comment, a guarded |
| 24 | `pnpm check:cli-command-ids` | 0 | ✓ check-cli-command-ids: 63
module(s) under packages/cli/src/commands examined, all of them
default-export a class whose inheritance chain reaches ocl |
| 25 | `pnpm check:closing-target-claim` | 0 | ✓
check-closing-target-claim self-test: 105 cases pass. |
| 26 | `pnpm check:commit-card-trailers` | 0 | ✓
check-commit-card-trailers self-test: 81 cases pass. |
| 27 | `pnpm check:cross-package-test-inputs` | 0 | All 255 self-test
cases passed. |
| 28 | `pnpm check:doc-authoring` | 0 | ✓ doc authoring guard:
sibling-package prose ids hold the baseline — 819 pinned site(s) across
231 file(s), 90595 string(s) read in 1247 parsed source |
| 29 | `pnpm check:driver-memory-census` | 0 |
check-driver-memory-census: OK — every declaration is ledgered, every
ledger entry is live, and every ruled file states "objectstack-ai#6664 census: 2 ruled
consume |
| 30 | `pnpm check:entry-guard` | 0 | ✓ check:entry-guard: 280 scripts/
file(s) — every entry guard goes through invoked-as.mjs; 219 export
bindings, 219 of them inert on import (0 known-u |
| 31 | `pnpm check:gitlink-declared` | 0 | ✓ check-gitlink-declared
--self-test: 36 assertions over throwaway git repos (real scan() path) |
| 32 | `pnpm check:issue-citations` | 0 | ✅ check-issue-citations
--self-test: grammar narrowed, four 404 causes kept apart, both board
strategies agree, diff scope red AND green, scope contra |
| 33 | `pnpm check:nul-bytes` | 0 | ✓ check-nul-bytes --self-test: 75
assertions over a temp git repo (real scan() path) |
| 34 | `pnpm check:parse-guard` | 0 |
packages/cli/test/published-subpath-hook-body.pin.test.ts:417
ts.createSourceFile |
| 35 | `pnpm check:partof-closing-keyword` | 0 | ✓
check-partof-closing-keyword self-test: 45 cases pass. |
| 36 | `pnpm check:pm-governed-merges` | 0 | ✓ check-governed-merges
--self-test: 441 assertions (the unified governed predicate + near
misses, subject→PR spellings, window parsing, the objectstack-ai#12633 la |
| 37 | `pnpm check:pm-half-states` | 0 | ✓ check-half-states self-test:
4912 cases pass. Batteries: H66 released queue card 182/172, H19
judged-set founding 37/34, H65 tier declaration spelli |
| 38 | `pnpm check:pm-post-stamped` | 0 | ✓ post-stamped self-test: 610
cases pass across 21 batteries — offline, no network, no token. |
| 39 | `pnpm check:pm-skill-id-lint` | 0 | ✓ check-skill-id-lint: 30
file(s) clean (pattern /#[0-9]{3,}/g). |
| 40 | `pnpm check:pm-skill-ratchet` | 0 | ✓ check-skill-line-ratchet:
declared cross-file moves: 1, total ceilings down 9 lines. |
| 41 | `pnpm check:pnpm-filter-targets` | 0 | ✓
check:pnpm-filter-targets: 152/207 `--filter` occurrence(s) across 41
file(s) resolve against 81 workspace package(s); 55 not judged (2
foreign, 30 |
| 42 | `pnpm check:ratchet-remedy-authority` | 0 | OK self-test: the
lexer holds, messages are bounded, both offer word orders and path-named
registries are reached, declaration registries are not, th |
| 43 | `pnpm check:refd-timer-probe` | 0 | ✓ check-refd-timer-probe
self-test: 11 cases pass, negative controls included. |
| 44 | `pnpm check:single-claim-paths` | 0 | ✓ check-single-claim-paths
self-test: 93 cases pass. |
| 45 | `pnpm check:skill-frame-sync` | 0 | ✓ check-skill-frame-sync: the
one declared copy of the decision frame is internally coherent
(.claude/skills/pm-dispatch/SKILL.md; no second copy to c |
| 46 | `pnpm check:watch-hint-literal` | 0 | ✓ check-watch-hint-literal:
71 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 47,
ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECL |
| 47 | `pnpm check:pm-dispatch-gates` | 0 | ✓ dispatch-gates self-test:
1905 cases pass. (738.5s on this box) |

Row 20 is exit 3 = PREREQUISITE NOT MET (the gate's own NOT-MEASURED
code, see Acceptance notes); every other row exit 0. The same 47 ran at
`bd5bbd2bb` with the same readings before the merge of `origin/main`.

## Line budget

- `.claude/agents/os-dev.md`: 402 / ceiling 402 before and after
(headroom 0, net 0 lines); `check:pm-skill-ratchet` exit 0 on the head.
Max content bytes per line 120 before and after.
- `skills/**` (the published catalog) is not touched, so no whole-file /
whole-package token readings are owed.
- `.claude/**` and `scripts/pm/**` publish nothing from any package's
`files[]` (fast lane), so `skip-changeset` applies and no changeset is
written.

## On the card

- `landing-operations.md` :27–:28 and the queue guard's SUCCESS line:
landed by PR objectstack-ai#19379 (merged `1f53b0b685`); the guard's :236 hit is a
verbatim ruling quotation and stays.
- `os-dev.md` :286–:287, H48, H43: this PR.
- REMAINDER, measured on `origin/main` at `fae870352` and not in this
PR's claimed file surface: `.claude/skills/pm-dispatch/SKILL.md:618`
(the size clause ending 「⛔ 无事实层例外」) still spells the retired word (the
skills seat's addendum 5737973707 joined it to this card), and 「规则层」
survives as a synonym for Tier H at `SKILL.md:617`,
`references/core-rules.md:122`, `references/landing-operations.md:26`
and `references/lanes/skills.md:17` (vocabulary only; each rule stays
true). That is why the first line is `Part of` rather than a closing
keyword: objectstack-ai#19146 remains open after this PR merges, with its own addendum
item still owed. The dispatch asked for a closing first line on the
premise that the two `os-dev.md` lines were the whole remainder; the
tree-wide grep says otherwise, and os-dev.md's rule (a PR whose merge
should not close the card uses `Part of`) wins.

## Acceptance notes (observations, not filed)

- `check-half-states.mjs` H48's sentence still says the handoff exists
because "a Tier H surface lands only on the maintainer's word" —
accurate for Tier H under PD objectstack-ai#14 (the maintainer's hand or an authorized
approval); the pre-existing "by hand" wording was narrowed to that in
the same edit.
- `pnpm --filter @objectstack/lint run check:doc-formula-expressions`
answers exit 3 PREREQUISITE NOT MET in this worktree (the gate needs
`@objectstack/formula` / `@objectstack/lint` built; this diff touches no
package, so no build closure is owed). Recorded as NOT MEASURED, not as
a failure; its population (docs formula expressions) is disjoint from
both changed paths.
- Landing: Tier S — draft stays draft; the owning seat renders the `##
Contract review` record for head `c9617adde` and lands it through the
queue after every check is green.

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

---------

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

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant