mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 23:49:41 +02:00
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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user