From 3f2e5ad9a95a44e2149ae302ab0ae0cdf65e9770 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Fri, 22 May 2026 19:52:00 +0200 Subject: [PATCH] spike T-134: Claude Code team/transcript internals (pinned to 2.1.148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Findings note for the tmux agent-team work (epic T-132), validated from real on-disk artifacts (42 past team configs, real team + subagent transcripts, current-version .meta.json) plus a synthetic control-mode test and the tmux manual — no live team run / quota needed. Key results: teammates get tmux panes (config tmuxPaneId) and write transcripts at //subagents/agent-.jsonl + .meta.json; tmux 3.6a has no %pane-died (use %window-close/%layout-change), and polling list-panes -a is the reliable lifecycle baseline; the real risk is identity linkage — pane/teammate (config: name@team, agentType, %pane) joins to the transcript only via agentType (config <-> .meta.json), ambiguous for same-type teammates, so T-139 needs a joinedAt/mtime or lead-transcript tiebreaker. Co-Authored-By: Claude --- .pql/changelog/ticket_history/2026-05.sql | 3 + .pql/changelog/tickets/2026-05.sql | 21 ++++++ docs/spikes/cc-team-internals-2.1.148.md | 78 +++++++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 docs/spikes/cc-team-internals-2.1.148.md diff --git a/.pql/changelog/ticket_history/2026-05.sql b/.pql/changelog/ticket_history/2026-05.sql index 914fcbab..9f606ab5 100644 --- a/.pql/changelog/ticket_history/2026-05.sql +++ b/.pql/changelog/ticket_history/2026-05.sql @@ -2109,3 +2109,6 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-134', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 16:46:34', '2026-05-22 16:46:34', '2026-05-22 16:46:34', NULL, '5e5f91eab17e4acefcc0c9e286787742', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-135', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 16:46:34', '2026-05-22 16:46:34', '2026-05-22 16:46:34', NULL, '79aed523589f14481c115538ae8b357c', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-133', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 16:46:34', '2026-05-22 16:46:34', '2026-05-22 16:46:34', NULL, 'e11fb2f37bf99262989f1881a7deed9c', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-133', 'status', 'in_progress', 'done', NULL, '2026-05-22 17:37:23', '2026-05-22 17:37:23', '2026-05-22 17:37:23', NULL, '010ab0fda58e4be2ea699c102e1b2695', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-135', 'status', 'in_progress', 'done', NULL, '2026-05-22 17:37:23', '2026-05-22 17:37:23', '2026-05-22 17:37:23', NULL, '1bfd08b75a99bbf3c46408b0c5f799f2', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-136', 'status', 'in_progress', 'done', NULL, '2026-05-22 17:37:23', '2026-05-22 17:37:23', '2026-05-22 17:37:23', NULL, 'eea74c531ac4796fe6dba221018e80dd', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-05.sql b/.pql/changelog/tickets/2026-05.sql index f3208225..346fa841 100644 --- a/.pql/changelog/tickets/2026-05.sql +++ b/.pql/changelog/tickets/2026-05.sql @@ -2448,3 +2448,24 @@ Refinement (2026-05-22, refinement-agent review): READY. Verified against real t - Tail: dart:io File.openRead with a byte-position cursor (File.length + RandomAccessFile.setPosition on poll). Do NOT use lib/src/files/watcher.dart. Zero new deps. - Logger: lib/kernel/src/log.dart for the version drift-guard. Coverage floor 95%.', 'in_progress', 'high', NULL, NULL, NULL, '2026-05-22 15:59:34', '2026-05-22 16:46:34', NULL, '54d91e3d25e042cdf931610197a12f0e', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-133', 'task', 'T-132', 'Author D-75: native transcript rendering + accepted CC-internals coupling', 'Author the D-75 architecture record (id already claimed via pql decisions claim): Claude is rendered natively from transcripts, not the terminal; accept isolated, version-pinned coupling to Claude Code internal contracts (transcript JSONL schema, ~/.claude/teams config, tmux control mode); terminal retained as a general IDE tool only. Record Claude-centric > CLI-first ordering with D-6 surfaces preserved. Acceptance: D-75 written in governance/decisions/architecture.md with Decision/Rationale/Cost/Cross-reference/Raised-by; governance README synced (pql decisions sync) + pql decisions validate ok; cited by T5/T7. Do early — frames the epic.', 'in_progress', 'high', NULL, NULL, NULL, '2026-05-22 15:50:06', '2026-05-22 16:46:34', NULL, '7ebcaf6b78bf011e1b52edcd91a568a4', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-133', 'task', 'T-132', 'Author D-75: native transcript rendering + accepted CC-internals coupling', 'Author the D-75 architecture record (id already claimed via pql decisions claim): Claude is rendered natively from transcripts, not the terminal; accept isolated, version-pinned coupling to Claude Code internal contracts (transcript JSONL schema, ~/.claude/teams config, tmux control mode); terminal retained as a general IDE tool only. Record Claude-centric > CLI-first ordering with D-6 surfaces preserved. Acceptance: D-75 written in governance/decisions/architecture.md with Decision/Rationale/Cost/Cross-reference/Raised-by; governance README synced (pql decisions sync) + pql decisions validate ok; cited by T5/T7. Do early — frames the epic.', 'done', 'high', NULL, NULL, NULL, '2026-05-22 15:50:06', '2026-05-22 17:37:23', NULL, '4b4b15c450f6791a4b41b85ad8817980', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-135', 'task', 'T-132', 'Native text-selection foundation (SelectionArea-compatible widgets)', 'Make clide''s native text widgets selectable so a SelectionArea can select across them. Change lib/widgets/src/clide_markdown.dart RichText (line ~68) -> Text.rich; same for lib/widgets/src/clide_code_block.dart; add a selectionBackground token to SurfaceTokens (lib/kernel/src/theme/tokens.dart + resolver + bundled theme YAMLs); verify copy via existing ClideClipboard (lib/kernel/src/clipboard.dart). Acceptance: a SelectionArea-wrapped column of markdown + code blocks supports select-across-blocks + Ctrl+C copy; widget tests; a11y unaffected; coverage >= floor. Reusable foundation. Blocks T5. + +--- +Refinement (2026-05-22, refinement-agent review): +- Convert ALL RichText sites, not one: clide_markdown.dart lines 68, 238, 242 + clide_code_block.dart:79 -> Text.rich (Text.rich wires the SelectionArea selectionRegistrar; same underlying render object). +- Tables (clide_markdown.dart:230) and tappable WidgetSpan links (ClideTappable ~293-310) do NOT participate in SelectionArea. v1 scope: text + code blocks are the selectable surface; tables/link-spans are known non-selectable islands (revisit later). Acceptance = select+copy across text/code blocks. +- selectionBackground token: add to SurfaceTokens (field + TokenKeys + TokenKeys.all) + resolver.dart (_defaultSurfaceMap default + constructor). Bundled theme YAMLs do NOT need it (default map covers it); per-theme override only if the default reads wrong. Do NOT retune existing palettes (published themes are user contracts). +- Add a selection contrast pair (globalForeground on selectionBackground) to lib/kernel/src/theme/contrast.dart canonicalPairs; re-run test/a11y. (Note: summer-night.yaml is missing from contrast_test bundledPaths -- pre-existing inconsistency, out of scope.) +- COPY PATH CORRECTION: SelectionArea Ctrl+C goes through Flutter services.Clipboard directly, NOT ClideClipboard. Test verifies Clipboard.getData returns the selection after copy. +- Coverage floor is 95%.', 'done', 'high', NULL, NULL, NULL, '2026-05-22 15:59:34', '2026-05-22 17:37:23', NULL, '8f5c2e3763cd186de3d0744f800a7980', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-136', 'task', 'T-132', 'TranscriptReader: tail + parse Claude transcript JSONL', 'New lib/builtin/claude/src/transcript_reader.dart. Resolve munged project path (/ -> -) + session-id; tail the append-only JSONL; parse records (type: user/assistant/attachment/tool; message; agentId; isSidechain) into ConversationItem models; drift-guard on the version field (log + degrade on unknown schema). Paths validated this session: ~/.claude/projects//.jsonl (lead) and /subagents/agent-.jsonl (teammates). Acceptance: unit tests against a snapshotted JSONL fixture (assistant text, tool_use, tool_result); appended lines stream in; unknown version degrades gracefully + logs. Blocks T5. + +--- +Refinement (2026-05-22, refinement-agent review): READY. Verified against real transcripts. +- Munge = absolutePath.replaceAll(''/'',''-'') (KEEP the leading ''-''; dirs start with -var-). +- Envelope fields: type, uuid, parentUuid, isSidechain, sessionId, version, timestamp. Render: user (message.content = String OR array of {type:text|tool_result}); assistant (message.content = array of {type:thinking|text|tool_use}). Skip attachment/system/last-prompt/permission-mode/file-history-snapshot/queue-operation (bookkeeping). +- SESSION-ID: claude does not expose its session UUID via argv/env (verified in claude_pane spawn). Approach: newest *.jsonl by mtime in ~/.claude/projects//; poll mtime and switch the tail when a newer session appears. +- Subagent transcripts: //subagents/agent-.jsonl + sibling .meta.json ({agentType,description}); each record carries agentId. +- Tail: dart:io File.openRead with a byte-position cursor (File.length + RandomAccessFile.setPosition on poll). Do NOT use lib/src/files/watcher.dart. Zero new deps. +- Logger: lib/kernel/src/log.dart for the version drift-guard. Coverage floor 95%.', 'done', 'high', NULL, NULL, NULL, '2026-05-22 15:59:34', '2026-05-22 17:37:23', NULL, 'f68fd700a3487296ac6a00632b050543', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/docs/spikes/cc-team-internals-2.1.148.md b/docs/spikes/cc-team-internals-2.1.148.md new file mode 100644 index 00000000..fc84959c --- /dev/null +++ b/docs/spikes/cc-team-internals-2.1.148.md @@ -0,0 +1,78 @@ +# Spike: Claude Code team / transcript internals (T-134) + +**Pinned to:** claude **2.1.148**, tmux **3.6a**. These are undocumented, version-drifting +internal contracts (per D-75) — re-validate on a CC bump. + +**Method:** validated from real on-disk artifacts (42 past team `config.json`s, real +team + Task-tool subagent transcripts, `.meta.json` written by the current version) plus +a synthetic `tmux -L clide` control-mode test and the tmux manual. No live team run was +needed to answer the questions — the existing artifacts are conclusive and cost no quota. + +--- + +## Findings + +### 1. Teammates get tmux panes; transcripts live under `subagents/` +- **Confirmed:** across real teams, **42 teammate members carry a populated `tmuxPaneId`** + (e.g. `%5`, `%120`); the lead's `tmuxPaneId` is `""`. So teammates spawn as panes and the + config records the pane id. +- **Teammate transcript location:** `~/.claude/projects///subagents/agent-.jsonl`, + with a sibling `agent-.meta.json`. ` = absolutePath.replaceAll('/','-')` + (leading `-` kept). Note: a team's `` dir held **0 top-level `*.jsonl`** and + **44 `subagents/agent-*.jsonl`** — teammate content is the subagent files, not top-level sessions. +- Each subagent record carries `agentId` (the **hex**, e.g. `a2a3530` — matches the filename), + `sessionId` (the dir), `isSidechain: true`, `slug` (a random codename), `type` + (`user`/`assistant`/…). This is the same JSONL schema `TranscriptReader` (T-136) already parses. + +### 2. Lifecycle signal — control mode vs. polling +- **tmux 3.6a control-mode notifications** (from `man tmux`): `%window-add`, `%window-close`, + `%window-pane-changed`, `%layout-change`, `%unlinked-window-add`, `%unlinked-window-close`, + `%session-changed`, `%sessions-changed`, `%pane-mode-changed`, `%exit`, `%output`/`%extended-output`, … + **There is NO `%pane-died`** (an-idea.md assumed one). Pane/teammate exit surfaces via + `%window-close` / `%layout-change` / `%window-pane-changed`. +- **Driving control mode from code is finicky:** a `tmux -L clide -C attach` captured + `%session-changed`/`%exit` but the attach exited early under non-interactive Bash; reliably + consuming the stream needs a long-lived managed client. +- **Polling `tmux -L clide list-panes -a -F '...'` works and is reliable** (validated: it + enumerated panes with pane-id/pid/title). **Recommendation for T-139: use polling as the + baseline lifecycle source**; treat control mode as a later optimization. + +### 3. Team config schema (`~/.claude/teams//config.json`) +- Top keys: `name`, `description`, `createdAt`, `leadAgentId`, `leadSessionId`, `members[]`. +- Member keys: `name`, `agentId` (=`@`), `agentType`, `model`, `cwd`, + `joinedAt`, `subscriptions`, `tmuxPaneId`. (Some runs also carry `backendType`/`isActive`/`mode` — + optional, version-varying.) The lead member has empty `tmuxPaneId`. + +### 4. ⚠️ Identity linkage — the real risk for T-139 +The pane/teammate identity in **config** does NOT share a key with the **transcript file**: +- Config: `{name: gestalt, agentId: gestalt@control-interaction, agentType: gestalt, tmuxPaneId: %120}`. +- Transcript: `agent-.jsonl` (records `agentId = `, `slug = `) + + `agent-.meta.json = {agentType, description}`. +- **The only join key is `agentType`** (config.member.agentType ↔ `.meta.json.agentType`). + This is **unambiguous only when teammates have distinct agentTypes** (e.g. `control-interaction`: + gestalt/ozzie/tyre/…). For **same-type teammates** (e.g. `art-requirements`: 3× `general-purpose`) + agentType is ambiguous → need a disambiguator: spawn order / `joinedAt` timestamp vs. file mtime, + or parse the **lead transcript's** teammate-spawn records (likely carry both ids). **T-139 must + handle this**; recommend: join on agentType, fall back to ordering by `joinedAt`/mtime, and + investigate the lead transcript's spawn events for an explicit hex↔name link. + +### 5. Resolved elsewhere +- **Session-id discovery (check 5):** Claude doesn't expose its session id; pick newest `*.jsonl` + by mtime — already implemented in `TranscriptReader` (T-136). +- **Paste (check 4):** scope to `@path` file references over `send-keys` (text channel). Clipboard + image paste needs an interactive display ($DISPLAY) and is out of scope for headless validation; + decide the composer's image handling in T-138/T-006. + +## Recommendations for the team tickets +- **T-139 (observer):** poll `list-panes -a` for lifecycle; read team `config.json` for the roster + + pane ids; tail teammate transcripts at `//subagents/agent-.jsonl`; resolve + pane→transcript via `agentType` join with a `joinedAt`/mtime tiebreaker (and confirm whether the + lead transcript gives an explicit link). Isolate all of this behind the one module (D-75). +- **Re-validate on any CC version bump** — key off the transcript `version` field. + +## Not done +- A **live, real-time** team run (watching a pane + transcript appear live) was not executed — + the static artifacts answer every question and a live run costs quota without adding certainty. + The one item a fresh run would pin precisely: whether the **lead transcript** records an explicit + teammate hex↔name mapping (would remove the same-type ambiguity). Worth a short observed run when + T-139 is implemented.