---
{
  "n": 2,
  "title": "Subagents, worktrees and the journal - two researched designs that disagree",
  "abstract": "Both agents settle the crux \u2014 a CLI inside a subagent cannot know it is one \u2014 then split on whether environments should have parents. Their disagreement is narrower than it looks, and the synthesis is",
  "refs": [],
  "seen": [
    "agent"
  ],
  "data": {},
  "created": 1788965100.0,
  "updated": 1788965100.0,
  "deleted": 0.0,
  "completed": 1789745298.5460548,
  "outcome": "",
  "type": "doc"
}
---
Two researchers were asked, separately, how subagents and worktree agents should interact
with the journal, and what to make of the user's idea of "a sub-journal — an environment
which is part of another environment". One designed; one looked for prior art. They agree
on the fact everything rests on, and disagree on the conclusion.

Nothing here has been implemented. To-dos 39 and 40 hold the two decisions and both are
marked as waiting on the user.

## The crux, settled twice, independently

A `journal` command running inside a subagent CANNOT know it is one.

`agent_id` exists only in the JSON payload delivered to a hook's stdin. It is not an
environment variable, not on argv, not in any file the child inherits. The CLI resolves
"who am I" through `CLAUDE_CODE_SESSION_ID`, and inside a subagent's shell that holds the
PARENT's id — measured, and recorded in `transcript.py`'s own docstring long before today.

