Compare commits

...

1 Commits

Author SHA1 Message Date
Naiyuan Qing
6e8c5b8f24 feat(cli): add squad member set-role
Co-authored-by: multica-agent <github@multica.ai>
2026-06-01 10:44:27 +08:00
10 changed files with 173 additions and 3 deletions

View File

@@ -88,7 +88,7 @@ CI나 headless 환경에서는 브라우저 플로우를 건너뛰세요. 웹
| `multica squad create --name "..." --leader <agent>` | 스쿼드 생성(owner / admin) |
| `multica squad update <id> ...` | 이름, 설명, 지침, 리더, 또는 아바타 업데이트 |
| `multica squad delete <id>` | 보관(소프트 삭제) — 할당된 이슈를 리더에게 이관 |
| `multica squad member list/add/remove <squad-id>` | 스쿼드 멤버 관리 |
| `multica squad member list/add/remove/set-role <squad-id>` | 스쿼드 멤버 관리 및 역할 직접 업데이트 |
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | 스쿼드 리더 에이전트가 매 턴마다 평가를 기록할 때 사용 |
전체 모델은 [스쿼드](/squads)를 참고하세요.

View File

@@ -88,7 +88,7 @@ For the difference between token types, see [Authentication and tokens](/auth-to
| `multica squad create --name "..." --leader <agent>` | Create a squad (owner / admin) |
| `multica squad update <id> ...` | Update name, description, instructions, leader, or avatar |
| `multica squad delete <id>` | Archive (soft-delete) — transfers assigned issues to the leader |
| `multica squad member list/add/remove <squad-id>` | Manage squad members |
| `multica squad member list/add/remove/set-role <squad-id>` | Manage squad members and update roles in place |
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | Used by squad leader agents to record an evaluation per turn |
See [Squads](/squads) for the full model.

View File

@@ -88,7 +88,7 @@ Token 类型的详细区分见 [认证与令牌](/auth-tokens)。
| `multica squad create --name "..." --leader <agent>` | 创建小队owner / admin|
| `multica squad update <id> ...` | 修改名字、描述、instructions、队长、头像 |
| `multica squad delete <id>` | 归档(软删除)—— 同时把分配给小队的 issue 转给队长 |
| `multica squad member list/add/remove <squad-id>` | 管理小队成员 |
| `multica squad member list/add/remove/set-role <squad-id>` | 管理小队成员并原地更新 role |
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | 队长智能体每轮结束时调用,记录 evaluation |
完整模型见 [小队](/squads)。

View File

@@ -123,6 +123,7 @@ multica squad member add <squad-id> --member-id <agent-or-user-uuid> --type agen
| `multica squad delete <id>` | 보관(소프트 삭제) — 할당된 이슈를 리더에게 이전 |
| `multica squad member list <id>` | 스쿼드의 멤버 목록 표시 |
| `multica squad member add <id> --member-id <uuid> --type agent\|member [--role "..."]` | 멤버 추가(owner / admin) |
| `multica squad member set-role <id> --member-id <uuid> --member-type agent\|member --role "..."` | 멤버를 제거하지 않고 역할 변경 |
| `multica squad member remove <id> --member-id <uuid> --type agent\|member` | 멤버 제거(리더는 제거할 수 없습니다 — 먼저 리더를 변경하세요) |
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | 리더 에이전트가 매 턴 종료 시 기록 |

View File

@@ -123,6 +123,7 @@ There is currently no unarchive command; create a new squad if you need the rout
| `multica squad delete <id>` | Archive (soft-delete) — transfers assigned issues to the leader |
| `multica squad member list <id>` | List a squad's members |
| `multica squad member add <id> --member-id <uuid> --type agent\|member [--role "..."]` | Add a member (owner / admin) |
| `multica squad member set-role <id> --member-id <uuid> --member-type agent\|member --role "..."` | Change a member's role without removing it |
| `multica squad member remove <id> --member-id <uuid> --type agent\|member` | Remove a member (the leader cannot be removed — change leader first) |
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | Recorded by the leader agent at the end of every turn |

View File

@@ -123,6 +123,7 @@ multica squad member add <squad-id> --member-id <agent-or-user-uuid> --type agen
| `multica squad delete <id>` | 归档(软删除)——同时把当前分配给小队的 issue 转给队长 |
| `multica squad member list <id>` | 列出小队成员 |
| `multica squad member add <id> --member-id <uuid> --type agent\|member [--role "..."]` | 加成员owner / admin|
| `multica squad member set-role <id> --member-id <uuid> --member-type agent\|member --role "..."` | 不移除成员,直接修改 role |
| `multica squad member remove <id> --member-id <uuid> --type agent\|member` | 移除成员(**不能移除队长**——先换队长)|
| `multica squad activity <issue-id> <action\|no_action\|failed> --reason "..."` | 队长每次结束前由它自己调用 |

View File

@@ -344,6 +344,56 @@ func runSquadMemberAdd(cmd *cobra.Command, args []string) error {
return nil
}
// ── Member Set Role ─────────────────────────────────────────────────────────
var squadMemberSetRoleCmd = &cobra.Command{
Use: "set-role <squad-id>",
Short: "Change a squad member's role",
Args: exactArgs(1),
RunE: runSquadMemberSetRole,
}
func runSquadMemberSetRole(cmd *cobra.Command, args []string) error {
memberID, _ := cmd.Flags().GetString("member-id")
memberType, _ := cmd.Flags().GetString("member-type")
role, _ := cmd.Flags().GetString("role")
if memberID == "" {
return fmt.Errorf("--member-id is required")
}
if memberType != "agent" && memberType != "member" {
return fmt.Errorf("--member-type must be 'agent' or 'member'")
}
if role == "" {
return fmt.Errorf("--role is required")
}
client, err := newAPIClient(cmd)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
body := map[string]any{
"member_type": memberType,
"member_id": memberID,
"role": role,
}
var result map[string]any
if err := client.PatchJSON(ctx, "/api/squads/"+args[0]+"/members/role", body, &result); err != nil {
return fmt.Errorf("set member role: %w", err)
}
output, _ := cmd.Flags().GetString("output")
if output == "json" {
return cli.PrintJSON(os.Stdout, result)
}
fmt.Fprintf(os.Stderr, "Member %s role updated to %s.\n", memberID, role)
return nil
}
// ── Member Remove ───────────────────────────────────────────────────────────
var squadMemberRemoveCmd = &cobra.Command{
@@ -487,6 +537,12 @@ func init() {
squadMemberRemoveCmd.Flags().String("type", "agent", "Member type: agent or member")
squadMemberRemoveCmd.Flags().String("output", "table", "Output format: table or json")
// member set-role
squadMemberSetRoleCmd.Flags().String("member-id", "", "Member or agent ID (required)")
squadMemberSetRoleCmd.Flags().String("member-type", "agent", "Member type: agent or member")
squadMemberSetRoleCmd.Flags().String("role", "", "New role in the squad (required)")
squadMemberSetRoleCmd.Flags().String("output", "json", "Output format: table or json")
// activity
squadActivityCmd.Flags().String("reason", "", "Short explanation of the decision")
squadActivityCmd.Flags().String("output", "table", "Output format: table or json")
@@ -494,6 +550,7 @@ func init() {
squadMemberCmd.AddCommand(squadMemberListCmd)
squadMemberCmd.AddCommand(squadMemberAddCmd)
squadMemberCmd.AddCommand(squadMemberRemoveCmd)
squadMemberCmd.AddCommand(squadMemberSetRoleCmd)
squadCmd.AddCommand(squadListCmd)
squadCmd.AddCommand(squadGetCmd)

View File

@@ -0,0 +1,107 @@
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/spf13/cobra"
)
func newSquadMemberSetRoleTestCmd() *cobra.Command {
cmd := &cobra.Command{Use: "set-role"}
cmd.Flags().String("server-url", "", "")
cmd.Flags().String("workspace-id", "", "")
cmd.Flags().String("profile", "", "")
cmd.Flags().String("member-id", "", "")
cmd.Flags().String("member-type", "agent", "")
cmd.Flags().String("role", "", "")
cmd.Flags().String("output", "json", "")
return cmd
}
func TestSquadMemberSetRoleCommandIsRegistered(t *testing.T) {
cmd, _, err := squadMemberCmd.Find([]string{"set-role", "squad-123"})
if err != nil {
t.Fatalf("find set-role command: %v", err)
}
if cmd == nil || cmd.Name() != "set-role" {
t.Fatalf("set-role command not registered; got %#v", cmd)
}
for _, flag := range []string{"member-id", "member-type", "role", "output"} {
if cmd.Flags().Lookup(flag) == nil {
t.Fatalf("set-role command missing --%s flag", flag)
}
}
}
func TestRunSquadMemberSetRolePatchesRole(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("MULTICA_TOKEN", "test-token")
t.Setenv("MULTICA_WORKSPACE_ID", "workspace-123")
var gotMethod, gotPath string
var gotBody map[string]any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotMethod = r.Method
gotPath = r.URL.Path
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
t.Fatalf("decode request body: %v", err)
}
if r.Header.Get("X-Workspace-ID") != "workspace-123" {
t.Fatalf("X-Workspace-ID = %q, want workspace-123", r.Header.Get("X-Workspace-ID"))
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"squad_id": "squad-123",
"member_id": "member-456",
"member_type": "agent",
"role": "reviewer",
})
}))
defer srv.Close()
t.Setenv("MULTICA_SERVER_URL", srv.URL)
cmd := newSquadMemberSetRoleTestCmd()
_ = cmd.Flags().Set("member-id", "member-456")
_ = cmd.Flags().Set("member-type", "agent")
_ = cmd.Flags().Set("role", "reviewer")
_ = cmd.Flags().Set("output", "json")
if err := runSquadMemberSetRole(cmd, []string{"squad-123"}); err != nil {
t.Fatalf("runSquadMemberSetRole: %v", err)
}
if gotMethod != http.MethodPatch {
t.Fatalf("method = %s, want PATCH", gotMethod)
}
if gotPath != "/api/squads/squad-123/members/role" {
t.Fatalf("path = %q, want /api/squads/squad-123/members/role", gotPath)
}
wantBody := map[string]any{"member_id": "member-456", "member_type": "agent", "role": "reviewer"}
for k, want := range wantBody {
if gotBody[k] != want {
t.Fatalf("body[%s] = %v, want %v (full body: %#v)", k, gotBody[k], want, gotBody)
}
}
}
func TestRunSquadMemberSetRoleValidatesRequiredFlags(t *testing.T) {
cmd := newSquadMemberSetRoleTestCmd()
if err := runSquadMemberSetRole(cmd, []string{"squad-123"}); err == nil {
t.Fatal("expected missing --member-id error")
}
cmd = newSquadMemberSetRoleTestCmd()
_ = cmd.Flags().Set("member-id", "member-456")
_ = cmd.Flags().Set("member-type", "invalid")
if err := runSquadMemberSetRole(cmd, []string{"squad-123"}); err == nil {
t.Fatal("expected invalid --member-type error")
}
cmd = newSquadMemberSetRoleTestCmd()
_ = cmd.Flags().Set("member-id", "member-456")
if err := runSquadMemberSetRole(cmd, []string{"squad-123"}); err == nil {
t.Fatal("expected missing --role error")
}
}

