Skip to content

Read Claude Code sessions as first-class peers in the Agents roster #84

Closed

Description

@androidand
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 --json prints every live session — interactive and background — as
    {pid, cwd, kind, startedAt, sessionId, name, status}, with --all and --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,
    statusUpdatedAt and the session's display name.

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
    published false. Talking to a Claude peer is claude-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 rule fleet-instance-presence already 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 --json is 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 cwd values are absolute paths, already the same class of
    data fleet-instance-presence publishes. 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.
  • /agents gains 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 --json output 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 in test/agent/presence-claude.test.ts.
  • 0.2 Confirm status semantics — specifically that an absent status means busy
    • Validation: observe a session mid-turn and a session at rest
    • Done 2026-09-17: live check via presence-claude.ts against this machine's real,
      running sessions — matched claude agents --json 1:1 (15/15).

Phase 1: Widen the model

  • 1.1 Owner becomes Schema.Literal("opencode-skein", "claude-code")
    • Validation: bun typecheck — zero errors
    • Done: Schema.Literals(["opencode-skein", "claude-code"]) in agent/presence.ts
      (this repo's Effect version has no variadic Schema.Literal).
  • 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
      a ses_-shaped string
    • Validation: a Claude record round-trips through the schema unchanged
    • Done: Info.sessionID widened to Schema.String — presence Info is a metadata
      projection, not the canonical session identity.
  • 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 of AgentPresence.Info
      exists in this repo (grepped packages/* — only the handler, the schema group,
      the SDK's generated types, and tests reference it).

Phase 2: The source

  • 2.1 Claude source module: run claude agents --json, parse, map to Info
    • 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/*.json when the binary is absent
    • Validation: test with the binary unavailable and the directory present
    • Done: fetchViaRegistry in presence-claude.ts, tried when which("claude") is
      null or the CLI call fails.
  • 2.3 Absent fields stay absent; no invented model, agent or provider
    • Validation: test asserts the mapped record has no fabricated fields
  • 2.4 Control capabilities are published false
    • Validation: test asserts canPrompt/canBtw/canAbort are all false
  • 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: fetchViaRegistry filters to *.json only; *.key reads live exclusively
      in peer/claude/registry.ts (outbound messaging), never in this module.
  • 2.6 Dead pids report unreachable rather 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 -json and 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 /agents merge 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 /agents gains 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.list HTTP 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 one claude process.
  • 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_SOURCE in effect/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 --json plus skein's own sessions,
      with no duplicates and no missing rows
    • Done 2026-09-17: listClaudePeers against this machine's 15 real, live Claude
      sessions matched claude agents --json's own count and pids exactly.

Unchecked items above: see Disposition in proposal.md (2026-09-18).

Plan changes

16 done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions