Skip to content

feat: Comprehensive CRM example demonstrating all ObjectStack protocol features - #14

Merged
huangyiirene merged 9 commits into
mainfrom
copilot/add-crm-functionality-example
Jan 19, 2026
Merged

huangyiirene merged 9 commits into
mainfrom
copilot/add-crm-functionality-example

Conversation

Copilot AI commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

Created production-grade CRM example (~2,900 LOC) showcasing complete protocol surface area. Previous example only covered 3 basic objects with minimal features.

Objects Added

New:

  • Lead - conversion tracking, status workflows
  • Case - SLA management, escalation automation
  • Task - polymorphic relations, recurrence patterns

Enhanced (Account, Contact, Opportunity):

  • All 24 field types: autonumber, formula, currency, select with colors, master-detail, etc.
  • Multiple view types: grid, kanban, calendar, gantt
  • Validation: script, uniqueness, state machines
  • Workflow automation: field updates, email alerts, state transitions

UI Components

  • Actions (10): Lead conversion, record cloning, bulk updates with parameters
  • Dashboards (3): 35+ widgets across sales/service/executive views (metrics, charts, tables)
  • Reports (8): Tabular, summary, matrix with embedded visualizations

Example

// Opportunity with state machine validation and automated probability updates
export const Opportunity = ObjectSchema.create({
  name: 'opportunity',
  fields: {
    stage: Field.select(['prospecting', 'qualification', 'proposal', 'negotiation', 
                         'closed_won', 'closed_lost']),
    probability: Field.percent(),
    amount: Field.currency(),
    expected_revenue: Field.currency({ readonly: true }),
  },
  validations: [{
    type: 'state_machine',
    field: 'stage',
    transitions: {
      'prospecting': ['qualification', 'closed_lost'],
      'qualification': ['needs_analysis', 'closed_lost'],
      // ... enforces valid stage progressions
    }
  }],
  workflows: [{
    triggerType: 'on_create_or_update',
    criteria: 'ISCHANGED(stage)',
    actions: [
      { type: 'field_update', field: 'probability', 
        value: 'CASE(stage, "prospecting", 10, "closed_won", 100, ...)' },
      { type: 'field_update', field: 'expected_revenue', 
        value: 'amount * (probability / 100)' }
    ]
  }]
});

Protocol Coverage

Area Before After
Field Types 5 24
View Types 1 (grid) 4 (grid, kanban, calendar, gantt)
Validations 0 4 types
Workflows 1 Multiple per object
UI Components 0 21 (actions, dashboards, reports)

Bilingual documentation (EN/中文) with learning resources and architecture patterns.

Original prompt

给我一个能体现协议各项功能的crm例子


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@vercel

vercel Bot commented Jan 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
spec Ready Ready Preview, Comment Jan 19, 2026 0:17am

Request Review

Copilot AI and others added 3 commits January 19, 2026 11:51
- Add phone field type helper to Field factory
- Update Field.select/multiselect to support both array and object signatures
- Add factory methods (Action.create, Dashboard.create, Report.create)
- Use z.input and parse for factory methods to support default values

Co-authored-by: huangyiirene <[email protected]>
- Add tests for Field.phone() helper
- Add tests for Field.select() backward compatibility (old and new API)
- Add tests for Field.multiselect() backward compatibility
- Add tests for Action.create() with default value handling
- Add tests for Dashboard.create() with default value handling
- All 279 tests passing

Co-authored-by: huangyiirene <[email protected]>
Copilot AI changed the title [WIP] Add example of CRM functionality for protocol feat: Comprehensive CRM example demonstrating all ObjectStack protocol features Jan 19, 2026
Copilot AI requested a review from hotlong January 19, 2026 12:04
Copilot AI and others added 3 commits January 19, 2026 12:05
Document backward-compatible API with examples for both old and new signatures

Co-authored-by: huangyiirene <[email protected]>
- Extract options from config object to avoid confusion
- Change spread order to options first, then finalConfig
- Ensures processed options always take precedence

