Skip to content

test(spec): order the sdui cleanup fd count on the child's own post-exec report - #18530

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-18010-sdui-manifest-lockfd-accounting
Sep 17, 2026
Merged

os-litant merged 1 commit into
mainfrom
claude/issue-18010-sdui-manifest-lockfd-accounting

Conversation

@os-warren

@os-warren os-warren commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #18010

Clause-②: no — the diff is one .test.ts under packages/spec/scripts/, which matches none of the package's files[] entries.

The mechanism, named from the tree

The card's hypothesis was that the count lags the child's exit (an fd inherited by a grandchild, or a lock fd closed asynchronously). Both halves are falsified by the tree, and the real cause is one line earlier:

  • The count is not taken after the child exits. It is deliberately taken while the session leader is alive (the comment at the old :98-102), and the failing CI run confirms it: LEADER_ALIVE_AT_COUNT=yes.
  • No grandchild can hold the fd. Measured here: a node process holding an inherited non-close-on-exec fd reports PARENT_SELF_LOCKFDS=1, and the child it spawns reports HELPER_LOCKFDS=0. libuv closes it across the spawn, so the helper is not a candidate.

What is left is the only candidate, and it is the one the test synchronises on:

sdui_spawn_detached (scripts/gen-sdui-manifest.sh:88) returns as soon as the pidfile is non-empty (:113-115). Its runner writes that pid on its first line (:96), before the for fd in /proc/$$/fd/* loop that closes the descriptors inherited from the caller (:98-105), and only then execs. So between those two steps the session leader is a bash process that is published as ready and still holds the caller's flock descriptor. That is precisely what Test Core (1/6) counted.

Measured on this tree, 25 runs, timestamps taken with bash's EPOCHREALTIME inside an instrumented copy of the script:

reading value
window, pid published to descriptors closed 151–297 us (median 174)
margin, descriptors closed minus count taken 10.4–54.3 ms, all 25 negative (the close won)
what buys that margin sdui_spawn_detached's own sleep 0.05 poll granularity (:117) — not a barrier

So the assertion was ordered on a signal that means "the leader exists", never "the leader let go of my descriptors", and a 0.2 ms critical section was being covered by an unrelated 50 ms sleep. On a six-way sharded runner a single bash process being descheduled for longer than that is ordinary. The assertion was racing something real; it was not wrong about the property.

What changed

The reading is now ordered on the resource, not on the pidfile:

  1. Barrier 1 — the spawned command's own post-exec report. The stub counts its own descriptors on the caller's lock and prints them. exec has by then replaced the shell that does the closing, so that line cannot exist until the close has run. The harness waits for the report, never for a value: the stub prints whatever it counts, so a real leak still arrives as a red (proved below).
  2. Barrier 2 — the helper has joined the session, so the session-wide count covers both members rather than whoever happened to exist.
  3. The session-wide LOCKFDS count is unchanged in meaning and now runs after both barriers, and names its holders (LOCKFD_HOLDERS) so a future red is a process rather than a number.
  4. The stub is released by a file instead of a 1200 ms timer, so LEADER_ALIVE_AT_COUNT stops racing node's boot time — the added barriers would otherwise have eaten into that budget.
  5. The orphan precondition waits for a member with PPID 1 instead of sleeping a guessed two seconds. A timed-out barrier leaves the field empty, which the existing assertion reads as a red.

Both new barriers are asserted (CHILD_REPORTED, HELPER_IN_SESSION), so a barrier that times out is its own named failure instead of a count taken at some other moment.

⛔ Nothing was skipped, quarantined, loosened to toBeLessThan, retry-wrapped, or given a longer timeout. The vitest timeout is still 120_000.

Ablations — one-time, on the real script, restored and proved byte-identical

Both legs mutate scripts/gen-sdui-manifest.sh in this worktree only, under trap ... EXIT INT TERM, restore with git checkout HEAD -- ..., and verify git hash-object against the HEAD blob.

(1) The lagging state — the OLD assertion fails on it, the NEW one passes. A sleep 0.5 injected between the pid publish and the fd-close loop, i.e. the deschedule made deterministic:

leg result
NEW test under the lag exit 0, Tests 1 passed (1)
OLD test (BASE 2882528786 content) under the same lag exit 1 — LOCKFDS=2, LEADER_ALIVE_AT_COUNT=yes, expected '2' to be '0'

The CI red read '1'; this reads '2' because the injected sleep is itself a session member that inherited the same descriptor — an artefact of the injection, not of the script.

(2) Non-vacuity — the NEW assertion still fails when the thing it pins is deleted. The fd hygiene disabled (if [ -d "/proc/$$/fd" ]; then to if false; then):

ABL_2_EXIT=1
CHILD_REPORTED=yes
CHILD_LOCKFDS=1
HELPER_IN_SESSION=yes
LOCKFDS=1
LOCKFD_HOLDERS=[ 28876(node)]
: expected '1' to be '0'

The holder is the exec'd node leader — the process that can actually survive cleanup, which is what property (2) exists to pin. Both barriers reported yes, so the red is the property, not a barrier artefact.

Restore proof for both legs: RESTORED rel=scripts/gen-sdui-manifest.sh blob=5233551907c06460756c91d061e2078276e42ca1 equal to HEAD:scripts/gen-sdui-manifest.sh, and RESTORE_DIFF_HEAD=[].

Verification

Exit codes captured by redirect-then-$?, never through a pipe.

  • Subject test, clean tree: 5 consecutive runs, all exit 0, 1.71–2.91 s (was 4.47 s on BASE — the blind sleep 2 is gone).
  • pnpm --filter @objectstack/spec test:repo — exit 0, 31 files / 526 tests. This is the project and the shard that failed.
  • pnpm --filter @objectstack/spec test (local project) — exit 0, 482 files / 13771 tests, 1 skipped.
  • pnpm --filter @objectstack/spec typecheck — exit 0; check:test-typecheck confirms the test layer compiles under packages/spec/tsconfig.test.json, so this file is in a tsc program.
  • Gate family from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 58 derived, 58 accounted, 0 UNRUN — 53 exit 0, 5 exit 3. The five are check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content, check:type-check-debt: all refuse for a missing built dist/ in this worktree. PREREQUISITE NOT MET — not measured, neither a pass nor a failure. They sweep published build output, which this diff cannot reach; CI's Build Core job supplies it.

Verify-lock confounder, ruled out by measurement rather than assertion. This test counts flock descriptors, so a lock-holding wrapper around it is a confounder. Running the probe inside scripts/pm/os-verify-lock.sh: SESSION_VERIFYLOCK_FDS=0 and CALLER_VERIFYLOCK_FDS=0 — the shared lock's descriptor is not in the wrapped tree at all. Lit control on the same grep: a process deliberately holding /tmp/os-heavy-verify.lock reads 1, so those zeros are facts, not a broken probe. The count itself greps the test's own caller.lock basename, which the shared lock's name cannot match; dark control LOCKFDS_DARK=0, lit control CALLER_LOCKFDS_LIT=1.

skip-changeset

Measured, not assumed: npm pack --dry-run --json in packages/spec lists 271 entries, 0 of them under scripts/, and the subject file is not among them. Positive control: README.md, llms.txt and package.json are in that listing, so it is a real reading. Nothing published moves.

Acceptance notes

Out of scope for this PR, noted and not filed:

  • sdui_spawn_detached publishes the leader pid before it closes the caller's descriptors, so the pidfile means less than its own doc comment claims ("no inherited descriptors above stderr"). It is not a production defect — the pre-exec shell always proceeds to the close, and nothing in the script reads the leader's descriptors — so the honest fix is the measurement point, not the script. Recorded because the next author to treat that pidfile as a readiness barrier will hit the same thing. Carrier: any future change to sdui_spawn_detached.
  • The old orphan scan (for p in $MEMBERS; do [ ... ] && ORPHAN="$p"; done) relies on set -e's &&-list exemption and would abort the harness if a non-orphan member were scanned last. It only ever passed because the session holds exactly one member and that member is the orphan. Rewritten as an if here as part of the barrier change. Carrier: this file.

Generated by Claude Code


Landing note (seat, 2026-09-17)

Contract review at CONTRACT_REVIEW_TIER on head 65c97a0760: PASS — record is comment 5706854237 on this PR.

⚠️ Correction to this body, appended rather than rewritten. The body attributes this file's tsc coverage to check:test-typecheck under tsconfig.test.json. That program's include stops at src/**/*, so it does not reach packages/spec/scripts/. The file is compiled by check:scripts-typecheck under tsconfig.scripts.json (include: scripts/**/*), which typecheck also runs and which is green on this head. The conclusion holds — the file IS in a tsc program — but the attribution named the wrong one. Corrected here because this repo squashes and the body becomes the permanent commit message on main.

What the review checked that the seat specifically asked it to be sceptical about: whether the reordering makes the assertion deterministic or merely makes the flake rarer. It concluded deterministic by program order, not by timing: the count cannot begin until EXECED=1 is visible, that line is written by node, node exists only after exec at script :104, and the fd-close loop at :96–103 precedes that exec in the same process. A barrier that never arrives reds by name (CHILD_REPORTED=no) — it never passes. All eight original assertions survive with identical matchers; no retry, no loosened matcher, no lengthened sleep, no skip.

Pre-landing checks: ① review PASS on record ✅ · ② check-clause2-carriers --pair 18530 exit 0; ⛔ no carriers were hung on this pair (Clause-②: no, no contract surface) so there is nothing to strip ✅ · ③ re-taken at landing time, latest-run-per-check-name ✅. Governed-surface predicate: 0 of 1 path hits the register ⇒ ordinary queue landing.


Generated by Claude Code

…xec report

`sdui_spawn_detached` returns as soon as the pidfile is non-empty, and its
runner writes that pid on its FIRST line — before the loop that closes the
descriptors inherited from the caller. Measured on this tree: the pid is
published 151–297us before the close completes, and the only thing keeping a
pidfile-ordered count green is the caller's unrelated 50ms polling sleep
(margin 10–54ms against a 0.2ms critical section). So the count was ordered on
a barrier that means "the leader exists", not "the leader let go of my
descriptors", and it could legitimately observe the leader still holding the
caller's lock fd — which is what `Test Core (1/6)` saw once as LOCKFDS=1 with
LEADER_ALIVE_AT_COUNT=yes.

