mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-28 18:09:14 +02:00
Compare commits
1 Commits
codex/agen
...
agent/agen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce20239742 |
@@ -311,6 +311,39 @@ func TestBuildPromptContainsIssueID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPromptAssignmentWorkflowOwnsIssueOutput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
issueID := "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
prompt := BuildPrompt(Task{
|
||||
IssueID: issueID,
|
||||
Agent: &AgentData{Name: "Local Codex"},
|
||||
}, "codex")
|
||||
|
||||
for _, want := range []string{
|
||||
"Workflow:",
|
||||
"multica issue get " + issueID + " --output json",
|
||||
"multica issue comment list " + issueID + " --output json",
|
||||
"multica issue status " + issueID + " in_progress",
|
||||
"Post final results with `multica issue comment add`",
|
||||
"multica issue comment add " + issueID,
|
||||
"multica issue status " + issueID + " in_review",
|
||||
} {
|
||||
if !strings.Contains(prompt, want) {
|
||||
t.Fatalf("assignment prompt missing %q\n---\n%s", want, prompt)
|
||||
}
|
||||
}
|
||||
|
||||
for _, absent := range []string{
|
||||
"--parent",
|
||||
"Triggering comment",
|
||||
} {
|
||||
if strings.Contains(prompt, absent) {
|
||||
t.Fatalf("assignment prompt should not contain %q\n---\n%s", absent, prompt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPromptNoIssueDetails(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -345,6 +378,10 @@ func TestBuildPromptAutopilotRunOnly(t *testing.T) {
|
||||
"Check dependencies and report outdated packages.",
|
||||
"multica autopilot get autopilot-1 --output json",
|
||||
"Do not run `multica issue get`",
|
||||
"Your final assistant output is captured automatically as the autopilot run result",
|
||||
"Do not use the issue comment/status workflow",
|
||||
"do not call `multica issue comment add` or `multica issue status`",
|
||||
"Keep the final output concise",
|
||||
} {
|
||||
if !strings.Contains(prompt, want) {
|
||||
t.Fatalf("autopilot prompt missing %q\n---\n%s", want, prompt)
|
||||
@@ -379,6 +416,8 @@ func TestBuildPromptCommentTriggered(t *testing.T) {
|
||||
commentContent,
|
||||
"Focus on THIS comment",
|
||||
commentID,
|
||||
"multica issue comment list " + issueID + " --output json",
|
||||
"Find the triggering comment (ID: `" + commentID + "`)",
|
||||
"multica issue comment add " + issueID + " --parent " + commentID,
|
||||
"do NOT reuse --parent values from previous turns",
|
||||
// Silence-as-valid-exit for agent-to-agent loops depends on the
|
||||
@@ -398,6 +437,37 @@ func TestBuildPromptCommentTriggered(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPromptChatWorkflowStaysDirect(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
prompt := BuildPrompt(Task{
|
||||
ChatSessionID: "chat-1",
|
||||
ChatMessage: "summarize the issue queue",
|
||||
}, "codex")
|
||||
|
||||
for _, want := range []string{
|
||||
"chat assistant",
|
||||
"A user is chatting with you directly",
|
||||
"summarize the issue queue",
|
||||
"Chat workflow:",
|
||||
"Use the `multica` CLI",
|
||||
"Reply directly in chat",
|
||||
} {
|
||||
if !strings.Contains(prompt, want) {
|
||||
t.Fatalf("chat prompt missing %q\n---\n%s", want, prompt)
|
||||
}
|
||||
}
|
||||
for _, absent := range []string{
|
||||
"Your assigned issue ID is:",
|
||||
"multica issue comment add",
|
||||
"multica issue status",
|
||||
} {
|
||||
if strings.Contains(prompt, absent) {
|
||||
t.Fatalf("chat prompt should not contain issue workflow %q\n---\n%s", absent, prompt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildPromptCommentTriggeredByAgent covers the agent-to-agent mention
|
||||
// loop signal injected into the per-turn prompt (MUL-1323 / GH#1576). When
|
||||
// the triggering comment was posted by another agent, the prompt must name
|
||||
|
||||
@@ -250,8 +250,8 @@ func renderIssueContext(provider string, ctx TaskContextForEnv) string {
|
||||
|
||||
// renderQuickCreateContext renders issue_context.md for quick-create tasks.
|
||||
// This file carries only task data (user input, skills). Behavioral rules
|
||||
// and guardrails live in AGENTS.md (runtime config) and the per-turn prompt
|
||||
// to avoid redundancy and conflicting instructions.
|
||||
// and guardrails live in the per-turn prompt to avoid redundancy and
|
||||
// conflicting instructions.
|
||||
func renderQuickCreateContext(ctx TaskContextForEnv) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("# Quick Create\n\n")
|
||||
|
||||
@@ -1003,12 +1003,9 @@ func TestPrepareWithRepoContextOpencode(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestInjectRuntimeConfigRequiresExplicitCommentPost ensures the injected
|
||||
// workflow makes "post a comment with results" an explicit, unmissable step in
|
||||
// both the assignment- and comment-triggered branches, plus hard-warns in the
|
||||
// Output section that terminal/log text is not user-visible. Agents were
|
||||
// silently finishing tasks without ever posting their result to the issue; see
|
||||
// MUL-1124. Covering this in a test prevents the guidance from decaying back
|
||||
// into a nested clause again.
|
||||
// stable Output section still tells issue-based agents that terminal/log text
|
||||
// is not user-visible, without embedding the current issue/comment IDs. The
|
||||
// exact issue-specific command now lives in BuildPrompt.
|
||||
func TestInjectRuntimeConfigRequiresExplicitCommentPost(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1035,11 +1032,9 @@ func TestInjectRuntimeConfigRequiresExplicitCommentPost(t *testing.T) {
|
||||
}
|
||||
s := string(data)
|
||||
|
||||
// The workflow must contain an explicit `multica issue comment add`
|
||||
// invocation for this issue — not just a prose mention of posting.
|
||||
mustContain := []string{
|
||||
"multica issue comment add issue-1",
|
||||
"mandatory",
|
||||
"results must be delivered via `multica issue comment add`",
|
||||
"does NOT see your terminal output",
|
||||
}
|
||||
for _, want := range mustContain {
|
||||
if !strings.Contains(s, want) {
|
||||
@@ -1047,15 +1042,13 @@ func TestInjectRuntimeConfigRequiresExplicitCommentPost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The Output section must carry a hard warning that terminal/log
|
||||
// output is not user-visible. This is the second line of defense
|
||||
// in case the agent skips past the workflow steps.
|
||||
for _, want := range []string{
|
||||
"Final results MUST be delivered via `multica issue comment add`",
|
||||
"does NOT see your terminal output",
|
||||
for _, banned := range []string{
|
||||
"multica issue comment add issue-1",
|
||||
"comment-1",
|
||||
"--parent comment-1",
|
||||
} {
|
||||
if !strings.Contains(s, want) {
|
||||
t.Errorf("%s: Output warning missing %q", tc.name, want)
|
||||
if strings.Contains(s, banned) {
|
||||
t.Errorf("%s: CLAUDE.md should not contain per-turn command fragment %q\n---\n%s", tc.name, banned, s)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1072,10 +1065,8 @@ func TestInjectRuntimeConfigRequiresExplicitCommentPost(t *testing.T) {
|
||||
// habit of emitting literal `\n` inside `--content "..."` (MUL-1467).
|
||||
// That mandate landed in the all-provider section and ended up steering
|
||||
// every provider at stdin — which then broke non-ASCII bytes on Windows
|
||||
// shells (#2198 / #2236 / #2376). This rollback keeps the strong
|
||||
// Codex-specific mandate in the Codex-Specific section (pinned by
|
||||
// TestInjectRuntimeConfigCodexLinuxEmphasizesStdin) and leaves the global
|
||||
// section neutral.
|
||||
// shells (#2198 / #2236 / #2376). The strong Codex-specific mandate now
|
||||
// lives in the per-turn prompt helpers, leaving runtime config neutral.
|
||||
//
|
||||
// Not parallel: mutates the package-level runtimeGOOS.
|
||||
func TestInjectRuntimeConfigAvailableCommandsIsNeutral(t *testing.T) {
|
||||
@@ -1134,13 +1125,13 @@ func TestInjectRuntimeConfigAvailableCommandsIsNeutral(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestInjectRuntimeConfigCodexLinuxEmphasizesStdin pins the
|
||||
// Codex-Specific Comment Formatting section's "MUST stdin" mandate on
|
||||
// non-Windows hosts. This is the MUL-1467 / #1795 / #1851 fix scoped
|
||||
// back to where it belongs.
|
||||
// TestInjectRuntimeConfigCodexOmitsCommentFormattingTemplate pins that
|
||||
// Codex-specific comment command templates live in BuildPrompt, not AGENTS.md.
|
||||
// This keeps per-turn parent IDs and HEREDOC snippets from being duplicated
|
||||
// across runtime config and the task prompt.
|
||||
//
|
||||
// Not parallel: mutates the package-level runtimeGOOS.
|
||||
func TestInjectRuntimeConfigCodexLinuxEmphasizesStdin(t *testing.T) {
|
||||
func TestInjectRuntimeConfigCodexOmitsCommentFormattingTemplate(t *testing.T) {
|
||||
saved := runtimeGOOS
|
||||
t.Cleanup(func() { runtimeGOOS = saved })
|
||||
runtimeGOOS = "linux"
|
||||
@@ -1158,34 +1149,31 @@ func TestInjectRuntimeConfigCodexLinuxEmphasizesStdin(t *testing.T) {
|
||||
}
|
||||
s := string(data)
|
||||
|
||||
for _, want := range []string{
|
||||
for _, banned := range []string{
|
||||
"Codex-Specific Comment Formatting",
|
||||
"always use `--content-stdin` with a HEREDOC",
|
||||
"even for short single-line replies",
|
||||
"Never use inline `--content` for agent-authored comments",
|
||||
"Keep the same `--parent` value",
|
||||
"do not rely on `\\n` escapes",
|
||||
"<<'COMMENT'",
|
||||
"comment-1",
|
||||
"--parent comment-1",
|
||||
} {
|
||||
if !strings.Contains(s, want) {
|
||||
t.Errorf("AGENTS.md missing Codex multiline guidance %q\n---\n%s", want, s)
|
||||
if strings.Contains(s, banned) {
|
||||
t.Errorf("AGENTS.md should not contain per-turn Codex formatting %q\n---\n%s", banned, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestInjectRuntimeConfigCodexWindowsUsesContentFile pins that on Windows
|
||||
// the Codex-Specific section directs the agent at `--content-file` instead
|
||||
// of `--content-stdin`. PowerShell 5.1 / cmd.exe re-encode piped HEREDOC
|
||||
// bytes through the active console codepage and silently drop non-ASCII
|
||||
// as `?` before reaching `multica.exe` (#2198 / #2236 / #2376).
|
||||
// TestInjectRuntimeConfigCodexWindowsKeepsCommandsNeutral pins that AGENTS.md
|
||||
// still lists the safe file mode but does not carry a turn-specific
|
||||
// `--content-file` reply template.
|
||||
//
|
||||
// Not parallel: mutates the package-level runtimeGOOS.
|
||||
func TestInjectRuntimeConfigCodexWindowsUsesContentFile(t *testing.T) {
|
||||
func TestInjectRuntimeConfigCodexWindowsKeepsCommandsNeutral(t *testing.T) {
|
||||
saved := runtimeGOOS
|
||||
t.Cleanup(func() { runtimeGOOS = saved })
|
||||
runtimeGOOS = "windows"
|
||||
|
||||
dir := t.TempDir()
|
||||
if _, err := InjectRuntimeConfig(dir, "codex", TaskContextForEnv{IssueID: "issue-1"}); err != nil {
|
||||
if _, err := InjectRuntimeConfig(dir, "codex", TaskContextForEnv{IssueID: "issue-1", TriggerCommentID: "comment-1"}); err != nil {
|
||||
t.Fatalf("InjectRuntimeConfig failed: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(dir, "AGENTS.md"))
|
||||
@@ -1194,25 +1182,28 @@ func TestInjectRuntimeConfigCodexWindowsUsesContentFile(t *testing.T) {
|
||||
}
|
||||
s := string(data)
|
||||
for _, want := range []string{
|
||||
"On Windows, **always write the comment body to a UTF-8 file",
|
||||
"$OutputEncoding",
|
||||
"--content-file",
|
||||
"silently dropping non-ASCII characters as `?`",
|
||||
"--description-file",
|
||||
"The per-turn prompt is the source of truth",
|
||||
} {
|
||||
if !strings.Contains(s, want) {
|
||||
t.Errorf("AGENTS.md missing Codex/Windows file-first guidance %q\n---\n%s", want, s)
|
||||
t.Errorf("AGENTS.md missing neutral command guidance %q\n---\n%s", want, s)
|
||||
}
|
||||
}
|
||||
for _, banned := range []string{
|
||||
"always use `--content-stdin` with a HEREDOC, even for short single-line replies",
|
||||
"Codex-Specific Comment Formatting",
|
||||
"--parent comment-1",
|
||||
"comment-1",
|
||||
"$OutputEncoding",
|
||||
} {
|
||||
if strings.Contains(s, banned) {
|
||||
t.Errorf("AGENTS.md still carries Codex stdin mandate %q on Windows\n---\n%s", banned, s)
|
||||
t.Errorf("AGENTS.md still carries per-turn Codex/Windows guidance %q\n---\n%s", banned, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectRuntimeConfigQuickCreateOutputPrefixAgnostic(t *testing.T) {
|
||||
func TestInjectRuntimeConfigQuickCreateDefersOutputRulesToPrompt(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
|
||||
@@ -1227,20 +1218,21 @@ func TestInjectRuntimeConfigQuickCreateOutputPrefixAgnostic(t *testing.T) {
|
||||
s := string(data)
|
||||
|
||||
for _, want := range []string{
|
||||
"quick-create task",
|
||||
"Created <identifier-or-id>: <title>",
|
||||
"identifier` from JSON output",
|
||||
"Do not assume any workspace issue prefix",
|
||||
"The per-turn prompt is the source of truth",
|
||||
"chat, quick-create, and run-only autopilot tasks",
|
||||
} {
|
||||
if !strings.Contains(s, want) {
|
||||
t.Errorf("quick-create runtime config missing %q\n---\n%s", want, s)
|
||||
t.Errorf("quick-create runtime config missing stable guidance %q\n---\n%s", want, s)
|
||||
}
|
||||
}
|
||||
for _, absent := range []string{
|
||||
"Created MUL-<n>",
|
||||
"Created <identifier-or-id>: <title>",
|
||||
"identifier` from JSON output",
|
||||
"multica issue create --output json",
|
||||
} {
|
||||
if strings.Contains(s, absent) {
|
||||
t.Errorf("quick-create runtime config should not contain %q\n---\n%s", absent, s)
|
||||
t.Errorf("quick-create runtime config should defer per-turn rule %q\n---\n%s", absent, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1267,20 +1259,20 @@ func TestInjectRuntimeConfigAutopilotRunOnlyNoIssueWorkflow(t *testing.T) {
|
||||
s := string(data)
|
||||
|
||||
for _, want := range []string{
|
||||
"Autopilot in run-only mode",
|
||||
"Autopilot run ID: `run-1`",
|
||||
"Check dependencies and report outdated packages.",
|
||||
"multica autopilot get autopilot-1 --output json",
|
||||
"Your final assistant output is captured automatically as the autopilot run result",
|
||||
"The per-turn prompt is the source of truth",
|
||||
"chat, quick-create, and run-only autopilot tasks",
|
||||
} {
|
||||
if !strings.Contains(s, want) {
|
||||
t.Errorf("autopilot runtime config missing %q\n---\n%s", want, s)
|
||||
t.Errorf("autopilot runtime config missing stable guidance %q\n---\n%s", want, s)
|
||||
}
|
||||
}
|
||||
|
||||
for _, absent := range []string{
|
||||
"Run `multica issue get",
|
||||
"Final results MUST be delivered via `multica issue comment add`",
|
||||
"Autopilot run ID: `run-1`",
|
||||
"Check dependencies and report outdated packages.",
|
||||
"multica autopilot get autopilot-1 --output json",
|
||||
"Your final assistant output is captured automatically as the autopilot run result",
|
||||
} {
|
||||
if strings.Contains(s, absent) {
|
||||
t.Errorf("autopilot runtime config should not contain %q\n---\n%s", absent, s)
|
||||
@@ -2696,30 +2688,30 @@ func TestInjectRuntimeConfigMentionLoopHardening(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("workflow-carries-silence-as-exit-and-no-signoff-mention", func(t *testing.T) {
|
||||
t.Run("runtime-config-omits-comment-trigger-workflow", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := readClaudeMD(t, commentTriggerCtx)
|
||||
// The anti-loop signal for CLAUDE.md lives in the numbered workflow
|
||||
// steps (4 + 5), not in a dedicated preamble. Lock in the key phrases
|
||||
// so the signal can't decay back into pure prose again.
|
||||
for _, want := range []string{
|
||||
for _, banned := range []string{
|
||||
"comment-1",
|
||||
"Find the triggering comment",
|
||||
"Decide whether a reply is warranted",
|
||||
"Silence is a valid and preferred way",
|
||||
"Never @mention the agent you are replying to as a thank-you or sign-off",
|
||||
"--parent comment-1",
|
||||
} {
|
||||
if !strings.Contains(s, want) {
|
||||
t.Errorf("comment-triggered CLAUDE.md missing %q", want)
|
||||
if strings.Contains(s, banned) {
|
||||
t.Errorf("comment-triggered CLAUDE.md should not contain per-turn workflow %q\n---\n%s", banned, s)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(s, "The per-turn prompt is the source of truth") {
|
||||
t.Errorf("CLAUDE.md missing per-turn source-of-truth guidance")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// TestInjectRuntimeConfigSquadLeaderNoActionDefersToPrompt verifies that
|
||||
// squad-leader no_action commands are no longer written into AGENTS.md /
|
||||
// CLAUDE.md with task-specific issue IDs. BuildPrompt owns those current-turn
|
||||
// commands.
|
||||
func TestInjectRuntimeConfigSquadLeaderNoActionDefersToPrompt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
@@ -2737,23 +2729,17 @@ func TestInjectRuntimeConfigSquadLeaderCommentTriggeredNoAction(t *testing.T) {
|
||||
}
|
||||
s := string(data)
|
||||
|
||||
// The comment-triggered workflow must contain the squad leader no_action rule.
|
||||
for _, want := range []string{
|
||||
for _, banned := range []string{
|
||||
"Squad leader rule",
|
||||
"DO NOT post any comment",
|
||||
"multica squad activity",
|
||||
"multica squad activity issue-1 no_action",
|
||||
"you MUST exit without posting any comment",
|
||||
"comment-1",
|
||||
} {
|
||||
if !strings.Contains(s, want) {
|
||||
t.Errorf("squad leader comment-triggered CLAUDE.md missing %q", want)
|
||||
if strings.Contains(s, banned) {
|
||||
t.Errorf("squad leader comment-triggered CLAUDE.md should not contain per-turn no_action fragment %q\n---\n%s", banned, s)
|
||||
}
|
||||
}
|
||||
|
||||
// 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",
|
||||
|
||||
@@ -2,11 +2,48 @@ package execenv
|
||||
|
||||
import "fmt"
|
||||
|
||||
// BuildCommentReplyInstructions returns the canonical block telling an agent
|
||||
// how to post its reply for a comment-triggered task. Both the per-turn
|
||||
// prompt (daemon.buildCommentPrompt) and the CLAUDE.md workflow
|
||||
// (InjectRuntimeConfig) call this so the trigger comment ID and the
|
||||
// --parent value cannot drift between surfaces.
|
||||
// BuildIssueResultInstructions returns the canonical per-turn block telling an
|
||||
// agent how to post final results for an assignment-triggered issue task.
|
||||
func BuildIssueResultInstructions(provider, issueID string) string {
|
||||
if issueID == "" {
|
||||
return ""
|
||||
}
|
||||
if runtimeGOOS == "windows" {
|
||||
return fmt.Sprintf(
|
||||
"Post final results with `multica issue comment add`. On Windows, write the comment body to a UTF-8 file with your file-write tool, then post it with `--content-file`.\n\n"+
|
||||
"Use this form:\n\n"+
|
||||
" # 1. Write the result body to a UTF-8 file (e.g. result.md) with your file-write tool.\n"+
|
||||
" # 2. Then run:\n"+
|
||||
" multica issue comment add %s --content-file ./result.md\n\n"+
|
||||
"Do NOT pipe via `--content-stdin` on Windows, and do NOT write literal `\\n` escapes to simulate line breaks.\n",
|
||||
issueID,
|
||||
)
|
||||
}
|
||||
if provider == "codex" {
|
||||
return fmt.Sprintf(
|
||||
"Post final results with `multica issue comment add`. For Codex on Linux/macOS, always use `--content-stdin` with a HEREDOC for agent-authored issue comments, even when the reply is a single line. Do NOT use inline `--content`.\n\n"+
|
||||
"Use this form:\n\n"+
|
||||
" cat <<'COMMENT' | multica issue comment add %s --content-stdin\n"+
|
||||
" First paragraph.\n"+
|
||||
"\n"+
|
||||
" Second paragraph.\n"+
|
||||
" COMMENT\n\n"+
|
||||
"Do NOT write literal `\\n` escapes to simulate line breaks; the HEREDOC preserves real newlines.\n",
|
||||
issueID,
|
||||
)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"Post final results with this form:\n\n"+
|
||||
" multica issue comment add %s --content \"...\"\n\n"+
|
||||
"For multi-line bodies, code blocks, or content with quotes/backticks, prefer `--content-stdin` (pipe a HEREDOC) or `--content-file <path>`.\n",
|
||||
issueID,
|
||||
)
|
||||
}
|
||||
|
||||
// BuildCommentReplyInstructions returns the canonical per-turn block telling
|
||||
// an agent how to post its reply for a comment-triggered task. The trigger
|
||||
// comment ID and --parent command must live in the per-turn prompt so resumed
|
||||
// sessions cannot carry a previous turn's --parent UUID forward.
|
||||
//
|
||||
// The explicit "do not reuse --parent from previous turns" wording exists
|
||||
// because resumed Claude sessions keep prior turns' tool calls in context
|
||||
|
||||
@@ -147,52 +147,138 @@ func TestBuildCommentReplyInstructionsEmptyWhenNoTrigger(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Pins runtimeGOOS to "linux" so the helper output is deterministic.
|
||||
// Provider is "claude" — exercises the non-codex inline path through
|
||||
// InjectRuntimeConfig end-to-end. Not parallel: mutates runtimeGOOS.
|
||||
func TestInjectRuntimeConfigCommentTriggerUsesHelper(t *testing.T) {
|
||||
func TestBuildIssueResultInstructionsCodexLinux(t *testing.T) {
|
||||
saved := runtimeGOOS
|
||||
t.Cleanup(func() { runtimeGOOS = saved })
|
||||
runtimeGOOS = "linux"
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
issueID := "11111111-1111-1111-1111-111111111111"
|
||||
triggerID := "22222222-2222-2222-2222-222222222222"
|
||||
|
||||
ctx := TaskContextForEnv{
|
||||
IssueID: issueID,
|
||||
TriggerCommentID: triggerID,
|
||||
}
|
||||
if _, err := InjectRuntimeConfig(dir, "claude", ctx); err != nil {
|
||||
t.Fatalf("InjectRuntimeConfig failed: %v", err)
|
||||
}
|
||||
got := BuildIssueResultInstructions("codex", issueID)
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(dir, "CLAUDE.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("read CLAUDE.md: %v", err)
|
||||
}
|
||||
|
||||
s := string(content)
|
||||
for _, want := range []string{
|
||||
triggerID,
|
||||
"multica issue comment add " + issueID + " --parent " + triggerID,
|
||||
"do NOT reuse --parent values from previous turns",
|
||||
"multica issue comment add " + issueID + " --content-stdin",
|
||||
"always use `--content-stdin` with a HEREDOC",
|
||||
"<<'COMMENT'",
|
||||
"Do NOT use inline `--content`",
|
||||
} {
|
||||
if !strings.Contains(s, want) {
|
||||
t.Errorf("CLAUDE.md missing %q", want)
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("codex/linux issue result instructions missing %q\n---\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin asserts the
|
||||
// end-to-end CLAUDE.md / AGENTS.md surface for a comment-triggered task on
|
||||
// a Windows daemon — across Codex and non-Codex providers — has no
|
||||
// prescriptive `--content-stdin` directive that could steer the agent at
|
||||
// the broken Windows pipe path.
|
||||
func TestBuildIssueResultInstructionsNonCodexLinux(t *testing.T) {
|
||||
saved := runtimeGOOS
|
||||
t.Cleanup(func() { runtimeGOOS = saved })
|
||||
runtimeGOOS = "linux"
|
||||
|
||||
issueID := "11111111-1111-1111-1111-111111111111"
|
||||
got := BuildIssueResultInstructions("claude", issueID)
|
||||
|
||||
for _, want := range []string{
|
||||
"multica issue comment add " + issueID + " --content \"...\"",
|
||||
"prefer `--content-stdin`",
|
||||
"`--content-file <path>`",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("non-codex/linux issue result instructions missing %q\n---\n%s", want, got)
|
||||
}
|
||||
}
|
||||
for _, banned := range []string{
|
||||
"<<'COMMENT'",
|
||||
"--content-file ./result.md",
|
||||
} {
|
||||
if strings.Contains(got, banned) {
|
||||
t.Fatalf("non-codex/linux issue result instructions should not contain %q\n---\n%s", banned, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildIssueResultInstructionsWindowsUsesContentFile(t *testing.T) {
|
||||
saved := runtimeGOOS
|
||||
t.Cleanup(func() { runtimeGOOS = saved })
|
||||
runtimeGOOS = "windows"
|
||||
|
||||
issueID := "11111111-1111-1111-1111-111111111111"
|
||||
for _, provider := range []string{"codex", "claude", "opencode"} {
|
||||
t.Run(provider, func(t *testing.T) {
|
||||
got := BuildIssueResultInstructions(provider, issueID)
|
||||
for _, want := range []string{
|
||||
"multica issue comment add " + issueID + " --content-file ./result.md",
|
||||
"write the comment body to a UTF-8 file",
|
||||
"Do NOT pipe via `--content-stdin` on Windows",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("%s/windows issue result instructions missing %q\n---\n%s", provider, want, got)
|
||||
}
|
||||
}
|
||||
for _, banned := range []string{
|
||||
"<<'COMMENT'",
|
||||
"cat <<",
|
||||
} {
|
||||
if strings.Contains(got, banned) {
|
||||
t.Errorf("%s/windows issue result instructions should not contain %q\n---\n%s", provider, banned, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Pins runtimeGOOS to "linux" so the AGENTS.md surface is deterministic.
|
||||
// Comment-trigger reply commands now live only in the per-turn prompt.
|
||||
// Not parallel: mutates runtimeGOOS.
|
||||
func TestInjectRuntimeConfigCommentTriggerOmitsPerTurnReplyCommand(t *testing.T) {
|
||||
saved := runtimeGOOS
|
||||
t.Cleanup(func() { runtimeGOOS = saved })
|
||||
runtimeGOOS = "linux"
|
||||
|
||||
issueID := "11111111-1111-1111-1111-111111111111"
|
||||
triggerID := "22222222-2222-2222-2222-222222222222"
|
||||
|
||||
for _, provider := range []string{"claude", "codex", "opencode"} {
|
||||
t.Run(provider, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ctx := TaskContextForEnv{
|
||||
IssueID: issueID,
|
||||
TriggerCommentID: triggerID,
|
||||
}
|
||||
if _, err := InjectRuntimeConfig(dir, provider, ctx); err != nil {
|
||||
t.Fatalf("InjectRuntimeConfig failed: %v", err)
|
||||
}
|
||||
|
||||
fileName := "CLAUDE.md"
|
||||
if provider != "claude" {
|
||||
fileName = "AGENTS.md"
|
||||
}
|
||||
content, err := os.ReadFile(filepath.Join(dir, fileName))
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", fileName, err)
|
||||
}
|
||||
s := string(content)
|
||||
|
||||
for _, banned := range []string{
|
||||
triggerID,
|
||||
"--parent " + triggerID,
|
||||
"<<'COMMENT'",
|
||||
"Find the triggering comment",
|
||||
"always use `--content-stdin` with a HEREDOC",
|
||||
} {
|
||||
if strings.Contains(s, banned) {
|
||||
t.Errorf("%s should not contain per-turn reply command fragment %q\n---\n%s", fileName, banned, s)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestInjectRuntimeConfigWindowsCommentTriggerHasNoPerTurnReplyCommand asserts
|
||||
// the end-to-end CLAUDE.md / AGENTS.md surface for a comment-triggered task on
|
||||
// a Windows daemon has no parent-specific reply command. The Windows-safe
|
||||
// command lives in BuildPrompt for the current turn.
|
||||
//
|
||||
// Not parallel: mutates the package-level runtimeGOOS.
|
||||
func TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin(t *testing.T) {
|
||||
func TestInjectRuntimeConfigWindowsCommentTriggerHasNoPerTurnReplyCommand(t *testing.T) {
|
||||
saved := runtimeGOOS
|
||||
t.Cleanup(func() { runtimeGOOS = saved })
|
||||
runtimeGOOS = "windows"
|
||||
@@ -221,21 +307,18 @@ func TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin(t *testing.T) {
|
||||
s := string(data)
|
||||
|
||||
for _, want := range []string{
|
||||
"multica issue comment add " + issueID + " --parent " + triggerID + " --content-file",
|
||||
"--content-file",
|
||||
"--description-file",
|
||||
"On Windows, write the reply body to a UTF-8 file",
|
||||
"The per-turn prompt is the source of truth",
|
||||
} {
|
||||
if !strings.Contains(s, want) {
|
||||
t.Errorf("%s missing %q\n---\n%s", fileName, want, s)
|
||||
t.Errorf("%s missing stable guidance %q\n---\n%s", fileName, want, s)
|
||||
}
|
||||
}
|
||||
|
||||
// Prescriptive stdin directives must NOT appear anywhere in
|
||||
// the Windows surface. Pin sentence-level substrings (not
|
||||
// bare flag names) so anti-prescriptive prose like "do NOT
|
||||
// pipe via `--content-stdin`" doesn't trip the ban.
|
||||
for _, banned := range []string{
|
||||
triggerID,
|
||||
"--parent " + triggerID + " --content-file",
|
||||
"--parent " + triggerID + " --content-stdin",
|
||||
"always use `--content-stdin` with a HEREDOC, even for short single-line replies",
|
||||
"MUST pipe via stdin",
|
||||
|
||||
@@ -151,11 +151,10 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
||||
// non-UTF-8 codepage and drops non-representable bytes as `?`
|
||||
// (issues #2198 / #2236 / #2376).
|
||||
//
|
||||
// Strong "MUST" wording lives in the Codex-Specific section below
|
||||
// where it actually belongs; non-Codex providers handle inline
|
||||
// escaping correctly and can pick whichever flag suits their
|
||||
// content. The `--content-file` line in the menu doubles as a
|
||||
// pointer at the Windows-safe path.
|
||||
// Strong provider/platform-specific wording lives in the per-turn
|
||||
// prompt helpers where the current issue/comment IDs are available;
|
||||
// the runtime command list stays neutral. The `--content-file` line
|
||||
// in the menu doubles as a pointer at the Windows-safe path.
|
||||
b.WriteString("- `multica issue comment add <issue-id> [--content \"...\" | --content-stdin | --content-file <path>] [--parent <comment-id>] [--attachment <path>]` — Post a comment. Three input modes, pick whichever fits the content:\n")
|
||||
b.WriteString(" - `--content \"...\"` for short single-line text. The CLI decodes `\\n`, `\\r`, `\\t`, `\\\\` so escaped multi-line is OK; do not embed raw newlines in the argument.\n")
|
||||
b.WriteString(" - `--content-stdin` to pipe the body via HEREDOC. Preserves multi-line and special characters verbatim. Cleanest in `bash` / `zsh`.\n")
|
||||
@@ -169,19 +168,6 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
||||
b.WriteString("- `multica autopilot trigger <id>` — Manually trigger an autopilot to run once\n")
|
||||
b.WriteString("- `multica autopilot delete <id>` — Delete an autopilot\n\n")
|
||||
|
||||
if provider == "codex" {
|
||||
b.WriteString("## Codex-Specific Comment Formatting\n\n")
|
||||
if runtimeGOOS == "windows" {
|
||||
b.WriteString("Codex often follows the per-turn reply command literally. On Windows, **always write the comment body to a UTF-8 file with your file-write tool first, then post it with `--content-file <path>`** — do NOT pipe via `--content-stdin`. PowerShell 5.1's `$OutputEncoding` defaults to ASCIIEncoding when piping to a native command, silently dropping non-ASCII characters as `?` before they reach `multica.exe`. Never use inline `--content` for agent-authored comments. ")
|
||||
b.WriteString("Keep the same `--parent` value from the trigger comment when replying. ")
|
||||
b.WriteString("Do not compress a multi-paragraph answer into one line and do not rely on `\\n` escapes.\n\n")
|
||||
} else {
|
||||
b.WriteString("Codex often follows the per-turn reply command literally. For issue comments, always use `--content-stdin` with a HEREDOC, even for short single-line replies. ")
|
||||
b.WriteString("Never use inline `--content` for agent-authored comments. Keep the same `--parent` value from the trigger comment when replying. ")
|
||||
b.WriteString("Do not compress a multi-paragraph answer into one line and do not rely on `\\n` escapes.\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Inject available repositories section.
|
||||
if len(ctx.Repos) > 0 {
|
||||
b.WriteString("## Repositories\n\n")
|
||||
@@ -214,90 +200,11 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
||||
}
|
||||
|
||||
b.WriteString("### Workflow\n\n")
|
||||
|
||||
if ctx.ChatSessionID != "" {
|
||||
// Chat task: interactive assistant mode
|
||||
b.WriteString("**You are in chat mode.** A user is messaging you directly in a chat window.\n\n")
|
||||
b.WriteString("- Respond conversationally and helpfully to the user's message\n")
|
||||
b.WriteString("- You have full access to the `multica` CLI to look up issues, workspace info, members, agents, etc.\n")
|
||||
b.WriteString("- If asked about issues, use `multica issue list --output json` or `multica issue get <id> --output json`\n")
|
||||
b.WriteString("- If asked about the workspace, use `multica workspace get --output json`\n")
|
||||
b.WriteString("- If asked to perform actions (create issues, update status, etc.), use the appropriate CLI commands\n")
|
||||
b.WriteString("- If the task requires code changes, use `multica repo checkout <url>` to get the code first. Use `--ref <branch-or-sha>` when you need an exact revision\n")
|
||||
b.WriteString("- Keep responses concise and direct\n\n")
|
||||
} else if ctx.QuickCreatePrompt != "" {
|
||||
// Quick-create task: detailed field / output rules live in the
|
||||
// per-turn prompt (BuildPrompt → buildQuickCreatePrompt) so they
|
||||
// have a single source of truth. Quick-create is one-shot, so the
|
||||
// per-turn message is always present and the agent reads the rules
|
||||
// from there. We only keep the hard guardrails here so a provider
|
||||
// that doesn't propagate the user message into its working context
|
||||
// (or a resumed session) still avoids the assignment-task workflow
|
||||
// pointing at an empty issue id.
|
||||
b.WriteString("**This task was triggered by quick-create.** There is NO existing Multica issue. Follow the field and output rules in the user message you just received; ignore the default assignment-task workflow.\n\n")
|
||||
b.WriteString("Hard guardrails (apply even if the user message is missing):\n")
|
||||
b.WriteString("- Run exactly one `multica issue create` invocation, then exit.\n")
|
||||
b.WriteString("- Do NOT call `multica issue get`, `multica issue status`, or `multica issue comment add` for this task — there is no issue to query, transition, or comment on. The platform writes the user's success/failure inbox notification automatically based on whether `multica issue create` succeeded.\n")
|
||||
b.WriteString("- If the CLI returns an error, exit with that error as the only output. Do not retry.\n\n")
|
||||
} else if ctx.AutopilotRunID != "" {
|
||||
// Autopilot run_only task: no issue exists, so the agent must not
|
||||
// follow the assignment/comment workflow.
|
||||
b.WriteString("**This task was triggered by an Autopilot in run-only mode.** There is no assigned Multica issue for this run.\n\n")
|
||||
fmt.Fprintf(&b, "- Autopilot run ID: `%s`\n", ctx.AutopilotRunID)
|
||||
if ctx.AutopilotID != "" {
|
||||
fmt.Fprintf(&b, "- Autopilot ID: `%s`\n", ctx.AutopilotID)
|
||||
}
|
||||
if ctx.AutopilotTitle != "" {
|
||||
fmt.Fprintf(&b, "- Autopilot title: %s\n", ctx.AutopilotTitle)
|
||||
}
|
||||
if ctx.AutopilotSource != "" {
|
||||
fmt.Fprintf(&b, "- Trigger source: %s\n", ctx.AutopilotSource)
|
||||
}
|
||||
if ctx.AutopilotTriggerPayload != "" {
|
||||
fmt.Fprintf(&b, "- Trigger payload:\n\n```json\n%s\n```\n", ctx.AutopilotTriggerPayload)
|
||||
}
|
||||
if strings.TrimSpace(ctx.AutopilotDescription) != "" {
|
||||
b.WriteString("\nAutopilot instructions:\n\n")
|
||||
b.WriteString(ctx.AutopilotDescription)
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
if ctx.AutopilotID != "" {
|
||||
fmt.Fprintf(&b, "- Run `multica autopilot get %s --output json` if you need the full autopilot configuration\n", ctx.AutopilotID)
|
||||
}
|
||||
b.WriteString("- Complete the autopilot instructions directly\n")
|
||||
b.WriteString("- Do not run `multica issue get`, `multica issue comment add`, or `multica issue status` for this run unless the autopilot instructions explicitly tell you to create or update an issue\n\n")
|
||||
} else if ctx.TriggerCommentID != "" {
|
||||
// Comment-triggered: focus on reading and replying
|
||||
b.WriteString("**This task was triggered by a NEW comment.** Your primary job is to respond to THIS specific comment, even if you have handled similar requests before in this session.\n\n")
|
||||
fmt.Fprintf(&b, "1. Run `multica issue get %s --output json` to understand the issue context\n", ctx.IssueID)
|
||||
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)
|
||||
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))
|
||||
b.WriteString("7. Do NOT change the issue status unless the comment explicitly asks for it\n\n")
|
||||
} else {
|
||||
// Assignment-triggered: defer to agent Skills for workflow specifics.
|
||||
b.WriteString("You are responsible for managing the issue status throughout your work.\n\n")
|
||||
fmt.Fprintf(&b, "1. Run `multica issue get %s --output json` to understand your task\n", ctx.IssueID)
|
||||
fmt.Fprintf(&b, "2. Run `multica issue comment list %s --output json` to read the full comment history (returns all comments, capped server-side at 2000) — this is mandatory, not optional. Earlier comments often carry context the issue body lacks (e.g. which repo to work in, the prior agent's findings, the reason the issue was reassigned to you). Skipping this step is the most common cause of agents acting on stale or incomplete instructions.\n", ctx.IssueID)
|
||||
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; 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)
|
||||
}
|
||||
fmt.Fprintf(&b, "6. When done, run `multica issue status %s in_review`\n", ctx.IssueID)
|
||||
fmt.Fprintf(&b, "7. If blocked, run `multica issue status %s blocked` and post a comment explaining why\n\n", ctx.IssueID)
|
||||
}
|
||||
b.WriteString("The per-turn prompt is the source of truth for this run's workflow, including current issue IDs, triggering comment IDs, reply parent IDs, output channel, and status changes.\n\n")
|
||||
b.WriteString("- Read and follow the per-turn prompt before acting.\n")
|
||||
b.WriteString("- Do not reuse issue IDs, `--parent` values, or reply commands from a previous turn.\n")
|
||||
b.WriteString("- For issue-based work, final results must be posted with `multica issue comment add` unless the per-turn prompt explicitly says no reply is warranted, the task is `no_action`, or the task is not issue-based.\n")
|
||||
b.WriteString("- For chat, quick-create, and run-only autopilot tasks, follow the per-turn prompt's output rules instead of the issue-comment workflow.\n\n")
|
||||
|
||||
if len(ctx.AgentSkills) > 0 {
|
||||
b.WriteString("## Skills\n\n")
|
||||
@@ -357,25 +264,11 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
||||
b.WriteString("do NOT attempt to work around it. Instead, post a comment mentioning the workspace owner to request the missing functionality.\n\n")
|
||||
|
||||
b.WriteString("## Output\n\n")
|
||||
switch {
|
||||
case ctx.AutopilotRunID != "":
|
||||
b.WriteString("This is a run-only autopilot task, so there may be no issue comment to post. Your final assistant output is captured automatically as the autopilot run result. Keep it concise and state the outcome.\n")
|
||||
case ctx.QuickCreatePrompt != "":
|
||||
b.WriteString("This is a quick-create task. There is NO existing issue to comment on. Your final stdout is captured automatically and the platform writes the user's success/failure inbox notification based on whether `multica issue create` succeeded.\n\n")
|
||||
b.WriteString("- Do NOT call `multica issue comment add` — the issue you just created has no conversation context for this run.\n")
|
||||
b.WriteString("- Print exactly one final line: `Created <identifier-or-id>: <title>` after a successful `multica issue create`. Use the created issue's `identifier` from JSON output when available; otherwise use its `id`. Do not assume any workspace issue prefix such as `MUL-`; workspaces can use custom prefixes.\n")
|
||||
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 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")
|
||||
}
|
||||
b.WriteString("Keep comments concise and natural — state the outcome, not the process.\n")
|
||||
b.WriteString("Good: \"Fixed the login redirect. PR: https://...\"\n")
|
||||
b.WriteString("Bad: \"1. Read the issue 2. Found the bug in auth.go 3. Created branch 4. ...\"\n")
|
||||
b.WriteString("When referencing an issue in a comment, use the issue mention format `[MUL-123](mention://issue/<issue-id>)` so it renders as a clickable link. (Issue mentions have no side effect; only member/agent mentions do — see the Mentions section above.)\n")
|
||||
}
|
||||
b.WriteString("Follow the per-turn prompt for this task's output channel. For issue-based work, the user does NOT see your terminal output, assistant chat text, or run logs; results must be delivered via `multica issue comment add` unless the per-turn prompt explicitly says not to comment.\n\n")
|
||||
b.WriteString("Keep comments concise and natural — state the outcome, not the process.\n")
|
||||
b.WriteString("Good: \"Fixed the login redirect. PR: https://...\"\n")
|
||||
b.WriteString("Bad: \"1. Read the issue 2. Found the bug in auth.go 3. Created branch 4. ...\"\n")
|
||||
b.WriteString("When referencing an issue in a comment, use the issue mention format `[MUL-123](mention://issue/<issue-id>)` so it renders as a clickable link. (Issue mentions have no side effect; only member/agent mentions do — see the Mentions section above.)\n")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -7,13 +7,10 @@ import (
|
||||
"github.com/multica-ai/multica/server/internal/daemon/execenv"
|
||||
)
|
||||
|
||||
// BuildPrompt constructs the task prompt for an agent CLI.
|
||||
// Keep this minimal — detailed instructions live in CLAUDE.md / AGENTS.md
|
||||
// injected by execenv.InjectRuntimeConfig. The provider string is used by
|
||||
// comment-triggered tasks: Codex's per-turn reply template needs the
|
||||
// platform-aware "stdin or file" variant, every other provider gets a
|
||||
// lightweight inline template (or Windows file for any provider on
|
||||
// Windows).
|
||||
// BuildPrompt constructs the per-turn task prompt for an agent CLI. Runtime
|
||||
// config files (CLAUDE.md / AGENTS.md / GEMINI.md) carry stable Multica
|
||||
// runtime guidance; this prompt is the authority for task-specific workflow,
|
||||
// IDs, and reply commands.
|
||||
func BuildPrompt(task Task, provider string) string {
|
||||
if task.ChatSessionID != "" {
|
||||
return buildChatPrompt(task)
|
||||
@@ -27,11 +24,34 @@ func BuildPrompt(task Task, provider string) string {
|
||||
if task.QuickCreatePrompt != "" {
|
||||
return buildQuickCreatePrompt(task)
|
||||
}
|
||||
return buildIssuePrompt(task, provider)
|
||||
}
|
||||
|
||||
// buildIssuePrompt constructs the per-turn prompt for an assignment-triggered
|
||||
// issue task. It carries the issue-specific workflow that used to live in
|
||||
// runtime config so resumed sessions cannot apply stale IDs or output rules.
|
||||
func buildIssuePrompt(task Task, provider string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("You are running as a local coding agent for a Multica workspace.\n\n")
|
||||
fmt.Fprintf(&b, "Your assigned issue ID is: %s\n\n", task.IssueID)
|
||||
fmt.Fprintf(&b, "Start by running `multica issue get %s --output json` to understand your task, then complete it.\n", task.IssueID)
|
||||
fmt.Fprintf(&b, "If you need comment history, `multica issue comment list %s --output json` returns all comments for the issue (server caps at 2000). Pass `--since <RFC3339>` to fetch only comments newer than a known cursor.\n", task.IssueID)
|
||||
b.WriteString("Workflow:\n\n")
|
||||
fmt.Fprintf(&b, "1. Run `multica issue get %s --output json` to understand the task.\n", task.IssueID)
|
||||
fmt.Fprintf(&b, "2. Run `multica issue comment list %s --output json` to read the full comment history. Earlier comments often carry context the issue body lacks.\n", task.IssueID)
|
||||
fmt.Fprintf(&b, "3. Run `multica issue status %s in_progress` before making changes.\n", task.IssueID)
|
||||
b.WriteString("4. Complete the requested work using the repo and skills that apply.\n")
|
||||
if isSquadLeaderTask(task) {
|
||||
fmt.Fprintf(&b, "5. If your evaluation outcome is `no_action`, call `multica squad activity %s no_action --reason \"...\"` and exit without posting a comment. Do not post a comment whose only purpose is to announce no_action or say you are exiting silently.\n", task.IssueID)
|
||||
b.WriteString("6. For `action` or `failed` outcomes, post your final result as an issue comment. Text in your terminal or run logs is not delivered to the user.\n\n")
|
||||
} else {
|
||||
b.WriteString("5. Post your final result as an issue comment. Text in your terminal or run logs is not delivered to the user.\n\n")
|
||||
}
|
||||
b.WriteString(execenv.BuildIssueResultInstructions(provider, task.IssueID))
|
||||
b.WriteString("\n")
|
||||
if isSquadLeaderTask(task) {
|
||||
fmt.Fprintf(&b, "7. When done, run `multica issue status %s in_review`. If blocked, run `multica issue status %s blocked` and post a comment explaining why.\n", task.IssueID, task.IssueID)
|
||||
} else {
|
||||
fmt.Fprintf(&b, "6. When done, run `multica issue status %s in_review`. If blocked, run `multica issue status %s blocked` and post a comment explaining why.\n", task.IssueID, task.IssueID)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -142,22 +162,40 @@ func buildCommentPrompt(task Task, provider string) string {
|
||||
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") {
|
||||
if isSquadLeaderTask(task) {
|
||||
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)
|
||||
fmt.Fprintf(&b, "If you need comment history, `multica issue comment list %s --output json` returns all comments for the issue (server caps at 2000). Pass `--since <RFC3339>` to fetch only comments newer than a known cursor.\n\n", task.IssueID)
|
||||
b.WriteString("Workflow:\n\n")
|
||||
fmt.Fprintf(&b, "1. Run `multica issue get %s --output json` to understand the issue context.\n", task.IssueID)
|
||||
fmt.Fprintf(&b, "2. Run `multica issue comment list %s --output json` to read the conversation (returns all comments, capped server-side at 2000). Pass `--since <RFC3339>` only when incrementally polling from a known cursor.\n", task.IssueID)
|
||||
fmt.Fprintf(&b, "3. Find the triggering comment (ID: `%s`) and answer THIS comment, not an older one.\n", task.TriggerCommentID)
|
||||
if isSquadLeaderTask(task) {
|
||||
fmt.Fprintf(&b, "4. Decide whether a reply is warranted. If your evaluation outcome is `no_action`, call `multica squad activity %s no_action --reason \"...\"` and exit without posting any comment. For `action` or `failed` outcomes, post the result as a normal reply.\n", task.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 as a normal reply. If the triggering comment was a pure acknowledgment, thanks, or sign-off from another agent and you produced no work this turn, do not post a reply.\n")
|
||||
}
|
||||
b.WriteString("5. If a reply is warranted, do the requested work first, then post the result as a comment. Do not @mention the agent you are replying to as a thank-you or sign-off.\n")
|
||||
b.WriteString("6. Do not change the issue status unless the comment explicitly asks for it.\n\n")
|
||||
b.WriteString(execenv.BuildCommentReplyInstructions(provider, task.IssueID, task.TriggerCommentID))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func isSquadLeaderTask(task Task) bool {
|
||||
return task.Agent != nil && strings.Contains(task.Agent.Instructions, "## Squad Operating Protocol")
|
||||
}
|
||||
|
||||
// buildChatPrompt constructs a prompt for interactive chat tasks.
|
||||
func buildChatPrompt(task Task) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("You are running as a chat assistant for a Multica workspace.\n")
|
||||
b.WriteString("A user is chatting with you directly. Respond to their message.\n\n")
|
||||
fmt.Fprintf(&b, "User message:\n%s\n", task.ChatMessage)
|
||||
b.WriteString("\nChat workflow:\n")
|
||||
b.WriteString("- Use the `multica` CLI when you need to look up issues, workspace info, members, agents, or squads.\n")
|
||||
b.WriteString("- If the user asks you to perform an issue action, use the appropriate CLI command.\n")
|
||||
b.WriteString("- If the task requires code changes, use `multica repo checkout <url>` to get the code first.\n")
|
||||
b.WriteString("- Reply directly in chat; do not post an issue comment unless the user explicitly asks for that.\n")
|
||||
// List attachments by id + filename so the agent can fetch them via
|
||||
// the CLI. We deliberately do NOT inline the URL: chat attachments
|
||||
// live behind a signed CDN with a short TTL, so by the time the agent
|
||||
@@ -211,5 +249,9 @@ func buildAutopilotPrompt(task Task) string {
|
||||
b.WriteString("Complete the instructions above.\n")
|
||||
}
|
||||
b.WriteString("Do not run `multica issue get`; this run does not have an issue ID.\n")
|
||||
b.WriteString("\nOutput rules:\n")
|
||||
b.WriteString("- Your final assistant output is captured automatically as the autopilot run result.\n")
|
||||
b.WriteString("- Do not use the issue comment/status workflow for this run: do not call `multica issue comment add` or `multica issue status` unless the autopilot instructions explicitly tell you to create or update an issue.\n")
|
||||
b.WriteString("- Keep the final output concise and state the outcome.\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user