diff --git a/.spec/CollaborationProtocol/ignition-prompts.md b/.spec/CollaborationProtocol/ignition-prompts.md index 0bff613..0f4d10e 100644 --- a/.spec/CollaborationProtocol/ignition-prompts.md +++ b/.spec/CollaborationProtocol/ignition-prompts.md @@ -51,8 +51,8 @@ You are part of a multi-agent team. Follow these rules: **Hierarchy** - You report to: {parent_agent} (or boss if no parent) - Send status updates to your manager via message when significant progress happens -- Message your manager directly when blocked; message the boss agent channel for boss-level decisions -- Escalate to boss only after manager unresponsive for 30+ minutes +- Message your manager when blocked; set the task to `blocked` status +- Escalate up the chain if your manager is unresponsive **Your Role** - Your role is defined by the agent that spawned you — check your ignition message for specifics @@ -72,7 +72,7 @@ You → {parent} → {grandparent} → boss Peers (same manager): {peer1}, {peer2} Your team (if manager): {child1}, {child2} -Note: The org structure may change as work evolves. Check your messages for hierarchy updates. +Note: Your position in the team is set by whoever spawned you. Check your ignition message for specifics. ``` ### Section: Work Loop diff --git a/.spec/CollaborationProtocol/messaging-protocol.md b/.spec/CollaborationProtocol/messaging-protocol.md index 8511e42..fb1424f 100644 --- a/.spec/CollaborationProtocol/messaging-protocol.md +++ b/.spec/CollaborationProtocol/messaging-protocol.md @@ -63,8 +63,8 @@ the Escalation section below. ### 4. Peer-to-Peer Coordination -Agents may message any peer directly — no manager authorization is required. Peer communication -is the default; a manager can explicitly forbid a specific interaction if needed. +Agents may message any peer directly — no authorization required. Direct peer messaging is always +allowed. ``` DevA sends message to DevB: @@ -76,10 +76,11 @@ update so the manager has visibility. ### 5. Escalation -If work is blocked and the manager is unresponsive for >30 minutes: +If work is blocked and the manager is unresponsive after multiple check cycles, escalate up the +chain — message the manager's manager (or the operator if no higher level exists): ``` -Agent sends message to boss: - "ESCALATION: TASK-{id} blocked on {blocker}. Manager {ManagerName} unresponsive for 30+ min." +Agent sends message to {ManagersManager}: + "TASK-{id} blocked on {blocker}. {ManagerName} has not responded. Requesting escalation." ``` ## Message Discipline diff --git a/.spec/CollaborationProtocol/organizational-model.md b/.spec/CollaborationProtocol/organizational-model.md index 638ecda..030bca1 100644 --- a/.spec/CollaborationProtocol/organizational-model.md +++ b/.spec/CollaborationProtocol/organizational-model.md @@ -115,7 +115,7 @@ Every task must have: an assignee, a parent (or be a root task), and a status th - Agents report status up (via messages and status updates) - Managers send decisions down (via task assignment and messages) -- Peers coordinate laterally (via direct messages — allowed by default; manager can restrict specific interactions as an exception) +- Peers coordinate laterally (via direct messages — always allowed, no authorization required) ### 5. Context at the edge diff --git a/.spec/CollaborationProtocol/team-formation.md b/.spec/CollaborationProtocol/team-formation.md index fa49245..97e4e88 100644 --- a/.spec/CollaborationProtocol/team-formation.md +++ b/.spec/CollaborationProtocol/team-formation.md @@ -18,10 +18,12 @@ meets **any** of these criteria: - Has more than one acceptance criterion - Is a planning, spec, or design task (always non-trivial) - Would benefit from an independent review pass -- Estimated effort exceeds ~30 minutes of focused work +- Could benefit from a second perspective or independent review +- Requires more than one focused action to complete (e.g. research then implement, or implement then test) -Solo work is appropriate only for tightly scoped leaf tasks: a single bug fix, a one-file -documentation update, or a clearly-specified implementation with no design decisions. +Solo work is appropriate only for atomic leaf tasks: a single, completely specified change with +no decisions to make and no more than one deliverable. When in doubt, form a team — the cost of +an extra agent is far lower than the cost of a solo agent going off in the wrong direction. ## Required Team Roles diff --git a/docs/AGENT_PROTOCOL.md b/docs/AGENT_PROTOCOL.md index b0330f7..94d09d1 100644 --- a/docs/AGENT_PROTOCOL.md +++ b/docs/AGENT_PROTOCOL.md @@ -136,7 +136,7 @@ X-Agent-Name: {agent} } ], "next_steps": "open PR after tests pass", - "questions": ["[?BOSS] should we use approach X or Y?"], + "questions": ["should we use approach X or Y?"], "blockers": ["waiting for DataMgr to merge PR #7"], "parent": "ManagerAgent", "role": "Developer", @@ -158,7 +158,7 @@ X-Agent-Name: {agent} | `items` | array | no | Bullet points shown in dashboard | | `sections` | array | no | Titled sub-sections with item lists | | `next_steps` | string | no | What you will do next | -| `questions` | array | no | Auto-tagged `[?BOSS]` in dashboard | +| `questions` | array | no | Questions surfaced in the dashboard for the human operator | | `blockers` | array | no | Highlighted in dashboard | | `parent` | string | no | Manager agent name — sticky hierarchy link | | `role` | string | no | Display label e.g. `"Developer"`, `"SME"` | @@ -568,7 +568,7 @@ X-Agent-Boss-Agent: {agent} 4. GET /agent/{agent}/events → open SSE stream (blocking) ↕ (in parallel) 5. Do work -6. POST /agent/{agent} → status update every ~10 minutes +6. POST /agent/{agent} → status update at meaningful milestones 7. On SSE message event → act immediately 8. POST /agent/{agent} → status: done when finished ``` @@ -691,7 +691,7 @@ Non-tmux agents (Docker, CI, remote, script) interact with the coordinator exact 1. **Register** with `agent_type` set to your runtime (`"http"`, `"docker"`, `"script"`, etc.) 2. **Set `heartbeat_interval_sec`** if you want staleness detection (recommended: 60) -3. **Post status updates** at least every 10 minutes during active work +3. **Post status updates** at meaningful milestones (task complete, blocker hit, PR opened, etc.) 4. **Send heartbeats** at your registered interval 5. **Subscribe to SSE** or poll `/messages` for incoming instructions 6. **Post `"status": "done"`** when finished diff --git a/frontend/src/components/ConversationsView.vue b/frontend/src/components/ConversationsView.vue index ce3cc38..b0e99d3 100644 --- a/frontend/src/components/ConversationsView.vue +++ b/frontend/src/components/ConversationsView.vue @@ -24,11 +24,22 @@ const props = defineProps<{ preselectAgent?: string }>() -// Derive the operator name from agent_type — avoids hardcoding 'boss' string. -// Fallback to 'operator' while the space loads. +// Collect ALL human agents (spaces may have both 'boss' legacy and 'operator' canonical). +// Using a Set lets isOperatorConversation match any of them for categorization. +const humanAgentNames = computed(() => + new Set( + Object.entries(props.space.agents ?? {}) + .filter(([, a]) => a.agent_type === 'human') + .map(([name]) => name), + ), +) + +// Canonical operator name used for composing messages. +// Prefer 'operator' (the current canonical name) over legacy 'boss' if both exist. const operatorName = computed(() => { - const entry = Object.entries(props.space.agents ?? {}).find(([, a]) => a.agent_type === 'human') - return entry?.[0] ?? 'operator' + if (humanAgentNames.value.has('operator')) return 'operator' + const [first] = humanAgentNames.value + return first ?? 'operator' }) interface ConversationMessage { @@ -273,15 +284,15 @@ const selectedConversation = computed((): Conversation | null => { const readKeys = ref(new Set()) function isOperatorConversation(conv: Conversation): boolean { - return conv.participants.includes(operatorName.value) + return conv.participants.some(p => humanAgentNames.value.has(p)) } function unreadCount(conv: Conversation): number { // Agent-to-agent conversations never show unread badges if (!isOperatorConversation(conv)) return 0 if (readKeys.value.has(conv.key)) return 0 - // Only count messages directed at the operator that haven't been acknowledged on the backend - return conv.messages.filter(m => m.recipient === operatorName.value && !m.read).length + // Count messages directed at ANY human agent that haven't been acknowledged on the backend + return conv.messages.filter(m => humanAgentNames.value.has(m.recipient) && !m.read).length } // ACK all unread messages to the operator in a conversation so the backend persists read state. @@ -289,9 +300,9 @@ function unreadCount(conv: Conversation): number { // the conversation stays read after navigate-away + return. function ackOperatorMessages(conv: Conversation) { if (!isOperatorConversation(conv)) return - const unread = conv.messages.filter(m => m.recipient === operatorName.value && !m.read) + const unread = conv.messages.filter(m => humanAgentNames.value.has(m.recipient) && !m.read) for (const msg of unread) { - api.ackMessage(props.space.name, operatorName.value, msg.id, operatorName.value).catch(() => {}) + api.ackMessage(props.space.name, msg.recipient, msg.id, msg.recipient).catch(() => {}) } } @@ -518,12 +529,12 @@ const inlineSending = ref(false) const inlineSendError = ref(null) const composeRef = ref(null) -// Operator can compose to the other participant (only if operator is in the conversation) +// Operator can compose to the other participant (only if a human agent is in the conversation) const composeRecipient = computed(() => { if (!selectedConversation.value) return null const { participants } = selectedConversation.value - if (!participants.includes(operatorName.value)) return null - return participants.find(p => p !== operatorName.value) ?? null + if (!participants.some(p => humanAgentNames.value.has(p))) return null + return participants.find(p => !humanAgentNames.value.has(p)) ?? null }) async function sendInlineCompose() { diff --git a/internal/coordinator/handlers_agent.go b/internal/coordinator/handlers_agent.go index 0a1e269..8b37298 100644 --- a/internal/coordinator/handlers_agent.go +++ b/internal/coordinator/handlers_agent.go @@ -386,6 +386,9 @@ func (s *Server) handleAgentMessage(w http.ResponseWriter, r *http.Request, spac s.mu.Lock() canonical := resolveAgentName(ks, agentName) + // Canonicalize sender name so "Cto" and "cto" resolve to the same conversation thread. + senderName = resolveAgentName(ks, senderName) + messageReq.Sender = senderName var recipients []string if scope == "subtree" { diff --git a/internal/coordinator/mcp_tools.go b/internal/coordinator/mcp_tools.go index b601562..476cd8e 100644 --- a/internal/coordinator/mcp_tools.go +++ b/internal/coordinator/mcp_tools.go @@ -407,6 +407,9 @@ func (s *Server) addToolSendMessage(srv *mcp.Server) { s.mu.Lock() canonical := resolveAgentName(ks, targetName) + // Canonicalize sender so "Cto" and "cto" produce the same conversation thread. + senderName = resolveAgentName(ks, senderName) + msgReq.Sender = senderName ag := ks.agentStatus(canonical) if ag == nil { ag = &AgentUpdate{