From 79b362f5454f346a60e73aeec0a670d967f11327 Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Sun, 12 Apr 2026 18:18:43 -0700 Subject: [PATCH 1/2] [Bugfix #664] Fix: Read issue number from DB instead of parsing builder name --- .../src/agent-farm/__tests__/overview.test.ts | 65 ++++++++++++++++++- .../codev/src/agent-farm/servers/overview.ts | 18 +++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/codev/src/agent-farm/__tests__/overview.test.ts b/packages/codev/src/agent-farm/__tests__/overview.test.ts index e012c972..0ea30937 100644 --- a/packages/codev/src/agent-farm/__tests__/overview.test.ts +++ b/packages/codev/src/agent-farm/__tests__/overview.test.ts @@ -27,12 +27,13 @@ import { // Mocks // ============================================================================ -const { mockFetchPRList, mockFetchIssueList, mockFetchRecentlyClosed, mockFetchMergedPRs, mockLoadProtocol } = vi.hoisted(() => ({ +const { mockFetchPRList, mockFetchIssueList, mockFetchRecentlyClosed, mockFetchMergedPRs, mockLoadProtocol, mockDbPrepare } = vi.hoisted(() => ({ mockFetchPRList: vi.fn(), mockFetchIssueList: vi.fn(), mockFetchRecentlyClosed: vi.fn(), mockFetchMergedPRs: vi.fn(), mockLoadProtocol: vi.fn(), + mockDbPrepare: vi.fn(), })); vi.mock('../../lib/github.js', async (importOriginal) => { @@ -50,6 +51,10 @@ vi.mock('../../commands/porch/protocol.js', () => ({ loadProtocol: mockLoadProtocol, })); +vi.mock('../db/index.js', () => ({ + getDb: () => ({ prepare: mockDbPrepare }), +})); + // ============================================================================ // Temp directory helper // ============================================================================ @@ -112,6 +117,7 @@ describe('overview', () => { mockFetchIssueList.mockResolvedValue([]); mockFetchRecentlyClosed.mockResolvedValue([]); mockFetchMergedPRs.mockResolvedValue([]); + mockDbPrepare.mockReturnValue({ all: () => [] }); }); afterEach(() => { @@ -1724,5 +1730,62 @@ describe('overview', () => { expect(data.recentlyClosed).toHaveLength(1); expect(data.recentlyClosed[0].prUrl).toBeUndefined(); }); + + it('enriches issueId from DB issue_number for unknown protocols (#664)', async () => { + // research-533 doesn't match any protocol regex → soft mode, issueId null + const worktreePath = createBuilderWorktree(tmpDir, 'research-533-context-window'); + + // Mock DB to return issue_number for this worktree + mockDbPrepare.mockReturnValue({ + all: () => [{ worktree: worktreePath, issue_number: 533 }], + }); + + const cache = new OverviewCache(); + const data = await cache.getOverview(tmpDir); + + expect(data.builders).toHaveLength(1); + expect(data.builders[0].issueId).toBe('533'); + }); + + it('DB issue_number overrides regex-parsed issueId (#664)', async () => { + // spir-42 matches the regex → issueId '42' from regex + // DB also has issue_number 42 → should still be '42' + const worktreePath = createBuilderWorktree(tmpDir, 'spir-42-feature', [ + "id: '0042'", + 'title: feature', + 'protocol: spir', + 'phase: implement', + 'gates:', + ].join('\n'), '0042-feature'); + + mockDbPrepare.mockReturnValue({ + all: () => [{ worktree: worktreePath, issue_number: 42 }], + }); + + const cache = new OverviewCache(); + const data = await cache.getOverview(tmpDir); + + expect(data.builders).toHaveLength(1); + expect(data.builders[0].issueId).toBe('42'); + }); + + it('falls back to regex-parsed issueId when DB has no issue_number (#664)', async () => { + createBuilderWorktree(tmpDir, 'spir-42-feature', [ + "id: '0042'", + 'title: feature', + 'protocol: spir', + 'phase: implement', + 'gates:', + ].join('\n'), '0042-feature'); + + // DB returns no rows → regex-parsed issueId preserved + mockDbPrepare.mockReturnValue({ all: () => [] }); + + const cache = new OverviewCache(); + const data = await cache.getOverview(tmpDir); + + expect(data.builders).toHaveLength(1); + expect(data.builders[0].issueId).toBe('42'); + }); }); }); diff --git a/packages/codev/src/agent-farm/servers/overview.ts b/packages/codev/src/agent-farm/servers/overview.ts index 5f86ffba..cfec8fab 100644 --- a/packages/codev/src/agent-farm/servers/overview.ts +++ b/packages/codev/src/agent-farm/servers/overview.ts @@ -19,6 +19,7 @@ import { } from '../../lib/github.js'; import type { ForgePR, ForgeIssueListItem } from '../../lib/github.js'; import { loadProtocol } from '../../commands/porch/protocol.js'; +import { getDb } from '../db/index.js'; // ============================================================================= // Types @@ -692,6 +693,23 @@ export class OverviewCache { return roleId !== null && activeBuilderRoleIds.has(roleId); }); } + + // Enrich issueId from DB issue_number — protocol-agnostic (fixes #664) + try { + const db = getDb(); + const rows = db.prepare( + 'SELECT worktree, issue_number FROM builders WHERE issue_number IS NOT NULL', + ).all() as Array<{ worktree: string; issue_number: number }>; + for (const row of rows) { + const builder = builders.find(b => b.worktreePath === row.worktree); + if (builder) { + builder.issueId = String(row.issue_number); + } + } + } catch { + // DB not available (e.g., no Tower running) — keep regex-parsed issueId + } + const activeBuilderIssues = new Set( builders .map(b => b.issueId) From 068bb59c3b854585bf0042d7050e8335dc54449c Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Sun, 12 Apr 2026 18:27:47 -0700 Subject: [PATCH 2/2] [Bugfix #664] Fix: Use workspace-scoped DB instead of singleton (CMAP feedback) --- .../src/agent-farm/__tests__/overview.test.ts | 36 ++++++++++--------- .../codev/src/agent-farm/servers/overview.ts | 29 +++++++++------ 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/packages/codev/src/agent-farm/__tests__/overview.test.ts b/packages/codev/src/agent-farm/__tests__/overview.test.ts index 0ea30937..e94200a9 100644 --- a/packages/codev/src/agent-farm/__tests__/overview.test.ts +++ b/packages/codev/src/agent-farm/__tests__/overview.test.ts @@ -9,6 +9,7 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; +import Database from 'better-sqlite3'; import { OverviewCache, parseStatusYaml, @@ -27,13 +28,12 @@ import { // Mocks // ============================================================================ -const { mockFetchPRList, mockFetchIssueList, mockFetchRecentlyClosed, mockFetchMergedPRs, mockLoadProtocol, mockDbPrepare } = vi.hoisted(() => ({ +const { mockFetchPRList, mockFetchIssueList, mockFetchRecentlyClosed, mockFetchMergedPRs, mockLoadProtocol } = vi.hoisted(() => ({ mockFetchPRList: vi.fn(), mockFetchIssueList: vi.fn(), mockFetchRecentlyClosed: vi.fn(), mockFetchMergedPRs: vi.fn(), mockLoadProtocol: vi.fn(), - mockDbPrepare: vi.fn(), })); vi.mock('../../lib/github.js', async (importOriginal) => { @@ -51,10 +51,6 @@ vi.mock('../../commands/porch/protocol.js', () => ({ loadProtocol: mockLoadProtocol, })); -vi.mock('../db/index.js', () => ({ - getDb: () => ({ prepare: mockDbPrepare }), -})); - // ============================================================================ // Temp directory helper // ============================================================================ @@ -105,6 +101,19 @@ function issueItem(number: number, title: string, labels: Array<{ name: string } return { number, title, url: `https://github.com/org/repo/issues/${number}`, labels, createdAt: '2026-01-01T00:00:00Z' }; } +/** Create a state.db in the workspace's .agent-farm/ with builder issue_number rows. */ +function createStateDb(root: string, rows: Array<{ worktree: string; issue_number: number }>): void { + const agentFarmDir = path.join(root, '.agent-farm'); + fs.mkdirSync(agentFarmDir, { recursive: true }); + const db = new Database(path.join(agentFarmDir, 'state.db')); + db.exec('CREATE TABLE IF NOT EXISTS builders (worktree TEXT, issue_number INTEGER)'); + const insert = db.prepare('INSERT INTO builders (worktree, issue_number) VALUES (?, ?)'); + for (const row of rows) { + insert.run(row.worktree, row.issue_number); + } + db.close(); +} + // ============================================================================ // Tests // ============================================================================ @@ -117,7 +126,6 @@ describe('overview', () => { mockFetchIssueList.mockResolvedValue([]); mockFetchRecentlyClosed.mockResolvedValue([]); mockFetchMergedPRs.mockResolvedValue([]); - mockDbPrepare.mockReturnValue({ all: () => [] }); }); afterEach(() => { @@ -1735,10 +1743,8 @@ describe('overview', () => { // research-533 doesn't match any protocol regex → soft mode, issueId null const worktreePath = createBuilderWorktree(tmpDir, 'research-533-context-window'); - // Mock DB to return issue_number for this worktree - mockDbPrepare.mockReturnValue({ - all: () => [{ worktree: worktreePath, issue_number: 533 }], - }); + // Create a real DB with issue_number for this worktree + createStateDb(tmpDir, [{ worktree: worktreePath, issue_number: 533 }]); const cache = new OverviewCache(); const data = await cache.getOverview(tmpDir); @@ -1758,9 +1764,7 @@ describe('overview', () => { 'gates:', ].join('\n'), '0042-feature'); - mockDbPrepare.mockReturnValue({ - all: () => [{ worktree: worktreePath, issue_number: 42 }], - }); + createStateDb(tmpDir, [{ worktree: worktreePath, issue_number: 42 }]); const cache = new OverviewCache(); const data = await cache.getOverview(tmpDir); @@ -1778,9 +1782,7 @@ describe('overview', () => { 'gates:', ].join('\n'), '0042-feature'); - // DB returns no rows → regex-parsed issueId preserved - mockDbPrepare.mockReturnValue({ all: () => [] }); - + // No state.db → regex-parsed issueId preserved const cache = new OverviewCache(); const data = await cache.getOverview(tmpDir); diff --git a/packages/codev/src/agent-farm/servers/overview.ts b/packages/codev/src/agent-farm/servers/overview.ts index cfec8fab..87c1a958 100644 --- a/packages/codev/src/agent-farm/servers/overview.ts +++ b/packages/codev/src/agent-farm/servers/overview.ts @@ -19,7 +19,7 @@ import { } from '../../lib/github.js'; import type { ForgePR, ForgeIssueListItem } from '../../lib/github.js'; import { loadProtocol } from '../../commands/porch/protocol.js'; -import { getDb } from '../db/index.js'; +import Database from 'better-sqlite3'; // ============================================================================= // Types @@ -695,19 +695,28 @@ export class OverviewCache { } // Enrich issueId from DB issue_number — protocol-agnostic (fixes #664) + // Open DB directly using workspaceRoot to avoid singleton path issues + // when Tower serves multiple workspaces. try { - const db = getDb(); - const rows = db.prepare( - 'SELECT worktree, issue_number FROM builders WHERE issue_number IS NOT NULL', - ).all() as Array<{ worktree: string; issue_number: number }>; - for (const row of rows) { - const builder = builders.find(b => b.worktreePath === row.worktree); - if (builder) { - builder.issueId = String(row.issue_number); + const dbPath = path.join(workspaceRoot, '.agent-farm', 'state.db'); + if (fs.existsSync(dbPath)) { + const db = new Database(dbPath, { readonly: true }); + try { + const rows = db.prepare( + 'SELECT worktree, issue_number FROM builders WHERE issue_number IS NOT NULL', + ).all() as Array<{ worktree: string; issue_number: number }>; + for (const row of rows) { + const builder = builders.find(b => b.worktreePath === row.worktree); + if (builder) { + builder.issueId = String(row.issue_number); + } + } + } finally { + db.close(); } } } catch { - // DB not available (e.g., no Tower running) — keep regex-parsed issueId + // DB not available — keep regex-parsed issueId } const activeBuilderIssues = new Set(