mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-22 06:59:19 +02:00
Compare commits
3 Commits
main
...
feat/react
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b48f4d0f97 | ||
|
|
dee81503cb | ||
|
|
ec15501adf |
@@ -11,11 +11,6 @@ if (typeof globalThis.ResizeObserver === "undefined") {
|
||||
} as unknown as typeof ResizeObserver;
|
||||
}
|
||||
|
||||
// jsdom doesn't implement elementFromPoint; input-otp uses it internally.
|
||||
if (typeof document.elementFromPoint !== "function") {
|
||||
document.elementFromPoint = () => null;
|
||||
}
|
||||
|
||||
// jsdom 29 / Node.js 22+ may not provide a proper Web Storage API.
|
||||
// Create a proper localStorage mock if methods are missing.
|
||||
if (
|
||||
|
||||
@@ -198,23 +198,6 @@ describe("LoginPage", () => {
|
||||
expect(screen.getByText(/test@example.com/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("autofocuses the OTP input when the code step opens", async () => {
|
||||
mockSendCode.mockResolvedValueOnce(undefined);
|
||||
renderWithI18n(<LoginPage onSuccess={onSuccess} />);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.type(screen.getByLabelText(/email/i), "test@example.com");
|
||||
await user.click(screen.getByRole("button", { name: /continue/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/check your email/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// The OTP field should be focused on mount so the user can type the code
|
||||
// without clicking it first — important when repeatedly switching accounts.
|
||||
expect(getOTPInput()).toHaveFocus();
|
||||
});
|
||||
|
||||
it("shows error when sendCode fails", async () => {
|
||||
mockSendCode.mockRejectedValueOnce(new Error("Rate limited"));
|
||||
renderWithI18n(<LoginPage onSuccess={onSuccess} />);
|
||||
|
||||
@@ -349,7 +349,6 @@ export function LoginPage({
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center gap-4">
|
||||
<InputOTP
|
||||
autoFocus
|
||||
maxLength={6}
|
||||
value={code}
|
||||
onChange={(value) => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/multica-ai/multica/server/internal/util"
|
||||
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
||||
)
|
||||
@@ -26,8 +25,6 @@ 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.
|
||||
Match the task to each member's listed **skills** and role in the Squad
|
||||
Roster below — prefer the member whose skills cover 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
|
||||
@@ -181,9 +178,7 @@ func renderMemberRow(ctx context.Context, q *db.Queries, m db.SquadMember) strin
|
||||
if ag.ArchivedAt.Valid {
|
||||
return ""
|
||||
}
|
||||
// Agents carry skills; surfacing them lets the leader delegate by
|
||||
// capability instead of guessing from the free-text role label.
|
||||
return formatRosterRow(ag.Name, "agent", role, agentSkillsRosterSegment(ctx, q, m.MemberID), formatMention(ag.Name, "agent", id))
|
||||
return formatRosterRow(ag.Name, "agent", role, formatMention(ag.Name, "agent", id))
|
||||
case "member":
|
||||
user, err := q.GetUser(ctx, m.MemberID)
|
||||
if err != nil {
|
||||
@@ -191,38 +186,14 @@ func renderMemberRow(ctx context.Context, q *db.Queries, m db.SquadMember) strin
|
||||
}
|
||||
// Mention syntax for humans uses the user_id (matches the rest of
|
||||
// the product — see util.MentionRe and frontend mention payloads).
|
||||
// Humans have no Multica skills, so no skills segment is rendered.
|
||||
userID := util.UUIDToString(m.MemberID)
|
||||
return formatRosterRow(user.Name, "member (human)", role, "", formatMention(user.Name, "member", userID))
|
||||
return formatRosterRow(user.Name, "member (human)", role, formatMention(user.Name, "member", userID))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// agentSkillsRosterSegment returns the roster segment describing an agent
|
||||
// member's assigned skills. "skills: a, b" when the agent has skills (the
|
||||
// names are pre-sorted by ListAgentSkillSummaries), "no skills assigned" when
|
||||
// it has none so the leader knows the capability is genuinely absent, and ""
|
||||
// only when the lookup fails — a transient DB error degrades to the prior
|
||||
// name+role row rather than asserting a misleading "no skills". Builtin
|
||||
// multica-* skills are added at runtime (not in agent_skill) and are
|
||||
// deliberately omitted; the leader cares about the configured capabilities.
|
||||
func agentSkillsRosterSegment(ctx context.Context, q *db.Queries, agentID pgtype.UUID) string {
|
||||
skills, err := q.ListAgentSkillSummaries(ctx, agentID)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if len(skills) == 0 {
|
||||
return "no skills assigned"
|
||||
}
|
||||
names := make([]string, 0, len(skills))
|
||||
for _, s := range skills {
|
||||
names = append(names, s.Name)
|
||||
}
|
||||
return "skills: " + strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
func formatRosterRow(name, kind, role, skills, mention string) string {
|
||||
func formatRosterRow(name, kind, role, mention string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("- ")
|
||||
sb.WriteString(name)
|
||||
@@ -233,10 +204,6 @@ func formatRosterRow(name, kind, role, skills, mention string) string {
|
||||
sb.WriteString(role)
|
||||
sb.WriteString(`"`)
|
||||
}
|
||||
if skills != "" {
|
||||
sb.WriteString(" — ")
|
||||
sb.WriteString(skills)
|
||||
}
|
||||
sb.WriteString(" — `")
|
||||
sb.WriteString(mention)
|
||||
sb.WriteString("`\n")
|
||||
|
||||
@@ -150,64 +150,6 @@ func TestBuildSquadLeaderBriefing_FullSquad(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// assignSkillToAgent creates a workspace skill and attaches it to the agent,
|
||||
// registering cleanup for the skill row (agent_skill cascades on skill delete).
|
||||
func assignSkillToAgent(t *testing.T, agentID, skillName string) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
var skillID string
|
||||
if err := testPool.QueryRow(ctx, `
|
||||
INSERT INTO skill (workspace_id, name, description, content, created_by)
|
||||
VALUES ($1, $2, '', '', $3)
|
||||
RETURNING id
|
||||
`, testWorkspaceID, skillName, testUserID).Scan(&skillID); err != nil {
|
||||
t.Fatalf("create skill %s: %v", skillName, err)
|
||||
}
|
||||
t.Cleanup(func() { testPool.Exec(ctx, `DELETE FROM skill WHERE id = $1`, skillID) })
|
||||
if _, err := testPool.Exec(ctx,
|
||||
`INSERT INTO agent_skill (agent_id, skill_id) VALUES ($1, $2)`,
|
||||
agentID, skillID,
|
||||
); err != nil {
|
||||
t.Fatalf("assign skill %s to agent: %v", skillName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildSquadLeaderBriefing_MemberSkillsInRoster locks in the delegation
|
||||
// fix: an agent member's assigned skills appear in the leader roster so the
|
||||
// leader can route by capability. Agents with no skills get an explicit
|
||||
// marker; human members never carry a skills segment.
|
||||
func TestBuildSquadLeaderBriefing_MemberSkillsInRoster(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
leaderID, _ := seededLeaderAgent(t)
|
||||
squad := seedSquadForBriefing(t, leaderID, "Skilled Squad", "")
|
||||
|
||||
skilled := createHandlerTestAgent(t, "Skilled Bot", []byte("[]"))
|
||||
addAgentMember(t, squad.ID, skilled, "backend")
|
||||
// ListAgentSkillSummaries orders by name ASC → "polars" before "stat…".
|
||||
assignSkillToAgent(t, skilled, "polars")
|
||||
assignSkillToAgent(t, skilled, "statistical-analysis")
|
||||
|
||||
plain := createHandlerTestAgent(t, "Plain Bot", []byte("[]"))
|
||||
addAgentMember(t, squad.ID, plain, "")
|
||||
|
||||
memberRowID, userID, userName := seededHumanMember(t)
|
||||
_ = memberRowID
|
||||
addHumanMember(t, squad.ID, userID, "reviewer")
|
||||
|
||||
out := buildSquadLeaderBriefing(ctx, testHandler.Queries, squad)
|
||||
|
||||
if !strings.Contains(out, "skills: polars, statistical-analysis") {
|
||||
t.Errorf("expected skilled member skills in roster, got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "Plain Bot — agent — no skills assigned") {
|
||||
t.Errorf("expected no-skills marker for skill-less agent, got:\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, userName+" — member (human), role: \"reviewer\" — skills:") ||
|
||||
strings.Contains(out, userName+" — member (human), role: \"reviewer\" — no skills") {
|
||||
t.Errorf("human member must not render a skills segment, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSquadLeaderBriefing_OnlyLeader(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
leaderID, _ := seededLeaderAgent(t)
|
||||
@@ -284,34 +226,34 @@ func TestBuildSquadLeaderBriefing_MentionsRoundTrip(t *testing.T) {
|
||||
// claimAndDecodeAgent runs ClaimTaskByRuntime for the given runtime and
|
||||
// returns the agent block of the response. Fails the test on non-200.
|
||||
func claimAndDecodeAgent(t *testing.T, runtimeID string) *TaskAgentData {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
req := newDaemonTokenRequest("POST", "/api/daemon/runtimes/"+runtimeID+"/claim", nil, testWorkspaceID, "test-claim-squad-briefing")
|
||||
req = withURLParam(req, "runtimeId", runtimeID)
|
||||
testHandler.ClaimTaskByRuntime(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("ClaimTaskByRuntime: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp struct {
|
||||
Task *struct {
|
||||
Agent *TaskAgentData `json:"agent"`
|
||||
} `json:"task"`
|
||||
}
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if resp.Task == nil || resp.Task.Agent == nil {
|
||||
t.Fatalf("expected task.agent in response, got: %s", w.Body.String())
|
||||
}
|
||||
return resp.Task.Agent
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
req := newDaemonTokenRequest("POST", "/api/daemon/runtimes/"+runtimeID+"/claim", nil, testWorkspaceID, "test-claim-squad-briefing")
|
||||
req = withURLParam(req, "runtimeId", runtimeID)
|
||||
testHandler.ClaimTaskByRuntime(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("ClaimTaskByRuntime: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp struct {
|
||||
Task *struct {
|
||||
Agent *TaskAgentData `json:"agent"`
|
||||
} `json:"task"`
|
||||
}
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if resp.Task == nil || resp.Task.Agent == nil {
|
||||
t.Fatalf("expected task.agent in response, got: %s", w.Body.String())
|
||||
}
|
||||
return resp.Task.Agent
|
||||
}
|
||||
|
||||
// queueSquadIssueTaskFor creates an issue assigned to the squad and a queued
|
||||
// task for the given (agentID, runtimeID). Returns the issue + task IDs.
|
||||
func queueSquadIssueTaskFor(t *testing.T, squadID, agentID, runtimeID string, issueNumber int) (issueID, taskID string) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
if err := testPool.QueryRow(ctx, `
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
if err := testPool.QueryRow(ctx, `
|
||||
INSERT INTO issue (
|
||||
workspace_id, title, status, priority, creator_id, creator_type,
|
||||
assignee_type, assignee_id, number, position
|
||||
@@ -319,101 +261,101 @@ assignee_type, assignee_id, number, position
|
||||
'squad', $3, $4, 0)
|
||||
RETURNING id
|
||||
`, testWorkspaceID, testUserID, squadID, issueNumber).Scan(&issueID); err != nil {
|
||||
t.Fatalf("create squad-assigned issue: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { testPool.Exec(ctx, `DELETE FROM issue WHERE id = $1`, issueID) })
|
||||
t.Fatalf("create squad-assigned issue: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { testPool.Exec(ctx, `DELETE FROM issue WHERE id = $1`, issueID) })
|
||||
|
||||
if err := testPool.QueryRow(ctx, `
|
||||
if err := testPool.QueryRow(ctx, `
|
||||
INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority)
|
||||
VALUES ($1, $2, $3, 'queued', 0)
|
||||
RETURNING id
|
||||
`, agentID, runtimeID, issueID).Scan(&taskID); err != nil {
|
||||
t.Fatalf("queue task: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { testPool.Exec(ctx, `DELETE FROM agent_task_queue WHERE id = $1`, taskID) })
|
||||
return
|
||||
t.Fatalf("queue task: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { testPool.Exec(ctx, `DELETE FROM agent_task_queue WHERE id = $1`, taskID) })
|
||||
return
|
||||
}
|
||||
|
||||
// TestClaimTask_LeaderGetsBriefing — when the squad leader claims a task on
|
||||
// a squad-assigned issue, the response's agent.instructions must include
|
||||
// the Operating Protocol + Roster + user instructions.
|
||||
func TestClaimTask_LeaderGetsBriefing(t *testing.T) {
|
||||
if testHandler == nil {
|
||||
t.Skip("database not available")
|
||||
}
|
||||
ctx := context.Background()
|
||||
if testHandler == nil {
|
||||
t.Skip("database not available")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
var leaderID, runtimeID string
|
||||
if err := testPool.QueryRow(ctx,
|
||||
`SELECT id, runtime_id FROM agent WHERE workspace_id = $1 ORDER BY created_at ASC LIMIT 1`,
|
||||
testWorkspaceID,
|
||||
).Scan(&leaderID, &runtimeID); err != nil {
|
||||
t.Fatalf("get leader agent: %v", err)
|
||||
}
|
||||
var leaderID, runtimeID string
|
||||
if err := testPool.QueryRow(ctx,
|
||||
`SELECT id, runtime_id FROM agent WHERE workspace_id = $1 ORDER BY created_at ASC LIMIT 1`,
|
||||
testWorkspaceID,
|
||||
).Scan(&leaderID, &runtimeID); err != nil {
|
||||
t.Fatalf("get leader agent: %v", err)
|
||||
}
|
||||
|
||||
squad := seedSquadForBriefing(t, leaderID, "Briefing Claim Squad", "Be terse.")
|
||||
squad := seedSquadForBriefing(t, leaderID, "Briefing Claim Squad", "Be terse.")
|
||||
|
||||
helper := createHandlerTestAgent(t, "Briefing Helper", []byte("[]"))
|
||||
addAgentMember(t, squad.ID, helper, "implementer")
|
||||
helper := createHandlerTestAgent(t, "Briefing Helper", []byte("[]"))
|
||||
addAgentMember(t, squad.ID, helper, "implementer")
|
||||
|
||||
queueSquadIssueTaskFor(t, util.UUIDToString(squad.ID), leaderID, runtimeID, 95001)
|
||||
queueSquadIssueTaskFor(t, util.UUIDToString(squad.ID), leaderID, runtimeID, 95001)
|
||||
|
||||
agent := claimAndDecodeAgent(t, runtimeID)
|
||||
for _, want := range []string{
|
||||
"## Squad Operating Protocol",
|
||||
"## Squad Roster",
|
||||
"Leader (you):",
|
||||
"## Squad Instructions (Briefing Claim Squad)",
|
||||
"Be terse.",
|
||||
"`[@Briefing Helper](mention://agent/" + helper + ")`",
|
||||
} {
|
||||
if !strings.Contains(agent.Instructions, want) {
|
||||
t.Errorf("expected agent.instructions to contain %q\n--- instructions ---\n%s", want, agent.Instructions)
|
||||
}
|
||||
}
|
||||
agent := claimAndDecodeAgent(t, runtimeID)
|
||||
for _, want := range []string{
|
||||
"## Squad Operating Protocol",
|
||||
"## Squad Roster",
|
||||
"Leader (you):",
|
||||
"## Squad Instructions (Briefing Claim Squad)",
|
||||
"Be terse.",
|
||||
"`[@Briefing Helper](mention://agent/" + helper + ")`",
|
||||
} {
|
||||
if !strings.Contains(agent.Instructions, want) {
|
||||
t.Errorf("expected agent.instructions to contain %q\n--- instructions ---\n%s", want, agent.Instructions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestClaimTask_NonLeaderGetsNoBriefing — when a non-leader squad member
|
||||
// claims a task on a squad-assigned issue, NO briefing is injected.
|
||||
func TestClaimTask_NonLeaderGetsNoBriefing(t *testing.T) {
|
||||
if testHandler == nil {
|
||||
t.Skip("database not available")
|
||||
}
|
||||
ctx := context.Background()
|
||||
if testHandler == nil {
|
||||
t.Skip("database not available")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
var leaderID string
|
||||
if err := testPool.QueryRow(ctx,
|
||||
`SELECT id FROM agent WHERE workspace_id = $1 ORDER BY created_at ASC LIMIT 1`,
|
||||
testWorkspaceID,
|
||||
).Scan(&leaderID); err != nil {
|
||||
t.Fatalf("get leader agent: %v", err)
|
||||
}
|
||||
var leaderID string
|
||||
if err := testPool.QueryRow(ctx,
|
||||
`SELECT id FROM agent WHERE workspace_id = $1 ORDER BY created_at ASC LIMIT 1`,
|
||||
testWorkspaceID,
|
||||
).Scan(&leaderID); err != nil {
|
||||
t.Fatalf("get leader agent: %v", err)
|
||||
}
|
||||
|
||||
squad := seedSquadForBriefing(t, leaderID, "Non-Leader Squad", "Squad guidance.")
|
||||
squad := seedSquadForBriefing(t, leaderID, "Non-Leader Squad", "Squad guidance.")
|
||||
|
||||
// Create a second agent (NOT the leader) with its own runtime so the
|
||||
// claim path picks its task without ambiguity.
|
||||
helperID := createHandlerTestAgent(t, "Non Leader Helper", []byte("[]"))
|
||||
addAgentMember(t, squad.ID, helperID, "")
|
||||
var helperRuntime string
|
||||
if err := testPool.QueryRow(ctx,
|
||||
`SELECT runtime_id FROM agent WHERE id = $1`, helperID,
|
||||
).Scan(&helperRuntime); err != nil {
|
||||
t.Fatalf("get helper runtime: %v", err)
|
||||
}
|
||||
// Create a second agent (NOT the leader) with its own runtime so the
|
||||
// claim path picks its task without ambiguity.
|
||||
helperID := createHandlerTestAgent(t, "Non Leader Helper", []byte("[]"))
|
||||
addAgentMember(t, squad.ID, helperID, "")
|
||||
var helperRuntime string
|
||||
if err := testPool.QueryRow(ctx,
|
||||
`SELECT runtime_id FROM agent WHERE id = $1`, helperID,
|
||||
).Scan(&helperRuntime); err != nil {
|
||||
t.Fatalf("get helper runtime: %v", err)
|
||||
}
|
||||
|
||||
queueSquadIssueTaskFor(t, util.UUIDToString(squad.ID), helperID, helperRuntime, 95002)
|
||||
queueSquadIssueTaskFor(t, util.UUIDToString(squad.ID), helperID, helperRuntime, 95002)
|
||||
|
||||
agent := claimAndDecodeAgent(t, helperRuntime)
|
||||
for _, mustNot := range []string{
|
||||
"Squad Operating Protocol",
|
||||
"Squad Roster",
|
||||
"Squad Instructions (Non-Leader Squad)",
|
||||
} {
|
||||
if strings.Contains(agent.Instructions, mustNot) {
|
||||
t.Errorf("non-leader claim should NOT contain %q\n--- instructions ---\n%s", mustNot, agent.Instructions)
|
||||
}
|
||||
}
|
||||
agent := claimAndDecodeAgent(t, helperRuntime)
|
||||
for _, mustNot := range []string{
|
||||
"Squad Operating Protocol",
|
||||
"Squad Roster",
|
||||
"Squad Instructions (Non-Leader Squad)",
|
||||
} {
|
||||
if strings.Contains(agent.Instructions, mustNot) {
|
||||
t.Errorf("non-leader claim should NOT contain %q\n--- instructions ---\n%s", mustNot, agent.Instructions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid "imported and not used: pgtype" if helpers above are the only users.
|
||||
|
||||
@@ -138,12 +138,7 @@ agent instructions. The briefing includes:
|
||||
- Squad Instructions, only when `instructions` is non-empty.
|
||||
|
||||
Roster entries include member name, member type, mention markdown, and non-empty
|
||||
role. For agent members the roster also lists their assigned skills
|
||||
(`skills: a, b`, or `no skills assigned` when the agent has none) so the leader
|
||||
can delegate by capability instead of guessing from the role label; human
|
||||
members carry no skills segment. Builtin `multica-*` skills are not listed —
|
||||
only the workspace skills explicitly attached to the agent. Archived agent
|
||||
members are skipped from the briefing roster.
|
||||
role. Archived agent members are skipped from the briefing roster.
|
||||
|
||||
## Issue assignment behavior
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ Contracts:
|
||||
Source:
|
||||
|
||||
```text
|
||||
server/internal/handler/squad_briefing.go # buildSquadLeaderBriefing ~104, buildSquadRoster ~121, renderMemberRow ~169, agentSkillsRosterSegment, formatRosterRow
|
||||
server/internal/handler/squad_briefing.go # buildSquadLeaderBriefing ~104, buildSquadRoster ~121, renderMemberRow ~169
|
||||
server/internal/handler/daemon.go # briefing injection ~1187, ~1530
|
||||
```
|
||||
|
||||
@@ -93,10 +93,6 @@ Contracts:
|
||||
(squad_briefing.go:104-117);
|
||||
- `instructions` section appears only when non-empty (squad_briefing.go:110-112);
|
||||
- archived agent members are skipped from roster (squad_briefing.go:178-179);
|
||||
- agent member roster rows list assigned workspace skills via
|
||||
`agentSkillsRosterSegment` (ListAgentSkillSummaries) — "skills: a, b" or
|
||||
"no skills assigned"; builtin multica-* skills are excluded and human
|
||||
members carry no skills segment (squad_briefing.go renderMemberRow);
|
||||
- no traced behavior injects `instructions` into every squad member.
|
||||
|
||||
## Issue Assignment
|
||||
|
||||
Reference in New Issue
Block a user