Co-authored-by: huangyiirene <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…ors — the PRODUCER half of the sort seam (objectstack-ai#17914)

Fixes objectstack-ai#17053

Clause-②: no

⚠️ **Governed surface** — this diff touches `skills/**`, so under Prime
Directive objectstack-ai#14 it is human-merge only. ⛔ Not queued, not ready-flipped,
no auto-merge armed by this round.

## The seam, which is what this is graded on

objectui ruled one sort orthography platform-wide — the array
(objectui#8221, decision batch objectstack-ai#77, 2026-09-07, option B) — and objectui
PR objectstack-ai#8758 executes it: `convertSortToQueryParams` refuses a runtime
string and its diagnostic names the array form.

`ListViewSchema.sort` is the **producer** of exactly those documents:
`object.list.sort` is what `deriveRelatedLists` reads. So a view
authored with `sort: 'created_at desc'` validated here, cleanly, and
then failed downstream — the contract minting a shape its consumer
rejects, with the author told off by the wrong layer. Triage graded p2
on that asymmetry, not on the string. This PR is the producer-side
pull-back.

## Reproduction, re-taken on today's tree

Taken on `origin/main` at `a9c6477` (the card measured against installed
17.3.0; this is the re-take), with `bogusProp` refused by name on the
same call as the firing control:

| input to `ListViewSchema.safeParse` | BEFORE | AFTER |
|---|---|---|
| `sort: 'name desc'` | PARSES | REFUSED at `sort` — `invalid_type`,
prescription naming the array |
| `sort: '-name'` | PARSES | REFUSED at `sort` |
| `sort: [{ field, order }]` | PARSES | PARSES, value-for-value
unchanged |
| `sort: 42` | REFUSED `sort/invalid_union` | REFUSED
`sort/invalid_type`, zod's default message |
| CONTROL `bogusProp` | refused by name | refused by name |

The control fires in both columns, so both the PARSES and the REFUSED
readings are verdicts rather than a schema reporting nothing.

## Requirement 1 — the census, and it is NOT a silent zero

The narrowing is not free, so the population was measured. The census
swept both the TS and JSON spellings of a string-valued `sort` across
the whole tree and each hit was read as a **structure**, never counted
as a token.

**It found three live authored sites on the narrowed slot, all converted
here:**

1. `examples/app-showcase/src/ui/views/task.view.ts` — `sort:
'estimate_hours desc'` on a shipped list view, carried since
objectui#2601 as a deliberate live coverage fixture for the string form.
2. `packages/lint/src/showcase-shape.fixtures.ts` — the frozen snapshot
of that same shipped shape (it tracks the app through `defineView`; its
subject, the three nameless form sections, is untouched).
3. `skills/objectstack-ui/rules/list-views.md` — the published rule
taught the clause in two code blocks.

⇒ **A migration entry is therefore owed, and this PR carries one**
(below). A silent narrowing would have been refused, correctly.

**Lit control — the census could have found one, and an independent
instrument agrees.** With the legacy string put back on the real shipped
showcase view and nothing else changed, `tsc` reds at exactly that line:

```
src/ui/views/task.view.ts(208,7): error TS2322: Type 'string' is not assignable to type '{ field: string; order: "asc" | "desc"; }[]'.
```

Mutation proven landed (`git hash-object` a83cbee → 38a2f9b, occurrence
count 0 → 1); restore proven by hash equality back to a83cbee and an
empty `git diff HEAD`.

**Read, not grepped — sites deliberately NOT converted.** ObjectQL
`query.sort` and the wire `normalizeSortNodes` (different doors,
different dialects); `packages/spec`'s `book`/`doc` field-mapping
records whose `sort: 'order'` is an unrelated key of the same name; and
the `packages/lint` rule fixtures, which feed the PRE-parse walker and
never reach this schema.

## Requirement 2 — objectstack-ai#16553 does NOT cover this, re-measured

Re-read rather than assumed. objectstack-ai#16553 is **closed as completed**, and its
title and body bound it to `ComponentPropsMap` for `object-grid` /
`object-calendar`. Its landed artifact, the semantic entry
`18.object-block-sort-item-array`, names only those two doors and states
in its own acceptance criteria that `record:related_list` is the one
deliberate exception — `ListViewSchema` appears nowhere in it. The
decisive reading is the reproduction above: on today's `origin/main`,
with that work already merged, `ListViewSchema.sort` still accepted
`'name desc'`. The gap is real and this PR is what closes it.

## What changed

`sort` survives as a key, one union arm lighter, so this is a **value**
narrowing with no `retiredKey()` tombstone to hang a prescription on.
The surviving array member's own `error` map carries it, keyed on
`issue.input` being a string — the same shape `view.type`'s retired
`'page'` value and `view.exportOptions`' retired `'pdf'` value already
use in this schema. Every other invalid value, and any string reaching a
**descendant** (a misspelled `order`, say), keeps zod's default report,
so nobody is told a clause they never wrote "was removed".

**Migration** — `list-view-sort-string-clause-to-array`, a D2 conversion
wired into the protocol-18 chain step, not a semantic TODO: the rewrite
is lossless and wholly mechanical. `'created_at desc'` is the tuple; a
bare field name meant ascending and is written out as `order: 'asc'`; a
comma-separated clause becomes one entry per key, in the same order. A
clause that does not parse as that grammar is left alone and meets the
door instead — the `'-field'` dialect belongs to
`RecordRelatedListProps.sort`, never reaches `convertSortToQueryParams`,
and retiring it was not ruled, so guessing a direction for it would
invent an ordering the author never wrote.

**A real consequence the gates caught.** Removing the string arm made
the sort entry's own keys visible to the liveness walk, which reported
`view/list.sort` as an undeclared container inheritance. Drilled with
in-repo evidence (`normalizeSortNodes` reads both `field` and `order`)
rather than parked in the shrink-only baseline, so the
container-coverage numbers did not grow.

## Ablation — both directions, with a cost-direction leg

Subject resolves through a same-package relative source import, so no
`dist` leg is in play; on-disk proof is still taken on every leg. Each
leg: mutate → prove it landed (occurrence count AND `git hash-object`) →
run → restore → prove restored by hash AND empty `git diff HEAD`.
Baseline blob `6053ffc`.

| leg | mutation | occurrences | hash | result |
|---|---|---|---|---|
| 1 — defect direction | re-admit the `z.string()` union arm | 5 → 6 |
6053ffc → a46df91 | **3 failed** / 3 passed |
| 2 — **cost direction** | over-narrow the surviving arm (`order` enum
loses `'desc'`) | 0 → 1 | 6053ffc → 48a1bcb | **2 failed** / 4 passed,
on the POSITIVE pin |
| control | unmutated tree | — | 6053ffc | 6 passed, 0 failed |

Leg 2 is the cost direction on purpose: the price of this change is
collateral narrowing of the spelling that has to keep working, and it
shows the positive pin catches exactly that.

## Verification

| run | exit |
|---|---|
| `pnpm --filter @objectstack/spec test` (`vitest run --project local`)
| **0** — 474 files, 13492 passed |
| `pnpm --filter @objectstack/spec test:repo` (`vitest run --project
repo`) | **0** — 31 files, 523 passed |
| `pnpm --filter @objectstack/spec check:generated` | **0** — "All 15
generated artifacts are up to date." |
| `pnpm --filter @objectstack/spec build` | **0** |
| typecheck: spec · lint · example-showcase | **0** each |
| `node scripts/check-skills-token-ratchet.mjs` | **0** |
| `node scripts/check-nul-bytes.mjs` | **0** |

Every exit code above was captured before any pipe (`cmd > file 2>&1;
EXIT=$?`), and each gate's own printed verdict line is what is quoted.

Regenerated artifacts moved exactly as a value narrowing should: nine
doc rows across three `content/docs/references/**` files and one
react-blocks contract row each lost the `string |` arm, and nothing else
moved.

## Clause-② — the two limbs, answered from the regenerated artifacts
with a lit control

Declared `no` as dispatched, and ⛔ not flipped by this round; the seat
sets the final value.

- **(a) Does the final diff add any exported symbol? — ZERO new
exports.** `packages/spec/api-surface/**` and
`packages/spec/export-origins/**` are **byte-identical** to the base
commit `a9c6477`.
- **(b) Does it add any key on a published payload? — No.**
`packages/spec/authorable-surface/**` and `authorable-defaults/**` are
likewise byte-identical. The diff removes a union arm; it declares no
new key anywhere.

**Lit control, so those zeros are measurements and not a blind
instrument.** One dummy `export const OsIssue17053LitControl = 1;`
appended to the same file, then rebuild + `gen:api-surface` +
`gen:export-origins`, moved **both** artifacts by exactly one line each
and named the symbol:

```
packages/spec/api-surface/ui.json:275:    "OsIssue17053LitControl (const)",
packages/spec/export-origins/ui.json:271:    "OsIssue17053LitControl": "src/ui/view.zod.ts#OsIssue17053LitControl (const)",
```

Mutation proven landed by hash (6053ffc → ba113de); source and both
artifacts restored and proven back at 6053ffc with a clean tree.

## Changeset

`@objectstack/spec: minor`, graded against this repo's own precedent for
an accept-set narrowing — the sibling `object-block-sort-item-array`
changeset took `minor` for the same ruling under the launch-window
convention for breaking changes. It carries the ADR-0087 registration
marker.

Publish surface measured rather than assumed, with controls:

- `@objectstack/spec` `files[]` ships `dist` **and** `src/**/*.zod.ts`,
so the edited `view.zod.ts` is literally published ⇒ a changeset is
owed.
- `@objectstack/lint` — **no changeset owed.** Its `files[]` is
`["dist","README.md","CHANGELOG.md"]`, and the edited fixture symbol
`SnapshotTaskViews` has **zero** occurrences in `packages/lint/dist/`,
while the positive control `validateSortableFields` has **three**. The
grep fires; the fixture is simply not published.
- `examples/app-showcase` is `private: true`.
- `skills/**` is in no package's `files[]`.

## Skills line budget

`skills/objectstack-ui/rules/list-views.md` — 306 lines before, 306
after (net 0). Package total over all `SKILL.md` files: 6134 before,
6134 after (no `SKILL.md` was edited).

The binding reading is the token ratchet, which reds where lines do not.
The first draft came in at 3154 tokens against a 3011 ceiling (+143). ⛔
The ceiling was not raised and no re-wrap was used as currency: the net
increase was paid **entirely by deleting content** — the retired string
examples this change makes wrong. Final reading **3009 / 3011, gate exit
0**. The migration prescription therefore lives in the parse error, the
changeset and the upgrade guide rather than in the published rule, which
is where an upgrading author actually meets it.

## 维护者速读(草稿)

**改了什么** — `ListViewSchema.sort` 不再接受旧的字符串子句(`'created_at desc'`),只接受 `{
field, order }[]` 数组。同批把树内三处仍在写字符串的文档改成数组,并补上一条 D2 迁移条目。

**为什么改** — objectui 已经裁定「一个拼法,数组」并在 PR objectstack-ai#8758 里让消费端**运行时拒收字符串**。spec
是那些文档的**生产者**,于是出现最坏的缝:文档在上游通过校验、在下游失败,作者被错误的那一层训斥。分诊原话:**契约铸造了一个消费者拒收的形状**。p2
判在这个不对称上,⛔ 不判在字符串本身。

**风险与代价(含回滚)** — 代价是真实的:用旧字符串写的文档停止通过校验。普查**测出**树内三处(showcase 列表视图、lint
快照、已发布技能),全部已转换,因此这不是静默收窄——D2 条目 `list-view-sort-string-clause-to-array`
可机械重写作者源码,存量行按既有路径重放。`RecordRelatedListProps.sort` 的 `'-field'`
方言**未被触碰**(不同方言、未经裁定),该形状的字符串本条目也拒绝猜测方向。回滚 = revert 本 PR:收窄是纯删除一条 union
分支,无数据迁移、无存量改写,回滚不留残迹。

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

**你要做的** — 定 Clause-② 终值(本轮实测两肢均为零,见上)。本 PR 触 `skills/**` ⇒ 受管面,按 Prime
Directive objectstack-ai#14 需**人工合并**。

## 验收备注

Out-of-scope observations, noted and deliberately NOT filed:

- `packages/lint`'s `readSortKeys` keeps a string arm for `sort`. It is
a **pre-parse** walker on raw authored stacks, so it is defensive rather
than dead, and `packages/lint` is a sibling round's declared face.
Carrier: whoever next revisits that rule. Noted, not filed.
- `packages/lint/src/showcase-shape.fixtures.ts` is the one file in this
diff inside `packages/lint`, the face declared for objectstack-ai#17319. It is a
one-line value change forced by `tsc` (the snapshot tracks the shipped
app through `defineView`), and it does not touch that round's subject.
Flagged here so the seat can see the overlap rather than discover it at
merge.
- The conversion walk reaches `stack.views[]` in all three persisted
spellings but **not** `objects[].listViews.*` — the same boundary
`view-page-mount-removed` states for itself. An object body carrying a
string clause is refused at its own door rather than converted. Stated
in the conversion's docblock rather than left to be discovered. Noted,
not filed: widening the walk is its own card with its own population
measurement.

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

---------

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…ce tiering, ui/triage lane lines (objectstack-ai#17942 chain) (objectstack-ai#18018)

Fixes objectstack-ai#17971
Fixes objectstack-ai#17742
Fixes objectstack-ai#17624
Part of objectstack-ai#17942
Part of objectstack-ai#17950
Part of objectstack-ai#17933
Part of objectstack-ai#17934
Part of objectstack-ai#17951

Dev session `session_01DAcomhvR9kKizeYgg89Vo8` on branch
`claude/issue-17942-charter-revision` (worktree
`objectstack-issue-17942`). Rules-layer charter revision for the
eight-card family whose chain head is objectstack-ai#17942. Deferred to the follow-up
references PR (files occupied by PR objectstack-ai#17990 / objectstack-ai#17992, seat-landable once
this PR lands the tiering): `contract-review.md` (the objectstack-ai#17934 落地前检③
exception, objectstack-ai#17933's per-lane table, objectstack-ai#17950's 落地前检③ line, objectstack-ai#17942's 复核归属 /
独立性件), `dispatch-runbook.md` (objectstack-ai#17942's 跨车道直接接手), `lanes/director.md` +
`decision-analysis.md` (objectstack-ai#17951's director half); the objectstack-ai#17950 register /
queue-guard script items are their own script PR. objectstack-ai#17934 has no edit
here — its ruling lives in a deferred file.

## Rulings landed (verbatim, one commit per card)

- **objectstack-ai#17942** — 5651976160 「同意」 to `1A(+三类表)·2(1)·3A·4B`; 5652079343
「以上接受你的建议。」; 5652544529 ruling ③ 「同意」 (「一律经过分诊 … 唯一例外:在飞卡的衍生子卡 …
立卡者不查重,只在卡面附 3–5 个查重词 … 查重缓存住在分诊席自己的容器里」). Q3 withdrawn — no
self-routing text had reached SKILL.md (`git grep -E '自路由|自定级|self-rout'
origin/main -- .claude/skills/pm-dispatch` exits 1; control 「分诊座位唯一」
hits), so nothing came out. The 「分诊不重指派」 wording does not exist in the
tree (git grep exits 1) — nothing to narrow.
- **objectstack-ai#17950** — 「我点头」 to the boundary 「规则层(你确认)… 事实层(席内契约复审档复核后入队):只有
`.claude/skills/pm-dispatch/references/**`」.
- **objectstack-ai#17971** — 5652306063 「C. approve 后不管后续改动都由席位落地:」.
- **objectstack-ai#17933** — the maintainer's misreading 「不是说你的 ui 车道不需要契约复审了吗?为什么还有
`needs:contract-review`」; `lanes/ui.md` :38 now states the
applicability.
- **objectstack-ai#17742** — self-triage 5642823259 (fourth disjunct if the population
is small); count posted on the card first (5653074219): `pm:queue` ∧
`domain:*` ∧ ¬`priority:*` = 7 objectstack / 31 objectui at
2026-09-13T11:46Z.
- **objectstack-ai#17951** — 「③ 受管面手合本身也是一条队列。能否简化我的审核步骤,比如我召唤项目总监时可以批量决定?」 — SKILL.md
director line paid (+1 inside the 812), AGENTS.md PD objectstack-ai#14 clause.
- **objectstack-ai#17624** — 5650203410 「同意」 to B; precondition measured:
`lanes/triage.md` :7 differed from the SKILL.md line only by 「分诊」
inserted and 「裁定」 dropped — no rule lost.

## Acceptance greps (`git grep -c -F` at `BASE` → this head; every
file's control still hits)

| card | 0 → 1 phrase | file | control (1 → 1) |
|:--|:--|:--|:--|
| objectstack-ai#17942 | 「认领即跟到 MERGED」, 「分诊座位唯一生产」, 「在飞卡衍生三分」; 「简单阻塞项」 3 → 0 |
SKILL.md | 「每个方案必须沿四条固定评估轴分析」 |
| objectstack-ai#17950 | 「受管面两层」 / `Landing is tiered` / 「两层分档」 | SKILL.md / AGENTS.md
/ lanes/skills.md | `GOVERNED_SURFACES` / 「QA 波次由维护者手动触发」 |
| objectstack-ai#17971 | 「席位落地」 0 → 3 (SKILL) and 0 → 1 (core-rules); `landed by the
owning seat` | SKILL.md, core-rules.md, AGENTS.md | 「一座位一车道双射」 |
| objectstack-ai#17933 | 「条款②复核本席适用」; 「不凭记忆或继承的注记」 1 → 0 | lanes/ui.md | 「零读数恒配点亮的正控」
|
| objectstack-ai#17742 | 「析取 ④」, 「未定级数另计析取④」; core 「队列卡缺域或缺定级」 | SKILL.md,
core-rules.md | 「分诊座位唯一」 |
| objectstack-ai#17951 | 「受管草稿呈为一批」; `director seat requests as ONE` | SKILL.md,
AGENTS.md | `A version release is performed by the maintainer` |
| objectstack-ai#17624 | 「六态属他席」; 「分诊不挂」 1 → 0 | lanes/triage.md | 「饥饿守卫」 |

## Line budgets (net 0 per file, paid by density inside the file —
payments named in each commit)

SKILL.md 812 → 812 (widest table row 342 B, untouched); core-rules.md
151 → 151; AGENTS.md 1075 → 1075 (widest row 768 B, untouched; the `node
-e` helper line under PD objectstack-ai#14 is the one deletion that is not a rule);
lanes/ui.md 38 → 38; lanes/triage.md 7 → 7; lanes/skills.md 33 → 33.
Every added line ≤ 120 B (`git diff BASE..HEAD | grep '^+' | awk
'length>120'` prints nothing); the whole-file `awk 'length($0)>120'`
baseline is 23 / 23 on SKILL.md and 15 → 14 on AGENTS.md — all table
rows or the dropped helper line, the shape the ratchet exempts (the PM's
"empty over edited files" expectation is false on `origin/main`
already). Decision frame block byte-identical: md5
`3327d02c56f8a0eca88569dad2270f32` (now at :733–:754).

## Deviations from the dispatch, stated

- P1 「:454 leave」 falsified: the ruling lifts the S-level cap that :454
stated; leaving it would keep SKILL.md contradicting Q2 class 2 while
the references PR cannot touch SKILL.md and stay seat-landable — the
four 简单阻塞项 lines became the three-class rule (commit 1).
- 5652544529's 「lanes/triage.md gains the cache line」: triage.md is 7/7
and its only spendable line went to ruling B — the cache rule lands in
SKILL.md 〈分诊座位职责〉 instead.
- The `references/**` spelling in prose: `check:pm-governed-prose` reds
any `**`-shaped code span outside the register, so the tier is spelled
`.claude/skills/pm-dispatch/references/` (AGENTS.md) and 本技能
`references/` (SKILL.md).
- SKILL.md 〈复核〉 had a carve-out letting `.claude/`
hooks/workflows/settings PRs self-land on the skills seat's review; the
objectstack-ai#17950 ruling names hooks and settings in the rules layer, so those
three lines became the tiering lines (provenance beyond the shallow
window not read).
- Table rows left untouched (widest-row pins): 状态模型 rows still list
「跨车道移交」 / 「跨域 PR 指定车道」; the bullet lines govern.

## Gates (derived with `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` on the merged head `269933d9`; all 18
run in the foreground, exit codes captured before any pipe, `--ran`
reconciled: 18 derived, 18 run, 0 unrun)

`check:pm-skill-ratchet` ✓ (SKILL.md 812/812 · core-rules 151/151 ·
AGENTS.md 1075/1075 · ui 38/38 · triage 7/7 · skills 33/33; both
widest-row pins at headroom 0) · `check:pm-skill-id-lint` ✓ 27 files
clean · `check:skill-frame-sync` ✓ · `check:pm-governed-prose` ✓ (5
register surfaces named, no over-claim) · `check:pm-governed-merges` ✓ ·
`check:nul-bytes` ✓ 8593 files · `check:refd-timer-probe` ✓ ·
`check:doc-authoring` ✓ · `check:required-contexts` ✓ ·
`check:watch-hint-literal` ✓ · `check:agent-test-spelling` ✓ ·
`check:docs-audit-scope` ✓ · `check:driver-memory-census` ✓ ·
`check-closing-keyword-parity` (+ self-test) ✓ ·
`check-comment-mask-corpus` ✓ · `check-governed-queue-guard --self-test`
✓ 183 cases · `@objectstack/lint check:doc-formula-expressions` ✓ (first
run exit 3 PREREQUISITE NOT MET — formula/lint unbuilt; built under the
verify lock, 2m52s, then ✓). No package is touched ⇒ no build/test
closure owed; changeset: `.claude/**` and `AGENTS.md` publish nothing ⇒
`skip-changeset`.

## Acceptance notes (noted, not filed; 承接者 named)

- `.claude/agents/os-dev.md` :50 「先搜再立」 still asks the dev to dedupe
before filing; ruling ③ moves dedupe to triage (filer attaches
keywords). 承接者: the skills seat — a class-1 sub-issue of objectstack-ai#17942
(os-dev.md has its own ceiling; not in this PR's file surface).
- `lanes/ui.md` :25 and objectui `AGENTS.md` §9 still read 「停在 draft
等人合」 — ruling C applies to objectui governed PRs too (the card names
objectui#9374 / objectstack-ai#9377). 承接者: the skills seat, sibling-repo PR.
- `SKILL.md` :113 `pm:retriage` row and :106 assignee row (table rows)
still name 改路由 / 跨车道移交 without the pre-dispatch qualifier; the bullet
「`pm:retriage` 改路由只对未派发卡」 governs. 承接者: whoever next edits those rows
under the 342 B pin.

## 维护者速读(草稿)

**改了什么**:把这周你在聊天里定下的八条裁决写进 PM 章程的规则层——认领的卡跟到合并(碰到 spec
面借复核不换席)、衍生子卡与阻塞项的三分表、定级/路由/查重收回分诊席(立卡者只附查重词)、受管面分两层(只有 pm-dispatch 的
references 目录经席内复核后入队,其余仍由你确认)、你批准之后由席位自己落地(后续推送也是)、ui 车道条款②适用面写明、分诊
sweep 补第四态、总监席把待你确认的受管草稿一批呈报、分诊席「并发出现的六态属他席」。SKILL.md、核心条款、AGENTS.md 第
14 条、三个车道文件,行数全部不变,每处新增都在同文件删重付账。

**为什么改**:每条都是已裁未落地的章程滞后;裁决原文逐字引在各 commit 与上文。

**风险与代价(含回滚)**:不发布任何包、无 changeset;规则层文本改动,回滚 = revert 本
PR。风险在两处判断:AGENTS.md 第 14 条为付行数删掉了 `node -e`
打印受管面的辅助命令(注册表所在文件仍点名);〈复核〉里「`.claude/` hooks/settings 纯代码面由 skills
席自审直接落地」的旧例外被你 09-13 的分层裁决覆盖,已删。

**未含**:references 层的对应条文(contract-review.md
的落地前检③例外与逐车道适用表、dispatch-runbook.md 的接手细则、director.md 与
decision-analysis.md 的总监批呈)另起一个 references PR,待 PR objectstack-ai#17990 / objectstack-ai#17992
落地后由席位自落;objectstack-ai#17950 的注册表与队列守卫脚本项另起脚本 PR。

**一句问**:同意「hooks/settings 的旧自审直落例外」随分层裁决一并删除吗(是/否)?

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

---------

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
… tier (objectstack-ai#18036)

Fixes objectstack-ai#18020

Part of objectstack-ai#17950. The maintainer tiered the governed surface on 2026-09-13
(「我点头」) and the charter text landed with PR objectstack-ai#18018, but
`check-governed-queue-guard.mjs` still demanded an authorized approval
for every governed path — the tier was declared, not enforced. The
merge-group leg now learns it.

## What changed

A governed pull request whose governed paths **all** lie under
`.claude/skills/pm-dispatch/references/` is satisfied by the skills
seat's **review of record on the current head** — a `## Contract review`
comment on the PR thread carrying a `Reviewed-by:` line and a
`Served-tier:` reading that stands — in place of the approval. One
rules-layer path in the same diff and today's predicate is the only way
through.

**Monotone by construction.** The tier leg is consulted *only* for an
entry no authorized approval satisfied, so it can lift a refusal and can
never create one. Nothing that passes the queue today newly refuses.

**Recognition is imported, never re-implemented** — the heading marker,
head-sha span test and newest-of resolution (`check-half-states.mjs`),
the `Reviewed-by:` / `Served-tier:` readers
(`check-clause2-carriers.mjs`). Zero new parsers;
`check-clause2-carriers.mjs` carries one **export-only** change (`const`
→ `export const REVIEWED_BY_LINE`, value expression md5-identical).

## Acceptance greps (both directions)

| reading | before | after |
| --- | ---: | ---: |
| `REFERENCES_TIER_PREFIX` in the guard | 0 | 9 |
| `readServedTier` / `servedTierStands` imported | 0 / 0 | 4 / 4 |
| `GOVERNED_APPROVERS` (lit control — still hits) | 34 | 39 |
| self-test cases | 183 | 229 |
| the tier VALUE spelled in the guard | 0 | 0 |
| predicate bodies md5-identical to their pre-change selves | — | **16
of 16 untouched** |

## Two measurements the route turned on

1. **The cycle is real, and indirect.** A module-scope import of the
recognisers deadlocks (node exits 13, "Detected unsettled top-level
await") — measured directly *and* through `check-clause2-carriers.mjs`,
which imports H31's file. So the import is **lazy**, which is legal only
because this file's dispatch no longer carries a top-level `await`. That
precondition is pinned against this file's own source; ablation D
(restoring `await main()`) reds exactly that one case, and ablation C
(in the self-test dispatch) reproduces the exit-13 deadlock.
2. **The thread read widens no scope.** `GET
/repos/{o}/{r}/issues/{n}/comments` answers
`X-Accepted-GitHub-Permissions: issues=read; pull_requests=read`, and
GitHub documents the semicolon as separating *alternative* permission
sets. The workflow's existing `pull-requests: read` is sufficient; ⛔ no
workflow change.

## Reverse verification (mutate → prove on disk → run → restore)

| ablation | cases red of 229 |
| --- | ---: |
| A — drop the prefix's trailing slash | 2 |
| B — `entrySatisfied` accepts any record state | 12 |
| C — top-level `await` in the self-test dispatch | exit 13, deadlock |
| D — top-level `await` in the live dispatch | 1 (the precondition pin)
|

Each leg proved its mutation on disk before running, and its restore by
blob hash against `HEAD`.

## Acceptance notes

- `AGENTS.md` PD objectstack-ai#14's sentence 「the queue guard refuses an unpinned
governed diff」 stays true: a references-only PR is pinned by its record.
No prose changed; `check:pm-governed-prose` is not in the derived set
for this diff and is green when run anyway.
- Noted, not filed: `makeLabelReader`'s docblock says the issues-labels
route "needs `issues: read`, which this workflow does not grant". The
live API answers `issues=read; pull_requests=read` for it too, so the
stated reason is stale — the choice to read the pull object is still
right (it reuses a call the leg already makes). Successor: whoever next
edits that reader, in this same file.

Authored by the `domain:skills` seat, session
`session_01DAcomhvR9kKizeYgg89Vo8`.

## Gates (all at `f088df57`)

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived **37** families; all 37 run, every
one with its exit code captured before any pipe, all **0**. `--ran`
reconciles: *37 derived, 37 run, 0 NOT-MEASURED (a DERIVED zero), 0
UNRUN*. Includes `check:pm-dispatch-gates` (1682 self-test cases),
`check:pm-clause2-carriers`, `check:nul-bytes`,
`check:refd-timer-probe`, `check:closing-target-claim`,
`check:whole-set-label-write`, and the guard's own `--self-test` (229
cases).

`check:pm-governed-prose` is **not** in the derived set for this diff —
no prose surface changed — and was run anyway: green, *2 instruction
surfaces name all 5 registered governed surfaces and claim no others*.

`eslint . --no-inline-config` ran the **whole** population rather than a
narrowing: **6722 files, 0 errors, 0 warnings**. No
`parserOptions.project` and no typed rules are configured, so no
untouched file's verdict can move with this diff.


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

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…ratch path and never trusts a file it did not write this turn (objectstack-ai#18061, half A) (objectstack-ai#18094)

Fixes objectstack-ai#18061

**Half A only.** Half B (the `check-reference-carrier-shape` blind spot)
is excluded per triage 5657095122 — see Acceptance notes. Nothing here
measures, asserts or touches it.

## What the card measured

One session's concurrently dispatched subagents share ONE flat
scratchpad directory. Generically-named files — `pr.json`, `card.json`,
the `card_comments.json` / `card-comments.json` pair — written minutes
apart by sibling rounds overwrite each other **silently and
well-formed**: a reviewer that re-read `pr.json` would have reported a
confident verdict about the wrong diff. The card corrects its own
reporting reviewer: cross-session isolation **held** (exactly one
session directory exists under the scratchpad root), so the scope is
session-specific but not agent-specific — a seat collides with its own
siblings.

## The change — one rule line, in the review subagent's order

`.claude/skills/pm-dispatch/references/contract-review.md`, immediately
after the two lines that already spell that order out (when to spin an
isolated reviewer up; what to feed it):

> - 隔离复核子代理暂存全写按所审 PR 命名的 `SCRATCHPAD/pr-N/`,⛔ 不读非本轮自写的暂存。

Two clauses, both from the triage's `⛔ Scope for half A`:

1. a **round-unique** scratch directory named by what is being reviewed
— the shape `.claude/agents/os-dev.md` already mandates dev-side as
`issue-N/`, applied to the PR;
2. ⛔ never read a scratch file you did not write yourself **in the same
turn** — the half that survives any path scheme.

⚠️ The path is spelled `SCRATCHPAD/pr-N/` **in this body only**. The
file itself carries the repo's ordinary angle-bracket placeholder
spelling, and every quotation below respells it the same way
(`issue-N/`, `batch:N`, `--test PATHS`); angle-bracket fragments do not
survive this surface intact (AGENTS.md, *GitHub mutates body BYTES*).

## Paying for the line — the file was at its ceiling

`check:pm-skill-ratchet` before the change, 2026-09-14T01:52Z, at
`f27e86b08`:

```
✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/contract-review.md: widest table row is 0 bytes (pin 0; headroom 0).
✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/contract-review.md is 60 lines (ceiling 60; headroom 0).
```

Headroom 0, so the line is paid for by a **density merge**, not by
deleting a rule. The two adjacent per-item lines ② and ③ of the review
record became one:

```diff
-- ② semver 定级:变更级别与 changeset 声明一致。
-- ③ 边界旗处置:dev 挂旗与 `open_questions` 逐旗答复或升级。
+- ② semver 定级与 changeset 声明一致;③ 边界旗:dev 挂旗与 `open_questions` 逐旗答复或升级。
```

Every clause survives. ② keeps `semver 定级`, and `变更级别` is the referent
of `定级` — the grading IS the change level, so the agreement clause
against the changeset declaration is intact. ③ loses only the head noun
`处置`, whose content is the body that follows it (`逐旗答复或升级`). ⛔ No rule
is dropped, and no wrap is counted as a line.

`check:pm-skill-ratchet` after the change, 2026-09-14T02:03Z, at
`7ded5bad7` — exit 0:

```
✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/contract-review.md: widest table row is 0 bytes (pin 0; headroom 0).
✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/contract-review.md is 60 lines (ceiling 60; headroom 0).
```

Widest line in the file is unchanged at 120 bytes (the 120-byte per-line
rule the ratchet enforces for non-table lines); the new rule line
measures 118 bytes and the merged line 115.

## The concurrency-budget half of the subject already landed

The triage notes that a path scheme alone does not address the lock
starvation the same wave caused. ⛔ No budget rule is added here, because
two already exist and are quoted below as they stand today at
`f27e86b08` (the angle-bracket placeholder in the first is spelled `N`
for this surface only):

- `.claude/skills/pm-dispatch/SKILL.md:60` — `| `batch:N` | 同时在飞的 dev 上限
| 默认 `2`;`n` 的维护者天花板 `5` |`
- `.claude/skills/pm-dispatch/SKILL.md:436` — `- 第 N 单派发前读
`scripts/pm/os-verify-lock.sh --status`:到达深度 ≥ `LOCK_DEPTH_HOLD`(=
2)即等。`

(`references/core-rules.md:11` carries the same pair in one line.) Both
landed in `7ef05f997` as write-identity lock 3.

## Premise readings

Taken in the worktree at `f27e86b08` before the first edit.

**P1 — the dev half of the remedy is already landed. TRUE.**
2026-09-14T01:50:36Z, `sed -n '30,35p' .claude/agents/os-dev.md`:

```
32:   - Scratchpad 按 issue 隔离:在 scratchpad 目录下建 `issue-N/` 子目录,临时文件全写进去。
33:   - 同批 agents 共用一个 scratchpad 目录,自然命名的文件会被彼此静默覆盖。
```

⇒ ⛔ os-dev.md is not touched by this PR.

**P2 — the colliding rounds had no rule to follow. TRUE.** Same
timestamp, `grep -c -i scratch`:

```
.claude/skills/pm-dispatch/references/contract-review.md : 0
.claude/skills/pm-dispatch/SKILL.md : 0
.claude/skills/pm-dispatch/references/dispatch-runbook.md : 0
.claude/agents/os-dev.md : 2      ← lit control, hits at :32 and :33
```

The lit control rules out a broken matcher. And the runbook is **not**
where the review subagent's order is templated: its only mention of this
review points away, at `:161` — `无主阻塞项的契约面(含 `packages/spec`)走
`contract-review.md` 独立性件的隔离达档复核` — and its `## 派发词构造细则` section
templates the **dev** dispatch word, not the reviewer's brief. ⇒ ⛔
`dispatch-runbook.md` is not touched either; `contract-review.md` is the
whole surface.

**P3 — the file is at its ceiling. TRUE.** 2026-09-14T01:52Z; the two
ratchet rows quoted above, headroom 0 on both. ⇒ the density merge is
mandatory, not stylistic.

## Verification

**Gate families** — derived in the worktree, run with exits captured by
redirect-then-capture (⛔ never through a pipe), recorded and reconciled:

`node scripts/pm/dispatch-gates.mjs --ran /tmp/ran-18061.txt` at
`7ded5bad7`:

```
✓ dispatch-gates --ran: 15 derived famil(ies) accounted for — 15 run, 0 NOT-MEASURED
  (a DERIVED zero — all 15 recorded an exit code and none of them is 3).
```

All 16 commands (the 15 derived plus `pnpm check:pm-governed-prose`,
recorded as outside the derivation) exited 0, including
`check:pm-skill-ratchet`, `check:skill-frame-sync`,
`check:pm-governed-merges`, `check:pm-skill-id-lint`, `check:nul-bytes`,
`check:doc-authoring`.

⚠️ One of them needed a second run to produce a reading at all. `pnpm
--filter @objectstack/lint run check:doc-formula-expressions` first
exited **3** — `PREREQUISITE NOT MET`, its own text saying *"Nothing was
measured … It is NOT a finding"*. That is NOT MEASURED, ⛔ not a failure.
Its prescribed fix (`turbo run build --filter=@objectstack/formula
--filter=@objectstack/lint`) was run under the shared verify lock
(`os-verify-lock: VERDICT command-exit 0 · held the lock 2s · waited
0s`), after which the gate exited **0**.

**Governed-surface tier.** `node scripts/pm/check-governed-merges.mjs
--test .claude/skills/pm-dispatch/references/contract-review.md` — exit
3 (the deliberate GOVERNED code, ⛔ not a finding):

```
governed-surface predicate: 1 of 1 path(s) hit the register (5 surfaces, repo-agnostic).
  ⛔  GOVERNED — a human merge is the review record for this PR (objectstack-ai#9495 regime).
      .claude/** ×1 — the agent instruction tree (skills, agents, hooks, settings)
```

Landing tier, read from `scripts/pm/check-governed-queue-guard.mjs`'s
own exported predicate:

```
REFERENCES_TIER_PREFIX = .claude/skills/pm-dispatch/references/
governedTierFor(['.claude/skills/pm-dispatch/references/contract-review.md']) = references
```

⚠️ Note for the dispatching seat: the `--test PATHS` predicate lives on
`check-governed-merges.mjs`, ⛔ not on `check-governed-queue-guard.mjs` —
the latter reads `GITHUB_EVENT_PATH` and exits 1 with *"could not look
must never exit 0 here"* when given a path. The tier above is computed
from its exported `governedTierFor`, which is the same decision.

**Lint — a proven narrowing, not a skipped run.** The repo-wide `pnpm
lint` is CI's run. The narrowing here is total, and all three readings
are present:

1. **Population, read from eslint's own config** (⛔ not guessed):
`ESLint#isPathIgnored('.claude/skills/pm-dispatch/references/contract-review.md')`
→ `true`; lit control
`isPathIgnored('scripts/pm/check-skill-line-ratchet.mjs')` → `false`.
2. **File count, from `--format json`**: `eslint THAT-FILE
--no-inline-config --format json` → exit 0, `errorCount: 0`, one warning
reading *"File ignored because no matching configuration was supplied."*
— **zero lintable files** in this diff.
3. **Immutability for untouched files**: the diff is one markdown file
that eslint's flat config does not match at all, so no untouched file's
verdict can move. Type-awareness does not enter — the file is never
parsed.

**Control characters.** `grep -naP` over the changed file for the C0/DEL
set: no hits (exit 1). `pnpm check:nul-bytes` exit 0.

**Changeset: none owed, and the label is the declaration.**
`changeset-check` (`.github/workflows/pr-automation.yml`) declares
exactly **two** exemptions — the `skip-changeset` label, and the
changesets release PR pinned by branch **and** author. There is ⛔ **no
path exemption**, so `.claude/**` earns no automatic pass: the label IS
the declaration, and it is applied on this PR. The substantive test is
satisfied independently — nothing published moves, `.claude/**` ships in
no package's `files[]`.

## Acceptance notes

- **Half B is excluded per triage 5657095122** — 「⚠️ ⛔ **Do not let half
B ride along on half A's PR.**」 Different lane, different surface; one
half is measured and the other is a hypothesis. ⛔ Nothing here measures,
asserts or touches
`packages/lint/scripts/check-reference-carrier-shape.mjs`, and this PR's
closing keyword is written as instructed by the dispatching seat.
- **The concurrency-budget half of the subject already landed** and is
quoted above rather than re-legislated: `batch` default `2` and the
`LOCK_DEPTH_HOLD` (= 2) arrival-depth wait, both from `7ef05f997`.
- noted, not filed: the dispatch word named
`check-governed-queue-guard.mjs --test PATHS`; that flag is on
`check-governed-merges.mjs`. A tooling-usage note for the dispatching
seat, ⛔ not a repo defect — neither script is wrong. Carrier: this PR's
reviewing seat.
- noted, not filed: this session's own scratchpad shows the card's exact
shape — 499 flat entries at the root, and the name `issue-18061` is
already taken **by a file** another round wrote, so the per-issue
subdirectory `.claude/agents/os-dev.md` mandates could not be created
under that name. This round wrote under `dev-18061/` instead and
re-fetched the card body itself rather than trusting the sibling's file.
Live confirmation of the finding, ⛔ not a second finding. Carrier: this
card.

`Clause-②: no` — `.claude/skills/pm-dispatch/references/**` is internal
dispatch doctrine, not published `skills/**`; the diff makes no
falsifiable operator or contract-semantics claim, adds no exported
symbol and no key on a published payload.

## 维护者速读(草稿)

**改了什么。** 席内契约复核细则加一条:隔离复核子代理的临时文件必须写进按所审 PR
命名的独立目录,且不读本轮不是自己写的临时文件。文件正好卡在 60 行天花板上,这一行由把复核记录 ②③
两条并成一条来支付——两条的每个子句都还在,⛔ 没有删规则。

**为什么改。** 一个席位同时派五个复核子代理时,它们共用同一个扁平暂存目录,`pr.json`
这种通名文件被兄弟轮次静默覆盖。失效形态是**静默且格式完好**:复核者拿到的是另一个 PR 的
payload,却会给出一个自信的裁决——受管面复核上的「自信的错答案」,⛔ 不是工具小毛病。跨会话隔离本身没坏,坏的是同一会话内部。

**风险与代价(含回滚)。** 代价是复核子代理多建一层目录。风险面只有一处:②③ 合并后阅读密度变高,但两条的操作性内容一字未少。回滚 =
`git revert` 单个 commit,受管面无运行时影响,不发布任何包,无 changeset。

**席位意见。** (留空,待席内定稿)

**你要做的。** 这是受管面(`.claude/**`),按 Prime Directive objectstack-ai#14 只能由你手动合并:⛔ 不入队、⛔ 不挂
auto-merge、⛔ 不翻出 draft。落地档位为 `references`。请确认那条新规则的中文措辞与本文件的机读语域一致,以及 ②③
合并是否可接受。

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

Co-authored-by: claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…records that mean them (objectstack-ai#18098)

Part of objectstack-ai#14361 — ⛔ this PR deliberately does NOT close it; see *"What
this PR leaves open on the same card"* below.

Clause-②: no

Director ruling **B — the repo-qualified spelling** (comment
`5507409725`), **as amended by the same director seat in comment
`5507573601`**, which is the operative form of the ruling and is
independently mandated by AGENTS.md Prime Directive objectstack-ai#13. See *"The one
place this PR departs from the dispatch brief"* below — please read it
before reviewing the diff.

## What was wrong

From this repository's point of view `ADR-0071` named two unrelated
decisions and only one of them had a record here.

- `docs/adr/0071-dataset-semantic-layer-depth.md` is **ADR-0071: Dataset
semantic-layer depth — multi-hop joins**.
- The identity / SCIM citations mean something else entirely: the
enterprise-identity decision taken in `objectstack-ai/cloud`, whose
**open mechanism half** has been mirrored into this repo since
2026-09-07 as
[ADR-0134](../blob/main/docs/adr/0134-env-side-scim-provisioning.md).

`check:adr-anchors` was green over all of them, because the number
*resolves* — it just resolves to a real page about the wrong subject,
which is worse than a dangling id: a plausible record invites belief
instead of a second question.

## The count, re-derived from the tree

The card said 39, measured on `e854a531a`. Re-derived on this branch's
merge base `66e34d14d`, per line rather than per file:

| bucket | lines | disposition |
|---|---|---|
| identity meaning, bare | **44** | re-pointed (43 → `ADR-0134`, 1 →
`cloud ADR-0071`) |
| identity meaning, already repo-qualified | 1 | untouched —
`auth-plugin.ts:1210` `cloud ADR-0071 verification objectstack-ai#1` |
| dataset meaning | 22 | ⛔ untouched — they match the local record |
| `docs/adr/**` (the 0071 record, ADR-0134, ADR-0135) | 18 | ⛔ untouched
— governed surface |
| six package CHANGELOGs | 15 | ⛔ untouched — historical archive |
| generated `content/docs/references/system/auth-config.mdx` | 2 |
regenerated from its producer, never hand-edited |
| **total `ADR-0071` lines in tracked files** (`pnpm-lock.yaml`
excluded) | **102** | over 46 files |

**How 44 differs from 39.** The card counted *files* (43, of which 39
identity); this counts *citation lines*, and several files carry more
than one (`auth-manager.ts` 8, `auth-manager.test.ts` 9,
`auth-config.zod.ts` 4). The surface also grew between `e854a531a` and
today: the two mirror ADRs landed, `auth-manager.test.ts` and
`last-admin-guard.{ts,test.ts}` gained citations, and the two changesets
the card counted have since been released into CHANGELOGs (`.changeset/`
carries no `ADR-0071` today). 24 source files are touched; the 39 in the
card is neither the file count nor the line count of what actually
needed moving.

## How each citation was classified — per site, by meaning, never by
path

The split is not a judgement call this PR invented. **AGENTS.md Prime
Directive objectstack-ai#13** states it directly:

> Decisions that draw the open/closed or commercial boundary live in
`objectstack-ai/cloud` and are cited from this repo as `cloud ADR-NNNN`
— ⛔ never as a bare number […] When a cloud decision's **mechanism
half** governs open code here, this repo carries its own ADR — own
number […] the commercial half left in cloud.

So the question asked at every site was: *which half of the cloud
decision is this sentence about?*

**(b) the open mechanism half → `ADR-0134`, 43 sites.** Every one of
them is describing code that lives in this repository, and ADR-0134's
own Consumers list names those exact files:

- `plugin-auth` (14) — effective SCIM forces the better-auth `admin`
plugin on; the construction-time refusal when `plugins.admin: false`
sits beside effective SCIM; `active:false` → ban + session revocation;
`@better-auth/scim` accepting no `schema` option; the SCIM/SSO adapter
model map.
- `plugin-auth` tests (10) — the four pins on the refusal message,
re-judged in place (see below).
- `spec` (7) — the `admin` flag's docblock and `.describe()` text,
`public-auth-features.ts`'s `notes`, and the v17 `default-changes.ts`
upgrade note.
- `platform-objects` (9) — the `protection.reason` on the eight
`sys_scim_*` objects and the `sys_user` action note.
- `qa/dogfood` (3), `pnpm-workspace.yaml` (1, the rc.2 seven-model
migration), `docs/qa/platform-checklist/areas/identity-auth.json` (1).

**(a) the commercial / boundary half → `cloud ADR-0071`, 1 site.**
`auth-manager.ts:3682`, *"the paid Identity lifecycle"* — that is D6 of
the cloud record, the paid-Enterprise-Identity framing which ADR-0134
§*What stays in the cloud record* explicitly refuses to restate. It
stays cited to cloud.

**Dataset meaning → untouched, 22 sites.** Each was read, not inferred:
all 22 are about multi-hop `include` chains, the 3-hop limit, join
allowlists and Cube joins — `service-analytics` (15),
`spec/src/ui/dataset.zod.ts` (3),
`lint/validate-dataset-references.{ts,test.ts}` (2), `analytics.mdx`
(1), `query-syntax.mdx` (1).

## Positive control against over-rewriting

The discrimination rule is not a regex over the id — it is an explicit
per-file allowlist of identity files, with a qualifier-aware
substitution inside them. Two independent proofs that nothing on the
dataset side moved:

```
$ git diff --stat origin/main -- packages/services/service-analytics packages/lint \
      packages/spec/src/ui/dataset.zod.ts content/docs/data-modeling/analytics.mdx \
      content/docs/protocol/objectql/query-syntax.mdx docs/adr/
(empty)
```

and, in the other direction, the already-qualified `cloud ADR-0071` at
`auth-plugin.ts:1210` survived a pass over its own file untouched — the
substitution skips any id already carrying a `CROSS_REPO_QUALIFIERS`
word.

## Bare `ADR-0071` still resolves — the gate is not weakened

Identical summary lines, before and after:

```
main   check-adr-anchors: OK (53 anchored file(s) … 35477 citation(s) across 4521 file(s) resolve; 1022 decision-letter citation(s) …)
HEAD   check-adr-anchors: OK (53 anchored file(s) … 35477 citation(s) across 4521 file(s) resolve; 1022 decision-letter citation(s) …)
```

22 bare `ADR-0071` citations remain in the tree and the gate resolves
every one of them. **Ablation**, to show that is a measurement and not a
vacuous pass — one bare dataset citation mutated to an id with no
record:

```
on-disk proof  : ADR-0071 3 -> 2 ; ADR-9071 injected = 1
ABLATION check-adr-anchors exit = 1
  • ADR-9071 is cited by 1 file(s) but names no record under docs/adr/ —
restored blob  : 5c90e77
RESTORE SETTLED: blob matches HEAD and 'git diff HEAD' is empty
```

The restore was settled by blob hash plus an empty `git diff HEAD`, ⛔
never by an exit code — `trap` is unreliable in this container (objectstack-ai#17875).

## Pins re-judged in place, ⛔ none deleted

`auth-manager.test.ts` carries four `toThrow(/…ADR-0071…/)` pins on the
operator-facing refusal message and one test name. They pin the
message's *content*, and the content moved, so the pins move with it —
the assertions still pin exactly what they pinned before: that the
refusal names the ADR that explains the coupling. Reverse-verified that
they are live rather than decorative: with the shipped message mutated
back to `ADR-0071`, the two message pins fail loudly.

```
MUTATED vitest exit = 1
AssertionError: expected [Function] to throw error matching /plugins\.admin[\s\S]*ADR-0134[\s\S]*p…/
AssertionError: expected [Function] to throw error matching /OS_SCIM_ENABLED[\s\S]*ADR-0134/
restored : f9b5c2a
RESTORE SETTLED: blob == HEAD and 'git diff HEAD' empty
```

## Generated docs

`content/docs/references/system/auth-config.mdx` lines 105 and 215
follow their producer (`auth-config.zod.ts`'s `.describe()`),
regenerated with `pnpm --filter @objectstack/spec gen:schema && …
gen:docs`. Exactly two lines drifted; nothing else in the 222 generated
files moved. `check:docs`, `check:generated` and
`check:authorable-surface` are green.

## Changeset — measured, not defaulted

`patch` for `@objectstack/plugin-auth`, `@objectstack/platform-objects`,
`@objectstack/spec`. Published bytes really do move, measured against
each package's `files[]` after a build:

- `@objectstack/spec` — `files[]` lists `src/**/*.zod.ts`, so the
changed `.describe()` ships verbatim; the generated `json-schema/`
bundle (also in `files[]`) carries it too.
- `@objectstack/plugin-auth` — `dist/index.mjs` carries 4 `ADR-0134`,
including the operator-facing refusal string (positive control: a
shipped literal greps at 1; negative control: a comment-only marker
greps at 0).
- `@objectstack/platform-objects` — `dist/index.mjs` carries the 9
`protection.reason` strings.

`patch` and not `minor`: no export, no schema shape, and no
accept/refuse face moves. The refusal fires on exactly the condition it
fired on before; only the ADR number inside its sentence changes. The
changeset says so, because a deployment grepping that message for
`ADR-0071` is the one consumer this can surprise.

## Reverse-read — which existing sentence does this make false?

Three, all of them in `docs/adr/**`, which this lane ⛔ must not touch
(governed surface, PD objectstack-ai#14). None is falsified in substance; each goes
tense-stale:

1. `docs/adr/0134-env-side-scim-provisioning.md:36` — *"Identity code
that writes a bare `ADR-0071` today therefore cites, by this repo's own
convention, the wrong document."* After this lands, no identity code
does. The sentence's headline claim — that `ADR-0071` is an ambiguous
string in this repo — stays **true**: the local 0071 is still the
dataset record and cloud's 0071 still exists.
2. `docs/adr/0134:38` and `:294` — *"Re-pointing the existing bare
citations is objectstack-ai#14361's work and is deliberately ⛔ not done by this
file."* Still true about the file; the pointer becomes past tense.
3. `docs/adr/0135-identity-and-access-architecture.md:59` —
*"Re-pointing the identity surface's existing bare citations at this
record is [objectstack-ai#14361]"* — same class, and see the scope note below, because
the `ADR-0024` half of that sentence is **still outstanding and still
true**.

**Reverse direction — a sentence this makes true rather than false:**
`docs/adr/0134:37`, *"**Always write `cloud ADR-0071` for the SCIM
record**, and `ADR-0134` for this one."* That instruction was correct
and simply unobeyed by the tree; this PR is the tree obeying it.
**Zero** other sentences in the tree assert a present-tense count or
claim about these citations — the only `39 files` strings in the repo
belong to `packages/cli` and are about something else entirely.

## The one place this PR departs from the dispatch brief

The dispatch brief asks for **every** identity citation to read `cloud
ADR-0071`, citing ruling `5507409725`. Three sources on `main` say
otherwise, and they agree with each other:

1. **The ruling's own amendment**, comment `5507573601`, same director
seat, 14 minutes later, self-titled *"Ruling amended"*: *"**Target for
the (b) set** (citations that mean the cloud decision's *open mechanism
half*): the **new local ADR numbers** that objectstack-ai#14506 / objectstack-ai#14507 / objectstack-ai#14508 land
— not `cloud ADR-NNNN`"*, and *"the operator-facing refusal text in
`auth-manager.ts:359` re-pointed to the new local number"*.
2. **The release comment** `5594577301` that unblocked this card
restates it: *"the (b) citation targets are the new local ADR numbers
those cards land"*, naming ADR-0134 as the mirror of cloud 0071.
3. **AGENTS.md Prime Directive objectstack-ai#13**, quoted above, which is binding
regardless of any comment and says mechanism-half → this repo's own
number.

Under the unamended reading, ruling B is still satisfied by this diff
(every identity citation is now unambiguous and the gate discriminates),
but 43 of the 44 would read `cloud ADR-0071` instead of `ADR-0134`. If
the reviewer prefers that reading it is a one-command flip on this
branch; I did not pick a side silently, which is why this section
exists.

## What this PR leaves open on the same card

The amendment also folds the sibling collision class into this card —
bare `ADR-0024` and bare `ADR-0081`, the mirrors of which landed as
ADR-0135 and ADR-0133 — *"one card over the identity surface (0071 +
0024 + 0081) … so it is not paid twice"*. The dispatch brief scopes this
lane to `ADR-0071` only, and the sibling surface is large: **151** bare
`ADR-0024` lines and **86** bare `ADR-0081` lines, most of which
legitimately mean this repo's own `0024-mcp-connectors` and
`0081-trusted-react-page-tier` records and must **not** move.
`docs/adr/0135:260` says so in as many words — *"Whether any individual
bare `ADR-0024` citation should move … cannot be a search-and-replace"*.
That is a real per-site pass with its own budget, and
`docs/adr/0135:260` says so in as many words. `objectql-adapter.ts:58`
now reads `See ADR-0024 / ADR-0134.`, where the `ADR-0024` half is
knowingly left for it.

⭐ **This PR therefore delivers the `ADR-0071` third of objectstack-ai#14361 — the 44
sites above — and nothing else.** The per-site judgement over bare
`ADR-0024` and bare `ADR-0081` is the remaining work of **the same
card**, not a new one: the amendment's Scope line reads *"the identity
surface's **whole collision class, one pass**: bare `ADR-0071`, bare
`ADR-0024`, bare `ADR-0081` — every citation read for its meaning"*, and
`docs/adr/0135:260` calls the `ADR-0024` half *"objectstack-ai#14361's per-site
call"*. That is why the first line of this body says `Part of objectstack-ai#14361`
and not `Closes`: merging this while two thirds of the ruled scope is
unwritten would close the card on a third of its work. The card stays
open for the next round.

## Verification

| what | result |
|---|---|
| `pnpm check:adr-anchors` | green, byte-identical summary to `main` |
| `pnpm --filter @objectstack/{spec,plugin-auth,platform-objects} test`
| 476 + 111 + 40 files, **16 517** tests, all pass |
| same three, `typecheck` | green (incl. `check:test-typecheck` ledgers,
unmoved) |
| `check:authorable-surface` · `check:docs` · `check:generated` | green
|
| `check:platform-checklist` · `check:doc-anchors` ·
`check:docs-single-h1` · `check:doc-authoring` ·
`check:docs-spec-enumerations` | green |
| `check:nul-bytes` · `check:published-files` ·
`check:corpus-claim-drift` · `check:quick-reference-counts` ·
`check:comment-mask-{adoption,corpus}` ·
`check:spec-docblock-symbol-anchors` | green |
| changeset gates (`check-changeset-fixed`, `check-changeset-no-major`,
`check-empty-changeset`, `check:changeset-gate-self-tests`) | green |
| `check:doc-frontmatter` · `check:docs-section-name` ·
`check:keyed-text-bounds` · `check:platform-object-tenancy-census` ·
`check:reference-carrier-shape` · `check:adr-0087-registration` | green
|
| `eslint . --no-inline-config` — the **whole repo**, not a narrowing |
**6743** files, 0 errors, 0 warnings, at `8d2dfb3` |

39 gate commands, every one `exit 0`, captured before any pipe.

**Declared narrowing.** `node scripts/pm/dispatch-gates.mjs --ran`
derives 123 commands from this change set and accounts 39 of them. I ran
the families this card names plus every one I could see implicated, and
— since it turned out to fit the foreground budget — the repo-wide lint
union rather than a narrowing of it. The remaining derived families are
CI's farm, not this lane's run. `packages/qa/dogfood`'s three edits are
comment-only — no dogfood boot was run locally, and that layer is
declared to CI.


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

---------

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
….md's spec pointer states the landed rule (objectstack-ai#18037) (objectstack-ai#18111)

Fixes objectstack-ai#18037

Two `references/` lines still carried pre-ruling wording after the
charter chain landed. `landing-operations.md` — the file a seat reads at
exactly the moment it decides whether to flip ready — sent **every**
governed hit to the human-merge lane, and `lanes/ui.md` :22 restated the
rule its SKILL.md target had retired. Both lines now state the landed
rules. Two files, equal line counts, no ceiling moved.

## What changed

**`.claude/skills/pm-dispatch/references/landing-operations.md`
:27–:29** — the pre-ready / pre-arm path fork now names the two landed
landing tiers:

- **facts tier** — a PR whose governed paths **all** lie under this
skill's `references/`: the in-seat contract-tier review record carries
it through 落地前检三条, then ready and the queue (`contract-review.md`; the
guard side of the same tier is the queue guard's references tier,
already landed).
- **rules layer** — everything else: the 四件套, draft until a human merge
or an authorized APPROVED review, after which the claiming seat lands it
(ruling C).
- ⛔ Neither tier is ever approved by a seat, and the kept half
「清标即落地同受此闸,漏判会被队列守卫在 merge group 里拒收」 survives verbatim.

**`.claude/skills/pm-dispatch/references/lanes/ui.md` :22** — the
pointer now states the rule its target actually carries (新
`packages/spec` 工作恒归 `domain:spec` 席,已派发卡 ⛔ 不因此转席), still pointing at
SKILL.md 〈多仓协调〉. Line :25 is untouched.

## The rules the rewrites agree with (verbatim on `origin/main`
`66aa2d98d`, untranslated)

- ruling A, SKILL.md :230 — 「新 `packages/spec` 工作恒由 `domain:spec`
席收口,不论谁需要它;已派发卡 ⛔ 不因触 spec 转席。」
- the tiering, SKILL.md 〈复核〉 :625–:626 — 「受管面两层:事实层仅本技能
`references/`,其余为规则层(含发布 `skills/**` 与 SKILL.md)。」 / 「规则层四件套等人合;事实层
PR(受管路径全在该目录)经席内达档复核后 ready → 入队。」
- `contract-review.md` :47 — 「规则层等维护者的字;受管路径全在
`.claude/skills/pm-dispatch/references/` 者达档过本三条入队。」
- ruling C, `lanes/ui.md` :25 (landed, untouched here) — 「⇒ 命中即停 draft;⛔
未获授权批准不 ready 不入队不自合、永不批准,获批后认领席落地。」

## Premise readings P1–P4

Taken in worktree `objectstack-issue-18037` on the branch base
`66aa2d98d`, before any edit, at **2026-09-14T03:05:55Z**:

- **P1** — `git grep -n '人工合并道' --
.claude/skills/pm-dispatch/references/landing-operations.md` → **exit
0**, hit at `:27`; its neighbour `:28` read 「⛔ 不翻
ready、不入队;清标即落地同受此闸,漏判会被队列守卫在 merge group 里拒收。」 ✅ premise holds.
- **P2** — `git grep -n '凡触' --
.claude/skills/pm-dispatch/references/lanes/ui.md` → **exit 0**, hit at
`:22`; the SKILL.md line it points at (:230) states the retiring rule. ✅
premise holds.
- **P3** (control) — `git grep -n 'mergeable_state' --
.claude/skills/pm-dispatch/references/landing-operations.md` → **exit
0**, 2 hits (`:29`, `:31`). ✅ premise holds.
- **P4** — `node scripts/pm/check-skill-line-ratchet.mjs` → **exit 0**,
the two rows verbatim:
- `✓ check-skill-line-ratchet:
.claude/skills/pm-dispatch/references/landing-operations.md is 69 lines
(ceiling 69; headroom 0).`
- `✓ check-skill-line-ratchet:
.claude/skills/pm-dispatch/references/lanes/ui.md is 38 lines (ceiling
38; headroom 0).`
- ⚠️ The card's `80/80` for `landing-operations.md` is **stale** — the
ceiling ratcheted down to 69 before this card was dispatched. 69/69 and
38/38 are what bind, and both files come out of this PR at exactly those
numbers.

## The executable criterion, before and after

Measured at **2026-09-14T03:16:09Z** (after the edit, same worktree),
exit codes captured before any pipe:

| grep | before | after | required |
| --- | --- | --- | --- |
| `人工合并道` in `landing-operations.md` | exit 0 (`:27`) | **exit 1** |
exit 1 ✅ |
| `凡触` in `lanes/ui.md` | exit 0 (`:22`) | **exit 1** | exit 1 ✅ |
| control `mergeable_state` in `landing-operations.md` | exit 0, 2 hits
(`:29`, `:31`) | **exit 0, 2 hits** (`:30`, `:32`) | must still hit ✅ |

Repo-wide, `git grep '人工合并道\|凡触' -- .claude/` now exits 1: no residue
elsewhere on the surface.

## Density payment, and why no ceiling moved

The fork needs three lines where two stood (one tier per line, plus the
kept 清标 half). ⛔ The ceiling was not raised and ⛔ no rule was deleted —
the line is paid for inside the same file by merging the two lines that
both said the same thing, that stripping labels happens in the same
action as confirming MERGED:

- old `:34` 「确认 MERGED 的同一动作里给 `Part of` 卡收口,`Fixes` 卡代关但标也须摘。」
- old `:37` 「摘标与 MERGED 确认是一个动作,⛔ 不拆到下轮巡检。」
- now one line — 「确认 MERGED 同一动作里给 `Part of` 卡收口、`Fixes` 卡代关但标也须摘,⛔
不拆到下轮巡检。」

Every clause of both lines survives: the `Part of` close-out, the
`Fixes` auto-close with its label still owed, the same-action
requirement, and ⛔ 不拆到下轮巡检.

**Per-line budget.** The ratchet's second rule is `MAX_LINE_BYTES =
120`. Every line written here was measured with the gate's own
`classifyLine` before it was written to disk — 117 / 112 / 111 bytes
(the three new `landing-operations.md` lines), 118 bytes (the merged
line), 120 bytes (`lanes/ui.md` :22) — each classified `null`, i.e.
compliant.

## Gates

### Follow-up commit `460b36b33` — 等人合 → 等人批

The PM seat's pre-read of head `7900d80b` caught a wording defect in the
new rules-layer line: 「四件套留 draft 等人合」 restated the human-MERGE lane
this card exists to remove. Under ruling C the rules layer waits for a
human APPROVAL and the claiming seat merges — the shape `lanes/ui.md`
:25 already carries — so the line now reads 「四件套留 draft 等人批,⛔
不翻正式不入队;获授权批准后认领席落地。」 Byte-neutral (112 bytes, `classifyLine` returns
`null`), equal line count, no ceiling moved, pushed as a second commit
rather than an amend. ⚠️ Worth a seat's eye: SKILL.md :626 — the landed
tiering line itself — still spells the same endgame 「规则层四件套等人合」, and
AGENTS.md Prime Directive objectstack-ai#14 keeps 人工直合 as one of the two endings. This
PR aligns the runbook line with `lanes/ui.md` :25 and ⛔ does not touch
either of those; the wording difference across the surface is noted, not
filed.

Derived, not recalled: `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` with no paths passed, so the change
set comes from the merge base (2 paths, working tree). All 15 derived
families re-run **after the final commit**, at `460b36b33` (and
identically at `7900d80bd` before the follow-up):

```text
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/pm/check-governed-queue-guard.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:pm-governed-merges :: exit 0
pnpm check:pm-skill-id-lint :: exit 0
pnpm check:pm-skill-ratchet :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:skill-frame-sync :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:pm-governed-prose :: exit 0   (dispatch-named; the derivation scores it an artifact roster, i.e. silent, not clear)
```

The ratchet at `460b36b33` still reads `landing-operations.md is 69
lines (ceiling 69; headroom 0)` and `lanes/ui.md is 38 lines (ceiling
38; headroom 0)`, and the criterion still passes on that head (人工合并道
exit 1, 凡触 exit 1, control `mergeable_state` exit 0 with 2 hits).

Reconciliation, from `node scripts/pm/dispatch-gates.mjs --ran … --repo
objectstack-ai/objectstack`:

```text
Run reconciliation — 15 derived, 15 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 15 derived famil(ies) accounted for — 15 run, 0 NOT-MEASURED (a DERIVED zero — all 15 recorded an exit code and none of them is 3).
```

One gate needed a prerequisite: `check:doc-formula-expressions` first
exited **3** (PREREQUISITE NOT MET — `@objectstack/formula` and
`@objectstack/lint` not built), which is not a finding. Built through
the shared verify lock (`OS_VERIFY_LOCK_SLOT=dev-18037`, `VERDICT
command-exit 0 · held the lock 1s · waited 0s`) and re-run to exit 0.

Beyond the gates: control-character self-scan over both files (`grep
-naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'`) exits 1 — clean.

Repo-wide scans (`pnpm lint` and the rest of the lint farm) are CI's
run, not this PR's local owing.

## Changeset

None owed; `skip-changeset` is the declaration. The Check Changeset job
(`.github/workflows/pr-automation.yml`, job `changeset-check`) has **no
path exemption** — it reads the PR's label list (the frozen payload as a
fast path, then two live re-reads) and exempts exactly two things: the
`skip-changeset` label and the `changeset-release/main` release PR
authored by `github-actions[bot]`. Nothing published moves here:
`.claude/**` lives outside every package, and no manifest's `files[]`
names it (0 of 82 manifests).

## Acceptance notes

Out-of-scope observations, noted and ⛔ not filed (no card, no label):

- `contract-review.md` :45 still reads 「③ PR 全部 check 全绿,⛔ 非 required
子集;受管面不适用,draft-only 终局不变。」 while :47 immediately below carves the
references tier out of that 「受管面不适用」. The two lines are consistent read
in order — :47 is the refinement — but a reader who greps only for 落地前检③
meets the older half first. Noted, not filed: the next PR that touches
`contract-review.md`'s 落地前检 block is the carrier. It is prose density,
not a defect, and it is not this card's surface.
- The scope fences from triage were kept: out of scope: objectstack-ai#18019
(`lanes/ui.md` :25, `os-dev.md` 先搜再立, the SKILL.md table rows) — not
addressed here and left open; out of scope: objectstack-ai#18020, the queue-guard half
— not addressed here, and this PR does not depend on it.

## 维护者速读(草稿)

**改了什么** — 两行落地指引的措辞。落地跑册 `landing-operations.md` 原来说「受管面一命中就整个 PR
走人工合并道、⛔ 不翻 ready 不入队」;现在按已落地的两层分档说话:受管路径全在 pm-dispatch 自己的
`references/`
目录里的,经席内达档复核走队列落地;其余(SKILL.md、AGENTS.md、agents、hooks、ADR、发布 skills 等)照旧留
draft 等维护者的字或授权批准。`lanes/ui.md` 的 spec 指针原来说「凡触 `packages/spec`
一律转席」,而它指向的那条规则早已改成「新 spec 工作归 spec 席;已派发卡不因触 spec 转席」,现在指针与目标一致。

**为什么改** — 旧措辞在被读到的那一刻就是错的指令,而且失败方向是静默的:席位会把一个本可落地的 PR 停在 draft
上,等一个裁决已经取消了的维护者点击,没有任何东西会报错。指针那条更尖:读者信了指针而不去跟进目标,拿到的是已退休的规则,却带着现行规则的权威。

**风险与代价(含回滚)** —
只动两个文件里的散文,无代码、无产物、无发布物;两文件行数不变(69/38),棘轮上限未动。风险是措辞被读窄或读宽:facts
层的判据必须是「受管路径**全部**在该目录」,本 PR 按此原文写。回滚 = 还原这一个 commit,没有任何迁移或数据面。

**席位意见** — (留空,待席位定稿)

**你要做的** — 一个动作:确认两层分档的措辞与您在 SKILL.md 〈复核〉 与 `contract-review.md`
上已落地的裁决一致;若一致,本 PR 属事实层,按已落地规则经席内复核走队列,无需您合并。

Clause-②: no


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

---------

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…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 <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…e direction is never an error (objectstack-ai#18135)

Fixes objectstack-ai#17963

## Why

`references/lanes/spec.md` answered one question — what **trips** clause
② — and was silent on the other: whether declaring `Clause-②: yes` on a
narrowing anyway is allowed. With the rulebook silent, a PR body became
the authority. The measured cause, verbatim from PR objectstack-ai#17498's body:

> This is a spec narrowing, and the objectui#8285 precedent says a spec
narrowing declares `yes`.

That is a per-card director ruling, not rulebook text, and nothing in
that body says so. Both statements are true at once — the rulebook says
what trips clause ② mechanically, the ruling said what one card should
declare — but nothing said so in one place, so the next author either
argues about which text wins or quietly stops declaring on narrowings
because the rulebook says they do not trip.

The conservative direction was already sanctioned in this same skill,
just never joined to the narrowing rule. `references/contract-review.md`
(unchanged by this PR) carries both:

> `Clause-②: yes | no` 按设计临时:只定是否必过席内契约复核的保守方向,⛔ 非终审。

> claim 拿不准 ⇒ 按 `yes` 挂标走席内契约复核;⛔ 不建全量分类学与 claim 时决策程序。

## The edit

One rule line, inserted directly under the rule it reconciles (`:19` →
new `:20`):

```text
- 收窄不触发条款②,但按 `yes` 申报恒不是错误;⛔ 个案裁决不改本行。
```

93 bytes. It carries all three ruled halves: a narrowing does not TRIP
clause ②, declaring `yes` on one is never an error, and a per-card
ruling does not change this line. No issue number in operational text,
so the provenance above lives in this PR rather than in the rule.

## Line budget — net 0, ceiling untouched

`lanes/spec.md` is pinned at 43 lines
(`scripts/pm/check-skill-line-ratchet.mjs`), zero headroom, every line ≤
120 bytes.

- **Extending `:19` in place was measured first and does not fit.**
`:19` is **108 bytes**; the budget leaves **12 bytes** — four CJK
characters — and the shortest wording carrying the new substance needs
30 or more.
- **The line is therefore paid for by deleting content, not by
re-wrapping.** `- 生成物门禁重生成提交,⛔ 手改。` leaves the same section. Its rule
survives elsewhere, in files that outrank this one:
- `AGENTS.md` → the Documentation Guardrails table (**AUTO-GEN** ❌ Never
hand-edit. Regenerated by …), § *Touched `packages/spec`? Regenerate its
artifacts BEFORE pushing* (match the change to the gate and regenerate
up front; no `check:` script regenerates anything), and Multi-agent
discipline §11 (`pre-commit` refuses a commit that still owes a
regeneration).
- this skill → `references/core-rules.md` 「入队资格是每一个检查全绿 ⛔ 不是必查子集;碰生成物的
PR 入队前先同步再重生成。」 and `references/landing-operations.md` §A.
- Arithmetic: 43 → 43 lines, +1 / −1. `LC_ALL=C awk 'length($0)>120'`
over the file prints nothing, before and after.

## Acceptance greps (both directions)

| assertion | reading |
|:---|:---|
| new clause present | `git grep -n '恒不是错误' --
.claude/skills/pm-dispatch/references/lanes/spec.md` → 0 hits on
`origin/main`, 1 hit (`:20`) here |
| lit control (the grep is not vacuous) | `git grep -n '收窄' --
…/lanes/spec.md` → `:19` still hits, plus the new `:20` |
| file surface | `git diff --stat origin/main` → `1 file changed, 1
insertion(+), 1 deletion(-)` |
| neighbours untouched | `git diff --stat a90a9f2 --
…/contract-review.md …/core-rules.md …/SKILL.md` → empty for each |
| line count | 43 → 43 |
| byte width | every edited line ≤ 120 bytes (`LC_ALL=C awk`) |

## Gates

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths — the tool takes its own change
set), all run in the foreground with `$?` captured before any pipe, then
reconciled with `--ran`:

> ✓ dispatch-gates --ran: 15 derived famil(ies) accounted for — 15 run,
0 NOT-MEASURED (a DERIVED zero — all 15 recorded an exit code and none
of them is 3).

All 15 exit 0, plus `pnpm check:pm-governed-prose` (exit 0, outside the
derivation, named by the dispatch). The named ones:
`check:pm-skill-ratchet`, `check:pm-skill-id-lint`,
`check:pm-governed-prose`, `check:nul-bytes` — the ratchet's own verdict
line reads `✓ check-skill-line-ratchet: declared cross-file moves: 1,
total ceilings down 9 lines.` with no ceiling raised.

Seven of the sixteen first exited **3 (PREREQUISITE NOT MET)** in the
fresh worktree — not a verdict. Six cleared after `pnpm install`;
`check:doc-formula-expressions` also needed `pnpm exec turbo run build
--filter=@objectstack/formula --filter=@objectstack/lint`, run under
`scripts/pm/os-verify-lock.sh` (`VERDICT command-exit 0 · held the lock
172s · waited 0s`), and then exited 0.

## Local verification scope

The diff touches no package, so there is no dependency-closure build and
no package test or typecheck to owe. The repo-wide lint is CI's run, and
the narrowing here is an empty intersection **measured by eslint
itself**, not asserted: `pnpm exec eslint --no-inline-config --format
json` on the edited path reports one file, 0 errors, and the warning
`File ignored because no matching configuration was supplied` — every
`files:` selector in `eslint.config.mjs` names only
`.ts/.tsx/.mts/.cts/.js/.jsx/.mjs/.cjs`, so a Markdown file is outside
the linted population and cannot move any untouched file's verdict.

## Changeset

`skip-changeset` label, applied through the additive labels endpoint and
read back. `.claude/**` ships in no package's `files[]` — it publishes
nothing.

## Acceptance notes (out of scope, not filed)

- The card's own citations have drifted by line number: it cites
`contract-review.md:9` for 「拿不准 ⇒ 按 `yes`」, which on `a90a9f267` is
`:14` (`:9` is the 保守方向 line), and `core-rules.md:113` for the review
rule, which on `a90a9f267` is `:112` (`:113` is the dispatch-word rule).
Nothing in the tree is wrong; this is why the new line cites by content
and not by number. Successor: whoever reads this card next — no repo
change owed.
- `lanes/spec.md:25` points at 「SKILL.md 模型分档」, which is not a heading
in `SKILL.md`; the rules it means are under `### 派发` and the keyword
does occur there (`:501`), so the pointer resolves by grep, not by
section. Polish, not a defect. Successor: the next edit to either file.

## 维护者速读(草稿)

**改了什么** —— PM 技能包 spec 车道说明加一行规则:收窄不触发条款②,但按 `yes`
申报恒不是错误,个案裁决不改这条。行数配额是零余量,这行由同一节里删掉「生成物门禁重生成提交,⛔ 手改」买单 —— 那条规则在
AGENTS.md 与本技能包的 core-rules / landing-operations 里都还在,不是丢掉。

**为什么改** —— 规则只写了「什么触发条款②」,没写「不触发的能不能照样申报」。空白处被一份 PR
正文顶上去当了权威:它引一张个案裁决说收窄要申报
`yes`。两句话其实都对(一个讲机制,一个讲那张卡),但没有一处把它们放在一起,下一个作者就得在两份文本之间二选一,或者干脆不再申报。一行话把口子合上,保守方向照旧许可,成本是零
—— 语义面卡本来就按契约复审档施工。

**风险与代价(含回滚)** —— 代价是删掉的那条生成物提醒不再出现在 spec 车道页,读者要去 AGENTS.md
看(那份文件本来就要求全文读,且冲突时它为准)。风险低:纯说明文字,无代码、无发布面、无生成物。回滚 = revert 这一个
commit,文件回到 43 行原样。

**席位意见** ——

**你要做的** —— 受管面(`.claude/**`),按 Prime Directive objectstack-ai#14 由维护者人工合并:本 PR 保持
draft,未挂 ready、未入队、未开
auto-merge。确认那一行读起来就是你要的裁决,以及「删这条买那条」的取舍你接受,然后人工合并。

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

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

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…us get-session (objectstack-ai#18140)

Closes objectstack-ai#18079

`Clause-②: no` — this neither loosens an accept set nor widens a
published contract surface. It tightens one CI assertion onto the
contract the code already declares.

## The question this card asked, and the answer

objectstack-ai#18079 named two hypotheses and asserted neither: either the anonymous
`GET /auth/get-session` behaviour regressed (fix the auth surface), or
the smoke's expectation is stale (fix the probe). **The second is true,
and it is established from the tree rather than inferred.**

`packages/plugins/plugin-auth/src/anonymous-session-refusal.ts` exists
for exactly this, and its header records the ruling:

> Director seat, decision batch objectstack-ai#117 item 4 (2026-09-12), maintainer
verbatim 「17238 B」 — the server answers the platform's standard ADR-0112
failure envelope with HTTP 401 instead of `200` + `null`, and
`SessionResponseSchema` is UNTOUCHED. The charter rule quoted in that
ruling: 「spec 与代码不一致默认改代码,改协议单独立卡非选项」.

`AuthManager.handleRequest` calls it on the one seam every vendor route
passes through, and the call site says in as many words that this one
**is** the admission move:

> ⚠️ NOT the same kind of change: that one is forbidden to move
admission and this one IS the admission move (`200` -> `401`).

So the 401 is the declared contract. The smoke was asserting a shape the
platform deliberately stopped serving. ⇒ **The auth surface is not
touched by this PR.**

Three independent legs agree on the exact envelope, so the assertion is
pinned to a measurement and not to a guess:

| leg | source | reading |
|---|---|---|
| the ruling | `anonymous-session-refusal.ts` header | 401 + ADR-0112
envelope |
| the wire | run 34774426350 / 34728125950 job log |
`{"success":false,"error":{"code":"UNAUTHENTICATED","message":"Sign in
first"}}` |
| the spec | `packages/spec/src/api/errors.zod.ts:174` | `401:
'UNAUTHENTICATED'` |

## Bisect — the true first failure, not the observed window

The card cautioned that the observed window (back to `b06b2db5c4`) is
not the start, because the runs on `a83dbb6124` and `6d647858b7` read
`no-run` and `cancelled`. Correct — and the real boundary is a day
earlier and is **not a `main` commit at all**.

- Causal commit on `main`: **`374d9d3afa`** — `fix(plugin-auth)!: an
anonymous get-session is refused with the declared 401 envelope, not
answered 200 null (objectstack-ai#17881)`, 2026-09-12T19:15:14Z. `git log
--diff-filter=A` names it as the commit that *adds* the refusal module;
the shallow graft boundary here is `ca0a1f83d6` (2026-07-29), far older,
so that add is genuine and not a graft artifact.
- Last green RC smoke: run `34726616436`, 2026-09-12T23:54:27Z, status
posted to RC head `a1effc8e44` (`chore: version packages`,
2026-09-12T18:12:26Z).
- First red RC smoke: run `34728125950`, 2026-09-13T00:30:11Z, job
`103645853837`, status posted to RC head `e612087feb` (`chore: version
packages`, 2026-09-13T00:29:49Z). Its log carries the same assertion and
the byte-identical body as the card's evidence run.

Those two runs carry the **same** `main` head sha (`a9c6477904`) and
opposite verdicts, which is the card's "NOT main-red" point showing up
as data: the smoke tests the release candidate, not the commit the check
attaches to. The decisive probe is therefore on content, not on timing —

```
                                anonymous-session-refusal.ts   platform-admin-gate.ts   (nonsense path)
a1effc8  LAST GREEN RC              ABSENT                        PRESENT                 ABSENT
e612087  FIRST RED RC               PRESENT                       PRESENT                 ABSENT
```

The firing control is present in both trees (the probe reaches them) and
the nonsense control is absent in both (the probe can say no).
`374d9d3afa` (19:15:14Z) falls inside the interval between those two
`chore: version packages` commits (18:12:26Z → 00:29:49Z), so the two
readings cross-validate.

## What changed

One file, `scripts/publish-smoke.sh`:

1. The anonymous probe now expects **401** and is paired with
`assert_body '.error.code == "UNAUTHENTICATED"'`. The status alone would
be satisfied by an origin check, a rate limiter or any later guard while
measuring nothing — the same reasoning the `SELF_REGISTRATION_CLOSED`
probe below it is already written under, and what this file's header
means by *"every assertion here is an HTTP status plus a `code` this
repo owns and publishes"*.
2. A comment pinning **why**, naming objectstack-ai#17881, objectstack-ai#17238 and the ruling, so
the next reader does not "fix" it back — the card asked for this
explicitly.
3. The header's declared-contract table updated to match.

The **signed-in** probe is deliberately untouched and still asserts 200:
the refusal seam converts only a 200 whose body is exactly `null`, so
that answer is byte-identical to before.

This is the objectstack-ai#14000 move repeated — that card re-pinned this same script
to the declared contract for `SELF_REGISTRATION_CLOSED` rather than
touching auth runtime code, and left a standing ⛔ against relaxing an
assertion *back toward 200*. This change runs the other way (200 → 401),
which is the direction that prohibition protects.

## Verification

- `bash -n scripts/publish-smoke.sh` → exit 0.
- The new `jq` filter, tested against the byte-exact body from job
`103645853837` and two controls: observed body → exit 0; a 401 carrying
`INVALID_ORIGIN` → exit 1; the old `null` → exit 1. So the assertion
accepts the real refusal and rejects both a foreign guard and the
retired shape.
- The full derived gate family for this diff — 26 commands from `node
scripts/pm/dispatch-gates.mjs --commands` — all exit 0, including
`check:bash32-floor` and `check:nul-bytes`.
- Control-character self-scan over the edited file: no matches.
- ⚠️ The smoke itself is **NOT MEASURED** locally. It packs ~70
tarballs, installs and builds a project outside the workspace and boots
a dev server; that is far past this container's foreground budget and
its shared-verification discipline. The real verdict is the next
`publish-smoke / packed-tarballs` status on the release-candidate head.

## Reverse-read, both directions

**Which currently-true sentence does this make false?** Inside this
file, the header's `→ 200 (anonymous)` row — updated in the same diff,
so the file does not contradict itself. Outside it, none: I grepped
`get-session` across `packages`, `scripts`, `docs`, `content` and
`.github` and no other statement depends on the smoke asserting 200.

**Which currently-false sentence does it make true?** Two. The file's
own claim that it asserts *"the DECLARED first-run contract"* was false
for this row and is now true. And the status text `Fresh install of the
release candidate: auth + CRUD green`, which this gate has been unable
to post since 2026-09-13T00:30Z, becomes reachable again — that is the
release-blocking half.

**Zero results reported as such:** no other consumer of the smoke's
expectation exists; no docs page restates it.

## Noted separately

The same grep found that objectstack-ai#17881 moved the wire answer but left the
**client SDK** still documenting `-> 200 null`
(`packages/client/src/index.ts:1474`) and a test double still modelling
it. That is `domain:services`, not this lane, and it is filed on its own
card — see objectstack-ai#18139. It is not addressed here.

## Merge channel

⚠️ The dispatch brief expected this fix to land in
`.github/workflows/publish-smoke.yml` and warned that such a PR cannot
be armed by the PM seat (HTTP 422, token lacks `workflows`). **That
premise does not hold** — the probe lives in `scripts/publish-smoke.sh`,
the workflow's driver script, and the workflow YAML contains no
assertion at all. This diff touches no path under `.github/workflows/`,
so that caveat does not apply to it. It also touches no governed surface
under Prime Directive objectstack-ai#14.

No changeset: the changed file is a CI driver shipped by nothing.
Measured rather than assumed — the root package is `private: true`, and
of the 70 package manifests declaring `files[]`, zero name this path.
The `skip-changeset` label carries that, not this sentence.

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

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…he pre-ruling landing (objectstack-ai#18148)

Fixes objectstack-ai#18083

`scripts/pm/check-governed-queue-guard.mjs` decides on an authorized
APPROVED review and has
since the 2026-09-04 predicate landed. What had not moved was the text a
seat is told to
consult: the docblock called a draft awaiting the maintainer's own merge
the regime's terminal
state, and the printed remedy's preferred option told a seat to take the
PR out of the queue
"and leave the merge to the maintainer". Since the charter chain landed
(PR objectstack-ai#18018 `9489e2c0`,
PR objectstack-ai#18038 `c185d087`, PR objectstack-ai#18051 `137eb00e`) the rule is ruling C — issue
objectstack-ai#17971, maintainer
2026-09-13, verbatim and untranslated:

> C. approve 后不管后续改动都由席位落地:

So the narration under-permitted: it told a seat not to do the thing the
rule now says it
should do once an authorized approval is on record.

## What moved

Wording only. ⛔ No decision branch changed — `entrySatisfied`, the
approval reduction, the
tier split and every exit code are untouched. Anchored by content, not
by line number.

1. **Docblock, the "why the PR leg must not redden" paragraph.** The
healthy resting state is
now a draft *waiting for an authorized approval*, in the landed rule's
own words from
`.claude/skills/pm-dispatch/references/landing-operations.md`: 「四件套留
draft 等人批,⛔ 不翻正式不入队;获授权批准后认领席落地。」
2. **Docblock, a new paragraph for what happens after the approval** —
ruling C, quoted
verbatim, plus `SKILL.md`'s operational half: 「席位落地 =
过落地前检、清标、ready、auto-merge,踢出/变基同法。」
The seat's ready → enqueue is now stated as the correct next act there
rather than a violation.
3. **Docblock, the "this guard cannot stop a direct merge" paragraph.**
The direct merge is now
the OTHER landing rather than the only one — `references/core-rules.md`:
「受管面由维护者人合或授权批准后席位落地」.
   The objectstack-ai#11387 measurement it cites is kept verbatim as history.
4. **The `pull_request` leg's EARLY WARNING rendering.** The ⛔ list
(flip ready / enqueue / arm
auto-merge) is now explicitly conditional — "while no authorized
APPROVED review is on
record", with AGENTS.md Prime Directive objectstack-ai#14's own "lift only for that
approval" — and a new
   ✅ line says what a seat DOES do after the approval.
5. **The `merge_group` refusal remedy.** Steps 1 → 2 are now an order
rather than a menu:
out of the queue first, then the authorized approval, and the CLAIMING
SEAT lands it from
there. The unapproved direct merge (人工直合) is kept as the landing that PR
still has.
6. **Two internal comments** that restated the same pre-ruling shape:
the tier-default
asymmetry cost (was "costs one hand merge") and the exit-code precedence
note.

The references tier is untouched and still says the in-seat review of
record lands it: remedy
option 3 renders byte-for-byte as before, and both of its pins (offered
on a references-only
refusal, withheld on a rules-layer one) still pass.

## Acceptance readings

Taken on `a2e4cd7d9`, in the worktree, against merge base `a90a9f267`.

**The two pre-ruling phrases, in the narration and the printed text: N →
0.**

| reading | before | after |
|---|---|---|
| `git grep -c -E 'leave the merge to the maintainer\|human merge IS the
review record'` | 2 | 2 |
| … of those, hits in narration or printed remedy | 2 | **0** |
| … of those, hits inside the self-test pin that FORBIDS them | 0 | 2 |

Both remaining hits are the new negative assertion itself — a comment
and the regex literal in
`⛔
a-refusal-never-tells-a-seat-to-leave-the-merge-to-the-maintainer-nor-calls-that-merge-the-record`.
A pin has to name what it forbids; neither is text the guard ever
prints.

**The landed phrase: 0 → 5** (`git grep -c 'CLAIMING SEAT
lands\|claiming seat lands'`).

**Every hit of the pre-ruling wording enumerated** (`git grep -n -E
'hand merge|human merge|leave the merge|人工合'`),
6 before → 4 after, and each remaining one accounted for:

- `:44` 「人工合并即人工审核」 (docblock) — **moved**.
- `:52` "the human merge IS the review record" (docblock) — **moved**.
- `:536` "costs one hand merge" (tier-default comment) — **moved**, now
"costs one authorized approval".
- `:1171` 「人工合并即人工审核」 (EARLY WARNING rendering) — **moved**.
- `:1242` "leave the merge to the maintainer. A human merge" (remedy) —
**moved**.
- `:2010` (was `:1983`) the `objectstack-ai#9319` replay fixture's name, quoting PR
objectstack-ai#9238's own body: "a .claude/skills PR whose own body said 'awaiting a
human merge'" — **stays**. It is a historical measurement naming what
that PR said in 2026; rewriting it would falsify the fixture.
- `:2429`, `:2436`, `:2468` — **new**, the three negative pins (two
English phrases, one 人工合并即人工审核).

**Self-test:** `node scripts/pm/check-governed-queue-guard.mjs
--self-test` :: exit 0,
**233 cases before → 238 after** (5 added, none removed, no battery
floor lowered).

**Diff surface:** `git diff --stat a90a9f2` names exactly one file,
`scripts/pm/check-governed-queue-guard.mjs`, 104 insertions / 32
deletions. `origin/main`
advanced under this worktree during the run (`a90a9f267` → `739ab526d`,
a shared-ref hazard
AGENTS.md names), so the anchor above is the merge base, not the moving
ref.

**Governed?** No — `node scripts/pm/check-governed-merges.mjs --test
scripts/pm/check-governed-queue-guard.mjs`
:: exit 0, "NOT governed — ordinary queue landing applies". This PR is
opened as a draft and the
seat lands it; nothing here flips ready or arms anything on its own.

## Reverse verification — the new pins can actually fail

One-shot, from the committed state, with a `trap … EXIT INT TERM`
restoring absolute paths.
Predicted direction: **turns red**.

- HEAD blob `f7938efe94a20f34a3c1e6f07e2aaca393f16a47`.
- Mutation: the remedy's step-2 line rewritten back to the pre-ruling
wording. On-disk
observation, anchored on both texts: landed anchor 1 → **0**, injected
stale phrase 0 → **1**;
blob `87b912dbc8acaf5af5d02fb86cc06eb2bcffd986` ≠ HEAD blob, so it
reached disk.
- Mutated leg: `--self-test` :: **exit 1**, `✗ 2 of 238 case(s) failed`
— exactly

`a-refusal-orders-the-remedy-DRAFT-then-the-authorized-APPROVAL-then-the-CLAIMING-SEAT-lands-it`
and `⛔
a-refusal-never-tells-a-seat-to-leave-the-merge-to-the-maintainer-nor-calls-that-merge-the-record`.
- Restore leg: `git checkout HEAD -- FILE` → blob back to `f7938efe…`
(byte-identical),
`git diff HEAD` empty, `git status --porcelain` empty, anchor restored
1, injected 0.
- Restored leg: `--self-test` :: exit 0, 238 cases pass.

## Gates

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`
(no paths, three-dot against merge base `a90a9f267`) — **33 commands,
all run in the
foreground, every exit code captured before any pipe, all `exit 0`.**
Reconciled:

```
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RAN_FILE
Run reconciliation — 33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN.
✓ 33 derived famil(ies) accounted for — a DERIVED zero — all 33 recorded an exit code and none of them is 3.
```

Including `node scripts/pm/check-governed-queue-guard.mjs --self-test`
:: exit 0 and
`pnpm check:nul-bytes` :: exit 0. `pnpm check:pm-governed-merges` ::
exit 0 was run too, though
the derivation does **not** place it for this path — it is a
`--self-test`-only checker-health
family here, so its green grades that checker's fixtures, not this diff.

Control characters: `grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'` over
the changed file — no hits.

No changeset: `scripts/pm/**` ships in no package's `files[]`, so this
publishes nothing — the
`skip-changeset` label carries it.

## Acceptance notes

Two observations outside this card's one-file surface, noted and not
filed (this seat's write
budget for the round is the branch push, this PR, the label and one
report comment):

- `scripts/pm/check-governed-merges.mjs` :140 and
`scripts/check-required-contexts.mjs` :288
both still say "a human merge IS the review record" for a governed PR.
Neither is wrong —
it is one of the two terminals the charter names 「终局两条:人工直合即审核记录;授权批准 ⇒
席位落地。」 —
but neither mentions the second one. That is an omission rather than a
contradiction, so it is
not the class this card is. Issue objectstack-ai#18083 fences the first file off from
this PR by name.
- `.claude/hooks/guard-governed-enqueue.sh` :548 was checked and is
already ruling-C shaped
(its steps read draft → wait for the approval → "Then enqueue"), so it
needed nothing.

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

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

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…lock 1, the two-tier landing and ruling C (objectstack-ai#18119) (objectstack-ai#18182)

Fixes objectstack-ai#18119

**Governed rules layer** — `.claude/skills/pm-dispatch/SKILL.md` +
`AGENTS.md` + `.claude/skills/checklist-test/SKILL.md`. This PR opens as
DRAFT and draft is its finished state: it waits for an authorized
APPROVED review by an account in `GOVERNED_APPROVERS`, after which the
claiming seat lands it (Prime Directive 14; ruling C, maintainer
verbatim, untranslated: 「C. approve 后不管后续改动都由席位落地:」, recorded on
objectstack-ai#17971). Never ready, never queued, never auto-merge, and no seat
approves it under any account.

Base `origin/main` `fffe3e5e7f` (fetched 2026-09-14T11:14Z); this branch
`claude/issue-18119-rules-layer-four-lines` at `6dc9ac3e52`, one commit,
three files, net 0 lines in each (`git diff --numstat`: 1/1 · 3/3 ·
3/3).

## What changed — seven equal-line edits, no other line moves

Every rewritten line stays within the enforced 120-byte cap
(`MAX_LINE_BYTES = 120` in `scripts/pm/check-skill-line-ratchet.mjs`);
byte widths after each row.

| File | Line | Before | After |
|---|---|---|---|
| pm-dispatch `SKILL.md` | 378 | ``- ⛔ `issue_write`
会替换标签集并清空未传字段:写入时必须回传 `assignees`。`` (96 B) | ``- 标签/assignee 写恒经
`scripts/pm/label-write.mjs`(四步、回读);⛔ 永不 MCP `issue_write`(锁 1 已拒)。``
(119 B) |
| pm-dispatch `SKILL.md` | 615 | `- PR 上的标签 =
待维护者审阅,不入六态;与请审同为等人合清单,随合并或撤回判决离开。` (116 B) | `- PR 上的标签 =
待维护者审阅,不入六态;与请审同为等人批清单,随获批或撤回判决离开。` (116 B) |
| pm-dispatch `SKILL.md` | 626 | `- 规则层四件套等人合;事实层 PR(受管路径全在该目录)经席内达档复核后
ready → 入队。` (115 B) | `- 规则层四件套等人批;事实层 PR(受管路径全在该目录)经席内达档复核后 ready →
入队。` (115 B) |
| `AGENTS.md` (PD 14) | 273 | `uncertified recompute, drift or a
hand-authored sibling keeps it governed. Unapproved, the bypass direct
merge` (114 B) | `uncertified recompute, drift or a hand-authored
sibling keeps it governed. Unapproved, no seat lands it: the` (112 B) |
| `AGENTS.md` (PD 14) | 274 | `(人工直合) is the only landing. **Landing is
tiered**: a PR whose governed paths all lie under` (102 B) | `ending is
that approval, then the owning seat. **Landing is tiered**: a PR whose
governed paths all lie under` (113 B) |
| `AGENTS.md` (PD 14) | 284 | `human merge IS the review record, ⛔ not a
relaxation. Behind it: the queue guard refuses an unpinned governed`
(115 B) | `spent approval IS the review record, ⛔ not a relaxation.
Behind it: the queue guard refuses an unpinned governed` (118 B) |
| checklist-test `SKILL.md` | 123 | ``用 `issue_write`(github MCP)立单:``
(36 B) | ``经 REST 代理 `POST .../issues` 立单(`Content-Type:
application/json`;⛔ 永不 MCP `issue_write`,锁 1 已拒):`` (117 B) |

Notes on the wording, each taken from the facts layer rather than
invented:

- :378 uses `rest-channel.md` :38–:39's words
(`scripts/pm/label-write.mjs`, 四步、回读, ⛔ 永不 MCP `issue_write`, 锁 1 已拒);
the full four-step rule already lives at SKILL.md :148, so the
parenthetical is a pointer, not a restatement.
- :615 — 等人合清单 → 等人批清单; the exit clause 随合并…离开 → 随获批…离开 keeps the same
semantics in the ruling-C mechanism: :617 has the seat clear the label
(清标) as the first landing step, on the approval, so the label leaves on
approval or on a withdrawn verdict, not on the merge.
- :626 — 等人合 → 等人批, the operative token of `landing-operations.md` :28
(「四件套留 draft 等人批,⛔ 不翻正式不入队;获授权批准后认领席落地」). The 获授权批准后认领席落地 half already
stands at SKILL.md :616–:617 (「席位落地 = 过落地前检、清标、ready、auto-merge」), and
no spelling that carries both halves fits the 120-byte cap with the 事实层
half unchanged (the shortest measured 131 B), so the line carries the
token and :617 carries the mechanism.
- AGENTS.md :273–:274 — the 人工直合 ending becomes the ending `lanes/ui.md`
:25 states (「⛔ 未获授权批准不 ready 不入队不自合、永不批准,获批后认领席落地」): unapproved, no seat
lands it; the ending is that approval (the authorized APPROVED review by
a `GOVERNED_APPROVERS` account named four lines above), then the owning
seat. The sentence says nothing about what the maintainer may do by
hand, so it does not contradict the queue guard's own refusal text (see
Consistency, item 5).
- AGENTS.md :284 — "a human merge IS the review record" is the phrase
the queue guard's self-test asserts ABSENT from its refusal text
(`check-governed-queue-guard.mjs` :2436, "never … calls that merge the
record"); under ruling C the maintainer's word is spent once, as the
approval, so "a spent approval IS the review record" — same sentence,
same line count.
- checklist-test :123 — the REST channel in `rest-channel.md`'s own
spelling (`POST .../issues`, request body as JSON). The field list that
follows (标题 · 标签 · 正文, :125–:150) is already the shape of a REST issue
body — title, labels, body — so no line below :123 needed to move.

## Premise readings (all against `origin/main` `fffe3e5e7f`)

- **P1 content** (2026-09-14T11:16Z) — all five anchors matched by
content at the named numbers: SKILL.md :378, :615, :626; AGENTS.md :274
(「(人工直合) is the only landing.」 inside PD 14); checklist-test :123. A
sixth line of the same shape sits at SKILL.md :610 (not named by the
card; see Consistency item 1).
- **P2 ratchets** (2026-09-14T11:16Z) — `pnpm check:pm-skill-ratchet`
exit 0 on the tip: SKILL.md 812/812 (widest row 342/342), AGENTS.md
1075/1075 (widest row 768/768), checklist-test 234/238 (widest row
221/221), core-rules 151/151. Frame block :733–:754 md5
`3327d02c56f8a0eca88569dad2270f32` before and after — identical.
- **P3 core-rules.md** — the target grep `issue_write|等人合|人工直合` reads 1
hit, NOT 0: :128 「…代裁清单、等人合项、受管合并审计与五指标。」 — the round-report rule, which
mirrors SKILL.md :672 (「awaiting a human merge 项」), not any of the four
lines. The control `grep -c 'label-write\|四件套'` reads 0 in that file as
well (neither control word is in core-rules today; stated as read). Per
the dispatch's branch, core-rules.md is untouched; the mirrored pair is
listed under Consistency item 2.
- **P4 facts layer** — quoted verbatim: `rest-channel.md` :37 「✓ 标签加法
`POST .../issues/{n}/labels`,定向删 `DELETE
.../issues/{n}/labels/{name}`;加法优先。」 :38 「标签/assignee 写恒经
`scripts/pm/label-write.mjs`:四步内建、回读、回退整组 PATCH 回传 assignees。」 :39 「⛔ 永不
MCP `issue_write`(锁 1 已拒);会话分类器拒改动 ⇒ 无通道,交有通道席位立卡。」; `lanes/ui.md` :25
「⇒ 命中即停 draft;⛔ 未获授权批准不 ready 不入队不自合、永不批准,获批后认领席落地。」;
`landing-operations.md` :27 「受管路径全在本技能 `references/`
者事实层:席内达档复核过落地前检三条即转正式入队。」 :28 「其余为规则层:四件套留 draft 等人批,⛔
不翻正式不入队;获授权批准后认领席落地。」 :29 「⛔ 两层不由席位批准;清标即落地同受此闸,漏判会被队列守卫在 merge group
里拒收。」
- **P5 serial** (2026-09-14T11:16Z) — `GET /pulls?state=open` returns
exactly the six the dispatch named (18176, 18175, 18173, 18131, 18096,
17076); each `/files` list read via REST; zero hits on the three files.
- **P6 lock 1** — `permissions.deny` on the tip carries **14** entries,
not 15, with `mcp__github__issue_write` first among them; the same 14 at
lock 1's own commit `7ef05f997`. The 15 was a miscount; the substance
(the tool is denied) holds.

## Consistency — sentences of the same retired shape this PR does NOT
move (the card's fence: no other line)

1. pm-dispatch `SKILL.md` :610 「② PR 留给维护者看得见地悬着;终局两条:人工直合即审核记录;授权批准 ⇒
席位落地。」 — still two endings, with the human direct merge as the review
record (the exact phrase retired at AGENTS.md :284 here and in the queue
guard). Not named by the card; left as is. An equal-line candidate
within the cap, for the seat to accept or reject: 「- ② PR
留给维护者看得见地悬着;终局一条:授权批准即审核记录 ⇒ 席位落地。」
2. pm-dispatch `SKILL.md` :622 「④ 轮次报告单列 awaiting a human merge。」 and
:672 「…awaiting a human merge 项…」 with its core-rules mirror :128 「等人合项」
— the round-report item still named after the human merge; per 〈优先级〉 :44
the :672 ↔ :128 pair moves together, in one PR.
3. `AGENTS.md` :255 (the PD 14 headline) — "confirmed and merged by the
maintainer, by hand — or confirmed by an authorized approval and then
landed by the owning seat" still names the hand merge as ending one. NOT
changed on purpose: `scripts/pm/check-governed-prose.mjs` :148 anchors
its AGENTS.md region on that exact sentence (`start: 'A governed surface
is confirmed and merged by the maintainer, by hand'`), so the sentence
and the anchor must move in the same PR — outside this card.
4. `AGENTS.md` :799 (Skills section) — "human-merge only, or queued
under Prime Directive objectstack-ai#14's pinned-approval path" — outside PD 14,
outside the card.
5. The mechanism layer still names 人工直合 for the unapproved case:
`scripts/pm/check-governed-queue-guard.mjs` :1197 and :1273 print
"Unapproved, the maintainer's own direct merge (人工直合) is the only
landing this pull request has", and its self-test :2441 pins that word
present; `landing-operations.md` :49 keeps 人工直合 as the main-red one-line
exception; `lanes/director.md` :44 says 「等人合清单」. The :274 wording chosen
here is compatible with all of them (it constrains seats, not the
maintainer).
6. Script header comments still carrying the phrase:
`scripts/pm/check-governed-merges.mjs` :140 and
`scripts/check-required-contexts.mjs` :288 ("human merge IS the review
record") — comments, not rules.

## Gates (foreground, exit captured before any pipe, all on
`6dc9ac3e52`)

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (18 families; the seat's derivation named
the same set). Each ran with stdout and stderr redirected into its own
log file and the exit status captured immediately after, before any
pipe:

| # | Family | Exit | Verdict line |
|---|---|---|---|
| 1 | `node scripts/check-closing-keyword-parity.mjs` | 0 | OK (3
parsers agree on all 9 keywords…) |
| 2 | `… --self-test` | 0 | 30 assertions, 5 mutations each driven to
red |
| 3 | `node scripts/check-comment-mask-corpus.mjs` | 0 | 6752 files, 0
disagree, 0 unparseable |
| 4 | `node scripts/pm/check-governed-queue-guard.mjs --self-test` | 0 |
238 cases pass |
| 5 | `pnpm --filter @objectstack/lint run
check:doc-formula-expressions` | 3 → 0 | first run PREREQUISITE NOT MET
(not a verdict: `@objectstack/formula`, `@objectstack/lint` unbuilt);
built under `scripts/pm/os-verify-lock.sh` (`VERDICT command-exit 0 ·
held the lock 5s · waited 0s`), re-run exit 0: 58 self-test cases, 22
record-scoped examples / 438 files clean |
| 6 | `pnpm check:agent-test-spelling` | 0 | 0 violations — 508 files |
| 7 | `pnpm check:doc-authoring` | 0 | 15340 strings across 978 spec
sources clean |
| 8 | `pnpm check:docs-audit-scope` | 0 | 14 release-owned pages
review-only; scope injection live |
| 9 | `pnpm check:driver-memory-census` | 0 | every declaration ledgered
|
| 10 | `pnpm check:nul-bytes` | 0 | scanned 8655 text files, no raw
ASCII control bytes |
| 11 | `pnpm check:pm-governed-merges` | 0 | 328 assertions; the real
generator declared 9 outputs and certified this tree |
| 12 | `pnpm check:pm-governed-prose` | 0 | 2 instruction surfaces name
all 5 registered governed surfaces and claim no others |
| 13 | `pnpm check:pm-skill-id-lint` | 0 | 27 files clean |
| 14 | `pnpm check:pm-skill-ratchet` | 0 | SKILL.md 812/812 (342/342) ·
AGENTS.md 1075/1075 (768/768) · checklist-test 234/238 (221/221) ·
core-rules 151/151 |
| 15 | `pnpm check:refd-timer-probe` | 0 | 6747 files swept |
| 16 | `pnpm check:required-contexts` | 0 | 7 required contexts pinned;
6 instruction surfaces scanned against 2 retired names |
| 17 | `pnpm check:skill-frame-sync` | 0 | the one declared copy is
internally coherent; 74 markdown files scanned for undeclared copies |
| 18 | `pnpm check:watch-hint-literal` | 0 | 70 declarations across 4
rostered names |

Reconciliation (`--ran`, exit codes recorded per command): `Run
reconciliation — 18 derived, 18 run, 0 NOT-MEASURED, 0 UNRUN.` … `✓
dispatch-gates --ran: 18 derived famil(ies) accounted for — 18 run, 0
NOT-MEASURED (a DERIVED zero — all 18 recorded an exit code and none of
them is 3).` Also outside the derivation: the pre-push hook
`check:commit-card-trailers` on the push (1 commit, no card relation, no
model identifier in the trailer pair); the control-byte self-scan `grep
-naP` over the three files reads none.

Changeset: none owed. The three paths are `.claude/**` and `AGENTS.md` —
nothing any package's `files[]` ships. The `changeset-check` job in
`pr-automation.yml` has two exemptions (the `skip-changeset` label and
the release PR) and no path exemption, so the label is applied through
the additive endpoint `POST /issues/{n}/labels` right after this PR
opens and read back; the read-back is recorded in the os-dev-report
comment on the card (this body is not re-sent).

## 维护者速读(草稿)

- 改了什么:四行规则文本(加同段两句)追上已落地的锁 1、两级落地与裁决 C —— `issue_write`
已被拒、规则层等的是批准而不是人合、批准后由认领席落地。
- 为什么改:规则层与它自己的 references 说法相反,会把席位训练回已退役的机制;文档以实际实现为准。
- 风险与代价(含回滚):零机制变化,纯文案,等行数;回滚即 revert 本 PR。
- 席位意见:(留空)
- 你要做的:一次授权批准(os-zhuang / hotlong 任一账号 APPROVE);之后由认领席落地,不需要你再点合并。

## Acceptance notes

- noted, not filed: Consistency items 1–6 above — same retired-shape
residue in lines the card's fence excludes (SKILL.md :610/:622/:672,
core-rules :128, AGENTS.md :255/:799, the guard's refusal text and two
script comments). 承接者: the `domain:skills` seat (owner of SKILL.md,
`references/**` and `scripts/pm/**`); items 3 and 5 need a gate/anchor
edit in the same PR as the prose.
- noted, not filed: the dispatch's P6 count (15) versus the measured 14
— a miscount in the dispatch text, no file to fix. 承接者: 无.
- Deviation, declared: the dispatch asked for a three-line 速读;
`.claude/agents/os-dev.md` fixes five segments, and that file wins on
conflict — five compact bullets above.
- No footer block was sent with this body (the platform appends one on
create per `platform-readings.md` :338); the body was read back after
creation.

Clause-②: no

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

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…ol — the reading discipline names the broken-instrument case (objectstack-ai#18044) (objectstack-ai#18194)

Fixes objectstack-ai#18044

**Governed rules layer** (`.claude/skills/**`, Prime Directive objectstack-ai#14):
this PR is a DRAFT by design and stays one — never ready, never queued,
never auto-merged, no approving review from any seat. Ruling C on
objectstack-ai#17971: 「C. approve 后不管后续改动都由席位落地:」 — the skills seat runs the
four-piece and lands after an authorized APPROVED review. One commit
(`7ec2a70acc`), two files, net 0 lines.

## What changed

| file:line | before (bytes) | after (bytes) |
|---|---|---|
| SKILL.md :161 | `- 零命中须用确定存在的邻近词反查;控制词须与主张同主体,否则该零作废,不是阴性。` (113) | `-
零命中须配同主体必中词,否则该零作废;同主体 = 同语料/路径形/包界/引法/失效形态。` (116) |
| SKILL.md :162 | `- 同主体 = 同语料、同路径形、同包界、同引法、同失效形态:跨行短语配跨行控制词。` (114) |
`- 同仪器的控制词双零是仪器坏,⛔ 不读作缺席:换法重画再报;哑火仪器与干净结果同值。` (117) |
| core-rules.md :45 | `- 零命中须用必中词反查,且与主张同语料/路径形/包界/引法/失效形态,否则该零作废。`
(113) | `- 零命中须配同主体必中词,否则该零作废;同仪器的控制词双零是仪器坏,⛔ 不读作缺席。` (117) |

Byte widths are line content without the newline, as
`scripts/pm/check-skill-line-ratchet.mjs` measures them (budget 120 per
line).

The two SKILL.md lines, mapped to the card's suggested shape:

- **(a) kept** — every zero needs a same-subject lit control or the zero
is void: `零命中须配同主体必中词,否则该零作废`. The same-subject enumeration stays on
:161 in core-rules's slash spelling (`同主体 = 同语料/路径形/包界/引法/失效形态`); `同主体`
remains a defined term because `.claude/agents/os-dev.md` :72 uses it.
- **(b) new** — a control drawn with the same instrument as the reading
is not a control: subject 0 and control 0 together indict the
instrument, not the corpus, and the control is re-drawn a different way
before either number is reported: `同仪器的控制词双零是仪器坏,⛔ 不读作缺席:换法重画再报`.
- **(d) new** — the one-line form from comment 5663849431, 「an
instrument that cannot fire returns the same value as a clean result」:
`哑火仪器与干净结果同值` (哑火 = misfire, cannot fire).
- **(c) did not fit** — see Acceptance notes.

core-rules :45 mirrors (a)+(b) in compressed form in the same PR
(SKILL.md 〈优先级〉: 「一条规则在本文与核心条款一处改动,另一处同 PR 同改」). Its two clauses are
byte-identical to the first clause of :161 and the first clause of :162,
so a grep for either clause fires in both files.

## Premise readings — all on `origin/main` = `a81a9d6e79` (worktree
BASE), 2026-09-14

- **P1** (content, 12:54Z): :161, :162 and core-rules :45 read
byte-for-byte as the dispatch quoted them (the *before* column). Subject
grep for a same-instrument clause, `同一仪器|同一工具|失效.*亮控|不能发火|cannot fire`,
over both files: 0 (grep exit 1). The lit control had to be re-drawn:
the first draw, `亮控`, read 0 in both files — the corpus does not spell
the concept that way — so it was re-drawn with the corpus's own
spellings: `控制词` = 3 hits in SKILL.md, `必中词` = 1 hit in core-rules
(:45). Premise holds: neither file said a control drawn the same way as
its reading is not a control.
- **P2** (ratchets, 12:55Z): `pnpm check:pm-skill-ratchet` on the
untouched tip, exit 0. Ceilings from the script's own table: SKILL.md
812 lines / widest table row 342 B; core-rules 151 lines / no table
rows. Measured on the tip: 812, 342 (at :244), 151; longest prose line
120 B (:98). Frame block `sed -n '733,754p' SKILL.md | md5sum` =
`3327d02c56f8a0eca88569dad2270f32`. After the edit: 812 / 342 / 151,
longest prose line still 120 (:98, untouched), frame md5 unchanged
`3327d02c56f8a0eca88569dad2270f32`.
- **P3** (facts layer, 12:54Z): `grep -n -E '亮控|控制词'
references/platform-readings.md` = five rows — :188, :189, :191, :192,
:255 — all about the `search_issues` channel control (「控制词命中只证通道活着」,
「零仍不是读数」). The new lines restate none and contradict none; :192 already
says a control HIT proves only the channel, and this PR adds the other
direction: a control MISS proves the instrument, not the corpus.
- **P4** (serial, 12:55Z): the nine open PRs' file lists via REST
`/pulls/N/files`, paged until a page came back under 100: 18192 (1
file), 18191 (1), 18189 (1), 18187 (25), 18186 (1), 18175 (17), 18131
(2), 18096 (3), 17076 (470 across five pages). None touches
`pm-dispatch/SKILL.md` or `references/core-rules.md`. Control for the
hit-regex: it fires on both literal paths (`true true`). `git ls-remote
--heads origin | grep issue-18044` lists this branch only.
- **P5** (seam, 12:56Z): objectstack-ai#15410 open — 「the repo mandates `--self-test`,
gates that it is wired, and documents nothing about its shape: 168
hand-rolled assertion helpers, and only 20 of 170 can be shown to fail
on zero cases」 — the gate-side half; objectstack-ai#13014 closed — 「A gate that reads
a site with the wrong extractor passes VACUOUSLY instead of failing」.
Named here, nothing taken. ⛔ Nothing mechanised in this PR: no script,
no gate, no self-test change.

## Gates — run on `7ec2a70acc`, sweep finished 13:08Z, each exit
captured before any pipe

Derived by `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (16 families) plus `pnpm
check:required-contexts`, which the seat's own derivation named (this
tree derives 16; the tool files the extra run as 「Outside this card's
derivation … Not an error」).

| command | exit |
|---|---|
| `node scripts/check-closing-keyword-parity.mjs` | 0 |
| `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 |
| `node scripts/check-comment-mask-corpus.mjs` | 0 |
| `node scripts/pm/check-governed-queue-guard.mjs --self-test` | 0 |
| `pnpm --filter @objectstack/lint run check:doc-formula-expressions` |
3 → 0 (see below) |
| `pnpm check:agent-test-spelling` | 0 |
| `pnpm check:doc-authoring` | 0 |
| `pnpm check:driver-memory-census` | 0 |
| `pnpm check:nul-bytes` | 0 |
| `pnpm check:pm-governed-merges` | 0 |
| `pnpm check:pm-governed-prose` | 0 |
| `pnpm check:pm-skill-id-lint` | 0 |
| `pnpm check:pm-skill-ratchet` | 0 |
| `pnpm check:refd-timer-probe` | 0 |
| `pnpm check:skill-frame-sync` | 0 |
| `pnpm check:watch-hint-literal` | 0 |
| `pnpm check:required-contexts` | 0 |

`check:doc-formula-expressions` first exited 3 — 「PREREQUISITE NOT MET —
the workspace package `@objectstack/formula` is not built」 — which is
not a verdict. Built `@objectstack/formula` + `@objectstack/lint` under
`scripts/pm/os-verify-lock.sh` (「VERDICT command-exit 0 · held the lock
17s · waited 0s」), re-ran, exit 0.

Reconciliation, `--ran` with exit codes recorded (separate invocation
from `--commands`): 「Run reconciliation — 16 derived, 16 run, 0
NOT-MEASURED, 0 UNRUN.」 … 「✓ 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).」

Control-byte scan of both files (`grep -naP` over C0/DEL): clean, exit
1. Changeset: none owed — `.claude/**` publishes nothing;
`skip-changeset` added through the additive labels endpoint and read
back.

## 维护者速读(草稿)


**改了什么**:读数纪律补一句话,占原来的两行、不加行:「一个零要配一个必中的控制词」这条不变;新增——控制词和主读数用同一把尺子量出来的「双零」,读作尺子坏了,不读作「语料里没有」,先换个画法重画控制再报任一个数;一句话形式:哑火的仪器和干净的结果给的是同一个值。核心条款
:45 同 PR 压缩镜像。

**为什么改**:一天之内四次撞上同一个坑(卡片正文两例、分诊席一例、objectui 席四例,其中一例是写进四份派发令的常设指令,四个
agent
都「照做并报告满足」,而那条指令根本打不响)。旧文只要求「零要配控制词」,没说控制词自己也回零时该怎么读——于是两个零被当成了干净的阴性。

**风险与代价(含回滚)**:纯规则文本,零机械化、零代码、不发包;行数 812/812、151/151 不动,最宽表行 342 不动,框架块
:733–:754 的 md5 不动。措辞压到 116/117/117 字节,读者第一遍可能要多看一眼「同仪器」「哑火」两个词。回滚 =
revert 本 PR 的一个 commit。

**席位意见**:(留空,席位定稿成评论)

**你要做的**:读两行新文加核心条款一行;认可即授权批准,席位落地;不认可,指出哪个词。

## Acceptance notes

- **(c) did not fit and is NOT in the rules layer.** The cross-line /
concatenated-literal line as a candidate, `-
拼接后才有的短语先拼后搜,有无两向皆然:字符串拼接、短语内 markdown 强调、折行。`, measures 113 B — a third
line, ⛔ forbidden by the equal-line fence. Folded into :162 as `…⛔
不读作缺席:换法重画再报;拼后才有的短语先拼后搜;哑火仪器与干净结果同值。` the line measures 151 B against a
120 B budget. Per the dispatch's fallback, (a)+(b)+(d) landed. (c) in
one sentence for whoever carries it: a phrase that only exists after
joining (string concatenation, markdown emphasis inside a phrase, a
wrapped line) is searched joined, in both the present and the absent
direction — the absent direction is the one in which a defect looks like
success.
- **The old :162 tail 「跨行短语配跨行控制词」 was replaced, not silently dropped.**
It was one worked instance of 同失效形态, which stays in the enumeration; the
new :162 covers the same case from the other side — a cross-line control
that reads 0 under a single-line grep is now a broken-instrument
reading, not a negative.
- **Vocabulary, so the seat's post-check control fires on the right
spelling:** the new clause spells the concept `同仪器` (not `同一仪器`) and the
cannot-fire form `哑火` (not `不能发火`). Phrases to grep for: `同仪器的控制词双零`,
`不读作缺席`, `哑火仪器`. The dispatch's P1 expected-absent pattern
(`同一仪器|…|不能发火`) still reads 0 on the new text — that is the pattern's
spelling, not a missing change.
- **The card's headline 「a control drawn with the same instrument is not
a control」 is carried in its operational form** (`双零是仪器坏,⛔ 不读作缺席`)
rather than the literal `不算控制`: a same-subject control is by
construction drawn with the same grep (同引法 / 同失效形态), so the literal form
would read as contradicting :161; what makes such a control "not a
control" is that its zero is void, which is what the line says. Both
spellings did not fit: :162 with `不算控制` added measures 130 B.
- **Mechanisation seam — named, not taken.** objectstack-ai#15410 (open) holds the
gate-side half: self-tests that cannot be shown to fail on zero cases.
This PR is the reading-side principle. The proposal in comment
5663849431 — a check that refuses to report a number unless its
self-test leg fired — sits on the seam between the two. objectstack-ai#13014 (closed)
is the same class one layer down. objectui's Console Performance Budget
bot (comment 5664006956) is an existence proof of the remedy shape: 「not
measured」 in place of a verdict, the failed precondition named, derived
artifacts withheld.
- **Instance-4 lesson for dispatch orders** (comment 5663849431), not
landed as text: a standing check that ships in a template ships with its
own firing leg — 「confirm X occurs 0 times」 handed to four agents was
satisfied by all four and could not fire. Noted for the seat's
dispatch-order authoring; a rules-layer line for it would not fit here
either and is a separate card if the seat wants one.
- **Write-channel ordering:** the draft PR was opened AFTER the gate
sweep rather than before it — the dispatch's write budget enumerates one
`POST /pulls` and no body PATCH, and the sweep on a three-line prose
diff took minutes with no lock wait, so the verdicts went into the one
body write.
- noted, not filed: this tree derives 16 families for the diff where the
seat's earlier derivation named 18 (incl. `check:required-contexts`);
`--ran` files the extra run as 「Outside this card's derivation」, which
the tool itself calls not an error. 承接者:无.

Clause-②: no

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

Co-authored-by: os-dev <[email protected]>
Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…board it names (objectstack-ai#18223)

Part of objectstack-ai#17512

⚠️ Deliberately `Part of`, and the dispatch asked for a closing keyword.
One half of the card is undelivered and is the seat's to rule on — see
**The half this does NOT deliver**. A merge that closed the card here
would close it while nothing sweeps the surfaces the card is about. One
word flips it back if the seat disagrees.

Clause-②: no
Reason: this is a gate ADDITION and widens no acceptance set. The diff
is one new `scripts/check-issue-citations.mjs` plus one new
root-manifest `check:*` key; no existing gate's rule, threshold, ledger,
exemption list or population is touched, and nothing that used to be
refused is now admitted.

## What this adds

`scripts/check-issue-citations.mjs` — extracts `#N` citations from a
declared surface set and resolves each against this repository's board.

| mode | what it does | verdict |
|---|---|---|
| default (diff-scoped) | judges only the citations **this change
adds**, against the merge base | exit 2 on a finding |
| `--census` | the whole declared surface | report-only, always exit 0 |
| `--probe-cause` | adds the web-endpoint probe that separates
transferred from deleted | — |
| `--list` | extraction only, no network | — |
| `--self-test` | offline; 56 cases over 6 batteries with a per-battery
floor | exit 1 on a breach |
| — | the board could not be read | exit 3, never a quiet pass |

## Re-measured on today's tree, before anything was written

The card was measured 2026-09-10. Re-measured 2026-09-14 against
`2d3d1c969`:

| number | 2026-09-10 | 2026-09-14 | carrier today |
|---|---|---|---|
| 16785 | 404 | **404** | the `dataset-compiler.ts` scope note is GONE;
the number now survives only in a changeset that already records it as
resolving to nothing |
| 16685 | 404 | **404** | still cited from
`content/docs/releases/v17/17-4.mdx:408` (a published page),
`measure-result-type.ts:115` and `aggregate-field-type-compatibility.ts`
|
| 14313 · 14832 · 14366 | 404 | **404** | unchanged |

Controls, same call, same token: 16099, 16737, 17444, 17560, 17242,
15809 all **200**.

⭐ **The card's own control has partly decayed, and that is the sharpest
reading here.** The card cited 16783 / 16784 / 16786 / 16787 — the
immediate neighbours of 16785 — as all resolving, proving a hole in a
dense sequence. Today only **16784** answers 200; **16783, 16786 and
16787 answer 404**. Three numbers stopped resolving in four days, with
no change to this repository.

## The surface is ~500× the five instances

Enumerating the whole board (159 cursor pages) and judging every
citation offline against it:

```
allocation frontier   18,219      the highest number ever minted here
resolvable            15,834
holes                  2,385      13.1% of every number this repo ever minted
```

and through this gate's own `--census --json`:

```
33,750  citations judged across 2,357 files
28,359    resolve
 1,410    resolve as a PULL REQUEST, not an issue
 1,196    cross-repo — UNJUDGED, never a finding
 2,785    UNRESOLVABLE: 103 sites / 71 distinct on the release pages
                        2,682 sites / 439 distinct in package docblocks
```

A second instrument written before the gate existed (a scratch scan with
the same projection) answered 2,782 on the same tree minutes earlier;
the two agree to within the three citations the frontier moved by while
they ran.

**Those two readings together decide the gate's shape.** A tree-wide
blocking verdict is refused for two reasons, not one: 2,785 findings is
the permanently-red gate this repo retired, and — worse — the predicate
is **not a function of this tree**. 16783 / 16786 / 16787 prove a still
tree goes red because somebody else deleted an issue. So the default
verdict is **diff-scoped**: the half of the class an author owns, which
cannot red on a still tree and which stops the debt growing. The 2,785
standing sites are a `--census` reading, not a verdict.

## The four 404 causes, kept apart

⛔ The card's hardest constraint. Each arm has its own decision
procedure, and none of them is a guess.

| cause | how it is decided | measured live? |
|---|---|---|
| `cross-repo-unjudged` | the citation NAMES another repo
(`owner/repo#N` or `repo#N`) — never resolved, ⛔ never a finding | ✅
`objectui#4356` stayed silent in the live ablation |
| `never-issued` | the number is beyond the allocation frontier | ✅
`#18888` against frontier 18221 |
| `transferred` | the web endpoint still redirects out of this repo (the
API does not keep that redirect) | ⛔ **NOT MEASURED** — no positive
specimen exists on this board; exercised only by the self-test stub |
| `deleted` | minted, absent from the board, and the web endpoint 404s
too — named as the residual class | ✅ 16785 under `--probe-cause` |

Without `--probe-cause` the last two do not separate and the finding
carries `allocated-but-absent`, which is a refusal to guess rather than
a third cause. Every unresolvable instance re-probed on 2026-09-14
(16785, 16783, 14366, plus twelve sampled at random from the census)
answered 404 on **both** endpoints, so no citation measured here was
transferred — and this gate's silence is ⛔ not evidence that none ever
was.

## Both directions, measured live on this branch

The ablation ran against the live board, on the committed
implementation, with the mutation proven on disk before the reading was
taken and the restore proven by blob hash afterwards.

```
leg 0  tree clean at HEAD, diff-scoped run           exit 0   "no issue citations added"
mutate one docblock line in a declared surface       blob f318190 -> bd2007413 (marker count 0 -> 1)
leg 1  diff-scoped run with --probe-cause            exit 2   1 deleted, 1 never-issued, 1 cross-repo-unjudged
       [deleted]      ...aggregate-field-type-compatibility.ts:8  objectstack-ai#16785
       [never-issued] ...aggregate-field-type-compatibility.ts:8  #18888
leg 2  git checkout HEAD -- THE_PATH                   blob restored to f318190, git diff HEAD empty
       diff-scoped run                               exit 0   "no issue citations added"
```

⛔ A first attempt at this ablation wrote nothing to disk (a `perl -i`
invocation that swallowed its argument and still exited 0). The on-disk
marker count caught it and the run was declared void rather than
re-rolled quietly; the reading above is from the run that landed.

## The scope contract, written down

The card rules that the hard part is scope, so it is a table with a
reason per row rather than a habit.

**Declared** — start where the damage is measured:

- `content/docs/releases/**/*.mdx` — the published release pages;
16685's carrier.
- `packages/**/src/**/*.ts` and `.tsx`, **comment prose only** (through
`scripts/symbol-anchors.mjs#commentProse`, so a gate's own fixtures and
string literals are blanked and line numbers survive) — 16785's carrier.

**Deferred, and the table is applied as a hard EXCLUSION rather than
kept as prose** (the first draft declared it and swept the files anyway
— 5,102 sites instead of 2,785 — which is why `surfaceFor` now checks
the deferred globs first):

- `**/CHANGELOG.md` — 27,978 citations, generated release prose, and the
surface the card means by 「会被 changelog 引用淹没」.
- `scripts/**` — objectstack-ai#15809's lane. ⛔ Not folded; the card is explicit that
folding the siblings fails the fix.
- `docs/adr/**` — a governed surface with its own anchor corpus; a
citation finding there would force a governed-surface PR over a number
somebody else deleted.
- `.changeset/**` — consumed and deleted at release.
- `packages/**/*.test.ts` and siblings — the next candidate widening,
held back so the first installation is judged on the surfaces the card
measured damage on.

Three grammar narrowings, each measured rather than assumed: a two-digit
floor (all 133 one-digit `#N` tokens in the declared surfaces are
ordinals — `Prime Directive objectstack-ai#9`, `acceptance objectstack-ai#5` — and none names an
issue), a six-digit ceiling (zero such tokens exist, which keeps hex
colours out), and `NON_CITATION_HEADS` for numbering systems that are
not the board's (`Directive objectstack-ai#14`, `batch objectstack-ai#127`, `re-charter objectstack-ai#26` — 405
sites).

## The half this does NOT deliver — the seat's ruling, not an oversight

⛔ **No workflow invokes this file.** `.github/workflows/**` was out of
the dispatch's file surface, and a census of the root manifest's 160
`check:*` keys found **every one of them named by a workflow**, directly
or through its alias — there is no precedent here for a gate that CI
does not run. The manifest carries `check:issue-citations`, which runs
the `--self-test` and nothing else (the `check:pm-half-states` shape,
for the same reason: the live modes need a board and a credential).

So today this is a tool a seat runs, and
`scripts/pm/check-half-states.mjs` states what that is worth: 「an alarm
added to a script nobody runs is still silence」. The two homes it wants
are in **different lanes**, which is the substance of the ruling being
asked for:

1. **the diff-scoped verdict** belongs in `lint.yml`'s `Lint & Repo
Gates` job — per PR, already holds a `GITHUB_TOKEN`, and the verdict is
a fact about the diff;
2. **`--census`** belongs in the patrol lane (`half-state-patrol.yml`) —
scheduled and report-only, which is the only honest posture for a
reading a third party can change between runs.

Neither is installed. A follow-up card owning `.github/workflows/**` can
install both; this branch cannot.

## Tests

- `node scripts/check-issue-citations.mjs --self-test` — 56 cases, 6
batteries, per-battery floor plus a roster-size floor and a verdict
handshake. Batteries: `grammar` (14), `causes` (12), `transport` (7),
`scope-contract` (12), `diff-scope` (6), `live-corpus` (3). The
`diff-scope` battery builds a throwaway git repository and asserts the
green direction and the red direction on the same tree; `causes`
provokes all four 404 arms including the two the live tree has no
specimen for; `transport` asserts the two board strategies answer
board-identically over one stubbed transport.
- `node scripts/pm/dispatch-gates.mjs --commands` → 36 families, all
run, **36 run / 0 NOT-MEASURED / 0 UNRUN** (`--ran` reconciled with exit
codes). Four exited 3 (PREREQUISITE NOT MET, no `dist/`), were re-run
after a full build under the shared verify lock, and all four then
exited 0.
- `pnpm lint` (full repo, `eslint . --no-inline-config`) — exit 0, no
narrowing to declare. Run on `061ab1ac1`.
- `pnpm build` under `scripts/pm/os-verify-lock.sh` — 73/73 tasks
successful.

## changeset: none, measured

No publishable package's `files[]` can ship `scripts/` (0 of 70
publishable manifests carry an entry that could), and the root manifest
is `private: true`. Negative control: the new file's `CITATION_SURFACES`
appears in 0 built `dist/` trees. Positive control, same instrument:
`AGGREGATE_FIELD_TYPE_COMPATIBILITY` appears in 10. Nothing published
moves, so `skip-changeset`.

## Acceptance notes

Noted, not filed — out of scope here, and each names who would meet it:

- **The `#N` citation debt is a 2,785-site class, not a five-instance
one**, and this branch repairs none of it. The carriers of 16685 that
are still live (a published release page and two `packages/spec` /
`service-analytics` docblocks) are deliberately untouched: the
disposition pattern the card records is to strike or annotate in place
with the reason, never to guess a replacement number, and picking three
sites out of 2,785 by hand is arbitrary. Successor: whoever the seat
gives the wiring ruling to, since the census is the input to that
decision.
- **The `/issues` list endpoint refuses `page` beyond a depth** on this
repository ("Pagination with the page parameter is not supported for
large datasets") and GitHub's own `Link` header answers in the
`repositories/{id}/...` form, which this session's proxy refuses. Both
are handled here (`normalizeNextUrl`), and any future board enumerator
in this tree hits the same two walls. Successor: none today — no other
script in this tree enumerates the whole board.
- **`--census` costs 159 requests and grows with the board.** Fine on a
schedule, wrong per PR; that asymmetry is the reason the two lanes above
are named separately.

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

---------

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
… records that decide them (objectstack-ai#18228)

Part of objectstack-ai#14361
Clause-②: no
Nothing this diff touches is an accept/reject surface: no schema, no
validator, no guard predicate and no error code moves. Comments, doc
comments, one operator-facing refusal sentence and four translation
leaves move, and the only runtime string that changes is the ADR number
inside a message whose condition, status and code are untouched.

Segments 2 and 3 of the card. Segment 1 (`ADR-0071` → `ADR-0134`) landed
as objectstack-ai#18098 and the card was returned to `pm:queue` for these two.

## The governing ruling

The director's **amendment** (comment `5507573601`), not the ruling 14
minutes before it. The amendment changed both the target — a **local**
ADR number for the open mechanism half, ⛔ not a `cloud ADR-NNNN`
spelling — and the scope: `0071 + 0024 + 0081`, one trip. Its three
`Blocked-by:` mirror ADRs were released by the H19 unlock double-check
(`5594577301`), and all three records are on `main` today: `ADR-0133`
(mirrors cloud 0081 D1/D3/D4), `ADR-0134` (mirrors cloud 0071),
`ADR-0135` (mirrors cloud 0024's open half).

## The classification rule, written down before any edit

`ADR-0024` names two unrelated decisions from this repository's point of
view, and the split is not guessable from a path. Three objective
criteria decided every line:

1. **The local record has no D-letters at all.**
`docs/adr/0024-mcp-connectors.md` numbers its Decision section `§1`–`§5`
and contains zero occurrences of `SSO`, `ai_seat` or `D5.2`. So any
`ADR-0024 Dk` citation is, mechanically, not about the local record.
2. **`ADR-0135` publishes the mirror boundary itself.** Its Decision
section restates cloud ADR-0024's D1, D2, D4, D5, D5.2, D6, D7 and D9,
keeping the cloud record's lettering on purpose — *"The lettering is the
cloud record's, kept so that a citation of `cloud ADR-0024 D5.2` and a
citation of this record's D5.2 name the same clause."* Its "What stays
in `cloud` ADR-0024" table names D3, D5.1, D8, D10 and V1 and states the
rule: *"code in this repository that means one of them must keep citing
`cloud ADR-0024`"*.
3. **The `②` spelling is identified by name.** ADR-0135 D6: *"Domain
verification is opt-in — the clause this repository's code cites as
`ADR-0024 ②`."* So `ADR-0024 ②` becomes `ADR-0135 D6`, which is
letter-checkable where `②` was not.

## `ADR-0024` — the three-way split, which adds up

Measured per **citation line** with the anchors gate's own regex
(`ADR_CITATION` in `scripts/check-adr-anchors.mjs`), `pnpm-lock.yaml`
excluded, on merge base `1bdbf82cb`.

| Class | Lines | Disposition |
|---|---:|---|
| **Points correctly at the local MCP record** | 15 | ⛔ untouched —
byte-identical to `main` |
| **Points at a clause `ADR-0135` restates** | 73 | re-pointed to
`ADR-0135` **with its letter** |
| **Points at a clause that stays in cloud** | 6 | re-pointed to `cloud
ADR-0024` |
| **Published archive — 7 package CHANGELOGs** | 36 | ⛔ untouched (the
ruling's fence) |
| **Governed surface — `docs/adr/**`** | 22 | ⛔ untouched (Prime
Directive objectstack-ai#14) |
| **Undeterminable** | **0** | — |
| **Total** | **152** | |

The dispatch brief's figure was **151**, taken 2026-09-14. Re-measured
today it is **152**; the one added line is inside `docs/adr/0135-*.md`,
which this diff does not enter.

Letters written, each cross-checked against the record's own decision
index: `D4` ×4 (source-of-truth marking), `D5.2` ×27 (the break-glass
invariant), `D6` ×40 (SSO per environment, including the `②` clause),
`D9` ×2 (environment users and org membership) — 73. Cloud-qualified:
`cloud ADR-0024 V1` ×1 (the SSO default-role provisioning — the very
site ADR-0135's own consequence paragraph names as belonging to cloud)
and `cloud ADR-0024 §7` ×5 (the `ai_seat` synthesis).

⚠️ **`§7` is a judgement, stated plainly rather than buried.** It is in
neither of ADR-0135's two lists — not restated, and not in the "stays in
cloud" table. It is provably not the local record (which has no `§7` and
no seat concept), and `ai_seat` appears **zero** times anywhere under
`docs/adr/`. `cloud ADR-0024 §7` is the only spelling that is true;
re-pointing it at `ADR-0135` would have put a pointer on a record that
does not carry the clause. ⛔ No decision was invented — if the reviewer
prefers a different disposition, these are 5 lines.

## `ADR-0081` — **nothing is owed, and that is the finding**

87 bare citation lines today (the brief said 86). Read per line, the
split is not the `ADR-0071` shape at all:

| Class | Lines |
|---|---:|
| Points correctly at the **local** record
(`0081-trusted-react-page-tier.md` — the `kind:'react'` tier) | 22 |
| Published archive — 8 package CHANGELOGs | 41 |
| Governed surface — `docs/adr/**` (incl. 17 lines that deliberately
**quote** the historical bare label) | 21 |
| Identity/org meaning, inside a dated `history[]` revision entry in
`docs/qa/platform-checklist/areas/identity-auth.json` | 1 |
| Points at the wrong record in live code | **0** |
| **Total** | **87** |

**The identity re-pointing for `0081` already landed** — `d1c86a745`,
*"docs(adr): qualify the pre-repo ADR-0081 citations as cloud ADR-0081,
letter-checked per site"* (objectstack-ai#15612, 2026-09-07). Every live `ADR-0081 D1`
/ `D2` site across `platform-objects`, `plugin-auth`, `organizations`,
`verify`, `plugin-security` and `publish-smoke.sh` already carries the
`cloud` qualifier; bare `ADR-0081 D1` in those two package trees,
excluding CHANGELOGs, measures **zero**, with `cloud ADR-0081 D1`
reading 7 as the firing control.

The one history-entry line is left alone deliberately:
`docs/qa/platform-checklist/README.md` states that run records pin the
revision they ran against and that the history exists to make old run
records interpretable — the same archive class as a CHANGELOG.

## Measurement controls

Every zero carries a firing control and a dark control, because a zero
from a dead instrument looks identical to a real one.

| Reading | Value | Firing control | Dark control |
|---|---:|---|---|
| bare `ADR-0024` | 152 lines / 53 files | `ADR-0069` → 431 lines,
`ADR-0079` → 214 | `ADR-9024` → **0**; `git grep ADR-0024ZZZ` → **0
files** |
| bare `ADR-0081` | 87 lines / 41 files | same | `ADR-9081` → **0**;
`git grep ADR-0081ZZZ` → **0 files** |
| bare `ADR-0081 D1` in `platform-objects` + `plugin-auth`,
non-CHANGELOG | **0** | `cloud ADR-0081 D1` → 7 | `cloud ADR-0081ZZZ D1`
→ 0 |

## Gate strength provably did not fall — it rose

`check:adr-anchors`, run on the merge base and on this branch, and every
delta accounted:

```text
merge base 1bdbf82 : 133 decision number(s); 35559 citation(s) across 4538 file(s) resolve; 1022 decision-letter citation(s)
this branch 5e2bfda : 133 decision number(s); 35572 citation(s) across 4539 file(s) resolve; 1036 decision-letter citation(s)
```

- **+13 citations / +1 file** = exactly the changeset file this PR adds
(7 `ADR-0024` + 6 `ADR-0135` mentions in its prose). Nothing else in the
corpus moved.
- **+14 decision-letter citations** = the new `ADR-0135 Dk` in the two
*anchored* files (`auth-manager.ts` 10, `auth-plugin.ts` 4). Those 14
are now letter-checked against ADR-0135's decision index, where the bare
`ADR-0024 Dk` they replaced was checked against nothing.
- Bare `ADR-0024` still resolves exactly as before: the 15 MCP-meaning
lines are byte-identical to `main`.

## Ablations — three legs, restores settled by blob hash, never by an
exit code

Each mutation is proved on disk by a before/after `grep -c` on the exact
text, **read before any result is read**. Restores are settled by `git
hash-object` equality with the `HEAD` blob plus an empty `git diff
HEAD`; the `trap` was kept only as a crash-path convenience. No
permanent test file was left by any leg. These gates read tracked source
text, not `dist/`, so no rebuild sits between mutation and reading.

| Leg | Mutation | Expected | Observed |
|---|---|---|---|
| **A1** | `ADR-0135 D5.2` → `ADR-0135 D3` in `auth-manager.ts` | red |
**exit 0 — did NOT fire.** See the finding below |
| **A2** | `ADR-0135 D5.2` → `ADR-0135 D99` in `auth-manager.ts` | red |
exit 1 — `ADR-0135 D99 is cited by 1 file(s), but ADR-0135 declares no
D99` |
| **B** | `ADR-0024 §2` → `ADR-9024 §2` in `mcp-connector.ts` (an
untouched local-MCP line) | red | exit 1 — `ADR-9024 is cited by 1
file(s) but names no record` |

A2 is the leg that matters: the letter check **does** fire on the
citations this PR writes, so the 73 re-pointed letters are validated
rather than decorative. B shows the resolution check still reads the 15
lines this PR deliberately left alone.

### ⚠️ Finding surfaced by leg A1 — filed, ⛔ not fixed here

`decisionIndexFor()` in `scripts/check-adr-anchors.mjs` harvests a
decision letter from **any** markdown table cell
(`DECISION_TABLE_CELL`). ADR-0135's *"What stays in `cloud` ADR-0024"*
table has rows `| D3 |`, `| D5.1 |`, `| D8 |`, `| D10 |` — the table
whose whole purpose is to say those clauses are **not** recorded here.
They land in ADR-0135's decision index anyway, so `ADR-0135 D3` passes
the letter check even though a reader following it lands on a row that
explicitly disclaims the clause. That is the same "resolves, but not to
what the citation says" family this card is about, one layer down. It is
⛔ not fixed here: the fix has to distinguish a table of decisions a
record MAKES from a table of clauses it DISCLAIMS, which is a design
call and not a mechanical edit. `ADR-0133` has the same shape for cloud
0081 D2. This PR is unaffected — every letter it writes (`D4`, `D5.2`,
`D6`, `D9`) is a real `### Dk` heading.

## Reverse read — which existing sentence does this change make false?

⛔ Not zero, and all of it lands on `docs/adr/**`, which this lane may
not edit (Prime Directive objectstack-ai#14). Filed, not fixed:

1. `docs/adr/0135-*.md:196` — *"the clause this repository's code cites
as `ADR-0024 ②`"* is present tense and is **now false**: no code cites
`ADR-0024 ②` any more. This PR is what falsifies it.
2. `docs/adr/0135-*.md` consequence paragraph — *"`auth-manager.ts`
cites `ADR-0024 V1`"* now reads stale: that site says `cloud ADR-0024
V1`. Its neighbouring *"Until then the citation still resolves to
`docs/adr/0024-mcp-connectors.md`"* has had its "until then" end.
3. **The reverse direction** — `docs/adr/0133-*.md:40` — *"The code
still carries the pre-repo labels. Several files in
`packages/platform-objects` and `packages/plugins/plugin-auth` cite
`ADR-0081 D1` in comments today"* was **already false on `main`** before
this diff, falsified by objectstack-ai#15612 on 2026-09-07. Independent of this
change.

Statements with a rev or a date attached were checked and are **not**
stale: ADR-0135's "measured 64 citing lines on the commit that
introduced it" is anchored to that commit and stays true.

## Changeset — measured, not assumed

The only criterion is whether published bytes move; `files[]` ships
`dist` in all five. Measured against the built tree with a positive
control:

| Package | What moves | User-visible? |
|---|---|---|
| `@objectstack/plugin-auth` | the operator-facing break-glass refusal
`detail`, and the guard's registration log | **yes** — ⚠️ a deployment
grepping that message for `ADR-0024` should grep `ADR-0135` |
| `@objectstack/platform-objects` | `sys_sso_provider` field help +
`protection.reason`, and the matching leaf in all four shipped locales |
**yes** |
| `@objectstack/spec` | the `ssoDomainVerification` doc comment, shipped
in `dist/` and as `src/**/*.zod.ts` | no behaviour |
| `@objectstack/core`, `@objectstack/cli` | doc comments in `dist/` only
| no behaviour |

So `skip-changeset` is **wrong** here and the label is not applied; a
patch changeset naming all five is included.

## The four locale files are not hand-edited generated output


`packages/platform-objects/src/apps/translations/*.objects.generated.ts`
carry the `ADR-0024 ②` citation in a translated `help` leaf. Their own
header is the rule: *"Edit translations in place… Merge only fills gaps:
correcting a source label/description does not push the correction into
a leaf here that already holds a translation… Re-translate it by hand
when its source changes."* Re-running the extractor would have been a
**no-op** on these four leaves. The producer
(`sys-sso-provider.object.ts`) was corrected as well, and
`check:i18n-coverage` reports `13 config(s), 621 baselined untranslated
string(s), none new`.

No generated reference doc carries `ADR-0024` (`grep` over
`content/docs/references/` reads **0**), so nothing under
`content/docs/references/` needed regenerating — the spec site changed
is a `/** */` doc comment, not a `.describe()` string.

## Verification

- **Gates: 117 of 117 derived families accounted, 0 NOT-MEASURED, 0
UNRUN.** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --ran RECORDFILE` at `5e2bfda81`, every exit
code captured before any pipe and recorded as `COMMAND :: exit CODE`
(the literal word `exit`, then the numeric code). Three families first
returned `exit 3` / `exit 1` **PREREQUISITE NOT MET** because sibling
packages had no `dist` (`check:skill-examples`,
`check:dual-build-cjs-loads`, `check:i18n-coverage`); after a full `pnpm
build` all three were re-run **whole** and are green. ⛔ None of the
three is counted as a finding or as a green measurement in its refused
state.
- **Also run, outside the derivation:** `check:adr-anchors`,
`check:adr-links`, `check:adr-symbol-anchors` — all green.
- **Tests:** `spec` 477 files / 13611 tests · `core` 51 / 1316 ·
`platform-objects` 40 / 575 · `plugin-security` 113 / 2181 ·
`plugin-auth` 111 / 2360 — **792 files, 20043 tests, 0 failures**
(`VERDICT command-exit 0`, 464s under the shared verify lock).
`@objectstack/cli` `--project unit`: 207 files / 2971 tests, 0 failures.
The `integration` tier is declared to CI — this diff touches no spawn
entry, only a comment in `serve.ts`.
- **Typecheck:** `spec`, `core`, `platform-objects`, `plugin-auth`,
`plugin-security`, `cli` — `VERDICT command-exit 0`, 0 `error TS`. ⚠️ An
earlier run of the same command reported `TS7016` against
`packages/spec/dist/**`; that run raced the concurrent gate sweep over
`dist/` and is reported as the artefact it was, not as a finding — the
clean re-run after `pnpm build` is the reading.
- **Lint: not narrowed.** The full union `eslint . --no-inline-config
--format json` at `5e2bfda81`: **6756 files eslint itself selected, 0
errors, 0 warnings**. `eslint.config.mjs` carries no
`parserOptions.project` and no typed rules, so no edit here could move a
verdict on an untouched file.
- **Build:** full `pnpm build` green before the final measurements.

## Why this is `Part of` and not `Fixes`

Segment 2 is done. Segment 3 is measured to owe **zero edits**, but that
is a reading a human should ratify rather than a seat closing the card
on its own — and the residue the card still names is the three
`docs/adr/**` sentences above, a governed surface this lane ⛔ may not
touch. Closing is the PM's call.

⛔ Draft on purpose. ⛔ No auto-merge armed, not flipped out of draft, not
queued.

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

---------

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…bucket into the REST channel register (objectstack-ai#18259)

Fixes objectstack-ai#18130

Measurement-first register work on
`.claude/skills/pm-dispatch/references/rest-channel.md`.
The card's readings 2 and 3 (`ccr/ready_for_review`, `PUT
ccr/auto_merge`) were already on
:48 / :51 and are not re-recorded. What this branch owes is one probe
per UNEXERCISED CCR
route named by the session proxy's own 403 body, plus the shared
per-user MCP bucket fact.
Every route below earned its row by being invoked once; nothing here is
written from the
card's text.

Session: `session_01HZfg2AwVX191qCizp88gQr` (skills seat, dev subagent).
All probes ran on
THIS draft PR, 2026-09-15T04:48Z–04:49Z. No probe touched another seat's
PR.

## Probe log

| # | Route | Method / body sent | Status | Body stored / returned |
Read-back |
|---|---|---|---|---|---|
| P1 | `https://api.github.com/graphql` | POST,
`markPullRequestReadyForReview` mutation | **403** | proxy's own body,
629 B, naming the five CCR routes verbatim | n/a — refused before GitHub
sees it |
| P2a | `POST /repos/{o}/{r}/pulls/18259/comments` | `commit_id` +
`path` + `line: 49` + `side: RIGHT` | **201** | review comment `id:
4012020890`, `user.login: claude[bot]` | thread appears in
`ccr/review_threads` |
| P2b | `GET .../pulls/18259/ccr/review_threads` | GET | **200** |
`[{"resolved":false,"outdated":false,"path":"…rest-channel.md","line":49,"comment_ids":[4012020890]}]`
| — |
| P2c | `POST .../ccr/comments/4012020890/resolve` | POST `{}` | **200**
| `{"comment_ids":[4012020890],"resolved":true}` | `review_threads` →
`"resolved":true` |
| P2d | `POST .../ccr/comments/4012020890/unresolve` | POST `{}` |
**200** | `{"comment_ids":[4012020890],"resolved":false}` |
`review_threads` → `"resolved":false` |
| P2e | `POST .../ccr/comments/4012020890/resolve` | POST `{}` | **200**
| `{"comment_ids":[4012020890],"resolved":true}` | `review_threads` →
`"resolved":true` (final state) |
| P3a | `PUT .../pulls/18259/ccr/auto_merge` | PUT
`{"merge_method":"SQUASH"}`, PR in DRAFT | **422** | `{"message":"Pull
request Pull request is a draft"}` | `GET /pulls/18259` → `auto_merge:
null` (nothing stored) |
| P3b | `DELETE .../pulls/18259/ccr/auto_merge` | DELETE, nothing armed
| **422** | `{"message":"Can't disable auto-merge for this pull
request."}` | `GET /pulls/18259` → `auto_merge: null` |
| P4a | `POST .../pulls/18259/ccr/ready_for_review` | POST `{}` |
**200** | `{"draft":false}` | `GET /pulls/18259` → `draft: false`;
timeline `ready_for_review`, actor `claude[bot]` |
| P4b | `POST .../pulls/18259/ccr/convert_to_draft` | POST `{}` |
**200** | `{"draft":true}` | `GET /pulls/18259` → `draft: true`;
timeline `convert_to_draft`, actor `claude[bot]` |

P1's route list, quoted from the 403 body itself rather than from the
card: `GET
/repos/{owner}/{repo}/pulls/{n}/ccr/review_threads`, `POST
/repos/{owner}/{repo}/pulls/{n}/ccr/comments/{comment_id}/resolve` (or
`/unresolve`), `PUT`
or `DELETE /repos/{owner}/{repo}/pulls/{n}/ccr/auto_merge`, `POST
/repos/{owner}/{repo}/pulls/{n}/ccr/ready_for_review`, `POST
/repos/{owner}/{repo}/pulls/{n}/ccr/convert_to_draft`. It matches the
card's quotation
word for word.

### What the probes settled, against the dispatch's assumptions

- **P2 (which id does `resolve` want?)** — the review-COMMENT id.
`ccr/review_threads`
returns no thread id at all: each element carries `resolved`,
`outdated`, `path`, `line`
and `comment_ids`, and `comment_ids[0]` is exactly what the resolve
route accepts. The
  register row names which.
- **P3 (auto-merge on a draft)** — the PUT is REFUSED, and the refusal
is the reading: 422,
nothing stored, `auto_merge` still `null` on read-back. The DELETE then
also answers 422
("Can't disable auto-merge for this pull request") because nothing was
armed — so a 422
from DELETE is not evidence of a failed disarm, and the read-back is
what decides.
- **`DELETE` on an ARMED PR stays unmeasured here, deliberately.**
Arming auto-merge would
require a non-draft PR, and this PR's diff touches a governed surface,
where Prime
Directive objectstack-ai#14 forbids arming outright. Recorded as not measured rather
than inferred.
- **`convert_to_draft`** was already written as a fact on :48 under a
tick it had never
earned; the probe makes the tick true. No new row is owed for it, so
none is added.

## Rows landed — five, and what paid for each

The file is at its 82-line ceiling with no standing raise exception, so
the currency is
deleted content, never a re-wrap. Line count 82 → 82; bytes 6,738 →
6,965.

| Row added (bytes) | Paid by |
|---|---|
| `- 两只桶:MCP 记链接用户 5000/时…` (102 B) | tick-convention row + the no-tick
prohibition, 108+56 B over 2 lines → 114 B over 1: `不是全局事实` restated
`按席位类别限定`, and `的形状` / `复述` / `一个` went with it |
| `- ✓ 线程 GET …/ccr/review_threads…` (115 B) | check-runs/actions row +
quota-read row, 88+61 → 120 over 1: `端点自身` and `自读` deleted |
| `- 线程自己建:POST …/pulls/{n}/comments…` (109 B) | update-branch row + its
rationale tail, 91+81 → 114 over 1: the `它是…手段` framing deleted, and
`不重写历史` is entailed by `真合并提交` |
| `- ✓ POST …/ccr/comments/{id}/resolve…` (114 B) | the two
`expected_head_sha` rows, 70+85 → 112 over 1: the pinned error prose `no
new commits on the base branch` deleted — nothing parses it, and the
standing rule is not to pin error copy |
| `- ⛔ PUT …/ccr/auto_merge 在 draft 上 422…` (112 B) | bare-`PATCH`-draft
row + the read-back rule, 97+113 → 115 over 1: `timeline 的
ready_for_review` restated the read-side timeline row |

A sixth deletion came first, in its own commit: the two provenance dates
left on the
bare-`PATCH` row and the MCP-fallback row (19 B and 33 B). They are the
class the file's own
rules-only lowering removed; deleting them is what let the rows above
fit.

No rule was dropped. Every merged row keeps both of its rules; what left
the file is
restatement, framing, an entailment and one pinned error string.

## Gates

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, no paths,
derived 15 families on head `95065f36`; all 15 run in the foreground
with `$?` captured by
redirect before any pipe; reconciled with `--ran`:

```
Run reconciliation — 15 derived, 15 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 15 derived famil(ies) accounted for — 15 run, 0 NOT-MEASURED
  (a DERIVED zero — all 15 recorded an exit code and none of them is 3).
```

One family first answered `exit 3` (PREREQUISITE NOT MET —
`@objectstack/formula` and
`@objectstack/lint` not built), which is not a red gate; after
`turbo run build --filter=@objectstack/formula
--filter=@objectstack/lint` under the shared
verify lock it exits 0. `check:pm-dispatch-gates` is not in this card's
derivation.

Reverse verification on the head:

- `wc -l` → **82**, equal to the ceiling.
- `awk 'length($0)>120'` → prints nothing, for touched and untouched
lines alike.
- `pnpm check:pm-skill-ratchet` → 0 · `pnpm check:pm-skill-id-lint` → 0
(27 files clean,
  pattern `/#[0-9]{3,}/g`) · `pnpm check:pm-governed-prose` → 0.

`pnpm lint` is CI-owned and was narrowed, with the narrowing proved
rather than assumed:
every `files:` selector in `eslint.config.mjs` is a JS/TS extension glob
and none names
`.md`; `eslint --no-inline-config --format json` over the one changed
file reports
`File ignored because no matching configuration was supplied`, 0 errors
and 0 warnings from
rules; and since the diff is a single markdown file outside that
population, no untouched
file's verdict can move.

`skip-changeset`: `.claude/**` is on the fast track — nothing any
package's `files[]` ships
moves here.

## 维护者速读(草稿)

**改了什么** —— PM 席位的 REST 通道对照表新增五条读数:评审线程的三条 CCR 路由(取线程、
解决、取消解决)、auto-merge 在 draft 上的两个拒绝,以及 MCP 与 REST 分属两只限流桶这一事实。

**为什么改** —— 会话代理自己的 403 报文点名了这批路由,但表里此前只有其中两条被实调过。
未实调的路由写进表里就是「未带 ✓ 的形状当已验证事实」,而这正是该表第一节明令禁止的。
这一轮每条路由都在本 PR 自己的 draft 上跑过一次才落行。两只桶那条解决的是另一件事:
席位的 MCP 状态动作会被同一 GitHub 用户下别的会话的读耗光,而 CCR 路由走的是另一只桶,
所以「换通道」在这里是合法退路,不是违规续写。

**风险与代价(含回滚)** —— 只改一个 markdown 参考表,不改任何运行时代码,发布面零变化。
文件行数仍是 82(天花板),字节数 +227。回滚 = revert 本 PR,无迁移、无残留状态。
探针在本 PR 上留下一条评审评论线程(已 resolved)与两次 draft 翻转,PR 终态是 draft、
auto-merge 未挂载。

**席位意见** ——

**你要做的** —— 无需维护者动作:本 PR 的受管路径全部落在 `references/**`,按分层裁定
走席位 contract-tier 复审后进队列。

## Acceptance notes

- noted, not filed: `added_to_merge_queue` now appears three times in
this one file (the
read-side timeline row, the write-side queue-read row, and queue
criterion ②), and the
write-side row still spells `git rev-list --parents` although :66 routes
that spelling to
`platform-readings.md` — against the file's own `⛔ 不在两处各存一份`.
Consolidating it
would free two more lines. Carrier: the next PR that pays density on
this file — the
rules-layer change already queued behind this one touches :53 in the
same block.
- noted, not filed: `DELETE .../ccr/auto_merge` on an ARMED pull request
has no reading in
this repo yet, and cannot get one from a governed-surface PR. It needs a
non-governed
card that legitimately arms auto-merge and disarms it again. Carrier:
none today.
- The probe thread on this PR is left RESOLVED. The PR ends draft,
`auto_merge: null`.


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

---------

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…e refused spelling (objectstack-ai#18270)

Fixes objectstack-ai#18141

`references/contract-review.md` :28 defined the review of record's shape
as 「所审 head sha 码段」 and
never said the span holds the sha ALONE. A seat reading it in good faith
writes the key and the sha
into ONE span; `H51_SHA_SPAN` matches a code span that is hex and
nothing else, so such a record
names no head and C6 reads 「no review of record on that head」 where a
complete review exists.

Taken by the file's own 「先删容许出错的构造,再让正确形态成唯一拼写」 order: the prose names
the one
correct spelling, and the reader keeps refusing the other while NAMING
it. The accept set is
unchanged — a second accepted spelling would be the trap's twin.

## What changed

- `.claude/skills/pm-dispatch/references/contract-review.md` :28 — 「所审
head sha 码段」 becomes
「所审 head sha 独占码段」, and the line sheds 「判词」 to pay for it. The file is
60 lines at its
60-line ceiling before and after; the touched line is 118 bytes (119
before). `check:pm-skill-ratchet`:
  "contract-review.md is 60 lines (ceiling 60; headroom 0)".
- `scripts/pm/check-clause2-carriers.mjs` —
- `HEAD_KEY_IN_SPAN` + `headSpanHoldsKey(pair)`: a DIAGNOSIS, read only
AFTER the locator has
already answered `absent`. It chooses no comment, admits none, and
returns nothing for a comment
the locator can already read. The newest-of idiom is
`latestMarkedComment`, the same one the
    locator resolves with — never a second one.
- C6's absent row is now two sentences: the empty case keeps 「a cleared
gate with nothing behind
it」, and a pair whose heading comment wrote the head inside the key's
span gets a row naming the
comment, quoting the span the seat actually wrote, and prescribing the
fix (key outside the span,
    sha in a span of its OWN, `--template` prints the whole record).
- The shared shape sentence now names the one spelling on both branches.
- The docblock quotation of the rule line was updated in the same edit,
so the file does not quote
    a sentence this PR replaced.
- 19 self-test cases in their own battery (658 before, 677 after; roster
floor 23, now 24).

## The measurements this PR was dispatched to take

**P1 — falsified in its live half; the mechanism half stands.** The
dispatch expected C6 to read
「no review of record」 for comment 5652813288 (PR objectstack-ai#17986) today. Measured
on the exact bytes (GET,
not edited) against this tree:

```
contractReviewHeadMatch(body, head)  =  "db55ea6dd"
hex-only spans in the body: 884e834, fc28c1d, ..., db55ea6, 8cdd696, 53ded82bf7a494f54e344e19099dbf00854b8694
spans that prefix the head:  db55ea6
key-in-span line ALONE:      null
locateReviewOfRecord:        { state: "found", id: 5652813288, sha: "db55ea6dd" }
```

Its `Head-sha:` span names nothing — the mechanism the card describes is
real — but the record is
still FOUND, because its own prose quotes the head in a bare span of its
own
("Cross-file staleness, searched at `db55ea6dd`"). So the sha C6 reports
for that record is read off
the prose and not off the line the seat wrote it on. Replayed end to end
through the CLI
(`--pair 17986 --pair-json`, verbatim bytes): no C6 row, the C6-RECORD
note naming `db55ea6dd`; exit 4
comes from C4 (`Implemented-by: branch …`, half written) and C7 (no
`Served-tier:` line), both facts
about that record that predate this PR. The defect is the SPELLING, not
that comment — the same
record trimmed to the spelling alone reads the refusal (below).

**P2 — holds.** `contractReviewRecordLines` prints `Head-sha: ` followed
by the sha in a span of its
own, and the `--template` note already says "7 to 40 hex in a span of
ITS OWN; a span holding the key
as well is not a sha". The prose now agrees with it, and the self-test
derives its refused fixture by
COLLAPSING the template's own line rather than retyping the key — a
template that renamed the key
reds this battery instead of drifting past it.

**P3 — the distinction was absent; it is the sentence that was added.**
Before this PR both cases
printed 「a cleared gate with nothing behind it, indistinguishable from
never reviewing」. Pinned
three ways now: a bare-sha span record reads `found`; a key-in-span
record is `absent` AND earns the
refusal naming the spelling; a comment with no heading is the plain
absence with no spelling
sentence.

**P4 — holds, pinned.** `locateReviewOfRecord` chooses the same comment
it chose before: a refused
spelling is never chosen over a correct record and never chosen at all
(pinned in both arrival
orders, and the pair with a correct record earns no C6 row and no
spelling sentence).

## Reverse verification

- Ablation (fix committed first, mutation proved on disk, restored
byte-identical): replacing
`const keyed = headSpanHoldsKey(pair);` with `const keyed = null;` turns
**5 of 677** self-test cases
red (`ABLATED EXIT=1`). Mutated blob `a3d4174b` vs HEAD blob `af1a124a`;
after restore the blob is
`af1a124a` again and `git diff HEAD` is empty. An earlier run of the
same ablation moved only 3
cases — two pins were reading a comment count and a thread name, which
survive the ablation; both
  were retied to the sentence the branch composes and are in the 5.
- Offline replay of the refused spelling (`--pair 17986 --pair-json`,
the record trimmed to comment
  5652813288's spelling): exit 4, one row, C6, reading
「⚠️ The SPELLING is why, and this pair is NOT the empty case: the PR
thread's comment 5652813288
… writes this head INSIDE one code span, as `Head-sha:
db55ea6`」
  with the remedy naming the span of its own and `--template`.
- Live control, a pair carrying a correct record: `--pair 18256` still
exits 0 with the C6-RECORD note
on comment 5674761187 (head `c96b507db288c20bf270c66c6137dc6fa7e79576`).

## Gates

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, no
paths, reconciled with `--ran`:

```
Run reconciliation — 42 derived, 42 run, 0 NOT-MEASURED, 0 UNRUN.
EXIT CODES — all 42 accounted famil(ies) carry one, so the NOT-MEASURED count above is DERIVED from them.
```

`pnpm --filter @objectstack/lint run check:doc-formula-expressions`
first exited **3** (PREREQUISITE
NOT MET — `@objectstack/formula` and `@objectstack/lint` unbuilt). Built
under the shared verify lock
and re-run: exit 0. `pnpm check:pm-dispatch-gates` ran to completion
(exit 0), not cap-killed.
`pnpm check:pm-clause2-carriers`: 677 cases pass.

Lint, as a proved narrowing rather than a repo sweep: `pnpm lint` is
`eslint . --no-inline-config`;
of this diff's two paths only `scripts/pm/check-clause2-carriers.mjs` is
inside eslint's own
population — the `.md` comes back "File ignored because no matching
configuration was supplied".
`--format json` returns 2 entries, 1 linted, 0 errors, 0 warnings.
`eslint.config.mjs` states of
itself that it "never enables type-aware linting (no
`parserOptions.project`, no typed
`@typescript-eslint` rules) for ANY file", so this diff cannot move the
verdict of a file it does not
contain. No package typecheck is owed: the diff is one `.mjs` under
`scripts/` and one `.md`.

Union head: the readings above were taken at `c3533346`.

## Deviation from the dispatch

The dispatch said the clause is "paid by density" at 60/60.
`.claude/agents/os-dev.md` states that
the only legal currency for the line ratchet is DELETED CONTENT and that
a re-wrap must never buy a
line for new content, so folding two clauses into one line to free a
61st was not available, and no
clause in this file is redundant enough to delete. The clause therefore
lands INSIDE :28: the file
never grows, the ceiling row is untouched, and the payment is 「判词」,
whose fact is carried by the
`PASS/FAIL` token it stood behind. Flagged here rather than chosen
silently.

## Acceptance notes

- noted, not filed: `contractReviewHeadMatch` scans the WHOLE comment,
so the head it reports can
come from a span in the prose rather than from the record's own
`Head-sha:` line — which is how
comment 5652813288 reads `found` today despite the refused spelling.
H51's declared shape is "the
head sha written as a code span somewhere in the comment", so this is
declared behaviour, not a
contract violation; it does mean the trap is survivable for some records
and not others. Successor:
  whoever next touches H51's recognition shape.
- noted, not filed: that same record carries `Implemented-by: branch
claude/…` (C4 half-written) and
no `Served-tier:` line (C7). Both are already recorded on the card; a
merged record is not edited.

## 维护者速读(草稿)

**改了什么** —— 契约复核记录的 head sha 从此必须单独占一个码段:`contract-review.md` :28 的措辞改成
「所审 head sha 独占码段」,并由同一行删去「判词」买单(文件仍是 60 行,不动天花板)。机读一侧,
`check-clause2-carriers.mjs` 在判定「本 head 无复核记录」之后,额外说出**为什么**:如果有人把
`Head-sha:` 和 sha 写进同一个码段,这条 C6 行会点名那条评论、引用他写的码段,并给出一次就能改对的修法。

**为什么改** —— 旧措辞只说「head sha 码段」,照字面写就会落进读不出的拼写:一份完整的复核记录,机器读
起来和「根本没人复核」完全一样。先让正确形态成为唯一拼写,再让拒收带上理由。

**风险与代价(含回滚)** —— 受理集合没有变宽:被拒的拼写仍然被拒,定位器选哪条评论一字未动(两个到达
顺序都已钉住)。新增的只是一句诊断文案与 19 条自测。回滚 = revert 本 PR,无数据、无产物、无发布面。

**席位意见** ——

**你要做的** —— 这是 `references/` 层受管面,按 Prime Directive objectstack-ai#14 走席内契约档复核 → ready
→ 入队,
不需要维护者逐条拍板;若对「删掉『判词』来买行」这笔密度支付有异议,请在此处说一句,我按你的说法改。

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

---------

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…t-stating surface is now a census (objectstack-ai#18244)

Fixes objectstack-ai#17798

`AGENTS.md`'s merge-queue paragraph named SIX required contexts and
called everything else "advisory and rides through". The live ruleset
has SEVEN — `Governed Surface Queue Guard`, enrolled 2026-08-27 (objectstack-ai#12427)
and pinned in `REQUIRED_CONTEXTS` by objectstack-ai#15233. The sentence a review seat
acts on therefore classified the one gate that refuses a zero-review
governed PR as advisory, which is verbatim the incident shape that guard
exists to prevent.

Nothing reddened on it, and that is the half worth fixing. `mustName` is
a FLOOR: it reds when a listed literal goes stale and is blind to one
going MISSING. So the registry grew, every surface's list stayed legal,
every gate stayed green, and the sentence stayed behind. The same
misclassification has now landed twice — 2 to 6 by the objectstack-ai#9677 ruling, 6
to 7 here.

## 维护者速读(草稿)

**改了什么。** 两处。一、`AGENTS.md` 的合并队列段落从「六个必需上下文」改成七个,把第七个 `Governed Surface
Queue Guard` 写进名单(等行数改写,1075
行没动)。二、`scripts/check-required-contexts.mjs` 的 `INSTRUCTION_SURFACES`
增加一个 `statesTheSet`
声明:声明了它的文件,其名单必须与注册表**逐个且等长**地对上。注册表本身(`REQUIRED_CONTEXTS`)一行未动,那是
objectstack-ai#15233 的面。

**为什么改。** 这句话不是描述,是审核席翻 ready / 挂 auto-merge / 入队前照着做的操作指令。它把治理面守卫说成
advisory,而那个守卫的职责恰恰是拒掉零审查的治理 PR —— objectstack-ai#12427 的事故形态。更关键的是:上一次入列(objectstack-ai#15233
加第七行)时,全部门禁是绿的,没有任何东西提示这句话已经过期。同一个漏洞已经发生两次,所以这次不只是手跟一遍数字,而是把「谁陈述了整个集合」变成机器可查的:下一次(第八个)入列时,加注册行的那个
PR 自己会变红。

**风险与代价(含回滚)。** 风险低。新规则只对**显式声明** `statesTheSet: true`
的条目生效,今天是两个文件(`AGENTS.md` 与 pm-dispatch 的
`platform-readings.md`);`review-checklist.md`
被明确归类为**不陈述集合**(它点名的是审核席亲手确认的两个
job,不是集合),保留它原有的两名下限,集合变大不会误伤它。声明也不能被悄悄摘掉换取豁免:名单已覆盖整个集合却没声明的条目同样变红。代价:每次入列多一处必须同
PR 跟进的数组。回滚 = `git revert`,两个文件都是纯文本,没有生成物、没有发布面、没有数据迁移。

**席位意见。**

**你要做的。** 确认一件事:第七个上下文 `Governed Surface Queue Guard` 今天确实在 Settings
的必需集合里(卡面 2026-09-12 的实测读数是七个,本 PR 不改 Settings)。其余不需要你操作。本 PR 触及受管面
`AGENTS.md`,按 Prime Directive objectstack-ai#14 走人工合并或已批准的队列路径。

## What changed

**1. `AGENTS.md` :505-:510 — six to seven.** The seventh name inserted,
"six" to "seven" in all three places, equal-line at the 1075 ceiling
(before 1075 / after 1075 / ceiling 1075). Each context literal is kept
whole on one line: the scan matches them contiguously, and a wrap that
split `TypeScript Type Check` across a line break made the surface red.
That is a real trap for the next hand-follow, so it is recorded here
rather than only avoided.

**2. `INSTRUCTION_SURFACES` gains `statesTheSet`.** An entry that
declares it must name the registry EXACTLY — membership *and* count:

- omitting a member reds, naming the omitted literal and the count it is
short by;
- padding past the registry length reds on the count, so a duplicate
cannot mask a member lost to a typo;
- a full list with the declaration dropped reds ("a list that covers the
whole set IS a statement of it"), so the exemption cannot be taken
silently;
- ablating every declaration reds rather than ticking (objectstack-ai#4690).

`review-checklist.md` is classified the other way and keeps its two-name
floor: it names the two required jobs a seat confirms by hand — its own
next line sends the seat to `true-green.md` for the rest — so it never
claimed to enumerate the set, and the set growing must not red it.

**3. Ten self-test cases, battery floor 31 to 41.** The 6-of-7 omission
and its restore-leg ablation; the eighth-row enrolment end to end, plus
the hand-off where following the ARRAYS clears the census red and leaves
the naming floor demanding the PROSE; the padded duplicate; the
undeclared full list; the no-declaration floor; and the two
classification pins (which surfaces state the set, by NAME never by
count; and the checklist's partial list staying legal).

## Evidence

Baseline first, on `origin/main` `b3b43b6` in a clean worktree, BEFORE
any edit — the known pit from hold note 5651882793 (PR objectstack-ai#17803 reported
`--self-test` red on a pre-existing `branches: [main]` filter on
`governed-surface-guard.yml`):

```
node scripts/check-required-contexts.mjs --self-test   exit 0   159 assertions
node scripts/check-required-contexts.mjs               exit 0   7 required context name(s) pinned across 3 workflow(s)
```

**The known pit is NOT red on `main` today.** The residual named in the
hold note is gone; the work below is measured against a green baseline,
not against a standing red.

After (`78959ab`): `--self-test` exit 0, 169 assertions; the pin exit 0.

**Reverse verification, both legs from the committed implementation,
each with its on-disk mutation proved and each restored byte-identical
(`git hash-object` vs the HEAD blob, `git diff HEAD` empty):**

| leg | mutation (proved on disk) | result |
|---|---|---|
| A — the registry array rots back | drop `'Governed Surface Queue
Guard'` from the `AGENTS.md` entry's `mustName` (grep 2 to 1; `git diff
--numstat` 0/1) | **RED**, exit 1: "declares statesTheSet: true, so its
mustName must be the required set EXACTLY — it lists 6 name(s) against a
registry of 7, missing 'Governed Surface Queue Guard'". Self-test exit 1
too. |
| B — the numeral alone rots back | `seven contexts block` to `six
contexts block` in `AGENTS.md`, all seven literals still listed (numstat
1/1) | **GREEN, exit 0** — reported as measured, not as expected. The
scan pins literals, never the word introducing them. Recorded as a
residual in the script header rather than implied covered. |
| B2 — the prose drops the literal | delete `` and `Governed Surface
Queue Guard` `` from the paragraph (grep 1 to 0; numstat 1/1) | **RED**,
exit 1: "AGENTS.md no longer names the required context 'Governed
Surface Queue Guard'" |
| floor control | battery floor 41 to 42 | **RED**, exit 1, naming the
exact count: "registered 41 case(s), below its pinned floor of 42" — so
the floor binds at headroom 0 and 41 is the measured count, not a number
below it |

Leg B is the honest finding of this PR: the census forces every enrolled
literal INTO the prose, so a stale numeral now sits next to a complete
list rather than a short one. Bounded, not covered; pinning the numeral
needs the arbitrary-literal recognition the script header already
measured as out of reach.

**Derived gate union**, run after the final commit, on `78959ab` (`git
rev-parse --short HEAD`), each exit captured by redirect before any
pipe:

```
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
  -> 37 command(s) from a 2-path change set (AGENTS.md, scripts/check-required-contexts.mjs)
```

36 of 37 exit 0. The one not measured: `pnpm check:pm-dispatch-gates`
(`scripts/pm/check-dispatch-gates.mjs`) runs past this session's
foreground ceiling — it was still printing passing cases at 560s. NOT
MEASURED, reason: runtime exceeds the foreground cap; CI owns it. Its
subject matter — the `ROOT_FILE_WATCH_HINTS` declaration this file
carries — is separately green under `pnpm check:watch-hint-literal`
(exit 0) and under this script's own `the dispatch-gates declaration
(objectstack-ai#9979)` battery (6 assertions).

`--ran` reconciliation is reported in the dev report; the
beyond-derivation families this card owes because it edits a gate script
— `check:required-contexts` and its `--self-test` — are the two green
readings above. `git grep` finds no `*.test.*` naming
`check-required-contexts.mjs`, so that script has no separate test suite
to owe.

**Lint, narrowed with the three readings that make a narrowing a
measurement:**

1. covered population read from eslint's own config, not guessed: `npx
eslint --print-config scripts/check-required-contexts.mjs` reports
exactly **2 rules enabled** for this path (`no-restricted-imports`,
`comment-swallow/no-code-inside-block-comment`); `eslint.config.mjs`
declares no markdown population at all, so `AGENTS.md` is outside the
lint verdict in either direction;
2. file count read from `--format json`: 1 file, 0 errors, 0 warnings;
3. invariance for untouched files: this repo runs one
`eslint.config.mjs` which **never enables type-aware linting for any
file** (no `parserOptions.project`, no typed rules — `eslint.config.mjs`
:326-:329, with its own positive-control measurement recorded there), so
this diff cannot move any untouched file's verdict.

The repo-wide `pnpm lint` scan is CI's run.

**`skip-changeset`, measured rather than asserted:** both paths lie
outside every package directory, and of the 70 published packages none
has a `files[]` entry escaping its own directory (0 entries starting
with `../` or `/`). Nothing published moves.

## Acceptance notes

Out of scope, noted, not filed:

- **`.claude/skills/pm-dispatch/references/platform-readings.md`
:385-:386 becomes FALSE when this lands.** It reads 「⭐ 本表的 `mustName`
不要求排他 ⇒ 第七个加注册行不会让本表变红」 and 「⇒ ⛔ 门绿不是本行已对的读数:计数行只能手跟改」. After this PR
that entry declares `statesTheSet: true` and the registry growing DOES
red it, so a seat reading those two lines would keep hand-following a
line the gate now holds. Not fixed here: the file is outside this card's
declared REGION claim (`AGENTS.md` :505-:510 plus this script), and the
script's own header records `.claude/skills/pm-dispatch/**` as a surface
a dev seat may not edit — the reason the checklist half of objectstack-ai#9325 was its
own card. **Successor: the `domain:skills` seat, in its own lane.**
Dedupe words: `platform-readings`, `mustName 不要求排他`, `计数行只能手跟改`,
`statesTheSet`, `required contexts 的名单`.
- The count WORD in a set-stating surface stays hand-followed (leg B
above). Recorded as a residual in the script header, in the paragraph
that already records the paraphrase-drift and shortening-rename
residuals. No card: it is bounded by the census and closing it needs
recognition the header measured as out of reach.
- The triage grading comment 5651027386, which this card's acceptance is
quoted from, answers **HTTP 404** — it is not on objectstack-ai#17798 (the card
carries exactly 3 comments) and the direct comment endpoint does not
find it. Its content survives verbatim inside hold note 5651882793 and
in this dispatch's own text, which is what acceptance items 1, 2 and 4
were read from here. Recorded as NOT MEASURED against the primary
source, not as "no flags".

Nothing else was touched. `REQUIRED_CONTEXTS` is byte-identical to
`origin/main`.

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

---------

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…d reads the REST token class per write, not per session (objectstack-ai#18359)

Fixes objectstack-ai#18320
Fixes objectstack-ai#18334

ONE file, one fold:
`.claude/skills/pm-dispatch/references/platform-readings.md`.
463 to 465 lines (ratchet ceiling 466, headroom 1); every edited and
every added
line is at or under 120 bytes; the widest-table-row pin stays at 0.

## Hunk A — the denied enqueue tool's rows (the first card): lines 41,
49, 50, 58, 60

Lock 1 denies MCP `enable_pr_auto_merge` and `disable_pr_auto_merge` (PR
objectstack-ai#18317),
and four rows still taught a seat how to call them. The live enqueue
route is
REST `PUT .../pulls/{n}/ccr/auto_merge`, which `rest-channel.md` owns.

- **`:49` retired** (the `mergeMethod: "SQUASH"` calling convention). It
is a
write-side recipe, and `:133` of this same file rules that write-side
recipes
  live in `rest-channel.md` and are never restated here —
  「逐操作通道归属、写侧配方与队列路由三读法见 `rest-channel.md`,⛔ 不在本表复述」.
  `rest-channel.md:50` already carries the live spelling with
`{"merge_method":"SQUASH"}`. Re-keying it here would have created the
exact
  duplicate that row forbids.
- **`:58` retired** (the tool succeeds on an already-`mergeable_state:
clean` PR,
against its own description). Its entire content is a contradiction
between one
MCP tool and that tool's description; nothing of it survives for the
REST route.
- **`:60` retired** (quota exhaustion returns success with no mount, so
verify the
effect and not the echo). Its conclusion is carried for the live route
by
  `rest-channel.md:52` 「⛔ `auto_merge` 与回显都不作数」, and inside this file by
  the surviving row 「回显两向不可靠 … ⛔ 不拿它当任何方向的证据」, which now stands
  directly above 「效果读数 = …」. The dispatch asked for no duplicate, so no
  replacement row was written.
- **`:41` re-keyed TOOL-NEUTRAL**, deliberately not keyed to the REST
unload. The
surviving truth (unloading auto-merge alone does not kick this repo's
queue) was
measured on the MCP `disable` call. I could cite no measurement of that
same
behaviour on `DELETE .../ccr/auto_merge`: `rest-channel.md:51` measures
only the
422 an unmounted PR answers, which is a different question. Keying the
row to the
REST route would have asserted an unmeasured platform fact, so it now
reads
  「补救:转 draft 与卸载 auto-merge 都做 —— 本仓卸载 auto-merge 单独不踢队。」
- **`:50` re-keyed in place**, forced by retiring `:49`: `:50` opened
with 「它」 and
`:49` was its antecedent. The reading is unchanged and the new subject
is
  route-neutral — 「挂上的 auto-merge 存的方法恒为 `merge`」. This is the one hunk
outside the four named lines, and it is declared as a deviation in the
report.

After this hunk, zero rows in the file name either denied tool in any
voice, so no
history row was needed either: the deny itself is already recorded for
the
operation in `rest-channel.md:53`, which is where `:133` says it
belongs.

## Hunk B — the REST token class (the second card): lines 129-130 become
126-132

`:129` said the class is fixed per session — 「按会话定」. Two measured flips
inside
one session, each with no seat action and with `GET /user` constant,
falsify that:

- the triage seat, 2026-09-15: `claude[bot]` at 15:54Z, then `os-sam` at
22:55Z;
- this seat, 2026-09-15 into 09-16: `claude[bot]` at 20:04Z, then
`os-zhuang` at 01:53Z.

The rows now say: two classes, both at core 15,000/h; the class follows
the Claude
Code account and NOT the session, and can flip between two writes of one
session
with no seat action; both flips are named by date and account; the class
is read
from EVERY write's own read-back (`user.login` plus `user.type`) and is
never
carried forward from the round-open marker, whose identity reading is
dated rather
than standing; and `performed_via_github_app`, `GET /user` and the core
rate-limit
header all answer the same for both classes, so none of the three
discriminates.

The consequence is split across two rows because it does not fit one
120-byte line:
a user-class write is author-bound — a suspended account 404s its
comments and its
filed cards, while labels, state, titles and bodies survive — so the
durability
calculus is re-run per class and never assumed to sign as `claude[bot]`;
and under
the user class the PR's author IS that user, so requesting them as
reviewer answers
422. That 422 was measured on PR objectstack-ai#18351 at 02:32Z. The second
measurement table
comes from objectstack-ai#18350, which the second card carries as its duplicate.

## Verification

Reverse verification, before and after, on the one file:

| reading | before (`1411cf2c`) | after |
|---|---|---|
| `grep -n -E 'enable_pr_auto_merge\|disable_pr_auto_merge'` | `:41 :49
:58 :60` | zero hits (grep exit 1) |
| `grep -n -E 'installation\|user-to-server'` | `:129` only | `:126`
only |
| 「按会话定」 | present on `:129` | absent; `:127` reads 「⛔ 不按会话定」 |
| every-write read-back | absent | `:129` 「类只认每次写回读的 …」 |
| consequence row | absent | `:131` and `:132` |
| `wc -l` | 463 | 465 |
| widest line | 120 B | 120 B, zero lines over 120 B |

Firing control — `rest-channel.md` is untouched and still carries the
live route:
`grep -n 'ccr/auto_merge'
.claude/skills/pm-dispatch/references/rest-channel.md`
still answers `:50` and `:51`, byte-identical. `git diff --stat` against
the merge
base is the one file, 9 insertions and 7 deletions.

Ratchet, both readings, quoted from the gate itself:

```
✓ check-skill-line-ratchet: .../platform-readings.md is 465 lines (ceiling 466; headroom 1).
✓ check-skill-line-ratchet: .../platform-readings.md: widest table row is 0 bytes (pin 0; headroom 0).
```

`node scripts/pm/dispatch-gates.mjs --commands THE-FILE` derived 16
families; all
16 were run, every one at exit 0, and reconciled with `--ran`:
`✓ dispatch-gates --ran: 16 derived famil(ies) accounted for — 16 run, 0
NOT-MEASURED`.
Outside that derivation I also ran `pnpm check:pm-settings-deny-roster`
(its roster sits under
`.claude/`, so its silence would not have been evidence in either
direction) and
the path face `node scripts/pm/check-governed-merges.mjs --test
THE-FILE`, which
answers GOVERNED as expected.

No changeset: nothing versioned moves. `.claude/**` ships in no package
`files[]`.

## Landing

This diff touches `.claude/**`, a governed surface (Prime Directive
objectstack-ai#14). Every
governed path lies under `.claude/skills/pm-dispatch/references/`, so
the landing
tier is the skills seat's in-seat review at `CONTRACT_REVIEW_TIER`
rather than the
maintainer's word. **This PR stays DRAFT.** I requested no reviewers,
touched no
ccr route, armed no auto-merge and flipped nothing.

## 维护者速读(草稿)

**改了什么** — 派发座位的平台事实表改了两处。一是删掉三条、改写两条教座位去调一个已被
锁 1 禁掉的 MCP 入队工具的规则,活路线的写法本来就在 `rest-channel.md`。二是把「REST 写
的身份按会话固定」这条改成实测的样子:身份跟 Claude Code 账号走,一次会话中间会变,每次
写都要自己回读一次。

**为什么改** — 这两条都是写着的事实与实测不符。前者让座位学一个它调不到的工具;后者更贵:
座位的耐久性判断(评论和卡会不会随账号被封而 404)整个建立在「署名恒为 `claude[bot]`」
上,而这一班里已经两次实测到写入落成了用户账号。四天内已经有两个分诊账号在班中被封并因此
丢掉全部书面记录,所以这条假设错的方向正是丢数据的方向。

**风险与代价(含回滚)** — 只动一个内部指令文件,不发布、不进任何包、无运行时影响。棘轮还
剩 1 行余量。回滚 = revert 这一个 commit。⛔ 本 PR 不改写入通道本身:换一个低权限账号或
改回 App installation token 是维护者的决定,这里只把事实表改成实测的样子。

**席位意见** —

**你要做的** — 目前不需要你做任何事。这是 references 层,按现行分层由技能席位在座评审后
自己落地;⛔ 不需要你的批准。若你希望把「写入身份」这件事本身处理掉(专用低权限账号,或
恢复 App installation token),那是另一张卡。

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

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…s — the unranged git log walked all of main and made force-with-lease dead letter (objectstack-ai#18351)

Fixes objectstack-ai#18178

## The defect

`AGENTS.md` Multi-agent §3 allows `--force-with-lease` only while all
five criteria hold.
Criterion ③ stated the bar — *nobody else has ever pushed it* — and then
operationalised it
as the author and committer sets of an **unranged** `git log` over the
branch's remote ref.
With no range that walks the branch's entire ancestry, all of `main`
included, so the sets
are main's whole history plus the agent and the test cannot pass for
**any** branch cut from
`main`. The stated criterion is satisfiable and right; only its
operationalisation was
impossible — a reader who follows the words never force-pushes, one who
follows the intent
does, and that divergence is the bug. The card was filed after the dev
on objectstack-ai#17330 declined to
take a reading it could not prove and shipped four `wip:` commits plus a
merge instead.

## The change

One hunk, two lines, `AGENTS.md` :473–:474. The parenthetical is
re-keyed to the branch's
**own** commits — the range form `origin/main..` in front of the branch
ref. Criteria ①②④⑤
are untouched, ③'s bar is untouched (same words, same strictness), and
the file stays at its
1075-line ceiling with no new line.

```diff
-   `claude/issue-*`; ② this worktree created it; ③ nobody else has ever pushed it (the
-   author and committer sets of `git log origin/BRANCH` are you alone); ④ no open PR
+   `claude/issue-*`; ② this worktree created it; ③ nobody else has ever pushed it (the author and
+   committer sets of its own commits, `git log origin/main..origin/BRANCH`, are you alone); ④ no open PR
```

⚠️ `BRANCH` above stands for the angle-bracket placeholder the file
actually spells; this
body writes the word instead, because a GitHub body sanitizer eats short
angle-bracket
fragments. The file itself is unchanged in that respect — read the diff
for the real bytes.

## Ratchet and byte budget

| reading | before | after |
|:--|--:|--:|
| `wc -l AGENTS.md` (ceiling 1075) | 1075 | 1075 |
| line :473 | 90 B | 101 B |
| line :474 | 88 B | 108 B |
| lines :472 / :475 / :476 / :477 | untouched | untouched |
| `git diff --stat` | — | 1 file, 2 insertions, 2 deletions |

`scanLineLengths` (the gate's own scanner, budget 120 B) reports **0
offenders** on the
edited file.

## Reverse verification

Taken on this very branch, a real `claude/issue-*` branch, and on a
constructed control.
⚠️ This checkout is **shallow**, so the unranged sets are truncated
relative to the card's
full-history reading (33 / 32 / 1); the *shape* is what reproduces, and
it reproduces exactly.

```
BEFORE — today's literal test, on this branch
  git log --format='%an|%cn' origin/claude/issue-18178-force-with-lease-criterion-scope | sort -u
  -> 7 pairs   (Claude|Claude, Jack Zhuang|GitHub, Leehom|GitHub, Warren Buffett|GitHub,
                claude[bot]|GitHub, os-elon-musk|GitHub, os-try-charles|GitHub)

CONTROL — the same literal test on origin/main itself
  -> 6 pairs   — the branch's 7 minus Claude|Claude, i.e. main's set plus the agent, exactly
                 as the card measured. `comm -23` of the two sets prints one line: Claude|Claude.

AFTER — the re-keyed test, same branch
  git log --format='%an|%cn' origin/main..origin/claude/issue-18178-... | sort -u
  -> 1 pair    Claude|Claude          (2 commits in range)

FIRING CONTROL — the re-keyed test on a branch that genuinely has a second pusher
  (a throwaway local repo, two clones, two identities; no probe branch was pushed to origin)
  unshared branch, literal test   -> 4 pairs   (wrongly refuses)
  unshared branch, re-keyed test  -> 1 pair    (permits — correct)
  SHARED branch,   re-keyed test  -> 2 pairs   Claude|Claude + os-try-charles|os-try-charles
                                               (still refuses — the bar is not loosened)
```

## The one design choice: which scoped spelling

The card proposed `$(git merge-base origin/main
origin/BRANCH)..origin/BRANCH` and called it a
proposal, not a prescription. This PR ships the plain two-dot range
`origin/main..origin/BRANCH`.
Both compute the same commit set — verified on the live branch and on
both control branches,
1 / 1 / 2 pairs either way — and the two-dot form is one moving part
fewer. The deciding
measurement is how each **fails**:

```
# the substitution form, when merge-base returns nothing (shallow clone, unrelated
# histories, a ref not fetched) — the range silently becomes HEAD..origin/BRANCH
  exit=0   pairs=0   on a branch a second agent had just pushed  -> "you alone" is
                     VACUOUSLY true, and the criterion permits exactly the clobber it forbids

# the two-dot form under the same fault
  exit=128 fatal: ambiguous argument ... unknown revision  -> loud, no verdict at all
```

That failure is live in this repository, not hypothetical: `git
merge-base origin/main REF`
exits 1 with empty output here for a branch whose base lies outside the
shallow window.
Preferring the spelling that cannot degrade into a different question is
the same principle
the card is about — a test that answers confidently and wrongly is worse
than one that stops.

## 维护者速读(草稿)

**改了什么** — `AGENTS.md` 多 agent 纪律 §3 里判断「分支是不是只有我一个人推过」的那条命令,
从 `git log` 加分支名(走遍整条祖先,包含整个 `main`),改成只看分支自己的提交(`origin/main..` 区间)。
正文两行,判据本身一字未动。

**为什么改** — 原命令对任何从 `main` 切出来的分支都必然返回「多人」,所以第五条允许的
`--force-with-lease` 实际上永远用不了:条文写着允许,测出来永远禁止。按字面读的 agent 从不 force-push,
按意图读的会,两种读法分叉本身就是缺陷。已经有一张卡的开发因此多推了四个 `wip:` 提交和一个 merge。

**风险与代价(含回滚)** — 改的是规则文本,不是代码,没有运行时影响。风险是「改松了判据」,
本 PR 用一个构造出来的、确有第二个推送者的分支做了发火对照:新命令仍然读出两组身份、仍然拒绝。
回滚成本 = 还原两行文字。

**席位意见** — (留空,待席位定稿)

**你要做的** — 这是受管面(`AGENTS.md`),需要您的一次授权批准;批准前没有任何 agent 席位会合并、
入队或挂 auto-merge,PR 保持 draft。

## Acceptance notes

- noted, not filed: the ratchet's line-length verdict prints **only on
failure**
(`check-skill-line-ratchet.mjs` `run()` logs `lv.msg` under `if
(!lv.ok)`), so a green run
says nothing about the 120-byte axis for any file. Not a defect class —
a silent green is
the normal shape for that gate — measured positively here by calling
`scanLineLengths`
directly instead of reading the gate's silence. Bearer: none; noted for
the next author who
  cites that gate's green as width evidence.
- Scope held: `AGENTS.md` only, criterion ③'s parenthetical only; :472
and :475–:477 were not
  re-flowed and not touched.

## Landing

Governed surface (`AGENTS.md`, Prime Directive objectstack-ai#14) —
`check-governed-merges.mjs --test`
answers **GOVERNED**, 1 of 1 path. The PR stays **draft**; no reviewer
was requested, no
auto-merge armed, nothing flipped. It waits for the maintainer's
authorized approval, which
the owning seat requests.

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

Co-authored-by: Claude <[email protected]>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…table, so its self-test is hermetic on a box that carries the sibling (objectstack-ai#18365)

Fixes objectstack-ai#18321

## The defect

`.claude/hooks/guard-governed-enqueue.selftest.sh`'s case "an
exception-row path in a repo
this container cannot resolve" rested on a premise about the **box**,
not about the hook:
that no `objectstack-ai/cloud` checkout sits beside this one. The hook
resolves a sibling
checkout by comparing origin slugs under the parent directory of its own
repo root, so on a
container that *does* carry a sibling `cloud` checkout the guard
resolved it, recomputed the
register predicate on that tree with `--root`, got the governed answer
and **blocked**. The
matrix read `54 passed, 1 failed` there, and was green in CI only
because the runner mounts
no sibling. `lint.yml`'s step comment calls these matrices hermetic;
this case was not.

## What changed

**The hook** now reads `OS_GOVERNED_ENQUEUE_SIBLING_ROOT` for the
directory a sibling is
resolved under. It moves **where** the search looks and nothing else —
the origin-slug
comparison remains the entire admission rule, so a sibling that resolves
is audited exactly
as before, and a root holding no matching checkout resolves nothing. No
fail-open path was
added or widened.

| value | meaning |
|:---|:---|
| unset | the parent of this checkout — today's behaviour, to the byte |
| empty | identical to unset; an empty value is an accident, and the
safe reading of an accident is "no override", never "look nowhere" |
| a directory carrying no matching checkout (one that does not exist
included) | nothing resolves; the run proceeds exactly as on a box
without the sibling |

**The matrix** now owns its own premise: the "cannot resolve" case
points the lookup at a
directory it creates and knows is empty, and one new case pins the other
half — a sibling
that *does* resolve is audited on its own tree. The throwaway sibling is
built in the
self-test (`git init` plus an `origin` naming the target repo is the
whole admission
requirement) and removed by the existing `trap` on the matrix's own temp
root, so no new
cleanup path was needed. Both temp directories live under that root by
construction.

The new case is written as **agreement with the register**, not as a
copied verdict — the
shape this file's own header says it learned the hard way, where a
hard-coded `expect allow`
went red over an upstream register change the hook had nothing to do
with.

## Reverse verification

BEFORE, on `origin/main` `1411cf2c`, this container, `/home/user/cloud`
present:

```
54 passed, 1 failed
  FAIL want=allow got=block  an exception-row path in a repo this container cannot resolve
```

AFTER, at `5391e5c0`, same container, `/home/user/cloud` still present —
four injection
states, all `exit 0`:

```
UNSET        exit=0  56 passed, 0 failed
EMPTYDIR     exit=0  56 passed, 0 failed
NONEXISTENT  exit=0  56 passed, 0 failed
EMPTYSTRING  exit=0  56 passed, 0 failed
```

**"Unset changes nothing" is proved directly**, not inferred: the
*untouched* matrix was run
against the *changed* hook, and its output was byte-identical to the
baseline log
(`diff` empty — still `54 passed, 1 failed`, still the same one case).

The variable's semantics were also measured against the hook directly,
with the real
sibling present, on a payload targeting `objectstack-ai/cloud`:

```
unset         -> block   (resolves the real sibling)
empty string  -> block   (identical to unset)
=/home/user   -> block   (explicit, same directory as the default)
empty dir     -> allow   (nothing resolves)
non-existent  -> allow   (nothing resolves)
```

**Firing control.** Two were run, each mutating the committed file,
proving the mutation
landed on disk by hash, and restoring with `git checkout HEAD --` under
a `trap`
(`git diff HEAD` empty afterwards, blob hash back to the HEAD blob):

- *the control this card prescribed* — give the new case a
**not-governed** fixture:
**stays green**, `56 passed, 0 failed`. It cannot fire, and that is a
property of the
assertion rather than a gap: an agreement assertion flips the register
leg and the hook
  together, so they still agree. Reported rather than papered over.
- *a control that targets the property under test* — the throwaway
sibling's `origin` names
a different repo, so it is no longer admitted: **red**, `55 passed, 1
failed`,
`FAIL want=block got=allow a sibling checkout that resolves is audited,
never waved through`.
This is the mutation that corresponds to "the hook stopped consulting
the sibling tree",
  which is what the case exists to catch.

Diff confined to the two files in the declared surface:

```
 .claude/hooks/guard-governed-enqueue.selftest.sh | 92 +++++++++++++++++++---
 .claude/hooks/guard-governed-enqueue.sh          | 41 +++++++++-
 2 files changed
```

## Gates

`dispatch-gates.mjs` derives 13 families from the real change set (2
paths, three-dot vs
merge base `1411cf2c6`); all 13 ran and all recorded `exit 0`.
Reconciliation:

```
Run reconciliation — 13 derived, 13 run, 0 NOT-MEASURED, 0 UNRUN.
```

`check-doc-formula-expressions` first returned **exit 3 (PREREQUISITE
NOT MET — nothing
measured)** because two workspace packages were unbuilt; it was re-run
to `exit 0` after a
targeted build, and only that second reading is recorded. The path face
`check-governed-merges.mjs --test` answers **GOVERNED** (`.claude/**`),
as expected.

`shellcheck` is **not measured**: the tree wires no shellcheck step
(`lint.yml` runs the hook
matrices, it does not lint them) and the binary is absent from this
container. `bash -n`
parses both files and `check:bash32-floor` passes.

## Acceptance notes

Two things measured on the way, both **out of scope for this PR** and
neither fixed here:

1. **The case never reached the fail-open it claimed to pin.** The
comment that used to sit
on it described the "no checkout of the target repo is available"
branch. It does not
reach that branch, in any environment including CI: with nothing
resolved the register is
asked *without* `--root`, answers about *this* tree, finds the path
byte-exact against its
own generator and lifts it, so the hook leaves at the cleared-predicate
`exit 0` with
**empty stderr**. That branch is reachable — a path hitting the
exception row but absent
from the generator's declared output set returns governed with a
non-empty `exceptions`
list — but no case in the matrix reaches it today. The stale comment is
corrected here
because it sits on the case being re-keyed; pinning the branch is
separate work.
2. **The slug reader keeps a `.git` suffix.** Its path character class
owns the dot and is
greedy, so an origin of `https://github.com/objectstack-ai/cloud.git`
yields
`objectstack-ai/cloud.git` and matches nothing. A sibling cloned with
the conventional URL
therefore does not resolve. The same expression also derives the slug
for a bare
`gh pr merge NUMBER`, where a non-matching slug makes the API read 404
and the guard fail
open. Measured here; reported for its own card. The new case uses the
bare URL form on
   purpose and says so in place, so nobody "tidies" a `.git` onto it.

Also noted, not filed: `lint.yml`'s step is named for the two matrices
that existed when it
was written, while discovery now picks up more. The step comment is
explicit that discovery
is the contract and that a hard-coded list would be the defect, so the
name is illustrative
rather than a ledger — nothing to reconcile. Whoever adds the next hook
matrix is the one who
reads it. `lint.yml` is outside this PR's declared surface and was read,
not edited; its
hermetic claim becomes true rather than edited.

Serial context: PR objectstack-ai#18317 touched `.claude/settings.json` and landed
before this branch was
cut; it is unrelated to either hook file and nothing here depends on it.

## 维护者速读(草稿)

**改了什么** — 给这个 PreToolUse 守卫加了一个环境变量,用来指定「到哪个目录下去找兄弟仓
checkout」。默认不设时行为与今天逐字节一致。自测脚本据此改写了一个用例,并新增一个用例。

**为什么改** — 这个自测用例原本依赖「这台机器上没有 cloud 仓的 checkout」这个环境事实,而不
是依赖守卫本身的行为。凡是挂了 `/home/user/cloud` 的机器上它就红,CI 绿只是因为 runner 上
恰好没有。测试的结论必须由被测代码决定,不能由机器上还挂了什么决定。

**风险与代价(含回滚)** — 风险低。变量只改变「去哪里找」,不改变「找到了算不算数」——
判定仍然是比对 origin slug,所以误设一个值只会让它找不到(退回到本来就存在的放行分支),
不会让它放过本该拦截的东西。已逐项实测 unset / 空串 / 空目录 / 不存在目录四种取值。回滚就是
还原这两个文件,无数据迁移、无发布物、无下游依赖。

**席位意见** — (待席位填写)

**你要做的** — `.claude/**` 属受管面,按 Prime Directive objectstack-ai#14 需要一次授权的 APPROVED 审核;
本 PR 保持 draft,未请求任何 reviewer,未触碰 auto-merge。除批准外无需其他动作。

---

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr

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

---------

Co-authored-by: Claude <[email protected]>
os-justin pushed a commit that referenced this pull request Sep 17, 2026
…ord's own landing PR and commit

Both records landed on `main` on 2026-09-07 (0132 via #16215, squash
`c677cda816`; 0133 via #16267, squash `13c08356ab`, both merged by the
authorized approver), while their `**Status**` lines still read
`Proposed — awaiting the maintainer's hand-merge, which is (itself) the
acceptance act`. Read by their own rule the acceptance act occurred; only
the line was never updated. Anyone judging citability from the Status
field got the wrong answer, and no gate reds on it.

Each line now takes the form the positive controls 0130 :3 / 0131 :3
already use — `Accepted (date) — accepted by the merge that landed it on
`main` ([#PR](url), commit `sha`), which is (itself) the acceptance act
for a governed surface (Prime Directive #14).` — with that record's own
landing facts, keeping each file's local form (0132 wraps under a list
dash, 0133 is one unwrapped list line). 0132's trailing
`Nothing below is settled until this record merges` sentence is dropped,
which is what 0130 and 0131 did with theirs; 0133's trailing sentence
says something else entirely (this file records no new decision) and is
kept byte-for-byte.

Nothing else moves: no decision text, no heading, no other line, and
ADR-0134 is untouched — its `Proposed` mirrors cloud ADR-0071's own
state and is self-explanatory.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <[email protected]>
os-justin pushed a commit that referenced this pull request Sep 17, 2026
AGENTS.md rule 2 handed the dev `git ls-remote --heads origin | grep
issue-<n>` as a one-command pre-check and said only what the command IS,
never what a HIT means. The reading rule stood only in the fact layer
(pm-dispatch references), which the dev definition never points at, so
the layer holding the rule had no reader and the layer with the reader
had no rule. The failure direction is the silent one: a stale head reads
as "already claimed" and nothing goes red.

One clause on the existing sentence states the reading. Paid in-file:
the Skills section's restatement of the governed-surface queue mechanism
retires, since Prime Directive #14 already carries it.

Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants