Compare commits

...

1 Commits

Author SHA1 Message Date
yushen
da19aa770f fix: squad leader no_action must not post comment on comment-triggered path
PR #2564 only added IsSquadLeader handling to the assignment-triggered
workflow path and the Output section. When a squad leader is triggered by
a comment (the common case for re-evaluation), the comment-triggered
workflow path had NO squad leader special handling, so the model still
posted comments announcing no_action/silence.

Changes:
- runtime_config.go: Add IsSquadLeader check to comment-triggered step 4
  with explicit prohibition against posting no_action announcement comments
- runtime_config.go: Strengthen Output section from 'may exit silently' to
  'MUST exit without posting any comment' with explicit DO NOT examples
- runtime_config.go: Strengthen assignment-triggered step 5 similarly
- prompt.go: Add squad leader no_action rule to per-turn comment prompt
  when trigger author is an agent and agent instructions contain the
  Squad Operating Protocol marker
- Add tests for both the per-turn prompt and CLAUDE.md generation

Fixes MUL-2168

Co-authored-by: multica-agent <github@multica.ai>
2026-05-14 12:29:58 +08:00
4 changed files with 117 additions and 3 deletions

View File

@@ -401,6 +401,53 @@ func TestBuildPromptCommentTriggeredNoContent(t *testing.T) {
}
}
// TestBuildPromptSquadLeaderNoActionProhibition verifies that when a squad
// leader is triggered by another agent's comment, the per-turn prompt
// explicitly forbids posting a comment whose only purpose is to announce
// no_action or "exiting silently". This is the fix for MUL-2168.
func TestBuildPromptSquadLeaderNoActionProhibition(t *testing.T) {
t.Parallel()
prompt := BuildPrompt(Task{
IssueID: "issue-1",
TriggerCommentID: "comment-1",
TriggerCommentContent: "Progress update: tests passing.",
TriggerAuthorType: "agent",
TriggerAuthorName: "Worker",
Agent: &AgentData{
Name: "Leader",
Instructions: "You lead the team.\n\n## Squad Operating Protocol\n\nYou are the LEADER.",
},
}, "claude")
for _, want := range []string{
"Squad leader no_action rule",
"DO NOT post any comment",
"multica squad activity",
} {
if !strings.Contains(prompt, want) {
t.Fatalf("squad leader prompt missing %q\n---\n%s", want, prompt)
}
}
// Non-squad-leader agent should NOT get the squad leader rule.
nonLeaderPrompt := BuildPrompt(Task{
IssueID: "issue-1",
TriggerCommentID: "comment-1",
TriggerCommentContent: "Progress update: tests passing.",
TriggerAuthorType: "agent",
TriggerAuthorName: "Worker",
Agent: &AgentData{
Name: "Regular",
Instructions: "You are a regular agent.",
},
}, "claude")
if strings.Contains(nonLeaderPrompt, "Squad leader no_action rule") {
t.Fatalf("non-squad-leader prompt should NOT contain squad leader rule\n---\n%s", nonLeaderPrompt)
}
}
func TestIsWorkspaceNotFoundError(t *testing.T) {
t.Parallel()

View File

@@ -2628,3 +2628,62 @@ func TestInjectRuntimeConfigMentionLoopHardening(t *testing.T) {
}
})
}
// TestInjectRuntimeConfigSquadLeaderCommentTriggeredNoAction verifies that
// when IsSquadLeader is true and the task is comment-triggered, the generated
// CLAUDE.md explicitly forbids posting comments that merely announce no_action.
// This is the fix for MUL-2168 — squad leaders were posting "Exiting silently"
// comments because the comment-triggered path lacked the prohibition.
func TestInjectRuntimeConfigSquadLeaderCommentTriggeredNoAction(t *testing.T) {
t.Parallel()
dir := t.TempDir()
ctx := TaskContextForEnv{
IssueID: "issue-1",
TriggerCommentID: "comment-1",
IsSquadLeader: true,
}
if _, err := InjectRuntimeConfig(dir, "claude", ctx); err != nil {
t.Fatalf("InjectRuntimeConfig failed: %v", err)
}
data, err := os.ReadFile(filepath.Join(dir, "CLAUDE.md"))
if err != nil {
t.Fatalf("read CLAUDE.md: %v", err)
}
s := string(data)
// The comment-triggered workflow must contain the squad leader no_action rule.
for _, want := range []string{
"Squad leader rule",
"DO NOT post any comment",
"multica squad activity",
} {
if !strings.Contains(s, want) {
t.Errorf("squad leader comment-triggered CLAUDE.md missing %q", want)
}
}
// The Output section must use strong prohibition language.
if !strings.Contains(s, "you MUST exit without posting any comment") {
t.Errorf("Output section missing strong prohibition for squad leader no_action")
}
// Non-squad-leader should NOT have the squad leader rule in comment-triggered path.
dir2 := t.TempDir()
ctx2 := TaskContextForEnv{
IssueID: "issue-1",
TriggerCommentID: "comment-1",
IsSquadLeader: false,
}
if _, err := InjectRuntimeConfig(dir2, "claude", ctx2); err != nil {
t.Fatalf("InjectRuntimeConfig failed: %v", err)
}
data2, err := os.ReadFile(filepath.Join(dir2, "CLAUDE.md"))
if err != nil {
t.Fatalf("read CLAUDE.md: %v", err)
}
s2 := string(data2)
if strings.Contains(s2, "Squad leader rule") {
t.Errorf("non-squad-leader CLAUDE.md should NOT contain squad leader rule")
}
}

View File

@@ -273,7 +273,12 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
fmt.Fprintf(&b, "2. Run `multica issue comment list %s --output json` to read the conversation (returns all comments, capped server-side at 2000)\n", ctx.IssueID)
b.WriteString(" - For incremental polling, use `--since <RFC3339-timestamp>` to fetch only comments newer than a known cursor\n")
fmt.Fprintf(&b, "3. Find the triggering comment (ID: `%s`) and understand what is being asked — do NOT confuse it with previous comments\n", ctx.TriggerCommentID)
b.WriteString("4. **Decide whether a reply is warranted.** If you produced actual work this turn (investigated, fixed, answered a real question), post the result via step 6 — that is a normal reply, not a noise comment. If the triggering comment was a pure acknowledgment / thanks / sign-off from another agent AND you produced no work this turn, do NOT post a reply — and do NOT post a comment saying 'No reply needed' or similar. Simply exit with no output. Silence is a valid and preferred way to end agent-to-agent conversations.\n")
if ctx.IsSquadLeader {
b.WriteString("4. **Decide whether a reply is warranted.** If you produced actual work this turn (investigated, fixed, answered a real question), post the result via step 6 — that is a normal reply, not a noise comment. If the triggering comment was a pure acknowledgment / thanks / sign-off from another agent AND you produced no work this turn, do NOT post a reply — and do NOT post a comment saying 'No reply needed' or similar. Simply exit with no output. Silence is a valid and preferred way to end agent-to-agent conversations.\n")
fmt.Fprintf(&b, " - **Squad leader rule:** If your evaluation outcome is `no_action`, call `multica squad activity %s no_action --reason \"...\"` and then EXIT IMMEDIATELY. DO NOT post any comment whose only purpose is to announce that you are taking no action, exiting silently, or acknowledging another agent. A comment like \"No action needed\" or \"Exiting silently\" is noise — the `squad activity` call already records your decision in the timeline.\n", ctx.IssueID)
} else {
b.WriteString("4. **Decide whether a reply is warranted.** If you produced actual work this turn (investigated, fixed, answered a real question), post the result via step 6 — that is a normal reply, not a noise comment. If the triggering comment was a pure acknowledgment / thanks / sign-off from another agent AND you produced no work this turn, do NOT post a reply — and do NOT post a comment saying 'No reply needed' or similar. Simply exit with no output. Silence is a valid and preferred way to end agent-to-agent conversations.\n")
}
b.WriteString("5. If a reply IS warranted: do any requested work first, then **decide whether to include any `@mention` link.** The default is NO mention. Only mention when you are escalating to a human owner who is not yet involved, delegating a concrete new sub-task to another agent for the first time, or the user explicitly asked you to loop someone in. Never @mention the agent you are replying to as a thank-you or sign-off.\n")
b.WriteString("6. **If you reply, post it as a comment — this step is mandatory when you reply.** Text in your terminal or run logs is NOT delivered to the user. ")
b.WriteString(BuildCommentReplyInstructions(provider, ctx.IssueID, ctx.TriggerCommentID))
@@ -286,7 +291,7 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
fmt.Fprintf(&b, "3. Run `multica issue status %s in_progress`\n", ctx.IssueID)
b.WriteString("4. Follow your Skills and Agent Identity to complete the task (write code, investigate, etc.)\n")
if ctx.IsSquadLeader {
fmt.Fprintf(&b, "5. **Post your final results as a comment** (unless your outcome is `no_action` — in that case, calling `multica squad activity %s no_action --reason \"...\"` alone is sufficient and you may exit silently): `multica issue comment add %s --content \"...\"`. Your results are only visible to the user if posted via this CLI call; text in your terminal or run logs is NOT delivered.\n", ctx.IssueID, ctx.IssueID)
fmt.Fprintf(&b, "5. **Post your final results as a comment** (unless your outcome is `no_action` — in that case, calling `multica squad activity %s no_action --reason \"...\"` alone is sufficient; you MUST exit without posting any comment. DO NOT post a comment announcing no_action or saying you are exiting silently): `multica issue comment add %s --content \"...\"`. Your results are only visible to the user if posted via this CLI call; text in your terminal or run logs is NOT delivered.\n", ctx.IssueID, ctx.IssueID)
} else {
fmt.Fprintf(&b, "5. **Post your final results as a comment — this step is mandatory**: `multica issue comment add %s --content \"...\"`. Your results are only visible to the user if posted via this CLI call; text in your terminal or run logs is NOT delivered.\n", ctx.IssueID)
}
@@ -357,7 +362,7 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
b.WriteString("- On CLI failure, exit with the CLI error as the only output. The platform translates that into a `quick_create_failed` inbox item carrying the original prompt for the user.\n")
default:
if ctx.IsSquadLeader {
b.WriteString("⚠️ **Final results MUST be delivered via `multica issue comment add`** — unless your outcome is `no_action`. When you evaluate a trigger and decide no action is needed, calling `multica squad activity <issue-id> no_action --reason \"...\"` alone is sufficient; you may exit silently without posting a comment. For all other outcomes (`action`, `failed`), a comment is still mandatory.\n\n")
b.WriteString("⚠️ **Final results MUST be delivered via `multica issue comment add`** — unless your outcome is `no_action`. When you evaluate a trigger and decide no action is needed, calling `multica squad activity <issue-id> no_action --reason \"...\"` alone is sufficient; you MUST exit without posting any comment. DO NOT post a comment that announces no_action, acknowledges another agent, or says you are exiting silently — such comments are noise. For all other outcomes (`action`, `failed`), a comment is still mandatory.\n\n")
} else {
b.WriteString("⚠️ **Final results MUST be delivered via `multica issue comment add`.** The user does NOT see your terminal output, assistant chat text, or run logs — only comments on the issue. A task that finishes without a result comment is invisible to the user, even if the work itself was correct.\n\n")
}

