Skip to content
Merged
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
22 changes: 17 additions & 5 deletions commands/boss.check.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
STOP. This is a mechanical status sync. Do NOT plan or analyze. Execute these 3 steps literally, then STOP.
STOP. This is a mechanical status sync. Do NOT plan or analyze. Execute these 4 steps literally, then STOP.

**Parse `$ARGUMENTS`:** `$ARGUMENTS` contains two words separated by a space. The FIRST word is your agent name. The SECOND word is the space name. Example: if `$ARGUMENTS` is `Overlord sdk-backend-replacement`, then your agent name is `Overlord` and the space name is `sdk-backend-replacement`.

Expand All @@ -10,14 +10,20 @@ If `$ARGUMENTS` contains only ONE word, it is the space name. Run `tmux display-
curl -s http://localhost:8899/spaces/SPACE_NAME/raw
```

Replace SPACE_NAME with the space name from `$ARGUMENTS`. Scan for anything addressed to you. Do NOT analyze other agents.
Replace SPACE_NAME with the space name from `$ARGUMENTS`. Scan your section for:
- **Messages** — look for a `#### Messages` section under your agent name. These are messages from the boss or other agents. Note any instructions or questions.
- **Standing orders** — anything addressed to you in shared contracts.

**Important rule**: Always use `curl`, never use Fetch tool. Fetch will *not* work on localhost. **Always** use curl. This is important!
Do NOT analyze other agents' sections.

**Important rule**: Always use `curl`, never use Fetch tool. Fetch will *not* work on localhost. **Always** use curl. This is important!

## Step 2: Write your status JSON and POST it

Create `/tmp/boss_checkin.json` reflecting your CURRENT state. Do not change your work — just report what you are doing right now.

If you found messages in Step 1, acknowledge them in your `items` array (e.g. `"Received message from boss: <summary>"`).

```bash
cat > /tmp/boss_checkin.json << 'CHECKIN'
{
Expand Down Expand Up @@ -47,6 +53,12 @@ curl -s -X POST http://localhost:8899/spaces/SPACE_NAME/agent/AGENT_NAME \

Replace SPACE_NAME and AGENT_NAME with the values from `$ARGUMENTS`. You MUST see `accepted for` in the response. If you do not, something is wrong — retry once.

## Step 3: STOP
## Step 3: Act on messages

If you found messages in Step 1 that contain instructions or task assignments, begin working on them now. If a message asks a question, answer it in your next status update.

If there were no messages, or messages were purely informational, skip this step.

## Step 4: STOP

Do not start any work. Do not analyze the blackboard. Do not make plans. STOP HERE.
If you had no actionable messages, STOP HERE. Do not start any work. Do not analyze the blackboard. Do not make plans.
9 changes: 9 additions & 0 deletions commands/boss.ignite.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,12 @@ Using the protocol and template from Step 2, post your initial status to your ch
- **Tag questions with `[?BOSS]`** when you need the human to make a decision.
- **Post to your own channel only** — the server rejects cross-channel posts.
- **Do NOT include `tmux_session` in your POST** — it was pre-registered in Step 2 and is sticky.
- **Check for messages** — when you read `/raw`, look for a `#### Messages` section under your agent name. These are messages from the boss or other agents sent via the dashboard. Acknowledge them in your next status POST and act on any instructions.
- **Send messages to other agents** — to message another agent, POST to their message endpoint:
```bash
curl -s -X POST http://localhost:8899/spaces/SPACE_NAME/agent/OTHER_AGENT/message \
-H 'Content-Type: application/json' \
-H 'X-Agent-Name: YOUR_NAME' \
-d '{"message": "your message here"}'
```
The message will appear in their `#### Messages` section on the next check-in.
4 changes: 3 additions & 1 deletion internal/coordinator/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Space: `{SPACE}`
|--------|---------|
| Post (JSON) | `curl -s -X POST http://localhost:8899/spaces/{SPACE}/agent/{name} -H 'Content-Type: application/json' -H 'X-Agent-Name: {name}' -d '{"status":"...","summary":"...","items":[...]}'` |
| Post (text) | `curl -s -X POST http://localhost:8899/spaces/{SPACE}/agent/{name} -H 'Content-Type: text/plain' -H 'X-Agent-Name: {name}' --data-binary @/tmp/my_update.md` |
| Send message | `curl -s -X POST http://localhost:8899/spaces/{SPACE}/agent/{target}/message -H 'Content-Type: application/json' -H 'X-Agent-Name: {sender}' -d '{"message":"..."}'` |
| Read section | `curl -s http://localhost:8899/spaces/{SPACE}/agent/{name}` |
| Read full doc | `curl -s http://localhost:8899/spaces/{SPACE}/raw` |
| Browser | `http://localhost:8899/spaces/{SPACE}/` (polls every 3s) |
Expand All @@ -28,7 +29,8 @@ Space: `{SPACE}`

