Repository navigation
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
Activity
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsTriage: first grade,
bug·priority:p1·target:v18·domain:services·area:workflow, intopm:queue. One PR under the cross-domain exception path, as #22725Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-11T11:56Z. ⛔ Not a claim, ⛔ not a dispatch. The direction is ruled (maintainer 「同意 P1」,6108375797on objectstack-ai/hotcrm#2057), so this grade sets only the lane, the priority and the order.- Why p1 and
target:v18: it is release-first, by the maintainer's rule 「发版本优先的都p1」.- Done-when item 4 retires approvals: a request opened under
onEmptyApprovers: 'admin_rescue'is in no one's pending queue —listRequests/countRequestshave no arm that returns it to the callers who can decide it #22725'sunstaffedfilter, anIApprovalServicemember and a REST query parameter that landed on the v18 line (PR 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,c1b7f2b168) and are not yet published. - Retiring them before v18 publishes costs a revert. After it, the retirement is an ADR-0087 breaking change on a released contract.
- The defect also blocks every fresh HotCRM install's default sign-offs.
- Done-when item 4 retires approvals: a request opened under
- Lane:
packages/plugins/plugin-approvalsisdomain:services, and that lane claims it. Under the exception path, as approvals: a request opened underonEmptyApprovers: 'admin_rescue'is in no one's pending queue —listRequests/countRequestshave no arm that returns it to the callers who can decide it #22725 did, it declares:packages/spec(theonEmptyApproversdescribe text and the retired member) andpackages/lint(one rule's message) on [PM seat] domain:spec — 🟢 os-warren · session_019G5ZnRrTMiwcHG6iZM19zf #6017;packages/rest(the retired query parameter) on its lane's seat post.- One PR, because the member's retirement and its REST door go together, and the slate change makes the filter obsolete in the same stroke.
- Siblings, executed in this round as done-when item 8 directs:
- approvals: My Pending and the bell surface
admin_rescuerequests to a caller who can decide them (the console half of the rescue-queue arm) objectui#12102 closesnot_planned, superseded by this card (6108389248's pointer is on it); - [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 already
pm:blockedon this card.
- approvals: My Pending and the bell surface
- The census first (done-when item 4): a real consumer of the
unstaffedmember goes back to the director seat, as the card says. Clause-②: yes. It retires a published-schema member and changes who receives a request, so the changeset states FROM → TO.
- Why p1 and
- addedarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2and removed
on Oct 11, 2026 objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsClaim: PM loop round 4 · 2026-10-11T12:08Z
Session:session_01CBAfsWMSfM3EToQGVStEcp
Account:os-project-manager(the seat's linked user asGET /useranswers 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」,6108375797on objectstack-ai/hotcrm#2057) and triage6108769161: done-when items 1–7 of the card body, as one PR under the cross-domain exception path triage named for this lane.- An empty slate under
admin_rescueopens on the request organization's tenant administrators, resolved through the same authorityisOverrideActor's tenant arm trusts (ADR-0095 D3), through the ordinaryapproversexpansion. ⛔ No second notion of "admin". - Ordinary paths: My Pending, the bell,
approval.requestedanddecideas an approval. - Break-glass stays break-glass for its two cases.
- The approvals: a request opened under
onEmptyApprovers: 'admin_rescue'is in no one's pending queue —listRequests/countRequestshave no arm that returns it to the callers who can decide it #22725unstaffedarm retires: the census comes first, and a real consumer goes back to the director seat. - The protocol text and the lint message say what ships.
File surface, read onorigin/mainefcbac73c: packages/plugins/plugin-approvals/src/:approval-service.ts(node entry underonEmptyApprovers, the notification fan-out, theunstaffedarm oflistRequests/countRequests),approver-address.ts, and tests beside them.- Under the exception path, declared on the owning seat posts in this act:
packages/spec/src/contracts/approval-service.ts(theunstaffedfilter member retires) andpackages/spec/src/automation/approval.zod.ts(theonEmptyApproversdescribe text):domain:spec, on [PM seat] domain:spec — 🟢 os-warren · session_019G5ZnRrTMiwcHG6iZM19zf #6017;packages/lint/src/validate-approval-approvers.ts(the message only):domain:spec, on [PM seat] domain:spec — 🟢 os-warren · session_019G5ZnRrTMiwcHG6iZM19zf #6017;packages/rest/src/rest-server.tsand its approvals filter test (theunstaffedquery parameter retires), pluspackages/qa/dogfood/test/approval-unstaffed-arm.dogfood.test.tsand its fixture (retire or rewrite to the named slate):domain:cli, on [PM seat] domain:cli — ⏳ vacant #6024;content/docs/automation/approvals.mdx:domain:devx, on [PM seat] domain:devx @ objectstack — 🟢 os-warren · session_012yK1ddAdAqfzwigRHZq3r2 #6023.
.changeset/22824-*.md, and the pending, unreleased.changeset/22725-approvals-unstaffed-arm.md: the arm it announces never ships, so it is withdrawn or rewritten in the same PR.- ⛔ No change to
isOverrideActor's authority, to the C2/C3 epic's territory (plugin-securitygrant reads are read, ⛔ not edited), to thefallback/auto_approve/failpolicies, or to objectui. Stop on breach; explain in the report.
Container & model:L,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectstackon this surface: "no path-derived mandate"; the default tier). The twopackages/spec/src/**hits are Clause-② SUSPECT surfaces, so the reviewer runs atCONTRACT_REVIEW_TIERafter the dev, insidebatch:1.
Clause-②: yes - A published-schema contract member (
IApprovalServicelistRequests/countRequests'sunstaffedfilter) retires, and who receives anadmin_rescuerequest changes (triage: "the changeset states FROM → TO").
Responsibility:this repository's plugin-approvals: an empty slate under the default admin_rescue opens a request whose only approver is a type:value literal naming nobody | the #3424 break-glass override, plus the #22725 unstaffed queue arm, which needs a rescue arm in every downstream feature | every fresh HotCRM install's default sign-offs (hotcrm#2057, measured on 17.7.0): in nobody's My Pending, bell 0, nobody notified
Thread-read: 6108769161
Serial constraints cleared: - No open PR touches
plugin-approvals,packages/rest, the approval contract or schema, the lint rule orapprovals.mdx(the open PRs' file lists, read at this stamp). - approvals: a request opened under
onEmptyApprovers: 'admin_rescue'is in no one's pending queue —listRequests/countRequestshave no arm that returns it to the callers who can decide it #22725 / PR 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 (the arm this retires) merged asc1b7f2b168. security(approvals, audit): a lookup target's title is resolved into the approvals inbox payload and the activity summary without asking the target's exposure (census rows 5-6 of #22661) #22738 (domain:engine,enrichRows) is closed. - This seat's own last dispatch, [finding] trigger-api: the inbound hook matches the flow and hook id before it verifies the signature, so an unsigned post's 401 versus 404 tells which flow names are armed #22806 / PR fix(trigger-api,spec): one answer for every inbound hook post that does not verify #22822, merged as
efcbac73c; nothing of this seat is in flight (batch:1).
Generated by Claude Code
- An empty slate under
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsos-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:test73 files / 1035 passed,typecheckexit 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. dogfoodtypecheckexit 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, isnode 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
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsos-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 bybash 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 withnode scripts/tenant-audit-census.mjs --writeon 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 isnode 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-examplesandpnpm 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.mjsexit 0 ('234 write call sites certified … 23 prose figures held'), its --self-test exit 0, andnode scripts/check-regen-pending.mjsexit 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
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsACCEPT (seat review): PR #22850 at
9e24d7812e·domain:servicesseat 1 (#6021) ·session_01CBAfsWMSfM3EToQGVStEcp· 2026-10-11T14:40ZClaim
6108865702. Reports: round 0 on this card, and patch round 16110138030. Both checked against GitHub, ⛔ not taken on trust.Checklist, as checked:
- PR shape: draft, base
main. The first line isFixes #22824andClause-②: yesstarts a line. No other card number sits beside a closing keyword. The assignee isos-project-manager. - Scope: 31 files (+1051 / -801).
check-governed-merges.mjs --pr 22850says NOT governed (0 of 32 paths), 1,852 lines.- The paths outside the claim's list each sit under its own reason in the report:
- the lint explain paragraph and hints, which the change made false;
- the generated reference and census pages;
- eight dogfood pins the behaviour change turned red, each kept on its own subject.
- The declarations are amended to match:
6109856981on [PM seat] domain:spec — 🟢 os-warren · session_019G5ZnRrTMiwcHG6iZM19zf #6017,6109862133on [PM seat] domain:cli — ⏳ vacant #6024,6109865590on [PM seat] domain:devx @ objectstack — 🟢 os-warren · session_012yK1ddAdAqfzwigRHZq3r2 #6023.
- The paths outside the claim's list each sit under its own reason in the report:
- Patch round 1:
origin/maind48931871came in through merge commit9152dfdf5(os-regen-merge.sh), with no rewrite. The census was regenerated by its generator in9e24d7812. The PR now readsmergeable: true, and a freshmerge-treeagainstmainshows 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
unstaffedmember 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 objectuimain. - The change, read:
organizationAdministratorsresolves the organization's members throughresolveUserAuthzGrantswith that organization as the active tenant, and asksholdsTenantAdminRung.isOverrideActor's tenant arm now calls the same predicate, with an identical expression. There is noplugin-securityedit.- The slate goes through the ordinary
expandApproversas 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
fallbackbranch kept no abandoned slate.
- Changeset (
plugin-approvalsminor,specpatch,lintpatch), 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
groupsreset. restcarries no changeset. Against the released17.7.0its net change is nil, because the arm it retires never shipped.- The semver levels are the contract review's ② to confirm.
- The FROM → TO matches the diff and the base measurement on
Check Changesetis 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, theallow-majorlabel 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: nonethere):- A position holder whose
sys_user_positionrow carries noorganization_idwas reachable before through theposition:NAMEliteral. 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-reviewgoes on the PR in this act.- The
CONTRACT_REVIEW_TIERreviewer is the one subagentbatch:1allows, and it reads the full check roster on9e24d7812e. - On a PASS with every other check green or an expected skip, the seat flips the PR ready and enables auto-merge.
Check Changesetstays red by design (SKILL.md's red-by-design clause;6109850105records the gate and its reason). - The seat is winding down: this is its last landing.
Generated by Claude Code
- PR shape: draft, base
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsos-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 HEADshowed 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) viabash 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 --writeregenerated 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_action14→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 isnode 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-driveris 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
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsACCEPT, delta for patch round 2: PR #22850 at
e25e6c27fe·domain:servicesseat 1 (#6021) ·session_01CBAfsWMSfM3EToQGVStEcp· 2026-10-11T16:18ZThis extends ACCEPT
6110166167(on9e24d7812e) to the new head. The report is6111036900, 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 ninemaincommits. - The hop, read:
- merge commit
5d0a4a152bringsorigin/main6364375b5; e25e6c27fregenerates both census files with their generator, and the page's census-held prose figures are re-held ascheck-tenant-audit-censusnames 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-treeagainstmainat55382dc02shows 0 conflicts. The PR readsmergeable: true.
- merge commit
- Re-measured on the merged tree by the dev:
- 116 derived gates, 116 run; the one red is the confirmed
22725deletion; plugin-approvals1035/1035;- the 12 approval-reading dogfood files, 43/43, on dists that carry feat(plugin-security)!: a package's isDefault suggestion takes effect once a manage_metadata holder accepts it for the deployment (#15204 stage U2) #22854 (
plugin-security) and fix(objectql): an aborting after* hook rolls its write back on the default write door #22819 (objectql).
- 116 derived gates, 116 run; the one red is the confirmed
Check Changesetone25e6c27f: step 12 is still the only failure (job114504419304).- The contract review does NOT carry.
unexplainedPathsBetween(9e24d7812 → e25e6c27f)names four of this PR's own paths that are notmerge=os-regenregenerations: the two census files,rest-server.tsandrule-explanations.ts. Soneeds:contract-reviewgoes back on the PR in this act, and a freshCONTRACT_REVIEW_TIERreview runs one25e6c27fbefore the queue. The PR body carries the seat's append for both rounds.
Generated by Claude Code
- Why the head moved: the merge queue dequeued the PR at 2026-10-11T15:18:59Z (
- added a commit that references this issue
on Oct 11, 2026 objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsLanded and closed out ·
domain:servicesseat 1 (#6021) ·session_01CBAfsWMSfM3EToQGVStEcp· 2026-10-11T16:56Z- PR feat(plugin-approvals): an empty admin_rescue slate opens on the organization's administrators; retire the unstaffed arm #22850 merged through the queue at 2026-10-11T16:55:58Z as
69d421805, from the reviewed heade25e6c27fe.- It is governed by ACCEPT
6110166167with its delta6111059961, and by the at-tier contract review PASS6111151568. - Read back: on
origin/mainat that commit,approval-service.tsdefinesorganizationAdministrators, and.changeset/22725-approvals-unstaffed-arm.mdis gone (the deletion the maintainer confirmed in6109850105). - The queue dequeued it once (
MERGE_CONFLICT, generated census files), and patch round 2 resolved that with the generator.
- It is governed by ACCEPT
- This card:
Fixes #22824closed itcompleted. In this act,pm:dispatchedand the assignee come off.bug,priority:p1,target:v18,domain:servicesandarea:workflowstay. - Outcome on record (route P1):
- An approval node whose approvers resolve to no one, under the default
admin_rescue, opens on the request organization's administrators: the holders of theTENANT_ADMINrung, through the same grant resolutionisOverrideActortrusts. - They act as ordinary approvers: My Pending, the bell,
approval.requested, and a decision withvia_override: false. - The Approval routed to an empty position permanently locks the record (no admin override, no recovery) #3424 override stays break-glass, for an organization with no administrator and for a slate whose approvers have all left.
- The never-published
unstaffedqueue arm (approvals: a request opened underonEmptyApprovers: 'admin_rescue'is in no one's pending queue —listRequests/countRequestshave no arm that returns it to the callers who can decide it #22725) retired before release.
- An approval node whose approvers resolve to no one, under the default
- Carried forward:
- [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:blockedon this card. It unblocks for the hotcrm seat's re-measure after the release (card done-when item 8). - The stale QA checklist item (the approvals empty-slate fork, which still says decidable only via the override) is relayed to the
domain:devxseat post (6110276158). - The organization-less
sys_user_positionholder (escalated by both contract reviews,6110255223and6111151568): not filed, because filing gate ① needs areach:.- Every write door stamps
organization_id, and so does the showcase seed. The only unstamped rows measured were three hand-seeded dogfood fixtures, now stamped. So no public entry or real producer reaches it today. - Carrier: none. Dedupe words:
position expansion organization-less assignment·unknown tenancy position holder routing·global sys_user_position approver slate.
- Every write door stamps
- [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
- Mis-close check: PR feat(plugin-approvals): an empty admin_rescue slate opens on the organization's administrators; retire the unstaffed arm #22850 names one closing target. 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 #22725unstaffedarm) #22824 is the only lane card closed in the window.
Generated by Claude Code
- PR feat(plugin-approvals): an empty admin_rescue slate opens on the organization's administrators; retire the unstaffed arm #22850 merged through the queue at 2026-10-11T16:55:58Z as
- added a commit that references this issue
on Oct 11, 2026
Filing gate: ③ a task the maintainer dispatched directly. The maintainer's words, verbatim, in the director chat on 2026-10-11:
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.
packages/plugins/plugin-approvalsis the home, sodomain:services.packages/spec(theonEmptyApproversdescription and theIApprovalServicemember this retires),packages/rest(the query parameter this retires) andpackages/lint(one rule's message). Those go through the cross-domain exception path, as approvals: a request opened underonEmptyApprovers: 'admin_rescue'is in no one's pending queue —listRequests/countRequestshave no arm that returns it to the callers who can decide it #22725 did, or as segments if triage splits them.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_rescueopens a request whose only "approver" is atype:valueliteral that names nobody. The#3424override 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:onEmptyApprovers: 'admin_rescue'is in no one's pending queue —listRequests/countRequestshave no arm that returns it to the callers who can decide it #22725 added anunstaffedfilter tolistRequests/countRequestsand the REST door (PR 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, landedc1b7f2b168);openNodeRequestsays a slate of literals "notifies nobody";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:lockRecorddeal stays locked until an admin thinks to open the All tab.The ruled change (P1)
An empty slate under
admin_rescueopens the request on a NAMED slate: the tenant's administrators.isOverrideActor's tenant arm trusts (ADR-0095 D3,ORGANIZATION_ADMIN_GRANTS), resolved to users. ⛔ No second notion of "admin".Done when
Open: at node entry, a slate with no concrete person under
admin_rescue(the default) opens on the tenant's administrators.approvers, so OOO delegation and the notification fan-out apply unchanged.sales_managerhas no holder") are kept in the request's__resolvedFromsnapshot, as thefallbackbranch already keeps them.Ordinary paths only:
approverIdquery;approval.requested;decide, recorded as an approval, not an override.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.
Retire the approvals: a request opened under
onEmptyApprovers: 'admin_rescue'is in no one's pending queue —listRequests/countRequestshave no arm that returns it to the callers who can decide it #22725unstaffedarm. Under the startup-stage rule, a parallel path is not kept beside its replacement. This covers:IApprovalService.listRequests/countRequestsfilter member;It follows the repository's retirement path for a
packages/speccontract 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.Protocol text: the
onEmptyApproversdescription inpackages/spec/src/automation/approval.zod.tsandapprovals.mdxsay thatadmin_rescueopens on the tenant's administrators.fallbackstays as the way to name someone else.Lint: the
approval-approvers-may-resolve-emptymessage no longer says the request "waits forever". It says the request opens on the tenant's administrators, and suggestsfallbackto name the right person.Pins:
fallback,auto_approveandfailare unchanged; a tenant with no administrator gets the nameless request plus the override.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:blockedon 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:None is this card. The nearest:
onEmptyApprovers: 'admin_rescue'is in no one's pending queue —listRequests/countRequestshave no arm that returns it to the callers who can decide it #22725 (closed: the queue arm this retires);manager_id#22607 (closed: notification on open, concrete approvers only);onEmptyApproversgains'fallback'with a siblingfallbackApproversat the node level — the empty{ type: 'manager' }rung becomes survivable (from #16678 Phase 2 §8.2, ruled) #17931 (closed:fallbackadded);approvals.mdxsays an approver entry that resolves to nobody opens an empty slate that "parks forever"; group entries keep theirtype:valueliteral, and every empty slate reachesonEmptyApprovers#22584 (closed: docs on empty slates);Dedupe words:
admin_rescue named slate tenant administrators·empty approver slate opens on admins·retire unstaffed armGenerated by Claude Code