Repository navigation
feat(pm): second Unlock-action: value — a label-transition exit on a named card (#19255) - #19351
Conversation
…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
…lock-action-label-transition
…lock-action-label-transition
Contract reviewServed-tier: ① Derived judgments
② Semver levelNone — nothing published; ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Seat correction — this PR was dequeued in error, at 2026-09-20T14:16:15ZThe What I acted onA 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 What I did not do first, and should haveRun the register. 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:
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
And the three landing preconditions, re-measured first-hand at 2026-09-20T14:21Z rather than recalled:
Why two independent readers both called this an anomaly — a stale rule, still on the boardThe register and
Both follow #19133, ruled 2026-09-18 — 「同意改规则。」 plus the amendment that moved Two other carriers of the same rule still state the SUPERSEDED 2026-09-13 boundary:
Applied to this PR, The defect in my act, namedTwo, and only the first is the text's doing.
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 ( Generated by Claude Code |
Correction to the cost claim in
|
| 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
…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>
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 —
LABELfor the label slot,absent|presentfor 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
e3b3cddgit 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_ACTIONvariants: zero hits. Control:Blocked-byhitsscripts/pm/check-half-states.mjs299 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 resolutionsresolveBlockerTarget(:24098) already holds, andh26BlockOnIndefiniteTargetjudges 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 sameH19id, 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.absentis the live case (the ruling lands andneeds-user-decisionleaves 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 onesBlocked-by:uses.What changed —
scripts/pm/check-half-states.mjs(+87 / −8 = +79 net, budget ≤ +80)directiveValuesJSDoc key union admits'Unlock-action'(one annotation line; the reader itself is unchanged).UNLOCK_LABEL_EXIT_RE,unlockLabelExits(issue, commentBodies, ownerRepo)(body then comments, keyed byblockerTargetKeylike the card's targets) andh19DeclaredExitFired(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 thenot plannedclose (「无机制可唤醒的卡 ⛔ 不 hold」 one state over) and names the card'sUnlock-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).h19DeclaredExitFiredrides the same resolutions and the same comment fallback as H19/H26; H26 is handed the comment bodies.SELF_TEST_BATTERIESgains'H19/H26 label-transition unlock exit': 10(11 registered);SELF_TEST_BATTERY_FLOOR6 → 7, and the three existing floor pins (:29049 / :35446 / :35789) move with it.selfTest()'s tail.Prose — three carriers, each rewritten in place, net 0, all under the 120-byte cap
SKILL.md:128- \Unlock-action: re-check PR #M` 行改写完工卡的解锁动作,只认此一值,别的拼写静默回落。` (111)- \Unlock-action:` 只认 `re-check PR #M` 与 `re-check #N when label LABEL absentstate-machine.md:29- 正文加机器可读行 \Unlock-action: re-check PR #M`,把解锁出口改为重查该 PR 落地。` (102)- 正文行 \Unlock-action: re-check PR #M`(重查落地)或 `re-check #N when label LABEL absentstate-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.」node scripts/ablation-replace.mjs, wrap mode, on the committede38a8f8whose script blob is byte-identical to55d5e47's —2b93576c1e88…at both): anchorfor (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 --porcelainempty 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 one38a8f8,cad8654and55d5e47. All 48 run on55d5e47, each exit captured before any pipe, 48 × exit 0; then--ranover 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).check:doc-formula-expressions(@objectstack/formula,@objectstack/lintclosures) underbash scripts/pm/os-verify-lock.sh: 「VERDICT command-exit 0 · held the lock 215s · waited 0s」.node scripts/pm/check-governed-merges.mjs --testwith the four paths (and--branchon 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.npx eslint --no-inline-config --format json scripts/pm/check-half-states.mjs→ exit 0, 1 file, 0 errors, 0 warnings): ① population read fromeslint.config.mjsitself — root-only flat config,.mjsunderscripts/in scope; ② file count 1 from the--format jsonoutput; ③ invariance:eslint.config.mjs:328states noparserOptions.projectand no typed rules, so this diff cannot move any untouched file's verdict. The repo-widepnpm lintis CI's run.grep -naPwith the exit captured before any pipe) → exit 1, zero matches;check:nul-bytesexit 0.origin/maintwice (596090e, then13d5294); neither moved the four files (three-dot);os-regen-pendingabsent; delta vsorigin/mainis exactly the four files.Mechanism-assumption readouts
state-machine.mdneeded two in-place lines (:29 and :30 — 「只认此一值」 → 「只认此二值」, or the pair would contradict itself); still net 0 and both ≤ 120 B.9180b909, read viagit 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 separateparts.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.re-check #N when label LABEL absent|present— one value, both directions, card + label + state all named.Acceptance notes
--ranaccepts a bare command list but grades exit codes only when each line is spelledcommand :: exit N(its own text says so); the run record here uses that form. 承接者: whoever next editsdispatch-gates.mjs(finding(scripts/pm):check-half-statesreads the LOCAL checkout with nocwd, so a cross-repo sweep validates H17's trigger-file index against the wrong repo'sgit ls-filesand H57 never runs at all #19191's seat).--commandsderivation one38a8f8printed 「STALE TREE — 3 commit(s) behind origin/main, 2 file(s) it derives from CHANGED」; re-derived after each merge, list identical. 承接者:无.parts.pushlands directly under the sentence that branch rewords; keep both on merge. 承接者: 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 dev.e38a8f8,PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-half-states.mjs --self-test→ exit 1, 3 of 5092 cases fail (「H38 seat: a numbered seat on a SIBLING board is still foreign」, 「H38 verdict: ⛔ on the OLD window the post reads STALE…」, 「seat marker window: …the held own-board seat it DOES read is a subset of H44's leg」); 0 of the 3 are in this card's battery or regions, and this card's cases pass under that env because they passREPO_OSexplicitly. Not re-measured onorigin/main. Dedupe words:self-test env-dependent PM_SWEEP_REPO·H38 seat sibling board self-test·check-half-states self-test foreign repo.维护者速读(草稿)
改了什么:状态模型的
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