checkpoint: pre-split branch cleanup

This commit is contained in:
ZenchantLive 2026-03-03 16:43:42 -08:00
parent 4c2ae2e5b7
commit b5db7a7753
276 changed files with 35912 additions and 60119 deletions

View file

@ -1,41 +1,41 @@
import { NextResponse } from 'next/server';
import path from 'node:path';
import { activityEventBus } from '../../../lib/realtime';
export const dynamic = 'force-dynamic';
function isValidProjectRoot(root: string): boolean {
try {
const resolved = path.resolve(root);
if (!path.isAbsolute(resolved)) {
return false;
}
// Prevent path traversal by ensuring resolved path stays within the project root
const allowedBase = process.cwd();
const relative = path.relative(allowedBase, resolved);
// If "resolved" is outside "allowedBase", "relative" will start with ".."
if (relative.startsWith('..') || path.isAbsolute(relative)) {
return false;
}
return true;
} catch {
return false;
}
}
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const projectRootParam = url.searchParams.get('projectRoot');
if (projectRootParam && !isValidProjectRoot(projectRootParam)) {
return NextResponse.json(
{ error: 'Invalid projectRoot path' },
{ status: 400 }
);
}
const projectRoot = projectRootParam || undefined;
const history = activityEventBus.getHistory(projectRoot);
return Response.json(history);
}
import { NextResponse } from 'next/server';
import path from 'node:path';
import { activityEventBus } from '../../../lib/realtime';
export const dynamic = 'force-dynamic';
function isValidProjectRoot(root: string): boolean {
try {
const resolved = path.resolve(root);
if (!path.isAbsolute(resolved)) {
return false;
}
// Prevent path traversal by ensuring resolved path stays within the project root
const allowedBase = process.cwd();
const relative = path.relative(allowedBase, resolved);
// If "resolved" is outside "allowedBase", "relative" will start with ".."
if (relative.startsWith('..') || path.isAbsolute(relative)) {
return false;
}
return true;
} catch {
return false;
}
}
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const projectRootParam = url.searchParams.get('projectRoot');
if (projectRootParam && !isValidProjectRoot(projectRootParam)) {
return NextResponse.json(
{ error: 'Invalid projectRoot path' },
{ status: 400 }
);
}
const projectRoot = projectRootParam || undefined;
const history = activityEventBus.getHistory(projectRoot);
return Response.json(history);
}

View file

@ -1,52 +1,52 @@
import { NextResponse } from 'next/server';
import path from 'node:path';
import { readIssuesFromDisk } from '../../../../../lib/read-issues';
import { activityEventBus } from '../../../../../lib/realtime';
import { getAgentMetrics } from '../../../../../lib/agent-sessions';
export const dynamic = 'force-dynamic';
function isValidProjectRoot(root: string): boolean {
try {
const resolved = path.resolve(root);
if (!path.isAbsolute(resolved)) {
return false;
}
// Prevent path traversal by ensuring resolved path stays within the project root
const allowedBase = process.cwd();
const relative = path.relative(allowedBase, resolved);
// If "resolved" is outside "allowedBase", "relative" will start with ".."
if (relative.startsWith('..') || path.isAbsolute(relative)) {
return false;
}
return true;
} catch {
return false;
}
}
export async function GET(
request: Request,
{ params }: { params: Promise<{ agentId: string }> }
): Promise<Response> {
const { agentId } = await params;
const url = new URL(request.url);
const projectRootParam = url.searchParams.get('projectRoot');
const projectRoot = projectRootParam ?? process.cwd();
if (projectRootParam && !isValidProjectRoot(projectRootParam)) {
return NextResponse.json({ ok: false, error: 'Invalid projectRoot path' }, { status: 400 });
}
try {
const issues = await readIssuesFromDisk({ projectRoot, preferBd: true });
const activity = activityEventBus.getHistory(projectRoot);
const metrics = await getAgentMetrics(agentId, issues, activity);
return NextResponse.json({ ok: true, metrics });
} catch (error) {
console.error('[API/Agents/Stats] Failed:', error);
return NextResponse.json({ ok: false, error: String(error) }, { status: 500 });
}
}
import { NextResponse } from 'next/server';
import path from 'node:path';
import { readIssuesFromDisk } from '../../../../../lib/read-issues';
import { activityEventBus } from '../../../../../lib/realtime';
import { getAgentMetrics } from '../../../../../lib/agent-sessions';
export const dynamic = 'force-dynamic';
function isValidProjectRoot(root: string): boolean {
try {
const resolved = path.resolve(root);
if (!path.isAbsolute(resolved)) {
return false;
}
// Prevent path traversal by ensuring resolved path stays within the project root
const allowedBase = process.cwd();
const relative = path.relative(allowedBase, resolved);
// If "resolved" is outside "allowedBase", "relative" will start with ".."
if (relative.startsWith('..') || path.isAbsolute(relative)) {
return false;
}
return true;
} catch {
return false;
}
}
export async function GET(
request: Request,
{ params }: { params: Promise<{ agentId: string }> }
): Promise<Response> {
const { agentId } = await params;
const url = new URL(request.url);
const projectRootParam = url.searchParams.get('projectRoot');
const projectRoot = projectRootParam ?? process.cwd();
if (projectRootParam && !isValidProjectRoot(projectRootParam)) {
return NextResponse.json({ ok: false, error: 'Invalid projectRoot path' }, { status: 400 });
}
try {
const issues = await readIssuesFromDisk({ projectRoot, preferBd: true });
const activity = activityEventBus.getHistory(projectRoot);
const metrics = await getAgentMetrics(agentId, issues, activity);
return NextResponse.json({ ok: true, metrics });
} catch (error) {
console.error('[API/Agents/Stats] Failed:', error);
return NextResponse.json({ ok: false, error: String(error) }, { status: 500 });
}
}

View file

@ -2,4 +2,4 @@ import { handleMutationRequest } from '../_shared';
export async function POST(request: Request): Promise<Response> {
return handleMutationRequest(request, 'close');
}
}

View file

@ -2,4 +2,4 @@ import { handleMutationRequest } from '../_shared';
export async function POST(request: Request): Promise<Response> {
return handleMutationRequest(request, 'comment');
}
}

View file

@ -2,4 +2,4 @@ import { handleMutationRequest } from '../_shared';
export async function POST(request: Request): Promise<Response> {
return handleMutationRequest(request, 'create');
}
}

View file

