mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 22:49:41 +02:00
fix(subagent-watcher): discover workflow-nested agents + harden meta→transcript upgrade
Two follow-ups to db93491 (the 2026-06 CC meta.json format change), after
reverse-engineering the new on-disk layout with a live current-CC subagent +
1Hz fs poller:
(1) Workflow recursion — the Workflow tool nests its agents at
subagents/workflows/{wf}/agent-{id}.jsonl, one level below the flat
subagents/ scan, so they were never tracked. Add watchWorkflowDirs()
(driven from scanForSubagents) to descend and watch each workflow dir
(idempotent; fs.watch recursive is unsupported on Linux, so the ~5s
periodic scan re-drives it — same latency as new-session discovery).
Require the `agent-` prefix in the flat readdir + watch callback so a
workflow dir's sibling journal.jsonl can't register a bogus "journal" agent.
E2E verified against real ~/.claude/projects: 32 workflow-nested agents
discovered (wf_fa35c1d8-4a9), 0 bogus journal agents.
(2) Transcript timing — empirically the per-agent .jsonl IS written at the
standard subagents/ path and grows incrementally (tailable); the
/tmp/.../tasks/<id>.output the prior probe found is just a symlink back to
it. meta.json lands at spawn, the .jsonl a beat later. Add a meta→transcript
upgrade in registerAgentFile: when an agent registered meta-only gets its
sibling .jsonl, re-point filePath, drop the stale sidecar context, start
tailing, and emit subagent:updated (not a duplicate discovered). Corrects the
now-inaccurate "no transcript to tail" doc comment on registerAgentMeta.
Tests: 2 new cases (workflow-nested discovery; journal.jsonl not registered).
All 56 pass; tsc/lint/format clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -475,6 +475,65 @@ describe('SubagentWatcher', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workflow-nested subagents (subagents/workflows/{wf}/)', () => {
|
||||
// The Workflow tool nests its agents one level deeper than TUI Task subagents:
|
||||
// subagents/workflows/{workflowId}/agent-{id}.jsonl — plus a sibling journal.jsonl.
|
||||
// The flat subagents/ scan misses them; watchWorkflowDirs() descends one level.
|
||||
function mockWorkflowLayout(wfFiles: string[]) {
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
const p = String(path);
|
||||
if (p.endsWith('wf_test')) return wfFiles;
|
||||
if (p.endsWith('workflows')) return ['wf_test'];
|
||||
if (p.endsWith('subagents')) return ['workflows']; // no flat agents, just the workflows dir
|
||||
if (p.includes('session1')) return ['subagents'];
|
||||
if (p.includes('project1')) return ['session1'];
|
||||
return ['project1'];
|
||||
});
|
||||
mockStatSync.mockReturnValue({
|
||||
isDirectory: () => true,
|
||||
birthtime: new Date(),
|
||||
mtime: new Date(),
|
||||
size: 100,
|
||||
});
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
return mockRl;
|
||||
}
|
||||
|
||||
it('should discover an agent nested under subagents/workflows/{wf}/', async () => {
|
||||
mockExistsSync.mockReturnValue(true); // sibling .jsonl exists for the meta
|
||||
const mockRl = mockWorkflowLayout(['agent-wf1.jsonl', 'agent-wf1.meta.json', 'journal.jsonl']);
|
||||
|
||||
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('wf1');
|
||||
expect(info.filePath.endsWith('.jsonl')).toBe(true);
|
||||
expect(info.filePath).toContain('workflows');
|
||||
});
|
||||
|
||||
it('should NOT register a workflow dir journal.jsonl as a bogus agent', async () => {
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
const mockRl = mockWorkflowLayout(['agent-wf1.jsonl', 'journal.jsonl']);
|
||||
|
||||
watcher.start();
|
||||
await flushAsyncScan();
|
||||
mockRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
const agents = watcher.getSubagents();
|
||||
expect(agents).toHaveLength(1); // only agent-wf1, never "journal"
|
||||
expect(agents.every((a) => a.agentId !== 'journal')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Status Lifecycle', () => {
|
||||
it('should start agents as active', async () => {
|
||||
const mockRl = createMockRl();
|
||||
|
||||
Reference in New Issue
Block a user