mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-31 00:40:46 +02:00
* feat: implement Squad feature MVP
- Add migration 084_squad: squad, squad_member, squad_activity_log tables
- Extend issue.assignee_type to support 'squad'
- Add sqlc queries for squad CRUD, member management, activity logs
- Add Go handler with full Squad API (CRUD, members, activity log)
- Register routes: /api/squads/*, /api/issues/{id}/squad-activity, /api/squad-activity
- Add Squad trigger logic:
- Assign Squad immediately triggers leader
- Every external comment on squad-assigned issue triggers leader
- Anti-loop: squad members' comments don't trigger leader
- Dedup: skip if leader already has pending task
- Add squad activity log API (方案 B) for leader no-op recording
- Add frontend TypeScript types (Squad, SquadMember, SquadActivityLog)
- Add protocol events: squad:created, squad:updated, squad:deleted
Co-authored-by: multica-agent <github@multica.ai>
* fix: address PR review blocking issues
1. validateAssigneePair now accepts 'squad' assignee_type
2. All squad endpoints validate workspace ownership via GetSquadInWorkspace
3. CreateSquadActivityLog restricted to squad leader agent only
4. AddSquadMember validates member exists in workspace
5. UpdateSquad auto-adds new leader to squad members
6. DeleteSquad transfers assigned issues to leader before deletion
7. IssueAssigneeType includes 'squad' in frontend types
Co-authored-by: multica-agent <github@multica.ai>
* feat: soft-delete squads via archive instead of hard delete
- Add migration 085: archived_at + archived_by columns on squad table
- ListSquads now excludes archived squads (ListAllSquads for admin)
- DeleteSquad → ArchiveSquad (sets archived_at, preserves all records)
- Transfer squad-assigned issues to leader before archiving
- SquadResponse includes archived_at/archived_by fields
- Frontend Squad type updated with nullable archived fields
Co-authored-by: multica-agent <github@multica.ai>
* feat: re-add Squads frontend entry (sidebar nav + pages)
Re-applies the frontend squad entry that was lost during a merge:
- Sidebar nav: Squads item with Users icon
- Paths: squads() and squadDetail() in workspace paths
- Routes: /squads and /squads/[id] pages
- Views: SquadsPage (list) and SquadDetailPage
- i18n: en 'Squads' / zh '小队'
- Reserved slug: 'squads'
Co-authored-by: multica-agent <github@multica.ai>
* fix: fix SquadsPage rendering - use PageHeader children pattern
PageHeader takes children, not title/actions props. The incorrect
usage caused a React rendering error. Now matches the pattern used
by autopilots and agents pages.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squads): add API client methods and package export for squads pages
* feat: complete Squad frontend - create dialog, member management, API methods
- Add CreateSquadModal with name/description/leader selection
- Register 'create-squad' in modal registry
- Wire 'New Squad' button to open the modal
- Add full API client methods: createSquad, updateSquad, deleteSquad,
addSquadMember, removeSquadMember
- Rewrite SquadDetailPage with:
- Member list showing resolved names
- Add/remove member UI
- Archive squad button
- Back navigation to squads list
Co-authored-by: multica-agent <github@multica.ai>
* feat: improve Squad UI - match create agent dialog style
- CreateSquadModal: proper Dialog with Header/Description/Footer,
agent picker with avatars, textarea for description
- SquadDetailPage: centered max-w-2xl layout, ActorAvatar for members,
Crown badge for leader, textarea for member description,
improved spacing and visual hierarchy
- Renamed 'role' field label to 'Description' in add member form
(describes the member's responsibilities in the squad)
Co-authored-by: multica-agent <github@multica.ai>
* feat(squad): add avatar, instructions; drop unique-name constraint
- 086: add squad.avatar_url
- 087: drop unique constraint on squad.name (squads with the same
name are legitimate across teams; uniqueness was an accidental
product constraint)
- 088: add squad.instructions (text, default '')
- UpdateSquad now COALESCEs avatar_url + instructions
- handler exposes Instructions in SquadResponse and accepts it in
UpdateSquad
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): assignable + mention target; trigger leader on assign
- assignee picker and @mention suggestion list squads alongside
agents and members; renders squad avatar/icon
- creating or updating an issue with assignee_type=squad enqueues
a task for the squad's current leader (mirrors agent-assignee
parking-lot rule: skip backlog only)
- workspace queries/hooks expose squads where needed for the
pickers
- locales updated for new picker copy
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): agent-style detail page with members + instructions tabs
- restructure squad detail page to mirror the agent detail page:
320px inspector (creator, leader, created/updated) + tabbed
pane (Members | Instructions) with dirty-guard AlertDialog
- inline name + avatar editing on the inspector
- inline description editor (modal textarea)
- members tab: leader + member picker with role descriptions,
swap leader, edit member roles, remove
- instructions tab: ContentEditor + Save (mirrors agent pattern)
- squads list shows the squad avatar/icon
- core types + api.updateSquad accept avatar_url + instructions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): inject leader briefing on claim (protocol + roster + instructions)
When a squad's leader agent claims a task on a squad-assigned issue,
append a system-level briefing to the agent's Instructions composed of:
1. Squad Operating Protocol — hard-coded rules: leader is a
coordinator, dispatch via @mention, stop after dispatching,
resume on re-trigger, do not work outside the roster.
2. Squad Roster — leader self-row plus one row per non-archived
member with a literal mention markdown string ([@Name](mention://
agent|member/<UUID>)) the leader can paste verbatim. Round-trips
through util.ParseMentions, enforced by a contract test.
3. Squad Instructions — the user-defined squad.instructions block,
omitted entirely when empty so we do not leave a dangling heading.
Non-leader members claiming the same issue receive no briefing.
Tests cover: full squad with mixed agent/human members, lone leader,
archived agents skipped, empty user instructions, mention round-trip,
and the leader/non-leader claim-handler gate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(squad): tell leader not to restate issue context in dispatch comment
After observing leaders padding their delegation comments with full
re-summaries of the issue body and prior discussion, make the
Operating Protocol explicit:
- assignees on Multica already have the full issue (title,
description, all comments, attachments) and workspace context;
- delegation comments should add only what cannot be inferred
(who is picked, why, extra constraints), aim for two or three
sentences;
- restating context is now an explicit hard rule violation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): unify leader evaluation into activity_log, add CLI command
- Squad member comments now trigger leader (only leader self-excluded)
- Replace squad_activity_log with activity_log (action: squad_leader_evaluated)
- Add CLI: multica squad activity <issue-id> <outcome> --reason
- Add API: POST /api/issues/{id}/squad-evaluated
- Update squad operating protocol to require evaluation recording
- Remove squad_activity_log table from schema and generated code
* feat(cli): add squad list, get, member list commands
* fix(squad): address review findings (P1+P2)
P1 fixes:
- Add 'squads' to reserved_slugs.json (source of truth)
- Add 'create-squad' to ModalType union
- Remove unused leaderOpen/selectedLeader in create-squad modal
- Replace literal JSX strings with i18n selectors (en + zh-Hans)
P2 fixes:
- Add 'squad' to mention regex (MentionRe)
- Fix human member lookup in squad briefing (use GetUser directly)
- Add squads routes to desktop app
- Add squad:created/updated/deleted to WSEventType + invalidation
- Reject archived squads as issue assignees
* fix(squad): restore zh-Hans key, publish activity event, invalidate issues on archive
- Restore create_project.title in zh-Hans modals.json (dropped by prior edit)
- Publish activity:created WS event after squad leader evaluation
- Invalidate issue queries on squad:deleted (archive transfers assignees)
- Add creator info to squad list cards
* fix(squad): realtime sync, rerun support, leader validation
- Use workspaceKeys.squads prefix for detail/member queries (realtime invalidation)
- Publish squad:updated after add/remove/role-change member mutations
- Support rerun for squad-assigned issues (targets leader agent)
- Reject assignment to squads whose leader is archived
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
207 lines
8.2 KiB
Go
207 lines
8.2 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/multica-ai/multica/server/internal/util"
|
|
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
|
)
|
|
|
|
// squadOperatingProtocol is the hard-coded system-level briefing prepended to
|
|
// every squad-leader claim. It explains the leader's coordinator role, the
|
|
// @mention dispatch mechanism, and the stop-after-dispatch contract.
|
|
//
|
|
// Keep this text English-only (matches existing agent-harness conventions)
|
|
// and keep the mention syntax exactly aligned with util.MentionRe — the
|
|
// "Squad Roster" block below renders concrete examples that round-trip
|
|
// through util.ParseMentions, and the protocol text refers to that format.
|
|
const squadOperatingProtocol = `## Squad Operating Protocol
|
|
|
|
You are the LEADER of a squad. Your job is to **coordinate**, not to execute
|
|
the work yourself.
|
|
|
|
Your responsibilities, in order:
|
|
|
|
1. **Read the issue** (title, description, latest comments, acceptance
|
|
criteria) and decide which squad member is best suited to do the work.
|
|
2. **Delegate by @mention.** Post a single comment on this issue that
|
|
@mentions the chosen member(s) and tells them what to do.
|
|
- **Be terse.** Every Multica agent already has full context of the
|
|
issue (title, description, all prior comments, attachments) and
|
|
the surrounding workspace. Do NOT restate or summarise the
|
|
issue body, prior discussion, or known facts in your delegation
|
|
comment — they read it themselves.
|
|
- Say only what cannot be inferred from the issue: who you're
|
|
picking, why them (one short clause), and any *additional*
|
|
constraints, hints, or sequencing you want them to follow.
|
|
Two or three sentences is usually plenty.
|
|
- Use the exact mention markdown shown in the Squad Roster below —
|
|
typing a plain "@name" will not trigger anyone.
|
|
3. **Record your evaluation.** After every trigger — whether you delegated,
|
|
decided no action is needed, or encountered an error — record it:
|
|
` + "`" + `multica squad activity <issue-id> <outcome> --reason "<short reason>"` + "`" + `
|
|
Outcome values: ` + "`" + `action` + "`" + ` (you delegated or acted),
|
|
` + "`" + `no_action` + "`" + ` (you evaluated and decided nothing is needed),
|
|
` + "`" + `failed` + "`" + ` (you hit an error).
|
|
This is mandatory on every turn — it records your decision in the
|
|
issue timeline so humans can see you evaluated the trigger.
|
|
4. **Stop after dispatching.** Once your delegation comment is posted
|
|
and evaluation recorded, end your turn. Do not continue working,
|
|
do not write code, do not open files. You will be re-triggered
|
|
automatically when:
|
|
- a delegated member posts an update or asks you a question;
|
|
- a delegated member finishes and the issue moves forward;
|
|
- someone @mentions you again on this issue.
|
|
5. **Re-evaluate on each trigger.** When you wake up again, read the new
|
|
activity and decide whether to delegate the next step, escalate to
|
|
the human reporter, or close the loop. If no action is needed
|
|
(e.g. a member posted a progress update that requires no response),
|
|
record ` + "`" + `no_action` + "`" + ` and exit silently.
|
|
|
|
Hard rules:
|
|
- Do NOT restate the issue body or prior comments in your delegation —
|
|
the assignee already has them. Repeating context is noise that
|
|
buries the actual instruction.
|
|
- Do NOT do the implementation work yourself unless the squad has no
|
|
other suitable members. The squad exists so work is split — bypassing
|
|
it defeats the point.
|
|
- Do NOT @mention members who don't appear in the Squad Roster below;
|
|
they are not part of this squad.
|
|
- One delegation comment per turn is enough. Avoid spamming multiple
|
|
near-identical comments.
|
|
- If the squad has no member capable of the task, post a comment
|
|
explaining the gap (and @mention the issue's reporter if possible)
|
|
rather than silently doing the work.
|
|
- ALWAYS call ` + "`" + `multica squad activity` + "`" + ` before ending your turn —
|
|
even when the outcome is no_action.`
|
|
|
|
// buildSquadLeaderBriefing composes the full system briefing appended to a
|
|
// squad leader's Instructions when it claims a task on a squad-assigned
|
|
// issue. The returned string contains three sections:
|
|
//
|
|
// 1. Squad Operating Protocol (constant, system-level rules).
|
|
// 2. Squad Roster (data — leader self-row + members with literal
|
|
// `[@Name](mention://<type>/<UUID>)` strings ready to paste).
|
|
// 3. Squad Instructions (user-defined `squad.instructions`, omitted when
|
|
// empty so we don't leave a dangling heading).
|
|
//
|
|
// Archived agent members are skipped — there's no point asking the leader
|
|
// to delegate to a retired agent. Members whose underlying record can't be
|
|
// loaded (deleted user/agent races, FK weirdness) are also skipped silently.
|
|
func buildSquadLeaderBriefing(ctx context.Context, q *db.Queries, squad db.Squad) string {
|
|
var sb strings.Builder
|
|
sb.WriteString(squadOperatingProtocol)
|
|
sb.WriteString("\n\n")
|
|
sb.WriteString(buildSquadRoster(ctx, q, squad))
|
|
|
|
if trimmed := strings.TrimSpace(squad.Instructions); trimmed != "" {
|
|
sb.WriteString("\n\n## Squad Instructions (")
|
|
sb.WriteString(squad.Name)
|
|
sb.WriteString(")\n\n")
|
|
sb.WriteString(trimmed)
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// buildSquadRoster renders the "## Squad Roster" section: a leader self-row
|
|
// plus one row per non-archived member, with literal mention markdown.
|
|
func buildSquadRoster(ctx context.Context, q *db.Queries, squad db.Squad) string {
|
|
var sb strings.Builder
|
|
sb.WriteString("## Squad Roster\n\n")
|
|
|
|
// Leader self-row. Leaders are always agents (FK enforced in schema).
|
|
leaderName := "Leader"
|
|
if leader, err := q.GetAgent(ctx, squad.LeaderID); err == nil {
|
|
leaderName = leader.Name
|
|
}
|
|
sb.WriteString("Leader (you):\n")
|
|
sb.WriteString("- ")
|
|
sb.WriteString(leaderName)
|
|
sb.WriteString(" — agent — `")
|
|
sb.WriteString(formatMention(leaderName, "agent", util.UUIDToString(squad.LeaderID)))
|
|
sb.WriteString("`\n")
|
|
|
|
members, err := q.ListSquadMembers(ctx, squad.ID)
|
|
if err != nil {
|
|
members = nil
|
|
}
|
|
|
|
rows := make([]string, 0, len(members))
|
|
for _, m := range members {
|
|
// Skip the leader if they happen to also be in the member list —
|
|
// they're already shown above and we don't want self-delegation.
|
|
if m.MemberType == "agent" && util.UUIDToString(m.MemberID) == util.UUIDToString(squad.LeaderID) {
|
|
continue
|
|
}
|
|
row := renderMemberRow(ctx, q, m)
|
|
if row != "" {
|
|
rows = append(rows, row)
|
|
}
|
|
}
|
|
|
|
if len(rows) == 0 {
|
|
sb.WriteString("\nMembers: (none — you are the only member of this squad)\n")
|
|
return sb.String()
|
|
}
|
|
|
|
sb.WriteString("\nMembers:\n")
|
|
for _, r := range rows {
|
|
sb.WriteString(r)
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// renderMemberRow renders a single roster row, returning "" if the member
|
|
// can't be resolved or should be skipped (e.g. archived agent).
|
|
func renderMemberRow(ctx context.Context, q *db.Queries, m db.SquadMember) string {
|
|
id := util.UUIDToString(m.MemberID)
|
|
role := strings.TrimSpace(m.Role)
|
|
switch m.MemberType {
|
|
case "agent":
|
|
ag, err := q.GetAgent(ctx, m.MemberID)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if ag.ArchivedAt.Valid {
|
|
return ""
|
|
}
|
|
return formatRosterRow(ag.Name, "agent", role, formatMention(ag.Name, "agent", id))
|
|
case "member":
|
|
user, err := q.GetUser(ctx, m.MemberID)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
// Mention syntax for humans uses the user_id (matches the rest of
|
|
// the product — see util.MentionRe and frontend mention payloads).
|
|
userID := util.UUIDToString(m.MemberID)
|
|
return formatRosterRow(user.Name, "member (human)", role, formatMention(user.Name, "member", userID))
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func formatRosterRow(name, kind, role, mention string) string {
|
|
var sb strings.Builder
|
|
sb.WriteString("- ")
|
|
sb.WriteString(name)
|
|
sb.WriteString(" — ")
|
|
sb.WriteString(kind)
|
|
if role != "" {
|
|
sb.WriteString(`, role: "`)
|
|
sb.WriteString(role)
|
|
sb.WriteString(`"`)
|
|
}
|
|
sb.WriteString(" — `")
|
|
sb.WriteString(mention)
|
|
sb.WriteString("`\n")
|
|
return sb.String()
|
|
}
|
|
|
|
// formatMention emits a mention markdown string that round-trips through
|
|
// util.ParseMentions. The label is the human display name; the link target
|
|
// uses the mention:// scheme with the entity type and UUID.
|
|
func formatMention(name, mentionType, id string) string {
|
|
return "[@" + name + "](mention://" + mentionType + "/" + id + ")"
|
|
}
|