docs(adr): ADR-0133 — organization management, the open basics; mirroring cloud ADR-0081 D1/D3/D4 with Provenance - #16267
Conversation
…cloud ADR-0081 D1/D3/D4 Records, in the repository whose code enforces them, the open-mechanism half of cloud ADR-0081 (Accepted 2026-07-09): D1 teammate-add stays open and always goes through better-auth invitations, D3 the in-shell surface is the organization record page reached by a templated nav deep-link, D4 control-plane roster reads are organization-scoped. Decides nothing. D2, the non-goals and the commercial consequences stay in cloud and are cited as `cloud ADR-0081`. Where a local record already decides a point it is cited rather than duplicated: ADR-0093 (membership lifecycle, D9's active- organization resolution), ADR-0105 D12 (the multi-org entitlement anchor), and ADR-0131 D1/D7/D9 (total organization ownership), whose C6 census (#15207) — not this file — answers whether sys_member keeps its organization column. Every code anchor was re-located on origin/main at 7778115 and is written as a symbol anchor, since check:adr-symbol-anchors makes a line number a hard finding. The objectui rendering half is cited as a cross-repo anchor and is explicitly not claimed as this repository's contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
PM verification — PASS on the record itself ·
|
| ADR-0132's own | cloud ADR-0081's (this card's) | |
|---|---|---|
| D1 | The multi-organization runtime is open core | Adding a teammate is open, via better-auth invitations |
| D3 | One name, two packages; the declaring manifest decides which | The in-shell surface is the org RECORD page |
| D4 | The open package entitles both walled postures | Control-plane roster reads are org-scoped |
Folding this card's D1/D3/D4 into that file would give one record two different D1s, two different D3s and two different D4s. That is not a mirror a reader can check against its original — it is the opposite.
⭐ And ADR-0132's own body already scopes itself away from them: "D1, D3 and D4 of that record are untouched — see What this record does not decide."
The decision I am escalating
A — land 0133 as a disjoint record (what this PR does). The two files decide nothing in common, so folding them later is a move, not a rewrite.
B — hold this card until #16215 merges, then extend that file. Honours the pointer literally, at the cost of blocking a ready card on another seat's governed hand-merge for an unbounded time, and still leaves the duplicate-D-number problem to solve.
My reading is A, on the two measurements above. ⛔ But @hotlong wrote the pointer and filed the card; if the answer is B, say so and I will hold the card — the seat's work is not wasted, it becomes the extension text.
The record itself — verified
- One file,
docs/adr/0133-org-management-open-basics.md, +267. Nothing else in the diff. - ⭐ Zero line anchors.
grep -cE '\.ts:[0-9]+|\.json:[0-9]+'→ 0. This matters:check:adr-symbol-anchorsmakes apath:NNNanchor a hard finding underdocs/adr/**([finding] ADR line anchors intosql-driver.tshave rotted — 4 of 4 sampled resolve to unrelated code, and the ADRs read as if they still point at the mechanism they name #13556, maintainer ruling 2026-09-01, explicitly no transition period), so the card's own three offsets (sys-member.object.ts:47,invite-entry-toolbar.test.ts:4,sys-user.object.ts:59) could not be used at all. The seat pre-verified every replacement against the gate's own resolver and let it reject three candidates rather than shipping them. - Zero
ADR-0132citations — the first draft cited it by number,check:adr-anchorsfailed exit 1 because a number naming no record underdocs/adr/is a squat, and the seat replaced every token with a PR reference. ⭐ That is the same failure as this card's own, seen from the other side, and it caught it. - D-numbering preserved:
### D1,### D3,### D4with D2 visibly absent — cloud's numbering kept unrenumbered so the mirror lines up against its original. - Cross-check honoured, including the pointer I failed to relay: ADR-0093 D9, ADR-0105 D12 and ADR-0131 D1/D7/D9 are cited rather than restated, and ⛔ D4 does not decide whether
sys_memberkeeps its column — deferred to ADR-0131 D7's writer-facts question and the C6 census (feat(spec,services): deployment-level state has no organization column — settings global rung, plumbing objects, the audit ledger, #12699 made total (ADR-0131 D7) #15207), exactly as 5536478826 required. - Number
0133derived from the tree and re-derived immediately before committing, checked against main, feat(organizations): bring the multi-organization runtime back to open core — the org-scoping registrar ships open, the licence gate stays in cloud (ADR-0132) #16215's branch, and all 40 open PRs. A number free onmainis not free if an open PR claims it.
Gates
17/17 green at the final head, family derived mechanically. One answered exit 3 = PREREQUISITE NOT MET (check:doc-formula-expressions, refusing to run against unbuilt output) — ⭐ cleared into a real measurement rather than reported as a pass, by building the two packages it names and re-running it green. The cross-repo objectui anchors are skipped-not-judged by default, so the gate was re-run with OBJECTUI_CHECKOUT set and both anchors left the skipped list — ⭐ skipped is not green, and the seat did not let it read as such.
Out of scope, filed not fixed
#16270 — "the org record page opens on tab-0 Members" is asserted in three places but declared by no metadata: zero relatedList prominence keys under packages/platform-objects/src/identity/, no relatedLayout override anywhere, so objectui's documented default collapses every related list into one stacked Related tab. The ADR therefore states the deep-link contract (which is declared) and does not assert a tab order (which is not). ⭐ The issue deliberately does not pick between "metadata missing a declaration" and "three documents stale" — telling them apart needs the running app. Correct call; declaring the key here would have been a metadata change the Clause-②: no forbids.
Status: Clause-②: no holds as declared — no code, no metadata. Draft, ready never flipped, auto-merge never armed, skip-changeset judged rather than defaulted. ⛔ Governed surface: human merge, and the A/B question above is owed an answer first.
Generated by Claude Code
Contract review (clause ②) — no gate · ⛔ GOVERNED — human merge only — PR #16267 at head
|
…aimed by an open PR) `0132` is free on `main` but claimed on the branch of an open PR — #16215 adds `docs/adr/0132-multi-organization-runtime-is-open-core.md`. Verified here by a full `git diff --name-only origin/main...<pr-head> -- docs/adr/` over all 27 open PRs, not by titles: 0132 is #16215's, 0133 is #16267's, 0134 is #16476's, and 0135 is unclaimed. File renamed with `git mv`, slug unchanged. The record's one self-citation (its title line) moves with it; the `ADR-0105` and `ADR-0108` references are landed records and are untouched. No line-number anchors were introduced — the file still carries none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
…r-org-management-open-basics
The record that reverses cloud ADR-0081 D2 has landed on `main` as ADR-0132, so this file's account of it is stale: it described that record as an open draft PR, cited it by PR number because an unmerged ADR number was a squat, and still said D2's packaging decision "stays in cloud". - header bullet — D2 is not mirrored because it has been REVERSED; what stays in cloud is the record, not the machinery, which now ships in this repo - "Relationship to the D2 reversal" heading and body — cite ADR-0132 by number and relative link; the PR-number citation the anchors gate used to force is retired, and the in-file link target moves with the heading - division-of-labour table — the D2 row points at ADR-0132 - the note for the hand-merge — folding the two records into one was blocked by ADR-0132 being an unmerged governed PR; that obstacle is gone, and this change still does not take the option, which stays a merge-time call - "What this record does not decide" item 4 — same re-pointing The file wrote its own trigger for this edit, and the condition is now met. Nothing about the reversal is re-derived, restated or evaluated here: ADR-0132 records that it does not touch cloud ADR-0081 D1/D3/D4, which are this file's whole content, so the two remain disjoint. `origin/main` is merged in so the cited record exists in the tree the gates read. Gates: check-adr-anchors exit 0; check-adr-symbol-anchors exit 0, verdict "0 line anchors survive"; check-governed-merges --test exit 3 GOVERNED. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
The hand-merge note said the only thing blocking a single combined record was ADR-0132 being unmerged, and that with it landed the choice "stands on its merits alone". True, but it leaves the reader to rediscover the merit that matters: that record carries its own D1 through D6, which are different decisions from cloud ADR-0081's D1/D3/D4, so folding this mirror into it puts two unrelated D1s in one document — exactly the property the deliberately unrenumbered mirror exists to keep checkable. Stated, not decided: the disposition is still the maintainer's merge-time call, and this change takes the option neither way. Gates re-run on this tree: check-adr-anchors exit 0, check-adr-symbol-anchors exit 0 with "0 line anchors survive", check-adr-links exit 0, check-nul-bytes exit 0, check-doc-authoring exit 0, check-governed-merges --test exit 3 GOVERNED. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
PM 独立复核 —— PASS。依赖已落地的更新到位,红也没了复核在独立 worktree 里 checkout 到 head 1. 那条红:陈旧读数,已消失
新 head 上实测:33 个 check run,0 个 failure。 2. 门禁,PM 自跑
3. 交付面
4. 席位声明的偏离 —— 接受它把 ⭐ 而且它给的理由成立且是我没预见到的: 5. ⭐ 席位多做的一处,是对的我列了 5 处要改,它改了 6 处(小节第二段是独立站点,同时承载「按 PR 号引用」的规则与本文件自己的更新触发条件),并从卡的评论历史里读出了第 7 处:
⇒ 之前「不能并进 0132」的理由是它还没合并;那个障碍随合并消失了,但另有一个结构性障碍幸存。文件把它陈述出来而不裁决,并明说合并时若维护者要合成一份,那是 merge-time 的决定、两份不相交、是移动不是重写。⭐ 这正是正确姿态:⛔ 席位不替维护者选。 6. 只报不改的两条
⛔ 保持 draft,等人工合并。 PM 未 flip ready、未 arm auto-merge、未 Generated by Claude Code |
Escalation answered by the maintainer's own act — option A, land ADR-0133 as a disjoint record (director seat, 2026-09-07)The A/B question in the PM verification comment (5558242242) — land Recorded so the earlier pointer ("extend ADR-0132") is read as superseded by measurement (ADR-0132's own D1/D3/D4 are different decisions) and by this ruling, not silently dropped. Generated by Claude Code |
Fixes #14508
⛔ DRAFT, and it stays draft. The diff touches
docs/adr/**, a governed surface: the maintainer hand-merges. Do not flip ready, do not arm auto-merge.One new file,
docs/adr/0133-org-management-open-basics.md. Nothing else in the tree changes.⭐ Update, 2026-09-07 — the dependency landed, so this file's account of it had gone stale
ADR-0132 is on
main. The record that reverses cloud ADR-0081 D2 — The multi-organization runtime is open core — landed asdocs/adr/0132-multi-organization-runtime-is-open-core.md(squashc677cda81, 2026-09-07 05:30 UTC), together with the now-openpackages/plugins/organizations. Measured, not assumed: onorigin/main,git log --oneline | grep -cfor that PR's squash suffix returns 1 (control: a known-landed sibling PR also returns 1), the ADR file resolves undergit cat-file -e origin/main:docs/adr/0132-..., and so does the package manifest.Six places in this file were written against the opposite fact. Each is anchored by its text, not by a line number:
- **Deliberately not mirrored**:## Relationship to the D2 reversal (…)main"(ADR-0132); body cites[ADR-0132](./0132-multi-organization-runtime-is-open-core.md)and states plainly that it landedmain"⚠️ A note for the hand-mergemain"D1s in one document, which is precisely what the deliberately unrenumbered mirror exists to keep checkable. Stated, not decided: this change takes the option neither way, and the disposition stays a merge-time callWhat this record does not decide, item 4[ADR-0132](./0132-multi-organization-runtime-is-open-core.md)The file wrote its own trigger for this edit, and the condition is now met. Zero occurrences of the old PR number survive anywhere in the file (
grep -creturns 0).⛔ Nothing about the reversal is re-derived, restated or evaluated here. ADR-0132 records, in its own "What this record does not decide" section, that it does not touch cloud ADR-0081 D1, D3 or D4 — which are this file's entire content. The two records stay disjoint; this one only cites.
origin/mainwas merged into the branch (merge commitc83097a1f) so the newly-cited record exists in the tree the gates read. The PR's own diff is still exactly one file.What it records
The open-mechanism half of cloud ADR-0081 (Accepted, founder-decided in session, 2026-07-09), stated as this repository enforces it — D1 (adding a teammate is open and always goes through better-auth invitations), D3 (the in-shell surface is the organization record page behind a templated nav deep-link) and D4 (control-plane roster reads are organization-scoped).
⛔ It decides nothing. The cloud D-numbers are kept unrenumbered — a mirror a reader cannot line up against its original is not checkable — so the sequence reads D1, D3, D4 with D2 visibly absent.
Where a local record already decides a point, the file cites it instead of restating it: ADR-0093 (membership lifecycle; its D9 already anchors the active-organization resolution the code cites as "ADR-0081 D1"), ADR-0105 D12 (the multi-org entitlement anchor), and ADR-0131 D1/D7/D9 (total organization ownership). Per the card's later pointer comment, D4 does not decide whether
sys_memberkeeps its organization column — that is ADR-0131 D7's writer-facts question, answered by the C6 census in #15207, which the file cites and leaves alone.The ADR number: 0133
ls docs/adr/onorigin/mainat77781151d, when the file was written — the highest record was 0131.⇒ 0133, and the number below it is no longer a gap: 0132 is a real, merged record.
⭐ Two findings that shaped how the file is written
1. A line-number anchor in an ADR is a hard gate failure. The card supplied three
file.tsplus line-offset positions.check:adr-symbol-anchors(the #13556 migration, maintainer ruling 2026-09-01) makes any surviving line anchor underdocs/adr/**a finding with no transition period — the census behind it measured 243 of 337 line anchors already broken. Every anchor in this file is therefore a symbol anchor (path#symbol) or a file-level path, and the gate re-confirms it: 0 line anchors survive.2. The record being cited could not be named by number — and now can. The file originally cited ADR-0132 by number and
check:adr-anchorsfailed: a citation of an ADR number naming no record underdocs/adr/is a squat, retroactively falsified if the record lands under a different number. The reversal was therefore referenced by PR number, with the reason recorded in the file so the next author would not undo it. That constraint is discharged by this round — the record exists, the number resolves, and the gate stays green with the citation in place.Open question left for the hand-merge, not resolved here
⭐ "Opens on tab-0 Members" is asserted in this repo's comments but declared by none of its metadata. Measured on
origin/mainat77781151dand re-checked on today'sorigin/main: no object underpackages/platform-objects/src/identity/declares therelatedListprominence key — the key objectui reads to promote a child list to its own tab — and norelatedLayoutoverride exists anywhere in this repository. Under the documented default with no primary list declared, related lists collapse into a single stacked tab. Two source comments and a QA checklist item nevertheless describe a Members/Invitations/Teams tab strip.So the ADR states the deep-link contract, which is declared, and does not assert a tab order, which is not — and records the discrepancy under "What this record does not decide" rather than repairing it. Filed separately; ⛔ not fixed in this PR.
Observed while re-checking, deliberately not touched
docs/adr/0132-multi-organization-runtime-is-open-core.mdstill carries- **Status**: Proposed (2026-09-06) — awaiting the maintainer's hand-mergealthough it has merged. That is residue on another record, owned by whoever lands its correction; ⛔ nothing in this PR edits it.Scope held
⛔ Not touched, deliberately: the cloud-repo pointer comment on cloud ADR-0081, and #14361's re-pointing of the bare
ADR-0081citations acrosspackages/platform-objects/src/identity/**andplugin-auth— both belong to other cards. No code and no metadata changed, so the card'sClause-②: nostill holds as declared.Changeset
Judged, not defaulted:
skip-changesetapplies. The diff publishes nothing from any package — it adds one file underdocs/adr/, which no package ships and nopackage.jsonreferences. That is the rule the label encodes, anddocs/adr/**is the documented example of it. The label was applied additively and read back on an earlier round; it is present on this PR now.Verification
Gate set derived mechanically on the final commit —
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, which reports its own provenance (derived from the tree of 'objectstack-ai/objectstack' at commit01b450d40) and takes its change set from the merge base rather than a hand-built diff.17 commands derived, 17 green at
01b450d40— the union was re-derived and re-run in full after the final commit, and the derivation confirmed the same 17-command family. Each exit code was captured immediately after a single redirected command, never through a pipe. The verdict lines are the gates' own:check-adr-symbol-anchors— 1970 anchors across 137 records resolve — 218 symbol (195 declaration, 23 literal), 1729 file-level, 23 cross-repo, 6 exempt, 3 continuation. 0 line anchors survive.check-adr-anchors— OK (53 anchored file(s), every governing ADR still referenced; 131 decision number(s), each naming one decision or an allowlisted pair; 33638 citation(s) across 4260 file(s) resolve; 997 decision-letter citation(s) on the anchored surface name a decision the record makes).check-adr-links— 667 relative link destination(s) under docs/adr/ resolve (this is what re-checks the new relative links and the heading anchor the header bullet points at)check-doc-authoring— 396 files clean — no bare metadata literalscheck-nul-bytes— OK (scanned 8128 text file(s) … no raw ASCII control bytes)check-closing-keyword-parity— OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 8135 tracked file(s), all registered).check-ci-filter-parity,check-comment-mask-corpus,check:cross-package-test-inputs,check:driver-memory-census,check:pm-governed-merges,check:refd-timer-probe,check:watch-hint-literal, and the four--self-testhalves — all exit 0.One gate first answered exit 3 — PREREQUISITE NOT MET, which is neither a pass nor a finding:
@objectstack/lint'scheck:doc-formula-expressionsrefuses to run against unbuilt workspace output. It was cleared rather than reported, by building the two packages it names through the shared verify lock (scripts/pm/os-verify-lock.sh— VERDICT command-exit 0 · held the lock 188s · waited 0s), then re-run to a real reading: 22 record-scoped formula example(s) across 432 files / 1371 TS blocks judged clean, plus its spec-TSDoc and field-level legs.Governance predicate, asserted rather than assumed:
node scripts/pm/check-governed-merges.mjs --test docs/adr/0133-org-management-open-basics.md→ exit 3, GOVERNED, "a human merge is the review record for this PR". That is the intended reading, not a failure.The repo-wide
pnpm lintsweep is CI's; the local scope was narrowed and the narrowing is measured, not assumed. Three readings: the population comes from eslint's own flat config, not from a guess — run against the changed path it answers "File ignored because no matching configuration was supplied";--format jsonreports 0 linted files for that path (one entry,errorCount: 0, carrying only the ignore notice); and no eslint configuration file is in this diff, so no untouched file's verdict can move either. No CI state is predicted here.Generated by Claude Code