Repository navigation
Read Claude Code sessions as first-class peers in the Agents roster #84
Description
Proposal
Depends on fleet-instance-presence. This adds a second source to the presence model
that change defines. It does not alter that model's transport, and it adds no new one.
Why
The human runs Claude Code and opencode-skein side by side, all day, across worktrees of
the same repos. fleet-instance-presence makes skein instances visible to each other and
solved half the problem. The other half is that roughly half the running agents are not
skein instances, and to the roster they do not exist. An /agents view that omits them
answers "what is everything doing" wrongly, which is worse than not answering.
The work needed here is small, because Claude Code already publishes exactly this data
locally. Verified on 2026-09-11 against Claude Code v2.1.268:
claude agents --jsonprints every live session — interactive and background — as
{pid, cwd, kind, startedAt, sessionId, name, status}, with--alland--cwd <path>.
It is a supported, documented CLI surface and does not require a TTY.- The same data is on disk at
~/.claude/sessions/<pid>.json, which additionally carries
version,peerProtocol,peerFeatures,messagingSocketPath,entrypoint,
statusUpdatedAtand the session's displayname.
So this change is an adapter, not an integration. Read the CLI, normalize, merge.
What Changes
1. Owner widens from a literal to a union
packages/opencode/src/agent/presence.ts defines
Owner = Schema.Literal("opencode-skein"). It becomes
Schema.Literal("opencode-skein", "claude-code").
This is the load-bearing edit. Everything downstream — /agents, the TUI view, skein's Go
aggregator — already keys off owner, so widening it is what makes a Claude session a
peer rather than a special case. Consumers that cannot handle a foreign owner must be
found and fixed as part of this change, not discovered later.
2. A Claude Code presence source
A new source module reads claude agents --json (preferring the CLI over the on-disk
registry; the registry is the fallback when the binary is absent) and maps each session
into the existing Info record:
Info field |
Source | Note |
|---|---|---|
owner |
constant | "claude-code" |
instanceID |
pid |
one process per session, unlike skein |
sessionID |
sessionId |
a UUID, not ses_… — see task 1.2 |
directory |
cwd |
|
status |
status |
idle maps to idle; absent means busy |
lastEventAt |
statusUpdatedAt, else startedAt |
|
heartbeatAt |
file mtime / poll time | |
canPrompt/canBtw/canAbort |
false for now |
raised by claude-peer-messaging |
agent/provider/model/loop* |
unset | Claude does not publish these |
Fields Claude does not publish stay absent, never guessed. A roster that invents a
model name is worse than one that admits it does not know.
The session's name is carried through as the display name. This matters more than it
looks: the human's /fanout workflow names sessions <slug>-<role>, so the name is
frequently the only thing that says what a peer is actually doing.
3. Freshness, not liveness
Claude sessions are discovered by polling, so a record is a snapshot. Each record carries
its observation time, and a record whose source process is gone is reported unreachable
— the same distinction fleet-instance-presence already draws between a crashed instance
and a cleanly exited one. Claude's registry is keyed by pid, so liveness is a cheap
kill(pid, 0)-equivalent check rather than a timeout.
4. Work identity via specsync topology
A directory is not a unit of work. Where specsync is available, its
topology -json (specsync change agent-topology-command) is read and each peer is
annotated with the change slug, tracker issue and branch matched by worktree path — for
skein sessions and Claude sessions alike, since the join is on directory, not on owner.
This is the payoff: /agents stops being a list of directories and becomes a list of
who is working on which change. Absent specsync, peers still list with directories.
5. Scope
Local machine only. Claude's registry and sockets are per-user and per-host; there is
nothing to discover over mDNS, so this source is not advertised to the network and does
not extend fleet-instance-presence's network surface.
Capabilities
MODIFIED Capabilities
agent-presence: gains a second owner and a local, poll-based source for it.
Non-Goals
- No messaging, prompting, or control. Read-only. Every control capability is
publishedfalse. Talking to a Claude peer isclaude-peer-messaging, deliberately
separate because it depends on a private protocol and this does not. - No writes into
~/.claude/. Nothing in this change creates, edits or deletes a file
under Claude Code's state directory. - No reading Claude transcripts, prompts or tool output. Metadata only — the same
content-suppression rulefleet-instance-presencealready enforces, and it is tested
the same way. - No claiming or mutual exclusion. Seeing a peer on a slug is not reserving it.
- Not a Claude Code plugin, hook, or MCP server. skein reads a public CLI; Claude Code is
not modified and need not know skein exists.
Risks
claude agents --jsonis a CLI contract that can change. Mitigation: parse
defensively, treat unknown fields as ignorable and missing fields as absent, and fail
the source (not the roster) on a shape that no longer matches. A broken Claude source
must degrade to "no Claude peers", never to a broken/agents.- Polling cost. Mitigation: poll on roster read with a short cache, not on a timer.
- Path disclosure. Claude
cwdvalues are absolute paths, already the same class of
datafleet-instance-presencepublishes. Because this source is local-only and never
advertised, it does not widen that exposure.
Impact
- Changed:
packages/opencode/src/agent/presence.ts(Owner), and every consumer that
assumed a single owner. - New: a Claude source module under
packages/opencode/src/agent/, a topology reader, and
their tests. /agentsgains an owner column and a slug column.
Disposition (2026-09-18)
Shipped. Shipped. 3.1/3.2 (specsync topology annotation) descoped: specsync topology does not exist; directory + branch are already on every peer.
Tasks
Phase 0: Verify the source contract
- 0.1 Re-verify
claude agents --jsonoutput shape against the installed version- Record the version checked; the shape is a CLI contract, not a spec
- Validation: a captured fixture committed as test data, with the version noted
- Done 2026-09-17: re-checked against 2.1.274 (was 2.1.261 at spike time) — shape
unchanged. Fixture intest/agent/presence-claude.test.ts.
- 0.2 Confirm
statussemantics — specifically that an absentstatusmeans busy- Validation: observe a session mid-turn and a session at rest
- Done 2026-09-17: live check via
presence-claude.tsagainst this machine's real,
running sessions — matchedclaude agents --json1:1 (15/15).
Phase 1: Widen the model
- 1.1
OwnerbecomesSchema.Literal("opencode-skein", "claude-code")- Validation:
bun typecheck— zero errors - Done:
Schema.Literals(["opencode-skein", "claude-code"])inagent/presence.ts
(this repo's Effect version has no variadicSchema.Literal).
- Validation:
- 1.2 Decide how a Claude UUID satisfies
sessionID: SessionID- Either widen the schema or carry a source-qualified id; do not coerce a UUID into
ases_-shaped string - Validation: a Claude record round-trips through the schema unchanged
- Done:
Info.sessionIDwidened toSchema.String— presenceInfois a metadata
projection, not the canonical session identity.
- Either widen the schema or carry a source-qualified id; do not coerce a UUID into
- 1.3 Find and fix every consumer that assumed one owner
- Validation: grep for the literal;
/agents, the TUI view and the Go aggregator each
render a foreign owner without special-casing - Done: only real in-repo consumer was
handlers/agents.ts, which hardcodes
owner: "opencode-skein"for its own records and simply appends Claude records
separately — no special-casing needed. No TUI/Go consumer ofAgentPresence.Info
exists in this repo (greppedpackages/*— only the handler, the schema group,
the SDK's generated types, and tests reference it).
- Validation: grep for the literal;
Phase 2: The source
- 2.1 Claude source module: run
claude agents --json, parse, map toInfo- Validation: fixture-driven unit tests, including unknown extra fields
- Done:
src/agent/presence-claude.ts,test/agent/presence-claude.test.ts.
- 2.2 Fall back to reading
~/.claude/sessions/*.jsonwhen the binary is absent- Validation: test with the binary unavailable and the directory present
- Done:
fetchViaRegistryinpresence-claude.ts, tried whenwhich("claude")is
null or the CLI call fails.
- 2.3 Absent fields stay absent; no invented
model,agentorprovider- Validation: test asserts the mapped record has no fabricated fields
- 2.4 Control capabilities are published
false- Validation: test asserts
canPrompt/canBtw/canAbortare all false
- Validation: test asserts
- 2.5 Never read any file under
~/.claude/other than the session registry, and
never read*.key- Validation: test asserts the read set; a peer token must never enter the process
- Done:
fetchViaRegistryfilters to*.jsononly;*.keyreads live exclusively
inpeer/claude/registry.ts(outbound messaging), never in this module.
- 2.6 Dead pids report
unreachablerather than disappearing- Validation: test with a registry entry whose pid does not exist
- 2.7 A malformed or failing source yields zero Claude peers, never a failed roster
- Validation: tests for non-JSON output, non-zero exit, and a changed shape
Phase 3: Work identity
- 3.1 Read
specsync topology -jsonand annotate peers by worktree path- Applies to both owners; the join is on directory
- Validation: a skein peer and a Claude peer in sibling worktrees of one change both
resolve to the same slug - Deferred: not part of this pass; the source and the
/agentsmerge work stands
on its own without it.
- 3.2 Degrade silently when specsync is absent or older than
topology- Validation: peers still list with directories and no slug; no error surfaces
- Deferred, same as 3.1.
Phase 4: Surface
- 4.1
/agentsgains owner and slug columns; Claude peers appear inline, not in a
separate section- Validation: manual check with live Claude and skein sessions running
- Done for the owner column (Claude peers are appended into the same array the
agents.listHTTP endpoint returns,owner: "claude-code"set). No "slug" column
yet — that's Phase 3, deferred.
- 4.2 Cache the poll for the roster read path
- Validation: repeated reads within the window spawn one subprocess
- Done: 2s TTL cache in
presence-claude.ts, shared between the presence
projection and target resolution (peer/claude/resolve.ts) so a burst of either
kind of call spawns at most oneclaudeprocess.
- 4.3 Config switch to disable the Claude source
- Validation: disabled, no Claude peers appear and nothing else changes
- Done:
OPENCODE_DISABLE_CLAUDE_CODE_PEER_SOURCEineffect/runtime-flags.ts.
Phase 5: Verify
- 5.1 Run with the human's real working set — several Claude sessions across
worktrees plus a skein instance- Validation: the roster matches
claude agents --jsonplus skein's own sessions,
with no duplicates and no missing rows - Done 2026-09-17:
listClaudePeersagainst this machine's 15 real, live Claude
sessions matchedclaude agents --json's own count and pids exactly.
- Validation: the roster matches
Unchecked items above: see Disposition in proposal.md (2026-09-18).
Plan changes
16 done
Activity
feat(peers): discover Claude Code sessions alongside opencode peers, …
androidand commentedon Sep 17, 2026
Archived 2026-09-18 — see the change's Disposition section.
feat(peers): discover Claude Code sessions alongside opencode peers, …
feat(peers): discover Claude Code sessions alongside opencode peers, …