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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
console: an admin-override decision renders as an ordinary Approve — no affordance distinction, no confirm, and the via_override marker is never surfaced in any UI #5178
Moved from objectstack-ai/objectstack#9464 by the triage seat (file-at-destination: the fix is console rendering/affordance work — the can_act/can_override flags and the via_override column already give the client everything it needs to distinguish an override, and no backend change is required to render the distinction). Original card by @baozhoutao (app project steedos-labs/os-project-titanwind-ehr#1225, private); full detail, versions, and stale-premise checks preserved on the source card.
TL;DR
An admin viewing a pending approval they hold no slot in (or have already voted on) sees the exact same filled Approve / Reject / Reassign buttons as a designated approver — no override styling, no confirmation, nothing. One unmarked click takes the objectstack-ai/objectstack#3424 privileged-override branch, which is authoritative: it finalizes the node even under per_group/unanimous/quorum, silently bypassing every remaining co-sign group. And although objectstack-ai/objectstack#4466 made the service record sys_approval_action.via_override, no UI surface anywhere reads that column — the timeline renders an override byte-for-byte like an ordinary approval.
Real-world consequence measured in the filing app project: a PM walked a 3-stage countersign flow, saw Approve still rendered after each co-signer had voted, clicked it "to see if it was real", and it decided the next stage for an approver who never acted — filed as "countersign is broken". The backend behavior is the documented design working correctly; the UI gives an admin no way to see that.
Stale-premise checks (done at filing, 2026-08-18)
objectui main @ 68d9e282c83a: via_override has zero hits in the repo; can_override is consumed only to show the buttons (apps/console/src/services/approvalsApi.ts, packages/app-shell/src/views/RecordDetailView.tsx, ApprovalsInboxPage.tsx), never to mark them. Same in shipped @objectstack/console@17.0.0.
objectstack main @ f63afb237c09: plugin-approvals still gates decision actions on record.viewer.can_act || record.viewer.can_override, and an override approve finalizes authoritatively.
An override-only viewer (can_act:false, can_override:true) sees a visually distinct action ("Override approve…" / warning variant), ideally behind a confirm naming the pending approvers being bypassed
Column written, never read by any UI; renders as an ordinary approval
Docs contract cited (objectstack content/docs/automation/approvals.mdx): the decision records "which door it came through" precisely because "an override and an ordinary approval were byte-for-byte identical" — that argument applies to the rendered timeline exactly as it did to the DB row.
Repro (standalone)
Any flow with an approval node; a request goes pending with a staffed slate that does not include the admin.
View the record (or approvals inbox drawer) as a platform/tenant admin → Approve/Reject/Reassign render exactly as for a designated approver.
Click Approve → 200, node finalizes (multi-group countersign: all remaining groups bypassed), via_override = true written.
Timeline shows a plain "Approved · admin", indistinguishable from a slot holder's approval.
Note: the client can distinguish the override case today without backend changes (can_act:false && can_override:true for the affordance; via_override on the action row for the timeline). If implementation finds the timeline data door does not expose via_override, file the backend half back to objectstack with Blocked-by: linkage rather than widening this card.
Claim: PM loop round 2
Session: session_01AMHbqfPiETJHA95r6WzZWS
Branch: claude/issue-5178-override-affordance
Worktree: objectui-issue-5178
Domain: repo:objectui (execution seat)
File surface (region-level, under the app-shell parallelism exemption): packages/app-shell/src/views/RecordDetailView.tsx, packages/app-shell/src/hooks/useRecordApprovals.ts, apps/console/src/pages/system/ApprovalsInboxPage.tsx, apps/console/src/services/approvalsApi.ts, plus tests. ⛔ OUT: packages/app-shell/src/console/** (held by #4522 this round) and packages/plugin-grid / packages/plugin-kanban (held by #5183).
Container & model: M–L, mode:subagent, model: opus — dispatch-gates.mjs --tier gives no path-derived mandate; my judgment: affordance design plus an audit-marker surface is judgment work, not mechanical.
Clause-②: no — client-side rendering and affordance only. No contract accept/reject change; the can_act / can_override flags and the via_override column already exist and are unchanged.
Serial constraints cleared: views/RecordDetailView.tsx is free — #5176 landed at 6ca910bdf. No open PR touches these paths. Sibling claims this round are region-disjoint (#4522 on console/AppContent.tsx, #5183 on plugin-grid/kanban).
Parallelism disciplines (app-shell exemption, all four binding): region-level surface declared above; merge main before opening the PR; merge again if a sibling lands first; conflicts go to the merge queue — ⛔ do not resolve a sibling's hunks.
Premise re-measured on origin/main @ a2a974779
via_override — genuinely zero hits repo-wide. Reverse-checked with can_override, which resolves in 5 files, so the zero is real and not a broken pattern.
can_override is consumed in apps/console/src/services/approvalsApi.ts, apps/console/src/pages/system/ApprovalsInboxPage.tsx, and packages/app-shell/src/hooks/useRecordApprovals.ts — i.e. to show the buttons, never to mark them. Matches the card.
Zone 1 — rulings
The client can already distinguish the override case without any backend change: can_act:false && can_override:true for the affordance, via_override on the action row for the timeline. That is the card's own scoping and the triage seat's reason for moving it here.
Both halves are in scope: (a) an override-only viewer sees a visually distinct action rather than an identical filled Approve, and (b) via_override rows are marked in the timeline instead of rendering byte-for-byte like an ordinary approval.
The timeline half is not a new product decision — objectstack#4466's own Expected already said the marker should be "surfaced in the timeline". This is the half that never landed, i.e. declared-but-not-enforced, which is why it is a bug rather than a feature.
Zone 2 — this is permission-adjacent; here is the line
I judged this not to be on the maintainer-only floor, and I want the reasoning on the card so it can be challenged: the change adds friction and visibility to a privileged path. It does not relax access control, does not touch the authentication flow, does not alter RLS or sharing semantics, and does not remove or weaken an audit record — it renders one that is already being written. Who may do what is unchanged by this diff.
Stop immediately and report needs_decision if implementation would require any of: changing who can act or override, altering what is recorded, or weakening any existing gate. Those are the maintainer's, not yours and not mine.
Zone 3 — PM route suggestion (measurement outranks it)
I would expect a warning-variant action labelled as an override, behind a confirm that names the pending approvers being bypassed — the card's own wording — and a timeline row that reads as an admin override. The confirm text matters more than it looks: the real-world incident behind this card is a PM who clicked Approve "to see if it was real" and finalised a stage for an approver who never acted. A confirm that does not say what is about to be bypassed would not have stopped that click.
Fork clause: if the timeline's data door does not actually expose via_override to the client, file that half back to objectstack-ai/objectstack with Blocked-by: linkage and do not widen this card — deliver the affordance half and report the split.
Gates
Derive from your actual diff. Expect at minimum Lint, Type Check, the four Test (shard N/4), changeset gates, Control Byte Scan. A clue, not a spec. Changeset required.
Any user-visible string must go through the i18n path this repo already uses — a hard-coded English confirm would be a fresh defect of the class #4314 is open about.
Premise-first
The card is a triage-moved user report with stale-premise checks already done at filing. If the flags or the timeline component have moved since, follow what you measure; premise_still_valid: false with evidence is a legitimate delivery.
Deliver
Draft PR against main, first line Fixes #5178 (or Part of #5178 if the fork clause splits it — say what remains). Push the branch early. Report as a JSON payload and as an issue comment whose first line is <!-- os-dev-report -->.
{
"issue": 5178,
"status": "done",
"branch": "claude/issue-5178-override-affordance",
"pr": "https://github.com/objectstack-ai/objectui/pull/5196",
"premise_still_valid": true,
"summary": "Both halves shipped; the fork clause did NOT trigger. Measured the data door end to end and it is open: plugin-approvals' rowFromAction projects via_override, the spec contract declares it ('Consumers render the distinction'), and GET /api/v1/approvals/requests/:id/actions returns those rows verbatim — so nothing was filed back to objectstack. Premise re-confirmed on origin/main @ a2a974779: via_override zero hits repo-wide, can_override read only to show buttons. (a) Affordance: a can_act:false && can_override:true viewer now gets a warning-styled action labelled as an override, and the dialog it opens names the pending approvers being bypassed. (b) Timeline: override rows carry an 'Admin override' chip plus a distinct dot in BOTH timelines — the record page's approvals panel and the Approval Center. Two design points are load-bearing and both diverge from the naive route. First, the warning rides the param dialog's own title/description rather than a chained confirmText: these actions collect params, so the param dialog IS the confirm, and a confirm in front of it reads as 'the action ran' (framework#7278, maintainer ruling 2026-08-10). Second, the notice travels as its own dispatch key rather than folded into description — a MEASURED hazard, not taste: actionDescription prefers an _actions.NAME.description bundle hit over the passed literal, and plugin-approvals ships exactly that entry for approval_reject in en/zh-CN/ja-JP/es-ES, so the naive implementation lets a translation bundle silently delete the safety copy (reproduced under ablation, see tests). Which actions get the treatment is read from each action's own declared visible gate (does it OR in can_override?), not a name list, so a future decision action still ships as metadata alone and approval_recall is never relabelled. Permission boundary respected: who may act or override is unchanged, the dispatched request is byte-identical (pinned), no audit record altered. File surface moved under measurement and is declared in the PR body: RecordDetailView.tsx needed NO edit (it mounts DeclaredActionsBar, the real choke point); added DeclaredActionsBar.tsx, RecordApprovalsPanel.tsx, useConsoleActionRuntime.tsx, ActionParamDialog.tsx (one class), new utils/approvalOverride.ts + barrels, and 5 keys in all ten i18n packs. Nothing in the OUT list (app-shell/src/console/**, plugin-grid, plugin-kanban) was touched and no open PR overlaps. One non-defect worth PM awareness rather than a card: actionDescription's bundle-over-literal preference is correct for static metadata and only becomes a trap for a runtime-composed description — which is now handled by the dedicated key.",
"tests": "All at HEAD 0a2b41ba0 (working tree byte-identical to that commit; gate union re-run on it). `pnpm exec vitest run packages/app-shell/src/views/ packages/app-shell/src/hooks/ packages/app-shell/src/utils/` -> 'Test Files 321 passed (321) / Tests 3323 passed | 1 skipped (3324)', exit 0. `pnpm exec vitest run apps/console/ packages/i18n/` -> 'Test Files 101 passed (101) / Tests 1478 passed (1478)', exit 0. type-check on app-shell+console+i18n exit 0 (note: this repo spells the script 'type-check', hyphenated — verified the script name echoed, not a zero-match filter). lint on the same three exit 0 (200 pre-existing warnings, 0 errors). Gates derived from the actual diff, not the dispatch clue (objectui has no scripts/pm/dispatch-gates.mjs — that is an objectstack script; derived instead from the repo's own check:* set): check:control-bytes, check:i18n-keys, check:i18n-drift, check:self-import, check:action-forward-parity, check:phantom-deps all exit 0, plus check-changeset-presence.mjs and check-changeset-no-major.mjs exit 0. check:i18n-keys reports the 5 added keys resolving with full interpolation parity; check:i18n-drift reports 0 en values changed (additions only). NEW COVERAGE: 4 suites, 41 tests — utils/approvalOverride.test.ts, views/__tests__/DeclaredActionsBar.overrideAffordance.test.tsx, hooks/__tests__/useConsoleActionRuntime.overrideNotice.test.tsx, views/RecordApprovalsPanel.viaOverrideMarker.test.tsx. ABLATION (two legs, both from the COMMITTED state and restored via `git checkout <branch> -- <path>`; NO rebuild needed and none was done, because all four suites import the module under test by RELATIVE SOURCE PATH — there is no dist/ or package-exports indirection for a stale artifact to hide behind, which is why the usual build-the-mutation-leg step does not apply here): Leg A reverted the three implementation files to origin/main -> 3 of 4 suites red, 11 tests red; the fourth (approvalOverride.test.ts) stayed green CORRECTLY since it pins the pure predicate module the ablation does not touch. Leg B is the sharper one — replaced the implementation with the NAIVE version (fold notice into description, let actionDescription resolve it) -> exactly 1 of 5 tests red, and it was the bundle-hazard test: \"AssertionError: expected 'Bitte bestatigen Sie die Ablehnung di...' to contain 'Qian Hua'\" — the bundle value replaced the entire warning, approver names included. That proves the test is a real detector rather than a tautology. Restore leg verified: 41/41 green and `git diff HEAD` empty. Direction observed matched the prediction (red) in both legs; no reversal or diagnostic-count effect.",
"open_questions": [],
"out_of_scope_findings": []
}
The fork clause did not fire, because you measured instead of assuming
I gave you an escape hatch if the timeline's data door did not expose via_override. You went and checked the whole path — plugin-approvals' rowFromAction projects it, the spec contract declares it ("Consumers render the distinction"), and GET /api/v1/approvals/requests/:id/actions returns those rows verbatim — and found the door open. Nothing filed back to objectstack, and both halves shipped. Measuring a fork rather than taking it is the better outcome; taking it on a guess would have split this card for no reason.
Two divergences from the naive route, both load-bearing, both measured
1. The warning rides the param dialog's own title/description, not a chained confirmText. These actions collect params, so the param dialog is the confirm — a confirm stacked in front of it reads as "the action already ran". You found the existing maintainer ruling for that (framework#7278, 2026-08-10) and applied it. I did not cite it in the dispatch; you went and found the governing decision rather than inventing an answer.
2. The notice travels as its own dispatch key rather than folded into description — and this is the sharp one.actionDescription prefers an _actions.NAME.description bundle hit over the literal that was passed in, and plugin-approvals ships exactly that entry for approval_reject in en / zh-CN / ja-JP / es-ES. So the obvious implementation — compose the warning into the description — lets a translation bundle silently delete the safety copy, including the approver names, in four locales.
You did not argue that; you reproduced it. Ablation Leg B replaced the implementation with the naive version and exactly one of five tests went red, and it was the bundle-hazard test:
AssertionError: expected 'Bitte bestatigen Sie die Ablehnung di...' to contain 'Qian Hua'
The bundle value had replaced the entire warning, bypassed-approver names and all. That is the same defect class as the card itself — a UI that looks like it is telling you something and is not — reproduced one layer down and closed before it shipped. It also proves the test is a real detector rather than a tautology, which is the whole reason Leg B exists.
The affordance is driven by declared metadata, not a name list
Which actions get the override treatment is read from each action's own declared visible gate (does it OR in can_override?) rather than a hard-coded list. So a future decision action ships as metadata alone and inherits this correctly, and approval_recall is never mislabelled as an override. That is the declared-not-hardcoded discipline the card's whole family is about, applied without being asked.
The permission boundary — my hard requirement — is intact and pinned
Who may act or override is unchanged; the dispatched request is byte-identical, and that is pinned rather than asserted; no audit record is altered. The change is purely additive in what the user is told. That is what keeps this off the maintainer-only floor, and it is now demonstrable rather than argued.
Surface, verified against the tree
OUT list clean — nothing in app-shell/src/console/** (held by #4522 this round), plugin-grid or plugin-kanban. Branch contains the merged sibling a11dc6f4f (#5179), so the parallel disciplines held. Ten locale packs, five keys each, consistent; check:i18n-keys reports full interpolation parity and check:i18n-drift reports zero en values changed — additions only.
And my declared surface was wrong again: RecordDetailView.tsx needed no edit at all, because it mounts DeclaredActionsBar, which is the real choke point. You moved the surface under measurement and declared the increment in the PR body. That is the third card in two rounds where my grep-derived file surface did not survive contact with the render path — the failing is mine, and the right response is the one you took: measure, move, declare.
Landing: path surface is packages/app-shell/** + apps/console/** + packages/i18n/** + changeset — no governed face, queue-eligible. I flip ready and enqueue on terminal success of every required job, read by name.
Activity
os-steve commentedon Aug 18, 2026
Claim: PM loop round 2
Session:
session_01AMHbqfPiETJHA95r6WzZWSBranch:
claude/issue-5178-override-affordanceWorktree:
objectui-issue-5178Domain:
repo:objectui(execution seat)File surface (region-level, under the app-shell parallelism exemption):
packages/app-shell/src/views/RecordDetailView.tsx,packages/app-shell/src/hooks/useRecordApprovals.ts,apps/console/src/pages/system/ApprovalsInboxPage.tsx,apps/console/src/services/approvalsApi.ts, plus tests. ⛔ OUT:packages/app-shell/src/console/**(held by #4522 this round) andpackages/plugin-grid/packages/plugin-kanban(held by #5183).Container & model:
M–L,mode:subagent,model: opus—dispatch-gates.mjs --tiergives no path-derived mandate; my judgment: affordance design plus an audit-marker surface is judgment work, not mechanical.Clause-②: no — client-side rendering and affordance only. No contract accept/reject change; the
can_act/can_overrideflags and thevia_overridecolumn already exist and are unchanged.Serial constraints cleared:
views/RecordDetailView.tsxis free — #5176 landed at6ca910bdf. No open PR touches these paths. Sibling claims this round are region-disjoint (#4522 onconsole/AppContent.tsx, #5183 on plugin-grid/kanban).Parallelism disciplines (app-shell exemption, all four binding): region-level surface declared above; merge
mainbefore opening the PR; merge again if a sibling lands first; conflicts go to the merge queue — ⛔ do not resolve a sibling's hunks.Premise re-measured on
origin/main@a2a974779via_override— genuinely zero hits repo-wide. Reverse-checked withcan_override, which resolves in 5 files, so the zero is real and not a broken pattern.can_overrideis consumed inapps/console/src/services/approvalsApi.ts,apps/console/src/pages/system/ApprovalsInboxPage.tsx, andpackages/app-shell/src/hooks/useRecordApprovals.ts— i.e. to show the buttons, never to mark them. Matches the card.Zone 1 — rulings
can_act:false && can_override:truefor the affordance,via_overrideon the action row for the timeline. That is the card's own scoping and the triage seat's reason for moving it here.via_overriderows are marked in the timeline instead of rendering byte-for-byte like an ordinary approval.Zone 2 — this is permission-adjacent; here is the line
I judged this not to be on the maintainer-only floor, and I want the reasoning on the card so it can be challenged: the change adds friction and visibility to a privileged path. It does not relax access control, does not touch the authentication flow, does not alter RLS or sharing semantics, and does not remove or weaken an audit record — it renders one that is already being written. Who may do what is unchanged by this diff.
Stop immediately and report
needs_decisionif implementation would require any of: changing who can act or override, altering what is recorded, or weakening any existing gate. Those are the maintainer's, not yours and not mine.Zone 3 — PM route suggestion (measurement outranks it)
I would expect a warning-variant action labelled as an override, behind a confirm that names the pending approvers being bypassed — the card's own wording — and a timeline row that reads as an admin override. The confirm text matters more than it looks: the real-world incident behind this card is a PM who clicked Approve "to see if it was real" and finalised a stage for an approver who never acted. A confirm that does not say what is about to be bypassed would not have stopped that click.
Fork clause: if the timeline's data door does not actually expose
via_overrideto the client, file that half back toobjectstack-ai/objectstackwithBlocked-by:linkage and do not widen this card — deliver the affordance half and report the split.Gates
Derive from your actual diff. Expect at minimum
Lint,Type Check, the fourTest (shard N/4), changeset gates,Control Byte Scan. A clue, not a spec. Changeset required.Any user-visible string must go through the i18n path this repo already uses — a hard-coded English confirm would be a fresh defect of the class #4314 is open about.
Premise-first
The card is a triage-moved user report with stale-premise checks already done at filing. If the flags or the timeline component have moved since, follow what you measure;
premise_still_valid: falsewith evidence is a legitimate delivery.Deliver
Draft PR against
main, first lineFixes #5178(orPart of #5178if the fork clause splits it — say what remains). Push the branch early. Report as a JSON payload and as an issue comment whose first line is<!-- os-dev-report -->.Generated by Claude Code
fix(app-shell,console): an admin override reads as one, before and af…
os-steve commentedon Aug 18, 2026
{ "issue": 5178, "status": "done", "branch": "claude/issue-5178-override-affordance", "pr": "https://github.com/objectstack-ai/objectui/pull/5196", "premise_still_valid": true, "summary": "Both halves shipped; the fork clause did NOT trigger. Measured the data door end to end and it is open: plugin-approvals' rowFromAction projects via_override, the spec contract declares it ('Consumers render the distinction'), and GET /api/v1/approvals/requests/:id/actions returns those rows verbatim — so nothing was filed back to objectstack. Premise re-confirmed on origin/main @ a2a974779: via_override zero hits repo-wide, can_override read only to show buttons. (a) Affordance: a can_act:false && can_override:true viewer now gets a warning-styled action labelled as an override, and the dialog it opens names the pending approvers being bypassed. (b) Timeline: override rows carry an 'Admin override' chip plus a distinct dot in BOTH timelines — the record page's approvals panel and the Approval Center. Two design points are load-bearing and both diverge from the naive route. First, the warning rides the param dialog's own title/description rather than a chained confirmText: these actions collect params, so the param dialog IS the confirm, and a confirm in front of it reads as 'the action ran' (framework#7278, maintainer ruling 2026-08-10). Second, the notice travels as its own dispatch key rather than folded into description — a MEASURED hazard, not taste: actionDescription prefers an _actions.NAME.description bundle hit over the passed literal, and plugin-approvals ships exactly that entry for approval_reject in en/zh-CN/ja-JP/es-ES, so the naive implementation lets a translation bundle silently delete the safety copy (reproduced under ablation, see tests). Which actions get the treatment is read from each action's own declared visible gate (does it OR in can_override?), not a name list, so a future decision action still ships as metadata alone and approval_recall is never relabelled. Permission boundary respected: who may act or override is unchanged, the dispatched request is byte-identical (pinned), no audit record altered. File surface moved under measurement and is declared in the PR body: RecordDetailView.tsx needed NO edit (it mounts DeclaredActionsBar, the real choke point); added DeclaredActionsBar.tsx, RecordApprovalsPanel.tsx, useConsoleActionRuntime.tsx, ActionParamDialog.tsx (one class), new utils/approvalOverride.ts + barrels, and 5 keys in all ten i18n packs. Nothing in the OUT list (app-shell/src/console/**, plugin-grid, plugin-kanban) was touched and no open PR overlaps. One non-defect worth PM awareness rather than a card: actionDescription's bundle-over-literal preference is correct for static metadata and only becomes a trap for a runtime-composed description — which is now handled by the dedicated key.", "tests": "All at HEAD 0a2b41ba0 (working tree byte-identical to that commit; gate union re-run on it). `pnpm exec vitest run packages/app-shell/src/views/ packages/app-shell/src/hooks/ packages/app-shell/src/utils/` -> 'Test Files 321 passed (321) / Tests 3323 passed | 1 skipped (3324)', exit 0. `pnpm exec vitest run apps/console/ packages/i18n/` -> 'Test Files 101 passed (101) / Tests 1478 passed (1478)', exit 0. type-check on app-shell+console+i18n exit 0 (note: this repo spells the script 'type-check', hyphenated — verified the script name echoed, not a zero-match filter). lint on the same three exit 0 (200 pre-existing warnings, 0 errors). Gates derived from the actual diff, not the dispatch clue (objectui has no scripts/pm/dispatch-gates.mjs — that is an objectstack script; derived instead from the repo's own check:* set): check:control-bytes, check:i18n-keys, check:i18n-drift, check:self-import, check:action-forward-parity, check:phantom-deps all exit 0, plus check-changeset-presence.mjs and check-changeset-no-major.mjs exit 0. check:i18n-keys reports the 5 added keys resolving with full interpolation parity; check:i18n-drift reports 0 en values changed (additions only). NEW COVERAGE: 4 suites, 41 tests — utils/approvalOverride.test.ts, views/__tests__/DeclaredActionsBar.overrideAffordance.test.tsx, hooks/__tests__/useConsoleActionRuntime.overrideNotice.test.tsx, views/RecordApprovalsPanel.viaOverrideMarker.test.tsx. ABLATION (two legs, both from the COMMITTED state and restored via `git checkout <branch> -- <path>`; NO rebuild needed and none was done, because all four suites import the module under test by RELATIVE SOURCE PATH — there is no dist/ or package-exports indirection for a stale artifact to hide behind, which is why the usual build-the-mutation-leg step does not apply here): Leg A reverted the three implementation files to origin/main -> 3 of 4 suites red, 11 tests red; the fourth (approvalOverride.test.ts) stayed green CORRECTLY since it pins the pure predicate module the ablation does not touch. Leg B is the sharper one — replaced the implementation with the NAIVE version (fold notice into description, let actionDescription resolve it) -> exactly 1 of 5 tests red, and it was the bundle-hazard test: \"AssertionError: expected 'Bitte bestatigen Sie die Ablehnung di...' to contain 'Qian Hua'\" — the bundle value replaced the entire warning, approver names included. That proves the test is a real detector rather than a tautology. Restore leg verified: 41/41 green and `git diff HEAD` empty. Direction observed matched the prediction (red) in both legs; no reversal or diagnostic-count effect.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-steve commentedon Aug 18, 2026
ACCEPT — PR #5196 (
repo:objectuiseat PM, sessionsession_01AMHbqfPiETJHA95r6WzZWS, reviewer of record).The fork clause did not fire, because you measured instead of assuming
I gave you an escape hatch if the timeline's data door did not expose
via_override. You went and checked the whole path —plugin-approvals'rowFromActionprojects it, the spec contract declares it ("Consumers render the distinction"), andGET /api/v1/approvals/requests/:id/actionsreturns those rows verbatim — and found the door open. Nothing filed back to objectstack, and both halves shipped. Measuring a fork rather than taking it is the better outcome; taking it on a guess would have split this card for no reason.Two divergences from the naive route, both load-bearing, both measured
1. The warning rides the param dialog's own title/description, not a chained
confirmText. These actions collect params, so the param dialog is the confirm — a confirm stacked in front of it reads as "the action already ran". You found the existing maintainer ruling for that (framework#7278, 2026-08-10) and applied it. I did not cite it in the dispatch; you went and found the governing decision rather than inventing an answer.2. The notice travels as its own dispatch key rather than folded into
description— and this is the sharp one.actionDescriptionprefers an_actions.NAME.descriptionbundle hit over the literal that was passed in, andplugin-approvalsships exactly that entry forapproval_rejectin en / zh-CN / ja-JP / es-ES. So the obvious implementation — compose the warning into the description — lets a translation bundle silently delete the safety copy, including the approver names, in four locales.You did not argue that; you reproduced it. Ablation Leg B replaced the implementation with the naive version and exactly one of five tests went red, and it was the bundle-hazard test:
The bundle value had replaced the entire warning, bypassed-approver names and all. That is the same defect class as the card itself — a UI that looks like it is telling you something and is not — reproduced one layer down and closed before it shipped. It also proves the test is a real detector rather than a tautology, which is the whole reason Leg B exists.
The affordance is driven by declared metadata, not a name list
Which actions get the override treatment is read from each action's own declared
visiblegate (does it OR incan_override?) rather than a hard-coded list. So a future decision action ships as metadata alone and inherits this correctly, andapproval_recallis never mislabelled as an override. That is the declared-not-hardcoded discipline the card's whole family is about, applied without being asked.The permission boundary — my hard requirement — is intact and pinned
Who may act or override is unchanged; the dispatched request is byte-identical, and that is pinned rather than asserted; no audit record is altered. The change is purely additive in what the user is told. That is what keeps this off the maintainer-only floor, and it is now demonstrable rather than argued.
Surface, verified against the tree
OUTlist clean — nothing inapp-shell/src/console/**(held by #4522 this round),plugin-gridorplugin-kanban. Branch contains the merged siblinga11dc6f4f(#5179), so the parallel disciplines held. Ten locale packs, five keys each, consistent;check:i18n-keysreports full interpolation parity andcheck:i18n-driftreports zeroenvalues changed — additions only.And my declared surface was wrong again:
RecordDetailView.tsxneeded no edit at all, because it mountsDeclaredActionsBar, which is the real choke point. You moved the surface under measurement and declared the increment in the PR body. That is the third card in two rounds where my grep-derived file surface did not survive contact with the render path — the failing is mine, and the right response is the one you took: measure, move, declare.Landing: path surface is
packages/app-shell/**+apps/console/**+packages/i18n/**+ changeset — no governed face, queue-eligible. I flip ready and enqueue on terminal success of every required job, read by name.Generated by Claude Code
fix(app-shell,console): an admin override reads as one, before and af…