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
approvals: an expression approver that yields the same user id twice opens two co_sign slots for one person — the slate is not deduped #28684
Carries item (c) of #28658; #28658 keeps items (a) and (b), which wait on a maintainer decision (the published contract says a decision output's type/multiple only shape the input widget). Part of #28658 · in-flight derived sub-card (execution-duties.md: in-scope derivative of a claimed card, seat-owned, priority inherited p2, dispatched directly).
What happens
Measured on #28658 at framework 6e11a929 (stock showcase, showcase_dynamic_approval): approving stage 1 with {"outputs":{"next_reviewers":["ADMIN_ID","ADMIN_ID"]}} makes stage 2's co_sign node open two approver slots for the same user. One person then has to sign twice, or a co-sign quorum counts one person as two signatures.
This needs no contract change: the expression approver already accepts "ids (string / CSV / string array)" (plugin-approvals/src/approval-service.ts, the expression branch, about L2715–2760), and nothing published says a slate may name one person twice.
Wanted
The approver slate built from an expression approver names each resolved user once (dedupe after resolution, including after out-of-office delegation, so two ids delegated to the same person also yield one slot). Order of first appearance is kept.
Check whether the static approver kinds and the graph resolveAs kinds already dedupe; if one of them has the same hole, fix it at the same seam and name it in the PR, otherwise say they are covered.
Tests
Stage 2 opened from ["A","A"] has one slot for A; ["A","B","A"] has two slots, A then B.
Two ids whose OOO delegation routes to the same delegate yield one slot.
Ablation: remove the dedupe, show red, restore.
Not here
Refusing an unknown id, a scalar for multiple: true, or duplicates at the decision itself: #28658 (a)/(b), pending the decision.
Dedupe: searched co_sign duplicate approver, expression approver dedupe, same user twice slot — no open card.
Claim: PM loop round 1 · card thread · 2026-11-01T13:27Z
Session: session_01FEkQ5oc56xChD6Wrc4cGSt (the seat); the card thread is session_01FEkQ5onyCFb1sK8Uvu76U7
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-v6mwdv (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#5
Ruling-ref: 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z); in-flight derived sub-card of #28658 (item (c)), seat-owned, priority inherited p2, dispatched directly; the same card thread continues, its #28658 claim (6558537451) is superseded for (c) by this one.
File surface: packages/plugins/plugin-approvals/src/approval-service.ts (approver slate resolution: the expression branch and, if they share the hole, the static/graph kinds at the same seam) and its tests; a @objectstack/plugin-approvals patch changeset. ⛔ No decision-output validation (#28658 (a)/(b), pending decision), ⛔ no packages/runtime/**, packages/spec/** or ui/**.
Container & model: S, mode:thread, model and effort as the thread was opened
Clause-②: no
Metadata-protocol: none
Responsibility: the code is @objectstack/plugin-approvals' approver slate resolution · no platform path dedupes it today · reached by any expression approver yielding a repeated id, used today by showcase_dynamic_approval
Thread-read: none
Premise: holds (read at origin/main2678600d: the expression branch pushes each resolved id into slots with no dedupe; measured on #28658 at 6e11a929)
Serial constraints cleared: #28657 (seat 1) is in packages/runtime/src/domains/actions.ts, not plugin-approvals. #28658 is out of flight (decision box), so nothing else edits approval-service.ts.
Resume-from: none — fresh from origin/main
State: created in pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28684.md in the project.
os-dev-report
{
"issue": 28684,
"status": "done",
"branch": "claude/project-thread-v6mwdv",
"pr": "#28705",
"session": "session_01FEkQ5onyCFb1sK8Uvu76U7",
"premise_still_valid": true,
"summary": "ApprovalService.expandApprovers now names each resolved person once per approver entry, after OOO delegation, in first-appearance order, and records each per_group tag once per person through one helper. Premise measured beyond the card: the tally already reads Object.keys(__approverGroups), so a duplicate slot does not make a person sign twice; the real effects were a doubled pending_approvers entry and, under per_group, a doubled group tag that let one approval count twice toward minApprovals (test 4, ablation 2: 'expected true to be false'). Ruling 2 check: expression user and graph resolveAs kinds and static field had the hole and are fixed at the same loop; static user/manager are single-valued; static graph kinds already return a Set. Across two approver entries the slate is unchanged (documented #24751 shape, display only), noted in Acceptance notes.",
"tests": "head 28e36c0 (code commit 09b3856; the main merge brought no plugin-approvals change). New describe in plugin-approvals approval-service.test.ts, 7 tests: base blob → 'Tests 6 failed | 1 passed' (test 7 is a control, green on both), head → 'Tests 7 passed'. Ablation 1 (slot dedupe off, both branches, via ablation-replace, injected count 2 on disk) → 6 failed; ablation 2 (tag dedupe off, mutant count 1) → 3 failed incl. per_group 'expected true to be false'; each restored with git checkout HEAD, git diff HEAD empty, blob hash matched. The suite imports ./approval-service.js from source, so no build sits in the ablation path. pnpm --filter @objectstack/plugin-approvals test: Test Files 106 passed (106), Tests 1373 passed (1373); typecheck exit 0 (check:test-typecheck OK). Live probe on a stock showcase (full pnpm build, pnpm dev -- --fresh): #28658 reproduction with next_reviewers [ADMIN,ADMIN] → base (base blob + plugin-approvals rebuild, dist-preflight --absent) co_sign pending_approvers [ADMIN,ADMIN]; head [ADMIN]. Commands in PR body and pm-orders/server/28684-pr-body.md.",
"gates": "head 28e36c0: dispatch-gates --commands derived 63 families; all run with exit codes; --ran: '63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED (a DERIVED zero — all 63 recorded an exit code and none of them is 3)'. 62 exit 0 first pass; check-plugin-teardown-shape --self-test exit 3 (shallow clone) → after git fetch --depth=1 of 621a487 exit 0, 48 cases pass. Scoped lint (eslint packages/plugins/plugin-approvals --no-inline-config) exit 0. dist predates the final main merge (no plugin-approvals change in it). CI check-runs: in_progress at report time. Record: pm-orders/server/28684-gates-ran.txt.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "3 — git push (branch); relay pr_create → #28705 (run 41316121616, read-back identical, no assignees, no labels: the order names none and the PR carries a changeset); POST issues/28684/comments (this report). Earlier on #28658 in this thread: 1 comment (report 6558577952).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed — two approver entries naming one person still store that person once per entry in pending_approvers until the first vote (documented in remind's #24751 comment; the tally is already per person). Product call on display only; in PR Acceptance notes.",
"carrier: #28658 · decision-door refusal of unknown ids / scalar for multiple:true / duplicates stays with #28658 (a)/(b), pending the maintainer decision."
]
}
Activity
objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Claim: PM loop round 1 · card thread · 2026-11-01T13:27Z
Session:
session_01FEkQ5oc56xChD6Wrc4cGSt(the seat); the card thread issession_01FEkQ5onyCFb1sK8Uvu76U7Account:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-v6mwdv(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#5Ruling-ref: 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z); in-flight derived sub-card of #28658 (item (c)), seat-owned, priority inherited p2, dispatched directly; the same card thread continues, its #28658 claim (6558537451) is superseded for (c) by this one.
File surface:
packages/plugins/plugin-approvals/src/approval-service.ts(approver slate resolution: the expression branch and, if they share the hole, the static/graph kinds at the same seam) and its tests; a@objectstack/plugin-approvalspatch changeset. ⛔ No decision-output validation (#28658 (a)/(b), pending decision), ⛔ nopackages/runtime/**,packages/spec/**orui/**.Container & model:
S,mode:thread, model and effort as the thread was openedClause-②: no
Metadata-protocol: none
Responsibility: the code is
@objectstack/plugin-approvals' approver slate resolution · no platform path dedupes it today · reached by any expression approver yielding a repeated id, used today byshowcase_dynamic_approvalThread-read: none
Premise: holds (read at
origin/main2678600d: the expression branch pushes each resolved id intoslotswith no dedupe; measured on #28658 at6e11a929)Serial constraints cleared: #28657 (seat 1) is in
packages/runtime/src/domains/actions.ts, not plugin-approvals. #28658 is out of flight (decision box), so nothing else editsapproval-service.ts.Resume-from: none — fresh from
origin/mainState: created in
pm:dispatched, assigned toos-project-manager, recorded by this claim. Order:pm-orders/server/order-28684.mdin the project.objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5onyCFb1sK8Uvu76U7
os-dev-report
{
"issue": 28684,
"status": "done",
"branch": "claude/project-thread-v6mwdv",
"pr": "#28705",
"session": "session_01FEkQ5onyCFb1sK8Uvu76U7",
"premise_still_valid": true,
"summary": "ApprovalService.expandApprovers now names each resolved person once per approver entry, after OOO delegation, in first-appearance order, and records each per_group tag once per person through one helper. Premise measured beyond the card: the tally already reads Object.keys(__approverGroups), so a duplicate slot does not make a person sign twice; the real effects were a doubled pending_approvers entry and, under per_group, a doubled group tag that let one approval count twice toward minApprovals (test 4, ablation 2: 'expected true to be false'). Ruling 2 check: expression user and graph resolveAs kinds and static field had the hole and are fixed at the same loop; static user/manager are single-valued; static graph kinds already return a Set. Across two approver entries the slate is unchanged (documented #24751 shape, display only), noted in Acceptance notes.",
"tests": "head 28e36c0 (code commit 09b3856; the main merge brought no plugin-approvals change). New describe in plugin-approvals approval-service.test.ts, 7 tests: base blob → 'Tests 6 failed | 1 passed' (test 7 is a control, green on both), head → 'Tests 7 passed'. Ablation 1 (slot dedupe off, both branches, via ablation-replace, injected count 2 on disk) → 6 failed; ablation 2 (tag dedupe off, mutant count 1) → 3 failed incl. per_group 'expected true to be false'; each restored with git checkout HEAD, git diff HEAD empty, blob hash matched. The suite imports ./approval-service.js from source, so no build sits in the ablation path. pnpm --filter @objectstack/plugin-approvals test: Test Files 106 passed (106), Tests 1373 passed (1373); typecheck exit 0 (check:test-typecheck OK). Live probe on a stock showcase (full pnpm build, pnpm dev -- --fresh): #28658 reproduction with next_reviewers [ADMIN,ADMIN] → base (base blob + plugin-approvals rebuild, dist-preflight --absent) co_sign pending_approvers [ADMIN,ADMIN]; head [ADMIN]. Commands in PR body and pm-orders/server/28684-pr-body.md.",
"gates": "head 28e36c0: dispatch-gates --commands derived 63 families; all run with exit codes; --ran: '63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED (a DERIVED zero — all 63 recorded an exit code and none of them is 3)'. 62 exit 0 first pass; check-plugin-teardown-shape --self-test exit 3 (shallow clone) → after git fetch --depth=1 of 621a487 exit 0, 48 cases pass. Scoped lint (eslint packages/plugins/plugin-approvals --no-inline-config) exit 0. dist predates the final main merge (no plugin-approvals change in it). CI check-runs: in_progress at report time. Record: pm-orders/server/28684-gates-ran.txt.",
"dogfood_battery": "n/a — no
!in the PR title and no (narrowing) arm","mcp_calls": "0",
"api_writes": "3 — git push (branch); relay pr_create → #28705 (run 41316121616, read-back identical, no assignees, no labels: the order names none and the PR carries a changeset); POST issues/28684/comments (this report). Earlier on #28658 in this thread: 1 comment (report 6558577952).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed — two approver entries naming one person still store that person once per entry in pending_approvers until the first vote (documented in remind's #24751 comment; the tally is already per person). Product call on display only; in PR Acceptance notes.",
"carrier: #28658 · decision-door refusal of unknown ids / scalar for multiple:true / duplicates stays with #28658 (a)/(b), pending the maintainer decision."
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
ACCEPT → PR #28705 comment 6559412079 · head
28e36c06· 2026-11-01T14:40Z. Landing:pr_ready+automerge_enablethrough the relay (run 41317292246); the merge queue closes this card.objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Landed · 2026-11-01T14:58Z · PR #28705 merged through the merge queue as
6cbdef1e44; this card closed as completed by itsFixesline.6cbdef1e44pm:dispatchedremoved in this stroke,domain:*/area:*kept#28658 stays open in
needs-user-decision(items (a)/(b)), as intended.