Repository navigation
fix(pm): check-harness-current watches the skills tree, and compares content as well as placement - #18610
Conversation
…content as well as placement `PATHS` enumerated three paths while the tool's own header claims it answers "is every harness-loaded file on `origin/main` already in the shared checkout's HEAD?". `.claude/skills/**` is loaded the same way and never reloaded, so a seat could run a whole shift against stale charter text with nothing to tell it. Two additions, both widening: - `.claude/skills/**` joins the watched population. Measured on this tree: the three candidate spellings select the same 9 files and the same `git log -1` sha, so the walker honours the recursive form as written. - A second reading per path: does the shared HEAD's copy of the path equal the ref's? The placement reading, and the round-open marker comparison built on it, are both blind to a checkout that was ALREADY behind when the seat sat down, because the touch sha is then identical from one marker to the next. The content reading answers the header's question of the bytes directly, so it also answers where placement is UNDECIDED, and it refuses (UNDECIDED) rather than reporting "identical" when the comparison cannot be made. A path is STALE if EITHER reading places it behind, counted once. The three original paths' placement lines are byte-identical: verified by running `origin/main`'s copy and this one against the same shared checkout. Self-test floor 15 -> 26. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
席内复核(派发席
|
| (2026-09-17T08:57Z 取) | origin/main 那份 |
本 PR 那份 |
|---|---|---|
| 末行判词 | STALE -- 3 harness-loaded path(s) |
STALE -- 4 |
输出里含 skills 的行 |
0 | 2(放置读数 + 内容读数) |
| 它说了什么 | 只点名 settings / agents / hooks | 另点名 .claude/skills/**,并列出 9 个有差异的文件,含 pm-dispatch/SKILL.md、references/core-rules.md、references/contract-review.md |
⇒ 缺陷与修复都不是转述:同一个检出,旧的那份说「3 条过期」,新的那份说「4 条」,多出来的那条正是章程本身。
其余机械读数
| 读数(2026-09-17T08:57Z 取) | origin/main |
PR 头 |
|---|---|---|
--self-test(各自树内真跑) |
all 15 cases passed,exit 0 | all 26 cases passed,exit 0 |
SELF_TEST_CASE_FLOOR |
15 | 26(上调) |
PATHS |
三条 | 四条,原三条原位保留,追加 .claude/skills/** |
对照 grep -c execFileSync |
4 | 4 |
execFileSync 在 git diff origin/main pr18610 -- <该文件> | grep -c 上读 0,才有资格当对照。我第一次想用的 harness-loaded 在 diff 里读 2(5 → 6),primary 更阴险 —— 两版都读 3、看着「没动」,而 diff 里它出现 4 次。⇒ 一个被 diff 移动过的词,即使两端计数碰巧相等,也只能证明探针是死的。
一处闭环,值得记
这个 PR 的主体证明了它自己:共享检出在 .claude/skills/** 上落后 9 个文件,其中包括 SKILL.md。本席整轮的章程读数都取自 /tmp/claude-0/probe-main 的 origin/main 一次性工作树,⛔ 从不取自共享检出 —— 但那正是这张卡说的:没有这条修复,一个照着共享检出读章程的席位不会被告知。
放行路径
绿判据 2026-09-17T08:57Z 读为 NOT GREEN(1 条 in_progress)⇒ 等;绿后翻 ready → 重读到 total_count 真的移动 → 武装。Part of ⇒ 落地后卡 ⛔ 不关,转回 pm:queue 并留 Release: 行点名章程那半的去向。
Generated by Claude Code
…rface `.claude/**`, not three paths (objectstack-ai#18656) Fixes objectstack-ai#18636 Clause-②: no ## The defect The round-open step-2 reading at `.claude/skills/pm-dispatch/SKILL.md:91` named three paths — `.claude/{settings.json,agents/*.md,hooks/*}` — while the harness loads the whole `.claude/**` tree from the primary checkout at session start and never reloads it, `.claude/skills/**` in full for every seat session (the load moment `scripts/pm/check-skill-line-ratchet.mjs`'s header states). So the charter told the seat to measure a set SMALLER than the set that rots it. The tool half — `scripts/pm/check-harness-current.mjs`'s `PATHS` widened to `.claude/skills/**`, plus a second CONTENT reading per path — landed in PR objectstack-ai#18610; this PR is the charter half that objectstack-ai#18544 comment 5706565860 said stays on this file, line-neutral. The card's own true positive: SKILL.md moved `ab1d359` → `7f4aa9e5c` at a round open and the step-2 reading said nothing, because the seat's copy of the tool was the old one too. The charter has to name the right set on its own, whichever copy of the tool the seat holds. ## The line, before and after `:91` only; nothing else in the file, no reference file, no script. Before (120 B, on `origin/main` `99fcb4ac1`, byte-identical to the `cf39b83c0` the card was dispatched against): > - 同读 harness 载入面 `.claude/{settings.json,agents/*.md,hooks/*}` 的最新触碰是否已在共享检出 HEAD。 After (96 B): > - 同读 harness 载入面 `.claude/**` 的最新触碰与内容是否已在共享检出 HEAD。 Payment: in place, line-neutral — 812 / 812, 1 insertion / 1 deletion (`git diff HEAD~1 HEAD --stat`), no second line, no re-wrap, no other line moved, the widest table row unchanged (342 B, pin 342). It reads as one block with `:92`, which is untouched: 「否 ⇒ 记座位贴,自然收班时换会话,⛔ 不中断批次;读数走 `scripts/pm/check-harness-current.mjs`。」 — the 「否」 still answers `:91`'s yes/no question, and `:92` still names the tool that takes the reading. Two things moved on the line: - `.claude/{settings.json,agents/*.md,hooks/*}` → `.claude/**` — the SET, the card's fix. - 「最新触碰」 → 「最新触碰与内容」 — the READINGS. The tool on `main` takes two per watched path (PLACEMENT: is the commit that last touched the path on `origin/main` an ancestor of the shared HEAD; CONTENT: are the shared HEAD's bytes of the path equal to `origin/main`'s), and a path is STALE on either. The charter now names both, so a seat holding an old copy of the tool — the card's reading 3 — still knows what it is supposed to measure and can see when the tool's report is short of it. ## Why the set is the directory and not a longer list — measured, then the four axes Measured on this tree at `14f751515` (`git ls-files` per pathspec): `.claude/**` selects 43 tracked files; the tool's four pathspecs select 1 (`settings.json`) + 1 (`agents/*.md`) + 10 (`hooks/*`) + 29 (`skills/**`) = 41. The two files outside the four pathspecs are `.claude/launch.json` and `.claude/workflows/docs-accuracy-audit.js` (both last touched `5de93728a`; their load moment is unmeasured, as the tool's header says of hooks and agents). Spelling the four pathspecs on the line would cost 131 B — over the 120-byte max-line budget — and would be the file-list spelling the tool's own `PATHS` comment names as the failure shape: 「naming the charters one by one is how a predicate ends up guarding less than it claims (the population it covers must be the population that is loaded)」. 四维分析(新记录,按章程用中文): - **实际业务需求** — 场景已实测,不是推断:objectstack-ai#18544 评论 5706565860 记录了一个席位整班执行已离开 `origin/main` 的旧 :92 处方(车道停摆约 7 小时);本卡读数 2 是同一机制的第二次真阳性(SKILL.md `ab1d359` → `7f4aa9e5c`,step-2 读数一字未提)。harness 侧的载入面本来就是目录——`check-skill-line-ratchet.mjs` 头部「read in full by every seat session」说的是 `.claude/skills/**`,不是三个文件;写 `.claude/**` 就是写实测的载入面。 - **项目长远合理性** — 契约优先:章程是契约,工具是读数的实现,章程应当说清「量哪个集合、量什么」,由工具追章程而不是章程追工具的常量。工具头部自己写明「the population it covers must be the population that is loaded」。逐个列 pathspec 的写法今天恰好等于 `PATHS`,`.claude/` 明天多一个子目录就再次落后——正是本卡的缺陷形态,只是换了个数字;目录写法不随 `PATHS` 腐烂。 - **防 AI 写代码犯错,尤其是防 AI 写元数据 app 犯错** — 席位是 AI,它量什么集合只取决于章程这一行字。读数 3 已证工具副本也会旧:章程点名 `.claude/**` 与两种读数之后,旧工具只报两条路径时席位能在结构上看出集合不齐;现在的三路径写法让它无从察觉。不写「与上轮」:那是一条形态未定的席位侧账本义务,120 B 里写不清的义务恰是 AI 会写错的那种。 - **创业阶段不扩散需求** — 零新机制:一行、两个词,两种读数都已在 `main` 的工具里落地;不加参考文件、不加脚本、不加座位贴字段。目录写法多覆盖的两个文件(`launch.json`、`workflows/*.js`)载入时刻未测,按从紧原则不据此扩 `PATHS`,记入验收备注交工具所在车道。 ## The directory-diff suggestion — taken in one half, left in the other The card proposes writing 「目录 diff 与上轮逐字节比对」 into step 2. Split along the same axes: - **Taken — the byte-level half**, as the tool's CONTENT reading against `origin/main`, which PR objectstack-ai#18610 landed and which `:91` now names (「与内容」). It is strictly more informative than the seat's byte-count habit the card describes: it names the differing files per path, needs no history, and fires on a checkout that was ALREADY stale at seating. - **Left — the 「与上轮」 half** (a number compared with the previous round's). (i) For the three charters, `:89`–`:90` already compare the latest touch sha with the previous round-open marker and prescribe the re-read, so a second round-over-round comparison on `:91` would duplicate them. (ii) For the harness-loaded rest (`settings.json`, hooks, agents) a re-read is not a remedy — the harness never reloads — so `:92` is the whole prescription, and "moved this round" versus "already stale" changes nothing the seat does. (iii) A round-over-round ledger is a new seat-side obligation whose form (what to record, where, compared how) does not fit in the 24 bytes the line has left, and an under-specified obligation is the kind an AI seat gets wrong. Where it would go if the seat wants it: the round-open marker's contents on `:90` (record the tool's named stale paths beside the touch sha), or a since-marker flag on the tool — not `:91`. ## Ratchet reading `pnpm check:pm-skill-ratchet` (self-test then gate) at `14f751515`, exit 0: ``` ✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/SKILL.md: widest table row is 342 bytes (pin 342; headroom 0). ✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/SKILL.md is 812 lines (ceiling 812; headroom 0). ``` The edited line went 120 B → 96 B, under the 120-byte max-line budget; `.claude/skills/pm-dispatch/SKILL.md` is the only path in the diff; `grep -naP` for control bytes over the file: 0 hits. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from the worktree with no hand-fed paths (change set from git: 1 path vs merge base `99fcb4ac1`, three-dot): 18 commands. Each run with the tool's own capture idiom (redirect, then `$?`, the printed command recorded byte for byte), then reconciled with `--ran`: **18 derived, 18 run, 0 NOT-MEASURED, 0 UNRUN** (exit 0, 「a DERIVED zero — all 18 recorded an exit code and none of them is 3」). | # | command | exit | |---|---|---| | 1 | `node scripts/check-closing-keyword-parity.mjs` | 0 | | 2 | `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 | | 3 | `node scripts/check-comment-mask-corpus.mjs` | 0 | | 4 | `node scripts/pm/check-governed-queue-guard.mjs --self-test` | 0 | | 5 | `node scripts/pm/check-harness-current.mjs --self-test` | 0 (26 cases) | | 6 | `pnpm --filter @objectstack/lint run check:doc-formula-expressions` | 0 — first run exit 3 `PREREQUISITE NOT MET` (unbuilt closure), not a measurement; `pnpm exec turbo run build --filter=@objectstack/lint` under `scripts/pm/os-verify-lock.sh` (VERDICT command-exit 0, held 141 s, waited 0 s), rerun exit 0 | | 7 | `pnpm check:agent-test-spelling` | 0 | | 8 | `pnpm check:doc-authoring` | 0 | | 9 | `pnpm check:driver-memory-census` | 0 | | 10 | `pnpm check:nul-bytes` | 0 | | 11 | `pnpm check:pm-governed-merges` | 0 | | 12 | `pnpm check:pm-governed-prose` | 0 (2 surfaces name all 5 governed surfaces) | | 13 | `pnpm check:pm-half-states` | 0 | | 14 | `pnpm check:pm-skill-id-lint` | 0 (27 files clean) | | 15 | `pnpm check:pm-skill-ratchet` | 0 | | 16 | `pnpm check:refd-timer-probe` | 0 | | 17 | `pnpm check:skill-frame-sync` | 0 (one declared copy, 4 axes, 74 markdown files scanned) | | 18 | `pnpm check:watch-hint-literal` | 0 | Not derived, run anyway: - `node scripts/pm/check-governed-merges.mjs --test .claude/skills/pm-dispatch/SKILL.md` → **exit 3, GOVERNED** (1 of 1 path hits the register) — the expected answer for this path, recorded as such and not as a failure. - `pnpm check:pm-settings-deny-roster` (the derivation flags its roster as living under `.claude`, so its silence is evidence in neither direction) → exit 0, 17 declared = 17 enforced. - `node scripts/check-skill-frame-freshness.mjs --no-fetch` → exit 0. - Tier: `dispatch-gates.mjs --tier --repo objectstack-ai/objectstack .claude/skills/pm-dispatch/SKILL.md` → the MANDATORY tier the claim comment's `Container & model` line carries, derived from the file surface; this run was at that tier. `pnpm lint` (the repo-wide union) is CI's run; locally it narrows to zero files by measurement, not by skipping: (1) population read from `eslint.config.mjs` itself — every `files:` glob is `{ts,tsx,mts,cts,js,jsx,mjs,cjs}`, no markdown; (2) `npx eslint --no-inline-config --format json` on the one changed path → 1 file, 「File ignored because no matching configuration was supplied」, 0 errors, exit 0; (3) invariance: the diff touches no file eslint lints, so no untouched file's verdict can move. Not measured locally: the 52 artifact-roster families, the 11 wide-population families and the 14 pending-changeset families the derivation lists outside its runnable total — CI's, as the derivation says. ## Changeset `skip-changeset`, measured on the fast track: `.claude/**` is repo-internal agent tooling, in no package's `files[]`; nothing published moves. ## Acceptance notes - noted, not filed: on `14f751515` the tool's four `PATHS` pathspecs select 41 of the 43 tracked files under `.claude/`; the two outside are `.claude/launch.json` and `.claude/workflows/docs-accuracy-audit.js`. Whether the harness loads either at session start is unmeasured (the same status the tool's header gives hooks and agents), so this is an observation, not a contract violation; 承接者: the tool's lane (`domain:devx`, `scripts/pm/check-harness-current.mjs`), if a load-moment measurement ever lands. - noted, not filed: `check-doc-formula-expressions` refuses with exit 3 on a fresh worktree until `@objectstack/lint`'s closure is built; its refusal text names the exact fix and the derivation classifies 3 as NOT-MEASURED, so nothing is hidden — 承接者:无. ## 维护者速读(草稿) - **改了什么**:`.claude/skills/pm-dispatch/SKILL.md` 第 91 行一行。开轮 step-2 的 harness 读数原来只点名三个路径 `.claude/{settings.json,agents/*.md,hooks/*}`,现在点名整个载入面 `.claude/**`,并把工具在 `main` 上已落地的两种读数(最新触碰、内容)一起写进去。812 / 812 行不变,该行 120 B → 96 B,别处一字未动。 - **为什么改**:harness 在会话开始时整份读入 `.claude/**`(`.claude/skills/**` 每个席位都整份读),章程却让席位只量三个文件——量的集合比会腐烂的集合小。objectstack-ai#18544 评论 5706565860 记录过一个席位整班执行旧规则、车道停摆约 7 小时;本卡又撞上一次(SKILL.md 动了,读数一字未提,因为席位手里的工具也是旧的)。工具那一半 PR objectstack-ai#18610 已合;这是章程那一半。 - **风险与代价(含回滚)**:风险极低——一行文字,不加机制、不加文件、不动棘轮上限;18 个派生门禁全绿,棘轮 812 / 812、最宽行 342 不变。回滚就是 revert 这一个 commit(`14f751515`),不牵连任何脚本。 - **席位意见**:(留给席位) - **你要做的(一个动作)**:批准或合并这个 draft PR(受管面 `.claude/**`,须维护者亲手或授权批准后由席位落地)。 --- _Generated by [Claude Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_ Co-authored-by: Claude <noreply@anthropic.com>
Part of #18544 — this PR is the TOOL half only. The card stays open for the charter half.
Clause-②: no
What moved
scripts/pm/check-harness-current.mjsonly. Its header declares the question it answers —"is every harness-loaded file on
origin/mainalready in the shared (primary) checkout's HEAD?" —while
PATHSenumerated three paths and left out.claude/skills/**, which the harness loads thesame way and never reloads. Two additions, both strictly widening:
.claude/skills/**joins the watched population. The card's blind spot, closed in thedirection the card names: the predicate's population catches up to the population it claims to
guard. ⛔ Nothing was narrowed or deleted to obtain a green verdict.
commit that last touched this path on the ref an ancestor of the shared HEAD?", and a seat's
round-open marker asks "has that sha moved since my last marker?". Both are blind to the same
case: a checkout that was ALREADY behind when the seat sat down. The touch sha is then identical
from one marker to the next — nothing moved, because the thing that is wrong never moves. The
new reading compares the bytes directly, needs no history at all, and refuses (UNDECIDED) rather
than reporting "identical" when the comparison cannot be made.
A path is STALE if EITHER reading places it behind, counted once per path.
The spelling was measured, not assumed
Every
PATHSentry is a git pathspec. On this tree the three candidate spellings are equivalent —each selects the same 9 files and the same
git log -1sha:git log -1 origin/main -- SPEC.claude/skills/**ab1d35925.claude/skills/*ab1d35925.claude/skillsab1d35925.claude/hooks/*(a term this diff does not move)674872a5bTaken on the shared checkout, HEAD
b48abc48a9vsorigin/maine81c4e5f30. The control fires onthe same tree with the same command shape, so the 9 is a reading and not a dead instrument.
The pre-existing three paths are byte-identical
origin/main's copy of the tool and this branch's copy, run back to back against the SAME sharedcheckout (HEAD
b48abc48a9), placement lines for the three original paths extracted the same wayfrom each run:
The self-test carries the same proof at fixture level: all 15 pre-existing cases pass unchanged
with the widened
PATHSand the second reading in place.Self-test: floor 15 to 26, and it is live
11 new cases. Both directions on a real fixture, the marker-blind case, and the anti-silent-green
case:
.claude/skills/**by name; 4 placement lines and 4 content lineson one run;
of a ref advance, are IDENTICAL — and the content reading still names the skills path, lists the
differing file, exits 1, and counts the path ONCE;
a reading of that path and not a blanket failure;
origin/mainreads CURRENT at exit0, summary byte-identical to the one seats already read;
origin/mainIS an ancestor of HEAD) while the loaded charter is not
origin/main's — still named STALE;Ablation ledger — 4 legs, each mutate / RED / restore-proven
scripts/ablation-replace.mjsis not on this base (e81c4e5f3), so the same evidence was producedby hand, with an EXIT/INT/TERM trap, absolute paths, before/after occurrence counts on the anchor,
the exit code captured before any pipe, and a restore proven against the HEAD blob.
.claude/skills/**fromPATHS(10 to 9)9b8c7a933f== HEAD blob,git diff HEADempty'unreadable'1 to 0)Every leg refused to proceed on an anchor miss, and every leg checked that the post-mutation hash
was non-empty and differed from the HEAD blob before reading any verdict — an empty hash reads as
FAILURE, not as success. After all four legs:
--self-testexit 0, all 26 cases passed, workingtree clean.
Gates
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, thenre-derived after
git fetch origin mainadvanced the localorigin/maintod93400f42: the commandset is IDENTICAL, 29 families. All 29 run, all exit 0.
check:pm-dispatch-gateswas detached andwaited on with
tail --pid: 1746 cases pass, 818.2s on this box.pnpm lintran as the full repo union at HEADb32fba174: 6804 files, 0 errors, 0 warnings — nonarrowing to declare.
Changeset: skip-changeset, measured
One criterion: does anything PUBLISHED move? Probe over all 82
package.jsonin the workspace (70publishable with a
files[]), asking whether the changed path lands inside any package's shippedcontent:
The
packages/lintrow is the exact trap the "scripts are exempt" reflex would have walked into;the probe distinguishes it from source that is not shipped. Nothing published moves, so
skip-changeset.Still open on the card
The charter half.
.claude/skills/pm-dispatch/SKILL.md:88-92's step-2 reading names THREE fileswhile the loaded surface is a DIRECTORY — the same shape as this card's title, a different
predicate. That file is a governed surface, it belongs to the
domain:skillsseat, and an open PRis already on it, so it is deliberately untouched here. Hence
Part of, not a closing keyword.Generated by Claude Code