working on a different approach for roles

This commit is contained in:
Levi Neely 2026-03-06 08:54:44 +01:00
parent 391f773b2a
commit b51bbf7e72
10 changed files with 114 additions and 44 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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()

View File

@ -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)
}

View File

@ -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 ""
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 <backend> <cwd> [sandbox=<sandbox>] [model=<model>]`
- Control session: `control_session.sh <session-id> <command>`
- Spawn agent: `spawn_agent.sh <agent-id> [cwd-path] [initial-context-prompt]`
- Spawn agent: `spawn_agent.sh <agent-id> [cwd-path] [initial-context-prompt]` (do NOT use execute_code)
- Kill agent: `kill_agent.sh [cwd-path]`
## Backends

View File

@ -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) <mount> <id> <property>`
- Read bead JSON: `bash <(9p read tools/beads/read_bead.sh) <mount> <id> json`
- Read bead comments: `bash <(9p read tools/beads/read_bead.sh) <mount> <id> 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) <mount> bd-abc [$AGENT_ID]`
- Complete bead: `bash <(9p read tools/beads/complete_bead.sh) <mount> bd-abc`
- Fail bead: `bash <(9p read tools/beads/fail_bead.sh) <mount> 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