@ -1,54 +1,54 @@
import { NextResponse } from 'next/server';
import path from 'node:path';
import { readIssuesFromDisk } from '../../../../lib/read-issues';
export const dynamic = 'force-dynamic';
function isValidProjectRoot(root: string): boolean {
try {
const resolved = path.resolve(root);
if (!path.isAbsolute(resolved)) {
return false;
}
// Prevent path traversal by ensuring resolved path stays within the project root
const allowedBase = process.cwd();
const relative = path.relative(allowedBase, resolved);
// If "resolved" is outside "allowedBase", "relative" will start with ".."
if (relative.startsWith('..') || path.isAbsolute(relative)) {
return false;
}
return true;
} catch {
return false;
}
}
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const projectRootParam = url.searchParams.get('projectRoot');
const projectRoot = projectRootParam ?? process.cwd();
if (projectRootParam && !isValidProjectRoot(projectRootParam)) {
return NextResponse.json(
{ ok: false, error: { classification: 'validation', message: 'Invalid projectRoot path' } },
{ status: 400 }
);
}
try {
const issues = await readIssuesFromDisk({ projectRoot, preferBd: true });
return NextResponse.json({ ok: true, issues });
} catch (error) {
console.error('[API/BeadsRead] Failed to read issues:', error);
return NextResponse.json(
{
ok: false,
error: {
classification: 'internal_error',
message: 'An internal error occurred while reading issues.',
},
},
{ status: 500 },
);
}
}
import { NextResponse } from 'next/server';
import path from 'node:path';
import { readIssuesFromDisk } from '../../../../lib/read-issues';
export const dynamic = 'force-dynamic';
function isValidProjectRoot(root: string): boolean {
try {
const resolved = path.resolve(root);
if (!path.isAbsolute(resolved)) {
return false;
}
// Prevent path traversal by ensuring resolved path stays within the project root
const allowedBase = process.cwd();
const relative = path.relative(allowedBase, resolved);
// If "resolved" is outside "allowedBase", "relative" will start with ".."
if (relative.startsWith('..') || path.isAbsolute(relative)) {
return false;
}
return true;
} catch {
return false;
}
}
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const projectRootParam = url.searchParams.get('projectRoot');
const projectRoot = projectRootParam ?? process.cwd();
if (projectRootParam && !isValidProjectRoot(projectRootParam)) {
return NextResponse.json(
{ ok: false, error: { classification: 'validation', message: 'Invalid projectRoot path' } },
{ status: 400 }
);
}
try {
const issues = await readIssuesFromDisk({ projectRoot, preferBd: true });
return NextResponse.json({ ok: true, issues });
} catch (error) {
console.error('[API/BeadsRead] Failed to read issues:', error);
return NextResponse.json(
{
ok: false,
error: {
classification: 'internal_error',
message: 'An internal error occurred while reading issues.',
},
},
{ status: 500 },
);
}
}

View file

@ -2,4 +2,4 @@ import { handleMutationRequest } from '../_shared';
export async function POST(request: Request): Promise<Response> {
return handleMutationRequest(request, 'reopen');
}
}

View file

@ -2,4 +2,4 @@ import { handleMutationRequest } from '../_shared';
export async function POST(request: Request): Promise<Response> {
return handleMutationRequest(request, 'update');
}
}

View file

