Skip to content

list: nest spawned lanes, and publish where each lane's chat lives - #93

Merged
JulienMartel merged 2 commits into
mainfrom
worktree-omit-child-lanes
Aug 29, 2026
Merged

list: nest spawned lanes, and publish where each lane's chat lives#93
JulienMartel merged 2 commits into
mainfrom
worktree-omit-child-lanes

Conversation

@JulienMartel

Copy link
Copy Markdown
Contributor

What

Two things, both about lanes that were spawned from another lane's pane.

  • scruff --json gains chat — the checkout whose conversation scruff <name> opens. Equal to path when the lane holds its own chat, the parent's path when it doesn't, and "" when scruff genuinely cannot tell. It reaches all five SDKs and ai/SKILL.md.
  • The table nests instead of dropping. A spawned lane is drawn └ <name> under the lane that spawned it, with the marker explained once beneath the table.

Why

A picker wants to hide lanes with no pane of their own — a scruff child checkout has no window, no panel and no transcript, so "opening" one starts a client where no conversation lives. parent cannot answer that: a lane opened with ⌘↵ from inside another lane's pane records the same parentage a scruff child does (new.go:192 and create.go:86 both write the spawning cwd), and that one is a full lane. chat is the honest test, and it is measured rather than inferred.

chat is deliberately not chatHome's answer. resume must always name a directory, so for codex/opencode/pi it falls back to the parent — a good guess when the next step is exec-ing a picker, and a lie when published: every such lane would read "no chat of my own" and disappear from a picker filtering on it. jsonChat publishes an answer only for a client scruff can actually probe, and "" otherwise, which every consumer must read as show it.

The listing nests rather than filters for the opposite reason: a spawned lane carries its own branch and its own PR, in another repo, and closing the parent's pane does not reap it. Once that pane is gone this listing is the only place the branch surfaces. Hiding it everywhere at once is how work goes missing.

Also: chatHome now answers a plain lane (parent == its own main checkout) before the cross-repo compare, which spent two rev-parse calls proving what the parent pointer already said. The listing asks this of every lane.

Verify

make check                      # 200/200 bats, go test, vet, fmt
./scruff                        # spawned lanes indented under their parents
./scruff --json | jq '.lanes[] | {name, path, chat}'

All five SDK suites run green locally (bun, pytest, go test, cargo test, swift test).

Watch out

  • Consumer contract: "" means undetermined, never "no chat". A consumer that reads it as falsy hides every lane on a machine that doesn't run Claude. Pinned by list --json: chat is EMPTY, not a guess….
  • chat is derived per call and flips from the parent's path to the lane's own the first time an agent leaves a conversation in that checkout. watch will emit one changed event for that — a real change in the lane, not noise, and noted in SPEC §2.2.
  • occupied_by is still absent from all five SDK Lane types. Pre-existing, deliberately left alone here.
  • The consumer this was built for is haus's Lanes picker (hausfold/haus, same lane name), which degrades safely against any scruff that predates the field.

🤖 Generated with Claude Code

https://claude.ai/code/session_011T9Lb6mUXWEudUcs2E1B9X

JulienMartel and others added 2 commits August 29, 2026 09:11
…re its chat is

Two facts a consumer of the listing could not get before.

`chat` is the checkout whose conversation `scruff <name>` opens: the lane's
own path when it has a chat, the parent's when it doesn't. That is the honest
test for "this lane has no pane of its own", and it is the one a picker must
filter on. `parent` cannot answer it — a lane opened with ⌘↵ from inside
another lane's pane is parented to that lane exactly as a `scruff child` is,
and it has a window, a panel and a chat. An empty value means undetermined and
must be read as "show it".

The table nests instead of dropping. A spawned lane carries its own branch and
its own PR, in another repo, and closing the parent's pane does not reap it —
this listing is the only place that branch surfaces once the pane is gone. So
the answer to "it is noise" is where the row sits, not whether it exists.

chatHome now answers a plain lane (parent == its own main checkout) before the
cross-repo compare, which spent two git invocations proving what the parent
pointer already said. The listing asks this of every lane.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011T9Lb6mUXWEudUcs2E1B9X
…y it

Follow-ups from the pre-PR assurance pass, all on the field this change
exists for.

`chat` is no longer `chatHome`'s answer. resume must always name a directory,
so for a client whose transcripts scruff cannot probe it falls back to the
parent — right when the next step is exec-ing a picker, and a lie when
published: every codex or opencode lane opened from inside another pane would
have answered "no chat of my own" and vanished from any picker that filtered
on it, window and agent and all. jsonChat publishes the answer only where
agentProbeable says one exists, and "" everywhere else. That is the value both
docs already promised and nothing could produce.

The table's legend claimed a nested lane "has no pane of its own", which is
false for exactly the case above. It now says what parentage actually means.

`chat` reaches all five SDKs, optional in the four that can express it so an
older CLI still decodes — nil and "" are the same "could not tell". ai/SKILL.md
gains the field and the rule, since an agent on a machine with no checkout
reads only that. docs/reference.md's `parent` row said `scruff child` was the
only thing that sets it; a lane opened inside a pane sets it too, and that is
the whole reason `parent` cannot answer this question.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011T9Lb6mUXWEudUcs2E1B9X
@JulienMartel
JulienMartel force-pushed the worktree-omit-child-lanes branch from fe831a7 to d61ef36 Compare August 29, 2026 14:18
@JulienMartel
JulienMartel merged commit fea441f into main Aug 29, 2026
4 checks passed
@JulienMartel
JulienMartel deleted the worktree-omit-child-lanes branch August 29, 2026 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant