Files
Codewalkers/apps/server/trpc/routers/initiative-activity.ts
Lukas May e3246baf51 feat: Show resolving_conflict activity state on initiative cards
Add 'resolving_conflict' to InitiativeActivityState and detect active
conflict agents (name starts with conflict-) in deriveInitiativeActivity.
Conflict resolution takes priority over pending_review since the agent
is actively working.

- Add resolving_conflict to shared types and activity derivation
- Include conflict agents in listInitiatives agent filter (name + mode)
- Map resolving_conflict to urgent variant with pulse in InitiativeCard
- Add merge: prefix to INITIATIVE_LIST_RULES for merge event routing
- Add spawnConflictResolutionAgent to INVALIDATION_MAP
- Add getActiveConflictAgent to detail page agent: SSE invalidation
2026-03-06 13:32:37 +01:00

94 lines
2.9 KiB
TypeScript

/**
* Initiative Activity — derives current activity state from initiative + phases.
*/
import type { Initiative, Phase } from '../../db/schema.js';
import type { InitiativeActivity, InitiativeActivityState } from '@codewalk-district/shared';
export interface ActiveArchitectAgent {
initiativeId: string;
mode: string;
status: string;
name?: string;
}
const MODE_TO_STATE: Record<string, InitiativeActivityState> = {
discuss: 'discussing',
plan: 'detailing',
detail: 'detailing',
refine: 'refining',
};
export function deriveInitiativeActivity(
initiative: Initiative,
phases: Phase[],
activeArchitectAgents?: ActiveArchitectAgent[],
): InitiativeActivity {
const phasesTotal = phases.length;
const phasesCompleted = phases.filter(p => p.status === 'completed').length;
const base = { phasesTotal, phasesCompleted };
if (initiative.status === 'archived') {
return { ...base, state: 'archived' };
}
// Check for active conflict resolution agent — takes priority over pending_review
// because the agent is actively working to resolve merge conflicts
const conflictAgent = activeArchitectAgents?.find(
a => a.initiativeId === initiative.id
&& a.name?.startsWith('conflict-')
&& (a.status === 'running' || a.status === 'waiting_for_input'),
);
if (conflictAgent) {
return { ...base, state: 'resolving_conflict' };
}
if (initiative.status === 'pending_review') {
return { ...base, state: 'pending_review' };
}
if (initiative.status === 'completed') {
return { ...base, state: 'complete' };
}
// Check for active architect agents BEFORE zero-phases check
// so architect agents (discuss/plan/detail/refine) surface activity
const activeAgent = activeArchitectAgents?.find(
a => a.initiativeId === initiative.id
&& !a.name?.startsWith('conflict-')
&& (a.status === 'running' || a.status === 'waiting_for_input'),
);
if (activeAgent) {
const state = MODE_TO_STATE[activeAgent.mode] ?? 'detailing';
return { ...base, state };
}
if (phasesTotal === 0) {
return { ...base, state: 'idle' };
}
// Priority-ordered state detection (first match wins)
const priorities: Array<{ status: Phase['status']; state: InitiativeActivityState }> = [
{ status: 'pending_review', state: 'pending_review' },
{ status: 'in_progress', state: 'executing' },
{ status: 'blocked', state: 'blocked' },
];
for (const { status, state } of priorities) {
const match = phases.find(p => p.status === status);
if (match) {
return { ...base, state, activePhase: { id: match.id, name: match.name } };
}
}
if (phasesCompleted === phasesTotal) {
return { ...base, state: 'complete' };
}
const approved = phases.find(p => p.status === 'approved');
if (approved) {
return { ...base, state: 'ready', activePhase: { id: approved.id, name: approved.name } };
}
return { ...base, state: 'planning' };
}