> **IMPORTANT: `repo_url` is REQUIRED in your first POST.** Without it, PR links in the dashboard are broken. Find it with `git remote get-url origin` and include it as `"repo_url": "https://..."`. You only need to send it once — the server remembers it.
8. **Register your tmux session.** Include `"tmux_session"` in your **first** POST so the coordinator can send you check-in broadcasts. Find your session name with `tmux display-message -p '#S'`. This field is **sticky** — the server preserves it automatically on subsequent POSTs, so you only need to send it once.
9. **Model economy.** Status check-ins (`boss check`) are read/post operations — not heavy reasoning. Use a lightweight model (e.g. Haiku) for check-ins, then switch back to your working model (e.g. Opus) for real work. The broadcast script handles this automatically via `/model` switching.
9. **Check your messages.** When you read `/raw`, look for a `#### Messages` section under your agent name. These are messages from the boss or other agents. Acknowledge them in your next status POST and act on any instructions. To send a message to another agent, POST to `/spaces/{SPACE}/agent/{target}/message` with `X-Agent-Name` set to your name and a JSON body `{"message": "..."}`.
10. **Model economy.** Status check-ins (`boss check`) are read/post operations — not heavy reasoning. Use a lightweight model (e.g. Haiku) for check-ins, then switch back to your working model (e.g. Opus) for real work. The broadcast script handles this automatically via `/model` switching.

### JSON Format Reference

Expand Down
84 changes: 51 additions & 33 deletions internal/coordinator/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,20 @@ type sseClient struct {
}

type Server struct {
port string
dataDir string
spaces map[string]*KnowledgeSpace
mu sync.RWMutex
httpServer *http.Server
running bool
runMu sync.Mutex
EventLog []string
eventMu sync.Mutex
stopLiveness chan struct{}
sseClients map[*sseClient]struct{}
sseMu sync.Mutex
interrupts *InterruptLedger
approvalTracked map[string]time.Time
port string
dataDir string
spaces map[string]*KnowledgeSpace
mu sync.RWMutex
httpServer *http.Server
running bool
runMu sync.Mutex
EventLog []string
eventMu sync.Mutex
stopLiveness chan struct{}
sseClients map[*sseClient]struct{}
sseMu sync.Mutex
interrupts *InterruptLedger
approvalTracked map[string]time.Time
}

