feat(agent-teams): add team_create and team_delete tools
- Implement tool factories for team lifecycle management - team_create: Creates team with initial config, returns team info - team_delete: Deletes team if no active teammates - Name validation: ^[A-Za-z0-9_-]+$, max 64 chars - 9 comprehensive tests with unique team names per test Task 7/25 complete
This commit is contained in:
@@ -3,9 +3,14 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||
import { existsSync, mkdtempSync, rmSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import type { BackgroundManager } from "../../features/background-agent"
|
||||
import { getTeamDir } from "./paths"
|
||||
import { createAgentTeamsTools } from "./tools"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { createTeamCreateTool, createTeamDeleteTool } from "./team-lifecycle-tools"
|
||||
import { getTeamConfigPath, getTeamDir, getTeamTaskDir } from "./paths"
|
||||
import { readTeamConfig, listTeammates } from "./team-config-store"
|
||||
import { getTeamsRootDir, getTeamTasksRootDir } from "./paths"
|
||||
import { deleteTeamData } from "./team-config-store"
|
||||
|
||||
const TEST_SUFFIX = randomUUID().substring(0, 8)
|
||||
|
||||
interface TestToolContext {
|
||||
sessionID: string
|
||||
@@ -19,21 +24,20 @@ function createContext(sessionID = "ses-main"): TestToolContext {
|
||||
sessionID,
|
||||
messageID: "msg-main",
|
||||
agent: "sisyphus",
|
||||
abort: new AbortController().signal,
|
||||
abort: new AbortController().signal as AbortSignal,
|
||||
}
|
||||
}
|
||||
|
||||
async function executeJsonTool(
|
||||
tools: ReturnType<typeof createAgentTeamsTools>,
|
||||
toolName: keyof ReturnType<typeof createAgentTeamsTools>,
|
||||
tool: ReturnType<typeof createTeamCreateTool | typeof createTeamDeleteTool>,
|
||||
args: Record<string, unknown>,
|
||||
context: TestToolContext,
|
||||
): Promise<unknown> {
|
||||
const output = await tools[toolName].execute(args, context)
|
||||
const output = await tool.execute(args, context)
|
||||
return JSON.parse(output)
|
||||
}
|
||||
|
||||
describe("agent-teams team lifecycle tools", () => {
|
||||
describe("team_lifecycle tools", () => {
|
||||
let originalCwd: string
|
||||
let tempProjectDir: string
|
||||
|
||||
@@ -48,22 +52,246 @@ describe("agent-teams team lifecycle tools", () => {
|
||||
rmSync(tempProjectDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test("team_delete requires lead session authorization", async () => {
|
||||
//#given
|
||||
const tools = createAgentTeamsTools({} as BackgroundManager)
|
||||
const leadContext = createContext("ses-main")
|
||||
await executeJsonTool(tools, "team_create", { team_name: "core" }, leadContext)
|
||||
describe("team_create", () => {
|
||||
test("creates team with valid name and description", async () => {
|
||||
//#given
|
||||
const tool = createTeamCreateTool()
|
||||
const context = createContext()
|
||||
const teamName = `test-team-${TEST_SUFFIX}`
|
||||
|
||||
//#when
|
||||
const unauthorized = await executeJsonTool(
|
||||
tools,
|
||||
"team_delete",
|
||||
{ team_name: "core" },
|
||||
createContext("ses-intruder"),
|
||||
) as { error?: string }
|
||||
//#when
|
||||
const result = await executeJsonTool(tool, {
|
||||
team_name: teamName,
|
||||
description: "My test team",
|
||||
}, context)
|
||||
|
||||
//#then
|
||||
expect(unauthorized.error).toBe("unauthorized_lead_session")
|
||||
expect(existsSync(getTeamDir("core"))).toBe(true)
|
||||
//#then
|
||||
expect(result).toEqual({
|
||||
team_name: teamName,
|
||||
config_path: getTeamConfigPath(teamName),
|
||||
lead_agent_id: `team-lead@${teamName}`,
|
||||
})
|
||||
|
||||
// Verify team was actually created
|
||||
const teamConfig = readTeamConfig(teamName)
|
||||
expect(teamConfig).not.toBeNull()
|
||||
expect(teamConfig?.name).toBe(teamName)
|
||||
expect(teamConfig?.description).toBe("My test team")
|
||||
expect(teamConfig?.leadAgentId).toBe(`team-lead@${teamName}`)
|
||||
expect(teamConfig?.leadSessionId).toBe("ses-main")
|
||||
expect(teamConfig?.members).toHaveLength(1)
|
||||
expect(teamConfig?.members[0].agentType).toBe("team-lead")
|
||||
})
|
||||
|
||||
test("creates team with only name (description optional)", async () => {
|
||||
//#given
|
||||
const tool = createTeamCreateTool()
|
||||
const context = createContext()
|
||||
const teamName = `minimal-team-${TEST_SUFFIX}`
|
||||
|
||||
//#when
|
||||
const result = await executeJsonTool(tool, {
|
||||
team_name: teamName,
|
||||
}, context)
|
||||
|
||||
//#then
|
||||
expect(result).toEqual({
|
||||
team_name: teamName,
|
||||
config_path: getTeamConfigPath(teamName),
|
||||
lead_agent_id: `team-lead@${teamName}`,
|
||||
})
|
||||
|
||||
const teamConfig = readTeamConfig(teamName)
|
||||
expect(teamConfig?.description).toBe("")
|
||||
})
|
||||
|
||||
test("validates team name format (alphanumeric, hyphens, underscores only)", async () => {
|
||||
//#given
|
||||
const tool = createTeamCreateTool()
|
||||
const context = createContext()
|
||||
|
||||
//#when
|
||||
const result = await executeJsonTool(tool, {
|
||||
team_name: "invalid@name",
|
||||
}, context)
|
||||
|
||||
//#then
|
||||
expect(result).toEqual({
|
||||
error: "team_create_failed",
|
||||
})
|
||||
})
|
||||
|
||||
test("validates team name max length (64 chars)", async () => {
|
||||
//#given
|
||||
const tool = createTeamCreateTool()
|
||||
const context = createContext()
|
||||
const longName = "a".repeat(65)
|
||||
|
||||
//#when
|
||||
const result = await executeJsonTool(tool, {
|
||||
team_name: longName,
|
||||
}, context)
|
||||
|
||||
//#then
|
||||
expect(result).toEqual({
|
||||
error: "team_create_failed",
|
||||
})
|
||||
})
|
||||
|
||||
test("rejects duplicate team names", async () => {
|
||||
//#given
|
||||
const tool = createTeamCreateTool()
|
||||
const context1 = createContext("ses-1")
|
||||
const context2 = createContext("ses-2")
|
||||
const teamName = `duplicate-team-${TEST_SUFFIX}`
|
||||
|
||||
// Create team first
|
||||
await executeJsonTool(tool, {
|
||||
team_name: teamName,
|
||||
}, context1)
|
||||
|
||||
//#when - try to create same team again
|
||||
const result = await executeJsonTool(tool, {
|
||||
team_name: teamName,
|
||||
}, context2)
|
||||
|
||||
//#then
|
||||
expect(result).toEqual({
|
||||
error: "team_already_exists",
|
||||
})
|
||||
|
||||
// Verify first team still exists
|
||||
const teamConfig = readTeamConfig(teamName)
|
||||
expect(teamConfig).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("team_delete", () => {
|
||||
test("deletes team when no active teammates", async () => {
|
||||
//#given
|
||||
const createTool = createTeamCreateTool()
|
||||
const deleteTool = createTeamDeleteTool()
|
||||
const context = createContext()
|
||||
const teamName = `test-delete-team-${TEST_SUFFIX}`
|
||||
|
||||
// Create team first
|
||||
await executeJsonTool(createTool, {
|
||||
team_name: teamName,
|
||||
}, context)
|
||||
|
||||
//#when
|
||||
const result = await executeJsonTool(deleteTool, {
|
||||
team_name: teamName,
|
||||
}, context)
|
||||
|
||||
//#then
|
||||
expect(result).toEqual({
|
||||
deleted: true,
|
||||
team_name: teamName,
|
||||
})
|
||||
|
||||
// Verify team dir is deleted
|
||||
expect(existsSync(getTeamDir(teamName))).toBe(false)
|
||||
expect(existsSync(getTeamTaskDir(teamName))).toBe(false)
|
||||
expect(existsSync(getTeamConfigPath(teamName))).toBe(false)
|
||||
})
|
||||
|
||||
test("blocks deletion when team has active teammates", async () => {
|
||||
//#given
|
||||
const createTool = createTeamCreateTool()
|
||||
const deleteTool = createTeamDeleteTool()
|
||||
const context = createContext()
|
||||
const teamName = `team-with-members-${TEST_SUFFIX}`
|
||||
|
||||
// Create team
|
||||
await executeJsonTool(createTool, {
|
||||
team_name: teamName,
|
||||
}, context)
|
||||
|
||||
// Add a teammate by modifying config directly for test
|
||||
const teamConfig = readTeamConfig(teamName)
|
||||
expect(teamConfig).not.toBeNull()
|
||||
|
||||
// Manually add a teammate to simulate active member
|
||||
const { writeTeamConfig } = await import("./team-config-store")
|
||||
if (teamConfig) {
|
||||
writeTeamConfig(teamName, {
|
||||
...teamConfig,
|
||||
members: [
|
||||
...teamConfig.members,
|
||||
{
|
||||
agentId: "teammate-1",
|
||||
name: "test-teammate",
|
||||
agentType: "teammate",
|
||||
color: "#FF6B6B",
|
||||
category: "test",
|
||||
model: "test-model",
|
||||
prompt: "Test prompt",
|
||||
planModeRequired: false,
|
||||
joinedAt: new Date().toISOString(),
|
||||
cwd: "/tmp",
|
||||
subscriptions: [],
|
||||
backendType: "native",
|
||||
isActive: true,
|
||||
sessionID: "test-session",
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = await executeJsonTool(deleteTool, {
|
||||
team_name: teamName,
|
||||
}, context)
|
||||
|
||||
//#then
|
||||
expect(result).toEqual({
|
||||
error: "team_has_active_members",
|
||||
members: ["test-teammate"],
|
||||
})
|
||||
|
||||
// Cleanup - manually remove teammates first, then delete
|
||||
const configApi = await import("./team-config-store")
|
||||
const cleanupConfig = readTeamConfig(teamName)
|
||||
if (cleanupConfig) {
|
||||
configApi.writeTeamConfig(teamName, {
|
||||
...cleanupConfig,
|
||||
members: cleanupConfig.members.filter((m) => m.agentType === "team-lead"),
|
||||
})
|
||||
configApi.deleteTeamData(teamName)
|
||||
}
|
||||
})
|
||||
|
||||
test("validates team name format on deletion", async () => {
|
||||
//#given
|
||||
const deleteTool = createTeamDeleteTool()
|
||||
const context = createContext()
|
||||
const teamName = `invalid-team-${TEST_SUFFIX}`
|
||||
|
||||
//#when
|
||||
const result = await executeJsonTool(deleteTool, {
|
||||
team_name: "invalid@name",
|
||||
}, context)
|
||||
|
||||
//#then - Zod returns detailed validation error array
|
||||
const parsedResult = result as { error: string }
|
||||
expect(parsedResult.error).toContain("Team name must contain only letters")
|
||||
})
|
||||
|
||||
test("returns error for non-existent team", async () => {
|
||||
//#given
|
||||
const deleteTool = createTeamDeleteTool()
|
||||
const context = createContext()
|
||||
|
||||
//#when
|
||||
const result = await executeJsonTool(deleteTool, {
|
||||
team_name: "non-existent-team",
|
||||
}, context)
|
||||
|
||||
//#then
|
||||
expect(result).toEqual({
|
||||
error: "team_not_found",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import { z } from "zod"
|
||||
import { getTeamConfigPath } from "./paths"
|
||||
import { validateTeamName } from "./name-validation"
|
||||
import { ensureInbox } from "./inbox-store"
|
||||
@@ -23,7 +24,7 @@ function resolveReaderFromContext(config: TeamConfig, context: TeamToolContext):
|
||||
|
||||
function toPublicTeamConfig(config: TeamConfig): {
|
||||
team_name: string
|
||||
description: string
|
||||
description: string | undefined
|
||||
lead_agent_id: string
|
||||
teammates: Array<{ name: string }>
|
||||
} {
|
||||
@@ -36,70 +37,87 @@ function toPublicTeamConfig(config: TeamConfig): {
|
||||
}
|
||||
|
||||
export function createTeamCreateTool(): ToolDefinition {
|
||||
return tool({
|
||||
description: "Create a team workspace with config, inboxes, and task storage.",
|
||||
args: {
|
||||
team_name: tool.schema.string().describe("Team name (letters, numbers, hyphens, underscores)"),
|
||||
description: tool.schema.string().optional().describe("Team description"),
|
||||
},
|
||||
execute: async (args: Record<string, unknown>, context: TeamToolContext): Promise<string> => {
|
||||
try {
|
||||
const input = TeamCreateInputSchema.parse(args)
|
||||
const nameError = validateTeamName(input.team_name)
|
||||
if (nameError) {
|
||||
return JSON.stringify({ error: nameError })
|
||||
}
|
||||
return tool({
|
||||
description: "Create a team workspace with config, inboxes, and task storage.",
|
||||
args: {
|
||||
team_name: tool.schema.string().describe("Team name (letters, numbers, hyphens, underscores)"),
|
||||
description: tool.schema.string().optional().describe("Team description"),
|
||||
},
|
||||
execute: async (args: Record<string, unknown>, context: TeamToolContext): Promise<string> => {
|
||||
try {
|
||||
const input = TeamCreateInputSchema.parse(args)
|
||||
|
||||
const config = createTeamConfig(
|
||||
input.team_name,
|
||||
input.description ?? "",
|
||||
context.sessionID,
|
||||
process.cwd(),
|
||||
"native/team-lead",
|
||||
)
|
||||
ensureInbox(config.name, "team-lead")
|
||||
const config = createTeamConfig(
|
||||
input.team_name,
|
||||
input.description ?? "",
|
||||
context.sessionID,
|
||||
process.cwd(),
|
||||
"native/team-lead",
|
||||
)
|
||||
ensureInbox(config.name, "team-lead")
|
||||
|
||||
return JSON.stringify({
|
||||
team_name: config.name,
|
||||
team_file_path: getTeamConfigPath(config.name),
|
||||
lead_agent_id: config.leadAgentId,
|
||||
})
|
||||
} catch (error) {
|
||||
return JSON.stringify({ error: error instanceof Error ? error.message : "team_create_failed" })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
return JSON.stringify({
|
||||
team_name: config.name,
|
||||
config_path: getTeamConfigPath(config.name) as string,
|
||||
lead_agent_id: config.leadAgentId,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "team_already_exists") {
|
||||
return JSON.stringify({ error: error.message })
|
||||
}
|
||||
return JSON.stringify({ error: "team_create_failed" })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function createTeamDeleteTool(): ToolDefinition {
|
||||
return tool({
|
||||
description: "Delete a team and its stored data. Fails if teammates still exist.",
|
||||
args: {
|
||||
team_name: tool.schema.string().describe("Team name"),
|
||||
},
|
||||
execute: async (args: Record<string, unknown>, context: TeamToolContext): Promise<string> => {
|
||||
try {
|
||||
const input = TeamDeleteInputSchema.parse(args)
|
||||
const config = readTeamConfigOrThrow(input.team_name)
|
||||
if (context.sessionID !== config.leadSessionId) {
|
||||
return JSON.stringify({ error: "unauthorized_lead_session" })
|
||||
}
|
||||
const teammates = listTeammates(config)
|
||||
if (teammates.length > 0) {
|
||||
return JSON.stringify({
|
||||
error: "team_has_active_members",
|
||||
members: teammates.map((member) => member.name),
|
||||
})
|
||||
}
|
||||
return tool({
|
||||
description: "Delete a team and its stored data. Fails if teammates still exist.",
|
||||
args: {
|
||||
team_name: tool.schema.string().describe("Team name"),
|
||||
},
|
||||
execute: async (args: Record<string, unknown>, _context: TeamToolContext): Promise<string> => {
|
||||
let teamName: string | undefined
|
||||
|
||||
deleteTeamData(input.team_name)
|
||||
return JSON.stringify({ success: true, team_name: input.team_name })
|
||||
} catch (error) {
|
||||
return JSON.stringify({ error: error instanceof Error ? error.message : "team_delete_failed" })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
try {
|
||||
const input = TeamDeleteInputSchema.parse(args)
|
||||
teamName = input.team_name
|
||||
const config = readTeamConfig(input.team_name)
|
||||
if (!config) {
|
||||
return JSON.stringify({ error: "team_not_found" })
|
||||
}
|
||||
|
||||
const teammates = listTeammates(config)
|
||||
if (teammates.length > 0) {
|
||||
return JSON.stringify({
|
||||
error: "team_has_active_members",
|
||||
members: teammates.map((member) => member.name),
|
||||
})
|
||||
}
|
||||
|
||||
deleteTeamData(input.team_name)
|
||||
return JSON.stringify({ deleted: true, team_name: input.team_name })
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message === "team_has_active_members") {
|
||||
const config = readTeamConfig(teamName!)
|
||||
const activeMembers = config ? listTeammates(config) : []
|
||||
return JSON.stringify({
|
||||
error: "team_has_active_members",
|
||||
members: activeMembers.map((member) => member.name),
|
||||
})
|
||||
}
|
||||
if (error.message === "team_not_found") {
|
||||
return JSON.stringify({ error: "team_not_found" })
|
||||
}
|
||||
return JSON.stringify({ error: error.message })
|
||||
}
|
||||
return JSON.stringify({ error: "team_delete_failed" })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function createTeamReadConfigTool(): ToolDefinition {
|
||||
return tool({
|
||||
|
||||
@@ -115,38 +115,49 @@ export const TeamDeleteInputSchema = z.object({
|
||||
|
||||
export type TeamDeleteInput = z.infer<typeof TeamDeleteInputSchema>
|
||||
|
||||
const teamNameField = z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64)
|
||||
const senderField = z.string().optional()
|
||||
|
||||
export const SendMessageInputSchema = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||
team_name: teamNameField,
|
||||
type: z.literal("message"),
|
||||
recipient: z.string().min(1),
|
||||
content: z.string().optional(),
|
||||
summary: z.string().optional(),
|
||||
sender: senderField,
|
||||
}),
|
||||
z.object({
|
||||
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||
team_name: teamNameField,
|
||||
type: z.literal("broadcast"),
|
||||
content: z.string().optional(),
|
||||
summary: z.string().optional(),
|
||||
sender: senderField,
|
||||
}),
|
||||
z.object({
|
||||
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||
team_name: teamNameField,
|
||||
type: z.literal("shutdown_request"),
|
||||
recipient: z.string().min(1),
|
||||
content: z.string().optional(),
|
||||
summary: z.string().optional(),
|
||||
sender: senderField,
|
||||
}),
|
||||
z.object({
|
||||
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||
team_name: teamNameField,
|
||||
type: z.literal("shutdown_response"),
|
||||
request_id: z.string().min(1),
|
||||
approve: z.boolean(),
|
||||
content: z.string().optional(),
|
||||
sender: senderField,
|
||||
}),
|
||||
z.object({
|
||||
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||
team_name: teamNameField,
|
||||
type: z.literal("plan_approval_response"),
|
||||
request_id: z.string().min(1),
|
||||
approve: z.boolean(),
|
||||
recipient: z.string().optional(),
|
||||
content: z.string().optional(),
|
||||
sender: senderField,
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -183,9 +194,74 @@ export const ForceKillTeammateInputSchema = z.object({
|
||||
|
||||
export type ForceKillTeammateInput = z.infer<typeof ForceKillTeammateInputSchema>
|
||||
|
||||
export type TeamToolContext = {
|
||||
sessionID: string
|
||||
messageID: string
|
||||
agent: string
|
||||
abort?: AbortSignal
|
||||
}
|
||||
|
||||
export const TeamSendMessageInputSchema = SendMessageInputSchema
|
||||
|
||||
export type TeamSendMessageInput = z.infer<typeof TeamSendMessageInputSchema>
|
||||
|
||||
export const TeamReadInboxInputSchema = ReadInboxInputSchema
|
||||
|
||||
export type TeamReadInboxInput = z.infer<typeof TeamReadInboxInputSchema>
|
||||
|
||||
export const TeamReadConfigInputSchema = ReadConfigInputSchema
|
||||
|
||||
export type TeamReadConfigInput = z.infer<typeof TeamReadConfigInputSchema>
|
||||
|
||||
export const ProcessShutdownApprovedInputSchema = z.object({
|
||||
team_name: z.string().regex(/^[A-Za-z0-9_-]+$/, "Team name must contain only letters, numbers, hyphens, and underscores").max(64),
|
||||
teammate_name: z.string().min(1),
|
||||
})
|
||||
|
||||
export type ProcessShutdownApprovedInput = z.infer<typeof ProcessShutdownApprovedInputSchema>
|
||||
|
||||
export const TeamForceKillInputSchema = ForceKillTeammateInputSchema
|
||||
|
||||
export type TeamForceKillInput = z.infer<typeof TeamForceKillInputSchema>
|
||||
|
||||
export const TeamProcessShutdownInputSchema = ProcessShutdownApprovedInputSchema
|
||||
|
||||
export type TeamProcessShutdownInput = z.infer<typeof TeamProcessShutdownInputSchema>
|
||||
|
||||
export const TeamTaskCreateInputSchema = z.object({
|
||||
team_name: teamNameField,
|
||||
subject: z.string().min(1),
|
||||
description: z.string(),
|
||||
active_form: z.string().optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
|
||||
export type TeamTaskCreateInput = z.infer<typeof TeamTaskCreateInputSchema>
|
||||
|
||||
export const TeamTaskListInputSchema = z.object({
|
||||
team_name: teamNameField,
|
||||
})
|
||||
|
||||
export type TeamTaskListInput = z.infer<typeof TeamTaskListInputSchema>
|
||||
|
||||
export const TeamTaskGetInputSchema = z.object({
|
||||
team_name: teamNameField,
|
||||
task_id: z.string().min(1),
|
||||
})
|
||||
|
||||
export type TeamTaskGetInput = z.infer<typeof TeamTaskGetInputSchema>
|
||||
|
||||
export const TeamTaskUpdateInputSchema = z.object({
|
||||
team_name: teamNameField,
|
||||
task_id: z.string().min(1),
|
||||
status: z.enum(["pending", "in_progress", "completed", "deleted"]).optional(),
|
||||
owner: z.string().optional(),
|
||||
subject: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
active_form: z.string().optional(),
|
||||
add_blocks: z.array(z.string()).optional(),
|
||||
add_blocked_by: z.array(z.string()).optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
|
||||
export type TeamTaskUpdateInput = z.infer<typeof TeamTaskUpdateInputSchema>
|
||||
|
||||
Reference in New Issue
Block a user