fix(subagent-watcher): discover subagents via agent-*.meta.json (CC format change)

Claude Code changed its subagent on-disk format (~2026-06-14): TUI Task
subagents now write `agent-{id}.meta.json` ({agentType,description,toolUseId})
into the session's `subagents/` dir and no longer reliably write a per-agent
`agent-{id}.jsonl` transcript there. The watcher discovered agents ONLY by
`.jsonl`, so it tracked zero — subagent windows and the monitor's "N TRACKED"
showed nothing.

- Add `registerAgentMeta()`: discover from the meta sidecar (description from
  meta.description/agentType), prefer a sibling `.jsonl` transcript when present
  (richer), never tail a meta file.
- Initial scan + directory watcher now handle `.meta.json` alongside `.jsonl`.
- Tests: 2 new cases (meta-only discovery; prefer-.jsonl-when-present).
  Verified e2e against a real ~/.claude/projects fixture.

Known follow-ups (not in scope): meta-only agents have no per-agent transcript
to tail (no live tool-call feed, status stays 'active'); workflow agents under
`subagents/workflows/{wf}/agent-*.jsonl` are still missed by the flat scan.

Also adds the README screenshot tooling used to surface this:
- capture-real-overview.mjs: DSF=2 + ?nowebgl crisp path (DOM renderer avoids
  the WebGL glyph-doubling at deviceScaleFactor>1).
- capture-readme-real.mjs: real-instance desktop-scene capture (dashboard/
  monitor/subagent) for an isolated beta seeded from prod settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-06-15 01:24:30 +02:00
parent b7ff54b2ec
commit db93491dd1
4 changed files with 373 additions and 5 deletions
+71
View File
@@ -404,6 +404,77 @@ describe('SubagentWatcher', () => {
});
});
describe('Meta-sidecar discovery (2026-06 Claude Code format)', () => {
it('should discover a subagent from agent-{id}.meta.json when no .jsonl exists', async () => {
// New format: TUI Task subagents write only a meta sidecar (no per-agent .jsonl).
mockExistsSync.mockImplementation((p: string) => !String(p).endsWith('.jsonl'));
mockReaddirSync.mockImplementation((path: string) => {
if (path.includes('subagents')) return ['agent-meta1.meta.json'];
if (path.includes('session1')) return ['subagents'];
if (path.includes('project1')) return ['session1'];
return ['project1'];
});
mockStatSync.mockReturnValue({
isDirectory: () => true,
birthtime: new Date(),
mtime: new Date(),
size: 120,
});
mockReadFileSync.mockReturnValue(
JSON.stringify({ agentType: 'general-purpose', description: 'Audit server.js', toolUseId: 'toolu_x' })
);
const discoveredHandler = vi.fn();
watcher.on('subagent:discovered', discoveredHandler);
watcher.start();
await flushAsyncScan();
expect(discoveredHandler).toHaveBeenCalled();
const info = discoveredHandler.mock.calls[0][0] as SubagentInfo;
expect(info.agentId).toBe('meta1');
expect(info.description).toBe('Audit server.js');
expect(info.status).toBe('active');
expect(watcher.getSubagents()).toHaveLength(1);
});
it('should prefer the real .jsonl transcript when one exists alongside the meta', async () => {
// Both sidecar and transcript present → defer to the richer .jsonl path.
mockExistsSync.mockReturnValue(true); // sibling agent-both.jsonl exists
mockReaddirSync.mockImplementation((path: string) => {
if (path.includes('subagents')) return ['agent-both.meta.json'];
if (path.includes('session1')) return ['subagents'];
if (path.includes('project1')) return ['session1'];
return ['project1'];
});
mockStatSync.mockReturnValue({
isDirectory: () => true,
birthtime: new Date(),
mtime: new Date(),
size: 100,
});
// Mirror the passing discovery tests: leave createReadStream unmocked so
// _resolveDescription fails gracefully (undefined) and discovery still fires.
mockReadFileSync.mockReturnValue('');
const mockRl = createMockRl();
mockCreateInterface.mockReturnValue(mockRl);
const discoveredHandler = vi.fn();
watcher.on('subagent:discovered', discoveredHandler);
watcher.start();
await flushAsyncScan();
mockRl.emit('close');
await vi.advanceTimersByTimeAsync(100);
expect(discoveredHandler).toHaveBeenCalled();
const info = discoveredHandler.mock.calls[0][0] as SubagentInfo;
expect(info.agentId).toBe('both');
// filePath points at the transcript, not the sidecar.
expect(info.filePath.endsWith('.jsonl')).toBe(true);
});
});
describe('Status Lifecycle', () => {
it('should start agents as active', async () => {
const mockRl = createMockRl();