@ -1,154 +1,154 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { canonicalizeWindowsPath } from '../../../lib/pathing';
import { issuesEventBus, activityEventBus, SSE_CONNECTED_FRAME, SSE_HEARTBEAT_FRAME, toSseFrame, toActivitySseFrame } from '../../../lib/realtime';
import { getIssuesWatchManager } from '../../../lib/watcher';
const encoder = new TextEncoder();
const HEARTBEAT_MS = 15_000;
const LAST_TOUCHED_POLL_MS = 1_000;
async function readLastTouchedVersion(filePath: string): Promise<number | null> {
try {
const stat = await fs.stat(filePath);
return stat.mtimeMs;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
// Log non-ENOENT errors but don't swallow them silently
console.error('[Events] Failed to read last-touched version:', error);
return null;
}
}
export const dynamic = 'force-dynamic';
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const projectRootSearchParam = url.searchParams.get('projectRoot');
const projectRoot = canonicalizeWindowsPath(projectRootSearchParam || process.cwd());
console.log(`[SSE /api/events] Connection request - raw param: "${projectRootSearchParam}", canonicalized: "${projectRoot}"`);
try {
getIssuesWatchManager().startWatch(projectRoot);
} catch (error) {
return Response.json(
{
ok: false,
error: {
classification: 'unknown',
message: error instanceof Error ? error.message : 'Failed to initialize watcher.',
},
},
{ status: 500 },
);
}
let cleanup = () => { };
const stream = new ReadableStream<Uint8Array>({
start(controller) {
let closed = false;
const write = (payload: string) => {
if (closed) {
return;
}
controller.enqueue(encoder.encode(payload));
};
write(SSE_CONNECTED_FRAME);
console.log(`[SSE /api/events] Subscribing to event bus with projectRoot: ${projectRoot}`);
const unsubscribeIssues = issuesEventBus.subscribe(
(event) => {
console.log('[SSE /api/events] Received ISSUES event from bus:', event.kind, 'projectRoot:', event.projectRoot);
write(toSseFrame(event));
},
{ projectRoot },
);
console.log(`[SSE /api/events] Subscriber count after subscribe: ${issuesEventBus.getSubscriberCount()}`);
const unsubscribeActivity = activityEventBus.subscribe(
(event) => {
console.log('[SSE /api/events] Received ACTIVITY event from bus');
write(toActivitySseFrame(event));
},
{ projectRoot },
);
const heartbeat = setInterval(() => {
write(SSE_HEARTBEAT_FRAME);
}, HEARTBEAT_MS);
const lastTouchedPath = path.join(projectRoot, '.beads', 'last-touched');
let lastTouchedVersion: number | null = null;
let isPolling = false;
const pollLastTouched = async () => {
if (isPolling) {
return;
}
isPolling = true;
try {
const nextVersion = await readLastTouchedVersion(lastTouchedPath);
if (nextVersion === null) {
return;
}
if (lastTouchedVersion === null) {
lastTouchedVersion = nextVersion;
return;
}
if (nextVersion !== lastTouchedVersion) {
lastTouchedVersion = nextVersion;
write(toSseFrame(issuesEventBus.emit(projectRoot, lastTouchedPath, 'telemetry')));
}
} finally {
isPolling = false;
}
};
const touchedPoll = setInterval(() => {
void pollLastTouched();
}, LAST_TOUCHED_POLL_MS);
void pollLastTouched();
const close = () => {
if (closed) {
return;
}
closed = true;
clearInterval(heartbeat);
clearInterval(touchedPoll);
unsubscribeIssues();
unsubscribeActivity();
request.signal.removeEventListener('abort', close);
try {
controller.close();
} catch {
// stream already closed
}
};
cleanup = close;
request.signal.addEventListener('abort', close);
},
cancel() {
// Called when client closes EventSource/reader.
// Ensures heartbeat + subscriber cleanup always runs.
cleanup();
return Promise.resolve();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
},
});
import fs from 'node:fs/promises';
import path from 'node:path';
import { canonicalizeWindowsPath } from '../../../lib/pathing';
import { issuesEventBus, activityEventBus, SSE_CONNECTED_FRAME, SSE_HEARTBEAT_FRAME, toSseFrame, toActivitySseFrame } from '../../../lib/realtime';
import { getIssuesWatchManager } from '../../../lib/watcher';
const encoder = new TextEncoder();
const HEARTBEAT_MS = 15_000;
const LAST_TOUCHED_POLL_MS = 1_000;
async function readLastTouchedVersion(filePath: string): Promise<number | null> {
try {
const stat = await fs.stat(filePath);
return stat.mtimeMs;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
// Log non-ENOENT errors but don't swallow them silently
console.error('[Events] Failed to read last-touched version:', error);
return null;
}
}
export const dynamic = 'force-dynamic';
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const projectRootSearchParam = url.searchParams.get('projectRoot');
const projectRoot = canonicalizeWindowsPath(projectRootSearchParam || process.cwd());
console.log(`[SSE /api/events] Connection request - raw param: "${projectRootSearchParam}", canonicalized: "${projectRoot}"`);
try {
getIssuesWatchManager().startWatch(projectRoot);
} catch (error) {
return Response.json(
{
ok: false,
error: {
classification: 'unknown',
message: error instanceof Error ? error.message : 'Failed to initialize watcher.',
},
},
{ status: 500 },
);
}
let cleanup = () => { };
const stream = new ReadableStream<Uint8Array>({
start(controller) {
let closed = false;
const write = (payload: string) => {
if (closed) {
return;
}
controller.enqueue(encoder.encode(payload));
};
write(SSE_CONNECTED_FRAME);
console.log(`[SSE /api/events] Subscribing to event bus with projectRoot: ${projectRoot}`);
const unsubscribeIssues = issuesEventBus.subscribe(
(event) => {
console.log('[SSE /api/events] Received ISSUES event from bus:', event.kind, 'projectRoot:', event.projectRoot);
write(toSseFrame(event));
},
{ projectRoot },
);
console.log(`[SSE /api/events] Subscriber count after subscribe: ${issuesEventBus.getSubscriberCount()}`);
const unsubscribeActivity = activityEventBus.subscribe(
(event) => {
console.log('[SSE /api/events] Received ACTIVITY event from bus');
write(toActivitySseFrame(event));
},
{ projectRoot },
);
const heartbeat = setInterval(() => {
write(SSE_HEARTBEAT_FRAME);
}, HEARTBEAT_MS);
const lastTouchedPath = path.join(projectRoot, '.beads', 'last-touched');
let lastTouchedVersion: number | null = null;
let isPolling = false;
const pollLastTouched = async () => {
if (isPolling) {
return;
}
isPolling = true;
try {
const nextVersion = await readLastTouchedVersion(lastTouchedPath);
if (nextVersion === null) {
return;
}
if (lastTouchedVersion === null) {
lastTouchedVersion = nextVersion;
return;
}
if (nextVersion !== lastTouchedVersion) {
lastTouchedVersion = nextVersion;
write(toSseFrame(issuesEventBus.emit(projectRoot, lastTouchedPath, 'telemetry')));
}
} finally {
isPolling = false;
}
};
const touchedPoll = setInterval(() => {
void pollLastTouched();
}, LAST_TOUCHED_POLL_MS);
void pollLastTouched();
const close = () => {
if (closed) {
return;
}
closed = true;
clearInterval(heartbeat);
clearInterval(touchedPoll);
unsubscribeIssues();
unsubscribeActivity();
request.signal.removeEventListener('abort', close);
try {
controller.close();
} catch {
// stream already closed
}
};
cleanup = close;
request.signal.addEventListener('abort', close);
},
cancel() {
// Called when client closes EventSource/reader.
// Ensures heartbeat + subscriber cleanup always runs.
cleanup();
return Promise.resolve();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
},
});
}

View file

@ -1,60 +1,60 @@
import { NextResponse } from 'next/server';
import { addProject, listProjects, RegistryValidationError, removeProject } from '../../../lib/registry';
export const runtime = 'nodejs';
function projectsPayload(projects: Array<{ path: string }>): { projects: Array<{ path: string }> } {
return {
projects: projects.map((project) => ({ path: project.path })),
};
}
async function readPathFromBody(request: Request): Promise<string> {
let body: unknown;
try {
body = await request.json();
} catch {
throw new RegistryValidationError('Request body must be valid JSON.');
}
const path = (body as { path?: unknown }).path;
if (typeof path !== 'string' || path.trim().length === 0) {
throw new RegistryValidationError('`path` is required and must be a non-empty string.');
}
return path;
}
export async function GET(): Promise<Response> {
const projects = await listProjects();
return NextResponse.json(projectsPayload(projects), { status: 200 });
}
export async function POST(request: Request): Promise<Response> {
try {
const projectPath = await readPathFromBody(request);
const result = await addProject(projectPath);
return NextResponse.json(projectsPayload(result.projects), { status: result.added ? 201 : 200 });
} catch (error) {
if (error instanceof RegistryValidationError) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json({ error: 'Failed to add project.' }, { status: 500 });
}
}
export async function DELETE(request: Request): Promise<Response> {
try {
const projectPath = await readPathFromBody(request);
const result = await removeProject(projectPath);
return NextResponse.json({ removed: result.removed, ...projectsPayload(result.projects) }, { status: 200 });
} catch (error) {
if (error instanceof RegistryValidationError) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json({ error: 'Failed to remove project.' }, { status: 500 });
}
}
import { NextResponse } from 'next/server';
import { addProject, listProjects, RegistryValidationError, removeProject } from '../../../lib/registry';
export const runtime = 'nodejs';
function projectsPayload(projects: Array<{ path: string }>): { projects: Array<{ path: string }> } {
return {
projects: projects.map((project) => ({ path: project.path })),
};
}
async function readPathFromBody(request: Request): Promise<string> {
let body: unknown;
try {
body = await request.json();
} catch {
throw new RegistryValidationError('Request body must be valid JSON.');
}
const path = (body as { path?: unknown }).path;
if (typeof path !== 'string' || path.trim().length === 0) {
throw new RegistryValidationError('`path` is required and must be a non-empty string.');
}
return path;
}
export async function GET(): Promise<Response> {
const projects = await listProjects();
return NextResponse.json(projectsPayload(projects), { status: 200 });
}
export async function POST(request: Request): Promise<Response> {
try {
const projectPath = await readPathFromBody(request);
const result = await addProject(projectPath);
return NextResponse.json(projectsPayload(result.projects), { status: result.added ? 201 : 200 });
} catch (error) {
if (error instanceof RegistryValidationError) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json({ error: 'Failed to add project.' }, { status: 500 });
}
}
export async function DELETE(request: Request): Promise<Response> {
try {
const projectPath = await readPathFromBody(request);
const result = await removeProject(projectPath);
return NextResponse.json({ removed: result.removed, ...projectsPayload(result.projects) }, { status: 200 });
} catch (error) {
if (error instanceof RegistryValidationError) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json({ error: 'Failed to remove project.' }, { status: 500 });
}
}

View file

@ -1,44 +1,44 @@
import { NextResponse } from 'next/server';
import { scanForProjects } from '../../../lib/scanner';
import type { ScanMode } from '../../../lib/scanner';
export const runtime = 'nodejs';
function parseMode(value: string | null): ScanMode {
if (!value || value === 'default') {
return 'default';
}
if (value === 'full-drive') {
return 'full-drive';
}
throw new Error('Invalid scan mode. Use mode=default or mode=full-drive.');
}
function parseDepth(value: string | null): number | undefined {
if (!value) {
return undefined;
}
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed < 0) {
throw new Error('Depth must be a non-negative integer.');
}
return parsed;
}
export async function GET(request: Request): Promise<Response> {
try {
const url = new URL(request.url);
const mode = parseMode(url.searchParams.get('mode'));
const maxDepth = parseDepth(url.searchParams.get('depth'));
const result = await scanForProjects({ mode, maxDepth });
return NextResponse.json(result, { status: 200 });
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to scan projects.';
return NextResponse.json({ error: message }, { status: 400 });
}
}
import { NextResponse } from 'next/server';
import { scanForProjects } from '../../../lib/scanner';
import type { ScanMode } from '../../../lib/scanner';
export const runtime = 'nodejs';
function parseMode(value: string | null): ScanMode {
if (!value || value === 'default') {
return 'default';
}
if (value === 'full-drive') {
return 'full-drive';
}
throw new Error('Invalid scan mode. Use mode=default or mode=full-drive.');
}
function parseDepth(value: string | null): number | undefined {
if (!value) {
return undefined;
}
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed < 0) {
throw new Error('Depth must be a non-negative integer.');
}
return parsed;
}
export async function GET(request: Request): Promise<Response> {
try {
const url = new URL(request.url);
const mode = parseMode(url.searchParams.get('mode'));
const maxDepth = parseDepth(url.searchParams.get('depth'));
const result = await scanForProjects({ mode, maxDepth });
return NextResponse.json(result, { status: 200 });
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to scan projects.';
return NextResponse.json({ error: message }, { status: 400 });
}
}

View file

