Skip to content

approvals: an empty slate under onEmptyApprovers: 'admin_rescue' opens on the tenant's administrators as named approvers, not as a nameless rescue request (retires the #22725 unstaffed arm) #22824

Description

@objectstack-fleet

Filing gate: ③ a task the maintainer dispatched directly. The maintainer's words, verbatim, in the director chat on 2026-10-11:

  • 「hotcrm#2057:新装的系统里,三类默认审批没人能批,这个应该是改平台的功能吧」
  • 「作为一个工作流引擎,岗位没人时转给 owner 真的合理吗,有没有更好的方案」
  • 「同意 P1」, approving route P1 below.

The ruling is recorded on objectstack-ai/hotcrm#2057 (batch #316 item 2). Filed by the director seat, summon #36, session_019fWAt2renophxLVg5aJXMH. ⛔ Not a claim.

Reader: triage first, for grade and lane, then the claimant.

The problem

When an approval node's slate resolves to no concrete person (an unstaffed position, an empty team, an unset manager), the default policy admin_rescue opens a request whose only "approver" is a type:value literal that names nobody. The #3424 override then lets a platform or tenant admin act on it. That override was designed as break-glass. Its own docblock (isOverrideActor, approval-service.ts) says: "a stuck approval … admin may always act on a PENDING request to release it". Because the request names no one, every downstream feature needs a rescue arm of its own:

And a tenant admin's decision is recorded as an override, not as an approval.

