diff --git a/src/features/team-mode/tools/tasks.test.ts b/src/features/team-mode/tools/tasks.test.ts
new file mode 100644
index 000000000..3781ee6a3
--- /dev/null
+++ b/src/features/team-mode/tools/tasks.test.ts
@@ -0,0 +1,153 @@
+///
+
+import { beforeEach, describe, expect, mock, test } from "bun:test"
+import type { ToolContext } from "@opencode-ai/plugin/tool"
+
+import type { TeamModeConfig } from "../../../config/schema/team-mode"
+import type { OpencodeClient } from "../../../tools/delegate-task/types"
+import type { RuntimeState, Task } from "../types"
+
+const mockClient = {} as OpencodeClient
+
+const createTaskMock = mock(async () => ({ id: "1", subject: "task one" } as Task))
+const listTasksMock = mock(async () => [{ id: "1", status: "pending" } as Task])
+const claimTaskMock = mock(async () => ({ id: "1", status: "claimed" } as Task))
+const updateTaskStatusMock = mock(async (_teamRunId: string, _taskId: string, status: Task["status"]) => ({
+ id: "1",
+ status,
+} as Task))
+const getTaskMock = mock(async () => ({ id: "1", status: "completed" } as Task))
+const loadRuntimeStateMock = mock(async (): Promise => ({
+ version: 1,
+ teamRunId: "team-run-1",
+ teamName: "team-alpha",
+ specSource: "project",
+ createdAt: 1,
+ status: "active",
+ leadSessionId: "lead-session",
+ members: [
+ { name: "lead-member", sessionId: "lead-session", agentType: "leader", status: "running", pendingInjectedMessageIds: [] },
+ { name: "member-a", sessionId: "member-session-a", agentType: "general-purpose", status: "running", pendingInjectedMessageIds: [] },
+ ],
+ shutdownRequests: [],
+ bounds: {
+ maxMembers: 8,
+ maxParallelMembers: 4,
+ maxMessagesPerRun: 10_000,
+ maxWallClockMinutes: 120,
+ maxMemberTurns: 500,
+ },
+}))
+
+mock.module("../team-state-store", () => ({ loadRuntimeState: loadRuntimeStateMock }))
+mock.module("../team-tasklist", () => ({
+ createTask: createTaskMock,
+ listTasks: listTasksMock,
+ claimTask: claimTaskMock,
+ updateTaskStatus: updateTaskStatusMock,
+ getTask: getTaskMock,
+}))
+
+const {
+ createTeamTaskCreateTool,
+ createTeamTaskListTool,
+ createTeamTaskUpdateTool,
+ createTeamTaskGetTool,
+} = await import("./tasks")
+
+function createConfig(): TeamModeConfig {
+ return {
+ enabled: true,
+ tmux_visualization: false,
+ max_parallel_members: 4,
+ max_members: 8,
+ max_messages_per_run: 10_000,
+ max_wall_clock_minutes: 120,
+ max_member_turns: 500,
+ message_payload_max_bytes: 32_768,
+ recipient_unread_max_bytes: 262_144,
+ mailbox_poll_interval_ms: 3_000,
+ }
+}
+
+function createContext(sessionID: string) {
+ return {
+ sessionID,
+ messageID: "message-1",
+ agent: "test-agent",
+ directory: "/tmp/team-mode",
+ worktree: "/tmp/team-mode/worktree",
+ abort: new AbortController().signal,
+ metadata: mock(() => {}),
+ ask: async () => {},
+ } satisfies ToolContext
+}
+
+describe("team task tools", () => {
+ beforeEach(() => {
+ createTaskMock.mockClear()
+ listTasksMock.mockClear()
+ claimTaskMock.mockClear()
+ updateTaskStatusMock.mockClear()
+ getTaskMock.mockClear()
+ loadRuntimeStateMock.mockClear()
+ })
+
+ test("create -> list -> claim -> complete flow", async () => {
+ // given
+ const config = createConfig()
+ const createTool = createTeamTaskCreateTool(config, mockClient)
+ const listTool = createTeamTaskListTool(config, mockClient)
+ const updateTool = createTeamTaskUpdateTool(config, mockClient)
+ const getTool = createTeamTaskGetTool(config, mockClient)
+
+ // when
+ const created = JSON.parse(await createTool.execute({ teamRunId: "team-run-1", subject: "task one", description: "desc" }, createContext("member-session-a")))
+ const listed = JSON.parse(await listTool.execute({ teamRunId: "team-run-1", status: "pending", owner: "member-a" }, createContext("member-session-a")))
+ const claimed = JSON.parse(await updateTool.execute({ teamRunId: "team-run-1", taskId: "1", status: "claimed" }, createContext("member-session-a")))
+ const inProgress = JSON.parse(await updateTool.execute({ teamRunId: "team-run-1", taskId: "1", status: "in_progress", owner: "member-a" }, createContext("member-session-a")))
+ const completed = JSON.parse(await updateTool.execute({ teamRunId: "team-run-1", taskId: "1", status: "completed", owner: "member-a" }, createContext("member-session-a")))
+ const fetched = JSON.parse(await getTool.execute({ teamRunId: "team-run-1", taskId: "1" }, createContext("member-session-a")))
+
+ // then
+ expect(created.taskId).toBe("1")
+ expect(created.task.subject).toBe("task one")
+ expect(listed.tasks).toHaveLength(1)
+ expect(claimed.task.status).toBe("claimed")
+ expect(inProgress.task.status).toBe("in_progress")
+ expect(completed.task.status).toBe("completed")
+ expect(fetched.task.status).toBe("completed")
+ expect(createTaskMock).toHaveBeenCalledWith("team-run-1", expect.objectContaining({ subject: "task one", description: "desc", blockedBy: [], status: "pending" }), config)
+ expect(listTasksMock).toHaveBeenCalledWith("team-run-1", config, { status: "pending", owner: "member-a" })
+ expect(claimTaskMock).toHaveBeenCalledWith("team-run-1", "1", "member-a", config)
+ expect(updateTaskStatusMock).toHaveBeenCalledWith("team-run-1", "1", "in_progress", "member-a", config)
+ expect(updateTaskStatusMock).toHaveBeenCalledWith("team-run-1", "1", "completed", "member-a", config)
+ expect(getTaskMock).toHaveBeenCalledWith("team-run-1", "1", config)
+ })
+
+ test("cross-owner update rejected", async () => {
+ // given
+ const config = createConfig()
+ updateTaskStatusMock.mockImplementationOnce(async () => { throw new Error("CrossOwnerUpdateError") })
+ const updateTool = createTeamTaskUpdateTool(config, mockClient)
+
+ // when
+ const result = updateTool.execute({ teamRunId: "team-run-1", taskId: "1", status: "in_progress", owner: "member-b" }, createContext("member-session-a"))
+
+ // then
+ expect(result).rejects.toThrow("CrossOwnerUpdateError")
+ })
+
+ test("blockedBy enforcement", async () => {
+ // given
+ const config = createConfig()
+ claimTaskMock.mockImplementationOnce(async () => { throw new Error("blocked by 2") })
+ const updateTool = createTeamTaskUpdateTool(config, mockClient)
+
+ // when
+ const result = updateTool.execute({ teamRunId: "team-run-1", taskId: "1", status: "claimed" }, createContext("member-session-a"))
+
+ // then
+ expect(result).rejects.toThrow("blocked by 2")
+ })
+})
diff --git a/src/features/team-mode/tools/tasks.ts b/src/features/team-mode/tools/tasks.ts
new file mode 100644
index 000000000..5fb35305e
--- /dev/null
+++ b/src/features/team-mode/tools/tasks.ts
@@ -0,0 +1,132 @@
+import { tool, type ToolDefinition, type ToolContext } from "@opencode-ai/plugin/tool"
+
+import type { TeamModeConfig } from "../../../config/schema/team-mode"
+import type { OpencodeClient } from "../../../tools/delegate-task/types"
+import { loadRuntimeState } from "../team-state-store"
+import { createTask, getTask, listTasks, updateTaskStatus, claimTask } from "../team-tasklist"
+
+type TeamTaskToolContext = ToolContext & {
+ sessionID?: string
+}
+
+type TeamTaskListFilter = {
+ status?: "pending" | "claimed" | "in_progress" | "completed" | "deleted"
+ owner?: string
+}
+
+type TeamTaskCreateArgs = {
+ teamRunId: string
+ subject: string
+ description: string
+ blockedBy?: string[]
+}
+
+type TeamTaskListArgs = {
+ teamRunId: string
+ status?: TeamTaskListFilter["status"]
+ owner?: string
+}
+
+type TeamTaskUpdateArgs = {
+ teamRunId: string
+ taskId: string
+ status: "pending" | "claimed" | "in_progress" | "completed" | "deleted"
+ owner?: string
+}
+
+type TeamTaskGetArgs = {
+ teamRunId: string
+ taskId: string
+}
+
+async function resolveSenderName(teamRunId: string, config: TeamModeConfig, sessionID: string | undefined): Promise {
+ const runtimeState = await loadRuntimeState(teamRunId, config)
+ const matchedMember = runtimeState.members.find((member) => member.sessionId === sessionID)
+ if (matchedMember) return matchedMember.name
+
+ const leadMember = runtimeState.members.find((member) => member.agentType === "leader")
+ if (leadMember) return leadMember.name
+
+ throw new Error(`team member not found for session ${sessionID ?? "unknown"}`)
+}
+
+export function createTeamTaskCreateTool(config: TeamModeConfig, client: OpencodeClient): ToolDefinition {
+ void client
+
+ return tool({
+ description: "Create a team task.",
+ args: {
+ teamRunId: tool.schema.string().describe("Team run ID"),
+ subject: tool.schema.string().describe("Task subject"),
+ description: tool.schema.string().describe("Task description"),
+ blockedBy: tool.schema.array(tool.schema.string()).optional().describe("Blocking task IDs"),
+ },
+ execute: async (args: TeamTaskCreateArgs): Promise => {
+ const createdTask = await createTask(args.teamRunId, {
+ subject: args.subject,
+ description: args.description,
+ blocks: [],
+ blockedBy: args.blockedBy ?? [],
+ status: "pending",
+ }, config)
+
+ return JSON.stringify({ taskId: createdTask.id, task: createdTask })
+ },
+ })
+}
+
+export function createTeamTaskListTool(config: TeamModeConfig, client: OpencodeClient): ToolDefinition {
+ void client
+
+ return tool({
+ description: "List team tasks.",
+ args: {
+ teamRunId: tool.schema.string().describe("Team run ID"),
+ status: tool.schema.enum(["pending", "claimed", "in_progress", "completed", "deleted"]).optional(),
+ owner: tool.schema.string().optional(),
+ },
+ execute: async (args: TeamTaskListArgs): Promise => {
+ const tasks = await listTasks(args.teamRunId, config, { status: args.status, owner: args.owner })
+ return JSON.stringify({ tasks })
+ },
+ })
+}
+
+export function createTeamTaskUpdateTool(config: TeamModeConfig, client: OpencodeClient): ToolDefinition {
+ void client
+
+ return tool({
+ description: "Update a team task.",
+ args: {
+ teamRunId: tool.schema.string().describe("Team run ID"),
+ taskId: tool.schema.string().describe("Task ID"),
+ status: tool.schema.enum(["pending", "claimed", "in_progress", "completed", "deleted"]).describe("Task status"),
+ owner: tool.schema.string().optional().describe("Task owner"),
+ },
+ execute: async (args: TeamTaskUpdateArgs, ctx?: TeamTaskToolContext): Promise => {
+ const senderName = await resolveSenderName(args.teamRunId, config, ctx?.sessionID)
+
+ const updatedTask = args.status === "claimed"
+ ? await claimTask(args.teamRunId, args.taskId, senderName, config)
+ : await updateTaskStatus(args.teamRunId, args.taskId, args.status, args.owner ?? senderName, config)
+
+ return JSON.stringify({ task: updatedTask })
+ },
+ })
+}
+
+export function createTeamTaskGetTool(config: TeamModeConfig, client: OpencodeClient): ToolDefinition {
+ void client
+
+ return tool({
+ description: "Get a team task.",
+ args: {
+ teamRunId: tool.schema.string().describe("Team run ID"),
+ taskId: tool.schema.string().describe("Task ID"),
+ },
+ execute: async (args: TeamTaskGetArgs): Promise => {
+ const task = await getTask(args.teamRunId, args.taskId, config)
+ return JSON.stringify({ task })
+ },
+ })
+}