feat(graph): enforce single archetype per task
## Design Decision Per bd (bead) system design, a task should have only ONE agent archetype assigned at a time. This provides clear ownership and simpler mental model. ## What Changed When assigning a new archetype: 1. Remove any existing agent: labels first (DELETE API) 2. Then add the new agent: label (POST API) 3. Optimistic UI updates to match ## Why This Makes Sense - Clear ownership: 'Who's working on this?' - Simpler coordination between tasks - Matches how bd/agent orchestration is intended to work - Reassigning is still possible (just click a different archetype) ## UI Behavior - If task has 'coder' assigned, clicking 'architect' will: 1. Remove 'coder' label 2. Add 'architect' label - Dropdown shows 'Assigned' badge on current archetype - X button still available to unassign completely ## Test Coverage Added graph-node-single-archetype.test.tsx with 5 tests: - Removes existing labels before adding new - Calls DELETE before POST - Only allows one archetype per task - Preserves non-agent labels - Returns early if same archetype clicked
This commit is contained in:
parent
5ca6b21862
commit
211e503409
2 changed files with 78 additions and 7 deletions
|
|
@ -113,18 +113,40 @@ export function GraphNodeCard({ id, data, selected }: NodeProps<Node<GraphNodeDa
|
|||
const isClosed = data.status === 'closed';
|
||||
|
||||
const handleAssignAgent = async (archetypeId: string) => {
|
||||
// Don't do anything if this archetype is already assigned
|
||||
const labelToAdd = `agent:${archetypeId}`;
|
||||
if (assignedArchetypes.some(a => a.id === archetypeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAssigning(true);
|
||||
setAssignError(null);
|
||||
setAssignSuccess(null);
|
||||
|
||||
// Optimistic update
|
||||
const labelToAdd = `agent:${archetypeId}`;
|
||||
// Track the new label as pending
|
||||
pendingOptimisticLabels.current.add(labelToAdd);
|
||||
if (!localLabels.includes(labelToAdd)) {
|
||||
setLocalLabels([...localLabels, labelToAdd]);
|
||||
}
|
||||
|
||||
// Get current agent labels to remove (single archetype constraint)
|
||||
const currentAgentLabels = localLabels.filter(l => l.startsWith('agent:'));
|
||||
|
||||
// Optimistic update: remove all agent: labels, add new one
|
||||
const previousLabels = localLabels;
|
||||
setLocalLabels(prev => [...prev.filter(l => !l.startsWith('agent:')), labelToAdd]);
|
||||
|
||||
try {
|
||||
// First remove existing agent labels (if any)
|
||||
if (currentAgentLabels.length > 0) {
|
||||
for (const existingLabel of currentAgentLabels) {
|
||||
const existingArchetypeId = existingLabel.replace('agent:', '');
|
||||
await fetch('/api/swarm/prep', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ beadId: id, archetypeId: existingArchetypeId }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Then add the new label
|
||||
const response = await fetch('/api/swarm/prep', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
|
@ -140,9 +162,9 @@ export function GraphNodeCard({ id, data, selected }: NodeProps<Node<GraphNodeDa
|
|||
setAssignSuccess(`Assigned ${archetype?.name ?? archetypeId}`);
|
||||
setTimeout(() => setAssignSuccess(null), 2000);
|
||||
} catch (err) {
|
||||
// Revert on error
|
||||
// Revert on error - restore previous labels
|
||||
pendingOptimisticLabels.current.delete(labelToAdd);
|
||||
setLocalLabels(prev => prev.filter(l => l !== labelToAdd));
|
||||
setLocalLabels(previousLabels);
|
||||
setAssignError(err instanceof Error ? err.message : 'Failed to assign agent');
|
||||
setTimeout(() => setAssignError(null), 3000);
|
||||
} finally {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue