Actor model: tbd cannot express who is accountable separately from who is executing, and Linear models that as a first-class axis
- Dominant language
- TypeScript
- Stars
- 79
- Forks
- 9
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 42
Description
Actor model: tbd cannot express who is accountable separately from who is executing, and Linear models that as a first-class axis
tbd has one actor field, `assignee: z.string()`. Linear has two — `assignee` (accountable) and `delegate` (acting) — and its entire agent platform hangs off the second. GitHub has a comparable split via agent assignment. The collapse is lossy, and today it is silent.
This is the same shape as #244: a single tbd enum standing in for two axes that both providers model separately. #244 is the closed end of the status axis; this is the actor axis.
Found while assigning two epics in a ~900-open-bead repository where nearly all execution is done by coding agents. `assignee` is used on **zero** beads there — not from neglect, but because there is nothing useful to put in it: writing the human means the field is constant and carries no information, and writing the agent is not expressible at all.
## The three models, as they actually are
Verified against the installed `get-tbd@0.7.1` dist and the Linear SDK schema rather than recollection.
**tbd** — one opaque string:
```ts
assignee: z.string()
```
The Linear adapter maps it to exactly one field:
```js
assigneeId = patch.assignee === null ? null : await this.res…
```
and the default field-flow rule carries the design assumption in a source comment:
```js
assignee: FieldFlowRule.default("local") // "`assignee` stays local because tracker assignees are people"
```
That comment is correct about 2024 and load-bearing today. It is the assumption this issue is about.
**Linear** — two actor fields, publicly mutable:
| Field | Meaning | Input |
| --- | --- | --- |
| `assignee` | accountable human | `IssueUpdateInput.assigneeId` |
| `delegate` | acting agent | `IssueUpdateInput.delegateId` |
Linear states the taxonomy explicitly as *delegation, not assignment*: the issue keeps a human assignee while the agent is the delegate. Setting `delegateId` automatically creates an `AgentSession` and fires the target agent's `AgentSessionEvent(created)` webhook. Agents are OAuth apps installed with `actor=app`, gated by `app:assignable` / `app:mentionable`, free of a seat, and surfaced natively in delegate-filtered views and Insights segmented by delegate.
**GitHub** — assignees are a list, and agent assignment is a distinct mechanism layered on it rather than a second value in the same slot.
### What the comparison shows
**1. tbd has no representation of an agent actor whatsoever.** `delegateId`, `agentSession`, `app:assignable`, and `actor=app` appear in **zero** files in the tbd dist. This is not a mapping gap in the adapter; the concept is absent from the model.
**2. `user_map` is the only lever, and it is the wrong shape for it.** It maps an alias to an email or user UUID, and it is deliberately closed — unmapped assignees are skipped on push. That closure is the *only* thing today keeping agent names out of a tracker, which means "do not publish this actor" is expressed by omission from a config table rather than by the actor having a kind. Two consequences:
- An agent alias cannot be recorded in tbd at all without either leaking to the tracker (if mapped) or becoming an untyped silent skip (if not).
- A Linear agent is an **app user**, not a person with an email. `user_map`'s value domain cannot name one even if you wanted to, and `delegateId` is a separate mutation field regardless.
**3. The two gates are independent and neither is diagnosable.** `field_sync.fields.assignee` (`local` by default) decides whether the field participates at all; `user_map` decides which identities may be pushed. With the default `local`, a push reports `updated N, skipped 0, failed 0` while the assignee is silently not a candidate. Nothing in `--verbose` names a field that was excluded by flow rule, and nothing distinguishes "not eligible" from "eligible and unchanged". Both gates are correct designs; the absence of a report is the defect.
**4. The single axis forces a false choice that has no good branch.** With one field and agents doing the work, a repository must pick: human everywhere (field is constant, so it carries nothing and Linear's delegate views stay empty), agent everywhere (Linear loses its accountable-human signal, and mapped agent identities pollute a shared workspace), or leave it empty (what actually happens — zero uses across ~900 beads).
The branch that gets picked in practice is "human everywhere", for a reason worth stating plainly: **issues in Linear are managed per human, not per agent.** Linear is where a person decides what is on their plate. An agent identity in the assignee slot answers a question nobody is asking there, while destroying the answer to the one they are. That is precisely why Linear separated the axes, and it is why a tracker-facing tool needs both — not one field that has to mean whichever the caller happened to intend.
## Recommendation
Take Linear's **shape** — two actor axes — without importing its app-user identity model.
```
assignee: # accountable. Unchanged field, unchanged meaning.
delegate: # acting. New. Absent means "same as assignee".
```
and give the identity table an actor kind, so publishability is a property of the actor rather than of its presence in a map:
```yaml
user_map:
josh: { email: josh@example.com, kind: human }
claude: { kind: agent } # no address: never pushed as an assignee
cyrus: { kind: agent, linear_app_user_id: } # pushable as a delegate
```
The plain `alias: email` form should keep parsing as `kind: human`, so this is additive.
Mapping is then total in both directions:
| tbd | Linear | GitHub |
| --- | --- | --- |
| `assignee` (human) | `assigneeId` | assignee |
| `delegate` (agent, no app id) | *not pushed* — local only | *not pushed* |
| `delegate` (agent with app id) | `delegateId` → AgentSession | agent assignment |
Two smaller asks that stand on their own even if the axis is not added:
- **Report fields excluded by flow rule.** A push that says `updated 2` should be able to say which fields were in scope. One line under `--verbose` — `assignee: not eligible (flow=local)` — would have saved a real debugging session.
- **Warn when an assignee is set on a bead whose field flow is `local`.** Silently accepting a write that can never reach the tracker is the same class of quiet loss #244 describes.
## What this deliberately does not propose
Agent **presence** — which session is touching a bead right now — does not belong in a bead field, and this issue is not asking for it. Heartbeat cadence is ~30s, tbd merges `extensions` as whole-object last-writer-wins, and sync-branch commits are the wrong granularity; that state belongs in a database with a TTL lease. `delegate` here is the durable, low-frequency fact of who a unit of work is assigned to, which is exactly the cadence a tracker field is for.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by locating the TypeScript actor schema, Linear adapter, field_sync flow handling, and user_map parsing described in the issue. Trace how assignee eligibility and --verbose reporting currently work. Done means representing separate assignee and delegate axes, supporting actor kinds and provider mappings, while preserving the plain alias-to-email form and reporting excluded fields.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- github, typescript
- Domain
- backend-api-design, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100