diff --git a/packages/codev/src/agent-farm/__tests__/overview.test.ts b/packages/codev/src/agent-farm/__tests__/overview.test.ts index e012c972..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, @@ -100,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 // ============================================================================ @@ -1724,5 +1738,56 @@ 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'); + + // 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); + + 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'); + + createStateDb(tmpDir, [{ 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'); + + // No state.db → regex-parsed issueId preserved + 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..87c1a958 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 Database from 'better-sqlite3'; // ============================================================================= // Types @@ -692,6 +693,32 @@ export class OverviewCache { return roleId !== null && activeBuilderRoleIds.has(roleId); }); } + + // 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 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 — keep regex-parsed issueId + } + const activeBuilderIssues = new Set( builders .map(b => b.issueId)