feat: #3613914 A user who is away has no way to let a colleague act on their tasks
Adds standing delegation: for a period, one user acts on another's tasks. Follows #3613913, which freed the word.
The seam
Core carries only the question, as DelegationResolverInterface: who does this account act for, who acts for it, and does it still want its own notifications. NoDelegation answers "nobody" to all three, so an install without the submodule pays one method call (pinned by NoDelegationTest). orchestra_delegation decorates orchestra.delegation_resolver and answers from a stored entity. Same shape as the existing orchestra.notification_context seam, so a site whose absences live in an HR system or LDAP can decorate it instead.
Why it reaches every surface
Two widenings, both central:
AssignmentMatcher::viewerTokens()unions the delegators' tokens. Today's body moves topersonalTokens(), the un-widened set, which is what lets a surface tell borrowed work from its own.- A new
actingFor()returns the user ids a viewer may act for. This is the half a tokens-only implementation would miss: a task the delegator has already claimed carries no candidate token at all, andassigneewas a hard equality inaccountVisibilityCondition(),userCanAct()and, as raw SQL, theCurrentUserCandidateviews filter. All three now readactingFor().
So the inbox, the pending-actions list, VBO, interaction tasks, content-bound tasks and the Views filters all follow without knowing delegation exists.
Read at the moment of asking, never written to the task
A step assigned to one named person is still auto-claimed for that person mid-absence; the assignee is never rewritten. That is load-bearing, not incidental: if assignment resolved through cover, a task created during an absence would belong to the stand-in for good, which is a reassignment with extra steps. Because nothing is written, declaring cover applies at once to work already waiting, revoking it takes the work back with nothing to unwind, and the assignee always answers "whose task is this".
What is recorded, and when it stays silent
A new on_behalf_of base field records who the actor was acting for. It is set when a delegate completes a task assigned to the person they cover for, and when one claims a pooled task that only their cover made visible. It is deliberately never a guess: a value appears only when exactly one delegation accounts for the reach. Two covered users in the same audience, either of whom could equally explain it, records nothing, because naming one would be decided by row order. It is also empty for a manager acting through the reassign permission, who stands in for nobody.
The two paths carry different strengths, and the field is not described as ownership because of it: on a task its assignee already held the person named is unambiguously whose work it was, while on one taken from a pool it records the capacity the actor was acting in, a pool being nobody's in particular.
Every covered row explains itself
holderLabel() takes an optional viewer. A held row reads bob (for alice); an unclaimed pooled row reached only through cover reads Offered to: role:reviewer (covering for alice), or (covering for another user) when the reach is ambiguous, so a stand-in is never shown a row addressed to a group they do not belong to with nothing to explain it. Omit the viewer and the wording stays viewer-independent, for a digest or a log line. The Views column varies per user accordingly.
Cardinality
Both directions are many and uncapped: several stand-ins for one person, and one person covering several. Only a duplicate of the same pair over an overlapping period is refused, since two rows saying the identical thing cannot be reasoned about. Cover does not chain (one hop), so no cycle is possible. A node's several assignments resolve into one candidate set before any of this runs, so a multi-audience step needs no rule of its own.
Notifications
The delegate joins the audience of the work they cover. A per-delegation "Keep notifying me" lets the delegator step out; with several delegations active it takes unanimity, this being the only place Orchestra removes a resolved recipient rather than adding one.
Views
on_behalf_of is exposed as a by-username filter plus a third personal lens, "Handled on the current user's behalf": the list to read on returning from an absence. It is the delegator's side of the same rows a stand-in sees in their own history, and both are true at once, one recording who acted and the other who they acted for. The three user-shaped columns stay plain ids, so the work-item table keeps no hard dependency on the user entity type; the stale docblock claiming they became relationships is corrected.
Coverage
52 tests across seven classes: the resolver (window, switch, tenant isolation, no chaining, both cardinality directions, anonymous), visibility (pooled and already-claimed work arriving, one-way cover, lapse, multi-assignment unions, the ambiguity rule, the read-time invariant, every holder-label wording), recipients, access (both permissions, and that being someone's delegate does not let you rewrite the arrangement), the Views lenses, the core no-op, and a functional round trip through real pages: declare cover, work another inbox, complete, revoke, plus every form refusal.
Docs: a new delegation.md, plus human-tasks.md, concepts.md, views.md, notifications.md, multi-tenancy.md, audit.md, extending.md, architecture.md, roadmap.md and the README. French complete for the new module and the new core and views strings.