diff --git a/apps/server/agent/manager.ts b/apps/server/agent/manager.ts index 5c4fc11..3e022bc 100644 --- a/apps/server/agent/manager.ts +++ b/apps/server/agent/manager.ts @@ -369,6 +369,7 @@ export class MultiProviderAgentManager implements AgentManager { agentId, pid, () => this.handleDetachedAgentCompletion(agentId), () => this.activeAgents.get(agentId)?.tailer, + this.createEarlyCompletionChecker(agentId), ); activeEntry.cancelPoll = cancel; @@ -406,6 +407,20 @@ export class MultiProviderAgentManager implements AgentManager { return this.toAgentInfo(agent); } + /** + * Create a callback that checks if an agent has a valid signal.json, + * used by pollForCompletion to detect hung processes. + */ + private createEarlyCompletionChecker(agentId: string): () => Promise { + return async () => { + const agent = await this.repository.findById(agentId); + if (!agent?.worktreeId) return false; + const agentWorkdir = this.processManager.getAgentWorkdir(agent.worktreeId); + const signal = await this.outputHandler.readSignalCompletion(agentWorkdir); + return signal !== null; + }; + } + /** * Handle completion of a detached agent. */ @@ -525,6 +540,7 @@ export class MultiProviderAgentManager implements AgentManager { agentId, pid, () => this.handleDetachedAgentCompletion(agentId), () => this.activeAgents.get(agentId)?.tailer, + this.createEarlyCompletionChecker(agentId), ); commitActiveEntry.cancelPoll = commitCancel; @@ -633,6 +649,7 @@ export class MultiProviderAgentManager implements AgentManager { agentId, pid, () => this.handleDetachedAgentCompletion(agentId), () => this.activeAgents.get(agentId)?.tailer, + this.createEarlyCompletionChecker(agentId), ); activeEntry.cancelPoll = cancel; @@ -704,6 +721,7 @@ export class MultiProviderAgentManager implements AgentManager { agentId, pid, () => this.handleDetachedAgentCompletion(agentId), () => this.activeAgents.get(agentId)?.tailer, + this.createEarlyCompletionChecker(agentId), ); activeEntry.cancelPoll = cancel; @@ -890,6 +908,7 @@ export class MultiProviderAgentManager implements AgentManager { agentId, pid, () => this.handleDetachedAgentCompletion(agentId), () => this.activeAgents.get(agentId)?.tailer, + this.createEarlyCompletionChecker(agentId), ); resumeActiveEntry.cancelPoll = resumeCancel; } @@ -1013,6 +1032,7 @@ export class MultiProviderAgentManager implements AgentManager { agentId, pid, () => this.handleDetachedAgentCompletion(agentId), () => this.activeAgents.get(agentId)?.tailer, + this.createEarlyCompletionChecker(agentId), ); const active = this.activeAgents.get(agentId); if (active) active.cancelPoll = cancel; diff --git a/apps/server/agent/output-handler.ts b/apps/server/agent/output-handler.ts index 28fdaf6..4da2d0c 100644 --- a/apps/server/agent/output-handler.ts +++ b/apps/server/agent/output-handler.ts @@ -1133,7 +1133,7 @@ export class OutputHandler { * Uses SignalManager for atomic read-and-validate when available. * Returns the raw JSON string on success, null if missing/invalid. */ - private async readSignalCompletion(agentWorkdir: string): Promise { + async readSignalCompletion(agentWorkdir: string): Promise { // Prefer SignalManager (unified implementation with proper validation) if (this.signalManager) { const signal = await this.signalManager.readSignal(agentWorkdir); diff --git a/apps/server/agent/process-manager.ts b/apps/server/agent/process-manager.ts index 144b348..105de84 100644 --- a/apps/server/agent/process-manager.ts +++ b/apps/server/agent/process-manager.ts @@ -328,27 +328,63 @@ export class ProcessManager { * When the process exits, calls onComplete callback. * Returns a cancel handle to stop polling (e.g. on agent cleanup or re-resume). * + * Optionally checks signal.json after a grace period to detect hung processes + * that completed work but failed to exit. If a valid signal is found while the + * process is still alive, SIGTERM is sent and normal completion proceeds. + * * @param onComplete - Called when the process is no longer alive * @param getTailer - Function to get the current tailer for final flush + * @param checkEarlyCompletion - Optional callback that returns true if signal.json indicates completion */ pollForCompletion( agentId: string, pid: number, onComplete: () => Promise, getTailer: () => FileTailer | undefined, + checkEarlyCompletion?: () => Promise, ): { cancel: () => void } { let cancelled = false; + const startTime = Date.now(); + const GRACE_PERIOD_MS = 60_000; + const SIGNAL_CHECK_INTERVAL_MS = 30_000; + let lastSignalCheck = 0; + + const finalize = async () => { + const tailer = getTailer(); + if (tailer) { + await new Promise((resolve) => setTimeout(resolve, 500)); + await tailer.stop(); + } + if (!cancelled) await onComplete(); + }; + const check = async () => { if (cancelled) return; if (!isPidAlive(pid)) { - const tailer = getTailer(); - if (tailer) { - await new Promise((resolve) => setTimeout(resolve, 500)); - await tailer.stop(); - } - if (!cancelled) await onComplete(); + await finalize(); return; } + + // Defensive signal check: after grace period, periodically check signal.json + if (checkEarlyCompletion) { + const elapsed = Date.now() - startTime; + if (elapsed >= GRACE_PERIOD_MS && Date.now() - lastSignalCheck >= SIGNAL_CHECK_INTERVAL_MS) { + lastSignalCheck = Date.now(); + try { + const hasSignal = await checkEarlyCompletion(); + if (hasSignal) { + log.warn({ agentId, pid, elapsedMs: elapsed }, 'signal.json found but process still alive — sending SIGTERM'); + try { process.kill(pid, 'SIGTERM'); } catch { /* already dead */ } + await new Promise((resolve) => setTimeout(resolve, 2000)); + await finalize(); + return; + } + } catch (err) { + log.debug({ agentId, err: err instanceof Error ? err.message : String(err) }, 'early completion check failed'); + } + } + } + if (!cancelled) setTimeout(check, 1000); }; check(); diff --git a/apps/server/cli/index.ts b/apps/server/cli/index.ts index cda8766..1e37383 100644 --- a/apps/server/cli/index.ts +++ b/apps/server/cli/index.ts @@ -13,6 +13,8 @@ import { createDefaultTrpcClient } from './trpc-client.js'; import { createContainer } from '../container.js'; import { findWorkspaceRoot, writeCwrc, defaultCwConfig } from '../config/index.js'; import { createModuleLogger } from '../logger/index.js'; +import { backfillMetricsFromPath } from '../scripts/backfill-metrics.js'; +import { getDbPath } from '../db/index.js'; /** Environment variable for custom port */ const CW_PORT_ENV = 'CW_PORT'; @@ -134,6 +136,22 @@ export function createCli(serverHandler?: (port?: number) => Promise): Com } }); + // Backfill metrics command (standalone — no server, no tRPC) + program + .command('backfill-metrics') + .description('Populate agent_metrics table from existing agent_log_chunks (run once after upgrading)') + .option('--db ', 'Path to the SQLite database file (defaults to configured DB path)') + .action(async (options: { db?: string }) => { + const dbPath = options.db ?? getDbPath(); + console.log(`Backfilling metrics from ${dbPath}...`); + try { + await backfillMetricsFromPath(dbPath); + } catch (error) { + console.error('Backfill failed:', (error as Error).message); + process.exit(1); + } + }); + // Agent command group const agentCommand = program .command('agent') diff --git a/apps/server/db/repositories/drizzle/agent-metrics.test.ts b/apps/server/db/repositories/drizzle/agent-metrics.test.ts new file mode 100644 index 0000000..2753a03 --- /dev/null +++ b/apps/server/db/repositories/drizzle/agent-metrics.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { createTestDatabase } from './test-helpers.js'; +import { agentMetrics } from '../../schema.js'; + +describe('agentMetrics table', () => { + it('select from empty agentMetrics returns []', async () => { + const db = createTestDatabase(); + const rows = await db.select().from(agentMetrics); + expect(rows).toEqual([]); + }); + + it('insert and select a metrics row round-trips correctly', async () => { + const db = createTestDatabase(); + await db.insert(agentMetrics).values({ + agentId: 'agent-abc', + questionsCount: 3, + subagentsCount: 1, + compactionsCount: 0, + updatedAt: new Date('2024-01-01T00:00:00Z'), + }); + const rows = await db.select().from(agentMetrics); + expect(rows).toHaveLength(1); + expect(rows[0].agentId).toBe('agent-abc'); + expect(rows[0].questionsCount).toBe(3); + expect(rows[0].subagentsCount).toBe(1); + expect(rows[0].compactionsCount).toBe(0); + }); + + it('agentId is primary key — duplicate insert throws', async () => { + const db = createTestDatabase(); + await db.insert(agentMetrics).values({ + agentId: 'agent-dup', + questionsCount: 0, + subagentsCount: 0, + compactionsCount: 0, + updatedAt: new Date(), + }); + await expect( + db.insert(agentMetrics).values({ + agentId: 'agent-dup', + questionsCount: 1, + subagentsCount: 0, + compactionsCount: 0, + updatedAt: new Date(), + }) + ).rejects.toThrow(); + }); +}); diff --git a/apps/server/db/repositories/drizzle/log-chunk.test.ts b/apps/server/db/repositories/drizzle/log-chunk.test.ts new file mode 100644 index 0000000..0c48d6a --- /dev/null +++ b/apps/server/db/repositories/drizzle/log-chunk.test.ts @@ -0,0 +1,129 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { DrizzleLogChunkRepository } from './log-chunk.js'; +import { createTestDatabase } from './test-helpers.js'; +import type { DrizzleDatabase } from '../../index.js'; + +describe('DrizzleLogChunkRepository', () => { + let db: DrizzleDatabase; + let repo: DrizzleLogChunkRepository; + const testAgentId = 'agent-test-001'; + + beforeEach(() => { + db = createTestDatabase(); + repo = new DrizzleLogChunkRepository(db); + }); + + it('AskUserQuestion chunk — questionsCount upserted correctly', async () => { + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'tool_use', name: 'AskUserQuestion', input: { questions: [{}, {}] } }), + }); + const metrics = await repo.findMetricsByAgentIds([testAgentId]); + expect(metrics).toEqual([{ + agentId: testAgentId, + questionsCount: 2, + subagentsCount: 0, + compactionsCount: 0, + }]); + }); + + it('Agent tool chunk — subagentsCount incremented', async () => { + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'tool_use', name: 'Agent' }), + }); + const metrics = await repo.findMetricsByAgentIds([testAgentId]); + expect(metrics).toEqual([{ + agentId: testAgentId, + questionsCount: 0, + subagentsCount: 1, + compactionsCount: 0, + }]); + }); + + it('Compaction event — compactionsCount incremented', async () => { + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'system', subtype: 'init', source: 'compact' }), + }); + const metrics = await repo.findMetricsByAgentIds([testAgentId]); + expect(metrics).toEqual([{ + agentId: testAgentId, + questionsCount: 0, + subagentsCount: 0, + compactionsCount: 1, + }]); + }); + + it('Irrelevant chunk type — no metrics row created', async () => { + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'text', text: 'hello' }), + }); + const metrics = await repo.findMetricsByAgentIds([testAgentId]); + expect(metrics).toEqual([]); + }); + + it('Malformed JSON chunk — chunk persisted, metrics row absent', async () => { + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: 'not-valid-json', + }); + const chunks = await repo.findByAgentId(testAgentId); + expect(chunks).toHaveLength(1); + const metrics = await repo.findMetricsByAgentIds([testAgentId]); + expect(metrics).toEqual([]); + }); + + it('Multiple inserts, same agent — counts accumulate additively', async () => { + // 3 Agent tool chunks + for (let i = 0; i < 3; i++) { + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'tool_use', name: 'Agent' }), + }); + } + // 1 AskUserQuestion with 2 questions + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'tool_use', name: 'AskUserQuestion', input: { questions: [{}, {}] } }), + }); + const metrics = await repo.findMetricsByAgentIds([testAgentId]); + expect(metrics).toEqual([{ + agentId: testAgentId, + questionsCount: 2, + subagentsCount: 3, + compactionsCount: 0, + }]); + }); + + it('findMetricsByAgentIds with empty array — returns []', async () => { + const metrics = await repo.findMetricsByAgentIds([]); + expect(metrics).toEqual([]); + }); + + it('findMetricsByAgentIds with agentId that has no metrics row — returns []', async () => { + await repo.insertChunk({ + agentId: testAgentId, + agentName: 'test-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'text', text: 'hello' }), + }); + const metrics = await repo.findMetricsByAgentIds([testAgentId]); + expect(metrics).toEqual([]); + }); +}); diff --git a/apps/server/db/repositories/drizzle/log-chunk.ts b/apps/server/db/repositories/drizzle/log-chunk.ts index 9d4632b..5d30c0b 100644 --- a/apps/server/db/repositories/drizzle/log-chunk.ts +++ b/apps/server/db/repositories/drizzle/log-chunk.ts @@ -4,10 +4,10 @@ * Implements LogChunkRepository interface using Drizzle ORM. */ -import { eq, asc, max, inArray } from 'drizzle-orm'; +import { eq, asc, max, inArray, sql } from 'drizzle-orm'; import { nanoid } from 'nanoid'; import type { DrizzleDatabase } from '../../index.js'; -import { agentLogChunks } from '../../schema.js'; +import { agentLogChunks, agentMetrics } from '../../schema.js'; import type { LogChunkRepository } from '../log-chunk-repository.js'; export class DrizzleLogChunkRepository implements LogChunkRepository { @@ -19,13 +19,58 @@ export class DrizzleLogChunkRepository implements LogChunkRepository { sessionNumber: number; content: string; }): Promise { - await this.db.insert(agentLogChunks).values({ - id: nanoid(), - agentId: data.agentId, - agentName: data.agentName, - sessionNumber: data.sessionNumber, - content: data.content, - createdAt: new Date(), + // better-sqlite3 is synchronous — transaction callback must be sync, use .run() not await + this.db.transaction((tx) => { + // 1. Always insert the chunk row first + tx.insert(agentLogChunks).values({ + id: nanoid(), + agentId: data.agentId, + agentName: data.agentName, + sessionNumber: data.sessionNumber, + content: data.content, + createdAt: new Date(), + }).run(); + + // 2. Parse content and determine metric increments + // Wrap only the parse + upsert block — chunk insert is not rolled back on parse failure + try { + const parsed = JSON.parse(data.content); + let deltaQuestions = 0; + let deltaSubagents = 0; + let deltaCompactions = 0; + + if (parsed.type === 'tool_use' && parsed.name === 'AskUserQuestion') { + deltaQuestions = parsed.input?.questions?.length ?? 0; + } else if (parsed.type === 'tool_use' && parsed.name === 'Agent') { + deltaSubagents = 1; + } else if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.source === 'compact') { + deltaCompactions = 1; + } + + // 3. Only upsert if there is something to increment + if (deltaQuestions > 0 || deltaSubagents > 0 || deltaCompactions > 0) { + tx.insert(agentMetrics) + .values({ + agentId: data.agentId, + questionsCount: deltaQuestions, + subagentsCount: deltaSubagents, + compactionsCount: deltaCompactions, + updatedAt: new Date(), + }) + .onConflictDoUpdate({ + target: agentMetrics.agentId, + set: { + questionsCount: sql`${agentMetrics.questionsCount} + ${deltaQuestions}`, + subagentsCount: sql`${agentMetrics.subagentsCount} + ${deltaSubagents}`, + compactionsCount: sql`${agentMetrics.compactionsCount} + ${deltaCompactions}`, + updatedAt: new Date(), + }, + }) + .run(); + } + } catch { + // Malformed JSON — skip metric upsert, chunk insert already committed within transaction + } }); } @@ -69,4 +114,22 @@ export class DrizzleLogChunkRepository implements LogChunkRepository { return result[0]?.maxSession ?? 0; } + + async findMetricsByAgentIds(agentIds: string[]): Promise<{ + agentId: string; + questionsCount: number; + subagentsCount: number; + compactionsCount: number; + }[]> { + if (agentIds.length === 0) return []; + return this.db + .select({ + agentId: agentMetrics.agentId, + questionsCount: agentMetrics.questionsCount, + subagentsCount: agentMetrics.subagentsCount, + compactionsCount: agentMetrics.compactionsCount, + }) + .from(agentMetrics) + .where(inArray(agentMetrics.agentId, agentIds)); + } } diff --git a/apps/server/db/repositories/log-chunk-repository.ts b/apps/server/db/repositories/log-chunk-repository.ts index 0283a0b..1e1f8a2 100644 --- a/apps/server/db/repositories/log-chunk-repository.ts +++ b/apps/server/db/repositories/log-chunk-repository.ts @@ -27,4 +27,16 @@ export interface LogChunkRepository { deleteByAgentId(agentId: string): Promise; getSessionCount(agentId: string): Promise; + + /** + * Batch-fetch pre-computed metrics for multiple agent IDs. + * Returns one row per agent that has metrics. Agents with no + * matching row in agent_metrics are omitted (not returned as zeros). + */ + findMetricsByAgentIds(agentIds: string[]): Promise<{ + agentId: string; + questionsCount: number; + subagentsCount: number; + compactionsCount: number; + }[]>; } diff --git a/apps/server/db/schema.ts b/apps/server/db/schema.ts index c6cd84a..469b02e 100644 --- a/apps/server/db/schema.ts +++ b/apps/server/db/schema.ts @@ -514,6 +514,21 @@ export const agentLogChunks = sqliteTable('agent_log_chunks', { export type AgentLogChunk = InferSelectModel; export type NewAgentLogChunk = InferInsertModel; +// ============================================================================ +// AGENT METRICS +// ============================================================================ + +export const agentMetrics = sqliteTable('agent_metrics', { + agentId: text('agent_id').primaryKey(), + questionsCount: integer('questions_count').notNull().default(0), + subagentsCount: integer('subagents_count').notNull().default(0), + compactionsCount: integer('compactions_count').notNull().default(0), + updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), +}); + +export type AgentMetrics = InferSelectModel; +export type NewAgentMetrics = InferInsertModel; + // ============================================================================ // CONVERSATIONS (inter-agent communication) // ============================================================================ diff --git a/apps/server/drizzle/0037_eager_devos.sql b/apps/server/drizzle/0037_eager_devos.sql new file mode 100644 index 0000000..3b09557 --- /dev/null +++ b/apps/server/drizzle/0037_eager_devos.sql @@ -0,0 +1,7 @@ +CREATE TABLE `agent_metrics` ( + `agent_id` text PRIMARY KEY NOT NULL, + `questions_count` integer DEFAULT 0 NOT NULL, + `subagents_count` integer DEFAULT 0 NOT NULL, + `compactions_count` integer DEFAULT 0 NOT NULL, + `updated_at` integer NOT NULL +); diff --git a/apps/server/drizzle/0038_worthless_princess_powerful.sql b/apps/server/drizzle/0038_worthless_princess_powerful.sql new file mode 100644 index 0000000..98efc6b --- /dev/null +++ b/apps/server/drizzle/0038_worthless_princess_powerful.sql @@ -0,0 +1 @@ +ALTER TABLE `initiatives` ADD `quality_review` integer DEFAULT false NOT NULL; diff --git a/apps/server/drizzle/meta/0036_snapshot.json b/apps/server/drizzle/meta/0036_snapshot.json new file mode 100644 index 0000000..5a2e6bc --- /dev/null +++ b/apps/server/drizzle/meta/0036_snapshot.json @@ -0,0 +1,1981 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "f85b9df3-dead-4c46-90ac-cf36bcaa6eb4", + "prevId": "c84e499f-7df8-4091-b2a5-6b12847898bd", + "tables": { + "accounts": { + "name": "accounts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'claude'" + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credentials": { + "name": "credentials", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_exhausted": { + "name": "is_exhausted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "exhausted_until": { + "name": "exhausted_until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_log_chunks": { + "name": "agent_log_chunks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_number": { + "name": "session_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_log_chunks_agent_id_idx": { + "name": "agent_log_chunks_agent_id_idx", + "columns": [ + "agent_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agents": { + "name": "agents", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "worktree_id": { + "name": "worktree_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'claude'" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'idle'" + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'execute'" + }, + "pid": { + "name": "pid", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_file_path": { + "name": "output_file_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_questions": { + "name": "pending_questions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_dismissed_at": { + "name": "user_dismissed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "agents_name_unique": { + "name": "agents_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "agents_task_id_tasks_id_fk": { + "name": "agents_task_id_tasks_id_fk", + "tableFrom": "agents", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_initiative_id_initiatives_id_fk": { + "name": "agents_initiative_id_initiatives_id_fk", + "tableFrom": "agents", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_account_id_accounts_id_fk": { + "name": "agents_account_id_accounts_id_fk", + "tableFrom": "agents", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "change_set_entries": { + "name": "change_set_entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "previous_state": { + "name": "previous_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_state": { + "name": "new_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "change_set_entries_change_set_id_idx": { + "name": "change_set_entries_change_set_id_idx", + "columns": [ + "change_set_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "change_set_entries_change_set_id_change_sets_id_fk": { + "name": "change_set_entries_change_set_id_change_sets_id_fk", + "tableFrom": "change_set_entries", + "tableTo": "change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "change_sets": { + "name": "change_sets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'applied'" + }, + "reverted_at": { + "name": "reverted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "change_sets_initiative_id_idx": { + "name": "change_sets_initiative_id_idx", + "columns": [ + "initiative_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "change_sets_agent_id_agents_id_fk": { + "name": "change_sets_agent_id_agents_id_fk", + "tableFrom": "change_sets", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "change_sets_initiative_id_initiatives_id_fk": { + "name": "change_sets_initiative_id_initiatives_id_fk", + "tableFrom": "change_sets", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_messages": { + "name": "chat_messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "chat_session_id": { + "name": "chat_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "chat_messages_session_id_idx": { + "name": "chat_messages_session_id_idx", + "columns": [ + "chat_session_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chat_messages_chat_session_id_chat_sessions_id_fk": { + "name": "chat_messages_chat_session_id_chat_sessions_id_fk", + "tableFrom": "chat_messages", + "tableTo": "chat_sessions", + "columnsFrom": [ + "chat_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_messages_change_set_id_change_sets_id_fk": { + "name": "chat_messages_change_set_id_change_sets_id_fk", + "tableFrom": "chat_messages", + "tableTo": "change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_sessions": { + "name": "chat_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "chat_sessions_target_idx": { + "name": "chat_sessions_target_idx", + "columns": [ + "target_type", + "target_id" + ], + "isUnique": false + }, + "chat_sessions_initiative_id_idx": { + "name": "chat_sessions_initiative_id_idx", + "columns": [ + "initiative_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chat_sessions_initiative_id_initiatives_id_fk": { + "name": "chat_sessions_initiative_id_initiatives_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_sessions_agent_id_agents_id_fk": { + "name": "chat_sessions_agent_id_agents_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "conversations": { + "name": "conversations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "from_agent_id": { + "name": "from_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "to_agent_id": { + "name": "to_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question": { + "name": "question", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "answer": { + "name": "answer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "conversations_to_agent_status_idx": { + "name": "conversations_to_agent_status_idx", + "columns": [ + "to_agent_id", + "status" + ], + "isUnique": false + }, + "conversations_from_agent_idx": { + "name": "conversations_from_agent_idx", + "columns": [ + "from_agent_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "conversations_from_agent_id_agents_id_fk": { + "name": "conversations_from_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "from_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_to_agent_id_agents_id_fk": { + "name": "conversations_to_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "to_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_initiative_id_initiatives_id_fk": { + "name": "conversations_initiative_id_initiatives_id_fk", + "tableFrom": "conversations", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_phase_id_phases_id_fk": { + "name": "conversations_phase_id_phases_id_fk", + "tableFrom": "conversations", + "tableTo": "phases", + "columnsFrom": [ + "phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_task_id_tasks_id_fk": { + "name": "conversations_task_id_tasks_id_fk", + "tableFrom": "conversations", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "errands": { + "name": "errands", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'main'" + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "errands_agent_id_agents_id_fk": { + "name": "errands_agent_id_agents_id_fk", + "tableFrom": "errands", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "errands_project_id_projects_id_fk": { + "name": "errands_project_id_projects_id_fk", + "tableFrom": "errands", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "initiative_projects": { + "name": "initiative_projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "initiative_project_unique": { + "name": "initiative_project_unique", + "columns": [ + "initiative_id", + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "initiative_projects_initiative_id_initiatives_id_fk": { + "name": "initiative_projects_initiative_id_initiatives_id_fk", + "tableFrom": "initiative_projects", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "initiative_projects_project_id_projects_id_fk": { + "name": "initiative_projects_project_id_projects_id_fk", + "tableFrom": "initiative_projects", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "initiatives": { + "name": "initiatives", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "execution_mode": { + "name": "execution_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'review_per_phase'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "sender_type": { + "name": "sender_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sender_id": { + "name": "sender_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_type": { + "name": "recipient_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'info'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requires_response": { + "name": "requires_response", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "messages_sender_id_agents_id_fk": { + "name": "messages_sender_id_agents_id_fk", + "tableFrom": "messages", + "tableTo": "agents", + "columnsFrom": [ + "sender_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "messages_recipient_id_agents_id_fk": { + "name": "messages_recipient_id_agents_id_fk", + "tableFrom": "messages", + "tableTo": "agents", + "columnsFrom": [ + "recipient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "messages_parent_message_id_messages_id_fk": { + "name": "messages_parent_message_id_messages_id_fk", + "tableFrom": "messages", + "tableTo": "messages", + "columnsFrom": [ + "parent_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "pages": { + "name": "pages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_page_id": { + "name": "parent_page_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "pages_initiative_id_initiatives_id_fk": { + "name": "pages_initiative_id_initiatives_id_fk", + "tableFrom": "pages", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pages_parent_page_id_pages_id_fk": { + "name": "pages_parent_page_id_pages_id_fk", + "tableFrom": "pages", + "tableTo": "pages", + "columnsFrom": [ + "parent_page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "phase_dependencies": { + "name": "phase_dependencies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "depends_on_phase_id": { + "name": "depends_on_phase_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "phase_dependencies_phase_id_phases_id_fk": { + "name": "phase_dependencies_phase_id_phases_id_fk", + "tableFrom": "phase_dependencies", + "tableTo": "phases", + "columnsFrom": [ + "phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "phase_dependencies_depends_on_phase_id_phases_id_fk": { + "name": "phase_dependencies_depends_on_phase_id_phases_id_fk", + "tableFrom": "phase_dependencies", + "tableTo": "phases", + "columnsFrom": [ + "depends_on_phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "phases": { + "name": "phases", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "merge_base": { + "name": "merge_base", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "phases_initiative_id_initiatives_id_fk": { + "name": "phases_initiative_id_initiatives_id_fk", + "tableFrom": "phases", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'main'" + }, + "last_fetched_at": { + "name": "last_fetched_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "projects_name_unique": { + "name": "projects_name_unique", + "columns": [ + "name" + ], + "isUnique": true + }, + "projects_url_unique": { + "name": "projects_url_unique", + "columns": [ + "url" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "review_comments": { + "name": "review_comments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "line_type": { + "name": "line_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'you'" + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved": { + "name": "resolved", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "review_comments_phase_id_idx": { + "name": "review_comments_phase_id_idx", + "columns": [ + "phase_id" + ], + "isUnique": false + }, + "review_comments_parent_id_idx": { + "name": "review_comments_parent_id_idx", + "columns": [ + "parent_comment_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "review_comments_phase_id_phases_id_fk": { + "name": "review_comments_phase_id_phases_id_fk", + "tableFrom": "review_comments", + "tableTo": "phases", + "columnsFrom": [ + "phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_comments_parent_comment_id_review_comments_id_fk": { + "name": "review_comments_parent_comment_id_review_comments_id_fk", + "tableFrom": "review_comments", + "tableTo": "review_comments", + "columnsFrom": [ + "parent_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "task_dependencies": { + "name": "task_dependencies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "depends_on_task_id": { + "name": "depends_on_task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "task_dependencies_task_id_tasks_id_fk": { + "name": "task_dependencies_task_id_tasks_id_fk", + "tableFrom": "task_dependencies", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_dependencies_depends_on_task_id_tasks_id_fk": { + "name": "task_dependencies_depends_on_task_id_tasks_id_fk", + "tableFrom": "task_dependencies", + "tableTo": "tasks", + "columnsFrom": [ + "depends_on_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tasks": { + "name": "tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'auto'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'execute'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "tasks_phase_id_phases_id_fk": { + "name": "tasks_phase_id_phases_id_fk", + "tableFrom": "tasks", + "tableTo": "phases", + "columnsFrom": [ + "phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_initiative_id_initiatives_id_fk": { + "name": "tasks_initiative_id_initiatives_id_fk", + "tableFrom": "tasks", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_parent_task_id_tasks_id_fk": { + "name": "tasks_parent_task_id_tasks_id_fk", + "tableFrom": "tasks", + "tableTo": "tasks", + "columnsFrom": [ + "parent_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/server/drizzle/meta/0037_snapshot.json b/apps/server/drizzle/meta/0037_snapshot.json index b710946..c46274d 100644 --- a/apps/server/drizzle/meta/0037_snapshot.json +++ b/apps/server/drizzle/meta/0037_snapshot.json @@ -1,8 +1,8 @@ { "version": "6", "dialect": "sqlite", - "id": "ef1d4ce7-a9c4-4e86-9cac-2e9cbbd0e688", - "prevId": "c84e499f-7df8-4091-b2a5-6b12847898bd", + "id": "eb30417e-d030-457f-911e-6566dce54fc9", + "prevId": "f85b9df3-dead-4c46-90ac-cf36bcaa6eb4", "tables": { "accounts": { "name": "accounts", @@ -155,6 +155,54 @@ "uniqueConstraints": {}, "checkConstraints": {} }, + "agent_metrics": { + "name": "agent_metrics", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "questions_count": { + "name": "questions_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "subagents_count": { + "name": "subagents_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "compactions_count": { + "name": "compactions_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, "agents": { "name": "agents", "columns": { @@ -1137,14 +1185,6 @@ "autoincrement": false, "default": "'review_per_phase'" }, - "quality_review": { - "name": "quality_review", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, "created_at": { "name": "created_at", "type": "integer", diff --git a/apps/server/drizzle/meta/0038_snapshot.json b/apps/server/drizzle/meta/0038_snapshot.json new file mode 100644 index 0000000..a49cdc4 --- /dev/null +++ b/apps/server/drizzle/meta/0038_snapshot.json @@ -0,0 +1,2037 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "ef1d4ce7-a9c4-4e86-9cac-2e9cbbd0e688", + "prevId": "eb30417e-d030-457f-911e-6566dce54fc9", + "tables": { + "accounts": { + "name": "accounts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'claude'" + }, + "config_json": { + "name": "config_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credentials": { + "name": "credentials", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_exhausted": { + "name": "is_exhausted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "exhausted_until": { + "name": "exhausted_until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_log_chunks": { + "name": "agent_log_chunks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_number": { + "name": "session_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "agent_log_chunks_agent_id_idx": { + "name": "agent_log_chunks_agent_id_idx", + "columns": [ + "agent_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_metrics": { + "name": "agent_metrics", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "questions_count": { + "name": "questions_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "subagents_count": { + "name": "subagents_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "compactions_count": { + "name": "compactions_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agents": { + "name": "agents", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "worktree_id": { + "name": "worktree_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'claude'" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'idle'" + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'execute'" + }, + "pid": { + "name": "pid", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_file_path": { + "name": "output_file_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_questions": { + "name": "pending_questions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_dismissed_at": { + "name": "user_dismissed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "agents_name_unique": { + "name": "agents_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "agents_task_id_tasks_id_fk": { + "name": "agents_task_id_tasks_id_fk", + "tableFrom": "agents", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_initiative_id_initiatives_id_fk": { + "name": "agents_initiative_id_initiatives_id_fk", + "tableFrom": "agents", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_account_id_accounts_id_fk": { + "name": "agents_account_id_accounts_id_fk", + "tableFrom": "agents", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "change_set_entries": { + "name": "change_set_entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "previous_state": { + "name": "previous_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_state": { + "name": "new_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "change_set_entries_change_set_id_idx": { + "name": "change_set_entries_change_set_id_idx", + "columns": [ + "change_set_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "change_set_entries_change_set_id_change_sets_id_fk": { + "name": "change_set_entries_change_set_id_change_sets_id_fk", + "tableFrom": "change_set_entries", + "tableTo": "change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "change_sets": { + "name": "change_sets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'applied'" + }, + "reverted_at": { + "name": "reverted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "change_sets_initiative_id_idx": { + "name": "change_sets_initiative_id_idx", + "columns": [ + "initiative_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "change_sets_agent_id_agents_id_fk": { + "name": "change_sets_agent_id_agents_id_fk", + "tableFrom": "change_sets", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "change_sets_initiative_id_initiatives_id_fk": { + "name": "change_sets_initiative_id_initiatives_id_fk", + "tableFrom": "change_sets", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_messages": { + "name": "chat_messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "chat_session_id": { + "name": "chat_session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "chat_messages_session_id_idx": { + "name": "chat_messages_session_id_idx", + "columns": [ + "chat_session_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chat_messages_chat_session_id_chat_sessions_id_fk": { + "name": "chat_messages_chat_session_id_chat_sessions_id_fk", + "tableFrom": "chat_messages", + "tableTo": "chat_sessions", + "columnsFrom": [ + "chat_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_messages_change_set_id_change_sets_id_fk": { + "name": "chat_messages_change_set_id_change_sets_id_fk", + "tableFrom": "chat_messages", + "tableTo": "change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_sessions": { + "name": "chat_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "chat_sessions_target_idx": { + "name": "chat_sessions_target_idx", + "columns": [ + "target_type", + "target_id" + ], + "isUnique": false + }, + "chat_sessions_initiative_id_idx": { + "name": "chat_sessions_initiative_id_idx", + "columns": [ + "initiative_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chat_sessions_initiative_id_initiatives_id_fk": { + "name": "chat_sessions_initiative_id_initiatives_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_sessions_agent_id_agents_id_fk": { + "name": "chat_sessions_agent_id_agents_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "conversations": { + "name": "conversations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "from_agent_id": { + "name": "from_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "to_agent_id": { + "name": "to_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question": { + "name": "question", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "answer": { + "name": "answer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "conversations_to_agent_status_idx": { + "name": "conversations_to_agent_status_idx", + "columns": [ + "to_agent_id", + "status" + ], + "isUnique": false + }, + "conversations_from_agent_idx": { + "name": "conversations_from_agent_idx", + "columns": [ + "from_agent_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "conversations_from_agent_id_agents_id_fk": { + "name": "conversations_from_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "from_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_to_agent_id_agents_id_fk": { + "name": "conversations_to_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "to_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_initiative_id_initiatives_id_fk": { + "name": "conversations_initiative_id_initiatives_id_fk", + "tableFrom": "conversations", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_phase_id_phases_id_fk": { + "name": "conversations_phase_id_phases_id_fk", + "tableFrom": "conversations", + "tableTo": "phases", + "columnsFrom": [ + "phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_task_id_tasks_id_fk": { + "name": "conversations_task_id_tasks_id_fk", + "tableFrom": "conversations", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "errands": { + "name": "errands", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'main'" + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "errands_agent_id_agents_id_fk": { + "name": "errands_agent_id_agents_id_fk", + "tableFrom": "errands", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "errands_project_id_projects_id_fk": { + "name": "errands_project_id_projects_id_fk", + "tableFrom": "errands", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "initiative_projects": { + "name": "initiative_projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "initiative_project_unique": { + "name": "initiative_project_unique", + "columns": [ + "initiative_id", + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "initiative_projects_initiative_id_initiatives_id_fk": { + "name": "initiative_projects_initiative_id_initiatives_id_fk", + "tableFrom": "initiative_projects", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "initiative_projects_project_id_projects_id_fk": { + "name": "initiative_projects_project_id_projects_id_fk", + "tableFrom": "initiative_projects", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "initiatives": { + "name": "initiatives", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "execution_mode": { + "name": "execution_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'review_per_phase'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quality_review": { + "name": "quality_review", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "sender_type": { + "name": "sender_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sender_id": { + "name": "sender_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_type": { + "name": "recipient_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'info'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requires_response": { + "name": "requires_response", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "messages_sender_id_agents_id_fk": { + "name": "messages_sender_id_agents_id_fk", + "tableFrom": "messages", + "tableTo": "agents", + "columnsFrom": [ + "sender_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "messages_recipient_id_agents_id_fk": { + "name": "messages_recipient_id_agents_id_fk", + "tableFrom": "messages", + "tableTo": "agents", + "columnsFrom": [ + "recipient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "messages_parent_message_id_messages_id_fk": { + "name": "messages_parent_message_id_messages_id_fk", + "tableFrom": "messages", + "tableTo": "messages", + "columnsFrom": [ + "parent_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "pages": { + "name": "pages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_page_id": { + "name": "parent_page_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "pages_initiative_id_initiatives_id_fk": { + "name": "pages_initiative_id_initiatives_id_fk", + "tableFrom": "pages", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pages_parent_page_id_pages_id_fk": { + "name": "pages_parent_page_id_pages_id_fk", + "tableFrom": "pages", + "tableTo": "pages", + "columnsFrom": [ + "parent_page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "phase_dependencies": { + "name": "phase_dependencies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "depends_on_phase_id": { + "name": "depends_on_phase_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "phase_dependencies_phase_id_phases_id_fk": { + "name": "phase_dependencies_phase_id_phases_id_fk", + "tableFrom": "phase_dependencies", + "tableTo": "phases", + "columnsFrom": [ + "phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "phase_dependencies_depends_on_phase_id_phases_id_fk": { + "name": "phase_dependencies_depends_on_phase_id_phases_id_fk", + "tableFrom": "phase_dependencies", + "tableTo": "phases", + "columnsFrom": [ + "depends_on_phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "phases": { + "name": "phases", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "merge_base": { + "name": "merge_base", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "phases_initiative_id_initiatives_id_fk": { + "name": "phases_initiative_id_initiatives_id_fk", + "tableFrom": "phases", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'main'" + }, + "last_fetched_at": { + "name": "last_fetched_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "projects_name_unique": { + "name": "projects_name_unique", + "columns": [ + "name" + ], + "isUnique": true + }, + "projects_url_unique": { + "name": "projects_url_unique", + "columns": [ + "url" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "review_comments": { + "name": "review_comments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "line_type": { + "name": "line_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'you'" + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved": { + "name": "resolved", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "review_comments_phase_id_idx": { + "name": "review_comments_phase_id_idx", + "columns": [ + "phase_id" + ], + "isUnique": false + }, + "review_comments_parent_id_idx": { + "name": "review_comments_parent_id_idx", + "columns": [ + "parent_comment_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "review_comments_phase_id_phases_id_fk": { + "name": "review_comments_phase_id_phases_id_fk", + "tableFrom": "review_comments", + "tableTo": "phases", + "columnsFrom": [ + "phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_comments_parent_comment_id_review_comments_id_fk": { + "name": "review_comments_parent_comment_id_review_comments_id_fk", + "tableFrom": "review_comments", + "tableTo": "review_comments", + "columnsFrom": [ + "parent_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "task_dependencies": { + "name": "task_dependencies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "depends_on_task_id": { + "name": "depends_on_task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "task_dependencies_task_id_tasks_id_fk": { + "name": "task_dependencies_task_id_tasks_id_fk", + "tableFrom": "task_dependencies", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_dependencies_depends_on_task_id_tasks_id_fk": { + "name": "task_dependencies_depends_on_task_id_tasks_id_fk", + "tableFrom": "task_dependencies", + "tableTo": "tasks", + "columnsFrom": [ + "depends_on_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tasks": { + "name": "tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "phase_id": { + "name": "phase_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initiative_id": { + "name": "initiative_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'auto'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'execute'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "tasks_phase_id_phases_id_fk": { + "name": "tasks_phase_id_phases_id_fk", + "tableFrom": "tasks", + "tableTo": "phases", + "columnsFrom": [ + "phase_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_initiative_id_initiatives_id_fk": { + "name": "tasks_initiative_id_initiatives_id_fk", + "tableFrom": "tasks", + "tableTo": "initiatives", + "columnsFrom": [ + "initiative_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_parent_task_id_tasks_id_fk": { + "name": "tasks_parent_task_id_tasks_id_fk", + "tableFrom": "tasks", + "tableTo": "tasks", + "columnsFrom": [ + "parent_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/apps/server/drizzle/meta/_journal.json b/apps/server/drizzle/meta/_journal.json index 1f15581..99380c5 100644 --- a/apps/server/drizzle/meta/_journal.json +++ b/apps/server/drizzle/meta/_journal.json @@ -264,8 +264,15 @@ { "idx": 37, "version": "6", + "when": 1772828694292, + "tag": "0037_eager_devos", + "breakpoints": true + }, + { + "idx": 38, + "version": "6", "when": 1772829916655, - "tag": "0037_worthless_princess_powerful", + "tag": "0038_worthless_princess_powerful", "breakpoints": true } ] diff --git a/apps/server/execution/orchestrator.ts b/apps/server/execution/orchestrator.ts index dceeab3..4ab94e0 100644 --- a/apps/server/execution/orchestrator.ts +++ b/apps/server/execution/orchestrator.ts @@ -21,7 +21,7 @@ import type { AgentRepository } from '../db/repositories/agent-repository.js'; import type { AgentManager } from '../agent/types.js'; import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js'; import type { ConflictResolutionService } from '../coordination/conflict-resolution-service.js'; -import { phaseBranchName, taskBranchName } from '../git/branch-naming.js'; +import { phaseBranchName, taskBranchName, isPlanningCategory } from '../git/branch-naming.js'; import { ensureProjectClone } from '../git/project-clones.js'; import { createModuleLogger } from '../logger/index.js'; import { phaseMetaCache, fileDiffCache } from '../review/diff-cache.js'; @@ -708,6 +708,23 @@ export class ExecutionOrchestrator { } } + // Clean up stale duplicate planning tasks (e.g. a crashed detail task + // that was reset to pending, then a new detail task was created and completed). + const tasksAfterRecovery = await this.taskRepository.findByPhaseId(phase.id); + const completedPlanningNames = new Set(); + for (const t of tasksAfterRecovery) { + if (isPlanningCategory(t.category) && t.status === 'completed') { + completedPlanningNames.add(`${t.category}:${t.phaseId}`); + } + } + for (const t of tasksAfterRecovery) { + if (isPlanningCategory(t.category) && t.status === 'pending' && completedPlanningNames.has(`${t.category}:${t.phaseId}`)) { + await this.taskRepository.update(t.id, { status: 'completed', summary: 'Superseded by retry' }); + tasksRecovered++; + log.info({ taskId: t.id, category: t.category }, 'recovered stale duplicate planning task'); + } + } + // Re-read tasks after recovery updates and check if phase is now fully done const updatedTasks = await this.taskRepository.findByPhaseId(phase.id); const allDone = updatedTasks.every((t) => t.status === 'completed'); diff --git a/apps/server/git/simple-git-branch-manager.ts b/apps/server/git/simple-git-branch-manager.ts index bfe969d..841b18c 100644 --- a/apps/server/git/simple-git-branch-manager.ts +++ b/apps/server/git/simple-git-branch-manager.ts @@ -46,8 +46,17 @@ export class SimpleGitBranchManager implements BranchManager { return; } - await git.branch([branch, baseBranch]); - log.info({ repoPath, branch, baseBranch }, 'branch created'); + try { + await git.branch([branch, baseBranch]); + log.info({ repoPath, branch, baseBranch }, 'branch created'); + } catch (err) { + // Handle TOCTOU race: branch may have been created between the exists check and now + if (err instanceof Error && err.message.includes('already exists')) { + log.debug({ repoPath, branch }, 'branch created by concurrent process'); + return; + } + throw err; + } } async mergeBranch(repoPath: string, sourceBranch: string, targetBranch: string): Promise { diff --git a/apps/server/scripts/backfill-metrics.test.ts b/apps/server/scripts/backfill-metrics.test.ts new file mode 100644 index 0000000..1dbb966 --- /dev/null +++ b/apps/server/scripts/backfill-metrics.test.ts @@ -0,0 +1,131 @@ +/** + * Tests for the backfill-metrics script. + * + * Uses an in-memory test database to verify that backfillMetrics correctly + * accumulates counts from agent_log_chunks and upserts into agent_metrics. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { createTestDatabase } from '../db/repositories/drizzle/test-helpers.js'; +import type { DrizzleDatabase } from '../db/index.js'; +import { agentLogChunks, agentMetrics } from '../db/index.js'; +import { backfillMetrics } from './backfill-metrics.js'; +import { nanoid } from 'nanoid'; +import { eq } from 'drizzle-orm'; + +async function insertChunk(db: DrizzleDatabase, agentId: string, content: object | string) { + await db.insert(agentLogChunks).values({ + id: nanoid(), + agentId, + agentName: 'test-agent', + sessionNumber: 1, + content: typeof content === 'string' ? content : JSON.stringify(content), + createdAt: new Date(), + }); +} + +describe('backfillMetrics', () => { + let db: DrizzleDatabase; + + beforeEach(() => { + db = createTestDatabase(); + }); + + it('AskUserQuestion chunks — questionsCount correct', async () => { + await insertChunk(db, 'agent-a', { type: 'tool_use', name: 'AskUserQuestion', input: { questions: [{}, {}] } }); + await insertChunk(db, 'agent-a', { type: 'tool_use', name: 'AskUserQuestion', input: { questions: [{}] } }); + + await backfillMetrics(db); + + const rows = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-a')); + expect(rows).toHaveLength(1); + expect(rows[0].questionsCount).toBe(3); + expect(rows[0].subagentsCount).toBe(0); + expect(rows[0].compactionsCount).toBe(0); + }); + + it('Agent tool chunks — subagentsCount correct', async () => { + await insertChunk(db, 'agent-b', { type: 'tool_use', name: 'Agent' }); + await insertChunk(db, 'agent-b', { type: 'tool_use', name: 'Agent' }); + + await backfillMetrics(db); + + const rows = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-b')); + expect(rows).toHaveLength(1); + expect(rows[0].questionsCount).toBe(0); + expect(rows[0].subagentsCount).toBe(2); + expect(rows[0].compactionsCount).toBe(0); + }); + + it('Compaction chunks — compactionsCount correct', async () => { + await insertChunk(db, 'agent-c', { type: 'system', subtype: 'init', source: 'compact' }); + + await backfillMetrics(db); + + const rows = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-c')); + expect(rows).toHaveLength(1); + expect(rows[0].questionsCount).toBe(0); + expect(rows[0].subagentsCount).toBe(0); + expect(rows[0].compactionsCount).toBe(1); + }); + + it('Irrelevant chunk type — no metrics row created', async () => { + await insertChunk(db, 'agent-d', { type: 'text', text: 'hello' }); + + await backfillMetrics(db); + + const rows = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-d')); + expect(rows).toEqual([]); + }); + + it('Malformed JSON chunk — skipped, no crash', async () => { + await insertChunk(db, 'agent-e', 'not-valid-json'); + await insertChunk(db, 'agent-e', { type: 'tool_use', name: 'Agent' }); + + await expect(backfillMetrics(db)).resolves.not.toThrow(); + + const rows = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-e')); + expect(rows).toHaveLength(1); + expect(rows[0].subagentsCount).toBe(1); + }); + + it('Multiple agents — counts isolated per agent', async () => { + await insertChunk(db, 'agent-f', { type: 'tool_use', name: 'AskUserQuestion', input: { questions: [{}, {}, {}] } }); + await insertChunk(db, 'agent-f', { type: 'tool_use', name: 'AskUserQuestion', input: { questions: [{}, {}, {}] } }); + await insertChunk(db, 'agent-g', { type: 'tool_use', name: 'Agent' }); + + await backfillMetrics(db); + + const rowsF = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-f')); + expect(rowsF).toHaveLength(1); + expect(rowsF[0].questionsCount).toBe(6); + expect(rowsF[0].subagentsCount).toBe(0); + expect(rowsF[0].compactionsCount).toBe(0); + + const rowsG = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-g')); + expect(rowsG).toHaveLength(1); + expect(rowsG[0].questionsCount).toBe(0); + expect(rowsG[0].subagentsCount).toBe(1); + expect(rowsG[0].compactionsCount).toBe(0); + }); + + it('Empty database — completes without error', async () => { + await expect(backfillMetrics(db)).resolves.not.toThrow(); + + const rows = await db.select().from(agentMetrics); + expect(rows).toEqual([]); + }); + + it('Re-run idempotency note — second run doubles counts', async () => { + // Documented behavior: run only once against a fresh agent_metrics table + await insertChunk(db, 'agent-h', { type: 'tool_use', name: 'Agent' }); + + await backfillMetrics(db); + const rowsAfterFirst = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-h')); + expect(rowsAfterFirst[0].subagentsCount).toBe(1); + + await backfillMetrics(db); + const rowsAfterSecond = await db.select().from(agentMetrics).where(eq(agentMetrics.agentId, 'agent-h')); + expect(rowsAfterSecond[0].subagentsCount).toBe(2); + }); +}); diff --git a/apps/server/scripts/backfill-metrics.ts b/apps/server/scripts/backfill-metrics.ts new file mode 100644 index 0000000..42dfe25 --- /dev/null +++ b/apps/server/scripts/backfill-metrics.ts @@ -0,0 +1,128 @@ +/** + * Backfill script for agent_metrics table. + * + * Reads all existing agent_log_chunks rows and populates agent_metrics with + * accumulated counts of questions, subagent spawns, and compaction events. + * + * Intended to be run once per production database after applying the migration + * that introduces the agent_metrics table. + * + * Idempotency note: Uses ON CONFLICT DO UPDATE with additive increments to match + * the ongoing insertChunk write-path behavior. Running against an empty + * agent_metrics table is fully safe. Running a second time will double-count — + * only run this script once per database, immediately after applying the migration. + */ + +import { asc, sql } from 'drizzle-orm'; +import { createDatabase, DrizzleDatabase, agentLogChunks, agentMetrics } from '../db/index.js'; + +const BATCH_SIZE = 500; +const LOG_EVERY = 1000; + +/** + * Core backfill function. Accepts a DrizzleDatabase for testability. + */ +export async function backfillMetrics(db: DrizzleDatabase): Promise { + const accumulator = new Map(); + let offset = 0; + let totalChunks = 0; + let malformedCount = 0; + + while (true) { + const batch = await db + .select({ agentId: agentLogChunks.agentId, content: agentLogChunks.content }) + .from(agentLogChunks) + .orderBy(asc(agentLogChunks.createdAt)) + .limit(BATCH_SIZE) + .offset(offset); + + if (batch.length === 0) break; + + for (const chunk of batch) { + let parsed: unknown; + try { + parsed = JSON.parse(chunk.content); + } catch { + malformedCount++; + totalChunks++; + if (totalChunks % LOG_EVERY === 0) { + console.log(`Processed ${totalChunks} chunks...`); + } + continue; + } + + if (typeof parsed !== 'object' || parsed === null) { + totalChunks++; + if (totalChunks % LOG_EVERY === 0) { + console.log(`Processed ${totalChunks} chunks...`); + } + continue; + } + + const obj = parsed as Record; + const type = obj['type']; + const name = obj['name']; + + if (type === 'tool_use' && name === 'AskUserQuestion') { + const input = obj['input'] as Record | undefined; + const questions = input?.['questions']; + const count = Array.isArray(questions) ? questions.length : 0; + if (count > 0) { + const entry = accumulator.get(chunk.agentId) ?? { questionsCount: 0, subagentsCount: 0, compactionsCount: 0 }; + entry.questionsCount += count; + accumulator.set(chunk.agentId, entry); + } + } else if (type === 'tool_use' && name === 'Agent') { + const entry = accumulator.get(chunk.agentId) ?? { questionsCount: 0, subagentsCount: 0, compactionsCount: 0 }; + entry.subagentsCount += 1; + accumulator.set(chunk.agentId, entry); + } else if (type === 'system' && obj['subtype'] === 'init' && obj['source'] === 'compact') { + const entry = accumulator.get(chunk.agentId) ?? { questionsCount: 0, subagentsCount: 0, compactionsCount: 0 }; + entry.compactionsCount += 1; + accumulator.set(chunk.agentId, entry); + } + + totalChunks++; + if (totalChunks % LOG_EVERY === 0) { + console.log(`Processed ${totalChunks} chunks...`); + } + } + + offset += BATCH_SIZE; + } + + // Upsert accumulated counts into agent_metrics. + // Uses additive ON CONFLICT DO UPDATE to match the ongoing insertChunk behavior. + for (const [agentId, counts] of accumulator) { + await db + .insert(agentMetrics) + .values({ + agentId, + questionsCount: counts.questionsCount, + subagentsCount: counts.subagentsCount, + compactionsCount: counts.compactionsCount, + updatedAt: new Date(), + }) + .onConflictDoUpdate({ + target: agentMetrics.agentId, + set: { + questionsCount: sql`${agentMetrics.questionsCount} + ${counts.questionsCount}`, + subagentsCount: sql`${agentMetrics.subagentsCount} + ${counts.subagentsCount}`, + compactionsCount: sql`${agentMetrics.compactionsCount} + ${counts.compactionsCount}`, + updatedAt: new Date(), + }, + }); + } + + console.log( + `Backfill complete: ${accumulator.size} agents updated, ${totalChunks} chunks processed, ${malformedCount} malformed chunks skipped` + ); +} + +/** + * CLI wrapper — opens a database from a path, then delegates to backfillMetrics. + */ +export async function backfillMetricsFromPath(dbPath: string): Promise { + const db = createDatabase(dbPath); + await backfillMetrics(db); +} diff --git a/apps/server/server/trpc-adapter.ts b/apps/server/server/trpc-adapter.ts index 4102068..cc80176 100644 --- a/apps/server/server/trpc-adapter.ts +++ b/apps/server/server/trpc-adapter.ts @@ -22,6 +22,7 @@ import type { LogChunkRepository } from '../db/repositories/log-chunk-repository import type { ConversationRepository } from '../db/repositories/conversation-repository.js'; import type { ChatSessionRepository } from '../db/repositories/chat-session-repository.js'; import type { ReviewCommentRepository } from '../db/repositories/review-comment-repository.js'; +import type { ErrandRepository } from '../db/repositories/errand-repository.js'; import type { AccountCredentialManager } from '../agent/credentials/types.js'; import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js'; import type { CoordinationManager } from '../coordination/types.js'; @@ -82,6 +83,8 @@ export interface TrpcAdapterOptions { reviewCommentRepository?: ReviewCommentRepository; /** Project sync manager for remote fetch/sync operations */ projectSyncManager?: ProjectSyncManager; + /** Errand repository for errand CRUD operations */ + errandRepository?: ErrandRepository; /** Absolute path to the workspace root (.cwrc directory) */ workspaceRoot?: string; } @@ -166,6 +169,7 @@ export function createTrpcHandler(options: TrpcAdapterOptions) { chatSessionRepository: options.chatSessionRepository, reviewCommentRepository: options.reviewCommentRepository, projectSyncManager: options.projectSyncManager, + errandRepository: options.errandRepository, workspaceRoot: options.workspaceRoot, }), }); diff --git a/apps/server/test/unit/radar-procedures.test.ts b/apps/server/test/unit/radar-procedures.test.ts index 01d642e..dacea69 100644 --- a/apps/server/test/unit/radar-procedures.test.ts +++ b/apps/server/test/unit/radar-procedures.test.ts @@ -326,6 +326,47 @@ describe('agent.listForRadar', () => { expect(row!.subagentsCount).toBe(0); expect(row!.compactionsCount).toBe(0); }); + + it('returns zero counts for agent with no metrics row', async () => { + const agents = new MockAgentManager(); + const ctx = makeCtx(agents); + + const now = new Date(); + // Agent with no log chunks at all — no agent_metrics row will exist + agents.addAgent({ id: 'agent-no-chunks', name: 'no-chunks-agent', status: 'running', createdAt: now }); + + const caller = createAgentCaller(ctx); + const result = await caller.listForRadar({ timeRange: 'all' }); + + const row = result.find(r => r.id === 'agent-no-chunks'); + expect(row).toBeDefined(); + expect(row!.questionsCount).toBe(0); + expect(row!.subagentsCount).toBe(0); + expect(row!.compactionsCount).toBe(0); + }); + + it('listForRadar response does not contain chunk content field', async () => { + const agents = new MockAgentManager(); + const ctx = makeCtx(agents); + const { logChunkRepo } = getRepos(ctx); + + const now = new Date(); + agents.addAgent({ id: 'agent-content', name: 'content-agent', status: 'running', createdAt: now }); + + await logChunkRepo.insertChunk({ + agentId: 'agent-content', + agentName: 'content-agent', + sessionNumber: 1, + content: JSON.stringify({ type: 'tool_use', name: 'Agent', input: { description: 'do stuff', prompt: 'some prompt' } }), + }); + + const caller = createAgentCaller(ctx); + const result = await caller.listForRadar({ timeRange: 'all' }); + + for (const row of result) { + expect(row).not.toHaveProperty('content'); + } + }); }); // ============================================================================= diff --git a/apps/server/trpc/routers/agent.ts b/apps/server/trpc/routers/agent.ts index 644b814..82d397d 100644 --- a/apps/server/trpc/routers/agent.ts +++ b/apps/server/trpc/routers/agent.ts @@ -475,8 +475,8 @@ export function agentProcedures(publicProcedure: ProcedureBuilder) { const uniqueTaskIds = [...new Set(filteredAgents.map(a => a.taskId).filter(Boolean) as string[])]; const uniqueInitiativeIds = [...new Set(filteredAgents.map(a => a.initiativeId).filter(Boolean) as string[])]; - const [chunks, messageCounts, taskResults, initiativeResults] = await Promise.all([ - logChunkRepo.findByAgentIds(matchingIds), + const [metrics, messageCounts, taskResults, initiativeResults] = await Promise.all([ + logChunkRepo.findMetricsByAgentIds(matchingIds), conversationRepo.countByFromAgentIds(matchingIds), Promise.all(uniqueTaskIds.map(id => taskRepo.findById(id))), Promise.all(uniqueInitiativeIds.map(id => initiativeRepo.findById(id))), @@ -486,37 +486,14 @@ export function agentProcedures(publicProcedure: ProcedureBuilder) { const taskMap = new Map(taskResults.filter(Boolean).map(t => [t!.id, t!.name])); const initiativeMap = new Map(initiativeResults.filter(Boolean).map(i => [i!.id, i!.name])); const messagesMap = new Map(messageCounts.map(m => [m.agentId, m.count])); - - // Group chunks by agentId - const chunksByAgent = new Map(); - for (const chunk of chunks) { - const existing = chunksByAgent.get(chunk.agentId); - if (existing) { - existing.push(chunk); - } else { - chunksByAgent.set(chunk.agentId, [chunk]); - } - } + const metricsMap = new Map(metrics.map(m => [m.agentId, m])); // Build result rows return filteredAgents.map(agent => { - const agentChunks = chunksByAgent.get(agent.id) ?? []; - let questionsCount = 0; - let subagentsCount = 0; - let compactionsCount = 0; - - for (const chunk of agentChunks) { - try { - const parsed = JSON.parse(chunk.content); - if (parsed.type === 'tool_use' && parsed.name === 'AskUserQuestion') { - questionsCount += parsed.input?.questions?.length ?? 0; - } else if (parsed.type === 'tool_use' && parsed.name === 'Agent') { - subagentsCount++; - } else if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.source === 'compact') { - compactionsCount++; - } - } catch { /* skip malformed */ } - } + const agentMetrics = metricsMap.get(agent.id); + const questionsCount = agentMetrics?.questionsCount ?? 0; + const subagentsCount = agentMetrics?.subagentsCount ?? 0; + const compactionsCount = agentMetrics?.compactionsCount ?? 0; return { id: agent.id, diff --git a/apps/server/trpc/routers/architect.ts b/apps/server/trpc/routers/architect.ts index 24bc3e9..b1f0354 100644 --- a/apps/server/trpc/routers/architect.ts +++ b/apps/server/trpc/routers/architect.ts @@ -337,6 +337,14 @@ export function architectProcedures(publicProcedure: ProcedureBuilder) { }); } + // Clean up orphaned pending/in_progress detail tasks from previous failed attempts + const phaseTasks = await taskRepo.findByPhaseId(input.phaseId); + for (const t of phaseTasks) { + if (t.category === 'detail' && (t.status === 'pending' || t.status === 'in_progress')) { + await taskRepo.update(t.id, { status: 'completed', summary: 'Superseded by retry' }); + } + } + const detailTaskName = input.taskName ?? `Detail: ${phase.name}`; const task = await taskRepo.create({ phaseId: phase.id, diff --git a/apps/server/trpc/routers/errand.ts b/apps/server/trpc/routers/errand.ts index 39b144c..3185f75 100644 --- a/apps/server/trpc/routers/errand.ts +++ b/apps/server/trpc/routers/errand.ts @@ -184,11 +184,11 @@ export function errandProcedures(publicProcedure: ProcedureBuilder) { .input(z.object({ projectId: z.string().optional(), status: z.enum(ErrandStatusValues).optional(), - })) + }).optional()) .query(async ({ ctx, input }) => { return requireErrandRepository(ctx).findAll({ - projectId: input.projectId, - status: input.status, + projectId: input?.projectId, + status: input?.status, }); }), diff --git a/apps/server/trpc/routers/phase-dispatch.ts b/apps/server/trpc/routers/phase-dispatch.ts index 4524390..ede1831 100644 --- a/apps/server/trpc/routers/phase-dispatch.ts +++ b/apps/server/trpc/routers/phase-dispatch.ts @@ -4,10 +4,35 @@ import { TRPCError } from '@trpc/server'; import { z } from 'zod'; -import type { Task } from '../../db/schema.js'; +import type { Phase, Task } from '../../db/schema.js'; import type { ProcedureBuilder } from '../trpc.js'; import { requirePhaseDispatchManager, requirePhaseRepository, requireTaskRepository } from './_helpers.js'; +const INTEGRATION_PHASE_NAME = 'Integration'; + +const INTEGRATION_TASK_DESCRIPTION = `Verify that all phase branches integrate correctly after merging into the initiative branch. + +Steps: +1. Build the project — fix any compilation errors +2. Run the full test suite — fix any failing tests +3. Run type checking and linting — fix any errors +4. Review cross-phase imports and shared interfaces for compatibility +5. Smoke test key user flows affected by the merged changes + +Only fix integration issues (type mismatches, conflicting exports, broken tests). Do not refactor or improve existing code.`; + +/** + * Find phase IDs that have no dependents (no other phase depends on them). + * These are the "end" / "leaf" phases in the dependency graph. + */ +function findEndPhaseIds( + phases: Phase[], + edges: Array<{ phaseId: string; dependsOnPhaseId: string }>, +): string[] { + const dependedOn = new Set(edges.map((e) => e.dependsOnPhaseId)); + return phases.filter((p) => !dependedOn.has(p.id)).map((p) => p.id); +} + export function phaseDispatchProcedures(publicProcedure: ProcedureBuilder) { return { queuePhase: publicProcedure @@ -23,7 +48,40 @@ export function phaseDispatchProcedures(publicProcedure: ProcedureBuilder) { .mutation(async ({ ctx, input }) => { const phaseDispatchManager = requirePhaseDispatchManager(ctx); const phaseRepo = requirePhaseRepository(ctx); - const phases = await phaseRepo.findByInitiativeId(input.initiativeId); + const taskRepo = requireTaskRepository(ctx); + + let phases = await phaseRepo.findByInitiativeId(input.initiativeId); + const edges = await phaseRepo.findDependenciesByInitiativeId(input.initiativeId); + + // Auto-create Integration phase if multiple end phases exist + const existingIntegration = phases.find((p) => p.name === INTEGRATION_PHASE_NAME); + if (!existingIntegration) { + const endPhaseIds = findEndPhaseIds(phases, edges); + if (endPhaseIds.length > 1) { + const integrationPhase = await phaseRepo.create({ + initiativeId: input.initiativeId, + name: INTEGRATION_PHASE_NAME, + status: 'approved', + }); + + for (const endPhaseId of endPhaseIds) { + await phaseRepo.createDependency(integrationPhase.id, endPhaseId); + } + + await taskRepo.create({ + phaseId: integrationPhase.id, + initiativeId: input.initiativeId, + name: 'Verify integration', + description: INTEGRATION_TASK_DESCRIPTION, + category: 'verify', + status: 'pending', + }); + + // Re-fetch so the new phase gets queued in the loop below + phases = await phaseRepo.findByInitiativeId(input.initiativeId); + } + } + let queued = 0; for (const phase of phases) { if (phase.status === 'approved') { diff --git a/apps/web/src/components/ErrandDetailPanel.tsx b/apps/web/src/components/ErrandDetailPanel.tsx index 04182a8..fe74258 100644 --- a/apps/web/src/components/ErrandDetailPanel.tsx +++ b/apps/web/src/components/ErrandDetailPanel.tsx @@ -331,8 +331,8 @@ export function ErrandDetailPanel({ errandId, onClose }: ErrandDetailPanelProps) {/* Info line */}
{errand.status === 'merged' - ? `Merged into ${errand.baseBranch} · ${formatRelativeTime(errand.updatedAt.toISOString())}` - : `Abandoned · ${formatRelativeTime(errand.updatedAt.toISOString())}`} + ? `Merged into ${errand.baseBranch} · ${formatRelativeTime(String(errand.updatedAt))}` + : `Abandoned · ${formatRelativeTime(String(errand.updatedAt))}`}
{/* Read-only diff */} diff --git a/apps/web/src/components/RefineSpawnDialog.tsx b/apps/web/src/components/RefineSpawnDialog.tsx index dc70a23..479c7f5 100644 --- a/apps/web/src/components/RefineSpawnDialog.tsx +++ b/apps/web/src/components/RefineSpawnDialog.tsx @@ -22,6 +22,8 @@ interface RefineSpawnDialogProps { showInstructionInput?: boolean; /** Placeholder text for the instruction textarea */ instructionPlaceholder?: string; + /** Pre-populate the instruction field (e.g. from a crashed agent's original instruction) */ + defaultInstruction?: string; /** Whether the spawn mutation is pending */ isSpawning: boolean; /** Error message if spawn failed */ @@ -38,6 +40,7 @@ export function RefineSpawnDialog({ description, showInstructionInput = true, instructionPlaceholder = "What should the agent focus on? (optional)", + defaultInstruction, isSpawning, error, onSpawn, @@ -53,18 +56,25 @@ export function RefineSpawnDialog({ onSpawn(finalInstruction); }; + const openDialog = () => { + setInstruction(defaultInstruction ?? ""); + setShowDialog(true); + }; + const handleOpenChange = (open: boolean) => { - setShowDialog(open); - if (!open) { + if (open) { + setInstruction(defaultInstruction ?? ""); + } else { setInstruction(""); } + setShowDialog(open); }; const defaultTrigger = ( - - ) - })} - - - ) -} diff --git a/apps/web/src/layouts/AppLayout.test.tsx b/apps/web/src/layouts/AppLayout.test.tsx new file mode 100644 index 0000000..8d12ba8 --- /dev/null +++ b/apps/web/src/layouts/AppLayout.test.tsx @@ -0,0 +1,66 @@ +// @vitest-environment happy-dom +import { render, screen, within } from '@testing-library/react' +import '@testing-library/jest-dom/vitest' +import { vi } from 'vitest' +import { AppLayout } from './AppLayout' + +// Mock dependencies +vi.mock('@tanstack/react-router', () => ({ + Link: ({ children, to }: any) => { + const content = typeof children === 'function' ? children({ isActive: false }) : children + return {content} + }, +})) +vi.mock('@/components/ThemeToggle', () => ({ ThemeToggle: () => null })) +vi.mock('@/components/HealthDot', () => ({ HealthDot: () => null })) +vi.mock('@/components/NavBadge', () => ({ + NavBadge: ({ count }: { count: number }) => ( + count > 0 ? {count} : null + ), +})) + +const mockUseQuery = vi.hoisted(() => vi.fn()) +vi.mock('@/lib/trpc', () => ({ + trpc: { + listAgents: { useQuery: mockUseQuery }, + }, +})) + +beforeEach(() => { + vi.clearAllMocks() + mockUseQuery.mockReturnValue({ data: [] }) +}) + +describe('AppLayout navItems', () => { + it('renders HQ nav link', () => { + render({null}) + expect(screen.getByRole('link', { name: /hq/i })).toBeInTheDocument() + }) + + it('does not render Inbox nav link', () => { + render({null}) + expect(screen.queryByRole('link', { name: /inbox/i })).not.toBeInTheDocument() + }) + + it('shows badge on HQ when agents are waiting_for_input', () => { + mockUseQuery.mockReturnValue({ + data: [ + { id: '1', status: 'waiting_for_input' }, + { id: '2', status: 'running' }, + ], + }) + render({null}) + // NavBadge rendered next to HQ link (count=1) + const hqLink = screen.getByRole('link', { name: /hq/i }) + const badge = within(hqLink).getByTestId('nav-badge') + expect(badge).toHaveTextContent('1') + }) + + it('does not show questions badge on any Inbox link (Inbox removed)', () => { + mockUseQuery.mockReturnValue({ + data: [{ id: '1', status: 'waiting_for_input' }], + }) + render({null}) + expect(screen.queryByRole('link', { name: /inbox/i })).not.toBeInTheDocument() + }) +}) diff --git a/apps/web/src/layouts/AppLayout.tsx b/apps/web/src/layouts/AppLayout.tsx index 7dbc2eb..043ac32 100644 --- a/apps/web/src/layouts/AppLayout.tsx +++ b/apps/web/src/layouts/AppLayout.tsx @@ -7,11 +7,11 @@ import { trpc } from '@/lib/trpc' import type { ConnectionState } from '@/hooks/useConnectionStatus' const navItems = [ - { label: 'HQ', to: '/hq', badgeKey: null }, + { label: 'HQ', to: '/hq', badgeKey: 'questions' as const }, { label: 'Initiatives', to: '/initiatives', badgeKey: null }, { label: 'Agents', to: '/agents', badgeKey: 'running' as const }, + { label: 'Errands', to: '/errands', badgeKey: null }, { label: 'Radar', to: '/radar', badgeKey: null }, - { label: 'Inbox', to: '/inbox', badgeKey: 'questions' as const }, { label: 'Settings', to: '/settings', badgeKey: null }, ] as const diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 411612e..1c12f5f 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -17,6 +17,7 @@ import { Route as AgentsRouteImport } from './routes/agents' import { Route as IndexRouteImport } from './routes/index' import { Route as SettingsIndexRouteImport } from './routes/settings/index' import { Route as InitiativesIndexRouteImport } from './routes/initiatives/index' +import { Route as ErrandsIndexRouteImport } from './routes/errands/index' import { Route as SettingsProjectsRouteImport } from './routes/settings/projects' import { Route as SettingsHealthRouteImport } from './routes/settings/health' import { Route as InitiativesIdRouteImport } from './routes/initiatives/$id' @@ -61,6 +62,11 @@ const InitiativesIndexRoute = InitiativesIndexRouteImport.update({ path: '/initiatives/', getParentRoute: () => rootRouteImport, } as any) +const ErrandsIndexRoute = ErrandsIndexRouteImport.update({ + id: '/errands/', + path: '/errands/', + getParentRoute: () => rootRouteImport, +} as any) const SettingsProjectsRoute = SettingsProjectsRouteImport.update({ id: '/projects', path: '/projects', @@ -87,6 +93,7 @@ export interface FileRoutesByFullPath { '/initiatives/$id': typeof InitiativesIdRoute '/settings/health': typeof SettingsHealthRoute '/settings/projects': typeof SettingsProjectsRoute + '/errands/': typeof ErrandsIndexRoute '/initiatives/': typeof InitiativesIndexRoute '/settings/': typeof SettingsIndexRoute } @@ -99,6 +106,7 @@ export interface FileRoutesByTo { '/initiatives/$id': typeof InitiativesIdRoute '/settings/health': typeof SettingsHealthRoute '/settings/projects': typeof SettingsProjectsRoute + '/errands': typeof ErrandsIndexRoute '/initiatives': typeof InitiativesIndexRoute '/settings': typeof SettingsIndexRoute } @@ -113,6 +121,7 @@ export interface FileRoutesById { '/initiatives/$id': typeof InitiativesIdRoute '/settings/health': typeof SettingsHealthRoute '/settings/projects': typeof SettingsProjectsRoute + '/errands/': typeof ErrandsIndexRoute '/initiatives/': typeof InitiativesIndexRoute '/settings/': typeof SettingsIndexRoute } @@ -128,6 +137,7 @@ export interface FileRouteTypes { | '/initiatives/$id' | '/settings/health' | '/settings/projects' + | '/errands/' | '/initiatives/' | '/settings/' fileRoutesByTo: FileRoutesByTo @@ -140,6 +150,7 @@ export interface FileRouteTypes { | '/initiatives/$id' | '/settings/health' | '/settings/projects' + | '/errands' | '/initiatives' | '/settings' id: @@ -153,6 +164,7 @@ export interface FileRouteTypes { | '/initiatives/$id' | '/settings/health' | '/settings/projects' + | '/errands/' | '/initiatives/' | '/settings/' fileRoutesById: FileRoutesById @@ -165,6 +177,7 @@ export interface RootRouteChildren { RadarRoute: typeof RadarRoute SettingsRoute: typeof SettingsRouteWithChildren InitiativesIdRoute: typeof InitiativesIdRoute + ErrandsIndexRoute: typeof ErrandsIndexRoute InitiativesIndexRoute: typeof InitiativesIndexRoute } @@ -226,6 +239,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof InitiativesIndexRouteImport parentRoute: typeof rootRouteImport } + '/errands/': { + id: '/errands/' + path: '/errands' + fullPath: '/errands/' + preLoaderRoute: typeof ErrandsIndexRouteImport + parentRoute: typeof rootRouteImport + } '/settings/projects': { id: '/settings/projects' path: '/projects' @@ -274,6 +294,7 @@ const rootRouteChildren: RootRouteChildren = { RadarRoute: RadarRoute, SettingsRoute: SettingsRouteWithChildren, InitiativesIdRoute: InitiativesIdRoute, + ErrandsIndexRoute: ErrandsIndexRoute, InitiativesIndexRoute: InitiativesIndexRoute, } export const routeTree = rootRouteImport diff --git a/apps/web/src/routes/errands/index.tsx b/apps/web/src/routes/errands/index.tsx index ef83b5e..9a7df0a 100644 --- a/apps/web/src/routes/errands/index.tsx +++ b/apps/web/src/routes/errands/index.tsx @@ -103,7 +103,7 @@ function ErrandsPage() { {e.agentAlias ?? '—'} - {formatRelativeTime(e.createdAt.toISOString())} + {formatRelativeTime(String(e.createdAt))} ))} diff --git a/apps/web/src/routes/hq.test.tsx b/apps/web/src/routes/hq.test.tsx index 066cd0d..c9f3ae2 100644 --- a/apps/web/src/routes/hq.test.tsx +++ b/apps/web/src/routes/hq.test.tsx @@ -4,9 +4,24 @@ import { render, screen, fireEvent } from '@testing-library/react' import { vi, describe, it, expect, beforeEach } from 'vitest' const mockUseQuery = vi.hoisted(() => vi.fn()) +const mockListWaitingAgentsQuery = vi.hoisted(() => vi.fn()) +const mockListMessagesQuery = vi.hoisted(() => vi.fn()) +const mockGetAgentQuestionsQuery = vi.hoisted(() => vi.fn()) +const mockResumeAgentMutation = vi.hoisted(() => vi.fn()) +const mockStopAgentMutation = vi.hoisted(() => vi.fn()) +const mockRespondToMessageMutation = vi.hoisted(() => vi.fn()) +const mockUseUtils = vi.hoisted(() => vi.fn()) + vi.mock('@/lib/trpc', () => ({ trpc: { getHeadquartersDashboard: { useQuery: mockUseQuery }, + listWaitingAgents: { useQuery: mockListWaitingAgentsQuery }, + listMessages: { useQuery: mockListMessagesQuery }, + getAgentQuestions: { useQuery: mockGetAgentQuestionsQuery }, + resumeAgent: { useMutation: mockResumeAgentMutation }, + stopAgent: { useMutation: mockStopAgentMutation }, + respondToMessage: { useMutation: mockRespondToMessageMutation }, + useUtils: mockUseUtils, }, })) @@ -15,8 +30,33 @@ vi.mock('@/hooks', () => ({ LiveUpdateRule: undefined, })) -vi.mock('@/components/hq/HQWaitingForInputSection', () => ({ - HQWaitingForInputSection: ({ items }: any) =>
{items.length}
, +vi.mock('@/components/InboxList', () => ({ + InboxList: ({ agents, selectedAgentId, onSelectAgent }: any) => ( +
+ {agents.map((a: any) => ( + + ))} +
+ ), +})) + +vi.mock('@/components/InboxDetailPanel', () => ({ + InboxDetailPanel: ({ agent, onSubmitAnswers, onDismissQuestions, onDismissMessage, onBack }: any) => ( +
+ {agent.name} + + + + +
+ ), })) vi.mock('@/components/hq/HQNeedsReviewSection', () => ({ @@ -56,6 +96,16 @@ const emptyData = { describe('HeadquartersPage', () => { beforeEach(() => { vi.clearAllMocks() + mockListWaitingAgentsQuery.mockReturnValue({ data: [], isLoading: false }) + mockListMessagesQuery.mockReturnValue({ data: [], isLoading: false }) + mockGetAgentQuestionsQuery.mockReturnValue({ data: null, isLoading: false, isError: false }) + mockResumeAgentMutation.mockReturnValue({ mutate: vi.fn(), isPending: false, isError: false }) + mockStopAgentMutation.mockReturnValue({ mutate: vi.fn(), isPending: false, isError: false }) + mockRespondToMessageMutation.mockReturnValue({ mutate: vi.fn(), isPending: false, isError: false }) + mockUseUtils.mockReturnValue({ + listWaitingAgents: { invalidate: vi.fn() }, + listMessages: { invalidate: vi.fn() }, + }) }) it('renders skeleton loading state', () => { @@ -68,7 +118,7 @@ describe('HeadquartersPage', () => { const skeletons = document.querySelectorAll('[class*="skeleton"], [class*="h-16"]') expect(skeletons.length).toBeGreaterThan(0) // No section components - expect(screen.queryByTestId('waiting')).not.toBeInTheDocument() + expect(screen.queryByTestId('inbox-list')).not.toBeInTheDocument() expect(screen.queryByTestId('needs-review')).not.toBeInTheDocument() expect(screen.queryByTestId('needs-approval')).not.toBeInTheDocument() expect(screen.queryByTestId('blocked')).not.toBeInTheDocument() @@ -93,24 +143,25 @@ describe('HeadquartersPage', () => { render() expect(screen.getByTestId('empty-state')).toBeInTheDocument() - expect(screen.queryByTestId('waiting')).not.toBeInTheDocument() + expect(screen.queryByTestId('inbox-list')).not.toBeInTheDocument() expect(screen.queryByTestId('needs-review')).not.toBeInTheDocument() expect(screen.queryByTestId('needs-approval')).not.toBeInTheDocument() expect(screen.queryByTestId('blocked')).not.toBeInTheDocument() }) - it('renders WaitingForInput section when items exist', () => { + it('renders InboxList when waitingForInput items exist', () => { mockUseQuery.mockReturnValue({ isLoading: false, isError: false, data: { ...emptyData, waitingForInput: [{ id: '1' }] }, }) + mockListWaitingAgentsQuery.mockReturnValue({ + data: [{ id: 'a1', name: 'Agent 1', status: 'waiting_for_input', taskId: null, updatedAt: new Date() }], + isLoading: false, + }) render() - expect(screen.getByTestId('waiting')).toBeInTheDocument() - expect(screen.queryByTestId('needs-review')).not.toBeInTheDocument() - expect(screen.queryByTestId('needs-approval')).not.toBeInTheDocument() - expect(screen.queryByTestId('blocked')).not.toBeInTheDocument() + expect(screen.getByTestId('inbox-list')).toBeInTheDocument() expect(screen.queryByTestId('empty-state')).not.toBeInTheDocument() }) @@ -127,9 +178,13 @@ describe('HeadquartersPage', () => { blockedPhases: [{ id: '6' }], }, }) + mockListWaitingAgentsQuery.mockReturnValue({ + data: [{ id: 'a1', name: 'Agent 1', status: 'waiting_for_input', taskId: null, updatedAt: new Date() }], + isLoading: false, + }) render() - expect(screen.getByTestId('waiting')).toBeInTheDocument() + expect(screen.getByTestId('inbox-list')).toBeInTheDocument() expect(screen.getByTestId('needs-review')).toBeInTheDocument() expect(screen.getByTestId('needs-approval')).toBeInTheDocument() expect(screen.getByTestId('resolving-conflicts')).toBeInTheDocument() @@ -160,4 +215,41 @@ describe('HeadquartersPage', () => { expect(screen.getByTestId('needs-review')).toBeInTheDocument() expect(screen.queryByTestId('empty-state')).not.toBeInTheDocument() }) + + it('shows InboxDetailPanel when an agent is selected from InboxList', async () => { + mockUseQuery.mockReturnValue({ + isLoading: false, + isError: false, + data: { ...emptyData, waitingForInput: [{ id: '1' }] }, + }) + mockListWaitingAgentsQuery.mockReturnValue({ + data: [{ id: 'a1', name: 'Agent 1', status: 'waiting_for_input', taskId: null, updatedAt: new Date() }], + isLoading: false, + }) + render() + + fireEvent.click(screen.getByTestId('agent-a1')) + + expect(await screen.findByTestId('inbox-detail')).toBeInTheDocument() + expect(screen.getByTestId('selected-agent-name')).toHaveTextContent('Agent 1') + }) + + it('clears selection when Back is clicked in InboxDetailPanel', async () => { + mockUseQuery.mockReturnValue({ + isLoading: false, + isError: false, + data: { ...emptyData, waitingForInput: [{ id: '1' }] }, + }) + mockListWaitingAgentsQuery.mockReturnValue({ + data: [{ id: 'a1', name: 'Agent 1', status: 'waiting_for_input', taskId: null, updatedAt: new Date() }], + isLoading: false, + }) + render() + + fireEvent.click(screen.getByTestId('agent-a1')) + expect(screen.getByTestId('inbox-detail')).toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: /back/i })) + expect(screen.queryByTestId('inbox-detail')).not.toBeInTheDocument() + }) }) diff --git a/apps/web/src/routes/hq.tsx b/apps/web/src/routes/hq.tsx index d1f881e..368e6bf 100644 --- a/apps/web/src/routes/hq.tsx +++ b/apps/web/src/routes/hq.tsx @@ -1,10 +1,13 @@ import { createFileRoute } from "@tanstack/react-router"; +import { useState } from "react"; import { motion } from "motion/react"; +import { toast } from "sonner"; import { trpc } from "@/lib/trpc"; import { useLiveUpdates, type LiveUpdateRule } from "@/hooks"; import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; -import { HQWaitingForInputSection } from "@/components/hq/HQWaitingForInputSection"; +import { InboxList } from "@/components/InboxList"; +import { InboxDetailPanel } from "@/components/InboxDetailPanel"; import { HQNeedsReviewSection } from "@/components/hq/HQNeedsReviewSection"; import { HQNeedsApprovalSection } from "@/components/hq/HQNeedsApprovalSection"; import { HQResolvingConflictsSection } from "@/components/hq/HQResolvingConflictsSection"; @@ -19,12 +22,108 @@ const HQ_LIVE_UPDATE_RULES: LiveUpdateRule[] = [ { prefix: "initiative:", invalidate: ["getHeadquartersDashboard"] }, { prefix: "phase:", invalidate: ["getHeadquartersDashboard"] }, { prefix: "agent:", invalidate: ["getHeadquartersDashboard"] }, + { prefix: "agent:", invalidate: ["listWaitingAgents", "listMessages"] }, ]; export function HeadquartersPage() { useLiveUpdates(HQ_LIVE_UPDATE_RULES); const query = trpc.getHeadquartersDashboard.useQuery(); + const [selectedAgentId, setSelectedAgentId] = useState(null); + + const utils = trpc.useUtils(); + const agentsQuery = trpc.listWaitingAgents.useQuery(); + const messagesQuery = trpc.listMessages.useQuery({}); + const questionsQuery = trpc.getAgentQuestions.useQuery( + { id: selectedAgentId! }, + { enabled: !!selectedAgentId } + ); + + const resumeAgentMutation = trpc.resumeAgent.useMutation({ + onSuccess: () => { + setSelectedAgentId(null); + toast.success("Answer submitted"); + }, + onError: () => { + toast.error("Failed to submit answer"); + }, + }); + + const dismissQuestionsMutation = trpc.stopAgent.useMutation({ + onSuccess: () => { + setSelectedAgentId(null); + toast.success("Questions dismissed"); + }, + onError: () => { + toast.error("Failed to dismiss questions"); + }, + }); + + const respondToMessageMutation = trpc.respondToMessage.useMutation({ + onSuccess: () => { + setSelectedAgentId(null); + toast.success("Response sent"); + }, + onError: () => { + toast.error("Failed to send response"); + }, + }); + + const agents = agentsQuery.data ?? []; + const messages = messagesQuery.data ?? []; + const selectedAgent = selectedAgentId + ? agents.find((a) => a.id === selectedAgentId) ?? null + : null; + const selectedMessage = selectedAgentId + ? messages + .filter((m) => m.senderId === selectedAgentId) + .sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + )[0] ?? null + : null; + const pendingQuestions = questionsQuery.data ?? null; + + function handleRefresh() { + void utils.listWaitingAgents.invalidate(); + void utils.listMessages.invalidate(); + } + + function handleSubmitAnswers(answers: Record) { + if (!selectedAgentId) return; + resumeAgentMutation.mutate({ id: selectedAgentId, answers }); + } + + function handleDismissQuestions() { + if (!selectedAgentId) return; + dismissQuestionsMutation.mutate({ id: selectedAgentId }); + } + + function handleDismiss() { + if (!selectedMessage) return; + respondToMessageMutation.mutate({ + id: selectedMessage.id, + response: "Acknowledged", + }); + } + + const serializedAgents = agents.map((a) => ({ + id: a.id, + name: a.name, + status: a.status, + taskId: a.taskId ?? "", + updatedAt: String(a.updatedAt), + })); + + const serializedMessages = messages.map((m) => ({ + id: m.id, + senderId: m.senderId, + content: m.content, + requiresResponse: m.requiresResponse, + status: m.status, + createdAt: String(m.createdAt), + })); + if (query.isLoading) { return ( {data.waitingForInput.length > 0 && ( - +
+

+ Waiting for Input +

+
+ {/* Left: agent list — hidden on mobile when an agent is selected */} +
+ +
+ {/* Right: detail panel */} + {selectedAgent ? ( + ({ + id: q.id, + question: q.question, + options: q.options, + multiSelect: q.multiSelect, + })) + : null + } + isLoadingQuestions={questionsQuery.isLoading} + questionsError={ + questionsQuery.isError ? questionsQuery.error.message : null + } + onBack={() => setSelectedAgentId(null)} + onSubmitAnswers={handleSubmitAnswers} + onDismissQuestions={handleDismissQuestions} + onDismissMessage={handleDismiss} + isSubmitting={resumeAgentMutation.isPending} + isDismissingQuestions={dismissQuestionsMutation.isPending} + isDismissingMessage={respondToMessageMutation.isPending} + submitError={ + resumeAgentMutation.isError + ? resumeAgentMutation.error.message + : null + } + dismissMessageError={ + respondToMessageMutation.isError + ? respondToMessageMutation.error.message + : null + } + /> + ) : ( +
+

No agent selected

+

Select an agent from the list to answer their questions

+
+ )} +
+
)} {(data.pendingReviewInitiatives.length > 0 || data.pendingReviewPhases.length > 0) && ( diff --git a/apps/web/src/routes/inbox.tsx b/apps/web/src/routes/inbox.tsx index 50e6399..56bb42f 100644 --- a/apps/web/src/routes/inbox.tsx +++ b/apps/web/src/routes/inbox.tsx @@ -1,269 +1,7 @@ -import { useState } from "react"; -import { createFileRoute } from "@tanstack/react-router"; -import { motion } from "motion/react"; -import { AlertCircle, MessageSquare } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; -import { Skeleton } from "@/components/Skeleton"; -import { toast } from "sonner"; -import { trpc } from "@/lib/trpc"; -import { InboxList } from "@/components/InboxList"; -import { InboxDetailPanel } from "@/components/InboxDetailPanel"; -import { useLiveUpdates } from "@/hooks"; +import { createFileRoute, redirect } from "@tanstack/react-router"; export const Route = createFileRoute("/inbox")({ - component: InboxPage, + beforeLoad: () => { + throw redirect({ to: "/hq" }); + }, }); - -function InboxPage() { - const [selectedAgentId, setSelectedAgentId] = useState(null); - - // Single SSE stream for live updates - useLiveUpdates([ - { prefix: 'agent:', invalidate: ['listWaitingAgents', 'listMessages'] }, - ]); - - const utils = trpc.useUtils(); - - // Data fetching - const agentsQuery = trpc.listWaitingAgents.useQuery(); - const messagesQuery = trpc.listMessages.useQuery({}); - const questionsQuery = trpc.getAgentQuestions.useQuery( - { id: selectedAgentId! }, - { enabled: !!selectedAgentId } - ); - - // Mutations - const resumeAgentMutation = trpc.resumeAgent.useMutation({ - onSuccess: () => { - setSelectedAgentId(null); - toast.success("Answer submitted"); - }, - onError: () => { - toast.error("Failed to submit answer"); - }, - }); - - const dismissQuestionsMutation = trpc.stopAgent.useMutation({ - onSuccess: () => { - setSelectedAgentId(null); - toast.success("Questions dismissed"); - }, - onError: () => { - toast.error("Failed to dismiss questions"); - }, - }); - - const respondToMessageMutation = trpc.respondToMessage.useMutation({ - onSuccess: () => { - setSelectedAgentId(null); - toast.success("Response sent"); - }, - onError: () => { - toast.error("Failed to send response"); - }, - }); - - // Find selected agent info - const agents = agentsQuery.data ?? []; - const messages = messagesQuery.data ?? []; - const selectedAgent = selectedAgentId - ? agents.find((a) => a.id === selectedAgentId) ?? null - : null; - - // Find the latest message for the selected agent - const selectedMessage = selectedAgentId - ? messages - .filter((m) => m.senderId === selectedAgentId) - .sort( - (a, b) => - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - )[0] ?? null - : null; - - const pendingQuestions = questionsQuery.data ?? null; - - // Handlers - function handleRefresh() { - void utils.listWaitingAgents.invalidate(); - void utils.listMessages.invalidate(); - } - - function handleSubmitAnswers(answers: Record) { - if (!selectedAgentId) return; - resumeAgentMutation.mutate({ id: selectedAgentId, answers }); - } - - function handleDismissQuestions() { - if (!selectedAgentId) return; - dismissQuestionsMutation.mutate({ id: selectedAgentId }); - } - - function handleDismiss() { - if (!selectedMessage) return; - respondToMessageMutation.mutate({ - id: selectedMessage.id, - response: "Acknowledged", - }); - } - - // Loading state - if (agentsQuery.isLoading && messagesQuery.isLoading) { - return ( -
- {/* Skeleton header */} -
-
- - -
- -
- {/* Skeleton message rows */} -
- {Array.from({ length: 4 }).map((_, i) => ( - -
-
-
- - -
- -
- -
-
- ))} -
-
- ); - } - - // Error state - if (agentsQuery.isError || messagesQuery.isError) { - const errorMessage = - agentsQuery.error?.message ?? messagesQuery.error?.message ?? "Unknown error"; - return ( -
- -

- Failed to load inbox: {errorMessage} -

- -
- ); - } - - // Serialize agents for InboxList (convert Date to string for wire format) - const serializedAgents = agents.map((a) => ({ - id: a.id, - name: a.name, - status: a.status, - taskId: a.taskId, - updatedAt: String(a.updatedAt), - })); - - // Serialize messages for InboxList - const serializedMessages = messages.map((m) => ({ - id: m.id, - senderId: m.senderId, - content: m.content, - requiresResponse: m.requiresResponse, - status: m.status, - createdAt: String(m.createdAt), - })); - - return ( - -
- {/* Left: Inbox List -- hidden on mobile when agent selected */} - - - - - {/* Right: Detail Panel */} - {selectedAgent && ( - ({ - id: q.id, - question: q.question, - options: q.options, - multiSelect: q.multiSelect, - })) - : null - } - isLoadingQuestions={questionsQuery.isLoading} - questionsError={ - questionsQuery.isError ? questionsQuery.error.message : null - } - onBack={() => setSelectedAgentId(null)} - onSubmitAnswers={handleSubmitAnswers} - onDismissQuestions={handleDismissQuestions} - onDismissMessage={handleDismiss} - isSubmitting={resumeAgentMutation.isPending} - isDismissingQuestions={dismissQuestionsMutation.isPending} - isDismissingMessage={respondToMessageMutation.isPending} - submitError={ - resumeAgentMutation.isError - ? resumeAgentMutation.error.message - : null - } - dismissMessageError={ - respondToMessageMutation.isError - ? respondToMessageMutation.error.message - : null - } - /> - )} - - {/* Empty detail panel placeholder */} - {!selectedAgent && ( -
- -
-

No message selected

-

Select an agent from the inbox to view details

-
-
- )} -
-
- ); -} diff --git a/apps/web/tsconfig.app.tsbuildinfo b/apps/web/tsconfig.app.tsbuildinfo index de94090..a48ea07 100644 --- a/apps/web/tsconfig.app.tsbuildinfo +++ b/apps/web/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/routetree.gen.ts","./src/router.tsx","./src/vite-env.d.ts","./src/components/accountcard.tsx","./src/components/actionmenu.tsx","./src/components/addaccountdialog.tsx","./src/components/agentactions.tsx","./src/components/agentdetailspanel.tsx","./src/components/agentoutputviewer.test.tsx","./src/components/agentoutputviewer.tsx","./src/components/browsertitleupdater.tsx","./src/components/changesetbanner.tsx","./src/components/commandpalette.tsx","./src/components/connectionbanner.tsx","./src/components/createinitiativedialog.tsx","./src/components/decisionlist.tsx","./src/components/dependencychip.tsx","./src/components/dependencyindicator.tsx","./src/components/emptystate.tsx","./src/components/errorboundary.tsx","./src/components/errorstate.tsx","./src/components/executiontab.tsx","./src/components/freetextinput.tsx","./src/components/healthdot.tsx","./src/components/inboxdetailpanel.tsx","./src/components/inboxlist.tsx","./src/components/initiativecard.tsx","./src/components/initiativeheader.tsx","./src/components/initiativelist.tsx","./src/components/keyboardshortcuthint.tsx","./src/components/messagecard.tsx","./src/components/navbadge.tsx","./src/components/optiongroup.tsx","./src/components/phaseaccordion.tsx","./src/components/progressbar.tsx","./src/components/progresspanel.tsx","./src/components/projectpicker.tsx","./src/components/questionform.tsx","./src/components/refinespawndialog.tsx","./src/components/registerprojectdialog.tsx","./src/components/saveindicator.tsx","./src/components/skeleton.tsx","./src/components/skeletoncard.tsx","./src/components/spawnarchitectdropdown.tsx","./src/components/statusbadge.tsx","./src/components/statusdot.tsx","./src/components/taskrow.tsx","./src/components/themetoggle.tsx","./src/components/updatecredentialsdialog.test.tsx","./src/components/updatecredentialsdialog.tsx","./src/components/chat/changesetinline.tsx","./src/components/chat/chatbubble.tsx","./src/components/chat/chatinput.tsx","./src/components/chat/chatslideover.tsx","./src/components/editor/blockdraghandle.tsx","./src/components/editor/blockselectionextension.ts","./src/components/editor/contenttab.tsx","./src/components/editor/deletesubpagedialog.tsx","./src/components/editor/pagebreadcrumb.tsx","./src/components/editor/pagelinkdeletiondetector.ts","./src/components/editor/pagelinkextension.tsx","./src/components/editor/pagetitlecontext.tsx","./src/components/editor/pagetree.tsx","./src/components/editor/phasecontenteditor.tsx","./src/components/editor/refineagentpanel.tsx","./src/components/editor/slashcommandlist.tsx","./src/components/editor/slashcommands.ts","./src/components/editor/tiptapeditor.tsx","./src/components/editor/slash-command-items.ts","./src/components/execution/executioncontext.tsx","./src/components/execution/phaseactions.tsx","./src/components/execution/phasedetailpanel.tsx","./src/components/execution/phasegraph.tsx","./src/components/execution/phasesidebaritem.tsx","./src/components/execution/phasewithtasks.tsx","./src/components/execution/phaseslist.tsx","./src/components/execution/plansection.tsx","./src/components/execution/progresssidebar.tsx","./src/components/execution/taskgraph.tsx","./src/components/execution/taskslideover.tsx","./src/components/execution/index.ts","./src/components/hq/hqblockedsection.tsx","./src/components/hq/hqemptystate.tsx","./src/components/hq/hqneedsapprovalsection.tsx","./src/components/hq/hqneedsreviewsection.tsx","./src/components/hq/hqsections.test.tsx","./src/components/hq/hqwaitingforinputsection.tsx","./src/components/hq/types.ts","./src/components/pipeline/pipelinegraph.tsx","./src/components/pipeline/pipelinephasegroup.tsx","./src/components/pipeline/pipelinestagecolumn.tsx","./src/components/pipeline/pipelinetab.tsx","./src/components/pipeline/index.ts","./src/components/review/commentform.tsx","./src/components/review/commentthread.tsx","./src/components/review/conflictresolutionpanel.tsx","./src/components/review/diffviewer.tsx","./src/components/review/filecard.tsx","./src/components/review/hunkrows.tsx","./src/components/review/initiativereview.tsx","./src/components/review/linewithcomments.tsx","./src/components/review/previewcontrols.tsx","./src/components/review/reviewheader.tsx","./src/components/review/reviewsidebar.test.tsx","./src/components/review/reviewsidebar.tsx","./src/components/review/reviewtab.tsx","./src/components/review/dummy-data.ts","./src/components/review/index.ts","./src/components/review/parse-diff.ts","./src/components/review/types.ts","./src/components/review/use-syntax-highlight.ts","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/select.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/textarea.tsx","./src/components/ui/tooltip.tsx","./src/hooks/index.ts","./src/hooks/useautosave.ts","./src/hooks/usechatsession.ts","./src/hooks/useconflictagent.ts","./src/hooks/useconnectionstatus.ts","./src/hooks/usedebounce.ts","./src/hooks/useglobalkeyboard.ts","./src/hooks/useliveupdates.ts","./src/hooks/useoptimisticmutation.ts","./src/hooks/usephaseautosave.ts","./src/hooks/userefineagent.ts","./src/hooks/usespawnmutation.ts","./src/hooks/usesubscriptionwitherrorhandling.ts","./src/layouts/applayout.tsx","./src/lib/category.ts","./src/lib/invalidation.ts","./src/lib/labels.ts","./src/lib/markdown-to-tiptap.ts","./src/lib/parse-agent-output.test.ts","./src/lib/parse-agent-output.ts","./src/lib/theme.tsx","./src/lib/trpc.ts","./src/lib/utils.ts","./src/routes/__root.tsx","./src/routes/agents.tsx","./src/routes/hq.test.tsx","./src/routes/hq.tsx","./src/routes/inbox.tsx","./src/routes/index.tsx","./src/routes/settings.tsx","./src/routes/initiatives/$id.tsx","./src/routes/initiatives/index.tsx","./src/routes/settings/health.tsx","./src/routes/settings/index.tsx","./src/routes/settings/projects.tsx"],"errors":true,"version":"5.9.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/routetree.gen.ts","./src/router.tsx","./src/vite-env.d.ts","./src/components/accountcard.tsx","./src/components/actionmenu.tsx","./src/components/addaccountdialog.tsx","./src/components/agentactions.tsx","./src/components/agentdetailspanel.tsx","./src/components/agentoutputviewer.test.tsx","./src/components/agentoutputviewer.tsx","./src/components/browsertitleupdater.tsx","./src/components/changesetbanner.tsx","./src/components/commandpalette.tsx","./src/components/connectionbanner.tsx","./src/components/createerranddialog.tsx","./src/components/createinitiativedialog.tsx","./src/components/decisionlist.tsx","./src/components/dependencychip.tsx","./src/components/dependencyindicator.tsx","./src/components/emptystate.tsx","./src/components/erranddetailpanel.tsx","./src/components/errorboundary.tsx","./src/components/errorstate.tsx","./src/components/executiontab.tsx","./src/components/freetextinput.tsx","./src/components/healthdot.tsx","./src/components/inboxdetailpanel.tsx","./src/components/inboxlist.tsx","./src/components/initiativecard.tsx","./src/components/initiativeheader.tsx","./src/components/initiativelist.tsx","./src/components/keyboardshortcuthint.tsx","./src/components/messagecard.tsx","./src/components/navbadge.tsx","./src/components/optiongroup.tsx","./src/components/phaseaccordion.tsx","./src/components/progressbar.tsx","./src/components/progresspanel.tsx","./src/components/projectpicker.tsx","./src/components/questionform.tsx","./src/components/refinespawndialog.tsx","./src/components/registerprojectdialog.tsx","./src/components/saveindicator.tsx","./src/components/skeleton.tsx","./src/components/skeletoncard.tsx","./src/components/spawnarchitectdropdown.tsx","./src/components/statusbadge.tsx","./src/components/statusdot.tsx","./src/components/taskrow.tsx","./src/components/themetoggle.tsx","./src/components/updatecredentialsdialog.test.tsx","./src/components/updatecredentialsdialog.tsx","./src/components/chat/changesetinline.tsx","./src/components/chat/chatbubble.tsx","./src/components/chat/chatinput.tsx","./src/components/chat/chatslideover.tsx","./src/components/editor/blockdraghandle.tsx","./src/components/editor/blockselectionextension.ts","./src/components/editor/contenttab.tsx","./src/components/editor/deletesubpagedialog.tsx","./src/components/editor/pagebreadcrumb.tsx","./src/components/editor/pagelinkdeletiondetector.ts","./src/components/editor/pagelinkextension.tsx","./src/components/editor/pagetitlecontext.tsx","./src/components/editor/pagetree.tsx","./src/components/editor/phasecontenteditor.tsx","./src/components/editor/refineagentpanel.tsx","./src/components/editor/slashcommandlist.tsx","./src/components/editor/slashcommands.ts","./src/components/editor/tiptapeditor.tsx","./src/components/editor/slash-command-items.ts","./src/components/execution/executioncontext.tsx","./src/components/execution/phaseactions.tsx","./src/components/execution/phasedetailpanel.tsx","./src/components/execution/phasegraph.tsx","./src/components/execution/phasesidebaritem.tsx","./src/components/execution/phasewithtasks.tsx","./src/components/execution/phaseslist.tsx","./src/components/execution/plansection.tsx","./src/components/execution/progresssidebar.tsx","./src/components/execution/taskgraph.tsx","./src/components/execution/taskslideover.tsx","./src/components/execution/index.ts","./src/components/hq/hqblockedsection.tsx","./src/components/hq/hqemptystate.tsx","./src/components/hq/hqneedsapprovalsection.tsx","./src/components/hq/hqneedsreviewsection.tsx","./src/components/hq/hqresolvingconflictssection.tsx","./src/components/hq/hqsections.test.tsx","./src/components/hq/hqwaitingforinputsection.tsx","./src/components/hq/types.ts","./src/components/pipeline/pipelinegraph.tsx","./src/components/pipeline/pipelinephasegroup.tsx","./src/components/pipeline/pipelinestagecolumn.tsx","./src/components/pipeline/pipelinetab.tsx","./src/components/pipeline/index.ts","./src/components/radar/compactioneventsdialog.tsx","./src/components/radar/interagentmessagesdialog.tsx","./src/components/radar/questionsaskeddialog.tsx","./src/components/radar/subagentspawnsdialog.tsx","./src/components/radar/types.ts","./src/components/radar/__tests__/compactioneventsdialog.test.tsx","./src/components/radar/__tests__/interagentmessagesdialog.test.tsx","./src/components/radar/__tests__/questionsaskeddialog.test.tsx","./src/components/radar/__tests__/subagentspawnsdialog.test.tsx","./src/components/review/commentform.tsx","./src/components/review/commentthread.tsx","./src/components/review/conflictresolutionpanel.tsx","./src/components/review/diffviewer.test.tsx","./src/components/review/diffviewer.tsx","./src/components/review/filecard.test.tsx","./src/components/review/filecard.tsx","./src/components/review/hunkrows.tsx","./src/components/review/initiativereview.tsx","./src/components/review/linewithcomments.tsx","./src/components/review/previewcontrols.tsx","./src/components/review/reviewheader.tsx","./src/components/review/reviewsidebar.test.tsx","./src/components/review/reviewsidebar.tsx","./src/components/review/reviewtab.test.tsx","./src/components/review/reviewtab.tsx","./src/components/review/comment-index.test.tsx","./src/components/review/comment-index.ts","./src/components/review/dummy-data.ts","./src/components/review/highlight-worker.ts","./src/components/review/index.ts","./src/components/review/parse-diff.ts","./src/components/review/types.test.ts","./src/components/review/types.ts","./src/components/review/use-syntax-highlight.fallback.test.ts","./src/components/review/use-syntax-highlight.test.ts","./src/components/review/use-syntax-highlight.ts","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/select.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/textarea.tsx","./src/components/ui/tooltip.tsx","./src/hooks/index.ts","./src/hooks/useautosave.ts","./src/hooks/usechatsession.ts","./src/hooks/useconflictagent.ts","./src/hooks/useconnectionstatus.ts","./src/hooks/usedebounce.ts","./src/hooks/useglobalkeyboard.ts","./src/hooks/useliveupdates.ts","./src/hooks/useoptimisticmutation.ts","./src/hooks/usephaseautosave.ts","./src/hooks/userefineagent.ts","./src/hooks/usespawnmutation.ts","./src/hooks/usesubscriptionwitherrorhandling.ts","./src/layouts/applayout.tsx","./src/lib/category.ts","./src/lib/invalidation.ts","./src/lib/labels.ts","./src/lib/markdown-to-tiptap.ts","./src/lib/parse-agent-output.test.ts","./src/lib/parse-agent-output.ts","./src/lib/theme.tsx","./src/lib/trpc.ts","./src/lib/utils.ts","./src/routes/__root.tsx","./src/routes/agents.tsx","./src/routes/hq.test.tsx","./src/routes/hq.tsx","./src/routes/inbox.tsx","./src/routes/index.tsx","./src/routes/radar.tsx","./src/routes/settings.tsx","./src/routes/errands/index.tsx","./src/routes/initiatives/$id.tsx","./src/routes/initiatives/index.tsx","./src/routes/settings/health.tsx","./src/routes/settings/index.tsx","./src/routes/settings/projects.tsx"],"errors":true,"version":"5.9.3"} \ No newline at end of file diff --git a/docs/database-migrations.md b/docs/database-migrations.md index 603d49e..3e6d3ae 100644 --- a/docs/database-migrations.md +++ b/docs/database-migrations.md @@ -55,3 +55,27 @@ Migrations 0000–0007 were generated by `drizzle-kit generate`. Migrations 0008 - **Migration files are immutable.** Once committed, never edit them. Make a new migration instead. - **Keep schema.ts in sync.** The schema file is the source of truth for TypeScript types; migrations are the source of truth for database DDL. Both must reflect the same structure. - **Test with `npm test`** after generating migrations to verify they work with in-memory databases. + +## Post-migration backfill scripts + +Some schema additions require a one-time data backfill because SQLite migrations cannot execute Node.js logic (e.g., JSON parsing). In these cases, the migration creates the table structure, and a separate Node.js script populates it from existing data. + +### agent_metrics backfill + +**When to run:** After deploying the migration that creates the `agent_metrics` table (introduced in the Radar Screen Performance initiative). Run this once per production database after upgrading. + +**Command:** +```sh +cw backfill-metrics +# Or with a custom DB path: +cw backfill-metrics --db /path/to/codewalkers.db +``` + +**What it does:** +- Reads all existing `agent_log_chunks` rows in batches of 500 (ordered by `createdAt ASC`) +- Parses each chunk's `content` JSON to count `AskUserQuestion` tool calls, `Agent` spawns, and compaction events +- Upserts the accumulated counts into `agent_metrics` using additive conflict resolution + +**Idempotency:** The script uses `ON CONFLICT DO UPDATE` with additive increments, matching the ongoing write-path behavior. Running it against an empty `agent_metrics` table is fully safe. Running it a second time will double-count — only run it once per database, immediately after applying the migration. + +**Batch size:** 500 rows per query, to avoid loading the full `agent_log_chunks` table into memory. Progress is logged every 1,000 chunks. diff --git a/docs/dispatch-events.md b/docs/dispatch-events.md index fea1ddb..f17e142 100644 --- a/docs/dispatch-events.md +++ b/docs/dispatch-events.md @@ -93,6 +93,18 @@ InitiativeChangesRequestedEvent { initiativeId, phaseId, taskId } 4. **Auto-queue tasks** — When phase starts (branches confirmed), pending execution tasks are queued (planning-category tasks excluded) 5. **Events** — `phase:queued`, `phase:started`, `phase:completed`, `phase:blocked` +### Auto-Integration Phase + +When `queueAllPhases` is called (i.e. the user clicks "Execute"), it auto-creates an **Integration** phase if the initiative has multiple end phases (leaf nodes with no dependents). This catches cross-phase incompatibilities before the initiative reaches review. + +- **Trigger**: `queueAllPhases` in `apps/server/trpc/routers/phase-dispatch.ts` +- **Guard**: Only created when `endPhaseIds.length > 1` and no existing "Integration" phase +- **Status**: Created as `approved` (same pattern as Finalization in orchestrator.ts) +- **Dependencies**: Integration depends on all end phases — dispatched last +- **Task**: A single `verify` category task instructs the agent to build, run tests, check types, and review cross-phase imports +- **Idempotency**: Name-based check prevents duplicates on re-execution +- **Coexistence**: Independent of the Finalization phase (different purpose, different trigger) + ### PhaseDispatchManager Methods | Method | Purpose | @@ -137,8 +149,11 @@ When an agent crashes (`agent:crashed` event), the orchestrator automatically re On server restart, `recoverDispatchQueues()` also recovers: - Stuck `in_progress` tasks whose agents are dead (status is not `running` or `waiting_for_input`) — reset to `pending` and re-queued - Erroneously `blocked` tasks whose agents completed successfully (status is `idle` or `stopped`) — marked `completed` so the phase can progress. This handles the legacy case where conflict resolution incorrectly blocked already-completed tasks. +- Stale duplicate planning tasks — if a phase has both a completed and a pending task of the same planning category (e.g. two `detail` tasks from a crash-and-retry), the pending one is marked `completed` with summary "Superseded by retry" - Fully-completed `in_progress` phases — after task recovery, if all tasks in an `in_progress` phase are completed, triggers `handlePhaseAllTasksDone` to complete/review the phase +The `detailPhase` mutation in `architect.ts` also cleans up orphaned pending/in_progress detail tasks before creating new ones, preventing duplicates at the source. + Manual retry via `retryBlockedTask()` resets `retryCount` to 0, giving the task a fresh set of automatic retries. ### Coalesced Scheduling