The stub now takes the reading of itself after `exec` has replaced that runner
and publishes it, and the harness orders the session-wide count on that report
and on the helper having joined the session. Both barriers wait for the reading
to become takeable, never for a value; a timed-out barrier is its own named
red. The stub is released by a file instead of a 1200ms timer, so
LEADER_ALIVE_AT_COUNT stops racing node's boot time, and the orphan
precondition waits for PPID 1 instead of sleeping a guessed two seconds.

No assertion was skipped, loosened, retried or given a longer timeout.

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
@os-warren os-warren added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 16, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1e496f9796126b0398c026c96fb8d240f1982e91 → packageMentionDocs.

Copy link
Copy Markdown
Collaborator

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 65c97a07606986f3565a175afad63af12d53ec92

① Derived judgments

  • Accept set: unchanged. Three-dot diff origin/main...65c97a07 touches one file, packages/spec/scripts/gen-sdui-manifest-cleanup.test.ts (+98/−18). The same diff restricted to packages/spec/src, json-schema, api-surface, liveness, prompts, spec-changes.json, package.json, CHANGELOG.md and .changeset is empty; the control on packages/spec/scripts returns the one file. No Zod schema, export, JSON schema, error code or ledger moved; 0 files added.
  • Public surface: unchanged. @objectstack/spec 17.4.0 files[] has no scripts/ entry and its only glob is src/**/*.zod.ts; tsup entries are ten src/*/index.ts; the build tsconfig excludes **/*.test.ts. The file can reach neither dist nor the tarball.
  • What the reordering asserts. Six fields now gate the count: CHILD_REPORTED=yes (the exec'd stub's EXECED=1 line appeared in the runner's stdout, polled up to 30s); CHILD_LOCKFDS=0 (the exec'd leader's own /proc/self/fd holds no link to the lock's realpath); HELPER_IN_SESSION=yes (helper pid in pgrep -s LEADER, up to 10s); LOCKFDS=0 plus LOCKFD_HOLDERS (session-wide count taken after both barriers, holders named by pid and comm); LEADER_ALIVE_AT_COUNT=yes (leader now exits on a release file, not a 1200ms timer); ORPHAN_BEFORE numeric (polled up to 20s for a PPID-1 member instead of sleep 2). All eight original assertions survive with identical matchers (8 → 11); the two 120s timeouts, the skipIf predicate and the survivor/flock-free checks are byte-identical. No retry, no toBeLessThan, no lengthened sleep, no skip.
  • Is it deterministic — the mechanism. Old order: pidfile non-empty → count. The runner publishes its pid at script :95, closes inherited fds at :96–103, execs at :104, so the caller could legally observe a pre-exec bash still holding the caller's flock fd; the only margin was the unrelated 50ms poll granularity at :115. New order: count only after EXECED=1 is in the runner's stdout. That line is written by node, which exists only after exec "$@" at :104, which is program-ordered after the close loop at :96–103 in the same process. "Report visible" therefore implies "close loop complete" by program order, not by timing. At that point the session has exactly two possible members — the exec'd leader and a helper forked from it after the close — so any member holding the lock fd means the hygiene failed to close it, which is precisely property (2). A barrier that never arrives reds by name (CHILD_REPORTED=no), it never passes. This is a corrected measurement point, not a window made wider; the flake's mechanism (counting during the pid-publish → close window) is structurally excluded, not made rarer.
  • Execution evidence. Not runnable here (no node_modules). On the head, Test Core (1/6) job 104975967974 is success and its @objectstack/spec:test:repo task reports 31 files / 526 tests passed with no skipped count; the file is one of the 31, so the case executed and passed on the very shard that flaked. One green is one datapoint; the determinism claim rests on the program-order argument above. The dev's ablations (old test reds under an injected 0.5s lag, new test reds with the hygiene disabled naming the exec'd node as holder) are unverified, source: PR body.

② Semver level

skip-changeset is right and Clause-②: no is right, from measurement: files[] of the only package touched ships nothing under scripts/; no tsup entry or build tsconfig path reaches the file; the diff adds no .changeset/*.md; the version is untouched at 17.4.0; the Check Changeset check run is success on the head with the label present; os-dev.md:296's criterion (nothing in a package's shipped files[] moved) is met. No path under packages/spec/src/**, no *.zod.ts, no error-code ledger, so neither clause-② limb is hit; Governed Surface Queue Guard is green and packages/spec/scripts/** is not in the governed register. Not re-run by me: npm pack --dry-run (the dev's 271-entry / 0-under-scripts reading is unverified, source: PR body).

③ Boundary flags

  1. Open question A/B/C (reorder sdui_spawn_detached so the pidfile is a real fd barrier): answer A, as delivered. Measured consumers of the pidfile on origin/main are the script's own sdui_wait_for_own_server (a liveness/port probe that never reads the leader's descriptors), the collision test (liveness only), and this test — the only consumer that ever counted fds, now ordered on the resource. B would widen the "empty pidfile" window while sdui_stop_detached returns 0 on an empty pidfile (:126), trading a silent reapability hole for a doc-comment nicety; C adds a lifecycle file to a helper whose design note is that silent cleanup failure is the defect. Residual debt: the comment at script :85–86 ("no inherited descriptors above stderr") promises slightly more than the pidfile guarantees. Out of this PR's file surface; needs no card unless a second fd-sensitive consumer appears. Not escalated.
  2. Old orphan scan (&&-list under set -e) rewritten as an if at head :183 — verified in the diff, bounded to this file, same gate family. Not a widening.
  3. Can the original flake still occur? In this file, no: the count cannot begin before the close loop has completed. Elsewhere, no: the fd-counting spelling (LOCKFDS|caller.lock|/proc/$p/fd) exists in no other test on origin/main. What remains is bounded waits on state (30s/10s/20s) that red by name on a pathologically slow runner, never pass; and the pre-existing PPID-1 orphan predicate, which a subreaper environment would have reddened on the old test identically.
  4. PR-body narration error, non-blocking. The body attributes the file's tsc coverage to check:test-typecheck under tsconfig.test.json; that program's include stops at src/**/*. The file is compiled by check:scripts-typecheck under tsconfig.scripts.json (include: scripts/**/*), which typecheck also runs and which is green on the head. The conclusion (the file is in a tsc program) holds; the attribution does not. No code change; a one-line body correction is optional.
  5. The "no grandchild can hold the fd" premise (libuv closing across spawn) is unverified by me and immaterial to the verdict: a helper that did inherit the fd would raise LOCKFDS, so the premise can only make the assertion stricter, never greener.
  6. Charter hold charter conflict: does a Clause-②: no PR that touches no contract surface still owe an in-seat review before it can land? #18536 (whether a Clause-②: no PR owes an in-seat review at all) is procedural, has no comments and no maintainer ruling; it must escalate to the maintainer and is not answered here. This record is the at-tier review the seat said it was holding for under the stricter reading.
  7. Minor, no hole: the HELPER_IN_SESSION case pattern would match an empty $HELPER against trailing whitespace, but HELPER is empty only when CHILD_REPORTED=no, which is asserted first, and a failed spawn yields HELPER=undefined, which cannot match a pid.

