diff --git a/internal/agent/resolver.go b/internal/agent/resolver.go index ce7dc9eb..aba039d1 100644 --- a/internal/agent/resolver.go +++ b/internal/agent/resolver.go @@ -160,6 +160,7 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc { // Inject TEAM.md for all team members (lead + members) so every agent // knows the team workflow: create/claim/complete tasks via team_tasks tool. hasTeam := false + isTeamLead := false if deps.TeamStore != nil { hasTeamMD := false for _, cf := range contextFiles { @@ -176,6 +177,13 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc { Path: bootstrap.TeamFile, Content: buildTeamMD(team, members, ag.ID), }) + // Detect lead role for tool policy + for _, m := range members { + if m.AgentID == ag.ID && m.Role == store.TeamRoleLead { + isTeamLead = true + break + } + } } } } else { @@ -325,7 +333,7 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc { Sessions: deps.Sessions, Tools: toolsReg, ToolPolicy: deps.ToolPolicy, - AgentToolPolicy: agentToolPolicyWithMCP(ag.ParseToolsConfig(), hasMCPTools), + AgentToolPolicy: agentToolPolicyForTeam(agentToolPolicyWithMCP(ag.ParseToolsConfig(), hasMCPTools), isTeamLead), SkillsLoader: deps.Skills, SkillAllowList: skillAllowList, HasMemory: hasMemory, diff --git a/internal/agent/resolver_helpers.go b/internal/agent/resolver_helpers.go index b508ac56..055ca257 100644 --- a/internal/agent/resolver_helpers.go +++ b/internal/agent/resolver_helpers.go @@ -145,7 +145,6 @@ func buildTeamMD(team *store.TeamData, members []store.TeamMemberData, selfID uu sb.WriteString("- action=search, query= → search tasks by subject/description\n") sb.WriteString("- action=complete, task_id=, result= → manually complete a task\n") sb.WriteString("- action=cancel, task_id=, reason= → cancel a pending task that is no longer needed\n\n") - sb.WriteString("Use `team_message` to send updates to team members.\n\n") sb.WriteString("For simple questions about team composition, answer directly from the member list above.\n") } else { sb.WriteString("As a member, when you receive a delegated task, just do the work.\n") @@ -156,13 +155,32 @@ func buildTeamMD(team *store.TeamData, members []store.TeamMemberData, selfID uu sb.WriteString("- action=list → check team task board (active tasks)\n") sb.WriteString("- action=get, task_id= → read a completed task's full result\n") sb.WriteString("- action=search, query= → search tasks\n\n") - sb.WriteString("Use `team_message` to send updates to your team lead.\n\n") + sb.WriteString("Use `team_message` to send progress updates to your team lead (one-way, no response expected).\n\n") sb.WriteString("For simple questions about team composition, answer directly from the member list above.\n") } return sb.String() } +// agentToolPolicyForTeam denies team_message for team leads. +// Leads should use spawn (which auto-announces results back) instead of team_message +// (one-way notification that leaks raw responses to the output channel). +func agentToolPolicyForTeam(policy *config.ToolPolicySpec, isLead bool) *config.ToolPolicySpec { + if !isLead { + return policy + } + if policy == nil { + policy = &config.ToolPolicySpec{} + } + for _, d := range policy.Deny { + if d == "team_message" { + return policy + } + } + policy.Deny = append(policy.Deny, "team_message") + return policy +} + // agentToolPolicyWithMCP injects "group:mcp" into the agent's alsoAllow list // when MCP tools are loaded, ensuring the PolicyEngine doesn't block them. func agentToolPolicyWithMCP(policy *config.ToolPolicySpec, hasMCP bool) *config.ToolPolicySpec { diff --git a/internal/tools/delegate_async.go b/internal/tools/delegate_async.go index 7f6de919..6a53d55d 100644 --- a/internal/tools/delegate_async.go +++ b/internal/tools/delegate_async.go @@ -166,8 +166,8 @@ func (dm *DelegateManager) DelegateAsync(ctx context.Context, opts DelegateOpts) var announceSummaries []protocol.DelegationAnnounceResultSummary for _, r := range artifacts.Results { preview := r.Content - if len(preview) > 200 { - preview = preview[:200] + "..." + if runes := []rune(preview); len(runes) > 200 { + preview = string(runes[:200]) + "..." } announceSummaries = append(announceSummaries, protocol.DelegationAnnounceResultSummary{ AgentKey: r.AgentKey, diff --git a/internal/tools/delegate_events.go b/internal/tools/delegate_events.go index c9902c18..1abcf028 100644 --- a/internal/tools/delegate_events.go +++ b/internal/tools/delegate_events.go @@ -100,8 +100,8 @@ func formatDelegateAnnounce(task *DelegationTask, artifacts *DelegateArtifacts, if len(r.Deliverables) > 0 { for _, d := range r.Deliverables { preview := d - if len(preview) > 4000 { - preview = preview[:4000] + "\n[...truncated, full content in team_tasks]" + if runes := []rune(preview); len(runes) > 4000 { + preview = string(runes[:4000]) + "\n[...truncated, full content in team_tasks]" } msg += fmt.Sprintf("\n[Deliverable]\n%s\n", preview) }