Skip to content

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

Draft
os-steve wants to merge 4 commits into
mainfrom
claude/issue-19255-unlock-action-label-transition
Draft

os-steve wants to merge 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 形状、只认一种拼写,别的拼写静默回落——最坏的失败形态,因为那行读起来像做了事。实测 e3b3cddUnlock-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:128state-machine.md:29check-half-states.mjsUNLOCK_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

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

1 participant