Files
88f6d6272b MUL-8057: feat(agent): run Meta Muse Code as an MSP runtime (#9309)
* feat(agent): run Meta Muse Code as an MSP runtime

Muse Code speaks MSP (Muse Session Protocol) over `muse serve`: JSON-RPC
2.0 on stdio, like ACP, but with its own method vocabulary. A session opens
with session/start or session/resume, work is submitted with turn/start, and
assistant text, reasoning and tool calls arrive as transcript items.

The adapter maps those items onto the Backend contract, reads the terminal
turn outcome, and reports a notFound resume as ResumeRejected so the daemon
retries on a fresh session instead of losing continuity silently.

Approval is a session property, not a launch flag: `muse serve` has no
--yolo, and the CLI documents --yolo as TUI-only. A fresh session is created
with approvalMode=allowAll and a resumed session re-asserts it with
session/setApprovalMode, which is what lets a claimed task run without
waiting for a human. The container and per-task worktree remain the
isolation boundary, as they are for every other backend.

commandId must be UUIDv7 — Muse derives a turn id from it and rejects
anything else — so the adapter mints time-ordered ids rather than v4.

The model catalog comes from the session-free model/list query and the pick
is applied through session/start modelId.

* feat(runtime-profiles): allow muse as a protocol family

A custom runtime profile may only be based on a protocol family the daemon
supports, and the database enforces the same list. Widening both keeps the
create flow, the server validation and the CHECK constraint in lockstep with
agent.SupportedTypes.

* fix(muse): complete runtime integration and lifecycle handling (MUL-8057)

Co-authored-by: multica-agent <[email protected]>

* docs(muse): document runtime setup and support (MUL-8057)

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-19 13:45:42 +08:00

62 lines
1.9 KiB
Go

package execenv
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestMusePrepareDeliversBriefAndSkills(t *testing.T) {
t.Parallel()
workDir := t.TempDir()
briefPath := filepath.Join(workDir, "AGENTS.md")
const repoInstructions = "# Repository rules\nKeep user files.\n"
if err := os.WriteFile(briefPath, []byte(repoInstructions), 0o644); err != nil {
t.Fatal(err)
}
env, err := Prepare(PrepareParams{
WorkspacesRoot: t.TempDir(), WorkspaceID: "muse-workspace", TaskID: "muse-task",
Provider: "muse", LocalWorkDir: workDir,
Task: TaskContextForEnv{
IssueID: "11111111-2222-3333-4444-555555555555",
AgentSkills: []SkillContextForEnv{{Name: "muse-workflow", Content: "Follow the Muse workflow."}},
},
}, discardLogger())
if err != nil {
t.Fatal(err)
}
if _, err := InjectRuntimeConfig(env.WorkDir, "muse", TaskContextForEnv{
IssueID: "11111111-2222-3333-4444-555555555555",
}); err != nil {
t.Fatal(err)
}
brief, err := os.ReadFile(briefPath)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{repoInstructions, "multica issue comment add", "## Output"} {
if !strings.Contains(string(brief), want) {
t.Errorf("runtime brief is missing %q", want)
}
}
skillPath := filepath.Join(workDir, ".agents", "skills", "muse-workflow", "SKILL.md")
skill, err := os.ReadFile(skillPath)
if err != nil || !strings.Contains(string(skill), "Follow the Muse workflow.") {
t.Errorf("native skill missing: content=%q err=%v", skill, err)
}
if err := CleanupSidecars(env.RootDir); err != nil {
t.Fatal(err)
}
if err := CleanupRuntimeConfig(workDir, "muse"); err != nil {
t.Fatal(err)
}
restored, err := os.ReadFile(briefPath)
if err != nil || string(restored) != repoInstructions {
t.Errorf("repository instructions changed after cleanup: %q, %v", restored, err)
}
if _, err := os.Stat(skillPath); !os.IsNotExist(err) {
t.Errorf("managed skill survived cleanup: %v", err)
}
}