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
3 changes: 3 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ src/
│ └── index.ts Mastra singleton
├── server/ Product domain services
│ ├── approvals/ Durable approval logic
│ ├── blockers/ Durable safety and workflow blockers
│ ├── chat/ Turn runner, Mastra adapter, stream projection
│ ├── projects/ Project and thread service layer
│ ├── targets/ Target inventory and authorization
Expand Down Expand Up @@ -132,6 +133,8 @@ Generated evidence and uploads should flow through `src/server/evidence/artifact

The artifact service writes or references object content, records metadata, links evidence to project/thread/target/task/finding scope, computes stable hashes, indexes text evidence for retrieval when possible, and notifies the project UI. Findings link back to evidence artifact IDs instead of relying on chat prose.

Blockers are canonical project records in the app database. Passive-auth normalization upserts one blocker per project, target, task, and reason, retaining the current report and raw source Artifact IDs. Its suggested actions are review-only labels: projecting a passive signal never retries a tool, broadens network scope, requests approval, or interacts with a target. The in-memory repository remains available only for explicitly isolated tests.

Evidence ingestion keeps format-aware segmentation for source symbols, log windows, record-oriented data, and diff hunks, then delegates final chunking to Mastra's document chunker. Vector and GraphRAG queries share one versioned index configuration and preserve canonical citation metadata. Both retrieval tools require project context; thread filtering is optional. When the embedding or chunk/filter schema changes, `pnpm rag:reindex` rebuilds readable artifact vectors into the current index. The v3 artifact index adds LanceDB's flattened metadata columns used by replacement deletes; reindexing succeeds before stale indexes are pruned.

The pinned `@mastra/lance` package carries a local patch for three adapter defects exercised by retrieval tests: camelCase metadata columns must be quoted for DataFusion filters, memory-style indexes must target their table when deleting multiple vectors, and local IVFFlat creation must use LanceDB's IVFFlat implementation. Remove each patch hunk when the corresponding upstream behavior is available in the pinned release.
Expand Down
66 changes: 66 additions & 0 deletions src/components/chat/WorkspaceDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ function getCockpitSignal(cockpit: ResearchCockpitView | null) {
if (!cockpit) {
return null;
}
const activeBlockers = cockpit.summary.blockers.total;
if (activeBlockers > 0) {
return {
label: `${activeBlockers} blocker${activeBlockers === 1 ? "" : "s"}`,
detail: "Review durable blocker evidence before continuing research.",
icon: <ShieldAlert size={14} />,
};
}
const pendingApprovals = cockpit.summary.approvals.byStatus.pending ?? 0;
if (pendingApprovals > 0) {
return {
Expand Down Expand Up @@ -178,6 +186,7 @@ export function WorkspaceDashboard({
targetAuthorizationDecisions,
});
const attackPaths = cockpit?.attackPaths ?? [];
const activeBlockers = cockpit?.activeBlockers ?? [];
const completeSignals =
planItems.filter((item) => item.status === "completed").length +
evidenceItems.filter((item) => item.done).length;
Expand All @@ -194,6 +203,7 @@ export function WorkspaceDashboard({
const hasDashboardContent =
planItems.length > 0 ||
evidenceItems.length > 0 ||
activeBlockers.length > 0 ||
attackPaths.length > 0 ||
Boolean(latestFinding) ||
Boolean(latestApproval);
Expand Down Expand Up @@ -308,6 +318,62 @@ export function WorkspaceDashboard({
</div>

<div className="dashboard-grid">
{activeBlockers.length > 0 ? (
<section
className="dashboard-panel dashboard-blocker-panel"
aria-labelledby="dashboard-blocker-title"
>
<div className="dashboard-panel-header">
<div>
<span className="dashboard-panel-kicker">
<ShieldAlert size={17} />
Active blockers
</span>
<h3 id="dashboard-blocker-title" className="sr-only">
Active blockers
</h3>
</div>
<span className="dashboard-status">
{activeBlockers.length < (cockpit?.summary.blockers.total ?? 0)
? `${activeBlockers.length} of ${cockpit?.summary.blockers.total}`
: activeBlockers.length}{" "}
blocker{(cockpit?.summary.blockers.total ?? activeBlockers.length) === 1 ? "" : "s"}
</span>
</div>
<ul className="dashboard-blocker-list">
{activeBlockers.map((blocker) => (
<li key={blocker.id}>
<div>
<strong title={blocker.title}>{blocker.title}</strong>
<span className={`badge is-${blocker.severity}`}>{blocker.severity}</span>
</div>
<p>{blocker.detail}</p>
<small>
{blocker.reason}
{blocker.targetId ? ` · target ${blocker.targetId}` : ""}
{blocker.taskId ? ` · task ${blocker.taskId}` : ""}
</small>
{blocker.summaryArtifactId || blocker.sourceRefId ? (
<small>
Summary Artifact: {blocker.summaryArtifactId ?? blocker.sourceRefId}
</small>
) : null}
{blocker.evidenceArtifactIds?.length ? (
<small>Source Artifacts: {blocker.evidenceArtifactIds.join(", ")}</small>
) : null}
{blocker.nextActions.length > 0 ? (
<div className="dashboard-blocker-actions">
{blocker.nextActions.map((action) => (
<span key={`${blocker.id}-${action.label}`}>{action.label}</span>
))}
</div>
) : null}
</li>
))}
</ul>
</section>
) : null}

{planItems.length > 0 ? (
<section
className="dashboard-panel dashboard-plan-panel"
Expand Down
21 changes: 21 additions & 0 deletions src/components/chat/messageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,11 @@ export type ResearchCockpitView = {
toolRuns: { total: number; byStatus: Record<string, number> };
attackPaths: { total: number; byStatus: Record<string, number> };
schedulerTasks: { total: number; byStatus: Record<string, number> };
blockers: {
total: number;
byReason: Record<string, number>;
bySeverity: Record<string, number>;
};
codeReviewRuns?: { total: number; byStatus: Record<string, number> };
artifacts: {
total: number;
Expand Down Expand Up @@ -300,6 +305,22 @@ export type ResearchCockpitView = {
blockerCount: number;
updatedAt: string;
}>;
activeBlockers: Array<{
id: string;
threadId?: string;
targetId?: string;
taskId?: string;
reason: string;
title: string;
detail: string;
severity: "info" | "warning" | "danger";
source: string;
sourceRefId?: string;
summaryArtifactId?: string;
evidenceArtifactIds?: string[];
nextActions: Array<{ label: string; action: string }>;
createdAt: string;
}>;
recentCodeReviewRuns?: Array<{
id: string;
threadId?: string;
Expand Down
12 changes: 10 additions & 2 deletions src/server/blockers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export {
createDatabaseBlockerRepository,
createInMemoryBlockerRepository,
DatabaseBlockerRepository,
InMemoryBlockerRepository,
} from "./repository";
export {
Expand All @@ -19,15 +21,21 @@ export type {
ListBlockersQuery,
} from "./types";

import { createInMemoryBlockerRepository } from "./repository";
import { createDatabaseBlockerRepository, createInMemoryBlockerRepository } from "./repository";
import { createBlockerService } from "./service";
import type { BlockerService } from "./types";

let defaultBlockerService: BlockerService | undefined;

export function getDefaultBlockerService(): BlockerService {
if (!defaultBlockerService) {
defaultBlockerService = createBlockerService(createInMemoryBlockerRepository());
// Product runtime must survive restart. Tests keep their existing isolated
// repository unless they explicitly exercise the database implementation.
const repository =
process.env.NODE_ENV === "test"
? createInMemoryBlockerRepository()
: createDatabaseBlockerRepository();
defaultBlockerService = createBlockerService(repository);
}
return defaultBlockerService;
}
Expand Down
Loading