Skip to content
Open
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
4 changes: 2 additions & 2 deletions internal/store/pg/skills_grants.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func (s *PGSkillStore) ListAccessible(ctx context.Context, agentID uuid.UUID, us
`SELECT DISTINCT s.name, s.slug, s.description, s.version, s.file_path FROM skills s
LEFT JOIN skill_agent_grants sag ON s.id = sag.skill_id AND sag.agent_id = $1
LEFT JOIN skill_user_grants sug ON s.id = sug.skill_id AND sug.user_id = $2`+stcJoin+`
WHERE s.status = 'active'`+tenantCond+stcFilter+` AND (
WHERE s.status = 'active' AND s.enabled = true`+tenantCond+stcFilter+` AND (
s.is_system = true
OR s.visibility = 'public'
OR (s.visibility = 'private' AND s.owner_id = $2)
Expand Down Expand Up @@ -218,7 +218,7 @@ func (s *PGSkillStore) ListWithGrantStatus(ctx context.Context, agentID uuid.UUI
s.is_system
FROM skills s
LEFT JOIN skill_agent_grants sag ON s.id = sag.skill_id AND sag.agent_id = $1
WHERE s.status = 'active'`+tenantCond+`
WHERE s.status = 'active' AND s.enabled = true`+tenantCond+`
ORDER BY s.name`, append([]any{agentID}, tcArgs...)...)
if err != nil {
return nil, err
Expand Down
4 changes: 2 additions & 2 deletions internal/store/sqlitestore/skills_grants.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (s *SQLiteSkillStore) ListAccessible(ctx context.Context, agentID uuid.UUID
`SELECT DISTINCT s.name, s.slug, s.description, s.version, s.file_path FROM skills s
LEFT JOIN skill_agent_grants sag ON s.id = sag.skill_id AND sag.agent_id = ?
LEFT JOIN skill_user_grants sug ON s.id = sug.skill_id AND sug.user_id = ?`+stcJoin+`
WHERE s.status = 'active'`+tenantCond+stcFilter+` AND (
WHERE s.status = 'active' AND s.enabled = 1`+tenantCond+stcFilter+` AND (
s.is_system = 1
OR s.visibility = 'public'
OR (s.visibility = 'private' AND s.owner_id = ?)
Expand Down Expand Up @@ -213,7 +213,7 @@ func (s *SQLiteSkillStore) ListWithGrantStatus(ctx context.Context, agentID uuid
s.is_system
FROM skills s
LEFT JOIN skill_agent_grants sag ON s.id = sag.skill_id AND sag.agent_id = ?
WHERE s.status = 'active'`+tenantCond+`
WHERE s.status = 'active' AND s.enabled = 1`+tenantCond+`
ORDER BY s.name`, queryArgs...)
if err != nil {
return nil, err
Expand Down
31 changes: 26 additions & 5 deletions internal/tools/team_tasks_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,37 @@ func (t *TeamTasksTool) executeCreate(ctx context.Context, args map[string]any)
return ErrorResult(err.Error())
}

// Gate: must list tasks before creating to prevent duplicates in concurrent group chat.
if ptd := PendingTeamDispatchFromCtx(ctx); ptd != nil && !ptd.HasListed() {
return ErrorResult("You must check existing tasks first. Call team_tasks(action=\"search\", query=\"<keywords>\") to check for similar tasks before creating — this saves tokens vs listing all. Alternatively use action=\"list\" to see the full board.")
}

subject, _ := args["subject"].(string)
if subject == "" {
return ErrorResult("subject is required for create action")
}

// Auto-dedup: if list/search wasn't called first, automatically search for similar active tasks.
if ptd := PendingTeamDispatchFromCtx(ctx); ptd != nil && !ptd.HasListed() {
chatID := ToolChatIDFromCtx(ctx)
lock := getTeamCreateLock(team.ID.String(), chatID)
lock.Lock()
ptd.SetTeamLock(lock)
ptd.MarkListed()

filterUserID := ""
ch := ToolChannelFromCtx(ctx)
if ch != ChannelTeammate && ch != ChannelSystem {
filterUserID = store.UserIDFromContext(ctx)
}
existing, _ := t.manager.Store().SearchTasks(ctx, team.ID, subject, 5, filterUserID)
var active []string
for _, et := range existing {
s := string(et.Status)
if s == "pending" || s == "in_progress" || s == "blocked" || s == "in_review" {
active = append(active, fmt.Sprintf("- #%s: %s [%s]", et.ID.String()[:8], et.Subject, s))
}
}
if len(active) > 0 {
return ErrorResult(fmt.Sprintf("Similar tasks already exist:\n%s\n\nUse team_tasks(action=\"get\", task_id=\"...\") to inspect, or re-submit create if this is truly new.", strings.Join(active, "\n")))
}
}

description, _ := args["description"].(string)
priority := 0
if p, ok := args["priority"].(float64); ok {
Expand Down
37 changes: 31 additions & 6 deletions internal/tools/team_tasks_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,22 +134,47 @@ func TestCreate(t *testing.T) {
_ = mb
})

t.Run("RequiresSearchGate", func(t *testing.T) {
t.Run("AutoSearchCreateAllowed", func(t *testing.T) {
_, tool, _, _, ctx := newTestTeamSetup()
// ptd exists but HasListed=false
// ptd exists, HasListed=false, no similar tasks in store → auto-search passes.
ptd := NewPendingTeamDispatch()
ctx = WithPendingTeamDispatch(ctx, ptd)

result := tool.Execute(ctx, map[string]any{
"action": "create",
"subject": "Test task",
"subject": "New unique task",
"assignee": "member-agent",
})
if result.IsError {
t.Fatalf("expected success with auto-search (no dupes), got error: %s", result.ForLLM)
}
if !strings.Contains(result.ForLLM, "Task created") {
t.Errorf("expected 'Task created', got: %s", result.ForLLM)
}
})

t.Run("AutoSearchBlocksDuplicate", func(t *testing.T) {
mb, tool, _, _, ctx := newTestTeamSetup()
// Seed an existing active task with a similar subject.
mb.taskStore.CreateTask(ctx, &store.TeamTaskData{
TeamID: testTeamID,
Subject: "Setup Meeting 10:30",
Status: "pending",
})

ptd := NewPendingTeamDispatch()
ctx = WithPendingTeamDispatch(ctx, ptd)

result := tool.Execute(ctx, map[string]any{
"action": "create",
"subject": "Setup Meeting 10:30",
"assignee": "member-agent",
})
if !result.IsError {
t.Fatal("expected error when list gate not passed")
t.Fatal("expected error when similar active task exists")
}
if !strings.Contains(result.ForLLM, "check existing tasks") {
t.Errorf("expected gate message, got: %s", result.ForLLM)
if !strings.Contains(result.ForLLM, "Similar tasks already exist") {
t.Errorf("expected duplicate warning, got: %s", result.ForLLM)
}
})

Expand Down