spike T-134: Claude Code team/transcript internals (pinned to 2.1.148)
test / unit + widget + golden + a11y (push) Failing after 31s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 31s
test / unit + widget + golden + a11y (push) Failing after 31s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 31s
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 <munged>/<sid>/subagents/agent-<hex>.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 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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/<munged>/<sid>.jsonl (lead) and <sid>/subagents/agent-<id>.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/<munged>/; poll mtime and switch the tail when a newer session appears.
|
||||
- Subagent transcripts: <munged>/<session-uuid>/subagents/agent-<id>.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);
|
||||
|
||||
@@ -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/<munged-cwd>/<session-id>/subagents/agent-<hex>.jsonl`,
|
||||
with a sibling `agent-<hex>.meta.json`. `<munged-cwd> = absolutePath.replaceAll('/','-')`
|
||||
(leading `-` kept). Note: a team's `<session-id>` 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/<team>/config.json`)
|
||||
- Top keys: `name`, `description`, `createdAt`, `leadAgentId`, `leadSessionId`, `members[]`.
|
||||
- Member keys: `name`, `agentId` (=`<name>@<team>`), `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-<hex>.jsonl` (records `agentId = <hex>`, `slug = <random>`) +
|
||||
`agent-<hex>.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 `<munged>/<sid>/subagents/agent-<hex>.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.
|
||||
Reference in New Issue
Block a user