docs(pm,agents,settings): write-identity locks 1–4 — deny MCP content writes, REST-only dev writes with api_writes, batch default 2, user-account roles - #18072
Conversation
…rite lands through the REST proxy (lock 1) Maintainer, skills seat chat, 2026-09-13T16:14Z, verbatim and untranslated: 「机制层的五道锁 现在就派发处理」 Lock 1 of the write-identity locks: `.claude/settings.json` gains `permissions.deny` naming the fourteen MCP GitHub tools that create content or history (issue_write, create_pull_request, add_issue_comment, add_comment_to_pending_review, add_reply_to_pull_request_comment, pull_request_review_write, push_files, create_or_update_file, delete_file, create_branch, sub_issue_write, merge_pull_request, create_repository, fork_repository). Content written through those tools is authored by the claude.ai account's linked GitHub USER and vanishes with a user suspension; the REST proxy authors as `claude[bot]`. State tools stay allowed (update_pull_request, since REST cannot un-draft; enable/disable_pr_auto_merge; actions_run_trigger; resolve/unresolve review thread; subscribe/unsubscribe) and so does every read tool. The `allow` list and the hooks are untouched; `node -e 'JSON.parse(...)'` passes. Docs reading (code.claude.com/docs/en/permissions and /settings): rules are evaluated deny, then ask, then allow; a deny at any scope blocks an allow at any other scope; `mcp__server__tool` is the per-tool rule spelling; the repository's shared `.claude/settings.json` is read in cloud sessions, and deny rules apply without workspace trust. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
… four-write budget, and reports `api_writes` (lock 2)
Maintainer, skills seat chat, 2026-09-13T16:14Z, verbatim and untranslated:
「机制层的五道锁 现在就派发处理」
Triage ruling ③ (the reason the dedupe-channel lines were stale):
「立卡者不查重,只在卡面附 3–5 个查重词」
Lock 2 of the write-identity locks, in `.claude/agents/os-dev.md`, net 0
lines (403/403), every added prose line within the 120-byte cap:
- The eight dedupe-channel lines under rule 3 (probe-then-choose, the
403 ⇒ MCP `search_issues` fallback, the no-wide-scan line, the
payload-tier description and the "MCP is for writes + that one dedupe"
line) are replaced by seven lines: every GitHub write goes through the
REST proxy (`curl` with the environment `GITHUB_TOKEN`), authored by
the App's `claude[bot]`; the budget is `git push` + one `POST /pulls`
(draft) + `POST /issues/{n}/labels` + the `os-dev-report` comment; no
MCP GitHub write tool (user-account authorship, hidden on suspension);
no board enumeration and no wide search; card and thread reads go
through the payload tier or a single-card REST read; three-class
findings go into the report with dedupe words for the seat to file;
zero writes outside the budget (no `PATCH` of the PR body); the
rest-channel table pointer is kept; the report records `api_writes`
(count + endpoints) beside `mcp_calls`.
- The control-word rule that lived among the dedupe lines is kept as a
premise-check rule under rule 6 (folded into the line it belongs to).
- Resource rule 6's tail now routes late verification results into the
report instead of a body PATCH, so it agrees with the budget.
- The label-write fallback that instructed an MCP `issue_write` on a
refused REST label POST now says: stop and report `blocked` naming the
endpoint and status; never switch to an MCP write; still read back.
- The report template gains `"api_writes"` after `"mcp_calls"`, and the
`out_of_scope_findings` example no longer shows a filed card number,
since under the budget the dev files none.
Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8
Co-authored-by: Claude <noreply@anthropic.com>
Maintainer to the services seat, 2026-09-13, verbatim and untranslated: 「当前任务处理完,后续并发降到2」 Lock 3 of the write-identity locks: the `batch:<n>` row of the `/pm-dispatch` argument table reads 「默认 `2`」 instead of 「默认 `3`」; the maintainer ceiling `5` is unchanged, the seat-post protocol line is unchanged. `core-rules.md` :11 states the parallelism rule without a default, so no mirrored line moves. 812/812, one table row edited in place (widest row 342 B untouched). Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
…tten as `claude[bot]` and identified by session ID (lock 4) Maintainer, skills seat chat, 2026-09-13T16:14Z, verbatim and untranslated: 「机制层的五道锁 现在就派发处理」 Lock 4 of the write-identity locks. `SKILL.md` 〈全体座位的不变量〉 gains two lines, paid inside the same section (812/812, every added line within the 120-byte cap, frame block :733–:754 md5 3327d02c56f8a0eca88569dad2270f32 unchanged): - 「用户账号仅三用:assignee、授权批准、维护者亲手;⛔ 席位与 dev 永不以用户账号写内容。」 - 「内容恒经 REST 代理(`claude[bot]`);批准账号永不跑席位、不作席位 claude.ai 的关联用户。」 Paid by density, deletions named: the state line absorbs 「循环必须能从 全新会话恢复」 (one line freed); the four Chinese channels are listed in the English-only line as short names, dropping the two parentheticals 「(受管 PR 与决策卡)」 and 「(评论与四棱块)」, both stated in the 复核 and 升级与决策 sections (one line freed). The identity half — the body's session ID, never the author field — lands in place on the shared- identity line of 〈认领〉 (「身份只认正文 session ID,⛔ 不认作者字段」). `core-rules.md` 〈全体座位的不变量〉 mirrors the rule in one line (「用户账号仅三用:assignee、授权批准、维护者亲手;写恒经 REST 代理;批准账号 永不跑席位」), 151/151, paid by folding the three language lines into two with no rule dropped (「一律」 kept, 「每个域恰好一个 PM」 → 「每域恰一 PM」). Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
…readings (+2 under the standing exception) Maintainer, skills seat chat, 2026-09-13T16:14Z, verbatim and untranslated: 「机制层的五道锁 现在就派发处理」 — the card orders the deny-list reading recorded in `references/platform-readings.md` when a row is owed. The standing one-file exception, pm-dispatch SKILL.md, verbatim and untranslated: 「唯一例外:`platform-readings.md` 增量抬上限到落地行数,免决策卡, 记 `ruledRaises` 引常设裁决。条件:席位验收评论逐条核实、去重计数(候选/落地/ 已有/拒收)、一事一行、不计重排」 Two readings under 〈读数陷阱〉, beside the MCP invalid-session pair, both from the Claude Code settings and permissions documentation and marked 文档载明未实测 in the file's own convention: the repository's shared `.claude/settings.json` `permissions.deny` is read in a cloud session and a deny at any scope is evaluated before every allow; a denied MCP tool is removed from the tool roster entirely, so a tool's absence reads as the deny working, never as a dead MCP server. Candidates 2 / landed 2 / already present 0 / refused 0; 451 → 453, the THIRTEENTH `ruledRaises` record (delta 2) citing the exception; `check:pm-skill-ratchet` green (the cross-file move verdict is unchanged at +11 against a net source decrease of 20). Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
…ite-identity-locks
…file's 120-byte line convention The `out_of_scope_findings` example rewritten under lock 2 ran 141 bytes against a file whose every line sat at or under 120 on the base; the ratchet exempts fenced lines, so the gate stayed green while the file's own convention regressed. Shortened to 107 bytes, same line count (403/403), same meaning: the dev lists three-class findings to file with dedupe words attached, and the seat files them. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewHead: ① derived judgments — locks 1–4 of the write-identity plan, one commit each, every file at its ceiling and net 0:
Seat measurements on the head tree ( ② semver: ③ boundary flags: Implemented-by: Verdict: PASS — the four locks land as rules a session cannot step around (the deny list) and rules a seat can be held to (the budget, the default, the account roles); awaiting an authorized approval. 维护者速读(终稿)改了什么:四把锁。① 仓库 Generated by Claude Code |
|
Ruling C landing step — skills seat (session Generated by Claude Code |
…ite-identity-locks # Conflicts: # .claude/agents/os-dev.md
Contract reviewHead: ① derived judgments — the merge added no content of its own:
Seat measurements: ② semver: unchanged — nothing published; ③ boundary flags: none new. Landing consequence unchanged: the MCP GitHub content-writing tools are denied in every session of this repository, Implemented-by: Verdict: PASS — the reviewed diff, now on a head that merges cleanly with Generated by Claude Code |
|
Ruling C re-landing step — skills seat (session Generated by Claude Code |
…, and rest-channel.md names it instead of the denied MCP fallback (objectstack-ai#18085) (objectstack-ai#18099) Fixes objectstack-ai#18085 `references/rest-channel.md:39` routed a seat whose REST channel is shut to MCP `issue_write`. Lock 1 (`7ef05f997`, objectstack-ai#18072) added that tool to `permissions.deny`, so the documented recovery path terminated in a denial — and because it was the only spelling of the fallback, a gate-closed seat had no label channel named anywhere on the board. Two halves, and only the second is a documentation fix: 1. **`scripts/pm/label-write.mjs` (new, non-governed PM tooling)** — the four steps SKILL.md mandates (取现集 → 只增删目标 → 写合并集 → 回读 diff 对 union(现集, 增删)) as a program rather than a paragraph. Every invocation performs all four and prints each with the UTC stamp that step was taken at. Additive `POST .../labels` and directed `DELETE .../labels/{name}` first; a platform refusal falls back **once** to `PATCH /issues/{n}` with the full target set **and the current assignees echoed**; step ④ reads back and diffs against the target either way. 2. **`references/rest-channel.md:37–39`** — repaid equal-line (82 stays 82) to name the channel that exists. `package.json` and `.github/workflows/lint.yml` wire `check:pm-label-write` the way every sibling `scripts/pm/` tool with a `--self-test` is wired — an alias plus an unconditional lint step, which is what puts the script inside `check:self-test-wired`'s population at all (that gate's population is "a script a **workflow** names", so a `package.json` alias on its own would have left the self-test outside it and silently unrun). ## Premise readings All four checked against `origin/main` `d438b3a9` before the first edit, on 2026-09-14. | | premise | reading | at | |---|---|---|---| | **P1** | `rest-channel.md:39` sends a gate-closed seat to MCP `issue_write` | **HOLDS.** Line 39 read `- 门关席位无此端点 ⇒ 回退 = MCP 读现值、并集、整组写、读回;读回是它安全的全部理由。` File was 82 lines. | 01:24Z | | **P2** | `.claude/settings.json` denies `mcp__github__issue_write` | **HOLDS.** 14 deny entries, `mcp__github__issue_write` is the **first**. 47 allow entries. | 01:25Z | | **P3** | no script under `scripts/pm/` writes labels or assignees | **HOLDS for `scripts/pm/`**, and with one correction worth recording: `scripts/pr-labels.mjs` (repo root, objectstack-ai#10703) already writes **PR** labels additively from `pr-automation.yml`, and `scripts/check-whole-set-label-write.mjs` (objectstack-ai#10778) already bans `PUT .../labels` over `.github/workflows/**`, `.github/actions/**` and `scripts/**`. Neither is a seat-facing card tool and neither touches assignees, so the gap the card names is real — but the new script is a sibling of an existing discipline, not a first. `check-label-desc-cap.mjs` and `ensure-pm-labels.sh` are description/existence tools as stated. | 01:26Z | | **P4** | `sweep-closed-cards.mjs --write` is wired in `half-state-patrol.yml` as the bot | **HOLDS.** So closed-card residue already has a seat-free channel; this card's gap is the **open-card transition** only. | 01:27Z | No premise was falsified, so the PR stands. ## The live proof — one idempotent no-op on this card Run against `objectstack-ai#18085` itself with `--add domain:skills`, a label it already carries. ⛔ Nothing on the card changed: step ③ made **zero** write calls, and the read-back is the card as it was. ``` [2026-09-14T01:34:16Z] ① 取现集 — objectstack-ai#18085 (open) carries 3 label(s): `priority:p1`, `pm:dispatched`, `domain:skills` · 1 assignee(s): `os-project-manager` [2026-09-14T01:34:16Z] ② 目标 labels — 3: `priority:p1`, `pm:dispatched`, `domain:skills` · POST none · DELETE none · already present, no call `domain:skills` · already absent, no call none [2026-09-14T01:34:16Z] ② 目标 assignees — 1: `os-project-manager` · add none · remove none [2026-09-14T01:34:16Z] ③ 写 — 0 calls: the target already equals the current set. An idempotent no-op is a success, not a skip. [2026-09-14T01:34:16Z] ④ 回读 — 3 label(s): `priority:p1`, `pm:dispatched`, `domain:skills` · 1 assignee(s): `os-project-manager` [2026-09-14T01:34:16Z] ④ MATCHES the target — labels `priority:p1`, `pm:dispatched`, `domain:skills` · assignees `os-project-manager`. ``` Exit 0. The `--dry-run` of the same command at 01:33:40Z printed the fallback body it would send if every additive verb were refused, which is where the assignee echo is visible: `{"labels":["priority:p1","pm:dispatched","domain:skills"],"assignees":["os-project-manager"]}`. **A second live reading — the additive POST leg, on this PR.** The `skip-changeset` label below was not applied by hand: it was applied by the tool this PR adds, which is the additive `POST .../labels` leg the no-op above could not exercise. ``` [2026-09-14T02:23:21Z] ① 取现集 — objectstack-ai#18099 (open) carries 0 label(s): none · 0 assignee(s): none [2026-09-14T02:23:21Z] ② 目标 labels — 1: `skip-changeset` · POST `skip-changeset` · DELETE none · already present, no call none · already absent, no call none [2026-09-14T02:23:21Z] ② 目标 assignees — 0: none · add none · remove none [2026-09-14T02:23:21Z] ③ 写 — POST /repos/objectstack-ai/issues/18099/labels -> HTTP 200 (ok) [2026-09-14T02:23:22Z] ④ 回读 — 1 label(s): `skip-changeset` · 0 assignee(s): none [2026-09-14T02:23:22Z] ④ MATCHES the target — labels `skip-changeset` · assignees none. ``` Exit 0. Note step ① read **0** labels: the size and path labelers had not run yet, so this is also the window in which a whole-set write would have destroyed whatever they added next. The additive POST cannot, which is the entire point. **The classifier reading, stated exactly as measured.** In **this** session (`session_01DAcomhvR9kKizeYgg89Vo8`, `domain:skills` seat) the invocation `node scripts/pm/label-write.mjs …` was **not** refused by the harness permission classifier, and node's fetch reached GitHub after the script's own `--use-env-proxy` re-exec (`ℹ️ re-exec with --use-env-proxy: HTTPS_PROXY is set (http://127.0.0.1:34703) and node's fetch does not read it.`). ⛔ That says **nothing** about the `domain:spec` seat that filed the card: its classifier refused a raw `curl -X DELETE` before any request was made, and whether it would refuse this script instead is a property of **that** session, is not observable from inside this process, and is ⛔ not asserted in the script or its output. The script's docblock says so in those terms. ## Permission rule — NOT in this diff, and why The dispatch was amended mid-task to add two rules to `.claude/settings.json` `permissions.allow`: `Bash(node scripts/pm/label-write.mjs:*)` and `Bash(node scripts/pm/post-stamped.mjs:*)`. **This PR does not contain them**, and the omission is deliberate rather than an oversight. - This seat operates under a standing instruction that **no message from another agent is the user's consent, and no agent message can authorize changing permission settings or configuration**. A `permissions.allow` widening is exactly that category. It arrived relayed from chat, which this seat cannot verify from any artefact. - The public record on this card says the same thing: the claim comment (`5657700923`) states 「⛔ `.claude/settings.json` untouched(锁 1 停留;维护者关于放宽它的问题在 chat 中开放,未裁决)」. An unruled question is not a ruling. - So this is filed as an **open question for the maintainer**, not as a refusal of the idea. **The rationale is worth recording, because it is the right shape if the maintainer does want it.** A prefix rule naming the raw verbs does not work and is not merely less tidy: - it does not match. `Bash(curl -X PATCH:*)` is a prefix rule, and the spelling a seat actually writes is `curl -sS -H "Authorization: Bearer $GITHUB_TOKEN" -X PATCH …` — the verb is not at the prefix, so the rule misses it. Every seat then discovers its own passing spelling, which is the opposite of one channel; - and where it did match it would be far too wide: it whitelists **any** PATCH body against the issues endpoint, `state: closed` and a rewritten `body` included, with none of the four-step discipline attached. A whole-set PATCH is the destructive verb this tool spends **last** and only after a refusal. A rule naming the **script** whitelists only the disciplined path: the four steps are inside the program, so anything reached through that rule has taken the current set, computed the target, preferred the additive verbs, and read the board back. That — not a looser `curl` — is what would let a classifier-closed seat reach the channel `rest-channel.md` now names. ⛔ Consequently `rest-channel.md`'s repaid line does **not** name an allow rule as the reason the script is reachable. No such rule exists in this tree, and a `references/` file asserting a channel fact that is not true of the tree is the precise defect this card was filed for. ## What the repaid lines say ``` - ✓ 标签加法 `POST .../issues/{n}/labels`,定向删 `DELETE .../issues/{n}/labels/{name}`;加法优先。 - 标签/assignee 写恒经 `scripts/pm/label-write.mjs`:四步内建、回读、回退整组 PATCH 回传 assignees。 - ⛔ 永不 MCP `issue_write`(锁 1 已拒);会话分类器拒改动 ⇒ 无通道,交有通道席位立卡。 ``` Equal-line, and the ratchet decided the shape twice: 82 lines is the ceiling with **zero** headroom, and it also enforces a **120-byte per-line** budget that the first draft broke on all three lines (144B / 310B / 202B). The lines above are 111B / 119B / 114B. `node scripts/pm/check-skill-line-ratchet.mjs` exits 0. ## Design notes a reviewer may want - **`PUT .../labels` is never issued, in any spelling.** `check-whole-set-label-write.mjs` bans it over `scripts/**` and this file is in that root; the gate passes. The `PATCH /issues/{n}` fallback is a **different endpoint carrying the same hazard**, which is why it is reached only after a refusal, echoes assignees, and is always read back. - **A 403 with `x-ratelimit-remaining: 0` does not reach the fallback.** It exits 3. Rate-limit refusal binds the identity, every seat on this board shares that identity, and `rest-channel.md`'s own rule is 「换通道续写与重试同罪」 — so a fallback there would be the banned act, spelled as a recovery. Pinned in the self-test. - **A 404 on a directed DELETE is idempotent success**, not a refusal — the label is already gone. Pinned both ways: the plan skips a removal that is not in the current set, and a 404 from a removal that raced is read as success. - **The three vocabularies are imported from `check-half-states.mjs`, never restated**: `PM_EXCLUSIVE_STATE_LABELS`, `PM_STATE_CLAIM`, `PM_RESIDUE_LABELS`. A target carrying two ONE-OF states is refused before any write — H29's finding asked of the **target**, at the one moment the pair is still one keystroke from correct — with `--allow-two-states` as the declared exception that still prints what it let through. - **`concurrentAdds` is reported and is ⛔ not a mismatch.** Preserving another seat's additive label is the entire reason POST/DELETE come first; treating it as a failure would push a seat toward the whole-set write. ## Tests `pnpm check:pm-label-write` — `✓ label-write self-test: 64 cases pass across 8 batteries` (the union/difference arithmetic, the ONE-OF refusal, the idempotent DELETE, the stripped-underneath re-add-and-report, the mismatch exit, the rate-limit non-fallback, and the assignee echo on the whole-set PATCH). The fake board it drives models `PATCH /issues/{n}` **destructively** on purpose — a fake that merged instead would pass every assertion while the echo went untested. Batteries carry a declared floor and a verdict handshake, so "every case held" and "the cases never ran" cannot print the same line. CLI exits verified directly: usage 2, `--help` 0, missing token 3. ## Gates `node scripts/pm/dispatch-gates.mjs --commands` derived **71 families** from the four changed paths. All 71 were run with per-command redirect-then-capture, twice — once before the `origin/main` merge and once after, at `0e803e026` — and the recorded exit codes were fed back: ``` Run reconciliation — 71 derived, 68 run, 3 NOT-MEASURED, 0 UNRUN. ✓ dispatch-gates --ran: 71 derived famil(ies) accounted for — 68 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3). ``` **68 green at `0e803e026`.** The 3 NOT MEASURED are `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:type-check-debt` — every one exits **3, PREREQUISITE NOT MET**, refusing because it reads a full workspace build that is not on disk. ⛔ Exit 3 is neither a pass nor a finding, and none of the three is read as either here. This is a **declared narrowing, and the narrowing is proven**, not asserted: the three are matched by the `Build Core` job's `package.json` path filter, and nothing in this diff can move them — - `git diff --name-only origin/main...HEAD -- packages/` returns **0 files**; - the entire `package.json` delta is **one line added to the `scripts` map**, touching no `exports`, no `files`, no dependency; - so the built closure those three read is byte-identical to `main`'s. CI's `Build Core` runs them against a real build, which is the right place for them. Three others (`check:doc-formula-expressions`, `check:dts-closure`, `check:sourcemap-no-sources-content`) also exited 3 at first and **were** measured rather than declared: they needed only `@objectstack/formula` + `@objectstack/lint` built (`pnpm exec turbo run build --concurrency=2 --filter=@objectstack/formula --filter=@objectstack/lint`, through `scripts/pm/os-verify-lock.sh`, `VERDICT command-exit 0`), and all three exit 0 after it. `origin/main` moved to `a26a114d7` after the readings above; re-deriving against it returns the **identical** 71-family list, so the reading stands and the PR's own CI covers the merge. `check:nul-bytes` is green, and a direct `grep -naP` for control bytes over the four changed files finds none. ## 维护者速读(草稿) **改了什么。** 新增一个 PM 工具 `scripts/pm/label-write.mjs`,把「标签写恒四步」从一段规矩变成一个可执行程序;把 `references/rest-channel.md` 里那三行改成指向它。另加两处接线(`package.json` 别名、`lint.yml` 自检步骤)。共 4 个文件,不动任何包源码。 **为什么改。** 锁 1 把 `mcp__github__issue_write` 加进了 deny,而那个工具正是 `rest-channel.md` 给「通道关闭的席位」写的唯一退路。锁是对的,不该回滚;错的是没人把那句退路偿上。结果是一个席位无法认领、因此无法派发任何卡,只能在卡上写段散文。 **风险与代价(含回滚)。** 风险很低:新脚本没有任何调用方,不进 CI 的活路径(CI 只跑它的离线 `--self-test`),不碰任何发布物。回滚就是 revert 这一个 PR,无残留。唯一值得注意的代价:`rest-channel.md` 已到顶(82/82 行),以后再加行必须先删行。 **席位意见。** **你要做的。** 两件。① 这是受管面(`.claude/**`),只能你亲自合,或给一个授权的 APPROVED review 让队列落地;席位不翻 ready、不入队、不挂 auto-merge。② 上面「Permission rule」一节里的问题需要你一句话:要不要把 `Bash(node scripts/pm/label-write.mjs:*)` 和 `Bash(node scripts/pm/post-stamped.mjs:*)` 加进 `permissions.allow`。本席位没有加,因为改权限配置需要你本人的话,而不是另一个 agent 转述的话。加了以后,分类器关着的席位才真的能用上这个脚本;不加也不阻碍本 PR 落地,只是那半个缺口留着。 ## Acceptance notes Observed, ⛔ not filed, ⛔ not fixed here: - **The two remaining `issue_write` references are still standing, by dispatch.** `pm-dispatch/SKILL.md:378` and `.claude/skills/checklist-test/SKILL.md:123` both still name the denied tool. They are the **rules** layer rather than the 事实 layer, and the dispatching seat holds the follow-up governed card for them. This PR deliberately does not widen into them — one governed file is already one too many to mix. - **Whether the script is reachable from a classifier-closed seat is still unmeasured.** This seat's classifier allowed it; the filing seat's refused raw `curl`. Nobody has run this script from a session in that state, and the script cannot measure it about itself. That is the open half of the card, and the permission-rule question above is what would settle it. - **`references/rest-channel.md` is at its ceiling with zero headroom** (82/82 lines, 120 bytes per line). Any future line there has to buy its space by deleting one. Worth knowing before the next channel fact needs recording; not a defect. - **`scripts/pr-labels.mjs` and `scripts/pm/label-write.mjs` are now two additive label writers** with no shared code. That is correct today — one is a CI job writing PR size/path labels from a workflow, the other is a seat writing card state over the proxy, and they share neither transport nor vocabulary — but it is the kind of pair that drifts. No card: nothing is wrong yet, and a premature merge would couple a CI script to the PM vocabulary module. Clause-②: no --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: claude <noreply@anthropic.com>
…03 for the seat token, MCP rerun_failed_jobs 201 (objectstack-ai#18025) (objectstack-ai#18129) Fixes objectstack-ai#18025 One row in `.claude/skills/pm-dispatch/references/platform-readings.md`, in the Actions / re-run block, plus the ceiling increment it costs. No other file moves. Dev session `session_01DAcomhvR9kKizeYgg89Vo8` on branch `claude/issue-18025-platform-readings-rerun-channel` (worktree `objectstack-issue-18025`), off `origin/main` `ca788604`, BASE `ca7886047b27667122f9c2c44de3b6e67eb361fe`. No merge was needed: nothing landed on `main` under either path since the branch point. ## What landed `platform-readings.md` :285, placed immediately after the existing re-run pair (:283–:284): ```text - 读数 2026-09-13:席位 REST `/jobs/{id}/rerun` 403、MCP `rerun_failed_jobs` 201 ⇒ 重跑可做,锁 1 未禁。 ``` 117 bytes against the 120-byte line cap; one matter on the line; a dated 读数 in the file's own register voice. It records the CHANNEL split the card measured, and nothing else: - the seat's REST token (`GITHUB_TOKEN` through the session proxy) answers **403** `Resource not accessible by integration` on `POST /repos/objectstack-ai/objectstack/actions/jobs/103706765153/rerun`; - MCP `actions_run_trigger` with `method: rerun_failed_jobs`, `run_id: 34750740645`, answers **201** and re-queues the failed shards — attempt 2 visible on the run. The consequence rides the same line, which is why the row is worth a ceiling increment at all: **the one confirming re-run of a failure that is not this PR's is exercisable by the seat**, and lock 1 (PR objectstack-ai#18072, `7ef05f997`) does not take it away — `actions_run_trigger` is state-shaped, so it is not on the write-identity deny list. The standing text that read 「re-run is 403 to this seat」 was true of ONE channel, which is the error the triage comment 5654613958 generalises: 「the seat cannot do X」 is a claim about a channel, ⛔ never about the seat. Two things the 120-byte cap did NOT buy, declared rather than quietly dropped: - **the 403's message text.** Error prose is not pinned unless a consumer parses its original words; nobody parses this one, and the operative discriminant is the status pair 403 / 201. The full text stays on the card. - **the `POST` verb and the `/actions` path segment.** The path `/jobs/{id}/rerun` exists only as a POST, and :291 already carries the sibling `GET /actions/jobs/{id}/logs` spelling, so the family is legible from the neighbourhood. Spelling both would have cost 8 bytes the line does not have. ## Ceiling 453 → 454 — the standing exception, no decision card `scripts/pm/check-skill-line-ratchet.mjs`: the `platform-readings.md` ceiling moves by exactly the landed delta, and the raise is recorded as the FOURTEENTH `ruledRaises` record on the cross-file-move declaration, in the same shape as the thirteenth (added by objectstack-ai#18072 at `7ef05f997`) and quoting the same ruling verbatim and untranslated — pm-dispatch SKILL.md 〈分诊座位职责〉: > 唯一例外:`platform-readings.md` 增量抬上限到落地行数,免决策卡,记 `ruledRaises` 引常设裁决。条件:席位验收评论逐条核实、去重计数(候选/落地/已有/拒收)、一事一行、不计重排 ⛔ No other ceiling moves. ⛔ No decision card, because the exception says none is owed. **Density was MEASURED, not assumed** — the exception's own precondition and the 2026-08-17 no-re-wrap-funding rule: - of the file's **427** adjacent bullet pairs, **ZERO** merge within the 120-byte cap; the smallest merged width is **134 B**. - the two neighbours the row joins offer **37** spare bytes (:283, 83 B) and **6** spare bytes (:284, 114 B), against the row's **115** bytes of content after the `- ` marker. Neither can absorb it, and folding it into :283 would put a second matter on a line — 一事一行. ⇒ the row could not be paid in place, so the increment is +1 and the landed count is 454. ## Premise readings — all four hold, none falsified Taken on this worktree at `BASE` `ca788604` unless noted. | # | premise | reading | verdict | |:--|:--|:--|:--| | P1 | the file is 453 lines at ceiling 453 | `node scripts/pm/check-skill-line-ratchet.mjs` at 2026-09-14T03:42Z, exit 0: `✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/platform-readings.md is 453 lines (ceiling 453; headroom 0).` | **holds** | | P2 | no row states the channel split | `git grep -n -i -E 'Resource not accessible|actions/jobs|rerun_failed_jobs'` on the file at 2026-09-14T03:40Z returns exactly TWO hits, quoted below | **holds** | | P3 | the objectstack-ai#18085 footer fact is already on :333–:334 | quoted below, byte-for-byte from `BASE` | **holds — already present** | | P4 | no in-flight branch touches the file | scan at 2026-09-14T03:47Z, below | **holds** | **P2, every hit quoted:** ```text 283: - `rerun_failed_jobs` 复用原 run 的提交与合并 ref,不拿新 main 重算。 291: - ⇒ 两者都答不了到底挂在哪;`GET /actions/jobs/{id}/logs` 被出口代理拒绝,CONNECT 403。 ``` :283 states only that a re-run reuses the original run's commit and merge ref — a fact about WHICH TREE is re-run, silent on WHO may re-run it. :291 is a different endpoint (`GET .../logs`, not `POST .../rerun`) failing a different way (the egress proxy's CONNECT 403, not GitHub's `Resource not accessible by integration`). Neither says a re-run is unavailable, so the card's 「if the file already carries a row saying re-runs are unavailable, replace it rather than add」 branch does not fire: this is an ADD, and the two lit controls prove the grep was looking in the right place rather than returning a silent zero. **P3, already present, ⛔ not a second row:** ```text 333: - 裸 REST `PATCH /pulls` 追加一个裸页脚并保留既有 session-URL 页脚,差恰 58 字节。 334: - 同路送无页脚正文存回恰一条(平台裸形)⇒ 该格处方是不送页脚,⛔ 不是不重送正文。 ``` **P4:** `git ls-remote --heads origin` lists 1102 branches; a name grep for `reading|rerun|re-run|channel|18025` hits only `claude/issue-13326-platform-readings-family` (landed long ago), `claude/issue-10979-...`, `claude/issue-7018-...` and this branch. Every remote-tracking ref dated today (`2026-09-14`) was then checked directly — `objectstack-ai#18074`, `objectstack-ai#18037`, `objectstack-ai#18085`, `objectstack-ai#18061`, `objectstack-ai#18055`, `objectstack-ai#18060`, `objectstack-ai#18047`, `objectstack-ai#18069`, `objectstack-ai#17959` — and `git log origin/main..origin/BRANCH -- the-file` returns **0** commits for each.⚠️ Scope declared: that scan sees the refs this container has fetched, which is every in-flight seat branch in this shift but not a branch pushed from elsewhere and never fetched here. ## Verification counts for the seat's ACCEPT One matter per line, deduplicated, re-wraps not counted: | | count | what | |:--|--:|:--| | candidates | 2 | ① the re-run channel split (REST 403 / MCP 201); ② the PR-body footer reading from objectstack-ai#18085's dev report | | landed | 1 | ① as :285, 117 B | | already present | 1 | ② — :333–:334 carry it verbatim; refused as a duplicate rather than restated | | refused | 0 | — | ⇒ landed 1 = the ceiling delta 1 = 453 → 454. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `9303a7b7e` (no paths passed — the script derives its own change set: 2 paths vs merge base `ca7886047`, three-dot semantics): **39 families**. Every one run in the foreground with its exit code captured by redirect-then-capture BEFORE any pipe; **39 of 39 exit 0**. Reconciliation, `--ran` with the exit code recorded per family: ```text Run reconciliation — 39 derived, 39 run, 0 NOT-MEASURED, 0 UNRUN. ✓ dispatch-gates --ran: 39 derived famil(ies) accounted for — 39 run, 0 NOT-MEASURED (a DERIVED zero — all 39 recorded an exit code and none of them is 3). ``` One family needed a second pass: `pnpm --filter @objectstack/lint run check:doc-formula-expressions` first answered **exit 3 — PREREQUISITE NOT MET** (`@objectstack/lint` not built; nothing measured, ⛔ not a finding). Built under the shared verify lock (`OS_VERIFY_LOCK_SLOT=issue-18025`, `VERDICT command-exit 0 · held the lock 1s · waited 0s`) and re-run: **exit 0**. The reconciliation above is the post-fix record. Named explicitly by the dispatch, and the roster families whose baseline sits under a directory this diff is in — outside the derived 39, each run and each exit code captured the same way: ```text pnpm check:pm-governed-prose :: exit 0 node scripts/check-published-list-mirrors.mjs :: exit 0 node scripts/check-published-list-mirrors.mjs --self-test :: exit 0 node scripts/check-skills-token-ratchet.mjs :: exit 0 node scripts/check-skills-token-ratchet.mjs --self-test :: exit 0 ``` Rule ⑤ — this diff edits a gate script, so that script's own suite is owed beyond the derived families. `scripts/pm/check-skill-line-ratchet.mjs` has no `*.test.ts`: `git grep -l` over `*.test.ts` / `*.test.mts` / `*.test.mjs` / `*.spec.ts` returns nothing, and its suite IS its `--self-test`, wired into `pnpm check:pm-skill-ratchet` (in the 39, exit 0): `✓ check-skill-line-ratchet self-test: 157 cases pass.` Its siblings that read the same module — `check:ratchet-remedy-authority`, `check:pm-dispatch-gates` — are in the 39 and green. Line-ratchet verdict after the edit: ```text ✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/platform-readings.md is 454 lines (ceiling 454; headroom 0). ✓ check-skill-line-ratchet: cross-file move into .claude/skills/pm-dispatch/references/platform-readings.md: +11 (314→454, less 129 lines of ordinary ruled raise) against a net source decrease of 20 … ``` The move's own arithmetic is unchanged at +11 against −20, which is what the `ruledRaises` record is for. Control characters: `grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'` over both changed files is empty, and `pnpm check:nul-bytes` is in the 39 at exit 0. Every added prose line is ≤ 120 B (the added row is 117 B; the added ratchet comment lines are JS source, outside that cap). ⛔ Not measured here, by design: whole-farm CI. The derivation itself names 52 artifact-roster families, 11 declared-wide families, 14 pending-changeset families and 1 path-scheduled CI job as outside the derived 39 — CI owns those. ## Changeset None owed, and `skip-changeset` is the declaration rather than a shortcut. There is no `check-changeset-presence.mjs` in this tree; the changeset gate is `changeset-check` in `.github/workflows/pr-automation.yml`, and it reads **no path filter at all** — its only two exemptions are the `skip-changeset` label (re-read live, because the event payload's label snapshot is stale by construction) and the `changeset-release/main` branch pushed by `github-actions[bot]`. So a docs/tooling PR cannot be exempted by its paths; it has to carry the label. Nothing here publishes: both paths — `.claude/skills/pm-dispatch/references/platform-readings.md` and `scripts/pm/check-skill-line-ratchet.mjs` — are on the fast-track non-publishing list (`.claude/**`, `scripts/pm/**`), inside no released package's `files[]`. ## Acceptance notes - noted, not filed (承接者: the skills seat reviewing this PR): the register now carries THREE `/actions/jobs/{id}/...` readings across :283–:291 — re-run tree reuse, the re-run channel split, and the logs endpoint's proxy CONNECT 403. They are three separate matters and each is one line, so no line merges; the observation is only that a future 段落 boundary there would read better if the endpoint family were contiguous. Not a defect, not a contract breach, not an authoring trap ⇒ ⛔ no card. - noted, not filed (承接者: 无): the card body's own budget line reads 「449/449 — pay inside the file」, measured when it was filed on 2026-09-13T12:28Z. The file was 453/453 by the time of dispatch. Nothing to act on — the card's instruction was the ceiling DISCIPLINE, not the number — and no PR or person will read that line again once this lands. Clause-②: no — the diff widens no declared contract; it records a reading and pays its line. ## 维护者速读(草稿) **改了什么**:事实表 `platform-readings.md` 加一行(453 → 454),记一条读数:CI 失败 job 的重跑,席位自己的 REST 令牌回 403,而 MCP 的 `rerun_failed_jobs` 回 201 并把失败分片重新排队。顺带把 `check-skill-line-ratchet.mjs` 里这个文件的行数上限抬 1,按常设例外记一条 `ruledRaises`,引用裁决原话。 **为什么改**:此前的记录只说「重跑对席位是 403」,那是一个**通道**的事实,被当成了**席位能力**的事实。结果是:一条本来能执行的规则(CI 红了先做一次确认性重跑)被当作做不到,objectstack-ai#18010 那张卡直接把它写进了阻塞原因,还惊动维护者问「17990 你自己不能解决吗」。这一行把通道和能力分开,下次没人再为这件事找人点一下。 **风险与代价(含回滚)**:只动文档与一个门禁脚本的常量,不发布任何包,不改运行时行为。代价是事实表长 1 行(每个座位每次会话都要读它,这就是上限存在的理由);本次为此实测了密度——全文 427 对相邻条目没有一对能合并进 120 字节,所以省不出来。回滚 = revert 本 PR 的那一个 commit。 **席位意见**:(留空,定稿成评论) **你要做的**:这是受管面(`.claude/**` + `scripts/pm/**`),按 Prime Directive objectstack-ai#14 只能由你手动合并 —— 席位不合、不进队列、不挂 auto-merge。你要确认的是两件事:① 这一行的读数属实(卡面 objectstack-ai#18025 有原始测量表);② 抬 1 行的上限动作走常设例外、不另开决策卡,是否照你的意思。 --- _Generated by [Claude Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_ Co-authored-by: Claude <noreply@anthropic.com>
…g 5 unchanged (objectstack-ai#18125) (objectstack-ai#18128) Fixes objectstack-ai#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 objectstack-ai#18072, commit `7ef05f997`, 「`batch` 默认 2」). The maintainer ceiling 5 stays. Ruling C (objectstack-ai#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.md` line 60, the `batch:N` row of the 〈入口与角色〉 argument table: - before: `| batch:N | 同时在飞的 dev 上限 | 默认 2;N 的维护者天花板 5 |` - after: `| batch:N | 同时在飞的 dev 上限 | 默认 3;N 的维护者天花板 5 |` (The row is quoted with `N` standing 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 in `git diff --stat ca78860 06c1015`. 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) - **P1 holds** (read 2026-09-14T03:51Z): SKILL.md line 60 read 默认 `2`; `git grep -n` for the literal 默认 followed by a backticked 2, over `.claude AGENTS.md CLAUDE.md`, returned exactly one hit, `.claude/skills/pm-dispatch/SKILL.md:60`. - **P2 holds** (read 2026-09-14T03:51Z): `references/core-rules.md` is 151 lines and states no batch DEFAULT. `grep -n batch` hits: 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 about `batch`. 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. - **P3 holds** (read 2026-09-14T03:51Z): SKILL.md is 812 lines at ceiling 812; `sed -n '733,754p' SKILL.md | md5sum` = `3327d02c56f8a0eca88569dad2270f32`. Same two readings on HEAD `06c10152b` after the edit: 812 lines, md5 `3327d02c56f8a0eca88569dad2270f32`. - **P4 holds** (read 2026-09-14T03:52:44Z–03:53:16Z): `git ls-remote --heads origin` listed 1106 heads; the 6 `claude/issue-18xxx` heads were compared to `main` through REST `GET .../compare/main...BRANCH` and all 9 open PRs through REST `GET .../pulls/N/files` (page count 9, fewer than 100, so the listing is complete): zero hits on `pm-dispatch/SKILL.md` or `core-rules.md` in any of them. Scope declared: open PRs plus this wave's `issue-18xxx` heads; the 62 older keyword-matched heads (seat session branches `pm-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 base `ca7886047`; the `--repo` assertion 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 0 - `pnpm 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 0 - `node scripts/check-closing-keyword-parity.mjs` and its `--self-test` — exit 0 / 0 - `node 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/formula` is not built」 (not a measurement); after `pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under `os-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 each Reconciliation (`--ran` with `COMMAND :: exit CODE` lines, 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-test` exit 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.mjs` on 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 from `eslint.config.mjs`: every `files:` block matches only `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` (six blocks; `COMMENT_SWALLOW_FILES` is the same glob), so a `.md` file is outside the linted population. Invariance: the config never enables type-aware linting (its own line 328: no `parserOptions.project`, no typed rules), so a one-line edit to an unlinted file cannot move the verdict of any linted file; the repo-wide `pnpm lint` is CI's run. `dispatch-gates --tier` was read for the claim; its model line is not reproduced here (model-free rule). ## Changeset None owed. The `Check Changeset` job (`.github/workflows/pr-automation.yml`, job `changeset-check`) reads exactly two exemptions — the `skip-changeset` label 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's `files[]` (fast-lane class per the dev definition). The label is applied with the additive `POST .../issues/N/labels` and read back; the read-back is recorded in the report comment on objectstack-ai#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 - Dispatch vs dev definition: the dispatch asked for a two-line 维护者速读; the dev definition sets five paragraphs, and the definition wins on conflict, so the five-paragraph form is used with the two-line business content inside it. Noted, no card. - noted, not filed: the card body says the core-rules mirror line changes "likewise"; the tree says there is no such line (P2 above). The claim comment already carries the correction. 承接者:无 (nothing to land). - noted, not filed: the first `check:doc-formula-expressions` run exited 3 on a fresh worktree because `@objectstack/formula` and `@objectstack/lint` are not built by `pnpm install`; the gate's own text says so and prescribes the build. Behaviour by design (Absence must be loud), not a defect. 承接者:无. - Out-of-scope findings of the three filing classes: none. - Clause-②: no --- _Generated by [Claude Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_ Co-authored-by: claude[bot] <claude[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
…— REST-only content writes, the ACCEPT refuses MCP writes, a stale shared checkout re-seats (objectstack-ai#18205) (objectstack-ai#18216) Fixes objectstack-ai#18205 ## The maintainer's order (verbatim, ⛔ not translated) 「派发令硬性指定 REST 通道:建议改。 你应该修改skills吧?」 and 「不只是 objectui 仓库,其他第三方元数据app仓库怎么办」 — the maintainer, 2026-09-14, in the skills seat's chat (audit comment 5666103417 on the card). Lock 1 (PR objectstack-ai#18072, `7ef05f9973`) stays in force as the channel rule; this PR corrects its identity claims and does not weaken it. Landing is governed by ruling C (objectstack-ai#17971): 「C. approve 后不管后续改动都由席位落地:」 — this PR is a DRAFT and stays one; the seat does the four-piece after ACCEPT and lands only after an authorized approval. ## What changed — equal-line under every ratchet, every touched line at or under 120 bytes Line numbers are on this branch at `7103d0b09f`; B = bytes of the line as stored. Readings taken 2026-09-14T16:27Z. | file | line | after | B | |---|---|---|---| | `.claude/agents/os-dev.md` | :51 | GitHub 写一律走 REST 代理(`curl` 带 `GITHUB_TOKEN`);归属 = 文本里的 session ID,非 `user.login`。 | 116 | | `.claude/agents/os-dev.md` | :53 | ⛔ 不用 MCP GitHub 写工具;令牌按会话定:installation ⇒ `claude[bot]`,user-to-server ⇒ 用户。 | 115 | | `.claude/agents/os-dev.md` | :369 | `"mcp_calls": "N — MCP GitHub calls with tool names; a write tool in the list = this report is refused",` (the file spells the placeholder N inside angle brackets, as the template always has) | 109 | | `SKILL.md` | :91 (new) | 同读 harness 载入面 `.claude/{settings.json,agents/*.md,hooks/*}` 的最新触碰是否已在共享检出 HEAD。 | 120 | | `SKILL.md` | :92 (new) | 否 ⇒ 收班、换新会话再派,⛔ 不推进共享检出;读数走 `scripts/pm/check-harness-current.mjs`。 | 115 | | `SKILL.md` | :97 | 用户账号仅三用:assignee、授权批准、维护者亲手;批准账号永不跑席位或作其关联用户。 | 117 | | `SKILL.md` | :98 | 内容写只走 REST 代理,⛔ 无 MCP 写;`user.login` 记令牌不记席位,归属 = 文本里的 session ID。 | 116 | | `SKILL.md` | :195 | 新仓登记是一张清单:座位贴、标签、类别归属、门禁盘点、写身份锁移植(deny + hooks)。 | 116 | | `SKILL.md` | :537 (merged) | 终报要求随派发词带一句:只收机器可核字段(gates / line_budget / deviations / files_changed)。 | 113 | | `SKILL.md` | :538 (new) | 派发令恒带 `Writes:` 行:只走 REST 代理、写预算(端点清单)、`mcp_calls` 计数,dev 两数都报。 | 117 | | `SKILL.md` | :556 (merged) | `mode:cloud` 只保留给 L/XL、活过 PM 会话的工作、浏览器/dogfood 验证;build 重的 M 卡逐卡判。 | 118 | | `SKILL.md` | :567 (merged) | 标记两种拼写等效(HTML 注释形、首行 `os-dev-report`);⛔ 永不把没收到失败通知读作还在跑。 | 120 | | `SKILL.md` | :602 (new) | `mcp_calls` 点名写工具(`settings.json` deny 清单 + `update_pull_request`)⇒ 拒收,⛔ 不带注放行。 | 115 | | `SKILL.md` | :775 (merged) | 终报 JSON 的权威形状住 `.claude/agents/os-dev.md` 终报消息节,⛔ 本文不抄第二份。 | 104 | | `references/core-rules.md` | :25 | 用户账号仅三用:assignee、授权批准、维护者亲手;写只走 REST 代理,署名随令牌非席位。 | 115 | | `references/platform-readings.md` | :129 | 容器 curl 的 REST 通道令牌按会话定:installation(`claude[bot]`)或 user-to-server(用户),core 15,000/时。 | 120 | | `references/rest-channel.md` | :54 | 直合仓 `PUT .../pulls/{n}/merge`;actor 记通道令牌:REST 按会话为 `claude[bot]` 或用户,MCP 恒用户。 | 118 | `SKILL.md` and `references/` are `.claude/skills/pm-dispatch/`. The two os-dev.md rule lines carry their 3-space list indent inside the count. - (a) facts and invariants: content writes go only through the REST proxy; ⛔ no MCP content write; `user.login` on a write names the channel's token — installation ⇒ `claude[bot]`, user-to-server ⇒ the bound user — per session, not the seat's to choose, never the actor; attribution is the session ID in the text carrier. os-dev.md :51/:53, SKILL.md :97–:98, core-rules :25, platform-readings :129, rest-channel :54. - (b) dispatch order and acceptance: SKILL.md :538 — every dispatch order carries a `Writes:` line (REST proxy only, the write budget as an endpoint list, `mcp_calls` counted, the dev reports both numbers); SKILL.md :602 — a report whose `mcp_calls` names a write tool (the `settings.json` deny list plus `update_pull_request`, which that list does not carry) is refused, ⛔ not accepted with a note; os-dev.md :369 says the same from the dev side. - (c) propagation: SKILL.md :91–:92 beside the three-charter-file reading — at fire time the seat also reads the latest `origin/main` touch of `.claude/settings.json`, `.claude/agents/*.md`, `.claude/hooks/*` against the shared checkout's HEAD; a touch not in HEAD ⇒ close the shift and re-seat in a fresh session before the next dispatch, ⛔ never advance the shared checkout in place. The reading is `scripts/pm/check-harness-current.mjs` (59 lines, git only, seat-side, ⛔ not wired into CI): exit 0 CURRENT, 1 STALE (each stale path with its touch), 2 UNDECIDED (shallow-clone negative that is not date-decided). - (d) fleet: SKILL.md :195 — the new-repo registration checklist gains the write-identity locks port (deny + hooks). The four repos without a port today: `cloud`, `objectos`, `hotcrm`, `www.objectos.ai`. Named here only; no cards from this PR — the seat that can reach each files its card (recorded on objectstack-ai#7623 until then). - (e) the managed-settings fact row: not landed in platform-readings (454/454, no payable pair in that file without deleting a ruled clause); recorded under Acceptance notes below with the doc sentences verbatim. ## Premise readings (falsified against the tree before writing; all UTC) - P1 (16:12Z, base `af3add1601`): all seven quoted lines read exactly as the dispatch quotes them — os-dev.md :51 「- GitHub 写一律走 REST 代理(`curl` 带环境 `GITHUB_TOKEN`),署名恒 App 的 `claude[bot]`。」 and :53 「- ⛔ 不用任何 MCP GitHub 写工具:用户账号署名,封号即隐;⛔ 不枚举板面、不宽词搜。」; SKILL.md :95 「- 用户账号仅三用:assignee、授权批准、维护者亲手;⛔ 席位与 dev 永不以用户账号写内容。」 and :96 「- 内容恒经 REST 代理(`claude[bot]`);批准账号永不跑席位、不作席位 claude.ai 的关联用户。」; core-rules :25 「- 用户账号仅三用:assignee、授权批准、维护者亲手;写恒经 REST 代理;批准账号永不跑席位。」; platform-readings :129 「- 容器 curl 的 REST 通道 = App installation token,core 15,000/时,与 GraphQL 池独立计。」; rest-channel :54 「- 直合仓另有 `PUT .../pulls/{n}/merge`;ccr 的 timeline actor 记 `claude[bot]`,MCP 记席位账号。」. Holds. - P2 (16:11:37Z): `git -C /home/user/objectstack rev-parse HEAD` = `84e6b05b6d295f1c744d236921300f447cf7791e`, `log -1 --format=%cI` = `2026-09-13T06:14:23+00:00`; `merge-base --is-ancestor 7ef05f9 HEAD` exit 1. Control legs for the negative (shallow checkout, `rev-list --count HEAD` = 4024): `is-ancestor 84e6b05 HEAD` exit 0 and, twelve commits deep, `is-ancestor d88a47d HEAD` (committed 2026-09-12T22:39:41Z) exit 0 at 16:12:17Z; the negative is also date-decided — `7ef05f9973` was committed 2026-09-13T23:27:23Z, seventeen hours after the shared HEAD. `grep -c 'mcp__github__' .claude/settings.json`: shared 1, worktree 15. Holds — with one sharpening: the shared file's single hit is a PreToolUse hook matcher (`mcp__github__enable_pr_auto_merge|mcp__github__merge_pull_request`), and the shared file has no `permissions.deny` key at all (`grep -c '"deny"'` = 0 against 1 on `origin/main`), so in this session no deny list was ever loaded, not a pre-lock-1 one. - P3 (16:12:09Z, `origin/main` = `af3add1601`): the grep hits are SKILL.md :31 (never edit the shared checkout), :160 (never verify main from its worktree), :506/:606/:774 (paths named as protocol/governed surfaces or as the report authority); core-rules :44/:149 (the same two); dispatch-runbook :215 (frontmatter `model:` exemption); platform-readings :30–:33 (merge-driver registration per clone), :214 (deny documented-not-measured), :344/:373/:415 (footer, transcript, sleep) and :413 (shallow-clone deepen); app-platform-boundary :60 and contract-review :57 (the word harness in other senses). None prescribes re-seating when a harness-loaded file lands after the session's clone; SKILL.md :86–:90 re-READS the three charter files, and reading does not reload the harness. Control `git grep -c '收班简报'` on SKILL.md = 5. Holds. - P4 (16:12:09Z): `派发令` hits are SKILL.md :153/:164/:214/:433/:444/:461/:478/:540/:543/:715/:790 and dispatch-runbook :184/:205/:232/:236 — all rule lines about what the order carries; 〈模板与表〉 holds only the claim-comment template. No fixed shape exists, so the `Writes:` mandate lands as a rule line (SKILL.md :538). Holds. - P5: os-dev.md :369–:370 are the `mcp_calls` / `api_writes` report fields; :57 already orders both counts. Holds; :369 rewritten, :370 untouched. - P6 (16:17:26Z on the base): `check-skill-line-ratchet` exit 0 with every one of the five files at its ceiling (812 / 403 / 454 / 82 / 151, headroom 0, table-row pins 342 / 0 / 0 / 0 / 0); `check:skill-frame-sync` exit 0; `git ls-remote --heads origin` matched only this branch for issue-17497/18205/18181/18158. Holds. - P7: `scripts/pm/dispatch-gates.mjs` is untouched; objectstack-ai#14290's `Restart-touch` surface is left alone; the seat-side check is the sibling file `scripts/pm/check-harness-current.mjs`. ## The mechanism, measured in this session (16:25Z) - The os-dev.md this dev runs under is the shared checkout's copy: its line 「通道先探后选…」 is in `git show 84e6b05:.claude/agents/os-dev.md` (1 hit) and absent on `origin/main` (0); `api_writes` is the inverse (0 in the old copy, 2 on `origin/main`); control `Worktree-first` 1 / 1. - `node scripts/pm/check-harness-current.mjs` from this worktree (shared checkout resolved through `--git-common-dir`): exit 1 — `.claude/settings.json` and `.claude/agents/*.md` latest touch `7ef05f9973` NOT in shared HEAD `84e6b05b6d`, `.claude/hooks/*` latest touch `d79f249915` (2026-09-12T09:41:28Z) in HEAD. `--shared /home/user/objectstack-issue-18205`: exit 0 CURRENT at `af3add1601`. `--shared /nonexistent`: exit 2. ## Gates (final head `7103d0b09f`, 16:28Z–16:37Z) `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths; change set derived from git, 6 paths, committed 6 / working tree 0 / untracked 0) printed 40 commands. All 40 run with redirect-then-capture, each recorded as `CMD :: exit N`: - 39 exit 0 on the first pass, including `check:pm-skill-ratchet`, `check:skill-frame-sync`, `check:pm-governed-prose`, `check:pm-skill-id-lint`, `check:nul-bytes`, `check:agent-model-declared`, `check:entry-guard`, `check:parse-guard`, `check-self-test-wired`, `check-scripts-symbol-anchors`, `check:commit-card-trailers`, `check:pm-governed-merges`. - `pnpm --filter @objectstack/lint run check:doc-formula-expressions` first read exit 3 = PREREQUISITE NOT MET (compiled `@objectstack/formula` and `@objectstack/lint` absent in the fresh worktree; the gate says "Nothing was measured"). Prerequisite cleared under the verify lock — `os-verify-lock.sh -c 'pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --concurrency=2'`: VERDICT command-exit 0, held the lock 172 s, waited 0 s — then rerun: exit 0, "22 record-scoped formula example(s) across 438 files / 1377 TS blocks judged clean by @objectstack/formula." - Reconciliation: `dispatch-gates --ran ran.list --repo objectstack-ai/objectstack` at 16:37:21Z on `7103d0b09f`: "Run reconciliation — 40 derived, 40 run, 0 NOT-MEASURED, 0 UNRUN." (exit 0; the derived 40 is recomputed by the tool from the tree, never read back from the record). - Ratchet on the final head: every one of the five files at its ceiling, headroom 0, pins unchanged (SKILL.md widest table row 342). First pass on the working tree had caught os-dev.md :51 at 122 B (the list indent was outside the draft measurement); fixed in the second commit to 116 B. - Lint, narrowed and measured: the checked population is eslint's own config (`files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`, which covers `scripts/pm/*.mjs`); the only non-markdown file in the diff is `scripts/pm/check-harness-current.mjs`; `eslint --no-inline-config --format json` on it: 1 file, 0 errors, 0 warnings (exit 0); invariance: `eslint.config.mjs` states it "never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file", so a one-file addition cannot move any untouched file's verdict. The repo-wide `pnpm lint` is CI's run. - Not run here, declared to CI: nothing else — the diff touches no package, so there is no ① build closure or ② package test suite; `.claude/**` and `scripts/pm/**` publish nothing, so `skip-changeset` applies (fast lane: `.claude/**` · `scripts/pm/**`). The seat writes the label; this container does not. ## Density paid inside each file - SKILL.md (four new lines, four merges): 终报要求 + 机器可核字段 → one line (drops only the implied 「⛔ 复述 PR body 叙事」); `mode:cloud` + build-heavy-M → one line (drops 「必须」); marker spellings + missing-notification → one line (the dropped 「仅凭 HTML 注释形式缺失永不读作报告未达」 is what 「两种拼写等效」 states); 报告契约 authority + no-second-copy → one line (drops the implied 「字段与拼写以那里为准」). - os-dev.md :53 drops 「⛔ 不枚举板面、不宽词搜」 — :50 (「⛔ 不扫 open issues、不拉板」) and :54 (single-card reads only) already carry it. - core-rules :25 (the compressed mirror) now carries the channel + identity reading; the approval-account clause could not fit beside it in 120 B and stays where it is authoritative, SKILL.md :97. ## 维护者速读(草稿) - 改了什么:① 署名跟令牌走、不跟账号走 —— GitHub 上写回读到的 `user.login` 只说明这条会话的令牌是 App 的还是用户的,不说明是谁在写;身份看文本里的 session ID。② dev 报告里出现任何 MCP 写工具即拒收,不带注放行。③ harness 读的文件(`settings.json`、agents、hooks)在 main 上动了而共享检出没跟上时,席位收班、换新会话再派,永不原地推进共享检出。 - 为什么改:锁 1 落地后 objectui 仍出现一条经 MCP 建的 PR,原因是运行中的会话只在克隆那一刻读一次这些文件;同时章程里「署名恒 `claude[bot]`」被四个会话的实测证伪。 - 风险与代价(含回滚):纯规则文本 + 一个只读 git 的席位脚本,零 CI 接线;回滚即 revert 这一个 PR。代价是每次开轮多一次 git 读数,与一次可能的换会话。 - 席位意见:(留空) - 你要做的:一个动作 —— 批准这份草稿,席位落地。 ## Acceptance notes - Item (e), recorded here instead of a fact row: code.claude.com/docs/en/settings 「Settings in cloud sessions」 states, verbatim: "**Shared project settings** (`.claude/settings.json`): read, because the file is part of the clone." / "**User and project local settings** (`~/.claude/settings.json` and `.claude/settings.local.json`): not read. Both stay on your machine, and the local file isn't in the clone." / "**Managed settings**: only server-managed settings reach a cloud session; a `managed-settings.json` file or MDM profile on your device doesn't." And code.claude.com/docs/en/server-managed-settings: "Server-managed settings are available for Claude for Teams and Claude for Enterprise customers." So a personal account has no managed tier, and the maintainer-level lever the card names (a user-level file written by the environment setup script inside the cloud VM) is not the file those sentences describe — the docs speak of the file on the user's own machine; whether a user file written inside the VM is read is not stated. Bearer: the round report (the card already routes the lever there). - The card's "1 `mcp__github__*` entry (pre-lock-1)" in the shared checkout's settings is a hook matcher, not a deny entry; the shared file has no `permissions.deny` at all. Whether a deny list loaded from the clone takes effect in a cloud session therefore remains documented-not-measured (platform-readings :214 stands); the first session cloned after `7ef05f9973` measures it by tool-table absence. Bearer: the skills seat's next fresh session. - `mcp__github__update_pull_request` edits PR bodies and titles through MCP and is not in `.claude/settings.json`'s deny list; the ACCEPT line names it explicitly for that reason. Reported in the dev report for the seat to file or fold (⛔ not changed here: `.claude/settings.json` is outside this card). - The script has no `--self-test` on purpose: it is not CI-wired (`check-self-test-wired` populates from workflows), it exports nothing (`check:entry-guard` rule two does not apply), and its three readings above are the measurement. Bearer: whoever wires it into a workflow later owes the self-test then. - `objectstack-ai#18181 remains open` (os-dev.md :287 label write is not addressed here); `objectstack-ai#18158 remains open` (the identity reading itself); the objectui port (PR objectstack-ai#9448) is untouched. Clause-②: no --- _Generated by [Claude Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #18068
Dev session
session_01DAcomhvR9kKizeYgg89Vo8on branchclaude/issue-18068-write-identity-locks(worktreeobjectstack-issue-18068), offorigin/main6d64785, merged e248c4d before opening (no incoming commit touched these files). One commit per lock; each quotes its ruling.Rulings (verbatim, untranslated)
platform-readings.md增量抬上限到落地行数,免决策卡,记ruledRaises引常设裁决。条件:席位验收评论逐条核实、去重计数(候选/落地/已有/拒收)、一事一行、不计重排」 — the +2 on platform-readings.What landed
.claude/settings.json:permissions.denywith the 14 content-writingmcp__github__*tools the card lists;allowand hooks untouched;JSON.parsepasses. Docs reading (code.claude.com/docs/en/permissions and /settings): rules evaluate deny, then ask, then allow; a deny at any scope beats an allow at any scope;mcp__server__toolis the per-tool spelling; the shared.claude/settings.jsonis read in cloud sessions and deny needs no workspace trust — lock 1 is ENFORCED, and a denied tool is removed from the roster. Recorded as two 文档载明未实测 lines inreferences/platform-readings.md(references tier, declared): 451 → 453, THIRTEENTHruledRaisesrecord; candidates 2 / landed 2 / already present 0 / refused 0, one matter per line..claude/agents/os-dev.md403/403: :51–:58 replaced by the REST-proxy write rule (curl+ environmentGITHUB_TOKEN, authoredclaude[bot]), the four-write budget, ⛔ no MCP GitHub write tool and no board enumeration, payload-or-single-card reads, findings reported for the seat to file, zero writes outside the budget (no PR-bodyPATCH), the rest-channel pointer (kept), andapi_writes+mcp_callsin the report; the report template gains"api_writes". In place, net 0: the control-word rule now sits under rule 6; resource rule 6 routes late results to the report; the label-write fallback no longer prescribes an MCPissue_write; theout_of_scope_findingsexample no longer shows a dev-filed card number.3」 → 「默认2」; ceiling5unchanged; core-rules :11 states no default, so nothing mirrored.3327d02c56f8a0eca88569dad2270f32unchanged.Executable criterion, BASE 6d64785 → HEAD 7e1aeca
grep -c mcp__github__issue_write .claude/settings.json0 → 1 (insidedeny); SKILL.md 「默认2」 0 → 1 and 「默认3」 1 → 0; os-dev.mdapi_writes0 → 2 (rule + template),search_issues1 → 0; SKILL.md 「批准账号」 / 「永不跑席位」 0 → 1, core-rules 「批准账号」 0 → 1. Lit controls unchanged: 「每个方案必须沿四条固定评估轴分析」 1 → 1, 「一座位一车道双射」 1 → 1 in both files, os-dev.mdmcp_calls2 → 2.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat 88e0610: 42 families, every one exit 0 in the foreground with the code captured before any pipe;--ran: 42 derived, 42 run, 0 NOT-MEASURED, 0 UNRUN.check:doc-formula-expressionsfirst answered exit 3 (PREREQUISITE NOT MET, lint/formula unbuilt) — built under the verify lock (187 s held) and re-run: exit 0. Rule ⑤ for the ratchet-script edit: its--self-test(insidecheck:pm-skill-ratchet),check:ratchet-remedy-authority,scripts/check-published-list-mirrors.mjsandscripts/check-skills-token-ratchet.mjsall exit 0. Re-run at 7e1aeca after the merge:pm-skill-ratchet,pm-skill-id-lint,skill-frame-sync,pm-governed-prose,nul-bytes,agent-model-declaredexit 0. Every added prose line ≤ 120 B (the one longer added line is inside the report's JSON fence, structurally exempt); SKILL.md over-120 baseline 23 → 23; control-character scan empty.skip-changeset: nothing published moves (.claude/**,scripts/pm/**only).Deviations, declared
PATCH; the rule says so explicitly and routes late gate results to the report comment. Allowing a body refresh would be one clause on that line, the seat's call.Acceptance notes
mcp__github__update_pull_request_branchwrites merge commits, andrequest_copilot_reviewwrites a review request, yet neither is on the card's list, so both stay allowed.维护者速读(草稿)
改了什么:四道机制锁。① 仓库的 Claude 设置里禁掉所有"以用户账号写内容"的 MCP GitHub 工具(建 issue、开 PR、评论、审查、推文件、合并等 14 个),状态类与只读工具照旧;② 开发 agent 对 GitHub 的写只走 REST 代理(署名
claude[bot]),预算固定四笔,报告新增api_writes供席位核对;③ 并发默认 3 → 2,天花板 5 不变;④ 写明用户账号只做三件事(assignee、授权批准、维护者亲手),批准账号永不跑席位,内容身份只认正文里的 session ID。为什么改:今天的封号事故证明,用 MCP 工具写的内容署在关联用户名下,用户一被停,内容整批消失;走 REST 代理的内容署在 App 名下不受影响。并发只是放大器,身份才是被封的对象。
风险与代价(含回滚):deny 名单在会话启动时读入,已开的会话不受影响;席位仍可用
update_pull_request等状态工具翻 ready、挂 auto-merge。回滚 = revert 本 PR 的任一 commit(每锁一个 commit,互不依赖)。platform-readings 上限 +2 走常设例外,不另开决策卡。席位意见:(留空,由席位定稿)
你要做的:一个动作 —— 在本 PR 上给出授权批准;受管面,席位按裁决 C 落地。
Generated by Claude Code