feat(api): add assigned_agent to Task model (MS22-DB-003, MS22-API-003) (#591)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #591.
This commit is contained in:
2026-03-01 03:54:28 +00:00
committed by jason.woltje
parent 8dab20c022
commit 1df20f0e13
7 changed files with 83 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "tasks" ADD COLUMN "assigned_agent" TEXT;

View File

@@ -379,6 +379,7 @@ model Task {
creatorId String @map("creator_id") @db.Uuid creatorId String @map("creator_id") @db.Uuid
projectId String? @map("project_id") @db.Uuid projectId String? @map("project_id") @db.Uuid
parentId String? @map("parent_id") @db.Uuid parentId String? @map("parent_id") @db.Uuid
assignedAgent String? @map("assigned_agent")
domainId String? @map("domain_id") @db.Uuid domainId String? @map("domain_id") @db.Uuid
sortOrder Int @default(0) @map("sort_order") sortOrder Int @default(0) @map("sort_order")
metadata Json @default("{}") metadata Json @default("{}")

View File

@@ -50,6 +50,12 @@ export class CreateTaskDto {
@IsUUID("4", { message: "parentId must be a valid UUID" }) @IsUUID("4", { message: "parentId must be a valid UUID" })
parentId?: string; 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() @IsOptional()
@IsInt({ message: "sortOrder must be an integer" }) @IsInt({ message: "sortOrder must be an integer" })
@Min(0, { message: "sortOrder must be at least 0" }) @Min(0, { message: "sortOrder must be at least 0" })

View File

@@ -52,6 +52,12 @@ export class UpdateTaskDto {
@IsUUID("4", { message: "parentId must be a valid UUID" }) @IsUUID("4", { message: "parentId must be a valid UUID" })
parentId?: string | null; 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() @IsOptional()
@IsInt({ message: "sortOrder must be an integer" }) @IsInt({ message: "sortOrder must be an integer" })
@Min(0, { message: "sortOrder must be at least 0" }) @Min(0, { message: "sortOrder must be at least 0" })

View File

@@ -48,6 +48,7 @@ describe("TasksService", () => {
creatorId: mockUserId, creatorId: mockUserId,
projectId: null, projectId: null,
parentId: null, parentId: null,
assignedAgent: null,
sortOrder: 0, sortOrder: 0,
metadata: {}, metadata: {},
createdAt: new Date(), 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", () => { describe("findAll", () => {
@@ -469,6 +492,26 @@ describe("TasksService", () => {
service.update(mockTaskId, mockWorkspaceId, mockUserId, { title: "Test" }) service.update(mockTaskId, mockWorkspaceId, mockUserId, { title: "Test" })
).rejects.toThrow(NotFoundException); ).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", () => { describe("remove", () => {

View File

@@ -67,6 +67,9 @@ export class TasksService {
metadata: createTaskDto.metadata metadata: createTaskDto.metadata
? (createTaskDto.metadata as unknown as Prisma.InputJsonValue) ? (createTaskDto.metadata as unknown as Prisma.InputJsonValue)
: {}, : {},
...(createTaskDto.assignedAgent !== undefined && {
assignedAgent: createTaskDto.assignedAgent,
}),
...(assigneeConnection && { assignee: assigneeConnection }), ...(assigneeConnection && { assignee: assigneeConnection }),
...(projectConnection && { project: projectConnection }), ...(projectConnection && { project: projectConnection }),
...(parentConnection && { parent: parentConnection }), ...(parentConnection && { parent: parentConnection }),
@@ -291,6 +294,9 @@ export class TasksService {
if (updateTaskDto.parentId !== undefined && updateTaskDto.parentId !== null) { if (updateTaskDto.parentId !== undefined && updateTaskDto.parentId !== null) {
data.parent = { connect: { id: updateTaskDto.parentId } }; data.parent = { connect: { id: updateTaskDto.parentId } };
} }
if (updateTaskDto.assignedAgent !== undefined) {
data.assignedAgent = updateTaskDto.assignedAgent;
}
// Handle completedAt based on status changes // Handle completedAt based on status changes
if (updateTaskDto.status) { if (updateTaskDto.status) {

View File

@@ -52,3 +52,22 @@
| **Total** | **31** | **15** | **~371K** | **~175K** | | **Total** | **31** | **15** | **~371K** | **~175K** |
Remaining estimate: ~143K tokens (Codex budget). 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 | — | |