docs(pm): batch default back to 3 on the maintainer's ruling — ceiling 5 unchanged (#18125) - #18128
Conversation
…g 5 unchanged One line in `.claude/skills/pm-dispatch/SKILL.md` 〈入口与角色〉: the `batch:<n>` table row's default moves 2 → 3 on the maintainer's ruling 「并发2 还是太慢了,默认恢复3吧」. The maintainer ceiling 5, the write-identity locks, `LOCK_DEPTH_HOLD` and the same-file serial rule are untouched; `references/core-rules.md` states no batch default, so it has no mirror line. Equal-line and byte-neutral: 812/812 lines, 73524 bytes before and after, frame block (lines 733–754) md5 unchanged. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewHead: ① derived judgments (seat-measured on the fetched head, ⛔ not taken from the report):
② semver: unchanged — nothing published. ③ boundary flags: the Implemented-by: Verdict: PASS — awaiting an authorized approval; ⛔ never ready / enqueue / auto-merge before it; the seat lands after. 维护者速读(终稿)按你 03:45Z 的话「并发2 还是太慢了,默认恢复3吧」:SKILL.md 那张参数表里 Generated by Claude Code |
|
Landing under ruling C — Generated by Claude Code |
…s still exists (objectstack-ai#18149) Fixes objectstack-ai#17966 `os-verify-lock.sh` already knew, for its own grant logic, whether the process behind the holder record and behind every queued ticket still exists — it keys tickets on pid **plus** the `/proc` start time. Both paths that decide it (`holder_line`, `queue_live`) then **delete** the record and print nothing about it. That is correct for ordering — a dead ticket must not wedge the queue — and it is exactly why the reading a seat needs was never on the page: by the time `--status` prints, the evidence that a run died holding this lock has been reaped silently, and the surface reads `state: lock is free` / `queue: empty`, which a reader takes as *"nothing is blocked"* rather than *"the thing you think is blocked already died"*. This PR is the card's **shape 1**: a liveness reading a seat can take in one command. ## What changed `--status` now prints, after every pre-existing line: - one `instrument:` line proving the prober itself works, then - one verdict line per holder and per queued ticket — the pid, whether it exists, whether it is still the **same** process by the recorded start time, and how long ago the record was written. The snapshot of the records is taken **before** `holder_line` and `queue_live` run, because those two reap what they judge. Nothing in the new path prunes, grants or decides; it reads and it reports. **`--status` was extended rather than given a new flag, and the output contract was measured before deciding.** `git grep -n -- '--status' -- scripts .claude .github` finds two consumers outside the script: - `.claude/agents/os-dev.md:103` — prose, "prints the holder and the queue". - `.claude/skills/pm-dispatch/SKILL.md:436` — arrival depth = the count of `queue N:` lines plus 1, with `state:` holder lines and `parked` lines explicitly **not** counted. Neither pins the shape beyond those spellings, so lines may be added and none may be reshaped. A case in the suite asserts those two spellings still appear, and every new line carries a prefix (`instrument:`, `liveness `, `refusing:`) that no existing reader counts. ## The reading carries its own control A prober broken in the **alive** direction reports every corpse as running — an instrument answering confidently, wrongly, and in the reassuring direction, which is the shape this card is about. Both directions are therefore proved in the same output, against pids whose answer is known before the test runs: this process must read **alive**, and a child this call started and **reaped** must read **dead** (a pid can be reused between the reap and the check, so the dead leg retries before concluding). If either leg fails, `--status` prints **no reading at all** — not a reading with a caveat, and not merely no verdicts. Every holder, waiter and parked line it prints is decided by that same `pid_alive`, so an unlit prober invalidates the whole listing; the command refuses with **exit 3**, names the leg that failed, and prints nothing else. `OS_VERIFY_LOCK_SIM_LIVENESS=alive|dead` exists solely so the suite can drive both failure directions — the one failure this control is built to catch cannot be produced by arranging real processes, and a control nothing has ever been seen to fail is not a control. It is documented at `pid_alive`, and every invocation announces it on stderr so it cannot be set quietly. ## Acceptance **A free lock — the control line, and no verdicts because there is nothing to judge.** ⚠ The absence is printed, not left to be inferred: that is the exact shape the card measured (harness "running", lock free, queue empty, process already dead). ``` state: lock is free queue: empty (entry-point waiters only — a free-hand flock waiter takes no ticket) ledger: ... (no records yet) boots: ... (1 recorded) — `--report` reads them back with their controls instrument: lit — own pid 6537 reads ALIVE, a child this call started and reaped (pid 6542) reads DEAD, and start times are readable here, so a reused pid is told apart from the original. liveness: no records to judge — this lock has no holder and no tickets. ⚠ That is NOT evidence that whatever you are waiting on is alive. A process that never reached this lock leaves nothing here to judge, which is the exact shape objectstack-ai#17966 measured: the harness said running, this command said free and empty, and the process was already dead. Read the process table next to a pid you KNOW is alive before you conclude anything about a run that left no record here. ``` **A fixture with a live holder, a live waiter, a dead waiter, a reused pid and a parked slot** — every pre-existing line unchanged, the reading appended: ``` state: holder pid 8283, held 95s — pnpm --filter core test queue 1: pid 8283 waiting 40s — live-waiter parked 1: slot skills, place kept 300s (blocks nobody) — skills-slot ... instrument: lit — own pid 8396 reads ALIVE, a child this call started and reaped (pid 8399) reads DEAD, and start times are readable here, so a reused pid is told apart from the original. liveness holder: pid 8283 ALIVE and still the same process (start time matches the record) — holding 95s (1m35s), since 2026-09-14T06:21:48Z — pnpm --filter core test liveness waiter ticket 00000000000000000001-8283: pid 8283 ALIVE and still the same process (start time matches the record) — waiting 40s, since 2026-09-14T06:22:43Z — live-waiter liveness waiter ticket 00000000000000000002-999999: pid 999999 DOES NOT EXIST — nothing is waiting behind this ticket; the record was written 70s (1m10s) ago, at 2026-09-14T06:22:13Z, and the next queue scan prunes it, so this is the one reading you get — dead-waiter liveness waiter ticket 00000000000000000003-r8283: pid 8283 EXISTS BUT IS NOT THE RECORDED PROCESS (start time 472121 recorded, 472114 now) — nothing is waiting behind this ticket and its pid has been reused; the next queue scan prunes it, so this is the one reading you get — reused-waiter liveness parked 00000000000000000004-sskills: pid 0 — BY DESIGN no process to judge; a place kept, not a waiter, taken 300s (5m00s) ago, at 2026-09-14T06:18:23Z — skills-slot ``` **Byte-identity of the pre-existing lines.** The pre-change script (`git show origin/main:scripts/pm/os-verify-lock.sh`) and this one were run against the **same** fixture state, both outputs truncated at the new block (`sed '/^instrument: /,$d'`), and diffed: - free lock, empty queue: `diff` empty, **exit 0**, no normalisation at all. - the rich fixture above: `diff` empty, **exit 0**, with one declared normalisation — `held|waiting|kept N s` durations, which differ between two runs taken seconds apart by construction and are not affected by this diff. **`git diff --stat` against the merge base names only the script** (its self-test lives in the same file): ``` scripts/pm/os-verify-lock.sh | 341 +++++++++++++++++++++++++++++++++- 1 file changed, 338 insertions(+), 3 deletions(-) ``` ## Tests `bash scripts/pm/os-verify-lock.sh --self-test` — **exit 0, 243 cases pass, 0 fail** (the CI step `Verify-lock entry-point self-test` in `lint.yml` runs exactly this command). 14 new cases, written in the suite's existing shape and driving `--status` as a **child process** against a private lock of their own, because what is owed is the surface a seat runs — cases that called the helpers in-process would pin the predicates and leave the output unpinned, which is the state this card is about. ``` ✓ a lit liveness reading exits 0 ✓ and says it is lit, on its own line, before any verdict ✓ a waiter whose process is really there reads ALIVE ✓ a holder whose process is gone reads DOES NOT EXIST — the one-line reading that was owed ✓ and a live pid that is NOT the recorded process is neither alive nor gone ✓ and the lines existing readers count are untouched ✓ a prober that reads every pid ALIVE refuses instead of reporting ✓ and names its instrument as not lit ✓ and prints no verdict at all — not one carrying a caveat ✓ and withholds the holder, queue and parked lines that same probe decides ✓ and the other direction — every pid reading DEAD — refuses too ✓ with nothing recorded it says there is nothing to judge ✓ and refuses to let that absence read as an all-clear ✓ and prints no verdict line, because there is nothing to judge ``` ### Ablation — both new claims proved able to fail Run from the committed fix, each mutation proved on disk (injected-marker count plus a `git hash-object` blob change against the `HEAD` blob), each restore leg `git checkout HEAD -- scripts/pm/os-verify-lock.sh` proved by an empty `git diff HEAD`, under a `trap ... EXIT INT TERM` with absolute paths. Both legs restored clean; the working tree is at `HEAD`. **A — the snapshot is taken before the pruning paths.** Moved `liveness_snapshot` to the foot of `mode_status`. Suite exit 1, **2 cases red**, exactly the ones about records that are reaped as they are judged: ``` ✗ a holder whose process is gone reads DOES NOT EXIST — the one-line reading that was owed want: 1 / got: 0 ✗ and a live pid that is NOT the recorded process is neither alive nor gone want: 1 / got: 0 ``` **B — a failed control refuses.** Made the dead leg's failure return success instead. Suite exit 1, **3 cases red**: ``` ✗ a prober that reads every pid ALIVE refuses instead of reporting want: 3 / got: 0 ✗ and prints no verdict at all — not one carrying a caveat want: 0 / got: 1 ✗ and withholds the holder, queue and parked lines that same probe decides want: 0 / got: 2 ``` ⚠ Worth recording from B: `and names its instrument as not lit` stayed **green** under the mutation — the `instrument: NOT LIT` line was still printed, beside a full set of verdicts. The line is not the mechanism; the refusal is. A suite that asserted only the wording would have passed the broken build. ### Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) derived **26** families. All 26 run in the foreground, each exit code captured before any pipe, all **exit 0**; reconciled with `--ran`: ``` ✓ dispatch-gates --ran: 26 derived famil(ies) accounted for — 26 run, 0 NOT-MEASURED (a DERIVED zero — all 26 recorded an exit code and none of them is 3). ``` The list was re-derived after a fresh `git fetch origin main` (base moved `a90a9f267` to `739ab526d`; no upstream commit touched this file) and came back identical. Eight artifact-roster families whose roster sits in a directory this path is in were also run. Six exit 0: `check-published-list-mirrors`, `check-published-list-mirrors --self-test`, `check:console-injection`, `check:dts-closure` excepted, `check:engine-double-contract`, `check:i18n-stale-fill`, `check:pm-label-desc-cap`. ⛔ **NOT MEASURED:** `check:dts-closure` and `check:published-readme-exports` both exit **3** (PREREQUISITE NOT MET — they need a full `pnpm build`'s dist closure). That is not a pass and not a finding. Neither gate reads a `.sh` file at all and neither roster names this script, so nothing in this diff can move them; they are left to CI, which builds. ## Deliberately not in this PR The card's **shapes 2 and 3** — the rule line in `SKILL.md` and dispatch briefs, and a seat-side sweep over in-flight dispatches — are a later PR. `SKILL.md` is serial behind PR objectstack-ai#18128, and it is a different layer (rules prose, four-piece) from this one (a script with its own self-test). Splitting them keeps this one landable now and keeps the prose half from queueing behind a script review. The lock's mutual-exclusion semantics, ticket ordering, ledger format and boots format are untouched: this is a **reading** only. ## Acceptance notes Observed while working here, out of scope, **not filed** (each would need a carrier that will actually touch the file): - `noted, not filed:` the CI step comment in `.github/workflows/lint.yml` describing this suite says "48 cases"; the suite ran 228 before this PR and 243 after. Pre-existing prose drift, no mechanism reads the number. Carrier: the next PR that edits the `Verify-lock entry-point self-test` step. - `noted, not filed:` the reaping that hid this reading is also invisible to the waiter's progress output and to `--report` — only `--status` gained the disclosure. Carrier: none identified; the seat-facing surface is `--status`, which is where the card asked for it. - `noted, not filed:` `skip-changeset` applies — `scripts/pm/**` publishes nothing (no package `files[]` ships it, it is repo tooling). The label is applied on this PR. _Generated by [Claude Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_ --- _Generated by [Claude Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #18125
Ruling
The maintainer, in the skills seat's chat at 2026-09-14T03:45Z, verbatim and untranslated: 「并发2 还是太慢了,默认恢复3吧」. It supersedes lock 3's default (PR #18072, commit
7ef05f997, 「batch默认 2」). The maintainer ceiling 5 stays. Ruling C (#17971, 「C. approve 后不管后续改动都由席位落地:」) governs the landing: this PR stays a DRAFT; the seat does the four-piece; nothing here is readied, queued, armed or approved by the dev.Change — one line
.claude/skills/pm-dispatch/SKILL.mdline 60, thebatch:Nrow of the 〈入口与角色〉 argument table:| batch:N | 同时在飞的 dev 上限 | 默认 2;N 的维护者天花板 5 || batch:N | 同时在飞的 dev 上限 | 默认 3;N 的维护者天花板 5 |(The row is quoted with
Nstanding in for the angle-bracket placeholder the file uses, so the body survives the sanitizer; the file itself is unchanged in that respect.)Equal-line and byte-neutral: 812 lines before and after (ceiling 812,
scripts/pm/check-skill-line-ratchet.mjs:320), 73524 bytes before and after, one insertion / one deletion ingit diff --stat ca7886047 06c10152b. Nothing else moves: write-identity locks 1, 2, 4, 5,LOCK_DEPTH_HOLD, the same-file serial rule and the ceiling 5 are untouched. The seats' Routine prompts are the seats' own to update on landing (card body).Premise readings (all against
origin/main=ca7886047, worktree created 2026-09-14T03:50:37Z)2;git grep -nfor the literal 默认 followed by a backticked 2, over.claude AGENTS.md CLAUDE.md, returned exactly one hit,.claude/skills/pm-dispatch/SKILL.md:60.references/core-rules.mdis 151 lines and states no batch DEFAULT.grep -n batchhits: line 11 「并行度以batch封顶,验证锁到达深度 ≥LOCK_DEPTH_HOLD(2)即等;同批按构造文件面不相交。」 (the verify-lock depth, a different number) and line 37 「插队标签可超batch立即派发,⛔ 不豁免同文件串行、深度等待与认领协议。」.grep -n 默认hits lines 7, 17, 18, 52, 77, 139, 140 — none is aboutbatch. So there is no mirror line, and core-rules.md is untouched (151/151); the card body's "core-rules mirror line likewise" is superseded by the claim comment's reading, which this grep confirms.sed -n '733,754p' SKILL.md | md5sum=3327d02c56f8a0eca88569dad2270f32. Same two readings on HEAD06c10152bafter the edit: 812 lines, md53327d02c56f8a0eca88569dad2270f32.git ls-remote --heads originlisted 1106 heads; the 6claude/issue-18xxxheads were compared tomainthrough RESTGET .../compare/main...BRANCHand all 9 open PRs through RESTGET .../pulls/N/files(page count 9, fewer than 100, so the listing is complete): zero hits onpm-dispatch/SKILL.mdorcore-rules.mdin any of them. Scope declared: open PRs plus this wave'sissue-18xxxheads; the 62 older keyword-matched heads (seat session branchespm-dispatch-*, old issue branches) were listed but not compared.Gates (run on the working tree at HEAD
06c10152b, 2026-09-14T03:55Z–04:01Z; every exit captured by redirect-then-$?)Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths; change set from the merge baseca7886047; the--repoassertion held): 16 commands — 9 matched by path, 7 whole-tree. All 16 exit 0:pnpm check:pm-skill-ratchet— exit 0 (ratchet green; widest-table-row pins and ceilings unchanged)pnpm check:skill-frame-sync— exit 0 (「the one declared copy of the decision frame is internally coherent … 74 markdown files scanned for undeclared copies」)pnpm check:pm-governed-prose— exit 0 (「2 instruction surface(s) name all 5 registered governed surfaces … and claim no others」)pnpm check:pm-skill-id-lint— exit 0 (27 files clean)pnpm check:pm-governed-merges— exit 0pnpm check:nul-bytes— exit 0 (8643 text files, no raw control bytes)node scripts/pm/check-governed-queue-guard.mjs --self-test— exit 0 (233 cases)pnpm check:agent-test-spelling— exit 0node scripts/check-closing-keyword-parity.mjsand its--self-test— exit 0 / 0node scripts/check-comment-mask-corpus.mjs— exit 0 (6747 files, 0 disagree)pnpm --filter @objectstack/lint run check:doc-formula-expressions— first run exit 3 「PREREQUISITE NOT MET —@objectstack/formulais not built」 (not a measurement); afterpnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lintunderos-verify-lock.sh(「VERDICT command-exit 0 · held the lock 167s」), rerun exit 0 (「22 record-scoped formula example(s) across 438 files … judged clean」)pnpm check:doc-authoring,pnpm check:driver-memory-census,pnpm check:refd-timer-probe,pnpm check:watch-hint-literal— exit 0 eachReconciliation (
--ranwithCOMMAND :: exit CODElines, 2026-09-14T04:01:47Z): 「✓ dispatch-gates --ran: 16 derived famil(ies) accounted for — 16 run, 0 NOT-MEASURED (a DERIVED zero — all 16 recorded an exit code and none of them is 3).」Tied family not on the derived list, run anyway:
node scripts/check-skill-frame-freshness.mjs(--self-testexit 0, scan exit 0: 「the decision frame in this tree is current with origin/main」). CI-measured only, not runnable here:check-governed-queue-guard.mjson the event payload.Lint, narrowed and declared:
eslint --no-inline-config --format json .claude/skills/pm-dispatch/SKILL.md— exit 0, 1 file, 0 errors, 1 warning 「File ignored because no matching configuration was supplied」. Population read fromeslint.config.mjs: everyfiles:block matches only**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}(six blocks;COMMENT_SWALLOW_FILESis the same glob), so a.mdfile is outside the linted population. Invariance: the config never enables type-aware linting (its own line 328: noparserOptions.project, no typed rules), so a one-line edit to an unlinted file cannot move the verdict of any linted file; the repo-widepnpm lintis CI's run.dispatch-gates --tierwas read for the claim; its model line is not reproduced here (model-free rule).Changeset
None owed. The
Check Changesetjob (.github/workflows/pr-automation.yml, jobchangeset-check) reads exactly two exemptions — theskip-changesetlabel and the changesets release PR pinned by branch and author — and no path exemption, so the label is the declaration. The diff publishes nothing:.claude/**is in no released package'sfiles[](fast-lane class per the dev definition). The label is applied with the additivePOST .../issues/N/labelsand read back; the read-back is recorded in the report comment on #18125.维护者速读(草稿)
改了什么:PM 派发技能
SKILL.md的batch参数默认值从 2 改回 3;上限 5 不变;只此一行。为什么改:维护者裁决「并发2 还是太慢了,默认恢复3吧」;lock 3 落地的默认 2 由此被取代。
core-rules.md本就没写默认值,故无需同改。风险与代价(含回滚):并发默认回到 3 意味着同一时刻多一个 dev 在飞,共享容器的验证锁排队会略长;其余四把写身份锁、验证锁深度等待、同文件串行规则均不动。回滚 = 把该行的
3改回2,一行、零副作用。席位意见:(留空,席位定稿成评论)
你要做的:确认后由授权账号 approve,席位按裁决 C 落地;各席位自行更新自己的 Routine 提示词。
Acceptance notes
check:doc-formula-expressionsrun exited 3 on a fresh worktree because@objectstack/formulaand@objectstack/lintare not built bypnpm install; the gate's own text says so and prescribes the build. Behaviour by design (Absence must be loud), not a defect. 承接者:无.Generated by Claude Code