img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2f7a4e7fc64a8874373c1584646a63f28cd63c47
--- /dev/null
+++ b/src/components/ui/collapsible.tsx
@@ -0,0 +1,33 @@
+"use client"
+
+import { Collapsible as CollapsiblePrimitive } from "radix-ui"
+
+function Collapsible({
+ ...props
+}: React.ComponentProps
) {
+ return
+}
+
+function CollapsibleTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CollapsibleContent({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d763cd9ab0e151d4bd8e32b337fbf6278bcdf3fb
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+
+ )
+}
+
+export { Input }
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1ac80f701ebfe9dc02d632c08cafade732e46625
--- /dev/null
+++ b/src/components/ui/label.tsx
@@ -0,0 +1,24 @@
+"use client"
+
+import * as React from "react"
+import { Label as LabelPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Label({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Label }
diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..584011b4f4a7800de8f7aa64e3c83b999d5c07ea
--- /dev/null
+++ b/src/components/ui/progress.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import { Progress as ProgressPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Progress({
+ className,
+ value,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export { Progress }
diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..facbbe7d8dfdb177b8498f338232c705768b7334
--- /dev/null
+++ b/src/components/ui/scroll-area.tsx
@@ -0,0 +1,55 @@
+"use client"
+
+import * as React from "react"
+import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function ScrollArea({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+
+ )
+}
+
+function ScrollBar({
+ className,
+ orientation = "vertical",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export { ScrollArea, ScrollBar }
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f09dfb489ab62e3ea68d640f2c345753f5277110
--- /dev/null
+++ b/src/components/ui/select.tsx
@@ -0,0 +1,192 @@
+"use client"
+
+import * as React from "react"
+import { Select as SelectPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
+
+function Select({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SelectGroup({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SelectValue({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SelectTrigger({
+ className,
+ size = "default",
+ children,
+ ...props
+}: React.ComponentProps & {
+ size?: "sm" | "default"
+}) {
+ return (
+
+ {children}
+
+
+
+
+ )
+}
+
+function SelectContent({
+ className,
+ children,
+ position = "item-aligned",
+ align = "center",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ )
+}
+
+function SelectLabel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SelectItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function SelectSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SelectScrollUpButton({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function SelectScrollDownButton({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectScrollDownButton,
+ SelectScrollUpButton,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+}
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d4570908d42d3da4e15111a776f52c88bd0e6911
--- /dev/null
+++ b/src/components/ui/separator.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import { Separator as SeparatorPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Separator }
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0118624f6e6a64b90c24502a4e2b8c5513098072
--- /dev/null
+++ b/src/components/ui/skeleton.tsx
@@ -0,0 +1,13 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..abeaced4b2efac98bf5c50ec2c3791bbc88bc0bd
--- /dev/null
+++ b/src/components/ui/table.tsx
@@ -0,0 +1,116 @@
+"use client"
+
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Table({ className, ...props }: React.ComponentProps<"table">) {
+ return (
+
+ )
+}
+
+function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
+ return (
+
+ )
+}
+
+function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
+ return (
+
+ )
+}
+
+function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
+ return (
+ tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+ return (
+
+ )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+ return (
+ |
+ )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+ return (
+ |
+ )
+}
+
+function TableCaption({
+ className,
+ ...props
+}: React.ComponentProps<"caption">) {
+ return (
+
+ )
+}
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..05f469f2afa93c7bc96070aead57021011c7916e
--- /dev/null
+++ b/src/components/ui/tabs.tsx
@@ -0,0 +1,90 @@
+"use client"
+
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { Tabs as TabsPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Tabs({
+ className,
+ orientation = "horizontal",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+const tabsListVariants = cva(
+ "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
+ {
+ variants: {
+ variant: {
+ default: "bg-muted",
+ line: "gap-1 bg-transparent",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function TabsList({
+ className,
+ variant = "default",
+ ...props
+}: React.ComponentProps &
+ VariantProps) {
+ return (
+
+ )
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..04d27f7d17584cd77ba7d6f2377f5be6779f1270
--- /dev/null
+++ b/src/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bb1ea523427d069fd2b6947802858f15766910c5
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -0,0 +1,57 @@
+"use client"
+
+import * as React from "react"
+import { Tooltip as TooltipPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
diff --git a/src/lib/rocmpilot/data.ts b/src/lib/rocmpilot/data.ts
new file mode 100644
index 0000000000000000000000000000000000000000..da13d4563ebad093d16a17270aaacb64115a8ca1
--- /dev/null
+++ b/src/lib/rocmpilot/data.ts
@@ -0,0 +1,1102 @@
+import type {
+ AgentMessage,
+ AgentMessageKind,
+ AgentMemory,
+ BenchmarkResult,
+ Finding,
+ GpuModelStatus,
+ LongContextMemoryStatus,
+ PatchPreview,
+ RocmRun,
+ RunMode,
+ RunStatus,
+ RunStage,
+ StageStatus,
+ SampleRepo,
+ RunTarget,
+} from "./types";
+import { isRealGitHubRepoUrl, parseGitHubRepoUrl } from "./github-url";
+import type { RepoAnalysis } from "./github-scanner";
+import {
+ buildMemoryConversationId,
+ DEFAULT_MEMORY_CUSTOMER_ID,
+ DEFAULT_MEMORY_USER_ID,
+} from "./memory-ids";
+
+const TOTAL_DURATION_MS = 30_000;
+
+const STAGES: Array & {
+ durationMs: number;
+}> = [
+ {
+ id: "repo-doctor",
+ agent: "Repo Doctor Agent",
+ title: "Repo compatibility scan",
+ description: "Dependency graph, Docker image, device paths, and runtime flags",
+ durationMs: 5_000,
+ },
+ {
+ id: "migration-planner",
+ agent: "Migration Planner Agent",
+ title: "ROCm migration plan",
+ description: "PyTorch ROCm wheels, vLLM runtime, and device abstraction changes",
+ durationMs: 6_000,
+ },
+ {
+ id: "build-runner",
+ agent: "Build Runner Agent",
+ title: "Build and smoke tests",
+ description: "Container validation, import checks, and inference dry run",
+ durationMs: 5_500,
+ },
+ {
+ id: "benchmark-agent",
+ agent: "Benchmark Agent",
+ title: "MI300X readiness benchmark",
+ description: "Throughput, latency, memory, and fallback path comparison",
+ durationMs: 5_000,
+ },
+ {
+ id: "report-agent",
+ agent: "Report Agent",
+ title: "Judge-ready report",
+ description: "Technical summary, business value, and AMD proof points",
+ durationMs: 6_000,
+ },
+];
+
+export const SAMPLE_REPOS: SampleRepo[] = [
+ {
+ id: "qwen-vllm-cuda",
+ name: "Qwen vLLM CUDA Starter",
+ repoUrl: "https://github.com/example/qwen-vllm-cuda-starter",
+ stack: "FastAPI, PyTorch, vLLM, Docker",
+ model: "Qwen/Qwen2.5-Coder-7B-Instruct",
+ description: "A common NVIDIA-first inference service with CUDA-only install steps.",
+ risk: "Hardcoded CUDA device checks block AMD Developer Cloud deployment.",
+ },
+ {
+ id: "torch-agent-worker",
+ name: "Torch Agent Worker",
+ repoUrl: "https://github.com/example/torch-agent-worker",
+ stack: "Python workers, PyTorch, Redis queue",
+ model: "Qwen/Qwen3-Coder-Next",
+ description: "Background coding-agent worker that assumes NVIDIA runtime images.",
+ risk: "Docker and benchmark scripts hide GPU vendor assumptions.",
+ },
+];
+
+export const FINDINGS: Finding[] = [
+ {
+ id: "cuda-device",
+ severity: "critical",
+ category: "Runtime device lock",
+ file: "src/inference/server.py",
+ line: 42,
+ explanation:
+ "`torch.device('cuda')` is used directly, so the app never checks ROCm-compatible PyTorch device availability or CPU fallback.",
+ recommendedFix:
+ "Introduce a device resolver that accepts HIP-backed PyTorch as CUDA-compatible and records the detected backend.",
+ },
+ {
+ id: "docker-image",
+ severity: "high",
+ category: "Container image",
+ file: "Dockerfile",
+ line: 1,
+ explanation:
+ "The base image is `nvidia/cuda`, which prevents a clean ROCm/vLLM deployment on AMD Developer Cloud.",
+ recommendedFix:
+ "Use the ROCm vLLM image for AMD runs and keep CUDA images only as an optional backend.",
+ },
+ {
+ id: "vllm-flags",
+ severity: "high",
+ category: "Serving configuration",
+ file: "scripts/serve.sh",
+ line: 9,
+ explanation:
+ "The vLLM launch script omits ROCm-oriented environment flags and does not expose tensor-parallel settings.",
+ recommendedFix:
+ "Add backend-aware vLLM launch arguments and document MI300X model-serving defaults.",
+ },
+ {
+ id: "metrics",
+ severity: "medium",
+ category: "Benchmark visibility",
+ file: "benchmarks/run_latency.py",
+ line: 18,
+ explanation:
+ "The benchmark reports request latency only and misses GPU memory, tokens/sec, and backend provenance.",
+ recommendedFix:
+ "Add AMD SMI/vLLM metrics capture so submission evidence includes GPU model, memory, and throughput.",
+ },
+];
+
+export const PATCHES: PatchPreview[] = [
+ {
+ id: "device-resolver",
+ file: "src/inference/device.py",
+ rationale:
+ "Centralizes device selection so ROCm-backed PyTorch can run without scattering vendor checks across the service.",
+ diff: `+import torch
++
++def resolve_device() -> tuple[str, str]:
++ if torch.cuda.is_available():
++ backend = "rocm" if getattr(torch.version, "hip", None) else "cuda"
++ return "cuda", backend
++ return "cpu", "cpu"
++
++DEVICE, GPU_BACKEND = resolve_device()
+`,
+ },
+ {
+ id: "rocm-docker",
+ file: "Dockerfile.rocm",
+ rationale:
+ "Adds an AMD-specific runtime image while preserving the original CUDA path for teams that need dual-vendor support.",
+ diff: `+FROM rocm/vllm:latest
++
++WORKDIR /workspace
++COPY requirements-rocm.txt .
++RUN pip install --no-cache-dir -r requirements-rocm.txt
++COPY . .
++
++ENV HIP_VISIBLE_DEVICES=0
++ENV VLLM_USE_ROCM=1
++CMD ["bash", "scripts/serve-rocm.sh"]
+`,
+ },
+ {
+ id: "serve-rocm",
+ file: "scripts/serve-rocm.sh",
+ rationale:
+ "Launches an OpenAI-compatible vLLM endpoint for the migration/report agents on AMD Instinct GPUs.",
+ diff: `+#!/usr/bin/env bash
++set -euo pipefail
++
++MODEL="\${MODEL:-Qwen/Qwen3-Coder-Next}"
++PORT="\${PORT:-8000}"
++
++python -m vllm.entrypoints.openai.api_server \\
++ --model "$MODEL" \\
++ --host 0.0.0.0 \\
++ --port "$PORT" \\
++ --tensor-parallel-size "\${TENSOR_PARALLEL_SIZE:-1}" \\
++ --max-model-len "\${MAX_MODEL_LEN:-32768}"
+`,
+ },
+];
+
+export const BENCHMARKS: BenchmarkResult[] = [
+ {
+ label: "Before migration",
+ backend: "CUDA-only config",
+ tokensPerSecond: 0,
+ p95LatencyMs: 0,
+ memoryGb: 0,
+ costNote: "Does not boot on AMD ROCm image.",
+ },
+ {
+ label: "ROCm-ready target",
+ backend: "ROCm + vLLM on MI300X",
+ tokensPerSecond: 182,
+ p95LatencyMs: 730,
+ memoryGb: 92,
+ costNote: "Estimated from demo profile; replace with live AMD run evidence.",
+ },
+ {
+ label: "Agent report model",
+ backend: "Qwen3-Coder-Next via OpenAI-compatible endpoint",
+ tokensPerSecond: 64,
+ p95LatencyMs: 1180,
+ memoryGb: 46,
+ costNote: "Runs as the Report Agent when AMD_QWEN_BASE_URL is configured.",
+ },
+];
+
+const LOGS = [
+ "queued run qwen-vllm-cuda-starter in mock-safe mode",
+ "repo-doctor: scanning pyproject.toml, Dockerfile, scripts, and src/inference",
+ "repo-doctor: found nvidia/cuda base image in Dockerfile",
+ "repo-doctor: found direct torch.device('cuda') usage in src/inference/server.py:42",
+ "migration-planner: generated ROCm runtime image proposal",
+ "migration-planner: created backend-aware device resolver",
+ "build-runner: docker build -f Dockerfile.rocm .",
+ "build-runner: import torch; torch.version.hip detected when ROCm wheel is present",
+ "build-runner: vLLM OpenAI endpoint smoke test passed in demo mode",
+ "benchmark-agent: captured target profile for MI300X/vLLM serving",
+ "report-agent: preparing technical and business summary",
+ "completed run with fallback-safe report path",
+];
+
+type MessageBlueprint = {
+ offsetMs: number;
+ agent: string;
+ toAgent: string;
+ role: string;
+ task: string;
+ leadAgent: string;
+ kind: AgentMessageKind;
+ replyToOffsetMs?: number;
+ memoryRefs?: string[];
+ message: (context: {
+ target: RunTarget;
+ sample: SampleRepo;
+ findings: Finding[];
+ patches: PatchPreview[];
+ }) => string;
+};
+
+const WAR_ROOM_MESSAGES: MessageBlueprint[] = [
+ {
+ offsetMs: 1_200,
+ agent: "Orchestrator",
+ toAgent: "Repo Doctor",
+ role: "Run coordinator",
+ task: "Repo compatibility scan",
+ leadAgent: "Repo Doctor",
+ kind: "question",
+ message: ({ target }) =>
+ `You are lead for the first task on ${target.label}. Ask the other agents what evidence they need before you mark any blocker as real.`,
+ },
+ {
+ offsetMs: 2_600,
+ agent: "Repo Doctor",
+ toAgent: "Build Runner",
+ role: "Compatibility scout",
+ task: "Repo compatibility scan",
+ leadAgent: "Repo Doctor",
+ kind: "question",
+ replyToOffsetMs: 1_200,
+ message: ({ target }) =>
+ target.type === "github"
+ ? `I am scanning ${target.scannedFiles || "the selected"} files. Which findings should I flag as build-breaking instead of just advisory?`
+ : "I am scanning Docker, vLLM scripts, PyTorch device paths, and benchmarks. Which findings should I flag as build-breaking instead of advisory?",
+ },
+ {
+ offsetMs: 3_700,
+ agent: "Build Runner",
+ toAgent: "Repo Doctor",
+ role: "Skeptical validator",
+ task: "Repo compatibility scan",
+ leadAgent: "Repo Doctor",
+ kind: "answer",
+ replyToOffsetMs: 2_600,
+ message:
+ () =>
+ "Treat container base images, hardcoded device selection, and missing smoke commands as build-breaking. Those decide whether the workload even starts on ROCm.",
+ },
+ {
+ offsetMs: 5_200,
+ agent: "Repo Doctor",
+ toAgent: "Migration Planner",
+ role: "Compatibility scout",
+ task: "Repo compatibility scan",
+ leadAgent: "Repo Doctor",
+ kind: "question",
+ replyToOffsetMs: 3_700,
+ message: ({ findings }) =>
+ findings[0]
+ ? `I found ${findings[0].category} in ${findings[0].file}:${findings[0].line}. Can you design the safest migration step for this first?`
+ : "I have no hard blocker yet. Can you prepare a safe migration pattern for hidden GPU vendor assumptions?",
+ },
+ {
+ offsetMs: 6_700,
+ agent: "Migration Planner",
+ toAgent: "Repo Doctor",
+ role: "Patch strategist",
+ task: "Repo compatibility scan",
+ leadAgent: "Repo Doctor",
+ kind: "proposal",
+ replyToOffsetMs: 5_200,
+ message: ({ findings }) =>
+ findings[0]
+ ? `Yes. First migration step: ${findings[0].recommendedFix} I will store that as the device-resolution pattern for later stages.`
+ : "Yes. I will store a pattern that keeps CUDA and ROCm paths explicit instead of hidden in environment assumptions.",
+ },
+ {
+ offsetMs: 8_200,
+ agent: "Repo Doctor",
+ toAgent: "Shared Memory",
+ role: "Compatibility scout",
+ task: "Repo compatibility scan",
+ leadAgent: "Repo Doctor",
+ kind: "memory",
+ replyToOffsetMs: 6_700,
+ memoryRefs: ["mem-device-resolution"],
+ message:
+ () =>
+ "Memory write: hardcoded device selection must be solved with one backend resolver, not scattered if/else checks.",
+ },
+ {
+ offsetMs: 9_600,
+ agent: "Orchestrator",
+ toAgent: "Migration Planner",
+ role: "Run coordinator",
+ task: "ROCm migration plan",
+ leadAgent: "Migration Planner",
+ kind: "question",
+ memoryRefs: ["mem-device-resolution"],
+ message:
+ () =>
+ "You are lead now. Use the device-resolution memory and ask Build Runner what would make the patch testable.",
+ },
+ {
+ offsetMs: 10_800,
+ agent: "Migration Planner",
+ toAgent: "Build Runner",
+ role: "Patch strategist",
+ task: "ROCm migration plan",
+ leadAgent: "Migration Planner",
+ kind: "question",
+ replyToOffsetMs: 9_600,
+ memoryRefs: ["mem-device-resolution"],
+ message:
+ () =>
+ "I can patch the resolver, but what acceptance check should prove this is ROCm-ready and not just cleaner code?",
+ },
+ {
+ offsetMs: 12_100,
+ agent: "Build Runner",
+ toAgent: "Migration Planner",
+ role: "Skeptical validator",
+ task: "ROCm migration plan",
+ leadAgent: "Migration Planner",
+ kind: "answer",
+ replyToOffsetMs: 10_800,
+ memoryRefs: ["mem-device-resolution"],
+ message:
+ () =>
+ "Acceptance check: import torch, report torch.version.hip when present, start vLLM with an OpenAI-compatible health request, then log backend provenance.",
+ },
+ {
+ offsetMs: 13_500,
+ agent: "Migration Planner",
+ toAgent: "Shared Memory",
+ role: "Patch strategist",
+ task: "ROCm migration plan",
+ leadAgent: "Migration Planner",
+ kind: "proposal",
+ replyToOffsetMs: 12_100,
+ memoryRefs: ["mem-device-resolution", "mem-rocm-acceptance"],
+ message: ({ patches }) =>
+ patches[0]
+ ? `Patch candidate for ${patches[0].file} is ready, and I am storing Build Runner's acceptance check with it.`
+ : "Patch candidate is ready, and I am storing Build Runner's acceptance check with it.",
+ },
+ {
+ offsetMs: 15_200,
+ agent: "Orchestrator",
+ toAgent: "Build Runner",
+ role: "Run coordinator",
+ task: "Build and smoke tests",
+ leadAgent: "Build Runner",
+ kind: "question",
+ memoryRefs: ["mem-device-resolution", "mem-rocm-acceptance"],
+ message:
+ () =>
+ "You are lead for validation. Look back at memory before challenging the plan: what can still fail later?",
+ },
+ {
+ offsetMs: 16_500,
+ agent: "Build Runner",
+ toAgent: "Migration Planner",
+ role: "Skeptical validator",
+ task: "Build and smoke tests",
+ leadAgent: "Build Runner",
+ kind: "challenge",
+ replyToOffsetMs: 15_200,
+ memoryRefs: ["mem-rocm-acceptance"],
+ message:
+ () =>
+ "I checked the memory. Device resolution is covered, but the Docker path can still fail. A CUDA image with ROCm notes is still a deployment trap.",
+ },
+ {
+ offsetMs: 17_800,
+ agent: "Migration Planner",
+ toAgent: "Build Runner",
+ role: "Patch strategist",
+ task: "Build and smoke tests",
+ leadAgent: "Build Runner",
+ kind: "answer",
+ replyToOffsetMs: 16_500,
+ message:
+ () =>
+ "Agreed. I will keep Dockerfile.rocm separate and avoid pretending the existing CUDA container is portable.",
+ },
+ {
+ offsetMs: 19_000,
+ agent: "Build Runner",
+ toAgent: "Shared Memory",
+ role: "Skeptical validator",
+ task: "Build and smoke tests",
+ leadAgent: "Build Runner",
+ kind: "memory",
+ replyToOffsetMs: 17_800,
+ memoryRefs: ["mem-container-split"],
+ message:
+ () =>
+ "Memory write: ROCm validation needs a separate container path plus a smoke command, not just migration notes in README.",
+ },
+ {
+ offsetMs: 20_300,
+ agent: "Orchestrator",
+ toAgent: "Benchmark Agent",
+ role: "Run coordinator",
+ task: "MI300X readiness benchmark",
+ leadAgent: "Benchmark Agent",
+ kind: "question",
+ memoryRefs: ["mem-rocm-acceptance", "mem-container-split"],
+ message:
+ () =>
+ "You are lead for measurement. Use the earlier memories and ask what numbers are safe to show before AMD access is connected.",
+ },
+ {
+ offsetMs: 21_500,
+ agent: "Benchmark Agent",
+ toAgent: "Report Agent",
+ role: "Evidence analyst",
+ task: "MI300X readiness benchmark",
+ leadAgent: "Benchmark Agent",
+ kind: "question",
+ replyToOffsetMs: 20_300,
+ memoryRefs: ["mem-rocm-acceptance", "mem-container-split"],
+ message:
+ () =>
+ "I can show estimated tokens/sec, p95 latency, memory, and bootability. How should I label estimates so the story stays credible?",
+ },
+ {
+ offsetMs: 22_700,
+ agent: "Report Agent",
+ toAgent: "Benchmark Agent",
+ role: "Submission narrator",
+ task: "MI300X readiness benchmark",
+ leadAgent: "Benchmark Agent",
+ kind: "answer",
+ replyToOffsetMs: 21_500,
+ message:
+ () =>
+ "Label estimates as a static ROCmPilot profile and reserve final proof for AMD Developer Cloud logs. Judges trust explicit provenance.",
+ },
+ {
+ offsetMs: 24_000,
+ agent: "Benchmark Agent",
+ toAgent: "Shared Memory",
+ role: "Evidence analyst",
+ task: "MI300X readiness benchmark",
+ leadAgent: "Benchmark Agent",
+ kind: "memory",
+ replyToOffsetMs: 22_700,
+ memoryRefs: ["mem-metric-provenance"],
+ message:
+ () =>
+ "Memory write: estimated metrics are acceptable only when labeled with provenance and paired with the exact AMD proof still needed.",
+ },
+ {
+ offsetMs: 25_200,
+ agent: "Orchestrator",
+ toAgent: "Report Agent",
+ role: "Run coordinator",
+ task: "Judge-ready report",
+ leadAgent: "Report Agent",
+ kind: "question",
+ memoryRefs: ["mem-device-resolution", "mem-container-split", "mem-metric-provenance"],
+ message:
+ () =>
+ "You are lead for the final report. Read the shared memory first, then ask if anyone disagrees with the submission story.",
+ },
+ {
+ offsetMs: 26_000,
+ agent: "Report Agent",
+ toAgent: "All agents",
+ role: "Submission narrator",
+ task: "Judge-ready report",
+ leadAgent: "Report Agent",
+ kind: "question",
+ replyToOffsetMs: 25_200,
+ memoryRefs: ["mem-device-resolution", "mem-container-split", "mem-metric-provenance"],
+ message:
+ () =>
+ "I read the memories. Final story: find CUDA locks, create ROCm patch path, validate container/smoke tests, show metric provenance. Any objections?",
+ },
+ {
+ offsetMs: 26_800,
+ agent: "Build Runner",
+ toAgent: "Report Agent",
+ role: "Skeptical validator",
+ task: "Judge-ready report",
+ leadAgent: "Report Agent",
+ kind: "answer",
+ replyToOffsetMs: 26_000,
+ memoryRefs: ["mem-container-split"],
+ message:
+ () =>
+ "No objection if the report says validation is complete for the demo path and pending for live MI300X logs.",
+ },
+ {
+ offsetMs: 27_400,
+ agent: "Benchmark Agent",
+ toAgent: "Report Agent",
+ role: "Evidence analyst",
+ task: "Judge-ready report",
+ leadAgent: "Report Agent",
+ kind: "answer",
+ replyToOffsetMs: 26_000,
+ memoryRefs: ["mem-metric-provenance"],
+ message:
+ () =>
+ "No objection if every metric names its source. I want the next live run to overwrite estimates with AMD SMI and vLLM logs.",
+ },
+ {
+ offsetMs: 28_200,
+ agent: "Orchestrator",
+ toAgent: "All agents",
+ role: "Run coordinator",
+ task: "Judge-ready report",
+ leadAgent: "Report Agent",
+ kind: "consensus",
+ replyToOffsetMs: 27_400,
+ memoryRefs: ["mem-device-resolution", "mem-container-split", "mem-metric-provenance"],
+ message:
+ () =>
+ "Consensus stored: ship a ROCm readiness report, patch previews, shared discussion memory, and an AMD/vLLM validation checklist. Next run should replace estimates with MI300X logs.",
+ },
+];
+
+type MemoryBlueprint = {
+ offsetMs: number;
+ id: string;
+ title: string;
+ scope: string;
+ learnedFromAgent: string;
+ summary: (context: { findings: Finding[]; patches: PatchPreview[] }) => string;
+ solution: string;
+};
+
+const WAR_ROOM_MEMORY: MemoryBlueprint[] = [
+ {
+ offsetMs: 8_200,
+ id: "mem-device-resolution",
+ title: "Device resolution pattern",
+ scope: "Runtime compatibility",
+ learnedFromAgent: "Repo Doctor",
+ summary: ({ findings }) =>
+ findings[0]
+ ? `${findings[0].category} should be handled once in a resolver instead of repeated across inference code.`
+ : "GPU backend detection should be handled once in a resolver instead of repeated across inference code.",
+ solution: "Create a backend-aware resolver that accepts HIP-backed PyTorch as CUDA-compatible and records backend provenance.",
+ },
+ {
+ offsetMs: 13_500,
+ id: "mem-rocm-acceptance",
+ title: "ROCm acceptance checks",
+ scope: "Build validation",
+ learnedFromAgent: "Build Runner",
+ summary: () =>
+ "A patch is not enough unless the run proves import, backend detection, vLLM health, and provenance logging.",
+ solution: "Use a smoke command that imports torch, reports torch.version.hip, starts vLLM, and hits the OpenAI-compatible health path.",
+ },
+ {
+ offsetMs: 19_000,
+ id: "mem-container-split",
+ title: "Separate ROCm container path",
+ scope: "Deployment safety",
+ learnedFromAgent: "Build Runner",
+ summary: () =>
+ "A CUDA image with ROCm comments still leaves teams with a deployment trap.",
+ solution: "Keep Dockerfile.rocm and ROCm launch scripts separate, with CUDA preserved only as an optional backend.",
+ },
+ {
+ offsetMs: 24_000,
+ id: "mem-metric-provenance",
+ title: "Metric provenance rule",
+ scope: "Benchmark evidence",
+ learnedFromAgent: "Benchmark Agent",
+ summary: () =>
+ "Estimated benchmark cards are useful for the MVP only when their source is explicit.",
+ solution: "Label estimates as static ROCmPilot profiles and replace them with AMD SMI plus vLLM logs when MI300X access is available.",
+ },
+];
+
+export type RunRecord = {
+ id: string;
+ sampleId: string;
+ mode: RunMode;
+ startedAt: number;
+ targetType: "sample" | "github";
+ repoUrl?: string;
+};
+
+export function getSample(sampleId: string | undefined) {
+ return SAMPLE_REPOS.find((sample) => sample.id === sampleId) ?? SAMPLE_REPOS[0];
+}
+
+function toBase64Url(value: string) {
+ return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
+}
+
+function fromBase64Url(value: string) {
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
+ return atob(padded);
+}
+
+function buildGitHubSample(repoUrl: string): SampleRepo {
+ const parsed = parseGitHubRepoUrl(repoUrl);
+
+ return {
+ id: "github-repo",
+ name: parsed?.label ?? "Public GitHub Repository",
+ repoUrl,
+ stack: "Detected from public GitHub files",
+ model: "Detected workload",
+ description: "A public repository scanned live by ROCmPilot.",
+ risk: "ROCm compatibility depends on detected CUDA/NVIDIA assumptions and live AMD validation.",
+ };
+}
+
+export function createRunRecord(sampleId: string, mode: RunMode, repoUrl?: string): RunRecord {
+ const startedAt = Date.now();
+ const safeSampleId = getSample(sampleId).id;
+ const nonce = Math.random().toString(36).slice(2, 8);
+ const parsedRepo = isRealGitHubRepoUrl(repoUrl) ? parseGitHubRepoUrl(repoUrl) : null;
+ const targetType = parsedRepo ? "github" : "sample";
+ const payload = parsedRepo ? toBase64Url(parsedRepo.repoUrl) : safeSampleId;
+
+ return {
+ id: `run.${startedAt.toString(36)}.${mode}.${targetType}.${payload}.${nonce}`,
+ sampleId: safeSampleId,
+ mode,
+ startedAt,
+ targetType,
+ repoUrl: parsedRepo?.repoUrl,
+ };
+}
+
+export function parseRunRecord(runId: string): RunRecord | null {
+ const parts = runId.split(".");
+
+ if (parts.length !== 6 || parts[0] !== "run") {
+ return null;
+ }
+
+ const [, startedAtBase36, mode, targetType, payload] = parts;
+ const startedAt = Number.parseInt(startedAtBase36, 36);
+
+ if (
+ !Number.isFinite(startedAt) ||
+ (mode !== "mock" && mode !== "amd") ||
+ (targetType !== "sample" && targetType !== "github")
+ ) {
+ return null;
+ }
+
+ if (targetType === "github") {
+ const repoUrl = fromBase64Url(payload);
+
+ if (!isRealGitHubRepoUrl(repoUrl)) {
+ return null;
+ }
+
+ return {
+ id: runId,
+ sampleId: "qwen-vllm-cuda",
+ mode,
+ startedAt,
+ targetType,
+ repoUrl,
+ };
+ }
+
+ const sample = SAMPLE_REPOS.find((candidate) => candidate.id === payload);
+
+ if (!sample) {
+ return null;
+ }
+
+ return {
+ id: runId,
+ sampleId: sample.id,
+ mode,
+ startedAt,
+ targetType,
+ };
+}
+
+export function getModelStatus(source: GpuModelStatus["source"] = "fallback"): GpuModelStatus {
+ const endpoint = process.env.AMD_QWEN_BASE_URL?.replace(/\/$/, "");
+ const model = process.env.AMD_QWEN_MODEL ?? "Qwen/Qwen3-Coder-Next";
+ const hfModel = process.env.HF_REPORT_MODEL ?? "Qwen/Qwen2.5-Coder-7B-Instruct";
+
+ if (endpoint && source === "amd-vllm") {
+ return {
+ status: "connected",
+ label: "AMD GPU Model: Connected",
+ model,
+ endpoint,
+ detail: "Report generation used the configured ROCm/vLLM OpenAI-compatible endpoint.",
+ source,
+ };
+ }
+
+ if (source === "hf-router") {
+ return {
+ status: "connected",
+ label: "HF Router: Connected",
+ model: hfModel,
+ endpoint: "https://router.huggingface.co/v1",
+ detail: "Report generation used Hugging Face Inference Providers as the temporary model backend.",
+ source,
+ };
+ }
+
+ if (endpoint) {
+ return {
+ status: "not-configured",
+ label: "AMD GPU Model: Endpoint configured",
+ model,
+ endpoint,
+ detail: "Endpoint is configured; report generation will attempt AMD-hosted Qwen first.",
+ source,
+ };
+ }
+
+ if (process.env.HF_TOKEN) {
+ return {
+ status: "not-configured",
+ label: "HF Router: Available",
+ model: hfModel,
+ endpoint: "https://router.huggingface.co/v1",
+ detail: "Hugging Face token is configured; final report generation will use HF unless AMD is configured.",
+ source: "hf-router",
+ };
+ }
+
+ return {
+ status: "fallback",
+ label: "AMD GPU Model: Demo fallback",
+ model,
+ endpoint: "Set AMD_QWEN_BASE_URL to enable live ROCm/vLLM inference",
+ detail: "The dashboard is using deterministic fallback output so the MVP demo remains reliable.",
+ source: "fallback",
+ };
+}
+
+export function getLongContextMemoryStatus(
+ runId: string,
+ storedItems = 0,
+ recalledItems = 0,
+ source: LongContextMemoryStatus["status"] = process.env.SYNAP_API_KEY ? "configured" : "fallback"
+): LongContextMemoryStatus {
+ const conversationId = buildMemoryConversationId(runId);
+ const customerId = process.env.SYNAP_CUSTOMER_ID ?? DEFAULT_MEMORY_CUSTOMER_ID;
+ const userId = process.env.SYNAP_USER_ID ?? DEFAULT_MEMORY_USER_ID;
+
+ if (source === "connected") {
+ return {
+ status: "connected",
+ label: "Synap Memory: Connected",
+ provider: "synap",
+ conversationId,
+ scope: `${customerId}/${userId}`,
+ detail: "Report Agent stored this run and retrieved scoped long-context memory from Synap.",
+ storedItems,
+ recalledItems,
+ };
+ }
+
+ if (source === "configured") {
+ return {
+ status: "configured",
+ label: "Synap Memory: Ready",
+ provider: "synap",
+ conversationId,
+ scope: `${customerId}/${userId}`,
+ detail: "Synap is configured; the Report Agent will ingest the war-room transcript during report generation.",
+ storedItems,
+ recalledItems,
+ };
+ }
+
+ if (source === "not-configured") {
+ return {
+ status: "not-configured",
+ label: "Synap Memory: Setup needed",
+ provider: "synap",
+ conversationId,
+ scope: `${customerId}/${userId}`,
+ detail: "Set SYNAP_API_KEY and run the Synap JS runtime setup to enable persistent memory.",
+ storedItems,
+ recalledItems,
+ };
+ }
+
+ return {
+ status: "fallback",
+ label: "Synap Memory: Local fallback",
+ provider: "local",
+ conversationId,
+ scope: "current stateless run",
+ detail: "Using reconstructed run memory now; Synap can persist it across sessions once credentials are configured.",
+ storedItems,
+ recalledItems,
+ };
+}
+
+function buildTarget(record: RunRecord, analysis?: RepoAnalysis): RunTarget {
+ if (record.targetType === "github" && record.repoUrl) {
+ const parsed = parseGitHubRepoUrl(record.repoUrl);
+
+ return {
+ type: "github",
+ repoUrl: record.repoUrl,
+ label: analysis?.label ?? parsed?.label ?? "GitHub repository",
+ branch: analysis?.branch ?? parsed?.branch,
+ scanStatus: analysis?.status ?? "pending",
+ scannedFiles: analysis?.scannedFiles ?? 0,
+ note:
+ analysis?.note ??
+ "ROCmPilot will fetch public GitHub files during the Repo Doctor stage.",
+ };
+ }
+
+ const sample = getSample(record.sampleId);
+
+ return {
+ type: "sample",
+ repoUrl: sample.repoUrl,
+ label: sample.name,
+ scanStatus: "fixture",
+ scannedFiles: 4,
+ note: "Using curated sample fixtures for a reliable demo run.",
+ };
+}
+
+function buildMessageId(record: RunRecord, offsetMs: number, agent: string) {
+ return `${record.id}.${offsetMs}.${agent.toLowerCase().replace(/\W+/g, "-")}`;
+}
+
+function buildAgentMessages(
+ elapsed: number,
+ record: RunRecord,
+ target: RunTarget,
+ sample: SampleRepo,
+ findings: Finding[],
+ patches: PatchPreview[]
+): AgentMessage[] {
+ return WAR_ROOM_MESSAGES.filter((blueprint) => elapsed >= blueprint.offsetMs).map((blueprint) => ({
+ id: buildMessageId(record, blueprint.offsetMs, blueprint.agent),
+ agent: blueprint.agent,
+ toAgent: blueprint.toAgent,
+ role: blueprint.role,
+ task: blueprint.task,
+ leadAgent: blueprint.leadAgent,
+ kind: blueprint.kind,
+ message: blueprint.message({ target, sample, findings, patches }),
+ replyToId:
+ blueprint.replyToOffsetMs === undefined
+ ? undefined
+ : WAR_ROOM_MESSAGES.find((message) => message.offsetMs === blueprint.replyToOffsetMs)
+ ? buildMessageId(
+ record,
+ blueprint.replyToOffsetMs,
+ WAR_ROOM_MESSAGES.find((message) => message.offsetMs === blueprint.replyToOffsetMs)?.agent ?? "unknown"
+ )
+ : undefined,
+ memoryRefs: blueprint.memoryRefs ?? [],
+ createdAt: new Date(record.startedAt + blueprint.offsetMs).toISOString(),
+ }));
+}
+
+function buildAgentMemory(
+ elapsed: number,
+ record: RunRecord,
+ messages: AgentMessage[],
+ findings: Finding[],
+ patches: PatchPreview[]
+): AgentMemory[] {
+ return WAR_ROOM_MEMORY.filter((memory) => elapsed >= memory.offsetMs).map((memory) => ({
+ id: memory.id,
+ title: memory.title,
+ scope: memory.scope,
+ learnedFromAgent: memory.learnedFromAgent,
+ summary: memory.summary({ findings, patches }),
+ solution: memory.solution,
+ createdAt: new Date(record.startedAt + memory.offsetMs).toISOString(),
+ usedBy: messages
+ .filter((message) => message.memoryRefs.includes(memory.id) && new Date(message.createdAt).getTime() > record.startedAt + memory.offsetMs)
+ .map((message) => message.agent),
+ }));
+}
+
+export function snapshotRun(record: RunRecord, analysis?: RepoAnalysis): RocmRun {
+ const elapsed = Math.max(0, Date.now() - record.startedAt);
+ const sample =
+ record.targetType === "github" && record.repoUrl
+ ? buildGitHubSample(record.repoUrl)
+ : getSample(record.sampleId);
+ const status: RunStatus = elapsed >= TOTAL_DURATION_MS ? "completed" : "running";
+ const target = buildTarget(record, analysis);
+ let cursor = 0;
+
+ const stages = STAGES.map((stage) => {
+ const stageStart = cursor;
+ const stageEnd = cursor + stage.durationMs;
+ cursor = stageEnd;
+
+ const stageElapsed = elapsed - stageStart;
+ const progress = Math.max(0, Math.min(100, Math.round((stageElapsed / stage.durationMs) * 100)));
+ const stageStatus: StageStatus =
+ progress >= 100 ? "completed" : progress > 0 ? "running" : "pending";
+
+ return {
+ id: stage.id,
+ agent: stage.agent,
+ title: stage.title,
+ description: stage.description,
+ status: stageStatus,
+ progress,
+ startedAt: stageElapsed > 0 ? new Date(record.startedAt + stageStart).toISOString() : undefined,
+ completedAt: stageStatus === "completed" ? new Date(record.startedAt + stageEnd).toISOString() : undefined,
+ };
+ });
+
+ const allFindings = analysis?.findings.length ? analysis.findings : FINDINGS;
+ const allPatches = analysis?.patches.length ? analysis.patches : PATCHES;
+ const allBenchmarks = record.targetType === "github"
+ ? BENCHMARKS.map((benchmark) => ({
+ ...benchmark,
+ costNote: benchmark.costNote.replace("demo profile", "static ROCmPilot profile until live AMD validation"),
+ }))
+ : BENCHMARKS;
+ const allLogs =
+ record.targetType === "github"
+ ? [
+ `queued public GitHub scan for ${target.label}`,
+ ...(analysis?.logs ?? ["repo-doctor: waiting for GitHub scan results"]),
+ "build-runner: generated ROCm validation plan without mutating repository files",
+ "benchmark-agent: prepared estimated MI300X profile pending live AMD run",
+ "report-agent: preparing technical and business summary",
+ ]
+ : LOGS;
+
+ const visibleFindings =
+ elapsed > 4_000
+ ? allFindings.slice(0, Math.min(allFindings.length, Math.ceil((elapsed - 4_000) / 3_000)))
+ : [];
+ const visiblePatches =
+ elapsed > 10_000
+ ? allPatches.slice(0, Math.min(allPatches.length, Math.ceil((elapsed - 10_000) / 4_000)))
+ : [];
+ const visibleBenchmarks = elapsed > 18_000 ? allBenchmarks : allBenchmarks.slice(0, 1);
+ const visibleLogs = allLogs.slice(0, Math.min(allLogs.length, Math.max(1, Math.ceil(elapsed / 2_300))));
+ const agentMessages = buildAgentMessages(
+ elapsed,
+ record,
+ target,
+ sample,
+ visibleFindings.length ? visibleFindings : allFindings,
+ visiblePatches.length ? visiblePatches : allPatches
+ );
+ const agentMemory = buildAgentMemory(
+ elapsed,
+ record,
+ agentMessages,
+ visibleFindings.length ? visibleFindings : allFindings,
+ visiblePatches.length ? visiblePatches : allPatches
+ );
+
+ return {
+ id: record.id,
+ sample,
+ target,
+ mode: record.mode,
+ status,
+ progress: Math.min(100, Math.round((elapsed / TOTAL_DURATION_MS) * 100)),
+ startedAt: new Date(record.startedAt).toISOString(),
+ completedAt: status === "completed" ? new Date(record.startedAt + TOTAL_DURATION_MS).toISOString() : undefined,
+ stages,
+ findings: visibleFindings,
+ patches: visiblePatches,
+ logs: visibleLogs,
+ agentMessages,
+ agentMemory,
+ longContextMemory: getLongContextMemoryStatus(
+ record.id,
+ agentMemory.length,
+ agentMessages.filter((message) => message.memoryRefs.length > 0).length
+ ),
+ benchmarks: visibleBenchmarks,
+ modelStatus: getModelStatus(),
+ };
+}
+
+export function buildFallbackReport(run: RocmRun, longContext?: string) {
+ const findingList = run.findings
+ .map((finding) => `- **${finding.category}** in \`${finding.file}:${finding.line}\`: ${finding.recommendedFix}`)
+ .join("\n");
+ const memoryList = run.agentMemory
+ .map((memory) => `- **${memory.title}**: ${memory.solution}`)
+ .join("\n");
+
+ return `# ROCmPilot Migration Report
+
+## Executive Summary
+
+ROCmPilot completed a multi-agent audit for **${run.sample.name}** and produced an AMD ROCm migration path for a PyTorch/vLLM workload. The system found CUDA-only assumptions, generated ROCm patch previews, and prepared the project for validation on AMD Developer Cloud.
+
+## Agent Findings
+
+${findingList || "- Findings are still being prepared."}
+
+## Shared Agent Memory
+
+${memoryList || "- Shared memory is still being written by the agents."}
+
+## Long-Context Memory
+
+${longContext || "- Synap memory was not available for this report, so ROCmPilot used the current run's reconstructed local memory."}
+
+## AMD GPU Usage
+
+- Primary model target: **Qwen/Qwen3-Coder-Next**
+- Serving path: **ROCm + vLLM OpenAI-compatible endpoint**
+- GPU goal: run the Migration Planner or Report Agent on AMD Instinct MI300X
+- MVP fallback: deterministic report generation when the endpoint is unavailable
+
+## Business Value
+
+ROCmPilot reduces the time needed to move inference services away from NVIDIA-only assumptions. Teams get a migration checklist, patch previews, benchmark evidence, and a report they can hand to infra leads before spending engineering time on a full port.
+
+## Next Step
+
+Connect \`AMD_QWEN_BASE_URL\` to a live ROCm/vLLM endpoint and rerun the report stage to replace demo metrics with captured MI300X evidence.`;
+}
+
+export function buildReportPrompt(run: RocmRun, longContext?: string) {
+ return `Create a concise hackathon submission report for ROCmPilot.
+
+Product: multi-agent ROCm migration dashboard.
+Track: AI Agents & Agentic Workflows.
+Sample repo: ${run.sample.name} (${run.sample.stack}).
+Target: ${run.target.label} (${run.target.repoUrl}).
+Scan status: ${run.target.scanStatus}, scanned files: ${run.target.scannedFiles}.
+GPU story: Qwen3-Coder-Next served on AMD Instinct MI300X with ROCm/vLLM powers the report or migration agent when configured.
+
+Findings:
+${run.findings.map((finding) => `- ${finding.severity}: ${finding.category} in ${finding.file}:${finding.line}. Fix: ${finding.recommendedFix}`).join("\n")}
+
+Patches:
+${run.patches.map((patch) => `- ${patch.file}: ${patch.rationale}`).join("\n")}
+
+Shared memory:
+${run.agentMemory.map((memory) => `- ${memory.title}: ${memory.solution}`).join("\n")}
+
+Long-context memory from Synap or fallback memory:
+${longContext || "- No long-context memory was available beyond the current run."}
+
+Benchmarks:
+${run.benchmarks.map((benchmark) => `- ${benchmark.label}: ${benchmark.backend}, ${benchmark.tokensPerSecond} tok/s, p95 ${benchmark.p95LatencyMs}ms, ${benchmark.memoryGb}GB.`).join("\n")}
+
+Write markdown with these sections only: Executive Summary, Agent Workflow, AMD GPU Proof, Business Value, Next 48 Hours.`;
+}
diff --git a/src/lib/rocmpilot/github-scanner.ts b/src/lib/rocmpilot/github-scanner.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d75f1ec8438186bfdd24708c160850e5493bc388
--- /dev/null
+++ b/src/lib/rocmpilot/github-scanner.ts
@@ -0,0 +1,462 @@
+import type { Finding, PatchPreview } from "./types";
+import { parseGitHubRepoUrl } from "./github-url";
+
+type GitHubTreeItem = {
+ path: string;
+ mode: string;
+ type: "blob" | "tree";
+ sha: string;
+ size?: number;
+ url: string;
+};
+
+type GitHubTreeResponse = {
+ tree?: GitHubTreeItem[];
+ truncated?: boolean;
+};
+
+type GitHubRepoResponse = {
+ default_branch?: string;
+};
+
+type GitHubBlobResponse = {
+ content?: string;
+ encoding?: string;
+};
+
+type ScannedFile = {
+ path: string;
+ content: string;
+};
+
+export type RepoAnalysis = {
+ status: "scanned" | "failed";
+ label: string;
+ repoUrl: string;
+ branch?: string;
+ scannedFiles: number;
+ findings: Finding[];
+ patches: PatchPreview[];
+ logs: string[];
+ stack: string;
+ note: string;
+};
+
+const globalForGitHubScan = globalThis as unknown as {
+ rocmPilotScanCache?: Map;
+};
+
+const scanCache = globalForGitHubScan.rocmPilotScanCache ?? new Map();
+globalForGitHubScan.rocmPilotScanCache = scanCache;
+
+function githubHeaders() {
+ const headers: Record = {
+ Accept: "application/vnd.github+json",
+ "User-Agent": "ROCmPilot",
+ "X-GitHub-Api-Version": "2022-11-28",
+ };
+
+ if (process.env.GITHUB_TOKEN) {
+ headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
+ }
+
+ return headers;
+}
+
+async function fetchGitHubJson(url: string): Promise {
+ const response = await fetch(url, {
+ headers: githubHeaders(),
+ next: { revalidate: 180 },
+ });
+
+ if (!response.ok) {
+ throw new Error(`GitHub returned ${response.status} for ${url}`);
+ }
+
+ return response.json() as Promise;
+}
+
+function isRelevantPath(path: string) {
+ const lower = path.toLowerCase();
+
+ if (
+ lower.includes("node_modules/") ||
+ lower.includes(".git/") ||
+ lower.includes("dist/") ||
+ lower.includes("build/") ||
+ lower.includes(".next/")
+ ) {
+ return false;
+ }
+
+ return (
+ /^dockerfile/i.test(path) ||
+ lower.endsWith("docker-compose.yml") ||
+ lower.endsWith("docker-compose.yaml") ||
+ lower.endsWith("requirements.txt") ||
+ lower.endsWith("requirements-rocm.txt") ||
+ lower.endsWith("pyproject.toml") ||
+ lower.endsWith("environment.yml") ||
+ lower.endsWith("environment.yaml") ||
+ lower.endsWith(".py") ||
+ lower.endsWith(".sh") ||
+ lower.endsWith(".yaml") ||
+ lower.endsWith(".yml") ||
+ lower.includes("vllm") ||
+ lower.includes("inference") ||
+ lower.includes("serve") ||
+ lower.includes("benchmark")
+ );
+}
+
+function firstLineOf(content: string, pattern: RegExp) {
+ const lines = content.split(/\r?\n/);
+ const index = lines.findIndex((line) => pattern.test(line));
+ return index >= 0 ? index + 1 : 1;
+}
+
+function addFinding(
+ findings: Finding[],
+ finding: Omit,
+ idSeed: string
+) {
+ if (findings.length >= 10) {
+ return;
+ }
+
+ const id = `${idSeed}-${findings.length + 1}`.replace(/[^a-z0-9-]/gi, "-").toLowerCase();
+
+ if (!findings.some((existing) => existing.file === finding.file && existing.category === finding.category)) {
+ findings.push({ id, ...finding });
+ }
+}
+
+function detectFindings(files: ScannedFile[]) {
+ const findings: Finding[] = [];
+
+ for (const file of files) {
+ const lowerPath = file.path.toLowerCase();
+ const content = file.content;
+
+ if (/nvidia\/cuda|nvidia-container|--gpus\s+all|nvidia-smi/i.test(content)) {
+ addFinding(
+ findings,
+ {
+ severity: "high",
+ category: "NVIDIA container/runtime assumption",
+ file: file.path,
+ line: firstLineOf(content, /nvidia\/cuda|nvidia-container|--gpus\s+all|nvidia-smi/i),
+ explanation:
+ "The repository includes NVIDIA-specific container/runtime configuration, which will not run cleanly on AMD ROCm infrastructure.",
+ recommendedFix:
+ "Add an AMD ROCm runtime path using a ROCm/vLLM image and keep NVIDIA launch flags behind a backend-specific profile.",
+ },
+ file.path
+ );
+ }
+
+ if (/torch\.device\(\s*["']cuda["']\s*\)|\.cuda\(|\.cuda\(\)|device_map\s*=\s*["']cuda["']/i.test(content)) {
+ addFinding(
+ findings,
+ {
+ severity: "critical",
+ category: "Hardcoded CUDA device path",
+ file: file.path,
+ line: firstLineOf(content, /torch\.device\(\s*["']cuda["']\s*\)|\.cuda\(|\.cuda\(\)|device_map\s*=\s*["']cuda["']/i),
+ explanation:
+ "The code moves models/tensors directly to CUDA, so the workload needs a backend-aware device resolver before AMD validation.",
+ recommendedFix:
+ "Introduce a resolver that treats HIP-backed torch.cuda availability as ROCm and records backend provenance in logs/metrics.",
+ },
+ file.path
+ );
+ }
+
+ if (/torch\.cuda|cuda_visible_devices|hip_visible_devices/i.test(content)) {
+ addFinding(
+ findings,
+ {
+ severity: "medium",
+ category: "GPU backend detection needs abstraction",
+ file: file.path,
+ line: firstLineOf(content, /torch\.cuda|cuda_visible_devices|hip_visible_devices/i),
+ explanation:
+ "The repo checks GPU availability through vendor-specific environment or PyTorch CUDA APIs without documenting AMD behavior.",
+ recommendedFix:
+ "Centralize backend detection and expose CUDA, ROCm, and CPU as explicit runtime modes.",
+ },
+ file.path
+ );
+ }
+
+ if (/cu12|cu118|cu121|nvidia-|cupy-cuda|bitsandbytes|flash-attn|xformers/i.test(content)) {
+ addFinding(
+ findings,
+ {
+ severity: "high",
+ category: "CUDA-oriented dependency",
+ file: file.path,
+ line: firstLineOf(content, /cu12|cu118|cu121|nvidia-|cupy-cuda|bitsandbytes|flash-attn|xformers/i),
+ explanation:
+ "One or more dependencies are pinned to CUDA/NVIDIA builds, which can block ROCm package resolution.",
+ recommendedFix:
+ "Create a ROCm requirements profile and verify PyTorch/vLLM wheels against the target ROCm version.",
+ },
+ file.path
+ );
+ }
+
+ if (/vllm/i.test(content) && /tensor-parallel-size|max-model-len|served-model-name/i.test(content) === false) {
+ addFinding(
+ findings,
+ {
+ severity: "low",
+ category: "vLLM serving defaults need AMD profile",
+ file: file.path,
+ line: firstLineOf(content, /vllm/i),
+ explanation:
+ "vLLM is present, but the repo does not expose the serving knobs that matter when moving to MI300X validation.",
+ recommendedFix:
+ "Add a backend-aware vLLM launch script with model length, tensor parallelism, and metrics capture settings.",
+ },
+ file.path
+ );
+ }
+
+ if (lowerPath.includes("benchmark") && /tokens|latency|memory|throughput/i.test(content) === false) {
+ addFinding(
+ findings,
+ {
+ severity: "medium",
+ category: "Benchmark evidence incomplete",
+ file: file.path,
+ line: 1,
+ explanation:
+ "The benchmark file exists but does not obviously capture tokens/sec, latency, memory, and backend metadata.",
+ recommendedFix:
+ "Add a ROCm benchmark profile that emits AMD SMI/vLLM metrics for the final migration report.",
+ },
+ file.path
+ );
+ }
+ }
+
+ if (findings.length === 0) {
+ findings.push({
+ id: "no-direct-cuda-blockers",
+ severity: "low",
+ category: "No direct CUDA blockers in scanned files",
+ file: "repository",
+ line: 1,
+ explanation:
+ "ROCmPilot did not find obvious CUDA-only strings in the scanned files, but the workload still needs a live AMD smoke test.",
+ recommendedFix:
+ "Run the generated ROCm validation script on AMD Developer Cloud and attach the benchmark evidence to the report.",
+ });
+ }
+
+ return findings;
+}
+
+function buildPatchPreviews(findings: Finding[]): PatchPreview[] {
+ const hasDocker = findings.some((finding) => finding.category.includes("container"));
+ const hasDevice = findings.some((finding) => finding.category.includes("CUDA device") || finding.category.includes("backend"));
+ const hasDeps = findings.some((finding) => finding.category.includes("dependency"));
+ const patches: PatchPreview[] = [];
+
+ if (hasDevice) {
+ patches.push({
+ id: "device-resolver",
+ file: "src/rocmpilot_device.py",
+ rationale:
+ "Adds a reusable runtime resolver so the project can run on CUDA, ROCm-backed PyTorch, or CPU without hardcoded model code.",
+ diff: `+import torch
++
++def resolve_accelerator() -> tuple[str, str]:
++ if torch.cuda.is_available():
++ backend = "rocm" if getattr(torch.version, "hip", None) else "cuda"
++ return "cuda", backend
++ return "cpu", "cpu"
++
++DEVICE, GPU_BACKEND = resolve_accelerator()
++print(f"ROCmPilot backend={GPU_BACKEND} device={DEVICE}")
+`,
+ });
+ }
+
+ if (hasDocker) {
+ patches.push({
+ id: "dockerfile-rocm",
+ file: "Dockerfile.rocm",
+ rationale:
+ "Creates an AMD-specific runtime container while preserving the original repository for existing CUDA deployments.",
+ diff: `+FROM rocm/vllm:latest
++
++WORKDIR /workspace
++COPY . .
++ENV HIP_VISIBLE_DEVICES=0
++ENV VLLM_USE_ROCM=1
++RUN pip install --no-cache-dir -r requirements-rocm.txt
++CMD ["bash", "scripts/serve-rocm.sh"]
+`,
+ });
+ }
+
+ if (hasDeps) {
+ patches.push({
+ id: "requirements-rocm",
+ file: "requirements-rocm.txt",
+ rationale:
+ "Separates ROCm dependencies from CUDA pins so CI and AMD Developer Cloud validation can install a clean environment.",
+ diff: `+torch
++transformers
++accelerate
++vllm
++sentencepiece
++# Verify exact ROCm-compatible wheel versions in AMD Developer Cloud before production use.
+`,
+ });
+ }
+
+ patches.push({
+ id: "serve-rocm",
+ file: "scripts/serve-rocm.sh",
+ rationale:
+ "Provides the OpenAI-compatible vLLM endpoint that ROCmPilot can call for the Report Agent on AMD hardware.",
+ diff: `+#!/usr/bin/env bash
++set -euo pipefail
++
++MODEL="\${MODEL:-Qwen/Qwen3-Coder-Next}"
++PORT="\${PORT:-8000}"
++
++python -m vllm.entrypoints.openai.api_server \\
++ --model "$MODEL" \\
++ --host 0.0.0.0 \\
++ --port "$PORT" \\
++ --tensor-parallel-size "\${TENSOR_PARALLEL_SIZE:-1}" \\
++ --max-model-len "\${MAX_MODEL_LEN:-32768}"
+`,
+ });
+
+ return patches.slice(0, 4);
+}
+
+function inferStack(files: ScannedFile[]) {
+ const joined = files.map((file) => `${file.path}\n${file.content.slice(0, 2_000)}`).join("\n").toLowerCase();
+ const stack = new Set();
+
+ if (joined.includes("vllm")) stack.add("vLLM");
+ if (joined.includes("torch")) stack.add("PyTorch");
+ if (joined.includes("transformers")) stack.add("Transformers");
+ if (joined.includes("fastapi")) stack.add("FastAPI");
+ if (joined.includes("dockerfile")) stack.add("Docker");
+ if (joined.includes("langchain")) stack.add("LangChain");
+ if (joined.includes("crewai")) stack.add("CrewAI");
+
+ return stack.size > 0 ? Array.from(stack).join(", ") : "Python/AI workload";
+}
+
+async function fetchRelevantFiles(owner: string, repo: string, ref: string) {
+ const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${encodeURIComponent(ref)}?recursive=1`;
+ const tree = await fetchGitHubJson(treeUrl);
+ const blobs = (tree.tree ?? [])
+ .filter((item) => item.type === "blob" && isRelevantPath(item.path) && (item.size ?? 0) <= 120_000)
+ .slice(0, 32);
+
+ const files: ScannedFile[] = [];
+
+ for (const blob of blobs) {
+ const data = await fetchGitHubJson(
+ `https://api.github.com/repos/${owner}/${repo}/git/blobs/${blob.sha}`
+ );
+
+ if (data.encoding === "base64" && data.content) {
+ files.push({
+ path: blob.path,
+ content: Buffer.from(data.content.replace(/\n/g, ""), "base64").toString("utf8"),
+ });
+ }
+ }
+
+ return files;
+}
+
+export async function analyzeGitHubRepository(repoUrl: string): Promise {
+ const parsed = parseGitHubRepoUrl(repoUrl);
+
+ if (!parsed) {
+ return failedAnalysis(repoUrl, "Invalid GitHub URL.");
+ }
+
+ const cacheKey = parsed.repoUrl;
+ const cached = scanCache.get(cacheKey);
+
+ if (cached && cached.expiresAt > Date.now()) {
+ return cached.analysis;
+ }
+
+ try {
+ const repo = await fetchGitHubJson(
+ `https://api.github.com/repos/${parsed.owner}/${parsed.repo}`
+ );
+ const ref = parsed.branch ?? repo.default_branch ?? "main";
+ const files = await fetchRelevantFiles(parsed.owner, parsed.repo, ref);
+ const findings = detectFindings(files);
+ const patches = buildPatchPreviews(findings);
+ const stack = inferStack(files);
+ const analysis: RepoAnalysis = {
+ status: "scanned",
+ label: parsed.label,
+ repoUrl: parsed.repoUrl,
+ branch: ref,
+ scannedFiles: files.length,
+ findings,
+ patches,
+ stack,
+ note: `Scanned ${files.length} public GitHub files from ${parsed.label}@${ref}.`,
+ logs: [
+ `github-scan: resolved ${parsed.label}@${ref}`,
+ `github-scan: selected ${files.length} relevant files for ROCm analysis`,
+ `repo-doctor: detected stack profile: ${stack}`,
+ `migration-planner: produced ${findings.length} findings and ${patches.length} patch previews`,
+ ],
+ };
+
+ scanCache.set(cacheKey, { analysis, expiresAt: Date.now() + 180_000 });
+ return analysis;
+ } catch (error) {
+ return failedAnalysis(
+ parsed.repoUrl,
+ error instanceof Error ? error.message : "Unknown GitHub scan failure.",
+ parsed.label,
+ parsed.branch
+ );
+ }
+}
+
+function failedAnalysis(repoUrl: string, message: string, label = "GitHub repository", branch?: string): RepoAnalysis {
+ return {
+ status: "failed",
+ label,
+ repoUrl,
+ branch,
+ scannedFiles: 0,
+ stack: "Public GitHub repository",
+ note: message,
+ findings: [
+ {
+ id: "github-scan-failed",
+ severity: "medium",
+ category: "GitHub scan unavailable",
+ file: "repository",
+ line: 1,
+ explanation:
+ "ROCmPilot could not fetch enough public repository data to complete a live scan. The app remains usable with sample fixtures.",
+ recommendedFix:
+ "Check that the repository is public, add GITHUB_TOKEN for higher API limits, or use the sample workload for the demo.",
+ },
+ ],
+ patches: buildPatchPreviews([]),
+ logs: [`github-scan: ${message}`, "fallback: using safe ROCm migration guidance"],
+ };
+}
diff --git a/src/lib/rocmpilot/github-url.ts b/src/lib/rocmpilot/github-url.ts
new file mode 100644
index 0000000000000000000000000000000000000000..99568568cb979139eb56ac99add7a58d9c1d4614
--- /dev/null
+++ b/src/lib/rocmpilot/github-url.ts
@@ -0,0 +1,47 @@
+export type GitHubRepoRef = {
+ owner: string;
+ repo: string;
+ branch?: string;
+ repoUrl: string;
+ label: string;
+};
+
+export function parseGitHubRepoUrl(input: string | undefined): GitHubRepoRef | null {
+ if (!input) {
+ return null;
+ }
+
+ try {
+ const url = new URL(input.trim());
+
+ if (url.hostname !== "github.com" && url.hostname !== "www.github.com") {
+ return null;
+ }
+
+ const [owner, repoWithSuffix, maybeTree, ...rest] = url.pathname
+ .split("/")
+ .filter(Boolean);
+ const repo = repoWithSuffix?.replace(/\.git$/, "");
+
+ if (!owner || !repo) {
+ return null;
+ }
+
+ const branch = maybeTree === "tree" && rest.length > 0 ? rest.join("/") : undefined;
+
+ return {
+ owner,
+ repo,
+ branch,
+ repoUrl: `https://github.com/${owner}/${repo}${branch ? `/tree/${branch}` : ""}`,
+ label: `${owner}/${repo}`,
+ };
+ } catch {
+ return null;
+ }
+}
+
+export function isRealGitHubRepoUrl(input: string | undefined) {
+ const repo = parseGitHubRepoUrl(input);
+ return Boolean(repo && repo.owner !== "example");
+}
diff --git a/src/lib/rocmpilot/memory-ids.ts b/src/lib/rocmpilot/memory-ids.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fb402d8e3e57101fea32b2ce6bc3aed554d02759
--- /dev/null
+++ b/src/lib/rocmpilot/memory-ids.ts
@@ -0,0 +1,6 @@
+export const DEFAULT_MEMORY_CUSTOMER_ID = "rocmpilot-hackathon";
+export const DEFAULT_MEMORY_USER_ID = "rocmpilot-agent-fleet";
+
+export function buildMemoryConversationId(runId: string) {
+ return `rocmpilot-${runId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 140)}`;
+}
diff --git a/src/lib/rocmpilot/store.ts b/src/lib/rocmpilot/store.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4a2b4f9c1b5e6625dd67cbd8f5370ce972e85697
--- /dev/null
+++ b/src/lib/rocmpilot/store.ts
@@ -0,0 +1,23 @@
+import { createRunRecord, parseRunRecord, snapshotRun } from "./data";
+import { analyzeGitHubRepository } from "./github-scanner";
+import type { RocmRun, RunMode } from "./types";
+
+export function createRun(sampleId: string, mode: RunMode = "mock", repoUrl?: string): RocmRun {
+ const record = createRunRecord(sampleId, mode, repoUrl);
+ return snapshotRun(record);
+}
+
+export async function getRun(runId: string): Promise {
+ const record = parseRunRecord(runId);
+
+ if (!record) {
+ return null;
+ }
+
+ if (record.targetType === "github" && record.repoUrl) {
+ const analysis = await analyzeGitHubRepository(record.repoUrl);
+ return snapshotRun(record, analysis);
+ }
+
+ return snapshotRun(record);
+}
diff --git a/src/lib/rocmpilot/synap-memory.ts b/src/lib/rocmpilot/synap-memory.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6519cb5209ee679299e91661977dbbd5bf4a256a
--- /dev/null
+++ b/src/lib/rocmpilot/synap-memory.ts
@@ -0,0 +1,316 @@
+import type {
+ ChatMessage,
+ ContextForPromptResult,
+ ContextResponse,
+ SynapClient,
+ SynapClientOptions,
+} from "@maximem/synap-js-sdk";
+import {
+ getLongContextMemoryStatus,
+} from "./data";
+import {
+ buildMemoryConversationId,
+ DEFAULT_MEMORY_CUSTOMER_ID,
+ DEFAULT_MEMORY_USER_ID,
+} from "./memory-ids";
+import type { LongContextMemoryStatus, RocmRun } from "./types";
+
+type SynapSyncResult = {
+ status: LongContextMemoryStatus;
+ promptContext: string;
+};
+
+function parseOptionalPort(value: string | undefined) {
+ if (!value) {
+ return undefined;
+ }
+
+ const port = Number.parseInt(value, 10);
+ return Number.isFinite(port) ? port : undefined;
+}
+
+function parseOptionalBoolean(value: string | undefined) {
+ if (value === undefined) {
+ return undefined;
+ }
+
+ return ["1", "true", "yes"].includes(value.toLowerCase());
+}
+
+function getSynapIdentity(run: RocmRun) {
+ return {
+ conversationId: buildMemoryConversationId(run.id),
+ customerId: process.env.SYNAP_CUSTOMER_ID ?? DEFAULT_MEMORY_CUSTOMER_ID,
+ userId: process.env.SYNAP_USER_ID ?? DEFAULT_MEMORY_USER_ID,
+ };
+}
+
+function buildSynapMessages(run: RocmRun): ChatMessage[] {
+ const targetSummary = [
+ `ROCmPilot run ${run.id}`,
+ `Target: ${run.target.label} (${run.target.repoUrl})`,
+ `Scan status: ${run.target.scanStatus}; scanned files: ${run.target.scannedFiles}`,
+ `Goal: migrate PyTorch/vLLM workload toward AMD ROCm readiness.`,
+ ].join("\n");
+
+ const agentDiscussion = run.agentMessages.map((message) => ({
+ role: "assistant" as const,
+ content: [
+ `[${message.agent} -> ${message.toAgent}] ${message.kind.toUpperCase()}`,
+ `Task: ${message.task}`,
+ `Lead: ${message.leadAgent}`,
+ `Message: ${message.message}`,
+ message.memoryRefs.length ? `Memory refs: ${message.memoryRefs.join(", ")}` : "",
+ ]
+ .filter(Boolean)
+ .join("\n"),
+ metadata: {
+ agent: message.agent,
+ toAgent: message.toAgent,
+ kind: message.kind,
+ task: message.task,
+ leadAgent: message.leadAgent,
+ runId: run.id,
+ },
+ }));
+
+ const sharedMemory = run.agentMemory.map((memory) => ({
+ role: "assistant" as const,
+ content: [
+ `Shared memory: ${memory.title}`,
+ `Scope: ${memory.scope}`,
+ `Learned from: ${memory.learnedFromAgent}`,
+ `Summary: ${memory.summary}`,
+ `Reusable solution: ${memory.solution}`,
+ memory.usedBy.length ? `Reused by: ${Array.from(new Set(memory.usedBy)).join(", ")}` : "",
+ ]
+ .filter(Boolean)
+ .join("\n"),
+ metadata: {
+ memoryId: memory.id,
+ scope: memory.scope,
+ learnedFromAgent: memory.learnedFromAgent,
+ runId: run.id,
+ },
+ }));
+
+ return [
+ {
+ role: "user",
+ content: targetSummary,
+ metadata: {
+ runId: run.id,
+ target: run.target.label,
+ repository: run.target.repoUrl,
+ },
+ },
+ ...agentDiscussion,
+ ...sharedMemory,
+ ];
+}
+
+function buildLocalMemoryContext(run: RocmRun) {
+ const memories = run.agentMemory
+ .map((memory) => `- ${memory.title} (${memory.scope}): ${memory.solution}`)
+ .join("\n");
+ const recentDiscussion = run.agentMessages
+ .slice(-8)
+ .map((message) => `- ${message.agent} -> ${message.toAgent}: ${message.message}`)
+ .join("\n");
+
+ return [
+ "Current run memory:",
+ memories || "- No shared memory has been written yet.",
+ "",
+ "Recent agent discussion:",
+ recentDiscussion || "- No agent discussion is available yet.",
+ ].join("\n");
+}
+
+function countContextItems(context: ContextResponse | null) {
+ if (!context) {
+ return 0;
+ }
+
+ return (
+ (context.facts?.length ?? 0) +
+ (context.preferences?.length ?? 0) +
+ (context.episodes?.length ?? 0) +
+ (context.emotions?.length ?? 0) +
+ (context.temporalEvents?.length ?? 0)
+ );
+}
+
+function summarizeContext(
+ context: ContextResponse | null,
+ promptContext: ContextForPromptResult | null
+) {
+ const sections: string[] = [];
+
+ if (promptContext?.formattedContext) {
+ sections.push(`Synap compacted context:\n${promptContext.formattedContext}`);
+ }
+
+ if (context?.facts?.length) {
+ sections.push(
+ `Synap facts:\n${context.facts
+ .slice(0, 5)
+ .map((fact) => `- ${fact.content}`)
+ .join("\n")}`
+ );
+ }
+
+ if (context?.episodes?.length) {
+ sections.push(
+ `Synap episodes:\n${context.episodes
+ .slice(0, 5)
+ .map((episode) => `- ${episode.summary}`)
+ .join("\n")}`
+ );
+ }
+
+ if (context?.preferences?.length) {
+ sections.push(
+ `Synap preferences:\n${context.preferences
+ .slice(0, 5)
+ .map((preference) => `- ${preference.content}`)
+ .join("\n")}`
+ );
+ }
+
+ return sections.join("\n\n");
+}
+
+function synapOptions(): SynapClientOptions {
+ return {
+ apiKey: process.env.SYNAP_API_KEY,
+ instanceId: process.env.SYNAP_INSTANCE_ID,
+ baseUrl: process.env.SYNAP_BASE_URL,
+ grpcHost: process.env.SYNAP_GRPC_HOST,
+ grpcPort: parseOptionalPort(process.env.SYNAP_GRPC_PORT),
+ grpcUseTls: parseOptionalBoolean(process.env.SYNAP_GRPC_TLS),
+ autoSetup: parseOptionalBoolean(process.env.SYNAP_AUTO_SETUP) ?? false,
+ requestTimeoutMs: 10_000,
+ initTimeoutMs: 10_000,
+ ingestTimeoutMs: 10_000,
+ onLog: (level, message) => {
+ if (level === "error") {
+ console.warn(`Synap ${level}: ${message}`);
+ }
+ },
+ } as SynapClientOptions & { instanceId?: string };
+}
+
+async function shutdownClient(client: SynapClient | null) {
+ if (!client) {
+ return;
+ }
+
+ try {
+ await client.shutdown();
+ } catch (error) {
+ console.warn("Synap shutdown warning:", error);
+ }
+}
+
+export async function syncRunMemoryWithSynap(run: RocmRun): Promise {
+ const localContext = buildLocalMemoryContext(run);
+ const identity = getSynapIdentity(run);
+
+ if (!process.env.SYNAP_API_KEY) {
+ return {
+ status: getLongContextMemoryStatus(
+ run.id,
+ run.agentMemory.length,
+ run.agentMessages.filter((message) => message.memoryRefs.length > 0).length,
+ "fallback"
+ ),
+ promptContext: localContext,
+ };
+ }
+
+ let client: SynapClient | null = null;
+
+ try {
+ const { createClient } = await import("@maximem/synap-js-sdk");
+ client = createClient(synapOptions());
+ await client.init();
+
+ const messages = buildSynapMessages(run);
+
+ await client.addMemory({
+ userId: identity.userId,
+ customerId: identity.customerId,
+ conversationId: identity.conversationId,
+ sessionId: run.id,
+ documentId: run.id,
+ documentType: "ai-chat-conversation",
+ documentCreatedAt: run.startedAt,
+ mode: "long-range",
+ metadata: {
+ product: "ROCmPilot",
+ track: "AI Agents & Agentic Workflows",
+ targetLabel: run.target.label,
+ targetRepo: run.target.repoUrl,
+ scanStatus: run.target.scanStatus,
+ agentMessages: run.agentMessages.length,
+ sharedMemories: run.agentMemory.length,
+ },
+ messages,
+ });
+
+ const [contextResult, promptContextResult] = await Promise.allSettled([
+ client.fetchUserContext({
+ userId: identity.userId,
+ customerId: identity.customerId,
+ conversationId: identity.conversationId,
+ searchQuery: [
+ "ROCm migration blockers",
+ "CUDA assumptions and AMD validation",
+ "agent decisions from prior ROCmPilot runs",
+ ],
+ maxResults: 8,
+ mode: "accurate",
+ }),
+ client.getContextForPrompt({
+ conversationId: identity.conversationId,
+ style: "structured",
+ }),
+ ]);
+
+ const context = contextResult.status === "fulfilled" ? contextResult.value : null;
+ const promptContext =
+ promptContextResult.status === "fulfilled" ? promptContextResult.value : null;
+ const synapContext = summarizeContext(context, promptContext);
+ const recalledItems =
+ countContextItems(context) + (promptContext?.recentMessageCount ?? 0);
+
+ return {
+ status: getLongContextMemoryStatus(
+ run.id,
+ messages.length,
+ recalledItems,
+ "connected"
+ ),
+ promptContext: synapContext || localContext,
+ };
+ } catch (error) {
+ console.warn("Synap memory fallback:", error);
+
+ return {
+ status: {
+ ...getLongContextMemoryStatus(
+ run.id,
+ run.agentMemory.length,
+ run.agentMessages.filter((message) => message.memoryRefs.length > 0).length,
+ "fallback"
+ ),
+ detail:
+ "Synap credentials are present, but the SDK runtime could not complete ingestion. Using local run memory for this report.",
+ },
+ promptContext: localContext,
+ };
+ } finally {
+ await shutdownClient(client);
+ }
+}
diff --git a/src/lib/rocmpilot/types.ts b/src/lib/rocmpilot/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..95242bc89dbcbd8d771fef09455b0a912a0b3574
--- /dev/null
+++ b/src/lib/rocmpilot/types.ts
@@ -0,0 +1,149 @@
+export type RunMode = "mock" | "amd";
+
+export type RunStatus = "queued" | "running" | "completed" | "failed";
+
+export type StageStatus = "pending" | "running" | "completed";
+
+export type FindingSeverity = "critical" | "high" | "medium" | "low";
+
+export type ModelSource = "amd-vllm" | "hf-router" | "fallback";
+
+export type RunTargetType = "sample" | "github";
+
+export type LongContextMemoryStatus = {
+ status: "connected" | "configured" | "fallback" | "not-configured";
+ label: string;
+ provider: "synap" | "local";
+ conversationId: string;
+ scope: string;
+ detail: string;
+ storedItems: number;
+ recalledItems: number;
+};
+
+export type GpuModelStatus = {
+ status: "connected" | "fallback" | "not-configured";
+ label: string;
+ model: string;
+ endpoint: string;
+ detail: string;
+ source: ModelSource;
+};
+
+export type SampleRepo = {
+ id: string;
+ name: string;
+ repoUrl: string;
+ stack: string;
+ model: string;
+ description: string;
+ risk: string;
+};
+
+export type RunStage = {
+ id: string;
+ agent: string;
+ title: string;
+ description: string;
+ status: StageStatus;
+ progress: number;
+ startedAt?: string;
+ completedAt?: string;
+};
+
+export type Finding = {
+ id: string;
+ severity: FindingSeverity;
+ category: string;
+ file: string;
+ line: number;
+ explanation: string;
+ recommendedFix: string;
+};
+
+export type PatchPreview = {
+ id: string;
+ file: string;
+ rationale: string;
+ diff: string;
+};
+
+export type BenchmarkResult = {
+ label: string;
+ backend: string;
+ tokensPerSecond: number;
+ p95LatencyMs: number;
+ memoryGb: number;
+ costNote: string;
+};
+
+export type AgentMessageKind =
+ | "question"
+ | "answer"
+ | "challenge"
+ | "proposal"
+ | "decision"
+ | "memory"
+ | "consensus";
+
+export type AgentMessage = {
+ id: string;
+ agent: string;
+ toAgent: string;
+ role: string;
+ task: string;
+ leadAgent: string;
+ kind: AgentMessageKind;
+ message: string;
+ replyToId?: string;
+ memoryRefs: string[];
+ createdAt: string;
+};
+
+export type AgentMemory = {
+ id: string;
+ title: string;
+ scope: string;
+ learnedFromAgent: string;
+ summary: string;
+ solution: string;
+ createdAt: string;
+ usedBy: string[];
+};
+
+export type RunTarget = {
+ type: RunTargetType;
+ repoUrl: string;
+ label: string;
+ branch?: string;
+ scanStatus: "fixture" | "scanned" | "failed" | "pending";
+ scannedFiles: number;
+ note: string;
+};
+
+export type RocmRun = {
+ id: string;
+ sample: SampleRepo;
+ target: RunTarget;
+ mode: RunMode;
+ status: RunStatus;
+ progress: number;
+ startedAt: string;
+ completedAt?: string;
+ stages: RunStage[];
+ findings: Finding[];
+ patches: PatchPreview[];
+ logs: string[];
+ agentMessages: AgentMessage[];
+ agentMemory: AgentMemory[];
+ longContextMemory: LongContextMemoryStatus;
+ benchmarks: BenchmarkResult[];
+ modelStatus: GpuModelStatus;
+};
+
+export type ReportResponse = {
+ report: string;
+ source: ModelSource;
+ modelStatus: GpuModelStatus;
+ memoryStatus: LongContextMemoryStatus;
+};
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bd0c391ddd1088e9067844c48835bf4abcd61783
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..cf9c65d3e0676a0169374d827f7abb97497789ef
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts"
+ ],
+ "exclude": ["node_modules"]
+}