Repository navigation
pm-dispatch: three landings the seat decides on its own record (ruling 1A, 2A, 3A) - #19970
Conversation
landing-operations.md gains the confirmation rule for a DELIBERATE CORRECTION red from check-empty-changeset (a same-head at-tier review PASS record naming the corrected note and judging each rewritten sentence is the confirmation; enqueue under the three-condition red-by-design path, gate unchanged) and a receipted, once-only re-queue for a first ejection on a new queue signature (import closure disjoint, queue base green on the same shard, timeout first error; a second ejection on the same signature stops). review-checklist.md gains the seat's own-review skip-changeset application when a dev's label write is refused by its classifier, with the refused command recorded in the dev report's deviations. Paid under the line ratchet by deleting duplicate statements: three index lines restating sections A, C and D of landing-operations.md, and two review-checklist.md lines already stated in execution-duties.md. Both files stay at their ceilings (101/101, 77/77). Claude-Session: https://claude.ai/code/session_01A22sUB3mUWs6M36VgfijBq Co-authored-by: Claude <noreply@anthropic.com>
|
Basis: the seat read this PR's changed files against the diff, not the report — Generated by Claude Code |
…firmation execution-duties.md :175 said a PR that edits an existing changeset is confirmed by the maintainer's verbatim word or a provenance paraphrase. Under the landing rule this branch adds, a same-head at-tier review PASS record is that confirmation for the DELIBERATE CORRECTION class, so the review-time line now names it first. In-place, same line count (183/183). Claude-Session: https://claude.ai/code/session_01A22sUB3mUWs6M36VgfijBq Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
… schedule trigger as leaving it absent (objectstack-ai#19900) (objectstack-ai#19991) Part of objectstack-ai#19900 Clause-②: no ## The pending release note this corrects This PR edits one sentence of `.changeset/19846-automation-caller-param-keys.md`, the pending note for `AutomationContext.callerParamKeys` (PR objectstack-ai#19899). The note's frontmatter covers `@objectstack/spec`, `@objectstack/runtime` and `@objectstack/service-automation`. Nothing else in the note changes, frontmatter included (1 line changed: +1 / −1). **Before:** > Record-change, schedule, time-relative and webhook triggers, and code calling `execute` directly, leave it absent. **After:** > Record-change, time-relative and webhook triggers, and code calling `execute` directly, leave it absent; the schedule trigger, whose run has no caller, states an empty list (objectstack-ai#19900). **What made it false:** PR objectstack-ai#19982, landed as `ae7a35a63b`. Since that commit, `ScheduleTrigger` sets `callerParamKeys: []` on every run context it builds, so the schedule trigger no longer leaves the key absent. PR objectstack-ai#19982's own changeset (`.changeset/19900-schedule-caller-param-keys.md`) reaches only the `@objectstack/trigger-schedule` and `@objectstack/spec` CHANGELOGs. The `@objectstack/runtime` and `@objectstack/service-automation` CHANGELOGs would publish this sentence unchanged. ## The gate is expected to stay red `node scripts/check-empty-changeset.mjs --base origin/main` exits 1 on this head. This is a DELIBERATE CORRECTION of a foreign pending note, and the gate stays red on it by design until a person confirms the rewritten sentence (the seat's ruled path: ruling D on objectstack-ai#17712, and the landing rule added in objectstack-ai#19970). Its output begins: ```text Diffing HEAD from ae7a35a (merge base with origin/main). ✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added). This PR changes a changeset it did not add: .changeset/19846-automation-caller-param-keys.md present on the merge base and CHANGED by this PR -- this is somebody else's release note ``` The DELIBERATE CORRECTION remedy it prints is: "do NOT restore it -- say so on the PR and get it confirmed". This section is that statement. ## `skip-changeset`: not applied Two readings, and they agree. (1) This diff changes release-note text that `changeset version` will compile into three published CHANGELOGs, so it is not a diff that publishes nothing. (2) The standing rule for a PR that edits an existing `.changeset/*.md` is never to apply `skip-changeset`: the red gate is correct, and the confirmation goes through the PR text. ## Gates (head `465a0f9342`) `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 19 commands from the change set (merge base `ae7a35a63`). All 19 ran with their exit codes recorded. 18 exited 0, and `check-empty-changeset.mjs --base origin/main` exited 1, as expected above. `pnpm check:changeset-gate-self-tests` exited 0. The `--ran` reconciliation reads: 19 derived, 19 run, 0 NOT-MEASURED. --- _Generated by [Claude Code](https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF)_ Co-authored-by: Claude <noreply@anthropic.com>
… of an array insert and a predicate update (objectstack-ai#19988) Fixes objectstack-ai#19950 Fixes objectstack-ai#19964 Clause-②: no (narrowing) ## What this fixes A row-level security `check` (declared on the policy, or defaulted from its `using`) is the write-side half of the policy: a row the check refuses is never stored (ADR-0058 D4: "on the write pre-image path that already exists for by-id writes … and on the AST-injected bulk path"). The write gate enforced it for a single-row insert and a by-id update, and not for the two multi-row write shapes: - **objectstack-ai#19964, array insert.** Step 3.6 excluded an array payload, so no judgement was installed and every row was stored unjudged, including under configurations that refuse every single-row insert. - **objectstack-ai#19950, predicate update** (`multi: true`, no row address). Step 3.6 skipped the new-row check and logged "governed by the using-scoped where". A policy that declares only `check` scopes nothing, and a scoped `where` says nothing about the new row in any case. The skip was unconditional, so it also covered a declared `check` that differs from `using` and a `check` defaulted from `using`. Both shapes are now judged row by row with the existing refusal (`PERMISSION_DENIED` / 403, nothing stored). One failing row refuses the whole write. The judgement is the existing `satisfiesCheck` over `matchesFilterCondition`: no predicate is compiled differently, and nothing is lowered into the `where`, so no compile surface moves. ## Landing: two packages, and why the engine is one of them - `packages/plugins/plugin-security/src/security-plugin.ts`, step 3.6 plus the seam declaration and the post-`next()` fail-closed guard (generalised from "the insert" to the operation). The by-id update branch is unchanged. The `explainAccessForCaller` wiring is not touched. - `packages/objectql/src/engine.ts`, the predicate-update branch of `update()` (`domain:engine`), plus the seam's doc comments. **Why the producer is the engine (measured, not assumed):** the rows a predicate update changes are the rows the middleware-COMPOSED AST selects. That AST is complete only after every middleware has run: step 3 of this middleware composes its own scope after step 3.6, and plugin-sharing composes its editable-rows filter onto the same AST (`sharing-plugin.ts:1405`), in plugin order. The suggested route, reading "under the caller's context" in the middleware, was measured two ways and does not hold: - A caller-context read applies READ scope and field masking. Where the read scope is narrower than the write scope, written rows go unjudged (fail-open). Where it is wider, rows that will not be written get judged (false refusals). Ablation 3 below shows the second: a judgement that is not over the actual matched rows falsely refuses the USING-only in-scope control. - The engine already holds the exact set: the D7 matched-row read (`readPriorRows`, bound to the composed AST), which the ruling says is read once and reused, and which already serves validation, the `readonlyWhen` strip and both per-row hook phases. So the security layer installs its judgement on the existing `OperationContext.postHookWriteImageCheck` seam (the one objectstack-ai#19952 built for inserts), and the engine calls it on the predicate branch. It hands over every matched row merged with the payload (the same shape as the per-row `afterUpdate` `result`), placed after `assertNoStrictDrops()`, where the payload is final. That placement follows the insert seam's contract review: "the row the seam judges must be the row that is stored". The readonly strips run earlier on this branch, so a pre-strip placement would judge values that never land. ## Mechanism hypotheses (dispatch Section 2), as measured on `2c1011b01b` 1. **Held.** Line 3000 carried `!Array.isArray(opCtx.data)`. Lines 3078-3083 set `postImage = null` for `extractSingleId(opCtx) == null` and logged "governed by the using-scoped where". 2. **Held.** `engine.ts` (the `postHookWriteImageCheck` call in `insert()`) hands `evaluate` every live row of an array insert. The array fix is plugin-side only. 3. **Refined.** The memoized `getCallerPreImage` is by-id and caller-context, so it is not reusable per row for the reasons above. The engine's memo serves instead, at no extra read wherever per-row hooks already read it. 4. **Held.** The skip was unconditional. A cell pins a `using` plus a differing `check`. One further hole, found and closed: the middleware treats a falsy scalar id (`''`, `0`) as a row address, and the engine does not (`resolveEngineUpdateDispatch`). A falsy payload id therefore carried a bulk update past the per-row judgement, admitted on a change-set-only image. The seam is now installed whenever the engine will not treat the write as addressing one row. The falsy case keeps its by-id judgement too, so it only refuses more. ## Surface beyond the claim, with reasons - `packages/objectql/src/engine.ts`: see above (cross-lane, `domain:engine`). - Existing plugin-security tests: `check-only-write-scope.test.ts` and `security-plugin.test.ts` carry engine doubles that must now honour the seam on a predicate update, the way the real engine does. Otherwise the fail-closed guard refuses them, which is the intended behaviour. One test title and comment said step 3.6 "declines to check" the bulk path; it now says 3.6 can refuse a bulk write but never scope one. That pin still discriminates a site-1 revert, now by refusal. Two comment-only edits (`rls-check-defaults-to-using.test.ts`, `rls-phantom-column-negation.test.ts`) stated the array exclusion as a fact. - **A pending release note, corrected in place: `.changeset/rls-check-defaults-to-using.md`** (from objectstack-ai#19952, not yet released). Its "What does not change" list said bulk updates "are not checked row by row", which this PR makes false. The bullet now says their new rows are checked row by row too, by this PR's entry. `check-empty-changeset` is RED on this by design: it is the DELIBERATE CORRECTION class (ruling D on objectstack-ai#17712). Its prescribed remedy is to keep the correction and get it confirmed on the PR; restoring the file would publish a false sentence. Under the landing rule objectstack-ai#19970 set, 「DELIBERATE CORRECTION 红:同 head 达档复核 PASS 记录即确认,⛔ 不等维护者」, the confirmation is a same-head at-tier review record with a PASS verdict, not the maintainer. The red is expected until that record is on the PR for the landing head. ## Behaviour that changes (all in the refusing direction) - A predicate update under a check-only policy is refused when any matched row's new image fails the check. - A predicate update that moves a matched row out of a policy's `using`, when no applicable policy declares `check`, is refused: the defaulted check, which is the answer the by-id update has given since objectstack-ai#19952. Triage's "已声明 `using` 的策略,行为保持不变" is held as: in-scope bulk updates under a `using` policy are admitted and scoped exactly as before (pinned). Only a bulk write that moves rows out of the `using` is newly refused, on the same terms as by-id. The seat confirmed this reading: by-id and bulk are two implementations of one operation and must not disagree (`RowLevelSecurityPolicySchema.check` 「defaults to USING clause if not specified」; ADR-0058 D4). - An array insert is refused when any row fails, including every configuration that already refused each single insert. - A host that installs the judgement on a predicate update and never runs it is refused (403, `error` log), as an insert already is. ## Tests **Patch round 2 (head `3247efeecd`, after merging `origin/main` `9bfbacbf8b` as merge commit `938b2acfde`):** `rls-check-multi-row-writes.test.ts` 32 passed (32); plugin-security suite 123 files, 2348 tests passed; objectql re-run because objectstack-ai#19979 touched that package: local project 155 files / 2434 tests + 154 files / 2758 tests, repo project 1 file / 5 tests; `typecheck` green for objectql and plugin-security (VERDICT command-exit 0 each). **Patch round 1 (head `9fff66cfdc`, after merging `origin/main` `3fd3a4f91b` as merge commit `a5ca1db166`):** `rls-check-multi-row-writes.test.ts` 32 passed (32); plugin-security suite 123 files, 2348 tests passed (VERDICT command-exit 0 each). The merge brought no change under `packages/objectql` or `packages/plugins/plugin-security`, so the objectql suite was not re-run; its last run is the one below, on a byte-identical `engine.ts`. **Round 1 (head `6d28dfe98d`):** New: `packages/plugins/plugin-security/src/rls-check-multi-row-writes.test.ts`, 32 cells on driver-sql (better-sqlite3) and driver-sqlite-wasm, real `SecurityPlugin` + `ObjectQL`. - Failing first, on the unmodified tree: 18 red and 12 green (the controls). Every negative cell failed as "admitted" (`expected true to be false`). In the unresolvable-policy cells the single-insert leg was refused and only the array leg was admitted. - objectstack-ai#19950 cells: the check-only repro; per row not per change set (a matched row failing on an unchanged field refuses the whole write); `using` plus a differing `check`; fail-closed (an inner middleware strips the seam, and the write is refused with "the update on 'qa_ticket' was executed without the row-level CHECK being evaluated"); falsy payload id. Controls: over-fix admit, USING-only in-scope admit and scoped, `using`+`check` in-scope admit, by-id unchanged, and USING-only move-out refused on both bulk and by-id. - objectstack-ai#19964 cells: the `[admitted, refused]` repro; over-fix admit; single insert unchanged; three refuse-every-insert configurations (an unresolvable sole `using` on `insert` and on `all`, an unresolvable declared `check`). - Every refusal asserts `code` `PERMISSION_DENIED`, `status` 403 and the developer half naming the gate and verb, then reads the stored rows back under a system context. Suites: plugin-security 123 files, 2348 tests pass. objectql 309 files, 5182 tests pass (local project in two halves, plus the repo project). `typecheck` is green for both packages (plugin-security test-layer debt: 0 files, 0 errors). Ablations: each committed first, mutated through `scripts/ablation-replace.mjs` (anchor hit 1 to 0, blob changed), restored with blob equal to HEAD and an empty `git diff HEAD`. Resolution path: the plugin is imported relatively, and `@objectstack/objectql` is aliased to `src/index.ts` in this package's `vitest.config.ts`, so no `dist/` sits between the mutation and the test. | # | mutation | result | |---|---|---| | 1 | restore the non-array guard for inserts | 8 red: the 4 array-insert negative cells x 2 drivers | | 2 | never install the seam on a predicate update | 12 red: the 6 bulk negative cells x 2 | | 3 | engine judges the payload alone, not the matched rows | 6 red: the per-row cell, the falsy-id cell, and the USING-only in-scope control (falsely refused) | | 4 | install only when the id is null (falsy counts as by-id) | 2 red: the falsy-id cell, admitted | | 5 | engine never calls the seam | 16 red: refusals now carry the not-evaluated message, controls refused | | 6 | disable the post-`next()` fail-closed guard | 2 red: the fail-closed cell, admitted | ## Gates **Patch round 2 (head `3247efeecd`):** the four comment lines this change rewrote now cite the surviving record, commit `a016f08b8a` (the insert-side check), instead of a card that answers 404, and say in words that the original card no longer resolves. `GITHUB_TOKEN="$GH_TOKEN" node scripts/check-issue-citations.mjs` probes the board and exits 0: "every citation this change adds resolves (or is a declared cross-repo reference)", with 9 judged, 9 resolving and 0 unresolved added. `dispatch-gates --commands` derived the same 68 families from the same 9 paths against merge base `9bfbacbf8`. All 68 were run; `--ran` answers "68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN". 67 exit 0. `check-empty-changeset` exits 1 on `.changeset/rls-check-defaults-to-using.md` only. **Patch round 1 (head `9fff66cfdc`):** `dispatch-gates --commands` derived the same 68 families from the same 9 paths, now against merge base `3fd3a4f91`. All 68 were run; `--ran` answers "68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN". 67 exit 0. `check-empty-changeset` exits 1 on `.changeset/rls-check-defaults-to-using.md` only (the deliberate correction under "Surface beyond the claim"). The changeset gates the seat named: `check-adr-0087-registration --base origin/main` exits 0 ("1 declared-breaking changeset(s), each carrying an ADR-0087 disposition"), `check-changeset-no-major --base origin/main` exits 0, and `pnpm check:changeset-gate-self-tests` exits 0. **Round 1 (head `6d28dfe98d`):** - `node scripts/pm/dispatch-gates.mjs --commands` derived 68 families from the 9 changed paths. All 68 were run with exit codes recorded. `--ran` answers "68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN". - 67 exit 0. One exits 1: `check-empty-changeset`, the deliberate correction above. - Three first answered `PREREQUISITE NOT MET` (exit 3): `check:dual-build-cjs-loads`, `check:i18n` and `check:type-check-debt`. They pass after the workspace closure build. `check-engine-split-ratio` passes after deepening the shallow clone to its window. - Lint, as a proven narrowing: `eslint --no-inline-config --format json` over the 7 changed `.ts` files reports 7 files linted, 0 errors and 0 warnings. All 7 fall in the config's `**/*.{ts,…}` block. The config never enables type-aware linting (`eslint.config.mjs:326-328`), so this diff cannot move a verdict on an untouched file. The full `pnpm lint` is CI's. ## Acceptance notes - **Refusal cost on the predicate path.** The judgement runs where the payload is final, after the per-row `beforeUpdate` hooks and after the credential channel (`encryptSecretFields`), which runs above the strips on this branch. A refused bulk update whose payload carries a `secret` field has therefore already minted its `sys_secret` row. A validation refusal two lines below pays the same cost today. Moving the credential channel below the strips is a separate engine change. A `check` naming a secret field judges the stored reference. - **Image timing differs between the two update paths.** A predicate update is judged on the post-hook image; a by-id update is still judged in the middleware on the pre-hook change set merged with the caller-visible pre-image. Where a `beforeUpdate` hook rewrites a checked field, the bulk path is the stricter of the two. The by-id path is untouched here. - **Read cost.** On an object with no per-row hooks, a checked predicate update now reads its matched rows. The read is unbounded by the per-row hook ceiling, which applies only when hooks dispatch. On a kernel with the usual global hooks the read already happens and is shared. - **Partial-row array insert** (`__partialRowErrors`): a failing row refuses the whole call rather than being reported per row. - **Version skew.** A plugin-security built from this change, run over an engine without the predicate-path call, refuses checked bulk updates (fail-closed). Both packages carry the changeset. - `.changeset/19950-rls-check-multi-row-writes.md` is declared a narrowing, per the seat's ruling and following objectstack-ai#19952: `minor` for `@objectstack/plugin-security` and `@objectstack/objectql`, a `!` headline, `Clause-②: no (narrowing)`, an ADR-0087 `not-required (no-migration-prescription)` disposition, and a **BREAKING** paragraph listing the newly refused writes and the remedy (declare `check` on the policy, or fix the data). - `origin/main` was merged twice with merge commits, both clean with no regeneration owed: at `3fd3a4f91b` (`a5ca1db166`) and at `9bfbacbf8b` (`938b2acfde`). PR objectstack-ai#19984 (objectstack-ai#19963, the explain wiring in the same file) had not landed by the second merge. - Patch round 2: citation fix only (four comment lines in `security-plugin.ts`); no behaviour change. --------- Co-authored-by: Claude <noreply@anthropic.com>
…ough the allow-listed ccr pair; a landing denial stops and surfaces (objectstack-ai#19997) Fixes objectstack-ai#19990 Clause-②: no Rule text only, in three `pm-dispatch` references. This PR adds no allow row, no tool and no gate. `.claude/settings.json`, `scripts/pm/**`, `SKILL.md` and `AGENTS.md` are untouched. Line counts are unchanged (183 / 101 / 37), and every edited line is at or under 120 bytes. The maintainer's words, in the `domain:engine#1` seat's session, quoted on the card verbatim and in order: > 「你的pr为什么没有挂在当前session上」 > 「写一个 skills 卡片,更新技能」 > 「包括你刚才为什么不能merge,我当前session设置的是auto」 The same words reached the `domain:skills` seat directly (claim comment 5817962037): 「你的pr应该挂在当前 session上,对应的卡片优先派发」. ## What changed | file · line (after) | bytes | rule | |---|---|---| | `execution-duties.md` :149 (new) | 118 | Case 1. When a report names a PR, a session seat subscribes it at once (`subscribe_pr_activity`) and lists it on the seat post. Reason, stated once: a PR the relay opens is never attached to the session automatically. | | `execution-duties.md` :147 | 87 → 116 | The collection line now covers both modes itself ("(两种模式)", "评论与返回消息皆无"), replacing the deleted report-channel line (see *Line budget*). | | `landing-operations.md` :49 | 82 → 117 | (b). The landing executes the verdict of record (ACCEPT, or the contract-review PASS). It is not a self-approval. | | `landing-operations.md` :51 | 65 → 115 | (a). Ready and auto-merge go only through the two ccr commands that `settings.json` allow-lists (`rest-channel.md` :51 / :55). | | `landing-operations.md` :54 (new) | 120 | (c). A classifier denial during landing means: stop, report to the maintainer, and record the command and the denial reason on the card. ⛔ Never respell the command or switch to the relay to get around it. | | `landing-operations.md` :77 | 106 + 89 → 111 | Case 1, landing side. Every PR in a session seat's window must be subscribed; subscribe any that is missing. The optional 「关键 PR」 wording is gone. The old :77 "not before the report" clause is folded in as 「⛔ 不早于报告」. Routine seats keep polling. | | `reading-discipline.md` :23 | 82 → 120 | (d). A timer text carries no verdict or landing write verb. | Wording choices that differ from the dispatch text: - **`判决`, not `裁决`, at :49.** In this corpus `裁决` is a maintainer ruling, and `判决` is the review verdict (`execution-duties.md` :180–:183, 「判决 ACCEPT / REWORK / ESCALATE」). - **`判决与落地类写动词`, not only `落地类写动词`, at :23.** The timer that was denied `[Self-Approval]` told the seat to post the ACCEPT as well as run the two landing ops. ## Why no new allow row is owed: case 2 (a) The allow-listed landing route already exists. The skill already names it, and this PR only makes §B's landing step name it too. - `.claude/settings.json` :61–:66 allow-lists `curl -sS -X POST …/pulls/*/ccr/ready_for_review` and `curl -sS -X PUT …/pulls/*/ccr/auto_merge` for all three repos. The hotcrm pair was added on 2026-09-24 by `e6a5ecb9`. That commit also deliberately gave `fleet-write/dispatch.mjs` no row. `git grep -n 'with-fleet' .claude/settings.json` gives 0 hits; the control `git grep -n 'label-write' .claude/settings.json` gives 2. - `SKILL.md` :201 says 「ready/draft 走 ccr 路」, and `platform-readings.md` :48 says 「undraft 单通道:席位凭据走 `POST .../pulls/{n}/ccr/ready_for_review`」. - Measured on the timeline (`GET /issues/N/timeline`, 2026-09-24T16:3xZ): - PRs objectstack-ai#19873, objectstack-ai#19895, objectstack-ai#19902, objectstack-ai#19941, objectstack-ai#19948, objectstack-ai#19956, objectstack-ai#19970 and objectstack-ai#19993 were landed by the `domain:skills` seat under auto mode. Each has `ready_for_review` and `added_to_merge_queue` with actor `os-zhuang` (the ccr route, which writes as the seat's linked user). - PRs objectstack-ai#19971, objectstack-ai#19972 and objectstack-ai#19979 have the same two events with actor `objectstack-fleet[bot]` (the relay route). - Both routes work. The ccr pair is the one with an allow row. The relay route has none, so under auto mode the classifier judges it call by call. ## Where the standing authorization is recorded: case 2 (b) It is already recorded in the tree, so this PR adds only the one clause at :49: - `AGENTS.md` Prime Directive objectstack-ai#14: Tier S lands "by the owning seat on a contract-tier review of record". - `AGENTS.md` Multi-agent discipline §7 and Post-Task Checklist step 2: arm auto-merge on a PR that is green and accepted. - `landing-operations.md` :59 (Tier S). Whether that is enough for a seat landing a PR written by its own `mode:subagent` dev is put to the maintainer below. This PR does not rule on it. ## Line budget: what left, and where each fact still lives All three files stand at headroom 0. Each new line is paid for by deleting content, not by re-wrapping or raising a ceiling. - **`execution-duties.md` old :147 deleted.** It read 「报告通道统一:GitHub 是两种模式共用的真相源;dev 终报先落 issue 评论、再作返回消息。」 - The dev-side ordering lives in `.claude/agents/os-dev.md` :17–:18 (「报告交付两次,GitHub 优先:同一段 JSON 先作 issue 评论 … 再作为终报消息」). - "GitHub is authoritative in both modes" lives in `os-dev.md` :324 (「两种派发模式(`mode:subagent` 与 `mode:cloud`)下 GitHub 都是报告的权威源」). It also stays on the collection line as 「(两种模式)」. - **`landing-operations.md` old :77, second clause, deleted.** It read 「订阅是感知补充,⛔ 不替代 flip 定点」. The fact lives on: - :50: the flip timer is set at ACCEPT. - :52: 「CI success webhook 不可靠:⛔ 不坐等」. - `platform-readings.md` :40: 「订阅来的 `check_suite.completed` 是唤醒不是放行读数」. - Its first clause is kept on :77 as 「⛔ 不早于报告」. - **`landing-operations.md` old :76 rewritten in place.** It dates from `42af12fe7` (the 2026-08-07 ruling on subscribing *key* PRs). The newer maintainer words quoted above replace its optional scope. ## Measured risk that stays open - An allow row does not stop a denial based on content. `mcp__Claude_Code_Remote__send_later` is allow-listed (`settings.json` :23, present since before 2026-09-20), yet the engine seat's timer was denied `[Self-Approval]`. `platform-readings.md` :435 records another content-based `[Self-Approval]` denial. - Two explanations are possible: that session did not load this settings file, or the classifier judges content over an allow row. Which one holds was not measured. The eight ccr landings are the positive reading. The new :54 line covers the negative case. - **Write identity, a tension this PR did not create.** The ccr pair writes as the seat's linked user, `os-zhuang`, which is in `GOVERNED_APPROVERS` (`scripts/pm/check-governed-queue-guard.mjs` :576). Three texts point the other way: - `AGENTS.md`: "Every GitHub write leaves through `scripts/pm/`, as `objectstack-fleet[bot]` … ⛔ Never a bare `curl` … write". - `SKILL.md` :92: 「批准账号永不跑席位或作其关联用户」. - `SKILL.md` :94: 「写侧恒为 `objectstack-fleet[bot]`」. `SKILL.md` :201 already routes ready/draft through ccr, so this tension predates this PR. The new :51 states the same route more plainly. The choice is the maintainer's; see the question below. ## Verification At `04357257d`, every command from `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` was run, with the exit code captured before any pipe. All exited 0: 17 derived commands, plus `pnpm check:pm-governed-prose`, `node scripts/check-skills-token-ratchet.mjs` and `pnpm check:pm-settings-deny-roster`. The reconciliation `dispatch-gates --ran` reports: "17 derived famil(ies) accounted for — 17 run, 0 NOT-MEASURED (a DERIVED zero …)". Verdict lines: - `check:pm-skill-ratchet`: `execution-duties.md is 183 lines (ceiling 183; headroom 0)` · `landing-operations.md is 101 lines (ceiling 101; headroom 0)` · `reading-discipline.md is 37 lines (ceiling 37; headroom 0)`. - `check:pm-skill-id-lint`: `34 file(s) clean`. - `check:skill-frame-sync`: `the one declared copy of the decision frame is internally coherent`. - `check:nul-bytes`: `OK … no raw ASCII control bytes`. - `check:doc-formula-expressions` first exited 3 (PREREQUISITE NOT MET, unbuilt `@objectstack/formula` / `@objectstack/lint`). It exited 0 after `turbo run build` for those two packages under the verify lock. That first run measured nothing; it was not a failure. No build, test, reverse check or ablation applies to this change: it is rule text only, with no code path. ## Acceptance notes - `dispatch-runbook.md` :128 has cloud cards subscribe as soon as the draft PR exists. The folded 「⛔ 不早于报告」 agrees with it only because a cloud dev reports at draft-PR time (runbook :135). No change is made. - `origin/main` was not merged before opening. It moved by one commit (a `docs(qa)` change touching none of these files), and the queue rebuilds on current `main`. ## 维护者速读(草稿) **改了什么** - dev 报告里点名了 PR,席位当场订阅这个 PR 的动态,并记进座位贴。规则里写明原因:经中继开出的 PR 永远不会自动挂到会话上。落地说明里原来"给关键 PR 挂订阅"是可选说法,现在改成"落地窗口里每个 PR 都要挂上,缺了就补"。 - 落地(转 ready、挂 auto-merge)只走 `.claude/settings.json` 已放行的两条 ccr 命令。席位落地是在执行已记录的复核判决,不是自己批准自己。 - 落地过程中如果被权限分类器拒绝:停手,报给您,在卡上记下命令和拒绝原因;不换写法,也不改走中继绕过去。 - 定时提醒的文本里不再写"判决/落地"这类写操作,只写"到时重读什么、满足什么条件"。 **为什么改** - 您问了两个问题。 - PR 为什么没挂在当前 session 上:中继开的 PR 不会自动挂上,而技能里只有可选的"关键 PR 挂订阅"。 - auto 模式下为什么不能 merge:engine 席被拒了两次,一次是一条回读命令,一次是一条写着"发 ACCEPT 并落地"的定时器。它随后改走中继落地,而中继没有放行规则。放行的路本来就有:本席今天在 auto 模式下用它落地了 8 个 PR,一次都没被拒。 **风险与代价(含回滚)** - 只改三个技能参考文件的规则文本。行数不变,每行不超过 120 字节。回滚就是 revert 本 PR。 - ccr 两条命令记在 os-zhuang 名下。而 AGENTS.md 写的是"所有 GitHub 写都经 scripts/pm,以 objectstack-fleet[bot] 身份,永不裸 curl 写",SKILL.md 也写"批准账号永不作席位的关联用户"。这个矛盾早就存在(SKILL.md 本来就写"ready/draft 走 ccr 路"),本 PR 没有新造,只是把它写得更明确。 - 放行规则不保证分类器一定放行。`send_later` 在放行清单里,engine 席那条定时器还是按内容被拒了。所以新加了"被拒就停手上报"这一条。 **席位意见** **你要做的** - 回一句话,确认下面两件事,或者指出要改哪一件: - ① AGENTS.md 第 14 条(Tier S 由所属席位在达档复核记录在案后落地)和 Multi-agent discipline 第 7 条(PR 全绿且已验收就挂 auto-merge),就是席位落地自己子代理所写 PR 的常设授权,不用另外记。 - ② 用 ccr 两条命令落地,算 AGENTS.md「写只经 scripts/pm」这条规则的例外。是把这个例外写进 AGENTS.md,还是改走中继并加一条新的放行规则,都由您决定。 --- _Generated by [Claude Code](https://claude.ai/code/session_01A22sUB3mUWs6M36VgfijBq)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19940
Clause-②: no
Implements the maintainer's ruling on this card (comment 5814546887, director seat batch #220 item 3, letters 1A · 2A · 3A, 「批 #220 同意」) as three seat rules in the two files the ruling names, each paid for under the line ratchet. Nothing else moves:
scripts/check-empty-changeset.mjs(logic and wording; ruling D on #17712 stands),.github/workflows/merge-queue-triage.yml,.claude/settings.json,.claude/agents/os-dev.mdandreferences/contract-review.mdare untouched, and the signature-ledger rule 「判据唯一来源是签名台账(锚点 issue),优先于现场判断;只有人工能升级台账。」 is byte-identical.What changed
1A:
references/landing-operations.md:15-:16 (入队与落地, directly after the PASS line)Premise re-checked on
origin/main2c1011b0: the gate's DELIBERATE CORRECTION text (scripts/check-empty-changeset.mjs:605-:612) says it stays red by design (condition 1 of SKILL.md :208-:209); it runs inpr-automation.yml, whose only trigger ispull_request(nomerge_group, condition 2); condition 3 (a PR comment naming the gate and cause) stays the seat's act. A PR that rewrites a pending.changesetnote already owes the at-tier review (contract-review.md:8 lists.changesetprose among the five review surfaces), so the record 1A names always exists on such a PR.2A:
references/landing-operations.md:32-:34 (the new-signature branch; old :31-:32 rewritten in place, one line added)The two duties of the old lines survive verbatim in meaning: the full signature and first diagnosis on the PR and its
Fixescard, and an audit comment per disposition. The default stays ⛔ no re-queue; the one exception is the ruled, receipted, once-only re-queue, and a second ejection on the same signature stops and goes to the next seat. The ledger lines (:28-:29) are unchanged, so the re-queue never promotes a signature to known-flaky.3A:
references/review-checklist.md:21-:22 (范围与 changeset, after theskip-changesetrouting lines)No allow rule is added:
.claude/settings.json:68 and :70 already allowlabel-write.mjs *in both spellings.Line-ratchet ledger
Both files stay at their ceilings:
landing-operations.md101/101 andreview-checklist.md77/77 (pnpm check:pm-skill-ratchet, exit 0). Every added line is at most 120 bytes (widest 119). The payment is five deleted duplicate statements, never a re-wrap. Each deleted rule still has a home:2c1011b0)landing-operations.md:18, the regen-before-enqueue index line (bash scripts/pm/os-regen-merge.sh)landing-operations.md:33, the depend-on-a-predecessor index line (见 landing-operations C)landing-operations.md:34, the serial-relay index line (见 landing-operations D)review-checklist.md:18 (取 changed files 核范围,⛔ 不看报告自述)execution-duties.md:171 (对 GitHub 核验,⛔ 不对报告的自述核验) and :174 (changed files 范围)review-checklist.md:41 (CI 收敛读数只属于复核侧 …)execution-duties.md:176, the same three factsSizing against the ruling's "one line each": each letter is one rule. At the 120-byte cap, 1A and 3A each take two physical lines, and 2A takes one new line plus in-place rewrites of the two lines it amends. That is +7/-7 in total, and both file counts are unchanged.
Verification (head
f6c65a66)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths; change set from the merge base2c1011b01) derived 18 commands, the same list the dispatch carried. All 18 exit 0.--ranreconciliation: 18 derived, 18 run, 0 NOT-MEASURED, a derived zero with every exit code recorded.pnpm check:pm-skill-ratchet0 (101/101, 77/77) ·pnpm check:pm-skill-id-lint0 (34 files clean) ·pnpm check:skill-frame-sync0 ·pnpm check:doc-authoring0 ·pnpm check:nul-bytes0 ·pnpm check:required-contexts0 (the checklist still namesLint & Repo GatesandTypeScript Type Check) ·pnpm check:pm-governed-merges0.pnpm --filter @objectstack/lint run check:doc-formula-expressionsfirst exited 3 (PREREQUISITE NOT MET:@objectstack/formulaand@objectstack/lintunbuilt; nothing measured). Afterturbo run build --filter=@objectstack/formula --filter=@objectstack/lintunderos-verify-lock.sh(VERDICT command-exit 0), it exited 0.node scripts/check-skills-token-ratchet.mjs0 (named by the dispatch) andpnpm check:pm-settings-deny-roster0 (its roster sits under.claude).node scripts/pm/check-governed-merges.mjs --branch HEAD: governed,.claude/**x2 only ⇒ Tier S; 14 changed lines.Landing
Tier S (every governed path is under
.claude/**). This PR stays draft until an at-tier in-seat## Contract reviewPASS record exists on its current head, as the ruling requires. It publishes nothing (.claude/**is outside every package'sfiles[]) ⇒skip-changeset.Acceptance notes
execution-duties.md:175 still reads 「改到已有.changeset/*.md的 PR ⛔ 不打skip-changeset;确认取维护者原话或出处三件转述。」. After 1A that sentence is incomplete for the DELIBERATE CORRECTION class: the same-head at-tier PASS record is now the confirmation. A reviewer reading only the review duties could still wait for the maintainer. The file is outside this card's claimed surface (the claim says stop on breach) and outside the ruling's named deliverable, so it is not edited here. A same-count, in-place candidate (119 bytes) for the seat: 「改到已有.changeset/*.md的 PR ⛔ 不打skip-changeset;确认取同 head 达档 PASS 或维护者原话。」os-dev.md:306-:307 still say 「席位代挂」/「席位代做」 for a refused label write. 3A frames the seat's label as its own review conclusion, not a relay of the dev's write. The two readings agree on the act (the seat applies the label). The dispatch putos-dev.mdout of bounds, so it is noted here and not edited.维护者速读(草稿)
改了什么:给席位的落地规则加了三条,都在两份
.claude参考文件里,两份文件的行数都没有增加。① 待发布更新说明被同一个 PR 改对、门禁按设计亮红时,同一版本上的达档复核 PASS 记录就算「确认」,席位按已有的「按设计而红」三条件带红入队,不再等您点头。② 合并队列第一次因一个新的抖动签名弹出时,如果三个事实都成立(失败文件与改动无关、队列基座同一分片是绿的、首个错误是超时不是断言),可以带三行回执重投一次;同一签名第二次弹出就停,交下一席重新诊断。③ 开发代理挂skip-changeset标签被权限分类器拒绝时,席位按自己的复核(确认只改了测试或文档)自己挂标签,评论写明依据是席位复核;开发代理的报告必须逐字记下被拒的命令。为什么改:上一轮有三个已经准备好的 PR 在等您一句「好」,分别空等了约 7 小时和 13 小时,其中一次还压住了一张 p1。您 9 月 24 日批准了 1A、2A、3A 三个方案(「批 #220 同意」)。
风险与代价(含回滚):① 改写后的更新说明句子,最后一道眼睛从您换成达档复核。复核必须点名那条说明并逐句判断,记录可以审计。② 真回归最多多跑一次队列,第二次弹出会被响亮停住,不会反复重投。③ 挂错标签就是席位复核错,评论里写了依据,可以审计。门禁、工作流、权限配置都没有改。回滚:撤回本 PR 即可恢复原规则,没有数据或配置残留。
席位意见:
你要做的:无需动作。本 PR 属 Tier S,席内达档复核 PASS 后由席位入队落地。如果某一条不同意,回一句即可撤掉那一条。
Generated by Claude Code