Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions packages/codev/src/agent-farm/__tests__/overview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
// ============================================================================
Expand Down Expand Up @@ -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');
});
});
});
27 changes: 27 additions & 0 deletions packages/codev/src/agent-farm/servers/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading