From 3621e1af7ba5d632a05a5e371ffeffd4a0cd8bb4 Mon Sep 17 00:00:00 2001 From: John Sell Date: Mon, 23 Mar 2026 15:26:53 -0400 Subject: [PATCH 1/4] =?UTF-8?q?fix(scripts):=20rebrand=20boss-dev=20?= =?UTF-8?q?=E2=86=92=20odis-dev=20in=20spawn-dev-agent.sh=20+=20dev-setup.?= =?UTF-8?q?sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns scripts with CLAUDE.md docs (PR #290). MCP server name changed from boss-dev to odis-dev in the generated JSON config, allowed tools list, and output messages. BOSS_API_TOKEN/BOSS_MCP_URL env vars kept as fallbacks for backward compatibility (prefer ODIS_API_TOKEN/ODIS_MCP_URL going forward). Co-Authored-By: Claude Opus 4.6 --- scripts/dev-setup.sh | 2 +- scripts/spawn-dev-agent.sh | 42 ++++++++++++++++++++------------------ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/scripts/dev-setup.sh b/scripts/dev-setup.sh index 4bec827..e2fd000 100755 --- a/scripts/dev-setup.sh +++ b/scripts/dev-setup.sh @@ -52,7 +52,7 @@ echo " make dev-start DEV_PORT=$DEV_PORT" echo "" echo "==> To register with claude MCP:" echo "" -echo " claude mcp add boss-dev --transport http http://localhost:${DEV_PORT}/mcp" +echo " claude mcp add odis-dev --transport http http://localhost:${DEV_PORT}/mcp" echo "" echo "==> Workflow:" echo " make dev-start # start isolated instance on port $DEV_PORT" diff --git a/scripts/spawn-dev-agent.sh b/scripts/spawn-dev-agent.sh index 4907d75..04eb637 100755 --- a/scripts/spawn-dev-agent.sh +++ b/scripts/spawn-dev-agent.sh @@ -1,14 +1,15 @@ #!/usr/bin/env bash -# spawn-dev-agent.sh — spawn a tmux agent session wired to BOTH boss-mcp and boss-dev. +# spawn-dev-agent.sh — spawn a tmux agent session wired to BOTH odis-mcp and odis-dev. # # Usage: bash scripts/spawn-dev-agent.sh [work-dir] # # The spawned tmux session gets: -# boss-mcp.* — production coordinator (check-in, post_status, tasks, messages) -# boss-dev.* — local dev instance (test API behavior against your branch's code) +# odis-mcp.* — production coordinator (check-in, post_status, tasks, messages) +# odis-dev.* — local dev instance (test API behavior against your branch's code) # -# If BOSS_API_TOKEN is set, it is included as an Authorization header for boss-mcp. -# If BOSS_MCP_URL is unset, defaults to http://localhost:8899/mcp. +# If ODIS_API_TOKEN (or legacy BOSS_API_TOKEN) is set, it is included as an +# Authorization header for odis-mcp. +# If ODIS_MCP_URL (or legacy BOSS_MCP_URL) is unset, defaults to http://localhost:8899/mcp. # # The agent runs in a restart loop — if claude exits it relaunches automatically. @@ -28,7 +29,7 @@ SPACE="$2" WORK_DIR="${3:-$WORKTREE_ROOT}" # ── Config ──────────────────────────────────────────────────────────────────── -BOSS_MCP_URL="${BOSS_MCP_URL:-http://localhost:8899/mcp}" +ODIS_MCP_URL="${ODIS_MCP_URL:-${BOSS_MCP_URL:-http://localhost:8899/mcp}}" DEV_PORT_FILE="$WORKTREE_ROOT/data-dev/boss.port" MCP_CONFIG_FILE="$WORKTREE_ROOT/data-dev/mcp-config-${AGENT_NAME}.json" @@ -53,19 +54,20 @@ echo "==> Dev instance at port ${DEV_PORT} (${DEV_MCP_URL})" # ── Generate MCP config JSON ────────────────────────────────────────────────── mkdir -p "$(dirname "$MCP_CONFIG_FILE")" -if [ -n "${BOSS_API_TOKEN:-}" ]; then - # Include Authorization header for production boss-mcp +API_TOKEN="${ODIS_API_TOKEN:-${BOSS_API_TOKEN:-}}" +if [ -n "${API_TOKEN:-}" ]; then + # Include Authorization header for production odis-mcp cat > "$MCP_CONFIG_FILE" < "$MCP_CONFIG_FILE" < MCP config written to $MCP_CONFIG_FILE" -echo " boss-mcp → $BOSS_MCP_URL" -echo " boss-dev → $DEV_MCP_URL" +echo " odis-mcp → $ODIS_MCP_URL" +echo " odis-dev → $DEV_MCP_URL" # ── Kill existing session if any ────────────────────────────────────────────── if tmux has-session -t "$SESSION_ID" 2>/dev/null; then @@ -112,7 +114,7 @@ sleep 0.3 # The claude command uses --mcp-config for both servers, --strict-mcp-config to # exclude any globally registered servers (clean environment for dev testing). # Wrapped in a restart loop: if claude exits unexpectedly, it relaunches automatically. -ALLOWED_TOOLS="mcp__boss-mcp__post_status,mcp__boss-mcp__check_messages,mcp__boss-mcp__send_message,mcp__boss-mcp__ack_message,mcp__boss-mcp__request_decision,mcp__boss-mcp__create_task,mcp__boss-mcp__list_tasks,mcp__boss-mcp__move_task,mcp__boss-mcp__update_task,mcp__boss-mcp__spawn_agent,mcp__boss-mcp__restart_agent,mcp__boss-mcp__stop_agent,mcp__boss-dev__post_status,mcp__boss-dev__check_messages,mcp__boss-dev__send_message,mcp__boss-dev__ack_message,mcp__boss-dev__request_decision,mcp__boss-dev__create_task,mcp__boss-dev__list_tasks,mcp__boss-dev__move_task,mcp__boss-dev__update_task,mcp__boss-dev__spawn_agent,mcp__boss-dev__restart_agent,mcp__boss-dev__stop_agent" +ALLOWED_TOOLS="mcp__odis-mcp__post_status,mcp__odis-mcp__check_messages,mcp__odis-mcp__send_message,mcp__odis-mcp__ack_message,mcp__odis-mcp__request_decision,mcp__odis-mcp__create_task,mcp__odis-mcp__list_tasks,mcp__odis-mcp__move_task,mcp__odis-mcp__update_task,mcp__odis-mcp__spawn_agent,mcp__odis-mcp__restart_agent,mcp__odis-mcp__stop_agent,mcp__odis-dev__post_status,mcp__odis-dev__check_messages,mcp__odis-dev__send_message,mcp__odis-dev__ack_message,mcp__odis-dev__request_decision,mcp__odis-dev__create_task,mcp__odis-dev__list_tasks,mcp__odis-dev__move_task,mcp__odis-dev__update_task,mcp__odis-dev__spawn_agent,mcp__odis-dev__restart_agent,mcp__odis-dev__stop_agent" CLAUDE_CMD="claude --dangerously-skip-permissions --mcp-config $(printf '%q' "$MCP_CONFIG_FILE") --strict-mcp-config --allowedTools $ALLOWED_TOOLS" RESTART_LOOP="while true; do $CLAUDE_CMD; echo '[spawn-dev-agent] claude exited — restarting in 2s...'; sleep 2; done" @@ -125,8 +127,8 @@ echo " Session: $SESSION_ID" echo " Agent: $AGENT_NAME" echo " Space: $SPACE" echo " Work dir: $WORK_DIR" -echo " boss-mcp: $BOSS_MCP_URL" -echo " boss-dev: $DEV_MCP_URL" +echo " odis-mcp: $ODIS_MCP_URL" +echo " odis-dev: $DEV_MCP_URL" echo "" echo " Attach: tmux attach -t $SESSION_ID" echo " Dev status: make dev-status" From 329e11740bbbcda5288c8a63897eb12c9a6f3c8d Mon Sep 17 00:00:00 2001 From: John Sell Date: Tue, 24 Mar 2026 10:16:52 -0400 Subject: [PATCH 2/4] fix(ui): operator messages appear in agent conversations instead of Your Conversations Root cause: both 'boss' (legacy) and 'operator' agents have agent_type='human' in the DB. operatorName computed returned whichever was found first via Object.entries, which is insertion-order. In spaces where 'boss' was created first, operatorName='boss'. Consequences when operatorName='boss' but messages use 'operator': - Conversations with 'operator' participant went to Agent Conversations section - composeRecipient was null for those conversations (no compose box shown) - Unread acks used wrong agent name Fix: collect ALL human agents into humanAgentNames Set. Use it for: - isOperatorConversation: any participant that is a human agent - composeRecipient: any participant that is NOT a human agent - unreadCount / ackOperatorMessages: any human-agent recipient - operatorName: prefer 'operator' (canonical) over legacy 'boss' Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/ConversationsView.vue | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) 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() { From f3c61624fdd08eb66f75f98ba4d753906690e1da Mon Sep 17 00:00:00 2001 From: John Sell Date: Tue, 24 Mar 2026 10:21:06 -0400 Subject: [PATCH 3/4] fix(messages): canonicalize sender name to prevent duplicate conversation threads When agents use different capitalizations (e.g. "CEO" vs "ceo"), messages were stored with the raw sender name, creating separate conversation threads in the UI for what should be a single conversation. Both handleAgentMessage (HTTP) and send_message (MCP tool) now call resolveAgentName on the sender after the space is loaded, mapping to the canonical stored name if the sender is a known agent. Unknown senders keep their original name unchanged. Co-Authored-By: Claude Sonnet 4.6 --- internal/coordinator/handlers_agent.go | 3 +++ internal/coordinator/mcp_tools.go | 3 +++ 2 files changed, 6 insertions(+) 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{ From 3c42c02520ab300d05961af0d6cd51b94ea084bc Mon Sep 17 00:00:00 2001 From: John Sell Date: Wed, 25 Mar 2026 15:10:13 -0400 Subject: [PATCH 4/4] =?UTF-8?q?spec(collab):=20second=20revision=20pass=20?= =?UTF-8?q?on=20CollaborationProtocol=20=E2=80=94=20TASK-061?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address remaining boss feedback after PR #134 (CLOSED): - Remove [?BOSS] tag syntax from docs/AGENT_PROTOCOL.md questions field example and description; replace with plain language - Replace wall-clock status cadence with milestone-based language in AGENT_PROTOCOL.md (Pattern A work loop + non-tmux checklist) - messaging-protocol.md: remove 'manager can explicitly forbid' qualifier from peer-to-peer section; peer messaging is always allowed, no authorization needed - messaging-protocol.md: reframe escalation to walk up the chain rather than hardcoding a 'boss' message target with 30-minute wall-time rule - organizational-model.md: remove 'manager can restrict' qualifier from information-flow principle; direct messaging is always allowed - ignition-prompts.md: remove 'boss agent channel for boss-level decisions' from hierarchy section; add blocked task status to blocker flow - ignition-prompts.md: soften org chart note — position set by spawning agent, not a warning about org instability - team-formation.md: remove time-based threshold (~30 min); replace with action-based criteria (multiple focused actions = non-trivial); strengthen the default-to-team recommendation Co-Authored-By: Claude Sonnet 4.6 --- .spec/CollaborationProtocol/ignition-prompts.md | 6 +++--- .spec/CollaborationProtocol/messaging-protocol.md | 11 ++++++----- .spec/CollaborationProtocol/organizational-model.md | 2 +- .spec/CollaborationProtocol/team-formation.md | 8 +++++--- docs/AGENT_PROTOCOL.md | 8 ++++---- 5 files changed, 19 insertions(+), 16 deletions(-) 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