fix: Show conflict resolution agents in HQ dashboard

getHeadquartersDashboard had no section for active conflict agents,
so initiatives with a running conflict-* agent disappeared from all
HQ sections. Add resolvingConflicts array to surface them.
This commit is contained in:
Lukas May
2026-03-06 16:39:48 +01:00
parent 02ca1d568e
commit 1e16ad82e8
8 changed files with 278 additions and 2 deletions

View File

@@ -108,6 +108,7 @@ describe('getHeadquartersDashboard', () => {
expect(result.pendingReviewInitiatives).toEqual([]);
expect(result.pendingReviewPhases).toEqual([]);
expect(result.planningInitiatives).toEqual([]);
expect(result.resolvingConflicts).toEqual([]);
expect(result.blockedPhases).toEqual([]);
});
@@ -291,6 +292,115 @@ describe('getHeadquartersDashboard', () => {
expect(item.lastMessage).toBeNull();
});
it('resolvingConflicts — running conflict agent appears', async () => {
const agents = new MockAgentManager();
const ctx = makeCtx(agents);
const initiativeRepo = ctx.initiativeRepository!;
const initiative = await initiativeRepo.create({ name: 'Conflicting Init', status: 'active' });
agents.addAgent({
id: 'agent-conflict',
name: 'conflict-1234567890',
status: 'running',
initiativeId: initiative.id,
userDismissedAt: null,
updatedAt: new Date('2025-06-01T12:00:00Z'),
});
const caller = createCaller(ctx);
const result = await caller.getHeadquartersDashboard();
expect(result.resolvingConflicts).toHaveLength(1);
const item = result.resolvingConflicts[0];
expect(item.initiativeId).toBe(initiative.id);
expect(item.initiativeName).toBe('Conflicting Init');
expect(item.agentId).toBe('agent-conflict');
expect(item.agentName).toBe('conflict-1234567890');
expect(item.agentStatus).toBe('running');
});
it('resolvingConflicts — waiting_for_input conflict agent appears', async () => {
const agents = new MockAgentManager();
const ctx = makeCtx(agents);
const initiativeRepo = ctx.initiativeRepository!;
const initiative = await initiativeRepo.create({ name: 'Conflicting Init', status: 'active' });
agents.addAgent({
id: 'agent-conflict',
name: 'conflict-1234567890',
status: 'waiting_for_input',
initiativeId: initiative.id,
userDismissedAt: null,
updatedAt: new Date('2025-06-01T12:00:00Z'),
});
const caller = createCaller(ctx);
const result = await caller.getHeadquartersDashboard();
expect(result.resolvingConflicts).toHaveLength(1);
expect(result.resolvingConflicts[0].agentStatus).toBe('waiting_for_input');
});
it('resolvingConflicts — dismissed conflict agent is excluded', async () => {
const agents = new MockAgentManager();
const ctx = makeCtx(agents);
const initiativeRepo = ctx.initiativeRepository!;
const initiative = await initiativeRepo.create({ name: 'Conflicting Init', status: 'active' });
agents.addAgent({
id: 'agent-conflict',
name: 'conflict-1234567890',
status: 'running',
initiativeId: initiative.id,
userDismissedAt: new Date(),
});
const caller = createCaller(ctx);
const result = await caller.getHeadquartersDashboard();
expect(result.resolvingConflicts).toEqual([]);
});
it('resolvingConflicts — idle conflict agent is excluded', async () => {
const agents = new MockAgentManager();
const ctx = makeCtx(agents);
const initiativeRepo = ctx.initiativeRepository!;
const initiative = await initiativeRepo.create({ name: 'Conflicting Init', status: 'active' });
agents.addAgent({
id: 'agent-conflict',
name: 'conflict-1234567890',
status: 'idle',
initiativeId: initiative.id,
userDismissedAt: null,
});
const caller = createCaller(ctx);
const result = await caller.getHeadquartersDashboard();
expect(result.resolvingConflicts).toEqual([]);
});
it('resolvingConflicts — non-conflict agent is excluded', async () => {
const agents = new MockAgentManager();
const ctx = makeCtx(agents);
const initiativeRepo = ctx.initiativeRepository!;
const initiative = await initiativeRepo.create({ name: 'Some Init', status: 'active' });
agents.addAgent({
id: 'agent-regular',
name: 'regular-agent',
status: 'running',
initiativeId: initiative.id,
userDismissedAt: null,
});
const caller = createCaller(ctx);
const result = await caller.getHeadquartersDashboard();
expect(result.resolvingConflicts).toEqual([]);
});
it('ordering — waitingForInput sorted oldest first', async () => {
const agents = new MockAgentManager();
const ctx = makeCtx(agents);

View File

@@ -145,7 +145,40 @@ export function headquartersProcedures(publicProcedure: ProcedureBuilder) {
planningInitiatives.sort((a, b) => a.since.localeCompare(b.since));
// -----------------------------------------------------------------------
// Section 4: blockedPhases
// Section 4: resolvingConflicts
// -----------------------------------------------------------------------
const resolvingConflicts: Array<{
initiativeId: string;
initiativeName: string;
agentId: string;
agentName: string;
agentStatus: string;
since: string;
}> = [];
for (const agent of activeAgents) {
if (
agent.name?.startsWith('conflict-') &&
(agent.status === 'running' || agent.status === 'waiting_for_input') &&
agent.initiativeId
) {
const initiative = initiativeMap.get(agent.initiativeId);
if (initiative) {
resolvingConflicts.push({
initiativeId: initiative.id,
initiativeName: initiative.name,
agentId: agent.id,
agentName: agent.name,
agentStatus: agent.status,
since: agent.updatedAt.toISOString(),
});
}
}
}
resolvingConflicts.sort((a, b) => a.since.localeCompare(b.since));
// -----------------------------------------------------------------------
// Section 5: blockedPhases
// -----------------------------------------------------------------------
const blockedPhases: Array<{
initiativeId: string;
@@ -207,6 +240,7 @@ export function headquartersProcedures(publicProcedure: ProcedureBuilder) {
pendingReviewInitiatives,
pendingReviewPhases,
planningInitiatives,
resolvingConflicts,
blockedPhases,
};
}),