@ -1,71 +1,71 @@
import { NextResponse } from 'next/server';
import path from 'node:path';
import { readIssuesFromDisk } from '../../../lib/read-issues';
import { activityEventBus } from '../../../lib/realtime';
import { buildSessionTaskFeed, getCommunicationSummary, getAgentLivenessMap, calculateIncursions } from '../../../lib/agent-sessions';
import { listAgents } from '../../../lib/agent-registry';
function isValidProjectRoot(root: string): boolean {
try {
const resolved = path.resolve(root);
if (!path.isAbsolute(resolved)) {
return false;
}
// Prevent path traversal by ensuring resolved path stays within the project root
const allowedBase = process.cwd();
const relative = path.relative(allowedBase, resolved);
// If "resolved" is outside "allowedBase", "relative" will start with ".."
if (relative.startsWith('..') || path.isAbsolute(relative)) {
return false;
}
return true;
} catch {
return false;
}
}
export const dynamic = 'force-dynamic';
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const projectRootParam = url.searchParams.get('projectRoot');
const projectRoot = projectRootParam ?? process.cwd();
if (projectRootParam && !isValidProjectRoot(projectRoot)) {
return NextResponse.json(
{ ok: false, error: { classification: 'validation', message: 'Invalid projectRoot path' } },
{ status: 400 }
);
}
try {
const issues = await readIssuesFromDisk({ projectRoot, preferBd: true });
const activity = activityEventBus.getHistory(projectRoot);
import { NextResponse } from 'next/server';
import path from 'node:path';
import { readIssuesFromDisk } from '../../../lib/read-issues';
import { activityEventBus } from '../../../lib/realtime';
import { buildSessionTaskFeed, getCommunicationSummary, getAgentLivenessMap, calculateIncursions } from '../../../lib/agent-sessions';
import { listAgents } from '../../../lib/agent-registry';
function isValidProjectRoot(root: string): boolean {
try {
const resolved = path.resolve(root);
if (!path.isAbsolute(resolved)) {
return false;
}
// Prevent path traversal by ensuring resolved path stays within the project root
const allowedBase = process.cwd();
const relative = path.relative(allowedBase, resolved);
// If "resolved" is outside "allowedBase", "relative" will start with ".."
if (relative.startsWith('..') || path.isAbsolute(relative)) {
return false;
}
return true;
} catch {
return false;
}
}
export const dynamic = 'force-dynamic';
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const projectRootParam = url.searchParams.get('projectRoot');
const projectRoot = projectRootParam ?? process.cwd();
if (projectRootParam && !isValidProjectRoot(projectRoot)) {
return NextResponse.json(
{ ok: false, error: { classification: 'validation', message: 'Invalid projectRoot path' } },
{ status: 400 }
);
}
try {
const issues = await readIssuesFromDisk({ projectRoot, preferBd: true });
const activity = activityEventBus.getHistory(projectRoot);
const communication = await getCommunicationSummary(projectRoot);
const livenessMap = await getAgentLivenessMap(projectRoot, activity);
const livenessMap = await getAgentLivenessMap(projectRoot, activity);
const incursions = await calculateIncursions(projectRoot, livenessMap);
const agentsResult = await listAgents({}, { projectRoot });
const feed = buildSessionTaskFeed(issues, activity, communication, livenessMap);
return NextResponse.json({
ok: true,
feed,
livenessMap,
incursions,
agents: agentsResult.data ?? []
});
} catch (error) {
console.error('[API/Sessions] Failed to load session feed:', error);
return NextResponse.json(
{
ok: false,
error: {
classification: 'internal_error',
message: 'An internal error occurred while loading the session feed.',
},
},
{ status: 500 },
);
}
}
const agentsResult = await listAgents({}, { projectRoot });
const feed = buildSessionTaskFeed(issues, activity, communication, livenessMap);
return NextResponse.json({
ok: true,
feed,
livenessMap,
incursions,
agents: agentsResult.data ?? []
});
} catch (error) {
console.error('[API/Sessions] Failed to load session feed:', error);
return NextResponse.json(
{
ok: false,
error: {
classification: 'internal_error',
message: 'An internal error occurred while loading the session feed.',
},
},
{ status: 500 },
);
}
}