Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .spec/CollaborationProtocol/ignition-prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
11 changes: 6 additions & 5 deletions .spec/CollaborationProtocol/messaging-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .spec/CollaborationProtocol/organizational-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 5 additions & 3 deletions .spec/CollaborationProtocol/team-formation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions docs/AGENT_PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"` |
Expand Down Expand Up @@ -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
```
Expand Down Expand Up @@ -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
Expand Down
35 changes: 23 additions & 12 deletions frontend/src/components/ConversationsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -273,25 +284,25 @@ const selectedConversation = computed((): Conversation | null => {
const readKeys = ref(new Set<string>())

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.
// This clears the sidebar badge (which reads msg.read from live space data) and ensures
// 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(() => {})
}
}

Expand Down Expand Up @@ -518,12 +529,12 @@ const inlineSending = ref(false)
const inlineSendError = ref<string | null>(null)
const composeRef = ref<HTMLTextAreaElement | null>(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() {
Expand Down
3 changes: 3 additions & 0 deletions internal/coordinator/handlers_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
3 changes: 3 additions & 0 deletions internal/coordinator/mcp_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
Loading