View File

@@ -613,6 +613,7 @@ func TestInjectRuntimeConfigAvailableCommandsCoreOnly(t *testing.T) {
"multica issue status <id> <status>",
"multica issue comment add <issue-id>",
"multica issue comment add --help",
"multica squad member set-role <squad-id>",
} {
if !strings.Contains(s, want) {
t.Errorf("AGENTS.md missing core command/help text %q\n---\n%s", want, s)

View File

@@ -452,6 +452,8 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
b.WriteString("- `multica issue metadata list <issue-id> [--output json]` — List every metadata key pinned to an issue. Empty `{}` is normal.\n")
b.WriteString("- `multica issue metadata set <issue-id> --key <k> --value <v> [--type string|number|bool]` — Pin (or overwrite) a single metadata key. The CLI auto-infers JSON primitives, so URLs and plain text are stored as strings — pass `--type number` or `--type bool` only when the semantic type matters.\n")
b.WriteString("- `multica issue metadata delete <issue-id> --key <k>` — Remove a metadata key.\n\n")
b.WriteString("### Squad maintenance\n")
b.WriteString("- `multica squad member set-role <squad-id> --member-id <id> --member-type <agent|member> --role <role> [--output json]` — Change a squad member role in place; use this instead of remove+add when only the role changes.\n\n")
if provider == "codex" {
b.WriteString("## Codex-Specific Comment Formatting\n\n")