From 05e65fbde073eb5f9a87fd47bb576012a1ed78a7 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:40:27 -0700 Subject: [PATCH] fix(agent-status): restore inventory-confirmed workflow lanes --- src/main/claude/hook-settings.ts | 4 ++-- src/shared/claude-subagent-roster.test.ts | 20 ++++++++++++++++++++ src/shared/claude-subagent-roster.ts | 3 +++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/claude/hook-settings.ts b/src/main/claude/hook-settings.ts index 1059a9a0945..b13c98ed6a9 100644 --- a/src/main/claude/hook-settings.ts +++ b/src/main/claude/hook-settings.ts @@ -35,8 +35,8 @@ export const CLAUDE_EVENTS = [ { eventName: 'StopFailure', definition: { hooks: [{ type: 'command', command: '' }] } }, // Why: subagent/teammate lifecycle feeds the sidebar's child rows and keeps // a pane 'working' while background children outlive the lead's turn. - // TeammateIdle retires the working-only row when SubagentStop is lost; - // idle teammates still report status "running" in Stop's background_tasks. + // TeammateIdle parks turn-based teammates without trusting their permanently + // "running" background_tasks entry to gate the pane. // Older Claude builds ignore unregistered event names (StopFailure precedent). { eventName: 'SubagentStart', definition: { hooks: [{ type: 'command', command: '' }] } }, { eventName: 'SubagentStop', definition: { hooks: [{ type: 'command', command: '' }] } }, diff --git a/src/shared/claude-subagent-roster.test.ts b/src/shared/claude-subagent-roster.test.ts index a3f620a7c27..4c5502309f4 100644 --- a/src/shared/claude-subagent-roster.test.ts +++ b/src/shared/claude-subagent-roster.test.ts @@ -65,6 +65,26 @@ describe('claude-subagent-roster', () => { expect(roster.has('alane-hooks-6d3cb5b5')).toBe(false) }) + it('restores a parked workflow lane to working when the inventory reports it running', () => { + const roster: ClaudeSubagentRoster = new Map() + upsertWorkingClaudeSubagent(roster, 'alane-hooks-6d3cb5b5', { agentType: 'lane-hooks' }, 100) + stopClaudeSubagent(roster, 'alane-hooks-6d3cb5b5') + + // Why: lifecycle hooks and the lead Stop inventory can arrive around the + // same boundary; an authoritative running task must keep the pane gated. + foldClaudeBackgroundTasksIntoRoster( + roster, + [task({ id: 'alane-hooks-6d3cb5b5', agentType: 'lane-hooks' })], + 150 + ) + + expect(roster.get('alane-hooks-6d3cb5b5')).toMatchObject({ + state: 'working', + listedAsSubagentTask: true + }) + expect(claudeRosterHasWorkingSubagent(roster)).toBe(true) + }) + it('revives an idle teammate as working while keeping its first-observed startedAt', () => { const roster: ClaudeSubagentRoster = new Map() upsertWorkingClaudeSubagent(roster, 'aprobe1-6d3cb5b5', { agentType: 'probe1' }, 100) diff --git a/src/shared/claude-subagent-roster.ts b/src/shared/claude-subagent-roster.ts index dbb9c50899e..421bddf4cc4 100644 --- a/src/shared/claude-subagent-roster.ts +++ b/src/shared/claude-subagent-roster.ts @@ -221,6 +221,9 @@ export function foldClaudeBackgroundTasksIntoRoster( pendingRunningTasks.delete(task.id) continue } + // Why: a Stop can park the row before the lead inventory confirms the + // same workflow lane is still running; the authoritative task wins. + existing.state = 'working' existing.agentType = task.agentType ?? existing.agentType existing.description = task.description ?? existing.description existing.listedAsSubagentTask = true