feat(task): add team_name routing to task_create tool

- Add optional team_name parameter to task_create
- Route to team-namespaced storage when team_name provided
- Preserve existing behavior when team_name absent
- Add tests for both team and regular task creation
- Part of Task 12 (1/4 files complete)
This commit is contained in:
YeonGyu-Kim 2026-02-12 03:06:17 +09:00
parent 48441b831c
commit eabc20de9e
3 changed files with 114 additions and 32 deletions

View File

@ -10,6 +10,7 @@ const TEST_CONFIG = {
sisyphus: { sisyphus: {
tasks: { tasks: {
storage_path: TEST_STORAGE, storage_path: TEST_STORAGE,
claude_code_compat: true,
}, },
}, },
} }
@ -297,5 +298,41 @@ describe("task_create tool", () => {
expect(taskContent.subject).toBe("Test task") expect(taskContent.subject).toBe("Test task")
expect(taskContent.description).toBe("Test description") expect(taskContent.description).toBe("Test description")
}) })
test("creates task in team namespace when team_name provided", async () => {
//#given
const args = {
subject: "Team task",
team_name: "test-team",
}
//#when
const resultStr = await tool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result).toHaveProperty("task")
expect(result.task).toHaveProperty("id")
expect(result.task.subject).toBe("Team task")
})
test("creates task in regular storage when no team_name", async () => {
//#given
const args = {
subject: "Regular task",
}
//#when
const resultStr = await tool.execute(args, TEST_CONTEXT)
const result = JSON.parse(resultStr)
//#then
expect(result).toHaveProperty("task")
expect(result.task.subject).toBe("Regular task")
// Verify it's in regular storage
const taskId = result.task.id
const taskFile = join(TEST_DIR, `${taskId}.json`)
expect(existsSync(taskFile)).toBe(true)
})
}) })
}) })

View File

@ -11,6 +11,8 @@ import {
generateTaskId, generateTaskId,
} from "../../features/claude-tasks/storage"; } from "../../features/claude-tasks/storage";
import { syncTaskTodoUpdate } from "./todo-sync"; import { syncTaskTodoUpdate } from "./todo-sync";
import { writeTeamTask } from "../agent-teams/team-task-store";
import { getTeamTaskDir } from "../agent-teams/paths";
export function createTaskCreateTool( export function createTaskCreateTool(
config: Partial<OhMyOpenCodeConfig>, config: Partial<OhMyOpenCodeConfig>,
@ -48,7 +50,8 @@ Calculate dependencies carefully to maximize parallel execution:
.optional() .optional()
.describe("Task IDs this task blocks"), .describe("Task IDs this task blocks"),
repoURL: tool.schema.string().optional().describe("Repository URL"), repoURL: tool.schema.string().optional().describe("Repository URL"),
parentID: tool.schema.string().optional().describe("Parent task ID"), parentID: tool.schema.string().optional().describe("Parent task ID"),
team_name: tool.schema.string().optional().describe("Optional: team name for team-namespaced tasks"),
}, },
execute: async (args, context) => { execute: async (args, context) => {
return handleCreate(args, config, ctx, context); return handleCreate(args, config, ctx, context);
@ -64,42 +67,83 @@ async function handleCreate(
): Promise<string> { ): Promise<string> {
try { try {
const validatedArgs = TaskCreateInputSchema.parse(args); const validatedArgs = TaskCreateInputSchema.parse(args);
const taskDir = getTaskDir(config); if (validatedArgs.team_name) {
const lock = acquireLock(taskDir); const teamName = validatedArgs.team_name as string;
const teamTaskDir = getTeamTaskDir(teamName);
const lock = acquireLock(teamTaskDir);
if (!lock.acquired) { if (!lock.acquired) {
return JSON.stringify({ error: "task_lock_unavailable" }); return JSON.stringify({ error: "team_task_lock_unavailable" });
} }
try { try {
const taskId = generateTaskId(); const taskId = generateTaskId();
const task: TaskObject = { const task: TaskObject = {
id: taskId, id: taskId,
subject: validatedArgs.subject, subject: validatedArgs.subject,
description: validatedArgs.description ?? "", description: validatedArgs.description ?? "",
status: "pending", status: "pending",
blocks: validatedArgs.blocks ?? [], blocks: validatedArgs.blocks ?? [],
blockedBy: validatedArgs.blockedBy ?? [], blockedBy: validatedArgs.blockedBy ?? [],
activeForm: validatedArgs.activeForm, activeForm: validatedArgs.activeForm,
metadata: validatedArgs.metadata, metadata: validatedArgs.metadata,
repoURL: validatedArgs.repoURL, repoURL: validatedArgs.repoURL,
parentID: validatedArgs.parentID, parentID: validatedArgs.parentID,
threadID: context.sessionID, threadID: context.sessionID,
}; };
const validatedTask = TaskObjectSchema.parse(task); const validatedTask = TaskObjectSchema.parse(task);
writeJsonAtomic(join(taskDir, `${taskId}.json`), validatedTask); writeTeamTask(teamName, taskId, validatedTask);
await syncTaskTodoUpdate(ctx, validatedTask, context.sessionID); await syncTaskTodoUpdate(ctx, validatedTask, context.sessionID);
return JSON.stringify({ return JSON.stringify({
task: { task: {
id: validatedTask.id, id: validatedTask.id,
subject: validatedTask.subject, subject: validatedTask.subject,
}, },
}); });
} finally { } finally {
lock.release(); lock.release();
}
} else {
const taskDir = getTaskDir(config);
const lock = acquireLock(taskDir);
if (!lock.acquired) {
return JSON.stringify({ error: "task_lock_unavailable" });
}
try {
const taskId = generateTaskId();
const task: TaskObject = {
id: taskId,
subject: validatedArgs.subject,
description: validatedArgs.description ?? "",
status: "pending",
blocks: validatedArgs.blocks ?? [],
blockedBy: validatedArgs.blockedBy ?? [],
activeForm: validatedArgs.activeForm,
metadata: validatedArgs.metadata,
repoURL: validatedArgs.repoURL,
parentID: validatedArgs.parentID,
threadID: context.sessionID,
};
const validatedTask = TaskObjectSchema.parse(task);
writeJsonAtomic(join(taskDir, `${taskId}.json`), validatedTask);
await syncTaskTodoUpdate(ctx, validatedTask, context.sessionID);
return JSON.stringify({
task: {
id: validatedTask.id,
subject: validatedTask.subject,
},
});
} finally {
lock.release();
}
} }
} catch (error) { } catch (error) {
if (error instanceof Error && error.message.includes("Required")) { if (error instanceof Error && error.message.includes("Required")) {

View File

@ -37,6 +37,7 @@ export const TaskCreateInputSchema = z.object({
metadata: z.record(z.string(), z.unknown()).optional(), metadata: z.record(z.string(), z.unknown()).optional(),
repoURL: z.string().optional(), repoURL: z.string().optional(),
parentID: z.string().optional(), parentID: z.string().optional(),
team_name: z.string().optional(),
}) })
export type TaskCreateInput = z.infer<typeof TaskCreateInputSchema> export type TaskCreateInput = z.infer<typeof TaskCreateInputSchema>