func NewServer(port, dataDir string) *Server {
Expand Down Expand Up @@ -275,7 +275,6 @@ func (s *Server) getOrCreateSpace(name string) *KnowledgeSpace {
return ks
}


func (s *Server) getSpace(name string) (*KnowledgeSpace, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
Expand Down Expand Up @@ -702,6 +701,14 @@ func (s *Server) handleSpaceAgent(w http.ResponseWriter, r *http.Request, spaceN
if update.RepoURL == "" && existing.RepoURL != "" {
update.RepoURL = existing.RepoURL
}
// Preserve messages — agents don't include them in updates
if len(update.Messages) == 0 && len(existing.Messages) > 0 {
update.Messages = existing.Messages
}
// Preserve documents — managed via the /agent/{name}/{slug} endpoint
if len(update.Documents) == 0 && len(existing.Documents) > 0 {
update.Documents = existing.Documents
}
}
ks.Agents[canonical] = &update
ks.UpdatedAt = time.Now().UTC()
Expand Down Expand Up @@ -753,7 +760,7 @@ func (s *Server) handleAgentMessage(w http.ResponseWriter, r *http.Request, spac
}

agentName = strings.TrimRight(agentName, "/")

// Sender authentication - require X-Agent-Name header
senderName := r.Header.Get("X-Agent-Name")
if senderName == "" {
Expand Down Expand Up @@ -793,7 +800,7 @@ func (s *Server) handleAgentMessage(w http.ResponseWriter, r *http.Request, spac
}

canonical := resolveAgentName(ks, agentName)

s.mu.Lock()
agent, exists := ks.Agents[canonical]
if !exists {
Expand All @@ -812,12 +819,12 @@ func (s *Server) handleAgentMessage(w http.ResponseWriter, r *http.Request, spac
agent.Messages = []AgentMessage{}
}
agent.Messages = append(agent.Messages, messageReq)

// Limit message history to last 50 messages
if len(agent.Messages) > 50 {
agent.Messages = agent.Messages[len(agent.Messages)-50:]
}

ks.UpdatedAt = time.Now().UTC()
if err := s.saveSpace(ks); err != nil {
s.mu.Unlock()
Expand All @@ -827,7 +834,7 @@ func (s *Server) handleAgentMessage(w http.ResponseWriter, r *http.Request, spac
s.mu.Unlock()

// Log the message event
s.logEvent(fmt.Sprintf("[%s/%s] Message from %s: %s", spaceName, canonical, senderName,
s.logEvent(fmt.Sprintf("[%s/%s] Message from %s: %s", spaceName, canonical, senderName,
func() string {
if len(messageReq.Message) > 50 {
return messageReq.Message[:47] + "..."
Expand All @@ -837,25 +844,25 @@ func (s *Server) handleAgentMessage(w http.ResponseWriter, r *http.Request, spac

// Broadcast SSE event for real-time updates
sseData, _ := json.Marshal(map[string]interface{}{
"space": spaceName,
"agent": canonical,
"sender": senderName,
"space": spaceName,
"agent": canonical,
"sender": senderName,
"message": messageReq.Message,
})
s.broadcastSSE(spaceName, "agent_message", string(sseData))

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "delivered",
"status": "delivered",
"messageId": messageReq.ID,
"recipient": canonical,
})
}

func (s *Server) handleAgentDocument(w http.ResponseWriter, r *http.Request, spaceName, agentName, documentSlug string) {
agentName = strings.TrimRight(agentName, "/")

// Agent name enforcement - ensure X-Agent-Name header matches for writes
if r.Method == http.MethodPost || r.Method == http.MethodPut {
callerName := r.Header.Get("X-Agent-Name")
Expand All @@ -868,7 +875,7 @@ func (s *Server) handleAgentDocument(w http.ResponseWriter, r *http.Request, spa
return
}
}

// Sanitize document slug
if !regexp.MustCompile(`^[a-zA-Z0-9_-]+$`).MatchString(documentSlug) {
http.Error(w, "invalid document slug: must be alphanumeric with underscores and dashes only", http.StatusBadRequest)
Expand Down Expand Up @@ -922,18 +929,18 @@ func (s *Server) handleAgentDocument(w http.ResponseWriter, r *http.Request, spa
// Update agent's documents list in the knowledge space
ks := s.getOrCreateSpace(spaceName)
canonical := resolveAgentName(ks, agentName)

s.mu.Lock()
if ks.Agents[canonical] == nil {
ks.Agents[canonical] = &AgentUpdate{
Status: StatusActive,
Summary: "Document uploaded",
Status: StatusActive,
Summary: "Document uploaded",
UpdatedAt: time.Now().UTC(),
}
}

agent := ks.Agents[canonical]

// Add or update document in the list
found := false
for i, doc := range agent.Documents {
Expand All @@ -950,10 +957,10 @@ func (s *Server) handleAgentDocument(w http.ResponseWriter, r *http.Request, spa
Content: string(content),
})
}

agent.UpdatedAt = time.Now().UTC()
ks.UpdatedAt = time.Now().UTC()

if err := s.saveSpace(ks); err != nil {
s.mu.Unlock()
http.Error(w, fmt.Sprintf("save space: %v", err), http.StatusInternalServerError)
Expand Down Expand Up @@ -1064,6 +1071,7 @@ func (s *Server) handleIgnition(w http.ResponseWriter, r *http.Request, spaceNam
} else {
b.WriteString("5. **Register your tmux session.** Include `\"tmux_session\"` in your first POST. Find it with `tmux display-message -p '#S'`. It is sticky — you only need to send it once.\n")
}
b.WriteString(fmt.Sprintf("6. **Check your messages.** When you read `/raw`, look for a `#### Messages` section under your agent name. These are messages from the boss or other agents. Acknowledge them in your status POST and act on any instructions. To send a message to another agent: `curl -s -X POST http://localhost%s/spaces/%s/agent/{target}/message -H 'Content-Type: application/json' -H 'X-Agent-Name: %s' -d '{\"message\": \"...\"}'`\n", s.port, spaceName, agentName))
b.WriteString("\n")

b.WriteString("## Peer Agents\n\n")
Expand Down Expand Up @@ -1097,6 +1105,16 @@ func (s *Server) handleIgnition(w http.ResponseWriter, r *http.Request, spaceNam
b.WriteString(fmt.Sprintf("- Next steps: %s\n", existing.NextSteps))
}
b.WriteString("\n")

if len(existing.Messages) > 0 {
b.WriteString("## Pending Messages\n\n")
b.WriteString("**You have unread messages. Read and act on them.**\n\n")
for _, msg := range existing.Messages {
b.WriteString(fmt.Sprintf("- **%s** (%s): %s\n",
msg.Sender, msg.Timestamp.Format("15:04"), msg.Message))
}
b.WriteString("\n")
}
}

b.WriteString("## JSON Post Template\n\n")
Expand Down
Loading