You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
plugin-approvals: a field approver id, or an out-of-office delegate, that names no account still opens a slot no one can take #27752
Found by the dev of #27397 (PR #27749, os-dev-report 6509613849, out-of-scope findings). Inferred from the code, not reproduced at a door. Out of that card's file surface.
What may be wrong
#27749 makes a { type: 'user', value } approver that names no account expand to nobody: it logs the shared "expanded to nobody" warning, adds no slot, and lets an emptied slate reach the node's onEmptyApprovers. Two sibling paths keep the old shape:
type: 'field'.resolveApproverSpec's field branch (packages/plugins/plugin-approvals/src/approval-service.ts) passes each id the record field holds straight to applyOooDelegation. An id no account carries opens a phantom slot, with no warning and no onEmptyApprovers.
Out-of-office delegation. A delegation whose delegate_id names a removed account routes the approver to that delegate unchecked, which is again a phantom slot.
Expected
Measure each. Where a slot names no account, treat it the way #27749 treats user: the same existence predicate (reassignTargetRefusal with no organization), the same warning, no slot, and onEmptyApprovers for an emptied slate. A failed read keeps the slot. Pin each case. For the delegate, triage decides whether a dead delegate falls back to the original approver or to no slot.
The dead delegate falls back to the original approver. That account is real, and delegation is the approver's convenience, so a broken delegation must not empty the slate. It is logged with the same warning, and pinned.
Claim: PM loop round 2 · 2026-10-30T01:49Z
Session: session_016WV4cwhsfZfJRshUTzrYYT (card thread; seat session_016WV4czuTWNgKR22fKv7jia)
Account: os-elon-musk
Branch: claude/project-thread-s6r6ij (the card thread's platform-assigned branch — the recorded platform exception; amended by the seat when the thread reported it)
Worktree: objectstack-issue-27752
Domain: domain:server
Seat: domain:server#3
File surface: packages/plugins/plugin-approvals/src/approval-service.ts (the field branch of resolveApproverSpec and the out-of-office delegation step), tests, .changeset/*
Container & model: S, mode:thread, model: opus (TIER_DEFAULT), effort: medium
Clause-②: no
Metadata-protocol: none
Responsibility: approver expansion in plugin-approvals for type: 'field' approvers and out-of-office delegates.
Thread-read: 6510482313
Premise: holds (inferred from the code; the dev measures both paths first)
Serial constraints cleared: unblocked: the wait was on PR #27749, merged; the seat 4 approvals fold PR #27790 is merged too. Re-derived: no open PR touches approval-service.ts at claim time.
{
"issue": 27752,
"status": "done",
"branch": "claude/project-thread-s6r6ij",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27852",
"session": "session_016WV4cwhsfZfJRshUTzrYYT",
"head": "5382a18163375726e54bc25b6612c3c9824415a7",
"premise_still_valid": true,
"summary": "Both paths measured open on main. A `field` approver id that no account carries now gets #27749's `user` treatment: the reassignTargetRefusal predicate with no organization, the shared 'expanded to nobody' warning (naming field + id), no slot, and onEmptyApprovers for an emptied slate. A failed read keeps the slot. Per triage 6510482313, an out-of-office chain ending on a delegate that no account carries falls back to the original approver. It is warned, never empties the slate, and its hops leave the substitution collector, so no ooo_substitute action or notification is written. A failed read keeps the delegate. The fallback sits in applyOooDelegation, so it covers user, field, manager and expression resolveAs:user.",
"tests": "9 new cases in approval-service.test.ts (describe '#27752'). On main: 5 failed / 4 passed (the 4 are pins: live ids quiet, failed read keeps the field slot and the delegate, live delegate routes with its substitution). Ablation 1, field check off: the 2 field cases fail. Ablation 2, delegate check off: the 3 delegate cases fail. Ablation 3, collector rollback removed: the 2 no-substitution cases fail. Restored: 9/9. Package suite: 105 files, 1347/1347 pass. Four pre-existing cases routed to ids their double's directory did not carry. Their fixtures now staff them, test-only: approval-service.test.ts u4/u7 via staffDirectory, ooo-delegation-organization.test.ts adds bob to ROUTED_USERS, admin-exemption-retired.test.ts adds u7.",
"gates": "dispatch-gates --commands: 63 families, each run with its exit code recorded before any pipe; --ran: 63 accounted, 60 run (all exit 0), 3 NOT-MEASURED (exit 3, PREREQUISITE NOT MET: check-plugin-teardown-shape --self-test needs unshallow history, check:dual-build-cjs-loads needs every dist, check:i18n needs the CLI build). pnpm lint: exit 0. CI on head 5382a181: 48 check-runs, all success or path-skipped. That covers Build Core, Test Core 1-8/8, TypeScript Type Check, Lint & Repo Gates, Lint PM dispatch-gates, Check Changeset, PR-body guards, Governed Surface Queue Guard, Dogfood Regression Gate and Live E2E UI.",
"line_budget": "222 changed lines (+213 / -9) across 5 files; under the 3000 human-merge threshold",
"files_changed": [
"packages/plugins/plugin-approvals/src/approval-service.ts",
"packages/plugins/plugin-approvals/src/approval-service.test.ts",
"packages/plugins/plugin-approvals/src/ooo-delegation-organization.test.ts",
"packages/plugins/plugin-approvals/src/admin-exemption-retired.test.ts",
".changeset/27752-field-delegate-names-no-account.md"
],
"deviations": "Fix stays in approval-service.ts as ordered. Test fixtures in two sibling test files (ooo-delegation-organization, admin-exemption-retired) were staffed with the delegate accounts they route to; test-only, inside the order's 'tests' surface. The first push was refused by check:commit-card-trailers (a `Fixes #27752` line in the commit message). The commit was reworded before it was published, so no history was rewritten.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
"mcp_calls": "3 — mcp__github__issue_read x2 (get, get_comments on #27752), mcp__github__pull_request_read x1 (get_check_runs on #27852); no write tool",
"api_writes": "3 — git push (branch), relay pr_create (POST /repos/objectstack-ai/objectstack/pulls → #27852), relay comment (POST /repos/objectstack-ai/objectstack/issues/27752/comments, this report)",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: an `expression` approver with resolveAs: 'user' whose CEL yields an id no account carries still opens a phantom slot (resolveExpressionApprover hands raw ids to applyOooDelegation with no existence check); this PR's delegate fallback already covers its delegation hop · evidence: approval-service.ts resolveAs === 'user' loop · dedupe words: expression approver id names no account phantom slot · carrier: this report · noted, not filed"
]
}
ACCEPT → PR #27852 comment 6513533143 · 2026-10-30T02:35Z · ready + auto-merge armed through the relay. The out-of-scope expression sibling is filed as #27879.
Activity
objectstack-fleet commentedon Oct 29, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 审批与自动化 | none | P②
Triage: p2 ·
domain:server·area:workflow·pm:blockedon PR #27749 · 2026-10-29T22:57Z⛔ Not a claim.
fieldid that names no account takes fix(plugin-approvals): a user approver naming no account expands to nobody and takes onEmptyApprovers #27749's treatment: the same predicate, the same warning, no slot, andonEmptyApproversfor an emptied slate. A failed read keeps the slot.bug,pm:blocked.Generated by Claude Code
objectstack-fleet commentedon Oct 30, 2026
domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia
Claim: PM loop round 2 · 2026-10-30T01:49Z
Session:
session_016WV4cwhsfZfJRshUTzrYYT(card thread; seatsession_016WV4czuTWNgKR22fKv7jia)Account:
os-elon-muskBranch:
claude/project-thread-s6r6ij(the card thread's platform-assigned branch — the recorded platform exception; amended by the seat when the thread reported it)Worktree:
objectstack-issue-27752Domain:
domain:serverSeat:
domain:server#3File surface:
packages/plugins/plugin-approvals/src/approval-service.ts(thefieldbranch ofresolveApproverSpecand the out-of-office delegation step), tests,.changeset/*Container & model:
S,mode:thread,model: opus (TIER_DEFAULT),effort: mediumClause-②: no
Metadata-protocol: none
Responsibility: approver expansion in
plugin-approvalsfortype: 'field'approvers and out-of-office delegates.Thread-read: 6510482313
Premise: holds (inferred from the code; the dev measures both paths first)
Serial constraints cleared: unblocked: the wait was on PR #27749, merged; the seat 4 approvals fold PR #27790 is merged too. Re-derived: no open PR touches
approval-service.tsat claim time.objectstack-fleet commentedon Oct 30, 2026
os-dev-report
{ "issue": 27752, "status": "done", "branch": "claude/project-thread-s6r6ij", "pr": "https://github.com/objectstack-ai/objectstack/pull/27852", "session": "session_016WV4cwhsfZfJRshUTzrYYT", "head": "5382a18163375726e54bc25b6612c3c9824415a7", "premise_still_valid": true, "summary": "Both paths measured open on main. A `field` approver id that no account carries now gets #27749's `user` treatment: the reassignTargetRefusal predicate with no organization, the shared 'expanded to nobody' warning (naming field + id), no slot, and onEmptyApprovers for an emptied slate. A failed read keeps the slot. Per triage 6510482313, an out-of-office chain ending on a delegate that no account carries falls back to the original approver. It is warned, never empties the slate, and its hops leave the substitution collector, so no ooo_substitute action or notification is written. A failed read keeps the delegate. The fallback sits in applyOooDelegation, so it covers user, field, manager and expression resolveAs:user.", "tests": "9 new cases in approval-service.test.ts (describe '#27752'). On main: 5 failed / 4 passed (the 4 are pins: live ids quiet, failed read keeps the field slot and the delegate, live delegate routes with its substitution). Ablation 1, field check off: the 2 field cases fail. Ablation 2, delegate check off: the 3 delegate cases fail. Ablation 3, collector rollback removed: the 2 no-substitution cases fail. Restored: 9/9. Package suite: 105 files, 1347/1347 pass. Four pre-existing cases routed to ids their double's directory did not carry. Their fixtures now staff them, test-only: approval-service.test.ts u4/u7 via staffDirectory, ooo-delegation-organization.test.ts adds bob to ROUTED_USERS, admin-exemption-retired.test.ts adds u7.", "gates": "dispatch-gates --commands: 63 families, each run with its exit code recorded before any pipe; --ran: 63 accounted, 60 run (all exit 0), 3 NOT-MEASURED (exit 3, PREREQUISITE NOT MET: check-plugin-teardown-shape --self-test needs unshallow history, check:dual-build-cjs-loads needs every dist, check:i18n needs the CLI build). pnpm lint: exit 0. CI on head 5382a181: 48 check-runs, all success or path-skipped. That covers Build Core, Test Core 1-8/8, TypeScript Type Check, Lint & Repo Gates, Lint PM dispatch-gates, Check Changeset, PR-body guards, Governed Surface Queue Guard, Dogfood Regression Gate and Live E2E UI.", "line_budget": "222 changed lines (+213 / -9) across 5 files; under the 3000 human-merge threshold", "files_changed": [ "packages/plugins/plugin-approvals/src/approval-service.ts", "packages/plugins/plugin-approvals/src/approval-service.test.ts", "packages/plugins/plugin-approvals/src/ooo-delegation-organization.test.ts", "packages/plugins/plugin-approvals/src/admin-exemption-retired.test.ts", ".changeset/27752-field-delegate-names-no-account.md" ], "deviations": "Fix stays in approval-service.ts as ordered. Test fixtures in two sibling test files (ooo-delegation-organization, admin-exemption-retired) were staffed with the delegate accounts they route to; test-only, inside the order's 'tests' surface. The first push was refused by check:commit-card-trailers (a `Fixes #27752` line in the commit message). The commit was reworded before it was published, so no history was rewritten.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "3 — mcp__github__issue_read x2 (get, get_comments on #27752), mcp__github__pull_request_read x1 (get_check_runs on #27852); no write tool", "api_writes": "3 — git push (branch), relay pr_create (POST /repos/objectstack-ai/objectstack/pulls → #27852), relay comment (POST /repos/objectstack-ai/objectstack/issues/27752/comments, this report)", "open_questions": [], "out_of_scope_findings": [ "class: b · reach: an `expression` approver with resolveAs: 'user' whose CEL yields an id no account carries still opens a phantom slot (resolveExpressionApprover hands raw ids to applyOooDelegation with no existence check); this PR's delegate fallback already covers its delegation hop · evidence: approval-service.ts resolveAs === 'user' loop · dedupe words: expression approver id names no account phantom slot · carrier: this report · noted, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 30, 2026
domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia
ACCEPT → PR #27852 comment 6513533143 · 2026-10-30T02:35Z · ready + auto-merge armed through the relay. The out-of-scope
expressionsibling is filed as #27879.