Merge origin/main into feature/assign-archetypes-to-tasks-ui
Resolved conflicts: - .gitignore: kept both bd.sock.startlock and .beadboard/ entries - package.json: kept feature branch test script (explicit enumeration) - API routes: kept dynamic export + isValidProjectRoot from main - globals.css: kept HEAD slideInFromRight animation - use-beads-subscription.ts: kept HEAD onopen handler - realtime.ts: kept main console.log in emit() - snapshot-differ.ts: kept main type-aware dependency diff Blue colors preserved from feature branch.
This commit is contained in:
commit
a8079813b8
28 changed files with 931 additions and 70 deletions
|
|
@ -1,11 +1,40 @@
|
|||
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 projectRoot = url.searchParams.get('projectRoot') || undefined;
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,42 @@
|
|||
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 projectRoot = url.searchParams.get('projectRoot') ?? process.cwd();
|
||||
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 });
|
||||
|
|
|
|||
|
|
@ -1,23 +1,51 @@
|
|||
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 projectRoot = url.searchParams.get('projectRoot') ?? process.cwd();
|
||||
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: 'unknown',
|
||||
message: error instanceof Error ? error.message : 'Failed to read issues.',
|
||||
classification: 'internal_error',
|
||||
message: 'An internal error occurred while reading issues.',
|
||||
},
|
||||
},
|
||||
{ status: 500 },
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ async function readLastTouchedVersion(filePath: string): Promise<number | null>
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -84,18 +86,27 @@ export async function GET(request: Request): Promise<Response> {
|
|||
const lastTouchedPath = path.join(projectRoot, '.beads', 'last-touched');
|
||||
let lastTouchedVersion: number | null = null;
|
||||
|
||||
let isPolling = false;
|
||||
const pollLastTouched = async () => {
|
||||
const nextVersion = await readLastTouchedVersion(lastTouchedPath);
|
||||
if (nextVersion === null) {
|
||||
if (isPolling) {
|
||||
return;
|
||||
}
|
||||
if (lastTouchedVersion === null) {
|
||||
lastTouchedVersion = nextVersion;
|
||||
return;
|
||||
}
|
||||
if (nextVersion !== lastTouchedVersion) {
|
||||
lastTouchedVersion = nextVersion;
|
||||
write(toSseFrame(issuesEventBus.emit(projectRoot, lastTouchedPath, 'telemetry')));
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,42 @@
|
|||
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 projectRoot = url.searchParams.get('projectRoot') ?? process.cwd();
|
||||
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 });
|
||||
|
|
@ -16,7 +44,7 @@ export async function GET(request: Request): Promise<Response> {
|
|||
const communication = await getCommunicationSummary();
|
||||
const livenessMap = await getAgentLivenessMap(projectRoot, activity);
|
||||
const incursions = await calculateIncursions();
|
||||
const agentsResult = await listAgents({});
|
||||
const agentsResult = await listAgents({}, { projectRoot });
|
||||
|
||||
const feed = buildSessionTaskFeed(issues, activity, communication, livenessMap);
|
||||
|
||||
|
|
@ -33,8 +61,8 @@ export async function GET(request: Request): Promise<Response> {
|
|||
{
|
||||
ok: false,
|
||||
error: {
|
||||
classification: 'unknown',
|
||||
message: error instanceof Error ? error.message : 'Failed to load session feed.',
|
||||
classification: 'internal_error',
|
||||
message: 'An internal error occurred while loading the session feed.',
|
||||
},
|
||||
},
|
||||
{ status: 500 },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue