---
{
  "n": 11,
  "title": "Subagents on 2.1 \u2014 the grant, the ledger, assign and report",
  "abstract": "How a subagent works under the journal on 2.1: lent an environment, declared on every command, its rows marked in one flat record, assign and report, and what stays refused \u2014 for approval before code",
  "refs": [
    "todo:77"
  ],
  "seen": [
    "agent",
    "user"
  ],
  "data": {},
  "created": 1789759538.6173792,
  "updated": 1789759608.791135,
  "deleted": 0.0,
  "completed": 0.0,
  "outcome": "",
  "type": "doc"
}
---


## What this settles
How a subagent works under the journal on 2.1: what it is lent, what it may write, how its rows are told apart from the dispatcher's, and how it hands a row back. It rests on doc 2's synthesis (environments stay flat; `--env` is the grant; `parent` would be metadata at most) and on what 2.1 already has: `journal environment <n> grant` / `--off`, `Sessions.granted`, `sessions.allowed()` with the `lent` flag on every type, and `--as` on the CLI.

## The fact everything rests on
A `journal` command inside a subagent cannot know it is one: the child's shell carries the dispatcher's session id and nothing finer (doc 2, measured twice). So a subagent is never detected, only **declared twice**: the dispatcher lends an environment, and the subagent names itself on every command. The hook holds the two against each other.

## The contract, in five words
- **grant** — `journal environment <n> grant` lends that environment to this session's subagents (already there); `--off` takes it back. `journal environment grants` lists what is lent. Granting moves nobody.
- **--env and --as** — a subagent runs every command as `journal --env="<lent>" --as="<id>" …`. `--as` is a new flag (today `--as` is the actor: user/agent/system; the subagent flag becomes `--agent="<id>"` to keep `--as` meaning the actor). Without both, the gate refuses the write and names the two flags.
- **the ledger** — a subagent's rows live in the same environment, marked: every resource it creates carries `data.agent = "<id>"`, and the agent row `agents/<id>` is created on its first write (title = the id, `data.dispatcher = <session>`). No separate folder, no second record: one record per environment, flat, as doc 2 rules; a filter on `agent` is what "its own ledger" means. The viewer shows an agent's rows grouped under it on the agent page (to-do 79) and a chip on each row.
- **assign** — `journal todo assign <n> --to="<id>"` marks the row `data.assigned = <id>`; `--off` clears it. A to-do assigned to one agent is refused for `work start` by any other (the gate feature reads it). Auto mode skips assigned rows.
- **report** — `journal todo report <n> --how="<how>"` by a subagent sets `data.reported = {agent, how, at}` and notifies the dispatcher (a nudge: "agent <id> reports to-do n done: <how>"); only the dispatcher runs `todo done`. A runner does not mark its own homework.

## What stays refused for a subagent, and why
| refused | why |
|---|---|
| `environment switch/claim/prepare/grant/leave` | they move a session, and the session it would move is the dispatcher's |
| `rule` | a rule binds every environment; it was lent one |
| `doc`, `tool`, `style` | project-scoped, not the environment's |
| `pin`, `reminder`, `suggestion` | inherited or ruling; a claim nobody reviewed must not sit at the top of the record |

This is the existing `lent` ClassVar: `lent = True` on message, todo, work, question, comment, report, plan, reaction, notification, nudge; `False` on the rest. `sessions.allowed()` already reads it.

## Liveness
A subagent's agent row is stamped `active` on every write. A row it holds (assigned or reported) lapses when its agent has not written for `agents.lapse` minutes (a setting, 20 by default): the assignment clears and the dispatcher is nudged once. Nothing can tell us a subagent died; the heartbeat is what we have.

## The engine's part
The engine follows the dispatcher, not the subagent: a subagent's writes are ordinary events on the lent environment, delivered to the dispatcher's terminal like any other (batched). The features that fire on `agent.updated` fire on the dispatcher's row only — a subagent never gets reminders, tags or inbox holds; its stop is silent, as doc 2 says.

## Build, as features
- `features/agents/feature.py` — the ledger: on any `created` event carrying `agent`, ensure the agent row and stamp `active`; the lapse sweep on `agent.updated` (dispatcher's ticks); the assign/report handlers (`todo.updated` with `reported` → nudge the dispatcher).
- `controllers/types.py` — `Todos.assign(n, to, off)`, `Todos.report(n, how)`; the gate feature refuses `work start` on a row assigned elsewhere.
- `commands/cli.py` — `--agent="<id>"` beside `--as`; `hook.py` passes it as the actor id `sessions.allowed()` already expects.
- `providers/base.py` — the gate reads `--agent` from the command line the hook sees (`journal --env=x --agent=y …`) and refuses a write that names one without the grant, or a grant without the flags.
- Tests: `tests/features/agents/` (grant → write allowed; no grant → refused with the two flags named; refused verbs; assign and report; lapse) and the sessions suite.
- Skill: the `journal-agents` text already describes this contract; it is regenerated from the package (`skills/journal.md` gets the five words).

## What I would leave out
- Child environments and `journal spawn` (doc 2's open question): not built; a naming convention (`feature-x-runner`) costs nothing and the ledger filter gives provenance.
- Worktrees: untouched, as doc 2 rules.
- Reading a subagent's transcript in the viewer: to-do 79's agent page, later.

## Open for the user
1. Is one record per environment with an `agent` mark enough for "its own ledger", or do you want the old folder `environments/<env>/agents/<id>/` on disk?
2. The lapse: 20 minutes without a write clears an assignment — or should a lapsed row only be flagged, never cleared?
3. `--agent` for the subagent id (keeping `--as` for the actor), or rename the actor flag instead?
