From b51bbf7e72641969e704401b74ee83455f44c6aa Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 6 Mar 2026 08:54:44 +0100 Subject: [PATCH] working on a different approach for roles --- OUTPUT_PROTOCOL.md | 2 ++ claude/agent/anvillm-agent.md | 2 ++ internal/backend/tmux/session.go | 15 ++++++++ internal/p9/roles.go | 20 +++++++++++ internal/p9/server.go | 29 +++++++++++++++- internal/supervisor/supervisor.go | 50 ++++++++++++++++++--------- roles/developer.md | 21 +++++------ skills/anvillm-communication/SKILL.md | 1 - skills/anvillm-sessions/SKILL.md | 4 +-- skills/beads/SKILL.md | 14 ++------ 10 files changed, 114 insertions(+), 44 deletions(-) diff --git a/OUTPUT_PROTOCOL.md b/OUTPUT_PROTOCOL.md index 817bf2a..a00a209 100644 --- a/OUTPUT_PROTOCOL.md +++ b/OUTPUT_PROTOCOL.md @@ -1,5 +1,7 @@ # Output Protocol +**SCOPE**: This protocol controls communication style only. It does NOT modify workflow steps, role requirements, or delegation mandates. + Be terse. No preamble. No summaries of completed actions. No narration. Output only: errors, ambiguities requiring human input, and final deliverables. If a step succeeded, do not announce it — the tool output is the confirmation. diff --git a/claude/agent/anvillm-agent.md b/claude/agent/anvillm-agent.md index e2965a6..ae4829c 100644 --- a/claude/agent/anvillm-agent.md +++ b/claude/agent/anvillm-agent.md @@ -6,6 +6,8 @@ permissionMode: bypassPermissions # Output Protocol +**SCOPE**: This protocol controls communication style only. It does NOT modify workflow steps, role requirements, or delegation mandates. + Be terse. No preamble. No summaries of completed actions. No narration. Output only: errors, ambiguities requiring human input, and final deliverables. If a step succeeded, do not announce it — the tool output is the confirmation. diff --git a/internal/backend/tmux/session.go b/internal/backend/tmux/session.go index e24cb5a..1fdb633 100644 --- a/internal/backend/tmux/session.go +++ b/internal/backend/tmux/session.go @@ -30,6 +30,7 @@ type Session struct { winID int // Acme window ID (0 if not applicable) pid int state string + role string // bot role (developer, reviewer, tester, etc.) context string // injected into first prompt only initialPromptSent bool // true after context was sent; reset on clear/compact/stop/restart createdAt time.Time @@ -234,6 +235,20 @@ func (s *Session) GetContext() string { return s.context } +// SetRole sets the bot role +func (s *Session) SetRole(role string) { + s.mu.Lock() + defer s.mu.Unlock() + s.role = role +} + +// GetRole gets the bot role +func (s *Session) GetRole() string { + s.mu.Lock() + defer s.mu.Unlock() + return s.role +} + // SetWinID sets the Acme window ID func (s *Session) SetWinID(winID int) { s.mu.Lock() diff --git a/internal/p9/roles.go b/internal/p9/roles.go index a2f8ea8..096b267 100644 --- a/internal/p9/roles.go +++ b/internal/p9/roles.go @@ -325,3 +325,23 @@ func (r *RolesFS) Read(path string) ([]byte, error) { return nil, fmt.Errorf("role not found") } + +// ReadRole reads a role by name, searching all focus areas +func (r *RolesFS) ReadRole(roleName string) (string, error) { + roles, err := r.listAllRoles() + if err != nil { + return "", err + } + + for _, role := range roles { + if role.Name == roleName { + content, err := os.ReadFile(role.Path) + if err != nil { + return "", err + } + return string(content), nil + } + } + + return "", fmt.Errorf("role %s not found", roleName) +} diff --git a/internal/p9/server.go b/internal/p9/server.go index 90f603f..8216ebc 100644 --- a/internal/p9/server.go +++ b/internal/p9/server.go @@ -111,10 +111,11 @@ const ( fileTmux fileMail fileModel + fileRole fileCount ) -var fileNames = []string{"ctl", "state", "pid", "cwd", "alias", "backend", "context", "sandbox", "tmux", "mail", "model"} +var fileNames = []string{"ctl", "state", "pid", "cwd", "alias", "backend", "context", "sandbox", "tmux", "mail", "model", "role"} // Directory names in session var dirNames = []string{"inbox", "outbox", "completed"} @@ -908,6 +909,27 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall { return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))} } + // /{id}/role - set role and auto-load from RolesFS + if len(parts) == 2 && parts[1] == "role" { + sess := s.mgr.Get(parts[0]) + if sess == nil { + return errFcall(fc, "session not found") + } + if tmuxSess, ok := sess.(*tmux.Session); ok { + tmuxSess.SetRole(input) + // Auto-load role definition from RolesFS + if s.roles != nil { + roleContent, err := s.roles.ReadRole(input) + if err != nil { + logging.Logger().Warn("role not found", zap.String("role", input), zap.Error(err)) + } else { + tmuxSess.SetContext(roleContent) + } + } + } + return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))} + } + // /{id}/state - set session state (with validation) if len(parts) == 2 && parts[1] == "state" { sessID := parts[0] @@ -1643,6 +1665,11 @@ func (s *Server) getSessionFile(sess backend.Session, idx int) string { return "" case fileModel: return sess.Model() + case fileRole: + if tmuxSess, ok := sess.(*tmux.Session); ok { + return tmuxSess.GetRole() + } + return "" } return "" } diff --git a/internal/supervisor/supervisor.go b/internal/supervisor/supervisor.go index 8692e1a..0c8ee0a 100644 --- a/internal/supervisor/supervisor.go +++ b/internal/supervisor/supervisor.go @@ -32,7 +32,7 @@ func (s *Supervisor) assignWork() { return } - // Get all claimable tasks from all projects + // Get all ready tasks from all projects ready, err := s.beads.Read("ready") if err != nil || len(ready) == 0 { return @@ -43,31 +43,21 @@ func (s *Supervisor) assignWork() { return } - // Filter for claimable tasks - claimable := []map[string]interface{}{} + // Filter for unassigned tasks + unassigned := []map[string]interface{}{} for _, bead := range beads { // Skip if already assigned if assignee, ok := bead["assignee"].(string); ok && assignee != "" { continue } - - labels, ok := bead["labels"].([]interface{}) - if !ok { - continue - } - for _, label := range labels { - if labelStr, ok := label.(string); ok && labelStr == "claimable" { - claimable = append(claimable, bead) - break - } - } + unassigned = append(unassigned, bead) } - if len(claimable) == 0 { + if len(unassigned) == 0 { return } - // Try to assign to idle bots in matching project directories + // Try to assign to idle bots with matching role bots := s.sessions.List() for _, botID := range bots { sess := s.sessions.Get(botID) @@ -77,8 +67,16 @@ func (s *Supervisor) assignWork() { } botCwd := sess.Metadata().Cwd + // Get bot role (default to "developer" if not set) + botRole := "developer" + if tmuxSess, ok := sess.(interface{ GetRole() string }); ok { + if role := tmuxSess.GetRole(); role != "" { + botRole = role + } + } + // Only assign if session CWD is exactly or under task mountpoint - for _, bead := range claimable { + for _, bead := range unassigned { taskCwd, ok := bead["cwd"].(string) if !ok { continue @@ -88,6 +86,24 @@ func (s *Supervisor) assignWork() { continue } + // Extract role label from bead (format: "role:developer") + beadRole := "developer" // default + if labels, ok := bead["labels"].([]interface{}); ok { + for _, label := range labels { + if labelStr, ok := label.(string); ok { + if role, found := strings.CutPrefix(labelStr, "role:"); found { + beadRole = role + break + } + } + } + } + + // Skip if role doesn't match + if beadRole != botRole { + continue + } + beadID := bead["id"].(string) mountName := filepath.Base(taskCwd) diff --git a/roles/developer.md b/roles/developer.md index 205ba69..917cec8 100644 --- a/roles/developer.md +++ b/roles/developer.md @@ -47,24 +47,21 @@ After writing code: ## Development Workflow -Iterate until both reviewer and tester approve: +Iterate until reviewer and/or tester approve (delegate as needed): ``` -Plan/Research => Develop => Review => Test - ^ ^ | | - | | v | - | '----------' | - '-------------------------------' +Plan/Research => Develop => Review/Test (as needed) + ^ ^ | + | | v + | '----------' + '----------------------' ``` -1. **Plan/Research**: Delegate research queries to researcher bot +1. **Plan/Research**: Delegate research queries to researcher bot if needed 2. **Develop**: Write code based on requirements and research -3. **Review**: Send code to reviewer bot, wait for REVIEW_RESPONSE - - If "LGTM": kill reviewer bot, proceed to Test +3. **Review/Test**: Delegate to reviewer and/or tester as needed, wait for responses + - If approved: kill delegated bots, complete - If rejected: fix issues and return to Develop -4. **Test**: Send APPROVAL_REQUEST to tester bot, wait for APPROVAL_RESPONSE - - If status "Approved": kill tester bot, complete - - If status "Rejected": fix issues and return to Develop ## Message Protocol diff --git a/skills/anvillm-communication/SKILL.md b/skills/anvillm-communication/SKILL.md index 68dce35..c51588b 100644 --- a/skills/anvillm-communication/SKILL.md +++ b/skills/anvillm-communication/SKILL.md @@ -29,7 +29,6 @@ when_to_load: Load when you need to send messages to other agents, respond to th 3. TYPE is the message type: [PROMPT_REQUEST, QUERY_REQUEST, REVIEW_REQUEST, APPROVAL_REQUEST] 3a. Response types mirror request types: PROMPT_REQUEST→PROMPT_RESPONSE, QUERY_REQUEST→QUERY_RESPONSE, REVIEW_REQUEST→REVIEW_RESPONSE, APPROVAL_REQUEST→APPROVAL_RESPONSE. 4. SUBJECT is a brief description of what you did -4a. Response subjects are the subject of the original message prefixed by "Re: " 5. BODY is a detailed summary of what you did, including actions performed, files changed, diffs, etc. ### Receiving Messages diff --git a/skills/anvillm-sessions/SKILL.md b/skills/anvillm-sessions/SKILL.md index 5e04bf8..209e78a 100644 --- a/skills/anvillm-sessions/SKILL.md +++ b/skills/anvillm-sessions/SKILL.md @@ -10,12 +10,12 @@ Create and control agent sessions. ## Commands -All commands must be run via `execute_code` tool: +All commands must be run via `execute_code` tool, EXCEPT spawn_agent.sh which must NOT use execute_code: - List sessions: `list_sessions.sh` - Create session: `create_session.sh [sandbox=] [model=]` - Control session: `control_session.sh ` -- Spawn agent: `spawn_agent.sh [cwd-path] [initial-context-prompt]` +- Spawn agent: `spawn_agent.sh [cwd-path] [initial-context-prompt]` (do NOT use execute_code) - Kill agent: `kill_agent.sh [cwd-path]` ## Backends diff --git a/skills/beads/SKILL.md b/skills/beads/SKILL.md index f285849..4760526 100644 --- a/skills/beads/SKILL.md +++ b/skills/beads/SKILL.md @@ -23,6 +23,9 @@ description: Manage tasks using the beads 9P interface. Use when creating, updat - Read bead: `bash <(9p read tools/beads/read_bead.sh) ` - Read bead JSON: `bash <(9p read tools/beads/read_bead.sh) json` - Read bead comments: `bash <(9p read tools/beads/read_bead.sh) comments` + +**IMPORTANT:** When working on a bead, always check `comment_count` in the JSON output. If > 0, read comments for additional context, decisions, or blockers. + - Claim bead: `bash <(9p read tools/beads/claim_bead.sh) bd-abc [$AGENT_ID]` - Complete bead: `bash <(9p read tools/beads/complete_bead.sh) bd-abc` - Fail bead: `bash <(9p read tools/beads/fail_bead.sh) bd-abc "reason"` @@ -35,17 +38,6 @@ description: Manage tasks using the beads 9P interface. Use when creating, updat **Note:** Delete does not cascade. To delete a parent and all children, delete children first, then parent. -## Workflow - -**IMPORTANT:** When working on a bead, always check `comment_count` in the JSON output. If > 0, read comments for additional context, decisions, or blockers. - -1. Find a ready task -2. Claim the ticket being worked on -3. Use agent-kb skill to search the knowledge base. -4. Do work -5. Verify all children completed -6. Only if all children completed, complete the parent. Otherwise, work on other unblocked child tasks. - ## Rules - Discover your mount first using mtab