diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index b2dae0402..e30d05e93 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -260,7 +260,10 @@ export namespace Agent {
return pipe(
await state(),
values(),
- sortBy([(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"]),
+ sortBy(
+ [(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"],
+ [(x) => x.name, "asc"],
+ ),
)
}
diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts
index d7aeb911f..5339691a0 100644
--- a/packages/opencode/src/skill/skill.ts
+++ b/packages/opencode/src/skill/skill.ts
@@ -204,7 +204,7 @@ export namespace Skill {
const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) {
yield* Effect.promise(() => state.ensure())
- const list = Object.values(state.skills)
+ const list = Object.values(state.skills).toSorted((a, b) => a.name.localeCompare(b.name))
if (!agent) return list
return list.filter((skill) => PermissionNext.evaluate("skill", skill.name, agent.permission).action !== "deny")
})
diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts
index 14ecea107..ad2b41f00 100644
--- a/packages/opencode/src/tool/task.ts
+++ b/packages/opencode/src/tool/task.ts
@@ -33,12 +33,13 @@ export const TaskTool = Tool.define("task", async (ctx) => {
const accessibleAgents = caller
? agents.filter((a) => PermissionNext.evaluate("task", a.name, caller.permission).action !== "deny")
: agents
+ const list = accessibleAgents.toSorted((a, b) => a.name.localeCompare(b.name))
const description = DESCRIPTION.replace(
"{agents}",
- accessibleAgents
- .map((a) => `- ${a.name}: ${a.description ?? "This subagent should only be called manually by the user."}`)
- .join("\n"),
+ list.map((a) => `- ${a.name}: ${a.description ?? "This subagent should only be called manually by the user."}`).join(
+ "\n",
+ ),
)
return {
description,
diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts
index d6b6ebb33..60c8e57c9 100644
--- a/packages/opencode/test/agent/agent.test.ts
+++ b/packages/opencode/test/agent/agent.test.ts
@@ -384,6 +384,32 @@ test("multiple custom agents can be defined", async () => {
})
})
+test("Agent.list keeps the default agent first and sorts the rest by name", async () => {
+ await using tmp = await tmpdir({
+ config: {
+ default_agent: "plan",
+ agent: {
+ zebra: {
+ description: "Zebra",
+ mode: "subagent",
+ },
+ alpha: {
+ description: "Alpha",
+ mode: "subagent",
+ },
+ },
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const names = (await Agent.list()).map((a) => a.name)
+ expect(names[0]).toBe("plan")
+ expect(names.slice(1)).toEqual(names.slice(1).toSorted((a, b) => a.localeCompare(b)))
+ },
+ })
+})
+
test("Agent.get returns undefined for non-existent agent", async () => {
await using tmp = await tmpdir()
await Instance.provide({
diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts
new file mode 100644
index 000000000..47f5f6fc2
--- /dev/null
+++ b/packages/opencode/test/session/system.test.ts
@@ -0,0 +1,59 @@
+import { describe, expect, test } from "bun:test"
+import path from "path"
+import { Agent } from "../../src/agent/agent"
+import { Instance } from "../../src/project/instance"
+import { SystemPrompt } from "../../src/session/system"
+import { tmpdir } from "../fixture/fixture"
+
+describe("session.system", () => {
+ test("skills output is sorted by name and stable across calls", async () => {
+ await using tmp = await tmpdir({
+ git: true,
+ init: async (dir) => {
+ for (const [name, description] of [
+ ["zeta-skill", "Zeta skill."],
+ ["alpha-skill", "Alpha skill."],
+ ["middle-skill", "Middle skill."],
+ ]) {
+ const skillDir = path.join(dir, ".opencode", "skill", name)
+ await Bun.write(
+ path.join(skillDir, "SKILL.md"),
+ `---
+name: ${name}
+description: ${description}
+---
+
+# ${name}
+`,
+ )
+ }
+ },
+ })
+
+ const home = process.env.OPENCODE_TEST_HOME
+ process.env.OPENCODE_TEST_HOME = tmp.path
+
+ try {
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const build = await Agent.get("build")
+ const first = await SystemPrompt.skills(build!)
+ const second = await SystemPrompt.skills(build!)
+
+ expect(first).toBe(second)
+
+ const alpha = first!.indexOf("alpha-skill")
+ const middle = first!.indexOf("middle-skill")
+ const zeta = first!.indexOf("zeta-skill")
+
+ expect(alpha).toBeGreaterThan(-1)
+ expect(middle).toBeGreaterThan(alpha)
+ expect(zeta).toBeGreaterThan(middle)
+ },
+ })
+ } finally {
+ process.env.OPENCODE_TEST_HOME = home
+ }
+ })
+})
diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts
index 7cfaee135..f622341d3 100644
--- a/packages/opencode/test/tool/skill.test.ts
+++ b/packages/opencode/test/tool/skill.test.ts
@@ -54,6 +54,56 @@ description: Skill for tool tests.
}
})
+ test("description sorts skills by name and is stable across calls", async () => {
+ await using tmp = await tmpdir({
+ git: true,
+ init: async (dir) => {
+ for (const [name, description] of [
+ ["zeta-skill", "Zeta skill."],
+ ["alpha-skill", "Alpha skill."],
+ ["middle-skill", "Middle skill."],
+ ]) {
+ const skillDir = path.join(dir, ".opencode", "skill", name)
+ await Bun.write(
+ path.join(skillDir, "SKILL.md"),
+ `---
+name: ${name}
+description: ${description}
+---
+
+# ${name}
+`,
+ )
+ }
+ },
+ })
+
+ const home = process.env.OPENCODE_TEST_HOME
+ process.env.OPENCODE_TEST_HOME = tmp.path
+
+ try {
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const first = await SkillTool.init()
+ const second = await SkillTool.init()
+
+ expect(first.description).toBe(second.description)
+
+ const alpha = first.description.indexOf("**alpha-skill**: Alpha skill.")
+ const middle = first.description.indexOf("**middle-skill**: Middle skill.")
+ const zeta = first.description.indexOf("**zeta-skill**: Zeta skill.")
+
+ expect(alpha).toBeGreaterThan(-1)
+ expect(middle).toBeGreaterThan(alpha)
+ expect(zeta).toBeGreaterThan(middle)
+ },
+ })
+ } finally {
+ process.env.OPENCODE_TEST_HOME = home
+ }
+ })
+
test("execute returns skill content block with files", async () => {
await using tmp = await tmpdir({
git: true,
diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts
new file mode 100644
index 000000000..df319d8de
--- /dev/null
+++ b/packages/opencode/test/tool/task.test.ts
@@ -0,0 +1,45 @@
+import { describe, expect, test } from "bun:test"
+import { Agent } from "../../src/agent/agent"
+import { Instance } from "../../src/project/instance"
+import { TaskTool } from "../../src/tool/task"
+import { tmpdir } from "../fixture/fixture"
+
+describe("tool.task", () => {
+ test("description sorts subagents by name and is stable across calls", async () => {
+ await using tmp = await tmpdir({
+ config: {
+ agent: {
+ zebra: {
+ description: "Zebra agent",
+ mode: "subagent",
+ },
+ alpha: {
+ description: "Alpha agent",
+ mode: "subagent",
+ },
+ },
+ },
+ })
+
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const build = await Agent.get("build")
+ const first = await TaskTool.init({ agent: build })
+ const second = await TaskTool.init({ agent: build })
+
+ expect(first.description).toBe(second.description)
+
+ const alpha = first.description.indexOf("- alpha: Alpha agent")
+ const explore = first.description.indexOf("- explore:")
+ const general = first.description.indexOf("- general:")
+ const zebra = first.description.indexOf("- zebra: Zebra agent")
+
+ expect(alpha).toBeGreaterThan(-1)
+ expect(explore).toBeGreaterThan(alpha)
+ expect(general).toBeGreaterThan(explore)
+ expect(zebra).toBeGreaterThan(general)
+ },
+ })
+ })
+})