From 1df20f0e13680950711ecf1c52bf2d101a9ea5ab Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 1 Mar 2026 03:54:28 +0000 Subject: [PATCH] feat(api): add assigned_agent to Task model (MS22-DB-003, MS22-API-003) (#591) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- .../migration.sql | 2 + apps/api/prisma/schema.prisma | 1 + apps/api/src/tasks/dto/create-task.dto.ts | 6 +++ apps/api/src/tasks/dto/update-task.dto.ts | 6 +++ apps/api/src/tasks/tasks.service.spec.ts | 43 +++++++++++++++++++ apps/api/src/tasks/tasks.service.ts | 6 +++ docs/TASKS.md | 19 ++++++++ 7 files changed, 83 insertions(+) create mode 100644 apps/api/prisma/migrations/20260301211000_ms22_task_assigned_agent/migration.sql diff --git a/apps/api/prisma/migrations/20260301211000_ms22_task_assigned_agent/migration.sql b/apps/api/prisma/migrations/20260301211000_ms22_task_assigned_agent/migration.sql new file mode 100644 index 0000000..e698d9c --- /dev/null +++ b/apps/api/prisma/migrations/20260301211000_ms22_task_assigned_agent/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "tasks" ADD COLUMN "assigned_agent" TEXT; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 04f37a8..936538b 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -379,6 +379,7 @@ model Task { creatorId String @map("creator_id") @db.Uuid projectId String? @map("project_id") @db.Uuid parentId String? @map("parent_id") @db.Uuid + assignedAgent String? @map("assigned_agent") domainId String? @map("domain_id") @db.Uuid sortOrder Int @default(0) @map("sort_order") metadata Json @default("{}") diff --git a/apps/api/src/tasks/dto/create-task.dto.ts b/apps/api/src/tasks/dto/create-task.dto.ts index 6b8d752..c53675f 100644 --- a/apps/api/src/tasks/dto/create-task.dto.ts +++ b/apps/api/src/tasks/dto/create-task.dto.ts @@ -50,6 +50,12 @@ export class CreateTaskDto { @IsUUID("4", { message: "parentId must be a valid UUID" }) parentId?: string; + @IsOptional() + @IsString({ message: "assignedAgent must be a string" }) + @MinLength(1, { message: "assignedAgent must not be empty" }) + @MaxLength(255, { message: "assignedAgent must not exceed 255 characters" }) + assignedAgent?: string; + @IsOptional() @IsInt({ message: "sortOrder must be an integer" }) @Min(0, { message: "sortOrder must be at least 0" }) diff --git a/apps/api/src/tasks/dto/update-task.dto.ts b/apps/api/src/tasks/dto/update-task.dto.ts index 1e0ce47..3db35d1 100644 --- a/apps/api/src/tasks/dto/update-task.dto.ts +++ b/apps/api/src/tasks/dto/update-task.dto.ts @@ -52,6 +52,12 @@ export class UpdateTaskDto { @IsUUID("4", { message: "parentId must be a valid UUID" }) parentId?: string | null; + @IsOptional() + @IsString({ message: "assignedAgent must be a string" }) + @MinLength(1, { message: "assignedAgent must not be empty" }) + @MaxLength(255, { message: "assignedAgent must not exceed 255 characters" }) + assignedAgent?: string | null; + @IsOptional() @IsInt({ message: "sortOrder must be an integer" }) @Min(0, { message: "sortOrder must be at least 0" }) diff --git a/apps/api/src/tasks/tasks.service.spec.ts b/apps/api/src/tasks/tasks.service.spec.ts index e751af5..5995a2c 100644 --- a/apps/api/src/tasks/tasks.service.spec.ts +++ b/apps/api/src/tasks/tasks.service.spec.ts @@ -48,6 +48,7 @@ describe("TasksService", () => { creatorId: mockUserId, projectId: null, parentId: null, + assignedAgent: null, sortOrder: 0, metadata: {}, createdAt: new Date(), @@ -158,6 +159,28 @@ describe("TasksService", () => { }) ); }); + + it("should include assignedAgent when provided", async () => { + const createDto = { + title: "Agent-owned Task", + assignedAgent: "fleet-worker-1", + }; + + mockPrismaService.task.create.mockResolvedValue({ + ...mockTask, + assignedAgent: createDto.assignedAgent, + }); + + await service.create(mockWorkspaceId, mockUserId, createDto); + + expect(prisma.task.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + assignedAgent: createDto.assignedAgent, + }), + }) + ); + }); }); describe("findAll", () => { @@ -469,6 +492,26 @@ describe("TasksService", () => { service.update(mockTaskId, mockWorkspaceId, mockUserId, { title: "Test" }) ).rejects.toThrow(NotFoundException); }); + + it("should update assignedAgent when provided", async () => { + const updateDto = { assignedAgent: "fleet-worker-2" }; + + mockPrismaService.task.findUnique.mockResolvedValue(mockTask); + mockPrismaService.task.update.mockResolvedValue({ + ...mockTask, + assignedAgent: updateDto.assignedAgent, + }); + + await service.update(mockTaskId, mockWorkspaceId, mockUserId, updateDto); + + expect(prisma.task.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + assignedAgent: updateDto.assignedAgent, + }), + }) + ); + }); }); describe("remove", () => { diff --git a/apps/api/src/tasks/tasks.service.ts b/apps/api/src/tasks/tasks.service.ts index aecf1b0..5e3af96 100644 --- a/apps/api/src/tasks/tasks.service.ts +++ b/apps/api/src/tasks/tasks.service.ts @@ -67,6 +67,9 @@ export class TasksService { metadata: createTaskDto.metadata ? (createTaskDto.metadata as unknown as Prisma.InputJsonValue) : {}, + ...(createTaskDto.assignedAgent !== undefined && { + assignedAgent: createTaskDto.assignedAgent, + }), ...(assigneeConnection && { assignee: assigneeConnection }), ...(projectConnection && { project: projectConnection }), ...(parentConnection && { parent: parentConnection }), @@ -291,6 +294,9 @@ export class TasksService { if (updateTaskDto.parentId !== undefined && updateTaskDto.parentId !== null) { data.parent = { connect: { id: updateTaskDto.parentId } }; } + if (updateTaskDto.assignedAgent !== undefined) { + data.assignedAgent = updateTaskDto.assignedAgent; + } // Handle completedAt based on status changes if (updateTaskDto.status) { diff --git a/docs/TASKS.md b/docs/TASKS.md index 03f45c7..ba9d35f 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -52,3 +52,22 @@ | **Total** | **31** | **15** | **~371K** | **~175K** | Remaining estimate: ~143K tokens (Codex budget). + +## MS22 — Fleet Evolution (Phase 0: Knowledge Layer) + +| id | status | milestone | description | issue | repo | branch | depends_on | blocks | agent | started_at | completed_at | estimate | used | notes | +| --------------- | ----------- | ------------ | ------------------------------------------------------------ | -------- | ----- | ------------------------------ | --------------------------------------------------------- | ------------- | ------------ | ---------- | ------------ | -------- | ---- | --------------------------------------------- | +| MS22-PLAN-001 | done | p0-knowledge | PRD + mission bootstrap + TASKS.md | TASKS:P0 | stack | feat/ms22-knowledge-schema | — | MS22-DB-001 | orchestrator | 2026-02-28 | 2026-02-28 | 10K | 8K | PRD-MS22.md, mission fleet-evolution-20260228 | +| MS22-DB-001 | done | p0-knowledge | Findings module (pgvector, CRUD, similarity search) | TASKS:P0 | api | feat/ms22-findings | MS22-PLAN-001 | — | codex | 2026-02-28 | 2026-02-28 | 20K | ~22K | PR #585 merged, CI green | +| MS22-API-001 | done | p0-knowledge | Findings API endpoints | TASKS:P0 | api | feat/ms22-findings | MS22-DB-001 | — | codex | 2026-02-28 | 2026-02-28 | — | — | Combined with DB-001 | +| MS22-DB-002 | done | p0-knowledge | AgentMemory module (key/value store, upsert) | TASKS:P0 | api | feat/ms22-agent-memory | MS22-DB-001 | — | codex | 2026-02-28 | 2026-02-28 | 15K | ~16K | PR #586 merged, CI green | +| MS22-API-002 | done | p0-knowledge | AgentMemory API endpoints | TASKS:P0 | api | feat/ms22-agent-memory | MS22-DB-002 | — | codex | 2026-02-28 | 2026-02-28 | — | — | Combined with DB-002 | +| MS22-DB-004 | done | p0-knowledge | ConversationArchive module (pgvector, ingest, search) | TASKS:P0 | api | feat/ms22-conversation-archive | MS22-DB-001 | — | codex | 2026-02-28 | 2026-02-28 | 20K | ~18K | PR #587 merged, CI green | +| MS22-API-004 | done | p0-knowledge | ConversationArchive API endpoints | TASKS:P0 | api | feat/ms22-conversation-archive | MS22-DB-004 | — | codex | 2026-02-28 | 2026-02-28 | — | — | Combined with DB-004 | +| MS22-API-005 | done | p0-knowledge | EmbeddingService (reuse existing KnowledgeModule) | TASKS:P0 | api | — | — | — | orchestrator | 2026-02-28 | 2026-02-28 | 0 | 0 | Already existed; no work needed | +| MS22-DB-003 | not-started | p0-knowledge | Task model: add assigned_agent field + migration | TASKS:P0 | api | feat/ms22-task-agent | MS22-DB-001 | MS22-API-003 | — | — | — | 8K | — | Small schema + migration only | +| MS22-API-003 | not-started | p0-knowledge | Task API: expose assigned_agent in CRUD | TASKS:P0 | api | feat/ms22-task-agent | MS22-DB-003 | MS22-TEST-001 | — | — | — | 8K | — | Extend existing TaskModule | +| MS22-TEST-001 | not-started | p0-knowledge | Integration tests: Findings + AgentMemory + ConvArchive | TASKS:P0 | api | test/ms22-integration | MS22-API-001,MS22-API-002,MS22-API-004 | MS22-VER-P0 | — | — | — | 20K | — | E2E with live postgres | +| MS22-SKILL-001 | not-started | p0-knowledge | OpenClaw mosaic skill (agents read/write findings/memory) | TASKS:P0 | stack | feat/ms22-openclaw-skill | MS22-API-001,MS22-API-002 | MS22-VER-P0 | — | — | — | 15K | — | Skill in ~/.agents/skills/mosaic/ | +| MS22-INGEST-001 | not-started | p0-knowledge | Session log ingestion pipeline (OpenClaw logs → ConvArchive) | TASKS:P0 | stack | feat/ms22-ingest | MS22-API-004 | MS22-VER-P0 | — | — | — | 20K | — | Script to batch-ingest existing logs | +| MS22-VER-P0 | not-started | p0-knowledge | Phase 0 verification: all modules deployed + smoke tested | TASKS:P0 | stack | — | MS22-TEST-001,MS22-SKILL-001,MS22-INGEST-001,MS22-API-003 | — | — | — | — | 5K | — | |