feat: add quality_review task status and qualityReview initiative flag

Adds two new fields to the database and propagates them through the
repository layer:

- Task status enum gains 'quality_review' (between in_progress and
  completed), enabling a QA gate before tasks are marked complete.
- initiatives.quality_review (INTEGER DEFAULT 0) lets an initiative be
  flagged for quality-review workflow without a data migration (existing
  rows default to false).

Includes:
- Schema changes in schema.ts
- Migration 0037 (ALTER TABLE initiatives ADD quality_review)
- Snapshot chain repaired: deleted stale 0036 snapshot, fixed 0035
  prevId to create a linear chain (0032 → 0034 → 0035), then generated
  clean 0037 snapshot
- Repository adapter already uses SELECT * / spread-update pattern so
  no adapter code changes were needed
- Initiative and task repository tests extended with qualityReview /
  quality_review_status describe blocks (7 new tests)
- docs/database.md updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas May
2026-03-06 21:47:34 +01:00
parent c150f26d4a
commit 5137a60e70
8 changed files with 1103 additions and 193 deletions

View File

@@ -147,4 +147,32 @@ describe('DrizzleInitiativeRepository', () => {
expect(archived[0].name).toBe('Archived'); expect(archived[0].name).toBe('Archived');
}); });
}); });
describe('qualityReview', () => {
it('defaults to false on create', async () => {
const initiative = await repo.create({ name: 'QR Test' });
expect(initiative.qualityReview).toBe(false);
});
it('can be set to true via update', async () => {
const created = await repo.create({ name: 'QR Toggle' });
const updated = await repo.update(created.id, { qualityReview: true });
expect(updated.qualityReview).toBe(true);
});
it('round-trips through findById', async () => {
const created = await repo.create({ name: 'QR Round-trip' });
await repo.update(created.id, { qualityReview: true });
const found = await repo.findById(created.id);
expect(found!.qualityReview).toBe(true);
});
it('round-trips false value', async () => {
const created = await repo.create({ name: 'QR False' });
await repo.update(created.id, { qualityReview: true });
await repo.update(created.id, { qualityReview: false });
const found = await repo.findById(created.id);
expect(found!.qualityReview).toBe(false);
});
});
}); });

View File

@@ -196,4 +196,38 @@ describe('DrizzleTaskRepository', () => {
); );
}); });
}); });
describe('quality_review status', () => {
it('can set status to quality_review', async () => {
const task = await taskRepo.create({
phaseId: testPhaseId,
name: 'QR Status Task',
order: 99,
});
const updated = await taskRepo.update(task.id, { status: 'quality_review' });
expect(updated.status).toBe('quality_review');
});
it('round-trips quality_review through findById', async () => {
const task = await taskRepo.create({
phaseId: testPhaseId,
name: 'QR Round-trip Task',
order: 100,
});
await taskRepo.update(task.id, { status: 'quality_review' });
const found = await taskRepo.findById(task.id);
expect(found!.status).toBe('quality_review');
});
it('can transition from quality_review to completed', async () => {
const task = await taskRepo.create({
phaseId: testPhaseId,
name: 'QR to Completed',
order: 101,
});
await taskRepo.update(task.id, { status: 'quality_review' });
const completed = await taskRepo.update(task.id, { status: 'completed' });
expect(completed.status).toBe('completed');
});
});
}); });

View File

@@ -26,6 +26,7 @@ export const initiatives = sqliteTable('initiatives', {
executionMode: text('execution_mode', { enum: ['yolo', 'review_per_phase'] }) executionMode: text('execution_mode', { enum: ['yolo', 'review_per_phase'] })
.notNull() .notNull()
.default('review_per_phase'), .default('review_per_phase'),
qualityReview: integer('quality_review', { mode: 'boolean' }).notNull().default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
}); });
@@ -151,7 +152,7 @@ export const tasks = sqliteTable('tasks', {
.notNull() .notNull()
.default('medium'), .default('medium'),
status: text('status', { status: text('status', {
enum: ['pending', 'in_progress', 'completed', 'blocked'], enum: ['pending', 'in_progress', 'quality_review', 'completed', 'blocked'],
}) })
.notNull() .notNull()
.default('pending'), .default('pending'),

View File

@@ -0,0 +1 @@
ALTER TABLE `initiatives` ADD `quality_review` integer DEFAULT false NOT NULL;

View File

@@ -2,7 +2,7 @@
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "c84e499f-7df8-4091-b2a5-6b12847898bd", "id": "c84e499f-7df8-4091-b2a5-6b12847898bd",
"prevId": "5fbe1151-1dfb-4b0c-a7fa-2177369543fd", "prevId": "443071fe-31d6-498a-9f4a-4a3ff24a46fc",
"tables": { "tables": {
"accounts": { "accounts": {
"name": "accounts", "name": "accounts",
@@ -238,6 +238,13 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"prompt": {
"name": "prompt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"exit_code": { "exit_code": {
"name": "exit_code", "name": "exit_code",
"type": "integer", "type": "integer",

View File

@@ -260,6 +260,13 @@
"when": 1772798869413, "when": 1772798869413,
"tag": "0036_icy_silvermane", "tag": "0036_icy_silvermane",
"breakpoints": true "breakpoints": true
},
{
"idx": 37,
"version": "6",
"when": 1772829916655,
"tag": "0037_worthless_princess_powerful",
"breakpoints": true
} }
] ]
} }

View File

@@ -20,6 +20,8 @@ All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.r
| name | text NOT NULL | | | name | text NOT NULL | |
| status | text enum | 'active' \| 'pending_review' \| 'completed' \| 'archived', default 'active' | | status | text enum | 'active' \| 'pending_review' \| 'completed' \| 'archived', default 'active' |
| branch | text nullable | auto-generated initiative branch (e.g., 'cw/user-auth') | | branch | text nullable | auto-generated initiative branch (e.g., 'cw/user-auth') |
| executionMode | text enum | 'yolo' \| 'review_per_phase', default 'review_per_phase' |
| qualityReview | integer (boolean) | default false; flags initiative for quality review workflow |
| createdAt, updatedAt | integer/timestamp | | | createdAt, updatedAt | integer/timestamp | |
### phases ### phases
@@ -48,7 +50,7 @@ All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.r
| type | text enum | 'auto' | | type | text enum | 'auto' |
| category | text enum | 'execute' \| 'research' \| 'discuss' \| 'plan' \| 'detail' \| 'refine' \| 'verify' \| 'merge' \| 'review' | | category | text enum | 'execute' \| 'research' \| 'discuss' \| 'plan' \| 'detail' \| 'refine' \| 'verify' \| 'merge' \| 'review' |
| priority | text enum | 'low' \| 'medium' \| 'high' | | priority | text enum | 'low' \| 'medium' \| 'high' |
| status | text enum | 'pending' \| 'in_progress' \| 'completed' \| 'blocked' | | status | text enum | 'pending' \| 'in_progress' \| 'quality_review' \| 'completed' \| 'blocked' |
| order | integer | default 0 | | order | integer | default 0 |
| summary | text nullable | Agent result summary — propagated to dependent tasks as context | | summary | text nullable | Agent result summary — propagated to dependent tasks as context |
| retryCount | integer NOT NULL | default 0, incremented on agent crash auto-retry, reset on manual retry | | retryCount | integer NOT NULL | default 0, incremented on agent crash auto-retry, reset on manual retry |