The second researcher confirmed it from the other end, live, from inside a subagent:
`CLAUDECODE=1`, `CLAUDE_CODE_CHILD_SESSION=1`, `CLAUDE_CODE_SESSION_ID=<the parent's>` —
and nothing finer. They also found the open Claude Code issues asking for even the coarse
session id to be exposed to tool processes (#47018, #25642), still unimplemented, and the
issue that got `agent_id`/`agent_type` added to subagent tool events for HOOKS only
(#16424).

WHAT FOLLOWS: "its own journal" cannot be detected. It can only be GRANTED, by the
dispatcher, in a way the child carries on its command line — which is exactly the explicit
declaration the user asked for, arrived at from the machine's side rather than the
design's.

## Where they agree

  - the grant is `--env="<name>"` on every journal command the subagent runs; that flag
    already exists and needs no change
  - the enforcement is the hook's existing single `agent_id` check, which gains one
    exception rather than a second question asked elsewhere
  - a subagent's stop should stay silent: it returns to its dispatcher, and the
    dispatcher's own stop hook is what surfaces the work
  - RULES AND DOCS ARE ALREADY PROJECT-WIDE and need nothing: every environment reads
    every rule and every doc today
  - WORKTREES ARE ORTHOGONAL. A worktree session is an ordinary session that reaches the
    main checkout's journal through a symlink. "An agent with its own journal" and "an
    agent in a worktree" are different questions that arrived together and were never
    actually the same thing. The deleted hand-off code said so itself: the runner got a
    worktree and the journal was shared on purpose, "so isolating it would isolate
    nothing."

## Where they disagree

THE DESIGNER says: give a subagent a CHILD environment — `journal spawn "<name>"` records
`parent = <the dispatcher's environment>`, prints the exact sentence the dispatcher must
pass on, and the hook allows a write only when the command carries `--env=<that child>` and
that child's parent is the dispatcher's own environment. Three concurrent subagents get
three children and real provenance; the old `delegate` gave all three the same environment
and no way to tell their pins apart. Crucially, this design copies NOTHING down: pins,
reminders and to-dos do not inherit — the parent is a pointer that says why the environment
exists.

THE RESEARCHER says: do not build hierarchy at all, and brings the strongest evidence in
either report:

  - ESLint's cascading `.eslintrc` — walk up the tree and merge what you find — "caused
    unexpected behavior and was completely dropped" for flat config in v9. A widely used
    tool built this exact shape and removed it.
  - the CSS cascade is the canonical hard case, and Cascade Layers, added to make override
    intent explicit, "create new pitfalls and confusion". What makes inheritance
    comprehensible is ONE uniform rule for who wins; the moment there are two, nobody can
    predict the result by reading either side.
  - this package's own ruling, from `worktree.py`: "Either way one record." The whole
    worktree mechanism exists to collapse two copies into one because two authoritative
    copies drift. A parent/child relation reopens that question one level down, with no
    filesystem primitive like a symlink to lean on.
  - delegation — one parent, one child, one direction — already cost "nine branches, six
    functions" and was torn out. Open-ended parents and children are the same shape at
    higher multiplicity: what happens to children when a parent is removed, whose rule wins,
    and every flat query (`search`, `cleanup`) becomes a graph walk whose ORDER decides the
    answer.
  - and the sharpest point: inheritance would NOT have prevented the failure that prompted
    all this. That agent did not create a child; it created a SIBLING and switched to it.
    Inheritance only helps if somebody remembers to declare the parent, which adds a second
    invisible axis to "did my guardrails survive the switch" — today the answer is a plain
    no; afterwards it would be "it depends".

## The synthesis, which is narrower than the disagreement looks

They are not arguing about the same thing. The designer's child environment carries NO
inheritance — pins, reminders and to-dos stay where they are written, and rules and docs are
already global. The researcher's case is against INHERITANCE, not against a parent field.

So what survives both is:

  1  `--env=` is the grant, and the dispatcher must print and pass it. This is the whole of
     the user's "say so explicitly", and it is forced by the machine, not by manners.
  2  environments stay FLAT in behaviour. Nothing inherits. Nothing overrides. Every
     existing query stays one hop.
  3  `parent` is METADATA — provenance, and grouping in the listing. It answers "why does
     this environment exist" and nothing else. If it ever starts deciding what applies to
     whom, it has become the thing ESLint deleted.
  4  worktrees are left exactly as they are.

AND A CHEAPER FIX FOR THE FAILURE THAT STARTED THIS, which the researcher surfaces and
neither had been asked for: `journal switch` already prints "N pin(s), N open". Have it
print the reminder count too — "you are now on X; 7 reminders on `default` are not in force
here". That is one line, no new concepts, and it catches the exact story on the record.

## What is NOT settled, and needs the user

  - is a child environment worth having at all, or is `feature-x` / `feature-x-runner` —
    a naming convention, no schema change, no new command — enough?
  - what happens to children when a parent environment is removed?
  - should `journal spawn` exist as a command, or is `prepare` plus `--env=` sufficient?
  - and the compatibility question that is more urgent than any of it: a colleague is
    running an orchestrator against this package today. If it uses `journal delegate` or
    `journal handoff`, the deletion on this branch breaks it. That is one question to them,
    and it should be asked before anything here is published.

## The adversarial read: what the report got wrong, and one bug it walked past
A fourth agent was asked to attack the report rather than agree with it. It verified every
factual claim against the code and found the report broadly right on facts — and wrong or
silent on five things that matter more.

## It found a live bug, in shipped code, sitting beside the fix the report proposed

The report suggested a "cheap fix" for the failure that started all this: have `journal
switch` print the reminders it is about to silence. The reviewer went to write it and found
the line next to it already broken.

`tracks.switch` builds its summary from the record's registry:

    held = tracks.get(name, {})
    kept = f"{len([p for p in held.get('pins', []) ...])} pin(s), ..."

1.34.0 moved pins and work OUT of `tracks.<name>` into `environments/<name>/`, and updated
every call site that needed these counts — `page`, `listing`, `_held_summary` — except this
one, three hunks below in the same file, in the same commit. **Every switch since has
printed "0 pin(s), 0 open" on environments holding both.** Confirmed live on this project:
switching to `reminders`, which had 3 pins and 1 open piece of work, printed zeroes.

FIXED, and both halves are now in: the counts read the environment's own files, and leaving
an environment says which reminders it just silenced, naming the one being LEFT — which is
the half no other line in that function had ever looked at. `test_tracks` holds both.

## A safety hole in the design the report recommended

The report's enforcement is "the hook's existing `agent_id` check gains one exception:
allow a write when the command carries `--env=<the child>`." The reviewer worked out what
that permits, and it is worse than a misfiled pin.

A subagent has no session of its own — `_stem()` resolves through `CLAUDE_CODE_SESSION_ID`,
which inside a subagent is the PARENT's id. `switch` is a journal write. So a generic
exception lets a subagent run `journal switch "<child>"`, which calls `tracks.switch(...,
stem=<the parent's own session id>)` — **silently rebinding the orchestrator's live session
to the child environment, mid-task.** Not the subagent's; there is no such thing.

Neither researcher mentioned excluding `switch`, `claim`, `--back` or `--session=` from the
exception. It has to be excluded BY VERB, explicitly, or the design is unsafe on its first
dispatch.

## Three things nobody had been asked about

ONE LOCK, NOT ONE PER ENVIRONMENT. `state.locked` is a single file at the project root, and
it is bounded: three seconds, then it PROCEEDS WITHOUT THE LOCK with only a line on stderr.
Three subagents scoped to three different environments still serialise on it. The design
promises "three concurrent subagents, real provenance"; the locking was sized for "two
hooks writing at once" — its own comment says so, in the singular — and a lost write under
that failure mode is silent, on a subprocess's stderr that nobody reads.

A CRASH MID-SEQUENCE. `state._write` is atomic per file, so no single file is ever half
written. Nothing wraps a SEQUENCE: `switch` writes the registry, then the bindings, then
the session index — three files, one lock, and a kill between them leaves them disagreeing
with nothing to detect or repair it. A spawn flow would have the same shape.

A REMOVED PARENT. `tracks.remove` has no idea a parent field exists and, under a
metadata-only design, never would. It would archive an environment while children keep
pointing at a name the registry no longer lists — a dangling reference, in the one module
whose stated first rule is that nothing disappears without somebody deciding it should.

## Two corrections to the report's own claims

"EVERY ENVIRONMENT READS EVERY DOC" is half true. There is no access control, but a doc's
`track` field is already a filter predicate: `tracks.page` shows an environment its own docs
first. Provenance that already decides what is surfaced.

"THE DELETION BREAKS A COLLEAGUE'S ORCHESTRATOR" is understated in depth and roughly right
in scope. `delegate` was not a command that printed a page: it was a permission model that
let subagents WRITE, wired through `SubagentStop`, a 108-line skill and a 433-line suite,
and preserved by an explicit ruling. Whatever uses it does not get a "command not found" —
it loses a capability. It is also only six days old, so it is unlikely to be load-bearing
everywhere yet.

## The two positions, argued hard: the strongest case each way
Two more agents were asked to argue opposite sides in good faith rather than to summarise.
Both went for the synthesis's soft middle — "keep a parent field, but let nothing read it"
— and both concluded it is the weakest of the three options, from opposite directions.

## FOR a real parent: the metadata-only version is the worst of both worlds

THE LIFECYCLE ARGUMENT, which neither original researcher made. `cleanup._environments`
flags an environment with "no pins, no open work, no to-dos, nobody on it". A dispatch
environment that did its job PERFECTLY — everything written, everything closed, the
subagent reported — ends in exactly that state and gets the identical sentence a neglected
experiment gets. Nothing in the record distinguishes "this withered" from "this closed its
loop". A parent pointer is what turns "empty" from an ambiguous fact into a finished one.

And here the middle position contradicts itself: if `parent` is truly inert, `cleanup`
keeps printing the same undifferentiated line and the field bought nothing. If `cleanup`
says something different for a child, the field is deciding what a command tells the
reader — which is the thing the middle position forbids. You pay the schema and get none of
the signal.

THE REPORTING ARGUMENT. After three subagents finish, the dispatcher reads three
environments by name or greps and hopes. With a parent it is one filter over the dict
`_all()` already returns — no recursion, because nobody is proposing grandchildren — and it
catches for free the case nothing catches today: you dispatched three and only two ever
wrote anything.

ON THE OBJECTIONS. ESLint killed the upward WALK — merging competing values from several
levels. Nothing here merges; nothing is copied down; there is no second place to look, so
there is no override arithmetic to get wrong. The CSS cascade is hard because of
specificity and source order, not because a DOM node has a `parentElement`. And "either way
one record" is about one fact existing in two authoritative places; a pointer BETWEEN two
environments is not that — `previous_track` already stores one environment's name inside
another's record and is read back by `--back`, and a pin's `--doc=` is the same category of
reference. If cross-referencing violated that ruling, both would already violate it.

AND THE SHARPEST POINT: the middle position quietly drops the one thing that made the
strong version safe. The designer's hook check was that the child's RECORDED parent equals
the dispatcher's own current environment. Metadata-only reduces that to "any subagent
carrying `--env=X` gets X", with nothing checked against who made X. It keeps the cost and
gives up the single place the field could have paid for itself as a safety check.

## AGAINST any parent: the field will not stay inert, and this repo proves it

THE EVIDENCE IS IN THIS PACKAGE, not in ESLint. `docs.py`'s `track` field was introduced
with an explicit promise, written into the CHANGELOG: "Its `track:` is provenance, not
ownership." Within the SAME RELEASE, `cleanup.py` began reading it to decide staleness —
"its environment `X` is gone" — and `tracks.page` uses it to filter what an environment is
shown. The smallest, most metadata-shaped field in the package, documented hardest against
growing behaviour, grew a behavioural consumer in one release.

"We will keep it inert" is not a promise this codebase's history supports.

THE COST, ITEMISED, before a single subagent has used it: creation threading through
switch/prepare/spawn, the listing and its sort, the environment page, removal's fifth
question (orphan, refuse, or cascade), cleanup's empty-environment check, the status block
that was just trimmed for width, a retroactive meaning for "no parent" in every existing
consumer record, and the skill.

THE SKILL COST, which nobody priced. It already teaches pins, rules, reminders, to-dos with
two kinds of blocked, docs, tools, work, tags and environments — and the user reported
agents confusing pins and reminders THIS WEEK, which took a five-paragraph section with a
four-way table to address. Adding "an environment can have a parent, which is metadata, not
inheritance, and is not the delegation that was removed" is not one sentence: it is a
paragraph that must actively defend against the two most natural misreadings.

WHAT THE FLAT MODEL ALREADY DOES: `journal prepare "feature-x-runner"` names the provenance
in the string; `listing` sorts by name so the prefix groups for free; `cleanup` already
detects the finished child with no pointer at all; `journal search <term> --all` walks every
environment. And a pin — "spawned by feature-x to fix the recompose 500" — is stronger
provenance than a foreign key, because a pin is prose the reader is handed at every start.

ONE REAL GAP IT CONCEDES: `prepare` switches the calling session onto what it creates, so a
dispatcher making a child is briefly moved onto it and must switch back. That is fixed by a
`--no-switch` flag, not by a schema. (To-do 29 already asks for that split for a different
reason: a command called "create" should not move the session.)

WHAT WOULD MAKE IT WRONG: a name is a convention and a field is enforced. A typo'd
`featurex-runner` silently breaks prefix grouping where a pointer would not. Whether that
has happened: no. Today's evidence is one flat mistake — a stray environment mistaken for
precedent — and zero instances of a grouping query failing for want of a parent.

## Where five agents leave it, and the three questions only you can answer
Five agents have now read this: two designed, one attacked, two argued opposite sides. What
they converge on, what they will not settle, and what is already fixed.

## Settled, by all five

  - a `journal` command inside a subagent CANNOT know it is one. The grant must be explicit
    and must travel on the command line: `--env="<name>"`. This is forced by the machine,
    which is a better guarantee than a convention.
  - the exception must be BY VERB. `switch`, `claim`, `--back` and `--session=` stay refused
    for a subagent, or it can silently rebind the orchestrator's own live session — because
    it is running under the orchestrator's session id. This is the single most important
    thing the adversarial pass found in the design.
  - worktrees are orthogonal and stay exactly as they are.
  - rules and docs are already project-wide and need nothing.
  - a subagent's stop stays silent; the dispatcher's own stop is what surfaces the work.

## Not settled, and both sides are strong

The middle position this report first proposed — keep `parent`, let nothing read it — was
attacked from both directions and survived neither. One side: a pointer nothing reads is
cost without signal. The other: this codebase has never kept a field inert, and `docs.track`
grew a behavioural consumer inside one release of being documented as "provenance, not
ownership".

So the honest state is a two-way choice, not a compromise:

  EITHER a parent that is REAL — read by `cleanup` to tell a finished child from a
  neglected one, read by a `children` query, and read by the hook to check that a child's
  recorded parent is the dispatcher's own environment before granting the write. Accept
  that it decides things, and design the removal semantics up front.

  OR NO PARENT AT ALL — `--env=` plus a naming convention, `prepare` gaining `--no-switch`,
  and the flat commands that already answer provenance, grouping, lifecycle and reporting.

## Already fixed, because looking at this found it

The `journal switch` line has printed "0 pin(s), 0 open" since 1.34.0 — it counted from a
registry that stopped holding those numbers. Fixed, and a switch now also names the
reminders it just silenced on the environment being left, which is the cheap answer to the
failure that started this whole investigation.

## The three questions for the user

  1  REAL PARENT OR NO PARENT? The middle is out. Which end?
  2  IS THE COLLEAGUE'S ORCHESTRATOR USING `journal delegate`? It did not just print a page
     — it let subagents WRITE. If that is load-bearing, the deletion on this branch is not
     a rename, it is a removed capability, and the rebuild has to land before the deletion
     is published. One question to them settles it.
  3  IS THERE A CASE FLAT CANNOT EXPRESS that has actually come up? Every argument for the
     pointer so far is about legibility after the fact. Nobody has yet named work that
     could not be done without it.