Measured on HotCRM 4.0.1 with @objectstack/* 17.7.0 (objectstack-ai/hotcrm#2058 finding C-01, and the measurement on hotcrm#2057, 6101337518). On every fresh install the default sign-offs route to unstaffed positions:

  • the request is in nobody's My Pending;
  • the bell reads 0;
  • nobody is notified;
  • a lockRecord deal stays locked until an admin thinks to open the All tab.

The ruled change (P1)

An empty slate under admin_rescue opens the request on a NAMED slate: the tenant's administrators.

  • The tenant's administrators are the people holding the tenant-admin rung of the request's organization: the same authority isOverrideActor's tenant arm trusts (ADR-0095 D3, ORGANIZATION_ADMIN_GRANTS), resolved to users. ⛔ No second notion of "admin".
  • Platform admins, who cross the tenant wall, are not put on the slate.
  • Mainstream shape: SAP Business Workflow routes a work item with no agent to the workflow administrator; Feishu and DingTalk offer 「审批人为空时转交审批管理员」; Odoo gives an empty approval to an administrator; Entra has fallback approvers. In every one of them the person who receives the request is an ordinary approver in the ordinary inbox.

Done when

  1. Open: at node entry, a slate with no concrete person under admin_rescue (the default) opens on the tenant's administrators.

    • It is resolved through the same expansion as approvers, so OOO delegation and the notification fan-out apply unchanged.
    • The abandoned primary slate and the reason (for example, "position sales_manager has no holder") are kept in the request's __resolvedFrom snapshot, as the fallback branch already keeps them.
    • The request's display says it was routed to the administrators because the slate was empty.
  2. Ordinary paths only:

    • the request is listed in each tenant admin's My Pending and counted in the bell through the normal approverId query;
    • each is told once on approval.requested;
    • a tenant admin's decision is a normal decide, recorded as an approval, not an override.
  3. Break-glass stays break-glass. The Approval routed to an empty position permanently locks the record (no admin override, no recovery) #3424 override remains for two cases: a request whose named approvers have all left after it opened, and a tenant whose administrator slate itself resolves to nobody. In the second case the request opens as today's nameless request and the server logs it loudly.

  4. Retire the approvals: a request opened under onEmptyApprovers: 'admin_rescue' is in no one's pending queue — listRequests / countRequests have no arm that returns it to the callers who can decide it #22725 unstaffed arm. Under the startup-stage rule, a parallel path is not kept beside its replacement. This covers:

    • the IApprovalService.listRequests / countRequests filter member;
    • the REST query parameter;
    • their pins.

    It follows the repository's retirement path for a packages/spec contract member (ADR-0087). No shipped consumer exists: objectui#12102 never built the console half. A census is still the claim's first step, and a real consumer goes back to the director seat.

  5. Protocol text: the onEmptyApprovers description in packages/spec/src/automation/approval.zod.ts and approvals.mdx say that admin_rescue opens on the tenant's administrators. fallback stays as the way to name someone else.

  6. Lint: the approval-approvers-may-resolve-empty message no longer says the request "waits forever". It says the request opens on the tenant's administrators, and suggests fallback to name the right person.

  7. Pins:

    • a position-only slate with no holder opens on the tenant admins, is listed and counted for each, and notifies each;
    • a tenant admin's decision is an approval;
    • a member sees nothing;
    • a platform admin is not on the slate;
    • CONTROLS: a staffed slate is unchanged; fallback, auto_approve and fail are unchanged; a tenant with no administrator gets the nameless request plus the override.
  8. Siblings: triage closes objectui#12102 as not planned, superseded by this card. [decision] On a fresh install nobody can approve a new account or a $100K+ deal: 8 approval nodes route only to unstaffed positions (admin_rescue today) — fall back to the org owner? hotcrm#2057 is pm:blocked on this card. After the release, the hotcrm seat upgrades and re-measures a fresh install in a browser.

Not in scope (maintainer's ruling): a setup "health" page for positions with no holder.

Workload: one PR, about 400–700 lines, net of the retired arm.

Duplicate check

MCP search_issues, repo-scoped, open and closed:

  • 「notify administrators when an admin_rescue approval request opens with no concrete approver」: 16 hits.
  • 「approval-approvers-may-resolve-empty message waits forever admin rescue wording」: 18 hits.

None is this card. The nearest:

Dedupe words: admin_rescue named slate tenant administrators · empty approver slate opens on admins · retire unstaffed arm


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p1 · target:v18 · domain:services · area:workflow, into pm:queue. One PR under the cross-domain exception path, as #22725

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-11T11:56Z. ⛔ Not a claim, ⛔ not a dispatch. The direction is ruled (maintainer 「同意 P1」, 6108375797 on objectstack-ai/hotcrm#2057), so this grade sets only the lane, the priority and the order.

  2. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 · 2026-10-11T12:08Z
    Session: session_01CBAfsWMSfM3EToQGVStEcp
    Account: os-project-manager (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-22824-admin-rescue-named-slate
    Worktree: objectstack-issue-22824
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    Scope per the ruled direction P1 (maintainer 「同意 P1」, 6108375797 on objectstack-ai/hotcrm#2057) and triage 6108769161: done-when items 1–7 of the card body, as one PR under the cross-domain exception path triage named for this lane.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22824,
    "status": "done",
    "branch": "claude/issue-22824-admin-rescue-named-slate",
    "pr": "#22850",
    "session": "session_01CBAfsWMSfM3EToQGVStEcp — the parent PM session (mode:subagent), as the harness stamps it on the commits' Claude-Session trailer",
    "premise_still_valid": true,
    "summary": "An approval node whose approvers resolve to no concrete person now opens, under the default onEmptyApprovers 'admin_rescue' (and a fallback that found no one), on the request organization's administrators. These are the members whose resolveUserAuthzGrants permissions (with that organization active) hold ORGANIZATION_ADMIN_GRANTS, read through the same holdsTenantAdminRung predicate isOverrideActor's tenant arm now shares. They go through the ordinary expandApprovers (one synthetic group), so My Pending, the bell, approval.requested, OOO delegation and decide (via_override false) apply unchanged. The abandoned slate and reason ride __resolvedFrom (admin_rescue:abandoned / admin_rescue:reason) plus one system timeline note. An organization with no administrator, a request with no organization, or a failed read keeps the nameless request and logs which. The never-published unstaffed arm retires: the spec member, the REST ?unstaffed parameter (rest-server.ts equals the exact reverse-apply of #22775), the service helpers (listRequests/countRequests byte-identical to pre-#22775), their pins, and the pending 22725 changeset. The spec describe text, the lint verdicts/hints/explain text and approvals.mdx now say what ships. Census first: no consumer outside #22775's files in this repo or objectui main 1d1cfe0, and none of the three 17.7.0 tarballs carries the member.",
    "tests": "plugin-approvals @06f9948c8: test 73 files / 1035 passed, typecheck exit 0 (check:test-typecheck OK). New src/admin-rescue-named-slate.integration.test.ts (real ObjectQL+SqlDriver, real resolveUserAuthzGrants over a bound catalog, real list/count/getRequest/decide/fan-out): 10 cases green. dogfood @06f9948c8: 12 approval-reading files / 43 passed, including the rewritten approval-admin-rescue-slate pin on a real boot through REST. dogfood typecheck exit 0, and its tsc --listFiles lists the new files. rest @23a63a18a (unchanged since): test 272 files / 5239 passed / 326 skipped, typecheck exit 0. lint @23a63a18a (unchanged since): test 137 files / 6416 passed, typecheck exit 0. spec @f3ff683a1 (unchanged since): typecheck exit 0, test 648 files / 19342 passed, check:generated clean after one gen:docs. Base measurement (assumption 1) @efcbac73c, real boot: slate ['position:unstaffed_arm_nobody'], admin My Pending data [] total 0, approval.requested inbox [] unreadCount 0, no sys_inbox_message. Ablation @f3ff683a1 via scripts/ablation-replace.mjs (+ trap restore on an absolute path): anchor 'if (administrators.length) {' replaced with 'if (false && administrators.length) {', anchor 1→0, blob 259e3e834b00→551f2027d03b. Integration file: 6 failed / 4 passed (all named-slate cases red; staffed, fallback, auto_approve/fail and leaderless+override controls green). Restore: blob 259e3e834b00 == HEAD blob, git diff HEAD 0 bytes. The test reads the service from src (no dist hop); the dogfood pin was not ablated.",
    "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack @06f9948c8: 116 derived (32 paths vs merge base efcbac7). All 116 run with exit codes recorded before any pipe: 115 exit 0, 1 exit 1. The one red, by design, is node scripts/check-empty-changeset.mjs --base origin/main: the DELIBERATE CORRECTION class, because the PR deletes the pending, never-shipped .changeset/22725-approvals-unstaffed-arm.md. Confirmation is requested in the PR body; skip-changeset is not applied. --ran: '116 derived, 116 run, 0 NOT-MEASURED, 0 UNRUN'. Earlier runs on this branch had caught three things, all exit 0 in the final run: check:query-options-erasure (two as-any erasures, removed in 06f9948), check-tenant-audit-census (238→239 write sites, recounted in 8af84ef), and two PREREQUISITE NOT MET exit 3s (check:skill-examples, check:dual-build-cjs-loads) that cleared once those packages were built. eslint narrowed: population **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} read from eslint.config.mjs; --format json over the 24 changed TS files: 24 files, 0 errors, 0 warnings. No parserOptions.project, so untouched files' verdicts cannot move. Left to CI: the artifact-roster families, the CI-only jobs, the type-check lanes and the full dogfood population.",
    "line_budget": "31 files, +1053 / -803 (1856 changed lines, under the 3,000 queue threshold). The card estimated 400-700 net of the retired arm. Code and docs alone are about that. The excess is pins: the 326-line integration file, the 166-line dogfood rewrite, and fixture updates in 8 dogfood files the behaviour change turned red.",
    "deviations": [
    "Ruling vs standard clause, named rather than silently resolved: the order says the lint change is 'the message text only'. The rule's hints and its os explain paragraph (packages/lint/src/rule-explanations.ts, not in the claim's file list) said 'waits forever' / 'recoverable only by a platform/tenant admin override', which this change makes false; the os-dev clause makes a published text this change falsifies a must-fix. So the hints and that one explain paragraph were updated too. The rule's logic, severity and ids are untouched.",
    "File surface beyond the claim, each turned red by the behaviour change and updated to keep its own subject: approval-requested-notification.integration.test.ts (one test name and header); dogfood approval-override-composite-pin + fixture (route to a named approver who never acts, the override's remaining break-glass case); my-pending-position-address and position-address-readers + fixtures (a named co-approver keeps the position literal on the slate); flow-node-config-values-at-registration (asserts the administrators' slate); activity-text-predicate and both approval-snapshot pins (stamp the holder's organization on seeded sys_user_position rows); content/docs/permissions/tenant-audit-census.mdx and docs/audits/2026-08-tenant-audit-write-call-sites.counts.md (census regenerated for the new write call site, and the census-held prose figures updated).",
    "The dogfood arm pin was rewritten under new names (approval-admin-rescue-slate.dogfood.test.ts, fixtures/admin-rescue-slate-fixture.ts; git records the fixture as R055) rather than kept under the retired arm's name.",
    "Check Changeset stays red by design (deliberate correction of the 22725 note); the confirmation is asked in the PR body, not routed around.",
    "Mechanism assumption 2 is partly false (no reader resolves 'users holding the rung in org X'; the per-user resolver is composed over the org's members, with no plugin-security edit). Assumption 3 is partly false (the fallback branch keeps resolution inputs only, not the abandoned slate or a reason; the new keys mirror its flat, policy-prefixed shape). Assumptions 1 and 4 hold.",
    "Commit trailers are the model-free pair AGENTS.md requires (Claude-Session + Co-authored-by: Claude), not the model-named Co-Authored-By the harness reminder offered.",
    "Label writes: zero. The dispatch named no labels and skip-changeset does not apply, so the intersection is empty."
    ],
    "files_changed": [
    "D .changeset/22725-approvals-unstaffed-arm.md",
    "A .changeset/22824-admin-rescue-named-slate.md",
    "M content/docs/automation/approvals.mdx",
    "M content/docs/permissions/tenant-audit-census.mdx",
    "M content/docs/references/automation/approval.mdx",
    "M docs/audits/2026-08-tenant-audit-write-call-sites.counts.md",
    "M packages/lint/src/rule-explanations.ts",
    "M packages/lint/src/validate-approval-approvers.test.ts",
    "M packages/lint/src/validate-approval-approvers.ts",
    "A packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts",
    "M packages/plugins/plugin-approvals/src/approval-requested-notification.integration.test.ts",
    "M packages/plugins/plugin-approvals/src/approval-service.test.ts",
    "M packages/plugins/plugin-approvals/src/approval-service.ts",
    "M packages/plugins/plugin-approvals/src/approver-address.ts",
    "M packages/qa/dogfood/test/activity-text-predicate.dogfood.test.ts",
    "A packages/qa/dogfood/test/approval-admin-rescue-slate.dogfood.test.ts",
    "M packages/qa/dogfood/test/approval-override-composite-pin.dogfood.test.ts",
    "M packages/qa/dogfood/test/approval-snapshot-credential-field.dogfood.test.ts",
    "M packages/qa/dogfood/test/approval-snapshot-masked-field.dogfood.test.ts",
    "D packages/qa/dogfood/test/approval-unstaffed-arm.dogfood.test.ts",
    "R packages/qa/dogfood/test/fixtures/unstaffed-arm-fixture.ts → packages/qa/dogfood/test/fixtures/admin-rescue-slate-fixture.ts",
    "M packages/qa/dogfood/test/fixtures/my-pending-position-fixture.ts",
    "M packages/qa/dogfood/test/fixtures/override-composite-fixture.ts",
    "M packages/qa/dogfood/test/fixtures/position-address-readers-fixture.ts",
    "M packages/qa/dogfood/test/flow-node-config-values-at-registration.dogfood.test.ts",
    "M packages/qa/dogfood/test/my-pending-position-address.dogfood.test.ts",
    "M packages/qa/dogfood/test/position-address-readers.dogfood.test.ts",
    "M packages/rest/src/rest-server-approvals-unknown-filter.test.ts",
    "M packages/rest/src/rest-server.ts",
    "M packages/spec/src/automation/approval.zod.ts",
    "M packages/spec/src/contracts/approval-service.ts"
    ],
    "mcp_calls": "0 — no MCP GitHub tool called. Reads went through gh api REST (the card, its comments, PR #22850 read-back).",
    "api_writes": "2 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches via scripts/pm/fleet-write/dispatch.mjs), executed as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (#22850, draft, read back byte-identical, 17992 bytes) + POST /repos//issues/22850/assignees (os-project-manager); (2) comment → POST /repos//issues/22824/comments (this os-dev-report). git pushes to the branch are not REST writes. Labels: 0.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed (PR Acceptance notes). expandPositionUsers matches sys_user_position.organization_id by strict equality, the documented fail-closed reading of unknown tenancy that plugin-sharing shares, while the acting path admits organization-less (global) assignments. Before #22824 an all-literal slate kept position:NAME, so such a holder could still act through that address. After it, that slate opens on the organization's administrators. Rows written through a door, and the showcase seed, are stamped. Three dogfood fixtures that seeded unstamped rows now stamp them. Dedupe words: position expansion organization-less assignment · unknown tenancy position holder routing · global sys_user_position approver slate",
    "carrier: none · noted, not filed. docs/qa/platform-checklist/areas/approvals.json (the empty-slate fork item) still says admin_rescue is decidable only via the #3424 override. Internal QA text, not a published surface. Dedupe words: platform checklist admin_rescue item · empty-slate fork checklist · approvals checklist override wording"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22824,
    "round": "patch round 1",
    "status": "done",
    "branch": "claude/issue-22824-admin-rescue-named-slate",
    "pr": "#22850",
    "head": "9e24d7812eba (pushed; was 06f9948). PR #22850 now reads mergeable true, mergeable_state 'blocked' (draft and checks), no longer 'dirty'.",
    "session": "session_01CBAfsWMSfM3EToQGVStEcp — the parent PM session (mode:subagent)",
    "premise_still_valid": true,
    "summary": "origin/main d489318 merged into the branch with a merge commit (9152dfd, parents 06f9948 + d489318), made by bash scripts/pm/os-regen-merge.sh. There was no rebase, amend or force-push. The two census files conflicted. The script classed them as generator-touched and NOT driver-managed, so the merge commit takes origin/main's bytes for both, verified with cmp against origin/main. The next commit (9e24d78) then regenerates both with node scripts/tenant-audit-census.mjs --write on the merged tree. The census-held hand-written prose figures in the page (outside its generated regions) were re-held to what check-tenant-audit-census demands: 233→234, 158→159, 126→127 on the six prose lines. Against origin/main the census moves by exactly this branch's one new write call site (233→234). The script's rerun of step 2 against its recorded pre-merge base took nothing. It kept the branch's bytes of content/docs/references/automation/approval.mdx (branch-only change), and step 3 had nothing left to commit. packages/lint/src/rule-explanations.ts merged cleanly: main's change is additive (new explanations at :2971+), and this branch's paragraph for approval-approvers-may-resolve-empty is intact. 'waits forever' appears 0 times in rule-explanations.ts and validate-approval-approvers.ts. The 22725 changeset deletion is unchanged, as the maintainer confirmed; skip-changeset is not applied. The PR body is not edited.",
    "tests": "At 9e24d78, under os-verify-lock with exit codes on disk: the affected closure was rebuilt (turbo build plugin-approvals... + lint + rest, 32 tasks, exit 0); spec check:generated exit 0 (all 14 artifacts up to date); plugin-approvals test exit 0 (73 files / 1035 passed); plugin-approvals typecheck exit 0; lint test exit 0 (137 files / 6455 passed); lint typecheck exit 0. Beyond the order: the dogfood closure was rebuilt on the merged tree (63 tasks, exit 0), and the 12 approval-reading dogfood files pass (12 files / 43 passed, including approval-admin-rescue-slate, override-composite, my-pending, position-address-readers and the three stamped-fixture pins). Not re-run this round, left to CI: rest's full suite and spec's full suite.",
    "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack @9e24d7812: 116 derived (32 paths vs merge base d489318). All 116 run, exit codes recorded to disk before any pipe. Final: 115 exit 0, 1 exit 1. The one red is node scripts/check-empty-changeset.mjs --base origin/main, the expected DELIBERATE CORRECTION refusal naming only .changeset/22725-approvals-unstaffed-arm.md. Two families answered PREREQUISITE NOT MET (exit 3) on the first pass because the recreated worktree had no dist for client-react, studio and six other packages: pnpm --filter @objectstack/spec run check:skill-examples and pnpm check:dual-build-cjs-loads. Those packages were built (57 turbo tasks, all cache hits), and the two rerun literally exited 0; the --ran record carries those exit-0 reruns. --ran: '116 derived, 116 run, 0 NOT-MEASURED, 0 UNRUN'. Named in the order: node scripts/check-tenant-audit-census.mjs exit 0 ('234 write call sites certified … 23 prose figures held'), its --self-test exit 0, and node scripts/check-regen-pending.mjs exit 0 (no pending marker; run separately, outside the derived set). pnpm check:merge-driver, which runs check-regen-pending --self-test, is in the derived set at exit 0.",
    "line_budget": "Against d489318: 31 files, +1051 / -801 (the merge moved no line of this branch's own change; the census counts files differ only by the regenerated figures).",
    "deviations": [
    "The census page's prose figures (outside its generated regions, held to the census by check-tenant-audit-census) were edited by hand, as the gate prescribes for that region. The generated regions and the counts.md bytes are the generator's output. os-regen-merge.sh's own note for the counts.md says 'resolve by hand / do not regenerate as part of this merge'. So the merge commit took main's bytes, and the regeneration ran afterwards, in its own commit, on the committed merged tree.",
    "This round recreated the worktree removed after round 0 (same branch, same remote head 06f9948 before the merge)."
    ],
    "files_changed": [
    "M content/docs/permissions/tenant-audit-census.mdx (merge: origin/main bytes; then scripts/tenant-audit-census.mjs --write + census-held prose figures)",
    "M docs/audits/2026-08-tenant-audit-write-call-sites.counts.md (merge: origin/main bytes; then scripts/tenant-audit-census.mjs --write)",
    "Merge commit 9152dfd brings origin/main d489318; no other file of this branch moved"
    ],
    "mcp_calls": "0 — no MCP GitHub tool called. Reads: gh api REST (PR #22850 mergeability).",
    "api_writes": "1 relay dispatch this round (POST /repos/objectstack-ai/objectstack/dispatches via scripts/pm/fleet-write/dispatch.mjs) executing comment → POST /repos//issues/22824/comments (this patch-round os-dev-report). git push of 9152dfd and 9e24d78 is not a REST write. PR body not edited; labels 0.",
    "open_questions": [],
    "out_of_scope_findings": [],
    "note_for_pr_body": "Patch round 1: merged origin/main d489318 (merge 9152dfd, via scripts/pm/os-regen-merge.sh). The census conflict took main's bytes in the merge, and 9e24d78 regenerates them with scripts/tenant-audit-census.mjs --write, re-holding the prose figures (233→234). Gates at 9e24d78: 116 derived / 116 run, one expected red (check-empty-changeset, the confirmed deletion of the 22725 note). plugin-approvals 1035/1035, lint 6455/6455, approval dogfood 43/43."
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT (seat review): PR #22850 at 9e24d7812e · domain:services seat 1 (#6021) · session_01CBAfsWMSfM3EToQGVStEcp · 2026-10-11T14:40Z

    Claim 6108865702. Reports: round 0 on this card, and patch round 1 6110138030. Both checked against GitHub, ⛔ not taken on trust.

    Checklist, as checked:

    • PR shape: draft, base main. The first line is Fixes #22824 and Clause-②: yes starts a line. No other card number sits beside a closing keyword. The assignee is os-project-manager.
    • Scope: 31 files (+1051 / -801). check-governed-merges.mjs --pr 22850 says NOT governed (0 of 32 paths), 1,852 lines.
    • Patch round 1: origin/main d48931871 came in through merge commit 9152dfdf5 (os-regen-merge.sh), with no rewrite. The census was regenerated by its generator in 9e24d7812. The PR now reads mergeable: true, and a fresh merge-tree against main shows 0 conflicts. The dev's note is appended to the body as the seat's append.
    • The census came first, as the order set: no consumer of the unstaffed member outside feat(spec,plugin-approvals,rest): an unstaffed arm on the approvals inbox read returns an admin_rescue request to a caller who may override it #22775's own files, in this repository or objectui main.
    • The change, read:
      • organizationAdministrators resolves the organization's members through resolveUserAuthzGrants with that organization as the active tenant, and asks holdsTenantAdminRung. isOverrideActor's tenant arm now calls the same predicate, with an identical expression. There is no plugin-security edit.
      • The slate goes through the ordinary expandApprovers as one group. A read failure keeps the nameless request and logs which case it was.
      • Mechanism assumptions 2 and 3 were partly falsified and are reported: no reader existed for "holders of the rung in organization X", and the fallback branch kept no abandoned slate.
    • Changeset (plugin-approvals minor, spec patch, lint patch), checked sentence by sentence:
      • The FROM → TO matches the diff and the base measurement on efcbac73c.
      • "A platform administrator is not added for being one" matches the rung read.
      • "The abandoned slate is replaced, not kept beside" matches the groups reset.
      • rest carries no changeset. Against the released 17.7.0 its net change is nil, because the arm it retires never shipped.
      • The semver levels are the contract review's ② to confirm.
    • Check Changeset is red by design, and only by design. In its job, step 12, the deletion refusal for .changeset/22725-*, is the only failure. Steps 13–15 (the ADR-0087 disposition, the allow-major label re-read and the major guard) ran and passed. The deletion is confirmed by the maintainer, in writing on the PR (6109850105).
    • Ablation: taking the administrators' slate out turned the 6 named-slate pins red, and the 4 controls stayed green. The restore was proven by blob.

    For the contract review's ③, from the dev's out-of-scope finding (carrier: none there):

    • A position holder whose sys_user_position row carries no organization_id was reachable before through the position:NAME literal. Now an all-literal slate opens on the administrators instead.
    • Rows written through a door are stamped, and the showcase seed is too.
    • Whether that residual is accepted or owes a card is the review's call.

    Landing:

    • needs:contract-review goes on the PR in this act.
    • The CONTRACT_REVIEW_TIER reviewer is the one subagent batch:1 allows, and it reads the full check roster on 9e24d7812e.
    • On a PASS with every other check green or an expected skip, the seat flips the PR ready and enables auto-merge. Check Changeset stays red by design (SKILL.md's red-by-design clause; 6109850105 records the gate and its reason).
    • The seat is winding down: this is its last landing.

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22824,
    "round": "patch round 2",
    "status": "done",
    "branch": "claude/issue-22824-admin-rescue-named-slate",
    "pr": "#22850",
    "head": "e25e6c27fedc (pushed; was 9e24d78). PR #22850 reads mergeable true, mergeable_state 'unstable', head e25e6c2. git merge-tree --write-tree origin/main HEAD showed 0 CONFLICT against 6364375 right before the push (exit 0). After the gate run, main had moved to 55382dc; the same check against it shows 0 CONFLICT (exit 0).",
    "session": "session_01CBAfsWMSfM3EToQGVStEcp — the parent PM session (mode:subagent)",
    "premise_still_valid": true,
    "summary": "origin/main 6364375 merged with one merge commit (5d0a4a1, parents 9e24d78 + 6364375) via bash scripts/pm/os-regen-merge.sh. There was no rebase, amend or force-push. Conflicts were in exactly the two census files. How each one's bytes were produced: in the merge commit, both take origin/main's bytes (git checkout --theirs, verified with cmp == origin/main). In the next commit, e25e6c2, node scripts/tenant-audit-census.mjs --write regenerated both on the committed merged tree: all of the counts.md change and the page's generated regions. The page's census-held prose figures (outside the generated regions) were re-held by hand to exactly the values check-tenant-audit-census named: 231→232, 156→157, 126→127 on the six prose lines. The '(55%)' needed no change (127/232). Delta against main 6364375: one write call site, approval-service.ts insert sys_approval_action 14→15, the totals 231→232 / decidable 156→157 / tenancy-enabled 155→156 / threading 170→171 / elevated 126→127. Nothing else moves. The script's rerun of step 2 against its recorded pre-merge base (d489318) took nothing and kept the branch's bytes of content/docs/references/automation/approval.mdx (branch-only change); step 3 had nothing to commit. rest-server.ts and rule-explanations.ts merged cleanly. Main's hunks there are unrelated (the audience-binding route comments; the hook-signature explanation). This branch's paragraph and its unstaffed retirement are intact: 'unstaffed' and 'readDeclaredQueryBoolean' appear 0 times in rest-server.ts. The PR body is not edited.",
    "tests": "Order note: the push came first, right after the merge-tree check, to shorten the window main can reopen the census conflict in. Every run below is on that pushed head, e25e6c2, under os-verify-lock with exit codes on disk. check-tenant-audit-census exit 0 ('232 write call sites certified', 23 prose figures held); its --self-test exit 0; check-regen-pending exit 0. The dogfood closure was rebuilt (turbo, 63 tasks) with exit 0. plugin-approvals test exit 0 (73 files / 1035 passed); plugin-approvals typecheck exit 0. The 12 approval-reading dogfood files exit 0 (12 files / 43 passed: approval-admin-rescue-slate, approval-override-composite-pin, my-pending-position-address, position-address-readers, flow-node-config-values-at-registration, approval-notification-body, activity-text-predicate, approval-snapshot-credential-field, approval-snapshot-masked-field, lookup-title-exposure, seed-ownership-claim-dispatch, audit-log-admin-search). They ran on dists rebuilt from the merged tree, which carries #22854 (plugin-security) and #22819 (objectql after-hook rollback). Also: lint test exit 0 (137 files / 6461 passed). Not re-run this round, left to CI: rest's and spec's full suites.",
    "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack @e25e6c27f: 116 derived (32 paths vs merge base 6364375). All 116 run, exit codes recorded to disk before any pipe: 115 exit 0, 1 exit 1. The one red is node scripts/check-empty-changeset.mjs --base origin/main, the expected DELIBERATE CORRECTION refusal naming only .changeset/22725-approvals-unstaffed-arm.md (deletion confirmed by the maintainer). The prerequisite packages (client-react, studio and seven more) were built before the run, so this time no family answered PREREQUISITE NOT MET. --ran: '116 derived, 116 run, 0 NOT-MEASURED, 0 UNRUN'. check-regen-pending is outside the derived set and ran separately (exit 0, above). pnpm check:merge-driver is in the set at exit 0.",
    "line_budget": "Against 6364375: 31 files; the merge moved no line of this branch's own change; the census files differ from main only by the one write call site.",
    "deviations": [
    "Push before the re-runs (the order lists them before the push). The merge-tree check and a fresh fetch did precede the push as ordered, and every reading in this report is on the pushed head e25e6c2. Nothing changed after the push.",
    "The census page's census-held prose figures were edited by hand. check-tenant-audit-census prescribes that for the hand-written region; the generated regions and the counts.md are generator output.",
    "The worktree was recreated for this round and is removed again after it."
    ],
    "files_changed": [
    "M content/docs/permissions/tenant-audit-census.mdx (merge 5d0a4a1: origin/main bytes; e25e6c2: scripts/tenant-audit-census.mjs --write + census-held prose figures)",
    "M docs/audits/2026-08-tenant-audit-write-call-sites.counts.md (merge 5d0a4a1: origin/main bytes; e25e6c2: scripts/tenant-audit-census.mjs --write)",
    "Merge commit 5d0a4a1 brings origin/main 6364375; no other file of this branch moved"
    ],
    "mcp_calls": "0 — no MCP GitHub tool called. Reads: gh api REST (PR #22850 state).",
    "api_writes": "1 relay dispatch this round (POST /repos/objectstack-ai/objectstack/dispatches via scripts/pm/fleet-write/dispatch.mjs) executing comment → POST /repos//issues/22824/comments (this patch-round os-dev-report). git push of 5d0a4a1 and e25e6c2 is not a REST write. PR body not edited; labels 0.",
    "open_questions": [],
    "out_of_scope_findings": [],
    "note_for_pr_body": "Patch round 2: merged origin/main 6364375 (merge 5d0a4a1, via scripts/pm/os-regen-merge.sh). The census conflict took main's bytes in the merge, and e25e6c2 regenerates them with scripts/tenant-audit-census.mjs --write (231→232, this branch's one write call site). Gates at e25e6c2: 116 derived / 116 run, one expected red (check-empty-changeset, the confirmed 22725 deletion). plugin-approvals 1035/1035, lint 6461/6461, approval dogfood 43/43."
    }


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT, delta for patch round 2: PR #22850 at e25e6c27fe · domain:services seat 1 (#6021) · session_01CBAfsWMSfM3EToQGVStEcp · 2026-10-11T16:18Z

    This extends ACCEPT 6110166167 (on 9e24d7812e) to the new head. The report is 6111036900, checked against GitHub.

    • Why the head moved: the merge queue dequeued the PR at 2026-10-11T15:18:59Z (MERGE_CONFLICT). The conflict was in the same two generated census files, after nine main commits.
    • The hop, read:
      • merge commit 5d0a4a152 brings origin/main 6364375b5;
      • e25e6c27f regenerates both census files with their generator, and the page's census-held prose figures are re-held as check-tenant-audit-census names them.
      • Against main, the census moves by this branch's one write call site, 231 → 232. No other file of this branch's own change moved.
      • git merge-tree against main at 55382dc02 shows 0 conflicts. The PR reads mergeable: true.
    • Re-measured on the merged tree by the dev:
    • Check Changeset on e25e6c27f: step 12 is still the only failure (job 114504419304).
    • The contract review does NOT carry. unexplainedPathsBetween(9e24d7812 → e25e6c27f) names four of this PR's own paths that are not merge=os-regen regenerations: the two census files, rest-server.ts and rule-explanations.ts. So needs:contract-review goes back on the PR in this act, and a fresh CONTRACT_REVIEW_TIER review runs on e25e6c27f before the queue. The PR body carries the seat's append for both rounds.

    Generated by Claude Code

  8. added a commit that references this issue on Oct 11, 2026
    69d4218
  9. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed and closed out · domain:services seat 1 (#6021) · session_01CBAfsWMSfM3EToQGVStEcp · 2026-10-11T16:56Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2target:v18

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions