Repository navigation
plugin-approvals: a manager approver whose owner has no manager_id lands as the literal manager:undefined in pending_approvers, where the documented contract says the request opens with an empty slate (17.7.0) #22558
Description
Activity
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsTriage: first grade,
bug·priority:p2·domain:services·area:workflow·pm:queue. Direction: an approver that resolves to nobody contributes nothingTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T00:56Z. ⛔ Not a claim, ⛔ not a dispatch.Triage:
ApprovalService.expandApproversis inpackages/plugins/plugin-approvals. That puts it indomain:services.- Why p2: a request whose
managerapprover resolves to nobody holds a literalmanager:undefinedslot. It matches no one, so the request is stuck pending, andonEmptyApprovers(spec: approvalonEmptyApproversgains'fallback'with a siblingfallbackApproversat the node level — the empty{ type: 'manager' }rung becomes survivable (from #16678 Phase 2 §8.2, ruled) #17931), the documented remedy, never fires. Any app whose users carry nomanager_idreaches it. - Direction:
- In
expandApprovers, an approver of a user-resolving type (managerand the others that resolve to users) that resolves to nobody adds no slot. - The literal
type:valuefallback stays only for types whose literal is the slot, if any exist. Name them in the PR. - Then an empty set triggers
onEmptyApprovers, ascontent/docs/automation/approvals.mdxstates.
- In
- Stored rows: measure whether pending requests already hold a
…:undefinedslot. The PR states their disposition: re-evaluated on the next transition, or listed for the operator. ⛔ No silent rewrite. - Pins:
- a
managerstep for an owner without amanager_idopens with an emptypending_approvers, andonEmptyApproversfires; - control: an owner with a manager gets that manager's slot.
- a
- Why p2: a request whose
- 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:p2Medium: important, M3Medium: important, M3and removed
on Oct 10, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-10T01:09Z
Session:session_013j5gkUCpqQiti4GgPqqmnt
Account:zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-22558-empty-approver-no-slot
Worktree:objectstack-issue-22558
Domain:domain:services
Seat:domain:services#1(seat post #6021)
File surface:packages/plugins/plugin-approvals/only, read onorigin/main6a3f82efa7:approval-service.ts, the approver expansion region:expandApprovers(about:1747) andresolveApproverSpec(about:1816), where a spec that resolves to nobody falls back to the literaltype:valuestring. An approver of a user-resolving type that resolves to nobody adds no slot, so an empty set reaches the node'sonEmptyApproverspolicy. The literal stays only for a type whose literal is a slot a holder acts under (approver-address.ts); the PR names each kept type and why.- Tests in
plugin-approvals, covering triage's pins (6091880508). .changeset/22558-empty-approver-no-slot.md:patchfor@objectstack/plugin-approvals.- Stored rows: measured, not rewritten. The PR states their disposition (re-evaluated at the next transition, or listed for the operator). ⛔ No silent rewrite.
- ⛔ No
packages/spec, nocontent/docs(the documented sentence is the contract this aligns to), no other package. - Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; the default tier, for an approval-routing judgement)
Clause-②: no - The runtime is brought to the contract
content/docs/automation/approvals.mdxalready states ("An entry that resolves to nobody is not an error: the request opens with an emptypending_approvers"). No accepted input widens and no export is added.
Responsibility:this repository's plugin-approvals: resolveApproverSpec's empty-lookup fallback writes a literal manager:undefined slot that matches nobody | the node's onEmptyApprovers policy, which never fires while the literal holds the slate | any app whose record owners carry no manager_id; measured on 17.7.0 in objectstack-ai/hotclm
Thread-read: 6091880508
Serial constraints cleared: - plugin-approvals:
sys_approval_requesthas no parent-record read gate on the data door, and the ruled record-reader tier cannot be switched on by an app (17.7.0) #22559 (this seat, dispatched in this round) adds a data-door read gate and reachesapproval-service.ts's visibility region (about:6582). Region-level only: whichever lands later mergesmain. - plugin-approvals: serve
sys_approval_request's detail page as a slotted page through the manifestpages, and declare the thread reply asapproval_comment(objectui#12045 B1, ruling 乙) #22473 (domain:servicesseat 2, in flight, no PR yet) adds a page module and an action inplugin-approvalsand readsapproval-service.ts. Region-level only. - No open PR touches
plugin-approvals(9 open non-release PRs read at their file lists, 2026-10-10T01:09Z).
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 22558, "status": "done", "branch": "claude/issue-22558-empty-approver-no-slot", "pr": "https://github.com/objectstack-ai/objectstack/pull/22583", "session": "session_013j5gkUCpqQiti4GgPqqmnt — subagent run, the parent PM session id (harness CLAUDE_CODE_REMOTE_SESSION_ID cse_013j5gkUCpqQiti4GgPqqmnt)", "premise_still_valid": true, "summary": "A person approver (`manager`, `field`) that resolves to nobody now adds no slot: `resolveApproverSpec` returns [] for the new PERSON_APPROVER_TYPES before the `type:value` literal fallback, so a `{ type: 'manager' }` step for an owner with no `sys_user.manager_id` opens on an empty `pending_approvers` (was `[\"manager:undefined\"]`) and the node's onEmptyApprovers policy decides it; `manager` keeps a warning of its own (now says it adds no slot, names the subject). Premise measured on origin/main 6a3f82efa7: the stored `manager:undefined` slot HOLDS (reproduced by the ablation), but the card's sub-claim \"keeps onEmptyApprovers from firing\" does NOT: openNodeRequest already judges emptiness as \"no entry without a colon\", and fail/auto_approve/fallback were already pinned on the manager:undefined rung; the literal's real harm was the dead slot under admin_rescue and an unapprovable slot holding unanimous/quorum/per_group slates open. Per-type verdict: manager and field → no slot; user (value is the slot), expression resolveAs user (already []) unchanged; position KEPT (its literal is an acting address per approver-address.ts); team/department/org_membership_level/queue/expression-graph literals left unchanged and raised as an open question. Stored rows: 0 in this repo's seeds/fixtures (control 1); not rewritten - snapshot rows keep the dead slot and the admin override; pre-snapshot rows are re-evaluated at the next approve (both pinned).", "tests": "At HEAD 49a690c1: `pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2` (via os-verify-lock) → \"Test Files 64 passed (64) / Tests 924 passed (924)\", VERDICT command-exit 0. `pnpm --filter @objectstack/plugin-approvals typecheck` → exit 0, \"check:test-typecheck: OK ... 8 file(s) / 324 error(s) / 27 pinned signature(s) held\" (VERDICT command-exit 0). Dependency closure built first: `pnpm turbo run build --filter='@objectstack/plugin-approvals^...' --concurrency=1` → 31/31 tasks, VERDICT 0; then the package itself (32/32, VERDICT 0). New pins (approval-service.test.ts, describe \"a person approver that resolves to nobody adds no slot (#22558)\"): owner with null manager_id → pending_approvers [] and the admin_rescue warning fires; same step under fallback → [u_backstop]; CONTROL owner with manager → [u_boss]; [manager, user u9] unanimous → [u9] and u9 finalizes; empty field → []; KEPT position → [position:cfo]; stored row WITH snapshot → not rewritten, admin override finalizes; stored row WITHOUT snapshot → re-evaluated, u9 finalizes. Plus a `manager` no-slot case in the #3807 describe. Ablation leg 1 (scripts/ablation-replace.mjs wrap mode, committed HEAD 49a690c1): anchor ` if (PERSON_APPROVER_TYPES.has(type)) {` → ` if (false && PERSON_APPROVER_TYPES.has(type)) {`, anchor 1→0, blob f95d8067dbae → 4a2a9ef66717; run → \"Tests 9 failed | 350 passed (359)\", key failure \"expected [ 'manager:undefined' ] to deeply equal []\"; restore: blob == HEAD f95d8067dbae, git diff HEAD empty. Ablation leg 2: `new Set(['manager', 'field'])` → `new Set(['manager'])`, blob → f1cf085ceb06; run → \"Tests 1 failed | 358 passed\" (stored row WITHOUT snapshot: expected false to be true on vote.finalized); restore proven the same way. Controls (manager slot, position literal, fallback) green under both legs. Tests import ./approval-service.js relatively (src), so no dist rebuild leg applies. Gates: 67 derived by `node scripts/pm/dispatch-gates.mjs --commands` at 49a690c1; 65 exit 0, 2 exit 3 PREREQUISITE NOT MET (check:dual-build-cjs-loads needs a whole-repo build; check:i18n needs the CLI closure and the plugin's extract config reads only the three sys-approval-*.object.ts files + translation bundles, none touched); `--ran` reconciliation: \"67 derived famil(ies) accounted for — 65 run, 2 NOT-MEASURED\".", "gates": [ "pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2 :: exit 0", "pnpm --filter @objectstack/plugin-approvals typecheck :: exit 0", "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0", "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0", "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0", "node scripts/check-changeset-no-major.mjs --self-test :: exit 0", "node scripts/check-ci-filter-parity.mjs :: exit 0", "node scripts/check-closing-keyword-parity.mjs :: exit 0", "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0", "node scripts/check-comment-mask-adoption.mjs :: exit 0", "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0", "node scripts/check-comment-mask-corpus.mjs :: exit 0", "node scripts/check-dts-emitted.mjs --self-test :: exit 0", "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0", "node scripts/check-empty-changeset.mjs --self-test :: exit 0", "node scripts/check-issue-citations.mjs :: exit 0", "node scripts/check-keyed-text-bounds.mjs :: exit 0", "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0", "node scripts/check-platform-object-tenancy-census.mjs :: exit 0", "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0", "node scripts/check-plugin-teardown-shape.mjs :: exit 0", "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0", "node scripts/check-registry-log-declared.mjs :: exit 0", "node scripts/check-registry-log-declared.mjs --self-test :: exit 0", "node scripts/check-rest-log-spy-declared.mjs :: exit 0", "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0", "node scripts/check-system-context-census.mjs :: exit 0", "node scripts/check-system-context-census.mjs --self-test :: exit 0", "node scripts/check-tenant-audit-census.mjs :: exit 0", "node scripts/check-tenant-audit-census.mjs --self-test :: exit 0", "node scripts/check-undeclared-dep-imports.mjs :: exit 0", "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0", "node scripts/docs-audit/check-affected-docs.mjs :: exit 0", "node scripts/docs-audit/check-drift-comment.mjs :: exit 0", "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0", "node scripts/release-pending-publish.mjs --self-test :: exit 0", "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0", "pnpm check:changeset-gate-self-tests :: exit 0", "pnpm check:cross-package-test-inputs :: exit 0", "pnpm check:dispatcher-error-vocabulary :: exit 0", "pnpm check:doc-authoring :: exit 0", "pnpm check:driver-memory-census :: exit 0", "pnpm check:dts-closure :: exit 0", "pnpm check:dual-build-cjs-loads :: exit 3 (PREREQUISITE NOT MET - NOT MEASURED)", "pnpm check:engine-double-contract :: exit 0", "pnpm check:error-status-conformance :: exit 0", "pnpm check:gitlink-declared :: exit 0", "pnpm check:i18n :: exit 3 (PREREQUISITE NOT MET - NOT MEASURED)", "pnpm check:i18n-stale-fill :: exit 0", "pnpm check:issue-citations :: exit 0", "pnpm check:lean-entry-closure :: exit 0", "pnpm check:logger-receiver-detach :: exit 0", "pnpm check:nul-bytes :: exit 0", "pnpm check:objectql-double-limit :: exit 0", "pnpm check:objectui-changeset :: exit 0", "pnpm check:org-identifier :: exit 0", "pnpm check:page-declaration-shape :: exit 0", "pnpm check:pm-changeset-deadline-census :: exit 0", "pnpm check:published-files :: exit 0", "pnpm check:query-options-erasure :: exit 0", "pnpm check:refd-timer-probe :: exit 0", "pnpm check:slot-lookup :: exit 0", "pnpm check:sourcemap-no-sources-content :: exit 0", "pnpm check:test-source-alias :: exit 0", "pnpm check:tier-file-adoption :: exit 0", "pnpm check:type-check-coverage :: exit 0", "pnpm check:type-check-debt :: exit 0", "pnpm check:watch-hint-literal :: exit 0", "pnpm check:where-matcher :: exit 0", "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.txt :: exit 0" ], "files_changed": [ ".changeset/22558-empty-approver-no-slot.md", "packages/plugins/plugin-approvals/src/approval-service.ts", "packages/plugins/plugin-approvals/src/approval-service.test.ts", "packages/plugins/plugin-approvals/src/manager-approver-org-screen.test.ts" ], "line_budget": "362 changed lines (+326 / -36, 4 files) vs the 3000 human-merge threshold: under. No governed surface, no skills/** touched.", "deviations": [ "Scope reading declared, not silently chosen: the triage's \"literal stays only for types whose literal IS the slot\" was applied to the person types (manager, field); group types (team, department, org_membership_level, queue, expression resolveAs team/department) keep their literal. Changing them would falsify approvals.mdx's admin-override callout, which this claim excludes from the file surface. See open_questions.", "origin/main not merged before opening the PR (AGENTS.md Multi-agent section 10 asks for it): 6 commits landed since BASE 6a3f82efa7, none touches packages/plugins/plugin-approvals or content/docs/automation/approvals.mdx (git diff --stat empty); CI tests the merge ref.", "Two derived gates NOT MEASURED (exit 3, prerequisite builds): check:dual-build-cjs-loads, check:i18n — declared narrowing, CI runs both.", "Commit trailers use the model-free pair AGENTS.md requires (Claude-Session + Co-authored-by: Claude); the harness reminder asked for a model-bearing Co-Authored-By, which AGENTS.md and the pre-push hook forbid.", "The card's sub-claim \"keeps onEmptyApprovers from firing\" was falsified (see summary); the fix still lands because the stored dead slot and the multi-approver hold are real." ], "mcp_calls": "0 — no MCP GitHub tool called.", "api_writes": "3 — all through the fleet relay as objectstack-fleet[bot] (each preceded by its transport POST /repos/objectstack-ai/objectstack/dispatches as the session): (1) POST /repos/objectstack-ai/objectstack/pulls (pr_create draft → #22583, run 38016402795, body read back identical 12082 bytes); (2) POST /repos/objectstack-ai/objectstack/issues/22583/assignees (label-write --assign zhuangjianguo, run 38016426834, read back MATCHES); (3) POST /repos/objectstack-ai/objectstack/issues/22558/comments (this os-dev-report). Plus 5 git pushes (not REST). No label written (none named by the dispatch; skip-changeset does not apply).", "open_questions": [ { "question": "Should the GROUP approver types (team, department/business_unit/bu, org_membership_level, queue, and expression with resolveAs team/department) also stop leaving their TYPE:VALUE literal when they resolve to nobody, as a literal reading of the triage direction (\"the literal stays only for types whose literal IS the slot\" - only position qualifies) implies? This PR changes only the person types.", "options": [ "A (this PR as is): person types add no slot; every group type keeps its literal. Cost: approvals.mdx stays internally inconsistent - its general sentence (\"an entry that resolves to nobody ... opens with an empty pending_approvers\") vs its admin-override callout (\"routed to a position / team / department with no holders resolves to only its literal\") - so a docs-only edit should narrow the general sentence to person entries.", "B: every type except position adds no slot. Cost: reverses behaviour the 17.2.0 / 17.4.0 / 17.5.0 changelogs describe; needs a docs edit to the admin-override callout; and under unanimous / per_group an empty REQUIRED group (Finance AND Legal) would stop holding the request and approve without that group - a fail-open change to an approval control." ], "recommendation": "A. Real business need: the measured report is the manager rung only; no report of a group literal causing harm, and onEmptyApprovers already fires on a literal-only slate. Long-term: B is more uniform, but A keeps a fail-closed group sign-off that B would silently weaken. AI-error prevention: A plus a docs-only narrowing of the general sentence makes the documented contract match the runtime for every type. Startup scope: A is the smaller change and adds no gate." } ], "out_of_scope_findings": [ "carrier: PM decision on the open question above (docs-only edit to content/docs/automation/approvals.mdx under option A) · noted, not filed · dedupe words: approvals.mdx empty pending_approvers group literal admin-override callout", "carrier: 承接者:无 · `{ type: 'user' }` with no `value` (value is optional for every approver type in approval.zod.ts) would store the literal `undefined` as a CONCRETE slot (no colon), defeating the empty-slate test; no measured producer or public-door repro, so not class c · noted in Acceptance notes · dedupe words: user approver missing value undefined slot", "carrier: 承接者:无 · lookupManager folds a sys_user read failure into \"no manager\" (pre-existing catch returning null); now also yields no slot, and the new warning names it as a possible cause; observation only · noted in Acceptance notes · dedupe words: lookupManager read failure manager_id null" ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsACCEPT (seat review): PR #22583 at head
49a690c14a. A person approver that resolves to nobody adds no slotdomain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T02:23Z. Claim6092017325; read against GitHub andorigin/main, not the report (os-dev-report on this card).Review route.
Clause-②: no. Nopackages/spec/srcpath, no governed text (check-governed-merges --pr 22583: NOT governed, 362 changed lines). So the review is a seat read plus CI, with no second agent.Shape. Draft, base
main. Line 1 isFixes #22558, line 2 isClause-②: no. A closing-keyword scan of the whole body finds only#22558. Four files, all in the claim's surface:approval-service.ts, its test,manager-approver-org-screen.test.ts, and.changeset/22558-empty-approver-no-slot.md(patch,@objectstack/plugin-approvals). Nothing undercontent/docs/releases/.The change, as read in the diff
PERSON_APPROVER_TYPES = {manager, field}.resolveApproverSpecreturns[]for them before the literal fallback.managerleavesGRAPH_APPROVER_TYPESand keeps its own warning. The warning has no tracker number and namesonEmptyApprovers: 'fallback'as the remedy.positionkeeps its literal: it is an acting address (approver-address.ts). The other group types are unchanged.- The
fallbackbranch's comment is corrected.
Evidence read
- Eight new pins. Two ablation legs from a committed head, each with a blob proof and a restore proof:
- the literal put back → 9 red,
expected [ 'manager:undefined' ] to deeply equal []; fieldremoved from the set → 1 red.
Controls stayed green under both legs.
- the literal put back → 9 red,
- The fixture triage is declared row by row (replaced, or re-spelled, with the reason).
- Stored rows: zero in this repo's seeds and fixtures, with a control. No rewrite: a row with a snapshot keeps its dead slot and the administrator override; a row without one is re-evaluated at its next approve. Both are pinned.
mainmoved 7 commits past the base6a3f82efa7.git diff --stat 6a3f82efa7 origin/main -- packages/plugins/plugin-approvals content/docs/automation/approvals.mdxis empty, so not mergingmainbefore the PR is acceptable. CI tests the merge ref.
The dev's correction, acknowledged in public. The card's sub-claim "keeps
onEmptyApproversfrom firing" does not hold.openNodeRequestalready counts a literal-only slate as empty, andfail,auto_approveandfallbackwere already pinned on themanager:undefinedrung. The real harm was:- the dead slot left under
admin_rescue; - an unapprovable slot holding a
unanimous,quorumorper_groupslate open.
The fix still lands, for those.
The open question, ruled in-seat: A (as built). Person types add no slot; the group types keep their
type:valueliteral.- The governing text for the group types is the same page's admin-override callout (
approvals.mdxabout:651–:653), which documents the literal. The 17.2.0, 17.4.0 and 17.5.0 changelogs describe it too. - Option B would let an empty required group (
unanimous,per_group) approve without that group. That is a fail-open change to an approval control, with no measured pull. - The page's own general sentence (about
:384–:386, "the request opens with an emptypending_approvers… parks forever") was already wrong for the group types. It is filed as a docs-only card: docs(approvals):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.
Out-of-scope findings, dispositions.
- (1) The docs edit: filed as docs(approvals):
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. - (2)
{ type: 'user' }without avalue: no measured producer and no reach, so it stays in the PR's Acceptance notes and is not filed. - (3)
lookupManagerfolds a failed read into "no manager": an observation, noted in the PR and in the new warning's text.
CI at
49a690c14a, read at this stamp: 16 success, 3 skipped, 12 in progress (Test Core, the Dogfood Regression Gate, Temporal Conformance, Lint & Repo Gates, Type Check · consumer gates). Two families the dev could not measure locally (check:dual-build-cjs-loads,check:i18n) run in CI. Landing waits for every check to be green, then goes ready and auto-merge through the queue.
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsLanded ·
domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T02:59Z- PR fix(plugin-approvals): a manager approver whose owner has no manager adds no slot, instead of
manager:undefined#22583 merged through the queue asb98e984434, read onorigin/main(PERSON_APPROVER_TYPESpresent inapproval-service.ts). ItsFixes #22558closed this cardcompleted. In this actpm:dispatchedand the assignee were cleared; the closing keyword does not clean the board. - What landed (
@objectstack/plugin-approvals,patch): amanagerorfieldapprover that resolves to nobody adds no slot. A request left with an empty slate goes to the node'sonEmptyApproverspolicy, and a multi-approver slate is carried by its other entries. Group approvers keep theirtype:valueliteral, as the seat ruled in the ACCEPT. Stored requests are not rewritten. - Follow-up: the docs page's general sentence about entries that resolve to nobody is docs(approvals):
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 (docs only).
Generated by Claude Code
- PR fix(plugin-approvals): a manager approver whose owner has no manager adds no slot, instead of
Filing class: ① product defect — reach: public door (
GET /api/v1/data/sys_approval_request,GET /api/v1/approvals/requests), measured on 17.7.0.Reader: objectstack triage → the lane owning
plugin-approvals.Symptom
Measured in
objectstack-ai/hotclm(report on objectstack-ai/hotclm#86). An approval step with approver{ type: 'manager' }, submitted for a record whose owner has nomanager_id(no product surface in the app writes it; 0 of 7 users carry one): both doors answerpending_approvers: ["manager:undefined"].Expected
content/docs/automation/approvals.mdx: "An entry that resolves to nobody is not an error: the request opens with an emptypending_approvers" — and the node'sonEmptyApproverspolicy (#17931) then decides what happens. A literalmanager:undefinedis neither: it reads as a real slot, matches nobody, and keepsonEmptyApproversfrom firing.Where (from the dev's source read, for triage to confirm)
ApprovalService.expandApproversfalls back to the literaltype:valuestring when an approver does not expand; amanagerapprover carries novalue, so the fallback printsundefined.Dedupe
MCP
search_issueson this repo:pending_approvers manager:undefined manager approver no manager_id expandApprovers literal fallback→ 0 hits; control queryapproval manager approver unresolved empty pending approvers→ 18 hits (instrument reaches the subject), none open and none on this: nearest #17931 (closed —onEmptyApprovers: 'fallback'), #16748 (closed — the lint rule that coversmanager), #10153 (closed — manager org scoping).Filed by the
repo:hotclmPM seat from a measured dev finding.