From 695abe22b1511a4af1767a8119c7b46f6ee481c5 Mon Sep 17 00:00:00 2001 From: Kirill Korikov <11762090+yourconscience@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:41:18 +0400 Subject: [PATCH] add Qwen Code support --- README.md | 4 +- cmd/dotagents/agents.go | 11 ++ cmd/dotagents/doctor.go | 1 + cmd/dotagents/harness.go | 27 ++++ cmd/dotagents/hooks.go | 20 +++ cmd/dotagents/qwen.go | 202 ++++++++++++++++++++++++++ cmd/dotagents/qwen_test.go | 249 ++++++++++++++++++++++++++++++++ cmd/dotagents/setup_scaffold.go | 1 + docs/roles.md | 6 +- docs/setup.md | 2 +- docs/site/index.html | 16 +- docs/skills.md | 2 + skills/dotagents/SKILL.md | 6 +- 13 files changed, 540 insertions(+), 7 deletions(-) create mode 100644 cmd/dotagents/qwen.go create mode 100644 cmd/dotagents/qwen_test.go diff --git a/README.md b/README.md index e779f89..875867b 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,14 @@ Five surfaces, each rendered into the harness's own format — dotagents does no | Factory Droid | yes | yes | yes | yes | -- | | Hermes | yes | -- | yes | yes | -- | | OpenCode | yes† | yes | yes | -- | -- | +| Qwen Code | yes, config-driven | yes | yes | yes | skills + MCP§ | | OMP (pi fork) | yes | yes | yes | --‡ | -- | | Pi* | yes | --* | --* | -- | -- | \* Vanilla [pi](https://github.com/earendil-works/pi) is skills-only by design; the OMP fork is detected as its own target. † OpenCode reads `~/.agents/skills/` natively; its only hook surface is a JS plugin API. ‡ OMP has no managed hook surface yet; register memory hooks manually if needed. +§ Qwen Code natively loads Agent Plugins v1 skills and MCP servers; dotagents manages those same surfaces without rewriting the plugin. Amp and OpenClaw read the repo's skills via standard conventions but are not managed. A "yes" above only appears after end-to-end verification. @@ -70,7 +72,7 @@ Candidates are inert until you promote them into durable instructions — consol ## Roles -Markdown role definitions in `~/.agents/agents/`, rendered to each harness's native format (Claude Markdown, Codex TOML, Droid). Generic `model` tiers (`haiku`/`sonnet`/`opus`) render natively per family; per-harness overrides pin exact ids. Six starter roles ship with the tool; yours win on name collision. Details in [docs/roles.md](docs/roles.md). +Markdown role definitions in `~/.agents/agents/`, rendered to each harness's native format (Claude Markdown, Codex TOML, Qwen Markdown, Droid). Generic `model` tiers (`haiku`/`sonnet`/`opus`) render natively per family; per-harness overrides pin exact ids. Six starter roles ship with the tool; yours win on name collision. Details in [docs/roles.md](docs/roles.md). ## Commands diff --git a/cmd/dotagents/agents.go b/cmd/dotagents/agents.go index c9e5b07..8533a08 100644 --- a/cmd/dotagents/agents.go +++ b/cmd/dotagents/agents.go @@ -39,6 +39,7 @@ type agentRole struct { OMP ompRoleOptions `yaml:"omp"` Droid droidRoleOptions `yaml:"droid"` Opencode opencodeRoleOptions `yaml:"opencode"` + Qwen qwenRoleOptions `yaml:"qwen"` } func (role *agentRole) UnmarshalYAML(value *yaml.Node) error { @@ -89,6 +90,10 @@ func (role *agentRole) UnmarshalYAML(value *yaml.Node) error { if err := node.Decode(&role.Opencode); err != nil { return err } + case "qwen": + if err := node.Decode(&role.Qwen); err != nil { + return err + } case "tools": tools, err := decodeRoleTools(node) if err != nil { @@ -147,6 +152,12 @@ type opencodeRoleOptions struct { Mode string `yaml:"mode"` } +type qwenRoleOptions struct { + Model string `yaml:"model"` + ApprovalMode string `yaml:"approval_mode"` + Tools []string `yaml:"tools"` +} + var droidToolMapping = map[string][]string{ "bash": {"Execute"}, "edit": {"Edit"}, diff --git a/cmd/dotagents/doctor.go b/cmd/dotagents/doctor.go index 04fd5da..efe6cd3 100644 --- a/cmd/dotagents/doctor.go +++ b/cmd/dotagents/doctor.go @@ -24,6 +24,7 @@ const ( agentOpenCode = "opencode" agentPi = "pi" agentOMP = "omp" + agentQwenCode = "qwen-code" dotagentsSkillsPathValue = "~/.agents/skills" ) diff --git a/cmd/dotagents/harness.go b/cmd/dotagents/harness.go index 3a48bb7..b82ca18 100644 --- a/cmd/dotagents/harness.go +++ b/cmd/dotagents/harness.go @@ -253,6 +253,33 @@ func initHarnesses() { }), Roles: &RolesCapability{Extension: ".md", Render: renderOMPAgentRole}, }, + + agentQwenCode: { + Skills: SkillsConfigDriven, + InspectSkills: func(agent agentConfig, expected map[string]string, agentsSkillRoot string, cfg config, home string) (agentReport, error) { + return inspectQwenAgent(agent, expected, agentsSkillRoot, cfg, home) + }, + Setup: patchQwenConfig, + MCP: mcpTargetPtr(mcpTarget{ + agentName: agentQwenCode, + configPath: qwenSettingsPath, + inspect: inspectJSONMCPServer, + patch: patchJSONMCPServer, + read: readJSONMCPServer, + rootKey: "mcpServers", + }), + Roles: &RolesCapability{Extension: ".md", Render: renderQwenAgentRole}, + Hooks: &hookTarget{ + agentName: agentQwenCode, + inspect: inspectQwenHook, + patch: patchQwenHook, + }, + RootInstructions: &RootInstructionsCapability{ + Path: func(home string) string { return filepath.Join(home, ".qwen", "QWEN.md") }, + Expected: func(repoRoot string) string { return filepath.Join(repoRoot, "AGENTS.md") }, + }, + IntegrationNote: "config-driven via ~/.qwen/settings.json -> skills.directories", + }, } } diff --git a/cmd/dotagents/hooks.go b/cmd/dotagents/hooks.go index 5064734..91da882 100644 --- a/cmd/dotagents/hooks.go +++ b/cmd/dotagents/hooks.go @@ -247,6 +247,23 @@ func patchDroidHook(hook hookConfig, home string) error { return patchNestedJSONHook(activeDroidHooksConfigPath(home), hook) } +func inspectQwenHook(hook hookConfig, home string) (string, error) { + return inspectNestedJSONHook(qwenSettingsPath(home), nativeQwenHook(hook)) +} + +func patchQwenHook(hook hookConfig, home string) error { + return patchNestedJSONHook(qwenSettingsPath(home), nativeQwenHook(hook)) +} + +func nativeQwenHook(hook hookConfig) hookConfig { + // Qwen Code expresses command hook timeouts in milliseconds. Canonical + // dotagents hook timeouts are seconds, matching the other harnesses. + if hook.Timeout > 0 { + hook.Timeout *= 1000 + } + return hook +} + func removeNativeManagedMemoryHooks(home string, root string, prior config, current config) (int, error) { commands := managedMemoryNativeCommands(home, root, prior, current) if len(commands) == 0 { @@ -266,6 +283,9 @@ func removeNativeManagedMemoryHooks(home string, root string, prior config, curr func(home string, commands []string) (bool, error) { return removeGroupedJSONHookCommands(droidLegacyHooksConfigPath(home), commands) }, + func(home string, commands []string) (bool, error) { + return removeGroupedJSONHookCommands(qwenSettingsPath(home), commands) + }, func(home string, commands []string) (bool, error) { return removeSimpleYAMLHookCommands(filepath.Join(home, ".hermes", "config.yaml"), commands) }, diff --git a/cmd/dotagents/qwen.go b/cmd/dotagents/qwen.go new file mode 100644 index 0000000..3b6cc43 --- /dev/null +++ b/cmd/dotagents/qwen.go @@ -0,0 +1,202 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" +) + +func qwenSettingsPath(home string) string { + return filepath.Join(home, ".qwen", "settings.json") +} + +func patchQwenConfig(home string, repoRoot string, _ config) (bool, error) { + configPath := qwenSettingsPath(home) + raw := map[string]interface{}{} + data, err := os.ReadFile(configPath) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return false, fmt.Errorf("read %s: %w", configPath, err) + } + } else if err := parseJSONConfig(configPath, data, &raw); err != nil { + return false, fmt.Errorf("parse %s: %w", configPath, err) + } + + skillsValue, skillsExists := raw["skills"] + skills, skillsValid := skillsValue.(map[string]interface{}) + if skillsExists && skillsValue != nil && !skillsValid { + return false, fmt.Errorf("skills key in %s is not an object", configPath) + } + if !skillsExists || skillsValue == nil { + skills = map[string]interface{}{} + raw["skills"] = skills + } + + target := filepath.Join(repoRoot, "skills") + directoriesValue, directoriesExists := skills["directories"] + directories, directoriesValid := directoriesValue.([]interface{}) + if directoriesExists && directoriesValue != nil && !directoriesValid { + return false, fmt.Errorf("skills.directories in %s is not an array", configPath) + } + for _, value := range directories { + directory, ok := value.(string) + if ok && filepath.Clean(expandPath(directory, home)) == filepath.Clean(target) { + return false, nil + } + } + skills["directories"] = append(directories, hermesExternalDirValue(home, target)) + + out, err := json.MarshalIndent(raw, "", " ") + if err != nil { + return false, fmt.Errorf("marshal %s: %w", configPath, err) + } + out = append(out, '\n') + if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { + return false, fmt.Errorf("create %s: %w", filepath.Dir(configPath), err) + } + if err := os.WriteFile(configPath, out, 0o644); err != nil { + return false, fmt.Errorf("write %s: %w", configPath, err) + } + return true, nil +} + +func qwenHasSkillsDirectory(home string, target string) (bool, error) { + configPath := qwenSettingsPath(home) + data, err := os.ReadFile(configPath) + if errors.Is(err, fs.ErrNotExist) { + return false, nil + } + if err != nil { + return false, fmt.Errorf("read %s: %w", configPath, err) + } + var raw map[string]interface{} + if err := parseJSONConfig(configPath, data, &raw); err != nil { + return false, fmt.Errorf("parse %s: %w", configPath, err) + } + skills, _ := raw["skills"].(map[string]interface{}) + directories, _ := skills["directories"].([]interface{}) + for _, value := range directories { + directory, ok := value.(string) + if ok && filepath.Clean(expandPath(directory, home)) == filepath.Clean(target) { + return true, nil + } + } + return false, nil +} + +func inspectQwenAgent(agent agentConfig, expected map[string]string, agentsSkillRoot string, cfg config, home string) (agentReport, error) { + report := agentReport{ + Name: agent.Name, + SkillRoot: agent.SkillRoot, + AgentRoot: agent.AgentRoot, + ExpectedSkills: expected, + Detected: isDetected(agent), + } + if !report.Detected { + return report, nil + } + + entries, err := os.ReadDir(agent.SkillRoot) + if err == nil { + for _, entry := range entries { + if !strings.HasPrefix(entry.Name(), ".") { + report.External = append(report.External, entry.Name()) + } + } + } else if !errors.Is(err, fs.ErrNotExist) { + return agentReport{}, fmt.Errorf("read %s: %w", agent.SkillRoot, err) + } + + configured, err := qwenHasSkillsDirectory(home, agentsSkillRoot) + if err != nil { + return agentReport{}, err + } + if !configured { + report.Missing = append(report.Missing, "config skills.directories") + report.Adds = append(report.Adds, "config skills.directories") + } + report.Managed = append(report.Managed, sortedKeys(expected)...) + if err := augmentMCPReport(&report, agent, cfg, home); err != nil { + return agentReport{}, err + } + if err := augmentHookReport(&report, agent, cfg, home); err != nil { + return agentReport{}, err + } + if err := inspectAgentRoles(&report, filepath.Dir(agentsSkillRoot), agent); err != nil { + return agentReport{}, err + } + if h := harnessFor(agent.Name); h != nil && h.RootInstructions != nil { + if err := inspectRootInstructions(&report, h.RootInstructions, filepath.Dir(agentsSkillRoot), home); err != nil { + return agentReport{}, err + } + } + + sortReportLists(&report) + report.Synced = isReportSynced(report) + return report, nil +} + +var qwenToolMapping = map[string]string{ + "bash": "run_shell_command", + "edit": "replace", + "glob": "glob", + "grep": "grep_search", + "read": "read_file", + "webfetch": "web_fetch", + "websearch": "web_search", + "write": "write_file", +} + +func renderQwenAgentRole(role agentRole) string { + model := strings.TrimSpace(role.Qwen.Model) + if model == "" { + model = "inherit" + } + tools := role.Qwen.Tools + if len(tools) == 0 { + tools = qwenToolsFor(role.Tools) + } + + var b strings.Builder + b.WriteString("---\n") + writeYAMLScalar(&b, "name", role.Name) + writeYAMLScalar(&b, "description", role.Description) + writeYAMLScalar(&b, "model", model) + writeYAMLScalar(&b, "approvalMode", role.Qwen.ApprovalMode) + if len(tools) > 0 { + b.WriteString("tools:\n") + for _, tool := range tools { + writeYAMLListItem(&b, tool) + } + } + b.WriteString("---\n\n") + b.WriteString("\n\n") + b.WriteString(role.Instructions) + b.WriteString("\n") + return b.String() +} + +func qwenToolsFor(tools []string) []string { + out := make([]string, 0, len(tools)) + seen := make(map[string]struct{}, len(tools)) + for _, tool := range tools { + mapped := qwenToolMapping[strings.ToLower(strings.TrimSpace(tool))] + if mapped == "" { + continue + } + if _, ok := seen[mapped]; ok { + continue + } + seen[mapped] = struct{}{} + out = append(out, mapped) + } + return out +} diff --git a/cmd/dotagents/qwen_test.go b/cmd/dotagents/qwen_test.go new file mode 100644 index 0000000..c8b71b3 --- /dev/null +++ b/cmd/dotagents/qwen_test.go @@ -0,0 +1,249 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "gopkg.in/yaml.v3" +) + +func TestQwenHarnessCapabilitiesAndDefaultDetection(t *testing.T) { + h := harnessFor(agentQwenCode) + if h == nil { + t.Fatal("Qwen Code harness is not registered") + } + if h.Skills != SkillsConfigDriven || h.Setup == nil || h.InspectSkills == nil { + t.Fatalf("Qwen skills capability = %#v, want config-driven setup and inspection", h) + } + if h.MCP == nil || h.Hooks == nil || h.Roles == nil || h.RootInstructions == nil { + t.Fatalf("Qwen capabilities incomplete: %#v", h) + } + + fakePath(t, "qwen") + detected, err := detectDefaultAgents("") + if err != nil { + t.Fatal(err) + } + if len(detected) != 1 || detected[0].Name != agentQwenCode { + t.Fatalf("detected agents = %#v, want only %s", detected, agentQwenCode) + } + if detected[0].SkillRoot != "~/.qwen/skills" || detected[0].AgentRoot != "~/.qwen/agents" { + t.Fatalf("Qwen native roots = %#v", detected[0]) + } +} + +func TestPatchQwenConfigPreservesSettingsAndIsIdempotent(t *testing.T) { + home := t.TempDir() + repoRoot := filepath.Join(home, ".agents") + configPath := qwenSettingsPath(home) + writeSyncTestFile(t, configPath, []byte(`{ + "model": {"name": "keep"}, + "skills": {"disabled": ["legacy"], "directories": ["/shared/skills"]} +}`)) + + changed, err := patchQwenConfig(home, repoRoot, config{}) + if err != nil || !changed { + t.Fatalf("first patch = %v, %v; want true, nil", changed, err) + } + changed, err = patchQwenConfig(home, repoRoot, config{}) + if err != nil || changed { + t.Fatalf("second patch = %v, %v; want false, nil", changed, err) + } + + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatal(err) + } + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + t.Fatal(err) + } + if raw["model"].(map[string]interface{})["name"] != "keep" { + t.Fatalf("unrelated setting changed: %#v", raw) + } + skills := raw["skills"].(map[string]interface{}) + if !reflect.DeepEqual(skills["disabled"], []interface{}{"legacy"}) { + t.Fatalf("skills.disabled changed: %#v", skills) + } + wantDirectories := []interface{}{`/shared/skills`, `~/.agents/skills`} + if !reflect.DeepEqual(skills["directories"], wantDirectories) { + t.Fatalf("skills.directories = %#v, want %#v", skills["directories"], wantDirectories) + } + if ok, err := qwenHasSkillsDirectory(home, filepath.Join(repoRoot, "skills")); err != nil || !ok { + t.Fatalf("qwenHasSkillsDirectory = %v, %v", ok, err) + } +} + +func TestPatchQwenConfigRejectsMalformedSkillsWithoutOverwrite(t *testing.T) { + home := t.TempDir() + path := qwenSettingsPath(home) + original := []byte(`{"skills":"keep"}`) + writeSyncTestFile(t, path, original) + + if _, err := patchQwenConfig(home, filepath.Join(home, ".agents"), config{}); err == nil || !strings.Contains(err.Error(), "not an object") { + t.Fatalf("patch error = %v, want malformed skills error", err) + } + after, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if string(after) != string(original) { + t.Fatalf("malformed settings overwritten: %s", after) + } +} + +func TestQwenMCPPatchSharesSettingsFile(t *testing.T) { + home := t.TempDir() + writeSyncTestFile(t, qwenSettingsPath(home), []byte(`{"skills":{"directories":["~/.agents/skills"]},"ui":{"theme":"keep"}}`)) + server := testMCPServer() + server.Agents = []string{agentQwenCode} + + if err := patchMCPServer(agentQwenCode, server, home); err != nil { + t.Fatal(err) + } + state, err := inspectMCPServer(agentQwenCode, server, home) + if err != nil || state != stateSynced { + t.Fatalf("Qwen MCP state = %q, %v", state, err) + } + data, err := os.ReadFile(qwenSettingsPath(home)) + if err != nil { + t.Fatal(err) + } + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + t.Fatal(err) + } + if raw["ui"].(map[string]interface{})["theme"] != "keep" || raw["skills"] == nil { + t.Fatalf("Qwen MCP patch dropped unrelated settings: %#v", raw) + } +} + +func TestQwenHookPatchUsesMillisecondsAndPreservesSettings(t *testing.T) { + home := t.TempDir() + writeSyncTestFile(t, qwenSettingsPath(home), []byte(`{"ui":{"theme":"keep"},"hooks":{"Stop":[{"hooks":[{"type":"command","command":"echo keep","timeout":10}]}]}}`)) + hook := testHook() + hook.Agents = []string{agentQwenCode} + + if err := patchQwenHook(hook, home); err != nil { + t.Fatal(err) + } + state, err := inspectQwenHook(hook, home) + if err != nil || state != stateSynced { + t.Fatalf("Qwen hook state = %q, %v", state, err) + } + data, err := os.ReadFile(qwenSettingsPath(home)) + if err != nil { + t.Fatal(err) + } + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + t.Fatal(err) + } + groups := raw["hooks"].(map[string]interface{})["Stop"].([]interface{}) + var managed map[string]interface{} + for _, groupRaw := range groups { + group, ok := groupRaw.(map[string]interface{}) + if !ok { + continue + } + for _, itemRaw := range group["hooks"].([]interface{}) { + item := itemRaw.(map[string]interface{}) + if item["command"] == hook.Command { + managed = item + } + } + } + if managed == nil || managed["timeout"] != float64(15000) || managed["type"] != "command" { + t.Fatalf("managed Qwen hook = %#v", managed) + } + if raw["ui"].(map[string]interface{})["theme"] != "keep" { + t.Fatalf("Qwen hook patch dropped settings: %#v", raw) + } +} + +func TestRenderQwenAgentRoleUsesNativeFrontmatter(t *testing.T) { + role := agentRole{ + Name: "reviewer", + Description: "Review changes", + Model: "opus", + Tools: []string{"Read", "Glob", "Grep", "Bash", "Read"}, + Qwen: qwenRoleOptions{ApprovalMode: "plan"}, + Instructions: "Review without editing.", + } + content := renderQwenAgentRole(role) + parts := strings.SplitN(content, "---\n", 3) + if len(parts) != 3 { + t.Fatalf("Qwen role lacks frontmatter:\n%s", content) + } + var frontmatter struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + Model string `yaml:"model"` + ApprovalMode string `yaml:"approvalMode"` + Tools []string `yaml:"tools"` + } + if err := yaml.Unmarshal([]byte(parts[1]), &frontmatter); err != nil { + t.Fatal(err) + } + if frontmatter.Name != role.Name || frontmatter.Description != role.Description || frontmatter.Model != "inherit" || frontmatter.ApprovalMode != "plan" { + t.Fatalf("Qwen frontmatter = %#v", frontmatter) + } + wantTools := []string{"read_file", "glob", "grep_search", "run_shell_command"} + if !reflect.DeepEqual(frontmatter.Tools, wantTools) { + t.Fatalf("Qwen tools = %#v, want %#v", frontmatter.Tools, wantTools) + } + if !strings.Contains(parts[2], generatedAgentMarker) || !strings.Contains(parts[2], role.Instructions) { + t.Fatalf("Qwen role body = %q", parts[2]) + } +} + +func TestQwenSyncEndToEnd(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + fakePath(t, "qwen") + repoRoot := filepath.Join(home, ".agents") + writeSyncTestFile(t, filepath.Join(repoRoot, "dotagents.yaml"), []byte(`version: 1 +agents: + - name: qwen-code + enabled: true + skill_root: ~/.qwen/skills + agent_root: ~/.qwen/agents + detect: qwen +mcp_servers: + - name: local + enabled: true + command: local-mcp + agents: [qwen-code] +hooks: + - name: memory-stop + enabled: true + event: Stop + command: ~/.agents/memory/hooks/stop.sh + timeout: 15 + agents: [qwen-code] +`)) + writeSyncTestFile(t, filepath.Join(repoRoot, "AGENTS.md"), []byte("# Shared instructions\n")) + writeSyncTestFile(t, filepath.Join(repoRoot, "skills", "sample", "SKILL.md"), []byte("---\nname: sample\ndescription: sample skill\n---\n")) + writeSyncTestFile(t, filepath.Join(repoRoot, "agents", "reviewer.md"), []byte("---\nname: reviewer\ndescription: Review code\n---\n\nReview carefully.\n")) + writeSyncTestFile(t, qwenSettingsPath(home), []byte(`{"ui":{"theme":"keep"}}`)) + + if err := runSync(runOptions{ConfigPath: filepath.Join(repoRoot, "dotagents.yaml"), Agents: agentQwenCode}); err != nil { + t.Fatal(err) + } + if err := runStatus(runOptions{ConfigPath: filepath.Join(repoRoot, "dotagents.yaml"), Agents: agentQwenCode}); err != nil { + t.Fatal(err) + } + if target, err := os.Readlink(filepath.Join(home, ".qwen", "QWEN.md")); err != nil || target != filepath.Join(repoRoot, "AGENTS.md") { + t.Fatalf("QWEN.md link = %q, %v", target, err) + } + if _, err := os.Stat(filepath.Join(home, ".qwen", "agents", "reviewer.md")); err != nil { + t.Fatalf("Qwen role missing: %v", err) + } + if _, err := os.Lstat(filepath.Join(home, ".qwen", "skills", "sample")); !os.IsNotExist(err) { + t.Fatalf("config-driven skills should not create a Qwen mirror: %v", err) + } +} diff --git a/cmd/dotagents/setup_scaffold.go b/cmd/dotagents/setup_scaffold.go index 54e9621..2f4a9d3 100644 --- a/cmd/dotagents/setup_scaffold.go +++ b/cmd/dotagents/setup_scaffold.go @@ -153,6 +153,7 @@ func defaultAgentConfigs() []agentConfig { {Name: agentOMP, Enabled: true, SkillRoot: "~/.omp/agent/skills", AgentRoot: "~/.omp/agent/agents", Detect: "omp"}, {Name: agentOpenCode, Enabled: true, SkillRoot: "~/.config/opencode/skills", AgentRoot: "~/.config/opencode/agents", Detect: "opencode"}, {Name: agentPi, Enabled: true, SkillRoot: "~/.pi/agent/skills", Detect: "pi"}, + {Name: agentQwenCode, Enabled: true, SkillRoot: "~/.qwen/skills", AgentRoot: "~/.qwen/agents", Detect: "qwen"}, } } diff --git a/docs/roles.md b/docs/roles.md index eeb19c6..16e3a0e 100644 --- a/docs/roles.md +++ b/docs/roles.md @@ -4,7 +4,7 @@ A role is a Markdown file in `~/.agents/agents/` with frontmatter (`name`, `desc ## Model tiers and overrides -The generic `model` value is a capability tier (`haiku`, `sonnet`, or `opus`). Claude Code, Codex, and Droid render it natively in their own model family. Harnesses without a native tier concept (OMP, OpenCode) render the value verbatim, so set a per-harness override there whenever you need an exact identifier or want to omit the model and inherit the active session model: +The generic `model` value is a capability tier (`haiku`, `sonnet`, or `opus`). Claude Code, Codex, and Droid render it natively in their own model family. Harnesses without a native tier concept use native inheritance or an exact per-harness override: ```yaml model: opus @@ -16,6 +16,9 @@ omp: model: gpt-5.6-luna-high opencode: model: openai/gpt-5.6 +qwen: + model: qwen3-coder-plus + approval_mode: plan ``` ## Rendering targets @@ -26,5 +29,6 @@ opencode: | Codex | TOML | `~/.codex/agents/.toml` | | Factory Droid | Markdown | `~/.factory/droids/.md` | | OMP | YAML frontmatter | `~/.omp/agent/agents/.md` | +| Qwen Code | YAML frontmatter | `~/.qwen/agents/.md` | Roles are regenerated on every `dotagents sync`; edit the canonical `.md`, never the rendered output. diff --git a/docs/setup.md b/docs/setup.md index 3a5ff94..df7fd11 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -49,4 +49,4 @@ dotagents setup --memory memsearch ## Root instructions -`~/.agents/AGENTS.md` is your single root instruction file. During sync, dotagents links it into each harness's native memory path — `~/.claude/CLAUDE.md` for Claude Code, `~/.codex/AGENTS.md` for Codex, `~/.factory/AGENTS.md` for Droid — so an edit in one place reaches every agent. `dotagents status` reports drift, and a file that exists but is not a symlink is never touched without your confirmation. +`~/.agents/AGENTS.md` is your single root instruction file. During sync, dotagents links it into each harness's native memory path — `~/.claude/CLAUDE.md` for Claude Code, `~/.codex/AGENTS.md` for Codex, `~/.factory/AGENTS.md` for Droid, and `~/.qwen/QWEN.md` for Qwen Code — so an edit in one place reaches every agent. `dotagents status` reports drift, and a file that exists but is not a symlink is never touched without your confirmation. diff --git a/docs/site/index.html b/docs/site/index.html index 2919956..4303d83 100644 --- a/docs/site/index.html +++ b/docs/site/index.html @@ -4,7 +4,7 @@ dotagents - public CLI for your private agent config - +