View File

@@ -128,6 +128,9 @@ func buildCommentPrompt(task Task, provider string) string {
fmt.Fprintf(&b, "> %s\n\n", task.TriggerCommentContent)
if task.TriggerAuthorType == "agent" {
b.WriteString("⚠️ The triggering comment was posted by another agent. Decide whether a reply is warranted. If you produced actual work this turn (investigated, fixed something, answered a real question), post the result as a normal reply — that is NOT a noise comment, and the standard rule that final results must be delivered via comment still applies. If the triggering comment was a pure acknowledgment, thanks, or sign-off AND you produced no work this turn, do NOT reply — and do NOT post a comment saying 'No reply needed' or similar. Simply exit with no output. Silence is the preferred way to end agent-to-agent threads. If you do reply, do not @mention the other agent as a sign-off (that re-triggers them and starts a loop).\n\n")
if task.Agent != nil && strings.Contains(task.Agent.Instructions, "## Squad Operating Protocol") {
fmt.Fprintf(&b, "⚠️ **Squad leader no_action rule:** If you decide no action is needed, call `multica squad activity %s no_action --reason \"...\"` and EXIT. DO NOT post any comment — not even one that says \"no action needed\" or \"exiting silently\". The squad activity call records your decision; a comment is redundant noise.\n\n", task.IssueID)
}
}
}
fmt.Fprintf(&b, "Start by running `multica issue get %s --output json` to understand your task, then decide how to proceed.\n\n", task.IssueID)