Implemented-by: claude/issue-18010-sdui-manifest-lockfd-accounting
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 17, 2026 01:06
@os-litant
os-litant added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 3ba5648 Sep 17, 2026
48 checks passed
@os-litant
os-litant deleted the claude/issue-18010-sdui-manifest-lockfd-accounting branch September 17, 2026 07:39
os-elon-musk pushed a commit that referenced this pull request Sep 18, 2026
`needsRecordRead` — C6's population gate, and the sweep's read budget — owed
a record only in the completed state (a cleared `Clause-②: yes`). Under the
lane rule the spec and skills lanes owe the contract review on EVERY round
they deliver, `no` included, and a `no` round hangs no carrier to mark its
review pending: PRs #18530 / #18529 (spec-lane `no` rounds) read 0 here with
no record on either head. The population now adds the `no` rounds of the two
lanes, read off the card's `domain:*` labels (`LANES_OWING_REVIEW`,
`laneOwesReview`; unreadable labels stay UNJUDGED). A `no` elsewhere still
owes nothing. A cleared `yes` outside the two lanes keeps its row and its
exit — the `yes` is a limb hit and limb-hit work is owed — but its remedy is
lane routing (re-lane to spec, or correct a false `yes`), never a
default-tier self-review and never an at-tier subagent from that lane. The
C6-RECORD note prescribes the clear-citation only where a clear exists. New
floored battery, 34 cases; roster floor 32 → 33.

Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…and skills owe it on every round, other lanes owe none (objectstack-ai#18903)

Fixes objectstack-ai#18536

Clause-②: no

## What this lands

The maintainer's lane rule, restated by the director record on the card
(comment 5717169811), quoted verbatim and untranslated:

> 「曾经要求只有 spec 和 skills 需要 fable,其他 opus 就够了,理论上其他车道不需要契约复审」「即使项目经理跑在
opus 上,也可以支持起 fable 子 agent 复审,这样最快。除非子 agent 用 fable 启动失败,也就是没有 fable」

carried into the governed text and the two machine readers:

1. **Spec and skills lanes** — every delivered round gets the contract
review at `CONTRACT_REVIEW_TIER`: in-seat when the seat's served tier is
that tier, otherwise by the at-tier review subagent the seat spawns (the
09-17 route stays). The record is the same-shape comment with
`Served-tier:`, on the PR or the card.
2. **Every other lane** — no contract review. The clause-② limbs (path
`packages/spec/src/**`, declaration `Clause-②: yes`) survive unchanged
and now answer the LANE question: a limb hit is spec-lane work and moves
there; a `Clause-②: no` PR outside the contract surface lands on the
three pre-checks and the gates. No default-tier self-review record is
demanded, and no other lane spawns the at-tier subagent.
3. **Tier unavailable** (the at-tier subagent cannot start) — the review
cannot be produced: the PR stays draft and out of the queue, and the
maintainer's own review is the only bypass, by their word each time.

The lane key is the 2026-09-10 key restored (PR objectstack-ai#17294) with the
2026-09-17 subagent route kept (PR objectstack-ai#18511); the 2026-09-16 tier key (PR
objectstack-ai#18363) is superseded. Not a byte-revert: both spec AND skills owe the
review, and 「余席默认档自审加门禁」 is gone — other lanes owe nothing.

## Charter (commit 1) — net 0 lines per file

| file | before → after | ratchet ceiling |
|:--|:--|:--|
| `.claude/skills/pm-dispatch/SKILL.md` | 812 → 812 | 812 |
| `references/contract-review.md` | 60 → 60 | 60 |
| `references/core-rules.md` | 151 → 151 | 151 |

Every edited line is at most 120 bytes; `check:pm-skill-ratchet` is
green on `bc0c2ec41`. Rule text carries no issue numbers
(`check:pm-skill-id-lint` green); the four-axis frame block is untouched
(`check:skill-frame-sync` / `-freshness` green).

**SKILL.md** — five in-place rewrites, one added line, one retired line:
- :512 强制条款② — 「达档复核归派发席席内」 → 「命中即 spec 车道的活」.
- :522 席位档策略 — 「按实测档:达档席内审;未达档席 ⛔ 不自审,起隔离达档子代理转录核档采信」 → 「按车道:spec 与
skills 席达档席内审;未达档 ⛔ 不自审,起隔离达档子代理」 (the transcript-verified tier of the
subagent stays stated in contract-review.md :54).
- :636 入队闸门 — 「无席内条款②复核 PASS 在案」 → 「无达档条款②复核 PASS 在案」; the limbs at :637
/ :638 are untouched.
- :639 交付后复核 — 「归派发席:达档席内审,未达档循保险丝起子代理;记录 = 同形评论落 PR 或卡」 → 「只 spec 与
skills 车道欠,每轮达档:席内审或起子代理;双肢命中即 spec 车道」 (the record's shape lives in
contract-review.md :27–:29).
- :640 ADDED (rule 3, where the enqueue gate lives) — 「子代理起不来 ⇒ 复核缺席,PR
留 draft 队列外等档;唯一旁路是维护者亲审,逐次为准。」
- :646 pointer — 「席内复核的适用面」 → 「契约复核的适用面」.
- RETIRED, paying for :640 — the former :672 「报告席记条款②默认档 FAIL
率入复审清单;超改制前达档史值 ⇒ 决策卡交维护者定回退。」 Reason: it meters the FAIL rate of
DEFAULT-TIER clause-② reviews, and under rules 1–2 no lane performs one
(spec and skills review at tier; other lanes review nothing), so the
metric's population is empty. It sits outside the claim's declared
SKILL.md bands; it is the one tier-keyed line in the file the ruling
empties, and paying in-file required it. Re-wrap is not currency: no
line was re-flowed.

**contract-review.md** — thirteen in-place rewrites:
- Title and :23 — 「(席内)」 → 「(按车道)」.
- :9, :11, :12, :14 — 「席内契约复核 / 席内复核」 → 「达档契约复核 / 达档复核」: the review is
at tier wherever it happens, and "in-seat" was the 09-16 key.
- :25 — 「交付后收集复核当轮席内完成;借复核不移卡,新 spec 工作恒归 spec 席」 → 「交付后收集复核当轮完成;只 spec
与 skills 车道欠,新 spec 工作恒归 spec 席」. The "borrow the isolated review" route
is what rule 2 closes for other lanes.
- :26 — 「按档位:达档席内审契约增量;未达档 ⛔ 不自审,走保险丝路;豁免仅独立性件与保险丝」 → 「按车道:spec 与 skills
席审契约增量;达档席内审,未达档 ⛔ 不自审,起达档子代理」.
- :27 — 「达档与默认档同形」 → 「席内与子代理同形」: there is no default-tier record any
more; one shape holds between in-seat and subagent (the two scripts'
quotations of this line are updated in the same PR).
- :32 — the independence case → 「独立性件(契约真分叉、dev
挂旗)与保险丝只免席内审,不免复核:起隔离达档子代理」. This is the 「豁免仅独立性件与保险丝」 rewrite the
ruling asked for: the independence case and the fuse exempt the seat
from IN-SEAT review only, never from the review's existence. The former
third trigger 「派发后的跨车道面(含 spec)」 is lane routing now (:25).
- :40 landing pre-check ① — 「席内条款②复核 PASS 在案 …(档位按实测)」 → 「达档条款②复核 PASS
在案 …(spec、skills 每轮)」.
- :50 the fuse — 「管每个出条款②裁决的席 … 未达档 ⛔ 不自审」 → 「只管 spec 与 skills 席的条款②复核」
(the 不自审 clause stays at :26).
- :52 — 「至席内复核完成」 → 「至达档复核完成」.
- :53 — 「标签原样留置,队列外等待是安全态」 → 「起不来即无复核,标签原样、队列外等档」 (rule 3 on the fuse
side).

**core-rules.md** — one rewrite: :112 「契约卡达档复核归派发席」 → 「契约复核只 spec、skills
欠」. Trace by REST `GET /pulls/N/files`: PR objectstack-ai#18363 (09-16) touched
SKILL.md, contract-review.md and platform-readings.md only, so the 09-16
re-key never landed in core-rules.md; :112's tier key came with PR
objectstack-ai#18511 (09-17), replacing PR objectstack-ai#17294's 「归 spec 席」. :122 「`references/`
席内达档复核后入队」 is left as the mirror of SKILL.md :625 (see acceptance
notes).

## Machine side

**commit 2 — `scripts/pm/dispatch-gates.mjs`.** The clause-② note and
the suspect tail that `--tier` prints (quoted into claim comments) said
「spec seat; default-tier build」 and 「in the spec seat」 — the 09-10 seat
key, which PR objectstack-ai#18363 never re-keyed. They now name the spec and skills
lanes, the in-seat-or-subagent route, and 「a hit outside those lanes is
spec-lane work and moves there」; the docblock above
`MANDATORY_TIER_GLOBS` carries the rule. Four self-test pins hold both
renderings to the lane key and refuse the two retired spellings.
`--self-test`: 1852 cases pass (baseline on `0b31d90`: 1848).

**commit 3 — `scripts/pm/check-clause2-carriers.mjs`.**
`needsRecordRead` — C6's population gate and the sweep's read budget —
owed a record only in the completed state (a cleared `yes`), so a
spec-lane `Clause-②: no` round read 0 with no record on its head:
exactly PRs objectstack-ai#18530 / objectstack-ai#18529, the card's measured pair. Now:
- `LANES_OWING_REVIEW` (`domain:spec`, `domain:skills`, frozen) and
`laneOwesReview(pair)`, read off the CARD's `domain:*` labels;
unreadable labels stay UNJUDGED through the existing labels gap.
- Population: the completed state (unchanged) OR a declared `no` on a
spec/skills card (new). A `no` anywhere else still owes nothing —
pinned.
- C6's row on a spec/skills `no` round with no record: exit 4; remedy =
the lane's review at tier (in-seat or by the at-tier subagent), with the
unavailable-tier state and its one bypass named.
- A cleared `yes` OUTSIDE the two lanes keeps its row and its exit 4
(the `yes` is a limb hit, and limb-hit work is owed), but its remedy is
lane ROUTING: re-lane the item to `domain:spec` (`pm:retriage`, or split
the contract work to a spec-lane card or PR per 「新 `packages/spec` 工作恒由
`domain:spec` 席收口」), or correct a false `yes` with a
`Clause-②-correction:` comment — never a default-tier self-review, never
an at-tier subagent from that lane.
- The C6-RECORD note prescribes the clear-citation only where a clear
exists; on a spec/skills `no` round with its record it says the lane
owes the record and it exists.
- New floored battery, 34 cases (floor 30); roster floor 32 → 33.
`--self-test`: 977 cases pass (baseline 941).

Deviation from the dispatch's suggested case (c) — a `yes` pair on a
`domain:cli` card as 「not owed as a record, plus a note」 at exit 0 —
implemented instead as owed at exit 4 with the routing remedy. Reasons:
contract-review.md :42 promises 「0 = … head 上有记录」; a cli seat clearing a
`yes` pair and landing it from that lane is never a legal workflow under
rule 2 (the work 「moves there」), so an exit-0 note would be the
0-with-a-message the file's own header bans; and the row's exit is
unchanged from today (only the remedy text moves), so no legal workflow
is re-blocked. The four-axis reading is in the report's
`open_questions`; the PR is draft for the maintainer's word either way.

## Gates (run on `bc0c2ec41`, the final commit)

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 46 families. 45 ran with exit 0
(each captured redirect-then-`$?`), among them `check:pm-skill-ratchet`,
`check:pm-skill-id-lint`, `check:skill-frame-sync`,
`check:pm-governed-prose`, `check:pm-clause2-carriers`,
`check:declared-population-live`, `check:pm-half-states`,
`check:nul-bytes`. `check:doc-formula-expressions` first exited 3 —
PREREQUISITE NOT MET (`@objectstack/formula` / `@objectstack/lint` not
built; nothing measured) — and reruns green after `turbo run build
--filter=@objectstack/formula --filter=@objectstack/lint` under the
verify lock (22 + 9 + 14 examples judged clean).
`check:pm-dispatch-gates` (430–450 s) was still running detached when
this body was written; its verdict and the `--ran` reconciliation are in
the `os-dev-report` comment on objectstack-ai#18536. Both edited scripts' own
`--self-test` pass (above). The four package tests that mention
`dispatch-gates.mjs` do so in docblocks only and read nothing from it,
so they are not owed. The derivation printed a STALE TREE warning (the
tree is behind an `origin/main` that moved during the run); the PM's
re-derivation after the report reads the true list.

`skip-changeset`: nothing published moves — `.claude/**` and
`scripts/pm/**` ship in no package's `files[]`.

## Acceptance notes

Lines outside this card's declared file surface that carry seat or tier
wording — reported, not edited (承接者: the skills seat's next SKILL.md
round; the hot-file queue behind this card holds objectstack-ai#18743 · objectstack-ai#18755 ·
objectstack-ai#18665 · objectstack-ai#18469 PR-B · objectstack-ai#18489):
- SKILL.md :231 「派发后发现的跨车道面(含 spec)不移卡,认领席借契约复审档隔离复核」 and core-rules.md
:62 (its mirror). The "borrow the isolated at-tier review" route is what
rule 2 closes for other lanes (no other lane spawns the at-tier
subagent). The ruling's 「moves there」 and SKILL.md :234 「已派发卡 ⛔ 不因触 spec
转席」 reconcile as "the contract WORK moves to the spec lane; the card is
not re-seated", which is how contract-review.md :25 now reads. Suggested
rewrite of :231, same byte budget: 「认领即跟到 MERGED:派发后发现的跨车道面(含
spec)不移卡,契约面工作交 spec 车道达档复核。」
- SKILL.md :608 「技能面 hunk 须由契约复审档的席复核,档外席先交 skills 席」, :623 「由本席按达档自审」,
:625 「经席内达档复核后 ready → 入队」 and core-rules.md :122 「`references/`
席内达档复核后入队」: consistent with rule 1 read as "the skills lane's review at
tier, in-seat or by subagent"; the word 席内 there is the 09-16 spelling
and could be read as in-seat-only.
- `references/lanes/spec.md` :19–:33 and `references/lanes/skills.md`:
no contradiction found; `lanes/skills.md` :13 「契约复核归派发席席内」 is ownership
wording (the seat owns the record it adopts) and stays true.
- The follow-up the ruling assigns to the lane seats after this lands,
not to this card: re-read the `needs:contract-review` carriers hung
outside the spec/skills lanes (the director's ledger ⑫) and re-lane or
strip each.
- Sweep-mode cost: `check-clause2-carriers.mjs` without `--pair` now
buys the PR thread for every spec/skills-lane `no` pair too (one read
per PR, cached per PR); the budget paragraph in the file's header is
unchanged in shape.

## 维护者速读(草稿)

**改了什么**:把契约复核的归属重新按「车道」写回章程:只有 spec 与 skills
两条车道的每一轮交付都要过契约复审档的复核——席位达档就席内审,不达档就起达档子代理;其余车道零契约复核,条款②命中即 spec
车道的活;达档子代理起不来时 PR 留 draft、队列外等档,唯一旁路是您亲审。三份章程文本行数不变(812 / 60 /
151),两个机读脚本(`--tier` 的提示行、`--pair` 的复核记录人口)同步改成按车道判。

**为什么改**:9-16 那次把「只在 spec 席」改成了「按席位实测档」,席位名单没了,于是同一张 `Clause-②: no` 的
spec 车道 PR 在两条细则下答案相反(objectstack-ai#18536 的两种读法),而账号级 429 让「等档位」在两种读法里含义完全不同。您 9-17
的裁决把车道规则说回来了,本 PR 只是把它落到文本和脚本上。

**风险与代价(含回滚)**:`--pair` 从此对 spec/skills 车道的 `no` 轮也要求 head 上有复核记录,没有就退
4——这正是 objectstack-ai#18530 / objectstack-ai#18529 该有的读数,但意味着这两条车道的 `no` 轮在记录落下前都不能入队;其它车道不受影响,`yes`
挂在别的车道上的旧读数(退 4)不变,只是补救措施从「自审」改成「改道 spec」。回滚 = revert 这三个
commit,文本与脚本一起回到 `0b31d90`。

**席位意见**:(留空)

**你要做的**:确认本 PR 是否如实落了您的车道规则,是则合并;合并后各车道席按裁决去清理挂在 spec/skills 之外的
`needs:contract-review` 载体。

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

---------

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

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gen-sdui-manifest-cleanup.test.ts counted one caller lock fd (LOCKFDS 1, expected 0) on an unrelated PR's Test Core (1/6) — main green the same day

3 participants