Merge branch 'refs/heads/main' into cw/agent-details-conflict-1772799979862
# Conflicts: # apps/server/drizzle/meta/_journal.json
This commit is contained in:
@@ -33,4 +33,10 @@ export interface ChangeSetRepository {
|
||||
findByInitiativeId(initiativeId: string): Promise<ChangeSet[]>;
|
||||
findByAgentId(agentId: string): Promise<ChangeSet[]>;
|
||||
markReverted(id: string): Promise<ChangeSet>;
|
||||
|
||||
/**
|
||||
* Find applied changesets that have a 'create' entry for the given entity.
|
||||
* Used to reconcile changeset status when entities are manually deleted.
|
||||
*/
|
||||
findAppliedByCreatedEntity(entityType: string, entityId: string): Promise<ChangeSetWithEntries[]>;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Implements ChangeSetRepository interface using Drizzle ORM.
|
||||
*/
|
||||
|
||||
import { eq, desc, asc } from 'drizzle-orm';
|
||||
import { eq, desc, asc, and } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { DrizzleDatabase } from '../../index.js';
|
||||
import { changeSets, changeSetEntries, type ChangeSet } from '../../schema.js';
|
||||
@@ -94,6 +94,32 @@ export class DrizzleChangeSetRepository implements ChangeSetRepository {
|
||||
.orderBy(desc(changeSets.createdAt));
|
||||
}
|
||||
|
||||
async findAppliedByCreatedEntity(entityType: string, entityId: string): Promise<ChangeSetWithEntries[]> {
|
||||
// Find changeset entries matching the entity
|
||||
const matchingEntries = await this.db
|
||||
.select({ changeSetId: changeSetEntries.changeSetId })
|
||||
.from(changeSetEntries)
|
||||
.where(
|
||||
and(
|
||||
eq(changeSetEntries.entityType, entityType as any),
|
||||
eq(changeSetEntries.entityId, entityId),
|
||||
eq(changeSetEntries.action, 'create'),
|
||||
),
|
||||
);
|
||||
|
||||
const results: ChangeSetWithEntries[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const { changeSetId } of matchingEntries) {
|
||||
if (seen.has(changeSetId)) continue;
|
||||
seen.add(changeSetId);
|
||||
const cs = await this.findByIdWithEntries(changeSetId);
|
||||
if (cs && cs.status === 'applied') {
|
||||
results.push(cs);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async markReverted(id: string): Promise<ChangeSet> {
|
||||
const [updated] = await this.db
|
||||
.update(changeSets)
|
||||
|
||||
336
apps/server/db/repositories/drizzle/errand.test.ts
Normal file
336
apps/server/db/repositories/drizzle/errand.test.ts
Normal file
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* DrizzleErrandRepository Tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { DrizzleErrandRepository } from './errand.js';
|
||||
import { createTestDatabase } from './test-helpers.js';
|
||||
import type { DrizzleDatabase } from '../../index.js';
|
||||
import { projects, agents, errands } from '../../schema.js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
describe('DrizzleErrandRepository', () => {
|
||||
let db: DrizzleDatabase;
|
||||
let repo: DrizzleErrandRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
db = createTestDatabase();
|
||||
repo = new DrizzleErrandRepository(db);
|
||||
});
|
||||
|
||||
// Helper: create a project record
|
||||
async function createProject(name = 'Test Project', suffix = '') {
|
||||
const id = nanoid();
|
||||
const now = new Date();
|
||||
const [project] = await db.insert(projects).values({
|
||||
id,
|
||||
name: name + suffix + id,
|
||||
url: `https://github.com/test/${id}`,
|
||||
defaultBranch: 'main',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}).returning();
|
||||
return project;
|
||||
}
|
||||
|
||||
// Helper: create an agent record
|
||||
async function createAgent(name?: string) {
|
||||
const id = nanoid();
|
||||
const now = new Date();
|
||||
const agentName = name ?? `agent-${id}`;
|
||||
const [agent] = await db.insert(agents).values({
|
||||
id,
|
||||
name: agentName,
|
||||
worktreeId: `agent-workdirs/${agentName}`,
|
||||
provider: 'claude',
|
||||
status: 'idle',
|
||||
mode: 'execute',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}).returning();
|
||||
return agent;
|
||||
}
|
||||
|
||||
// Helper: create an errand
|
||||
async function createErrand(overrides: Partial<{
|
||||
id: string;
|
||||
description: string;
|
||||
branch: string;
|
||||
baseBranch: string;
|
||||
agentId: string | null;
|
||||
projectId: string | null;
|
||||
status: 'active' | 'pending_review' | 'conflict' | 'merged' | 'abandoned';
|
||||
createdAt: Date;
|
||||
}> = {}) {
|
||||
const project = await createProject();
|
||||
const id = overrides.id ?? nanoid();
|
||||
return repo.create({
|
||||
id,
|
||||
description: overrides.description ?? 'Test errand',
|
||||
branch: overrides.branch ?? 'feature/test',
|
||||
baseBranch: overrides.baseBranch ?? 'main',
|
||||
agentId: overrides.agentId !== undefined ? overrides.agentId : null,
|
||||
projectId: overrides.projectId !== undefined ? overrides.projectId : project.id,
|
||||
status: overrides.status ?? 'active',
|
||||
});
|
||||
}
|
||||
|
||||
describe('create + findById', () => {
|
||||
it('should create errand and find by id with all fields', async () => {
|
||||
const project = await createProject();
|
||||
const id = nanoid();
|
||||
|
||||
await repo.create({
|
||||
id,
|
||||
description: 'Fix the bug',
|
||||
branch: 'fix/bug-123',
|
||||
baseBranch: 'main',
|
||||
agentId: null,
|
||||
projectId: project.id,
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
const found = await repo.findById(id);
|
||||
expect(found).toBeDefined();
|
||||
expect(found!.id).toBe(id);
|
||||
expect(found!.description).toBe('Fix the bug');
|
||||
expect(found!.branch).toBe('fix/bug-123');
|
||||
expect(found!.baseBranch).toBe('main');
|
||||
expect(found!.status).toBe('active');
|
||||
expect(found!.projectId).toBe(project.id);
|
||||
expect(found!.agentId).toBeNull();
|
||||
expect(found!.agentAlias).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all errands ordered by createdAt desc', async () => {
|
||||
const project = await createProject();
|
||||
const t1 = new Date('2024-01-01T00:00:00Z');
|
||||
const t2 = new Date('2024-01-02T00:00:00Z');
|
||||
const t3 = new Date('2024-01-03T00:00:00Z');
|
||||
|
||||
const id1 = nanoid();
|
||||
const id2 = nanoid();
|
||||
const id3 = nanoid();
|
||||
|
||||
await db.insert(errands).values([
|
||||
{ id: id1, description: 'Errand 1', branch: 'b1', baseBranch: 'main', agentId: null, projectId: project.id, status: 'active', createdAt: t1, updatedAt: t1 },
|
||||
{ id: id2, description: 'Errand 2', branch: 'b2', baseBranch: 'main', agentId: null, projectId: project.id, status: 'active', createdAt: t2, updatedAt: t2 },
|
||||
{ id: id3, description: 'Errand 3', branch: 'b3', baseBranch: 'main', agentId: null, projectId: project.id, status: 'active', createdAt: t3, updatedAt: t3 },
|
||||
]);
|
||||
|
||||
const result = await repo.findAll();
|
||||
expect(result.length).toBeGreaterThanOrEqual(3);
|
||||
// Find our three in the results
|
||||
const ids = result.map((e) => e.id);
|
||||
expect(ids.indexOf(id3)).toBeLessThan(ids.indexOf(id2));
|
||||
expect(ids.indexOf(id2)).toBeLessThan(ids.indexOf(id1));
|
||||
});
|
||||
|
||||
it('should filter by projectId', async () => {
|
||||
const projectA = await createProject('A');
|
||||
const projectB = await createProject('B');
|
||||
const now = new Date();
|
||||
|
||||
const idA1 = nanoid();
|
||||
const idA2 = nanoid();
|
||||
const idB1 = nanoid();
|
||||
|
||||
await db.insert(errands).values([
|
||||
{ id: idA1, description: 'A1', branch: 'b-a1', baseBranch: 'main', agentId: null, projectId: projectA.id, status: 'active', createdAt: now, updatedAt: now },
|
||||
{ id: idA2, description: 'A2', branch: 'b-a2', baseBranch: 'main', agentId: null, projectId: projectA.id, status: 'active', createdAt: now, updatedAt: now },
|
||||
{ id: idB1, description: 'B1', branch: 'b-b1', baseBranch: 'main', agentId: null, projectId: projectB.id, status: 'active', createdAt: now, updatedAt: now },
|
||||
]);
|
||||
|
||||
const result = await repo.findAll({ projectId: projectA.id });
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((e) => e.id).sort()).toEqual([idA1, idA2].sort());
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const project = await createProject();
|
||||
const now = new Date();
|
||||
|
||||
const id1 = nanoid();
|
||||
const id2 = nanoid();
|
||||
const id3 = nanoid();
|
||||
|
||||
await db.insert(errands).values([
|
||||
{ id: id1, description: 'E1', branch: 'b1', baseBranch: 'main', agentId: null, projectId: project.id, status: 'active', createdAt: now, updatedAt: now },
|
||||
{ id: id2, description: 'E2', branch: 'b2', baseBranch: 'main', agentId: null, projectId: project.id, status: 'pending_review', createdAt: now, updatedAt: now },
|
||||
{ id: id3, description: 'E3', branch: 'b3', baseBranch: 'main', agentId: null, projectId: project.id, status: 'merged', createdAt: now, updatedAt: now },
|
||||
]);
|
||||
|
||||
const result = await repo.findAll({ status: 'pending_review' });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe(id2);
|
||||
});
|
||||
|
||||
it('should filter by both projectId and status', async () => {
|
||||
const projectA = await createProject('PA');
|
||||
const projectB = await createProject('PB');
|
||||
const now = new Date();
|
||||
|
||||
const idMatch = nanoid();
|
||||
const idOtherStatus = nanoid();
|
||||
const idOtherProject = nanoid();
|
||||
const idNeither = nanoid();
|
||||
|
||||
await db.insert(errands).values([
|
||||
{ id: idMatch, description: 'Match', branch: 'b1', baseBranch: 'main', agentId: null, projectId: projectA.id, status: 'pending_review', createdAt: now, updatedAt: now },
|
||||
{ id: idOtherStatus, description: 'Wrong status', branch: 'b2', baseBranch: 'main', agentId: null, projectId: projectA.id, status: 'active', createdAt: now, updatedAt: now },
|
||||
{ id: idOtherProject, description: 'Wrong project', branch: 'b3', baseBranch: 'main', agentId: null, projectId: projectB.id, status: 'pending_review', createdAt: now, updatedAt: now },
|
||||
{ id: idNeither, description: 'Neither', branch: 'b4', baseBranch: 'main', agentId: null, projectId: projectB.id, status: 'active', createdAt: now, updatedAt: now },
|
||||
]);
|
||||
|
||||
const result = await repo.findAll({ projectId: projectA.id, status: 'pending_review' });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe(idMatch);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return agentAlias when agentId is set', async () => {
|
||||
const agent = await createAgent('known-agent');
|
||||
const project = await createProject();
|
||||
const id = nanoid();
|
||||
const now = new Date();
|
||||
|
||||
await db.insert(errands).values({
|
||||
id,
|
||||
description: 'With agent',
|
||||
branch: 'feature/x',
|
||||
baseBranch: 'main',
|
||||
agentId: agent.id,
|
||||
projectId: project.id,
|
||||
status: 'active',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const found = await repo.findById(id);
|
||||
expect(found).toBeDefined();
|
||||
expect(found!.agentAlias).toBe(agent.name);
|
||||
});
|
||||
|
||||
it('should return agentAlias as null when agentId is null', async () => {
|
||||
const project = await createProject();
|
||||
const id = nanoid();
|
||||
const now = new Date();
|
||||
|
||||
await db.insert(errands).values({
|
||||
id,
|
||||
description: 'No agent',
|
||||
branch: 'feature/y',
|
||||
baseBranch: 'main',
|
||||
agentId: null,
|
||||
projectId: project.id,
|
||||
status: 'active',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const found = await repo.findById(id);
|
||||
expect(found).toBeDefined();
|
||||
expect(found!.agentAlias).toBeNull();
|
||||
});
|
||||
|
||||
it('should return undefined for unknown id', async () => {
|
||||
const found = await repo.findById('nonexistent');
|
||||
expect(found).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update status and advance updatedAt', async () => {
|
||||
const project = await createProject();
|
||||
const id = nanoid();
|
||||
const past = new Date('2024-01-01T00:00:00Z');
|
||||
|
||||
await db.insert(errands).values({
|
||||
id,
|
||||
description: 'Errand',
|
||||
branch: 'feature/update',
|
||||
baseBranch: 'main',
|
||||
agentId: null,
|
||||
projectId: project.id,
|
||||
status: 'active',
|
||||
createdAt: past,
|
||||
updatedAt: past,
|
||||
});
|
||||
|
||||
const updated = await repo.update(id, { status: 'pending_review' });
|
||||
expect(updated.status).toBe('pending_review');
|
||||
expect(updated.updatedAt.getTime()).toBeGreaterThan(past.getTime());
|
||||
});
|
||||
|
||||
it('should throw on unknown id', async () => {
|
||||
await expect(
|
||||
repo.update('nonexistent', { status: 'merged' })
|
||||
).rejects.toThrow('Errand not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete errand and findById returns undefined', async () => {
|
||||
const errand = await createErrand();
|
||||
await repo.delete(errand.id);
|
||||
const found = await repo.findById(errand.id);
|
||||
expect(found).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cascade and set null', () => {
|
||||
it('should cascade delete errands when project is deleted', async () => {
|
||||
const project = await createProject();
|
||||
const id = nanoid();
|
||||
const now = new Date();
|
||||
|
||||
await db.insert(errands).values({
|
||||
id,
|
||||
description: 'Cascade test',
|
||||
branch: 'feature/cascade',
|
||||
baseBranch: 'main',
|
||||
agentId: null,
|
||||
projectId: project.id,
|
||||
status: 'active',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
// Delete project — should cascade delete errands
|
||||
await db.delete(projects).where(eq(projects.id, project.id));
|
||||
|
||||
const found = await repo.findById(id);
|
||||
expect(found).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set agentId to null when agent is deleted', async () => {
|
||||
const agent = await createAgent();
|
||||
const project = await createProject();
|
||||
const id = nanoid();
|
||||
const now = new Date();
|
||||
|
||||
await db.insert(errands).values({
|
||||
id,
|
||||
description: 'Agent null test',
|
||||
branch: 'feature/agent-null',
|
||||
baseBranch: 'main',
|
||||
agentId: agent.id,
|
||||
projectId: project.id,
|
||||
status: 'active',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
// Delete agent — should set null
|
||||
await db.delete(agents).where(eq(agents.id, agent.id));
|
||||
|
||||
const [errand] = await db.select().from(errands).where(eq(errands.id, id));
|
||||
expect(errand).toBeDefined();
|
||||
expect(errand.agentId).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
89
apps/server/db/repositories/drizzle/errand.ts
Normal file
89
apps/server/db/repositories/drizzle/errand.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Drizzle Errand Repository Adapter
|
||||
*
|
||||
* Implements ErrandRepository interface using Drizzle ORM.
|
||||
*/
|
||||
|
||||
import { eq, desc, and } from 'drizzle-orm';
|
||||
import type { DrizzleDatabase } from '../../index.js';
|
||||
import { errands, agents } from '../../schema.js';
|
||||
import type {
|
||||
ErrandRepository,
|
||||
ErrandWithAlias,
|
||||
ErrandStatus,
|
||||
CreateErrandData,
|
||||
UpdateErrandData,
|
||||
} from '../errand-repository.js';
|
||||
import type { Errand } from '../../schema.js';
|
||||
|
||||
export class DrizzleErrandRepository implements ErrandRepository {
|
||||
constructor(private db: DrizzleDatabase) {}
|
||||
|
||||
async create(data: CreateErrandData): Promise<Errand> {
|
||||
const now = new Date();
|
||||
const [created] = await this.db
|
||||
.insert(errands)
|
||||
.values({ ...data, createdAt: now, updatedAt: now })
|
||||
.returning();
|
||||
return created;
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<ErrandWithAlias | undefined> {
|
||||
const result = await this.db
|
||||
.select({
|
||||
id: errands.id,
|
||||
description: errands.description,
|
||||
branch: errands.branch,
|
||||
baseBranch: errands.baseBranch,
|
||||
agentId: errands.agentId,
|
||||
projectId: errands.projectId,
|
||||
status: errands.status,
|
||||
createdAt: errands.createdAt,
|
||||
updatedAt: errands.updatedAt,
|
||||
agentAlias: agents.name,
|
||||
})
|
||||
.from(errands)
|
||||
.leftJoin(agents, eq(errands.agentId, agents.id))
|
||||
.where(eq(errands.id, id))
|
||||
.limit(1);
|
||||
return result[0] ?? undefined;
|
||||
}
|
||||
|
||||
async findAll(opts?: { projectId?: string; status?: ErrandStatus }): Promise<ErrandWithAlias[]> {
|
||||
const conditions = [];
|
||||
if (opts?.projectId) conditions.push(eq(errands.projectId, opts.projectId));
|
||||
if (opts?.status) conditions.push(eq(errands.status, opts.status));
|
||||
|
||||
return this.db
|
||||
.select({
|
||||
id: errands.id,
|
||||
description: errands.description,
|
||||
branch: errands.branch,
|
||||
baseBranch: errands.baseBranch,
|
||||
agentId: errands.agentId,
|
||||
projectId: errands.projectId,
|
||||
status: errands.status,
|
||||
createdAt: errands.createdAt,
|
||||
updatedAt: errands.updatedAt,
|
||||
agentAlias: agents.name,
|
||||
})
|
||||
.from(errands)
|
||||
.leftJoin(agents, eq(errands.agentId, agents.id))
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(desc(errands.createdAt));
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateErrandData): Promise<Errand> {
|
||||
const [updated] = await this.db
|
||||
.update(errands)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(errands.id, id))
|
||||
.returning();
|
||||
if (!updated) throw new Error(`Errand not found: ${id}`);
|
||||
return updated;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.db.delete(errands).where(eq(errands.id, id));
|
||||
}
|
||||
}
|
||||
@@ -18,3 +18,4 @@ export { DrizzleLogChunkRepository } from './log-chunk.js';
|
||||
export { DrizzleConversationRepository } from './conversation.js';
|
||||
export { DrizzleChatSessionRepository } from './chat-session.js';
|
||||
export { DrizzleReviewCommentRepository } from './review-comment.js';
|
||||
export { DrizzleErrandRepository } from './errand.js';
|
||||
|
||||
@@ -23,7 +23,43 @@ export class DrizzleReviewCommentRepository implements ReviewCommentRepository {
|
||||
lineNumber: data.lineNumber,
|
||||
lineType: data.lineType,
|
||||
body: data.body,
|
||||
author: data.author ?? 'you',
|
||||
author: data.author ?? 'user',
|
||||
parentCommentId: data.parentCommentId ?? null,
|
||||
resolved: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
const rows = await this.db
|
||||
.select()
|
||||
.from(reviewComments)
|
||||
.where(eq(reviewComments.id, id))
|
||||
.limit(1);
|
||||
return rows[0]!;
|
||||
}
|
||||
|
||||
async createReply(parentCommentId: string, body: string, author?: string): Promise<ReviewComment> {
|
||||
// Fetch parent comment to copy context fields
|
||||
const parentRows = await this.db
|
||||
.select()
|
||||
.from(reviewComments)
|
||||
.where(eq(reviewComments.id, parentCommentId))
|
||||
.limit(1);
|
||||
const parent = parentRows[0];
|
||||
if (!parent) {
|
||||
throw new Error(`Parent comment not found: ${parentCommentId}`);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const id = nanoid();
|
||||
await this.db.insert(reviewComments).values({
|
||||
id,
|
||||
phaseId: parent.phaseId,
|
||||
filePath: parent.filePath,
|
||||
lineNumber: parent.lineNumber,
|
||||
lineType: parent.lineType,
|
||||
body,
|
||||
author: author ?? 'user',
|
||||
parentCommentId,
|
||||
resolved: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
@@ -44,6 +80,19 @@ export class DrizzleReviewCommentRepository implements ReviewCommentRepository {
|
||||
.orderBy(asc(reviewComments.createdAt));
|
||||
}
|
||||
|
||||
async update(id: string, body: string): Promise<ReviewComment | null> {
|
||||
await this.db
|
||||
.update(reviewComments)
|
||||
.set({ body, updatedAt: new Date() })
|
||||
.where(eq(reviewComments.id, id));
|
||||
const rows = await this.db
|
||||
.select()
|
||||
.from(reviewComments)
|
||||
.where(eq(reviewComments.id, id))
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
async resolve(id: string): Promise<ReviewComment | null> {
|
||||
await this.db
|
||||
.update(reviewComments)
|
||||
|
||||
@@ -71,13 +71,13 @@ describe('DrizzleTaskRepository', () => {
|
||||
it('should accept custom type and priority', async () => {
|
||||
const task = await taskRepo.create({
|
||||
phaseId: testPhaseId,
|
||||
name: 'Checkpoint Task',
|
||||
type: 'checkpoint:human-verify',
|
||||
name: 'High Priority Task',
|
||||
type: 'auto',
|
||||
priority: 'high',
|
||||
order: 1,
|
||||
});
|
||||
|
||||
expect(task.type).toBe('checkpoint:human-verify');
|
||||
expect(task.type).toBe('auto');
|
||||
expect(task.priority).toBe('high');
|
||||
});
|
||||
});
|
||||
|
||||
15
apps/server/db/repositories/errand-repository.ts
Normal file
15
apps/server/db/repositories/errand-repository.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Errand, NewErrand } from '../schema.js';
|
||||
|
||||
export type ErrandStatus = 'active' | 'pending_review' | 'conflict' | 'merged' | 'abandoned';
|
||||
export type ErrandWithAlias = Errand & { agentAlias: string | null };
|
||||
|
||||
export type CreateErrandData = Omit<NewErrand, 'createdAt' | 'updatedAt'>;
|
||||
export type UpdateErrandData = Partial<Omit<NewErrand, 'id' | 'createdAt'>>;
|
||||
|
||||
export interface ErrandRepository {
|
||||
create(data: CreateErrandData): Promise<Errand>;
|
||||
findById(id: string): Promise<ErrandWithAlias | undefined>;
|
||||
findAll(opts?: { projectId?: string; status?: ErrandStatus }): Promise<ErrandWithAlias[]>;
|
||||
update(id: string, data: UpdateErrandData): Promise<Errand>;
|
||||
delete(id: string): Promise<void>;
|
||||
}
|
||||
@@ -82,3 +82,11 @@ export type {
|
||||
ReviewCommentRepository,
|
||||
CreateReviewCommentData,
|
||||
} from './review-comment-repository.js';
|
||||
|
||||
export type {
|
||||
ErrandRepository,
|
||||
ErrandWithAlias,
|
||||
ErrandStatus,
|
||||
CreateErrandData,
|
||||
UpdateErrandData,
|
||||
} from './errand-repository.js';
|
||||
|
||||
@@ -13,11 +13,14 @@ export interface CreateReviewCommentData {
|
||||
lineType: 'added' | 'removed' | 'context';
|
||||
body: string;
|
||||
author?: string;
|
||||
parentCommentId?: string; // for replies
|
||||
}
|
||||
|
||||
export interface ReviewCommentRepository {
|
||||
create(data: CreateReviewCommentData): Promise<ReviewComment>;
|
||||
createReply(parentCommentId: string, body: string, author?: string): Promise<ReviewComment>;
|
||||
findByPhaseId(phaseId: string): Promise<ReviewComment[]>;
|
||||
update(id: string, body: string): Promise<ReviewComment | null>;
|
||||
resolve(id: string): Promise<ReviewComment | null>;
|
||||
unresolve(id: string): Promise<ReviewComment | null>;
|
||||
delete(id: string): Promise<void>;
|
||||
|
||||
@@ -55,6 +55,7 @@ export const phases = sqliteTable('phases', {
|
||||
status: text('status', { enum: ['pending', 'approved', 'in_progress', 'completed', 'blocked', 'pending_review'] })
|
||||
.notNull()
|
||||
.default('pending'),
|
||||
mergeBase: text('merge_base'),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
});
|
||||
@@ -137,7 +138,7 @@ export const tasks = sqliteTable('tasks', {
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
type: text('type', {
|
||||
enum: ['auto', 'checkpoint:human-verify', 'checkpoint:decision', 'checkpoint:human-action'],
|
||||
enum: ['auto'],
|
||||
})
|
||||
.notNull()
|
||||
.default('auto'),
|
||||
@@ -156,6 +157,7 @@ export const tasks = sqliteTable('tasks', {
|
||||
.default('pending'),
|
||||
order: integer('order').notNull().default(0),
|
||||
summary: text('summary'), // Agent result summary — propagated to dependent tasks as context
|
||||
retryCount: integer('retry_count').notNull().default(0),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
});
|
||||
@@ -260,7 +262,7 @@ export const agents = sqliteTable('agents', {
|
||||
})
|
||||
.notNull()
|
||||
.default('idle'),
|
||||
mode: text('mode', { enum: ['execute', 'discuss', 'plan', 'detail', 'refine', 'chat'] })
|
||||
mode: text('mode', { enum: ['execute', 'discuss', 'plan', 'detail', 'refine', 'chat', 'errand'] })
|
||||
.notNull()
|
||||
.default('execute'),
|
||||
pid: integer('pid'),
|
||||
@@ -617,12 +619,46 @@ export const reviewComments = sqliteTable('review_comments', {
|
||||
lineType: text('line_type', { enum: ['added', 'removed', 'context'] }).notNull(),
|
||||
body: text('body').notNull(),
|
||||
author: text('author').notNull().default('you'),
|
||||
parentCommentId: text('parent_comment_id').references((): ReturnType<typeof text> => reviewComments.id, { onDelete: 'cascade' }),
|
||||
resolved: integer('resolved', { mode: 'boolean' }).notNull().default(false),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
}, (table) => [
|
||||
index('review_comments_phase_id_idx').on(table.phaseId),
|
||||
index('review_comments_parent_id_idx').on(table.parentCommentId),
|
||||
]);
|
||||
|
||||
export type ReviewComment = InferSelectModel<typeof reviewComments>;
|
||||
export type NewReviewComment = InferInsertModel<typeof reviewComments>;
|
||||
|
||||
// ============================================================================
|
||||
// ERRANDS
|
||||
// ============================================================================
|
||||
|
||||
export const errands = sqliteTable('errands', {
|
||||
id: text('id').primaryKey(),
|
||||
description: text('description').notNull(),
|
||||
branch: text('branch').notNull(),
|
||||
baseBranch: text('base_branch').notNull().default('main'),
|
||||
agentId: text('agent_id').references(() => agents.id, { onDelete: 'set null' }),
|
||||
projectId: text('project_id').references(() => projects.id, { onDelete: 'cascade' }),
|
||||
status: text('status', {
|
||||
enum: ['active', 'pending_review', 'conflict', 'merged', 'abandoned'],
|
||||
}).notNull().default('active'),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
});
|
||||
|
||||
export const errandsRelations = relations(errands, ({ one }) => ({
|
||||
agent: one(agents, {
|
||||
fields: [errands.agentId],
|
||||
references: [agents.id],
|
||||
}),
|
||||
project: one(projects, {
|
||||
fields: [errands.projectId],
|
||||
references: [projects.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export type Errand = InferSelectModel<typeof errands>;
|
||||
export type NewErrand = InferInsertModel<typeof errands>;
|
||||
|
||||
Reference in New Issue
Block a user