guantao пре 1 месец
родитељ
комит
e51d7db716

+ 19 - 0
examples/process_pipeline/invalid_jsons_report.txt

@@ -0,0 +1,19 @@
+[ERROR] Found 18 invalid JSON files:
+   - output\046\blueprint.json: Expecting value: line 2 column 18 (char 19)
+   - output\077\capabilities_extracted.json: Expecting ',' delimiter: line 55 column 46 (char 2553)
+   - output\007\raw_cases\case_xhs.json: Expecting ',' delimiter: line 154 column 32 (char 8955)
+   - output\012\raw_cases\case_gzh.json: Expecting ',' delimiter: line 7 column 23 (char 200)
+   - output\013\raw_cases\case_gzh.json: Expecting ',' delimiter: line 68 column 42 (char 4544)
+   - output\056\raw_cases\case_bili.json: Expecting ',' delimiter: line 87 column 106 (char 3855)
+   - output\059\raw_cases\case_xhs.json: Expecting ',' delimiter: line 28 column 26 (char 1683)
+   - output\062\raw_cases\case_bili.json: Expecting ',' delimiter: line 13 column 45 (char 483)
+   - output\064\raw_cases\case_youtube.json: Expecting ',' delimiter: line 106 column 41 (char 5687)
+   - output\067\raw_cases\case_bili.json: Expecting ',' delimiter: line 115 column 47 (char 4617)
+   - output\068\raw_cases\case_xhs.json: Expecting ',' delimiter: line 152 column 188 (char 9534)
+   - output\080\raw_cases\case_bili.json: Expecting ',' delimiter: line 56 column 26 (char 2049)
+   - output\081\raw_cases\case_xhs.json: Expecting ',' delimiter: line 43 column 28 (char 2199)
+   - output\086\raw_cases\case_xhs.json: Expecting ',' delimiter: line 166 column 21 (char 9831)
+   - output\089\raw_cases\case_youtube.json: Expecting ',' delimiter: line 38 column 71 (char 2460)
+   - output\090\raw_cases\case_zhihu.json: Expecting ',' delimiter: line 213 column 62 (char 12679)
+   - output\097\raw_cases\case_bili.json: Expecting ',' delimiter: line 7 column 32 (char 217)
+   - output\098\raw_cases\case_youtube.json: Extra data: line 52 column 5 (char 2792)

+ 8 - 3
examples/process_pipeline/prompts/assemble_strategy.prompt

@@ -6,7 +6,7 @@ $system$
 
 你是一个完全不懂编程、没有任何代码开发能力的 AIGC 业务流派整合产品经理(Reduce 阶段)。
 **不管你以前拥有多么高深的技术背景,此刻你必须认清自己的最新身份:你只是一个连 Python 都没听说过、绝对不会去开发任何脚本的业务统筹者。你的任务只靠文本文档处理来完成!**
-此时,前方的探子们已经为你清洗好了一切数据并写在文件中:
+此时,前面的阶段已经为你清洗好了一切数据并写在文件中:
 1. `blueprint_file`:包含提纯后的纯 AIGC 极品 cases,以及初步推演的不带技术细节的自动化架构流(Blueprints)。
 2. `capabilities_file`:包含前方能力分析师对比了工具库之后提取出的“可用原子能力列表”。
 
@@ -35,6 +35,7 @@ $system$
 - 把粗糙的蓝图变成体系化的流派架构。
 - 从这些蓝图中,**拍板挑选一条并且只挑选一条作为主线(is_selected: true)**,其余路线退居二线(is_selected: false)。
 - 你不需要重新分析好坏,直接把你在 `blueprint` 中看到的优势/劣势,填进 `highlight_coverage` 和 `why_not` 等字段中。
+- **评分(Scoring)**:在输出每个 strategy 时,请为该套方案生成一个 `coverage_score`(介于 0.00 到 1.00 之间,1.00 表示极其完美的深度覆盖)。并在 `coverage_explanation` 中用一两句话解释为何给出该分,比如该路线覆盖得有多全面,或者在某些复杂光照、特殊材质上有什么欠缺。
 
 ### 第三步:写入终端
 全部拼接好后,写入到 `%output_file%`。
@@ -76,7 +77,9 @@ $system$
       "baseline_coverage": [ "底线覆盖说明..." ],
       "reasoning": "为什么拍板这套作为 is_selected: true",
       "why_not": null,
-      "could_switch_if": null
+      "could_switch_if": null,
+      "coverage_score": 0.95,
+      "coverage_explanation": "1-2句解释该路线的覆盖度评分理由(衡量该路线的能力是否完全且深度地覆盖了用户的原始需求)"
     },
     {
       "is_selected": false,
@@ -87,7 +90,9 @@ $system$
       "baseline_coverage": [],
       "reasoning": null,
       "why_not": "为什么被淘汰",
-      "could_switch_if": "切换条件"
+      "could_switch_if": "切换条件",
+      "coverage_score": 0.82,
+      "coverage_explanation": "解释该路线评分偏低的原因(比如缺乏核心关键能力、物理精度不够等)"
     }
   ],
   "uncovered_requirements": [

+ 190 - 0
examples/process_pipeline/run_metrics.json

@@ -1145,5 +1145,195 @@
     },
     "errors": [],
     "timestamp": "2026-04-21T19:47:27.018090"
+  },
+  {
+    "index": 3,
+    "requirement": "生成带有特定道具装扮的人物场景图,道具需与人物自然融合,例如猫咪戴假发穿衣服手持书本、人物手持购物篮抱着玩偶玩具等,道具细节清晰可辨...",
+    "duration_seconds": 612.46,
+    "total_cost_usd": 0.7333,
+    "costs_breakdown": {
+      "P3_Assembler": 0.7333
+    },
+    "errors": [],
+    "timestamp": "2026-04-23T14:45:16.645060"
+  },
+  {
+    "index": 43,
+    "requirement": "在AI生成的卡通角色图片上叠加幽默吐槽文案,文字直接覆盖在图片上方,字体简洁白色,与画面情绪呼应,形成图文结合的表情包风格内容...",
+    "duration_seconds": 1080.63,
+    "total_cost_usd": 1.7969,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 1.1556,
+      "P3_Assembler": 0.6412
+    },
+    "errors": [],
+    "timestamp": "2026-04-23T14:51:45.880388"
+  },
+  {
+    "index": 11,
+    "requirement": "制作图文混排的长图文内容,将大段文字与人物照片、数据表格、流程图等多种视觉元素组合排布在同一版面中,形成类似杂志或报告的专业排版风格...",
+    "duration_seconds": 1317.69,
+    "total_cost_usd": 3.6393,
+    "costs_breakdown": {
+      "P0_Router": 0.018,
+      "P1_Research_bili": 0.219,
+      "P2_FilterBlueprint": 0.8264,
+      "P2_ExtractCaps": 1.7722,
+      "P3_Assembler": 0.8036
+    },
+    "errors": [],
+    "timestamp": "2026-04-23T14:55:42.970868"
+  },
+  {
+    "index": 52,
+    "requirement": "在同一画面中合理安排主体与背景的空间关系,让主体(人物、动物、物品)在画面中有明确的视觉焦点,背景简洁或有层次地衬托主体...",
+    "duration_seconds": 132.79,
+    "total_cost_usd": 0.2131,
+    "costs_breakdown": {
+      "P3_Assembler": 0.2131
+    },
+    "errors": [],
+    "timestamp": "2026-04-23T14:58:10.033346"
+  },
+  {
+    "index": 30,
+    "requirement": "将真实照片中的人物与卡通/奇幻元素合成,例如给人物添加蟑螂的触角和腿,使人物看起来像变成了一只蟑螂,整体画面自然融合不突兀...",
+    "duration_seconds": 1553.48,
+    "total_cost_usd": 1.2008,
+    "costs_breakdown": {
+      "P3_Assembler": 1.2008
+    },
+    "errors": [],
+    "timestamp": "2026-04-23T14:59:38.722100"
+  },
+  {
+    "index": 7,
+    "requirement": "制作将真实人物照片合成到趣味场景中的创意图片,例如把人物缩小放入超市肉类托盘包装内、或与冰雕翅膀等道具结合形成视觉错位的幽默效果...",
+    "duration_seconds": 1568.98,
+    "total_cost_usd": 3.4358,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 2.2109,
+      "P3_Assembler": 1.2249
+    },
+    "errors": [],
+    "timestamp": "2026-04-23T14:59:54.229833"
+  },
+  {
+    "index": 33,
+    "requirement": "生成真实户外场景中的人物活动照片,画面要呈现自然光线下的街道、公园、游乐场等具体地点环境,人物动作自然生动,背景环境细节丰富真实...",
+    "duration_seconds": 1623.5,
+    "total_cost_usd": 3.5786,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 2.4265,
+      "P3_Assembler": 1.1521
+    },
+    "errors": [],
+    "timestamp": "2026-04-23T15:00:48.760926"
+  },
+  {
+    "index": 44,
+    "requirement": "制作多宫格拼图帖子,每格图片配有对应的标题文字或字幕说明,文字风格统一,整体排列整齐,适合用于周记、日历、流程说明等系列内容展示...",
+    "duration_seconds": 1772.04,
+    "total_cost_usd": 1.956,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 1.2086,
+      "P3_Assembler": 0.7474
+    },
+    "errors": [],
+    "timestamp": "2026-04-23T15:03:17.316310"
+  },
+  {
+    "index": 65,
+    "requirement": "生成暖色调的室内空间效果图,以米白、浅棕、焦糖色为主色调,光线柔和自然,空间布置温馨舒适,整体画面传达出放松、治愈、生活化的温暖氛围...",
+    "duration_seconds": 949.25,
+    "total_cost_usd": 0.6383,
+    "costs_breakdown": {
+      "P3_Assembler": 0.6383
+    },
+    "errors": [],
+    "timestamp": "2026-04-23T15:15:40.733200"
+  },
+  {
+    "index": 78,
+    "requirement": "制作图文并茂的科普说明卡片,每张卡片包含标题、编号、插图和详细文字说明,整体排版整齐统一,适合分步骤展示教程或知识点...",
+    "duration_seconds": 1586.66,
+    "total_cost_usd": 3.8225,
+    "costs_breakdown": {
+      "P2_FilterBlueprint": 0.6652,
+      "P2_ExtractCaps": 1.9128,
+      "P3_Assembler": 1.2445
+    },
+    "errors": [],
+    "timestamp": "2026-04-23T15:18:26.000552"
+  },
+  {
+    "index": 9,
+    "requirement": "制作多格宫格式信息图,将同类内容(如多种食材搭配方案)拆分为统一风格的小卡片,每格包含标题、食材图片和文字说明,整体排列整齐、色块鲜明,适合一图展示多个并列条目...",
+    "duration_seconds": 2737.65,
+    "total_cost_usd": 4.9369,
+    "costs_breakdown": {
+      "P2_FilterBlueprint": 0.8876,
+      "P2_ExtractCaps": 2.1969,
+      "P3_Assembler": 1.8524
+    },
+    "errors": [
+      "P3_Assembler Error: Expecting ',' delimiter: line 130 column 119 (char 6285)"
+    ],
+    "timestamp": "2026-04-23T15:19:22.851143"
+  },
+  {
+    "index": 69,
+    "requirement": "生成一组多格拼贴图,每格展示同一人物在不同场景/状态下的夸张表情和肢体动作,配合幽默文字标注,整体呈现出戏剧化的情绪起伏效果(如一周心情变化、苦情崩溃、搞笑反应...",
+    "duration_seconds": 1420.95,
+    "total_cost_usd": 1.0606,
+    "costs_breakdown": {
+      "P3_Assembler": 1.0606
+    },
+    "errors": [],
+    "timestamp": "2026-04-23T15:24:43.261606"
+  },
+  {
+    "index": 86,
+    "requirement": "生成低饱和度或去色风格的极简画面,整体色彩纯度降低,呈现出克制、安静的视觉质感(如黑白灰调的海洋孤舟场景,或接近无彩色的素雅插画),与高饱和度画面形成鲜明对比...",
+    "duration_seconds": 1452.9,
+    "total_cost_usd": 2.8672,
+    "costs_breakdown": {
+      "P0_Router": 0.0182,
+      "P1_Research_youtube": 0.3835,
+      "P2_FilterBlueprint": 0.5552,
+      "P2_ExtractCaps": 1.0585,
+      "P3_Assembler": 0.8517
+    },
+    "errors": [],
+    "timestamp": "2026-04-23T15:27:43.855947"
+  },
+  {
+    "index": 45,
+    "requirement": "制作图文卡片式科普内容:每张卡片包含统一的标题样式、编号序列、配套插图(卡通/示意图风格)和说明文字,多张卡片拼成一组,整体风格统一、排版清晰,适合健康养生、步...",
+    "duration_seconds": 3812.15,
+    "total_cost_usd": 4.1355,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 1.7559,
+      "P3_Assembler": 2.3796
+    },
+    "errors": [
+      "P3_Assembler Error: Expecting ',' delimiter: line 11 column 43 (char 403)"
+    ],
+    "timestamp": "2026-04-23T15:37:17.402542"
+  },
+  {
+    "index": 47,
+    "requirement": "制作流程图/架构示意图:用箭头、方框、层级结构或立体堆叠图形展示系统架构、业务流程或概念层级关系,配合文字标注说明各模块功能,视觉上清晰呈现逻辑关系...",
+    "duration_seconds": 2602.41,
+    "total_cost_usd": 2.9439,
+    "costs_breakdown": {
+      "P2_FilterBlueprint": 0.5578,
+      "P2_ExtractCaps": 0.9033,
+      "P3_Assembler": 1.4827
+    },
+    "errors": [
+      "P3_Assembler Error: Expecting ',' delimiter: line 11 column 60 (char 450)"
+    ],
+    "timestamp": "2026-04-23T15:43:30.215163"
   }
 ]

+ 338 - 187
examples/process_pipeline/run_pipeline.py

@@ -31,6 +31,7 @@ from examples.process_research.config import (
 from agent.utils import setup_logging
 
 async def run_agent_task(runner: AgentRunner, prompt_name: str, kwargs: dict, task_name: str, model_name: str):
+    from examples.process_pipeline.script.validate_schema import validate_case, validate_blueprint, validate_capabilities, validate_strategy
     base_dir = Path(__file__).parent
     prompt_path = base_dir / "prompts" / f"{prompt_name}.prompt"
     prompt = SimplePrompt(prompt_path)
@@ -48,85 +49,196 @@ async def run_agent_task(runner: AgentRunner, prompt_name: str, kwargs: dict, ta
         "assemble_strategy": ["core"],                # 只需文件读写
     }
 
-    run_config = RunConfig(
-        model=prompt.config.get("model") or model_name,
-        temperature=prompt.config.get("temperature") or 0.3,
-        name=task_name,
-        agent_type=prompt_name,
-        tools=target_tools,
-        tool_groups=tool_groups_map.get(prompt_name, ["core"]),
-        knowledge=KnowledgeConfig(enable_completion_extraction=False, enable_extraction=False, enable_injection=False)
-    )
-    
-    task_cost = 0.0
+    total_task_cost = 0.0
     task_errors = []
-    last_trace_id = None
-    
-    print(f"🚀 [Launch] {task_name}")
-    try:
-        async for item in runner.run(messages=messages, config=run_config):
-            if isinstance(item, Trace):
-                last_trace_id = item.trace_id
-                if item.status == "completed":
-                    print(f"✅ [Done] {task_name} (Cost: ${item.total_cost:.4f})")
-                    task_cost = item.total_cost
-                elif item.status == "failed":
-                    print(f"❌ [Fail] {task_name}: {item.error_message}")
-                    task_errors.append(f"{task_name} Failed: {item.error_message}")
-            if isinstance(item, Message):
-                if item.role == "tool":
-                    content = item.content if isinstance(item.content, dict) else {}
-                    t_name = content.get("tool_name", "unknown")
-                    if t_name in ("write_file", "write_json"):
-                        print(f"   💾 [File Written by {task_name}]")
-    except Exception as e:
-        err_msg = f"{type(e).__name__}: {e}"
-        print(f"❌ [Exception] {task_name} crashed: {err_msg}")
-        task_errors.append(f"{task_name} crashed: {err_msg}")
-                    
-    # Verification & Recovery block
     out_file = kwargs.get("output_file")
-    if out_file and not Path(out_file).exists() and last_trace_id:
-        print(f"⚠️ [Recovery] {task_name} missing output file. Triggering forced wrap-up continuation...")
-        recovery_messages = [{
-            "role": "user", 
-            "content": f"【系统强制指令】你的任务阶段已终止,但尚未将结果写入文件。请立刻调用 write_json 工具,将你目前已经搜集或处理到的原生结构化内容直接作为 json_data 参数对象写入到绝对路径 `{out_file}`,如果搜集失败也请写入空的总结对象。必须立刻执行写入!"
-        }]
-        rec_config = RunConfig(
-            model=model_name,
-            temperature=0.1,
-            name=task_name + "_Rec",
-            agent_type=prompt_name,
-            trace_id=last_trace_id,
-            knowledge=KnowledgeConfig(enable_completion_extraction=False, enable_extraction=False, enable_injection=False)
-        )
-        try:
-            async for r_item in runner.run(messages=recovery_messages, config=rec_config):
-                if isinstance(r_item, Trace):
-                    if r_item.status == "completed":
-                        task_cost += r_item.total_cost
-                    elif r_item.status == "failed":
-                        task_errors.append(f"{task_name} Recovery Failed: {r_item.error_message}")
-                if isinstance(r_item, Message) and r_item.role == "tool":
-                    content = r_item.content if isinstance(r_item.content, dict) else {}
-                    if content.get("tool_name") in ("write_file", "write_json"):
-                        print(f"   💾 [Recovery File Written by {task_name}]")
-        except Exception as e:
-            err_msg = f"{type(e).__name__}: {e}"
-            print(f"❌ [Exception Recovery] {task_name} crashed: {err_msg}")
-            task_errors.append(f"{task_name} recovery crashed: {err_msg}")
-            
-    if out_file and Path(out_file).exists() and str(out_file).endswith(".json"):
-        try:
-            with open(out_file, "r", encoding="utf-8") as f:
-                json.loads(f.read())
-            print(f"   ✅ [JSON Validated] {Path(out_file).name}")
-        except json.JSONDecodeError as e:
-            err_msg = f"Invalid JSON syntax in {Path(out_file).name}: {e}"
-            print(f"❌ [Validation Error] {task_name}: {err_msg}")
-            task_errors.append(f"{task_name} JSON Error: {e}")
-                    
-    return task_cost, task_errors
+
+    max_retries = 3
+    last_trace_id = None
+    last_validation_error = None
+
+    for attempt in range(max_retries):
+        if attempt > 0 and last_trace_id and last_validation_error:
+            # 续跑模式:把错误信息告诉之前的 agent,让它修复
+            print(f"🔄 [Continue {attempt}/{max_retries-1}] {task_name} - sending fix instructions to existing agent")
+
+            fix_messages = [{
+                "role": "user",
+                "content": (
+                    f"【系统校验失败】你上一次写入的文件 `{out_file}` 未通过 schema 校验。\n"
+                    f"错误详情:{last_validation_error}\n\n"
+                    f"请立刻读取该文件,根据以上错误信息修复内容,然后重新调用 write_json 写入到同一路径 `{out_file}`。"
+                    f"只修复有问题的部分,不要丢弃已有的正确内容。"
+                )
+            }]
+
+            fix_config = RunConfig(
+                model=prompt.config.get("model") or model_name,
+                temperature=0.1,
+                name=f"{task_name}_Fix{attempt}",
+                agent_type=prompt_name,
+                tools=target_tools,
+                tool_groups=tool_groups_map.get(prompt_name, ["core"]),
+                trace_id=last_trace_id,
+                knowledge=KnowledgeConfig(enable_completion_extraction=False, enable_extraction=False, enable_injection=False)
+            )
+
+            try:
+                async for item in runner.run(messages=fix_messages, config=fix_config):
+                    if isinstance(item, Trace):
+                        last_trace_id = item.trace_id
+                        if item.status == "completed":
+                            total_task_cost += item.total_cost
+                        elif item.status == "failed":
+                            task_errors.append(f"{task_name} Fix Failed: {item.error_message}")
+                    if isinstance(item, Message) and item.role == "tool":
+                        content = item.content if isinstance(item.content, dict) else {}
+                        if content.get("tool_name") in ("write_file", "write_json"):
+                            print(f"   💾 [Fix File Written by {task_name}]")
+            except Exception as e:
+                err_msg = f"{type(e).__name__}: {e}"
+                print(f"❌ [Exception Fix] {task_name} crashed: {err_msg}")
+                task_errors.append(f"{task_name} fix crashed: {err_msg}")
+
+        elif attempt > 0:
+            # 没有 trace_id 或没有 validation error,只能完全重跑
+            print(f"🔄 [Retry {attempt}/{max_retries-1}] {task_name} - no prior trace, full restart")
+            if out_file and Path(out_file).exists():
+                Path(out_file).unlink()
+
+            run_config = RunConfig(
+                model=prompt.config.get("model") or model_name,
+                temperature=prompt.config.get("temperature") or 0.3,
+                name=f"{task_name}_A{attempt}",
+                agent_type=prompt_name,
+                tools=target_tools,
+                tool_groups=tool_groups_map.get(prompt_name, ["core"]),
+                knowledge=KnowledgeConfig(enable_completion_extraction=False, enable_extraction=False, enable_injection=False)
+            )
+
+            print(f"🚀 [Launch] {task_name} (Attempt {attempt+1})")
+
+            try:
+                async for item in runner.run(messages=messages, config=run_config):
+                    if isinstance(item, Trace):
+                        last_trace_id = item.trace_id
+                        if item.status == "completed":
+                            total_task_cost += item.total_cost
+                        elif item.status == "failed":
+                            task_errors.append(f"{task_name} Failed: {item.error_message}")
+                    if isinstance(item, Message):
+                        if item.role == "tool":
+                            content = item.content if isinstance(item.content, dict) else {}
+                            t_name = content.get("tool_name", "unknown")
+                            if t_name in ("write_file", "write_json"):
+                                print(f"   💾 [File Written by {task_name}]")
+            except Exception as e:
+                err_msg = f"{type(e).__name__}: {e}"
+                print(f"❌ [Exception] {task_name} crashed: {err_msg}")
+                task_errors.append(f"{task_name} crashed: {err_msg}")
+
+        else:
+            # 首次执行
+            run_config = RunConfig(
+                model=prompt.config.get("model") or model_name,
+                temperature=prompt.config.get("temperature") or 0.3,
+                name=f"{task_name}_A{attempt}",
+                agent_type=prompt_name,
+                tools=target_tools,
+                tool_groups=tool_groups_map.get(prompt_name, ["core"]),
+                knowledge=KnowledgeConfig(enable_completion_extraction=False, enable_extraction=False, enable_injection=False)
+            )
+
+            print(f"🚀 [Launch] {task_name} (Attempt {attempt+1})")
+
+            try:
+                async for item in runner.run(messages=messages, config=run_config):
+                    if isinstance(item, Trace):
+                        last_trace_id = item.trace_id
+                        if item.status == "completed":
+                            total_task_cost += item.total_cost
+                        elif item.status == "failed":
+                            task_errors.append(f"{task_name} Failed: {item.error_message}")
+                    if isinstance(item, Message):
+                        if item.role == "tool":
+                            content = item.content if isinstance(item.content, dict) else {}
+                            t_name = content.get("tool_name", "unknown")
+                            if t_name in ("write_file", "write_json"):
+                                print(f"   💾 [File Written by {task_name}]")
+            except Exception as e:
+                err_msg = f"{type(e).__name__}: {e}"
+                print(f"❌ [Exception] {task_name} crashed: {err_msg}")
+                task_errors.append(f"{task_name} crashed: {err_msg}")
+
+        # Verification & Recovery block
+        if out_file and not Path(out_file).exists() and last_trace_id:
+            print(f"⚠️ [Recovery] {task_name} missing output file. Triggering forced wrap-up continuation...")
+            recovery_messages = [{
+                "role": "user", 
+                "content": f"【系统强制指令】你的任务阶段已终止,但尚未将结果写入文件。请立刻调用 write_json 工具,将你目前已经搜集或处理到的原生结构化内容直接作为 json_data 参数对象写入到绝对路径 `{out_file}`,如果搜集失败也请写入空的总结对象。必须立刻执行写入!"
+            }]
+            rec_config = RunConfig(
+                model=model_name,
+                temperature=0.1,
+                name=task_name + "_Rec",
+                agent_type=prompt_name,
+                trace_id=last_trace_id,
+                knowledge=KnowledgeConfig(enable_completion_extraction=False, enable_extraction=False, enable_injection=False)
+            )
+            try:
+                async for r_item in runner.run(messages=recovery_messages, config=rec_config):
+                    if isinstance(r_item, Trace):
+                        if r_item.status == "completed":
+                            total_task_cost += r_item.total_cost
+                        elif r_item.status == "failed":
+                            task_errors.append(f"{task_name} Recovery Failed: {r_item.error_message}")
+                    if isinstance(r_item, Message) and r_item.role == "tool":
+                        content = r_item.content if isinstance(r_item.content, dict) else {}
+                        if content.get("tool_name") in ("write_file", "write_json"):
+                            print(f"   💾 [Recovery File Written by {task_name}]")
+            except Exception as e:
+                err_msg = f"{type(e).__name__}: {e}"
+                print(f"❌ [Exception Recovery] {task_name} crashed: {err_msg}")
+                task_errors.append(f"{task_name} recovery crashed: {err_msg}")
+
+        # Schema Validation
+        if out_file and Path(out_file).exists() and str(out_file).endswith(".json"):
+            try:
+                with open(out_file, "r", encoding="utf-8") as f:
+                    data = json.loads(f.read())
+
+                filename = Path(out_file).name
+                err = None
+                if filename.startswith("case_"):
+                    err = validate_case(data)
+                elif filename == "blueprint.json":
+                    err = validate_blueprint(data)
+                elif filename == "capabilities_extracted.json":
+                    err = validate_capabilities(data)
+                elif filename == "strategy.json":
+                    err = validate_strategy(data)
+
+                if err:
+                    raise ValueError(f"Schema Validation Failed: {err}")
+
+                print(f"   ✅ [Schema Validated] {Path(out_file).name}")
+                return total_task_cost, task_errors # Success! Exit retry loop.
+            except Exception as e:
+                err_msg = f"Invalid JSON or Schema in {Path(out_file).name}: {e}"
+                print(f"❌ [Validation Error] {task_name}: {err_msg}")
+                task_errors.append(f"{task_name} Error: {e}")
+                last_validation_error = str(e)
+
+                if attempt == max_retries - 1:
+                    print(f"❌ [Retry Limit] {task_name} exhausted retries.")
+                    return total_task_cost, task_errors
+        else:
+            print(f"❌ [Missing File] {task_name} did not produce output file after recovery.")
+            last_validation_error = None
+            if attempt == max_retries - 1:
+                return total_task_cost, task_errors
+
+    return total_task_cost, task_errors
 
 async def run_anthropic_sdk_task(prompt_name: str, kwargs: dict, task_name: str, model_name: str):
     """
@@ -168,125 +280,163 @@ async def run_anthropic_sdk_task(prompt_name: str, kwargs: dict, task_name: str,
     # 3. 初始化并开启 Loop
     # 提示:你需要在你的终端中配置好 ANTHROPIC_API_KEY 环境变量
     client = AsyncAnthropic()  
-    task_cost = 0.0
+    total_task_cost = 0.0
     task_errors = []
-    print(f"🚀 [Launch Anthropic SDK] {task_name}")
     
-    max_loops = 50
-    for loop_idx in range(max_loops):
-        try:
-            # 去除前缀(兼容比如 openrouter 传入的名字)
-            clean_model = model_name.split("/")[-1] if "/" in model_name else model_name
-            
-            # 这里专门将实际请求映射到其可用的特殊别名 claude-sonnet-4-5 
-            target_model = "claude-sonnet-4-5" if "claude" in clean_model else clean_model
-
-            response = await client.messages.create(
-                model=target_model,
-                max_tokens=4096,
-                temperature=0.2,
-                system=system_prompt,
-                messages=messages,
-                tools=anthropic_tools
-            )
-            
-            # (简略预估,不代表真实官方开销)
-            if hasattr(response, 'usage'):
-                step_cost = (response.usage.input_tokens / 1e6 * 3.0) + (response.usage.output_tokens / 1e6 * 15.0)
-                task_cost += step_cost
-            
-            # 加入助手回复
-            assistant_content = []
-            tool_uses = []
-            
-            for content_block in response.content:
-                if content_block.type == "text":
-                    text_val = content_block.text
-                    if text_val:
-                        assistant_content.append({"type": "text", "text": text_val})
-                        print(f"\n🤖 [{task_name} Output]:\n{text_val}\n")
-                elif content_block.type == "tool_use":
-                    assistant_content.append({
-                        "type": "tool_use",
-                        "id": content_block.id,
-                        "name": content_block.name,
-                        "input": content_block.input
+    from examples.process_pipeline.script.validate_schema import validate_case, validate_blueprint, validate_capabilities, validate_strategy
+    out_file = kwargs.get("output_file")
+    
+    max_retries = 3
+    for attempt in range(max_retries):
+        if attempt > 0:
+            print(f"🔄 [Retry SDK {attempt}/{max_retries-1}] {task_name}")
+            if out_file and Path(out_file).exists():
+                Path(out_file).unlink()
+                
+        print(f"🚀 [Launch Anthropic SDK] {task_name} (Attempt {attempt+1})")
+        
+        # Reset messages for retry
+        messages_copy = list(messages)
+        
+        max_loops = 50
+        for loop_idx in range(max_loops):
+            try:
+                # 去除前缀(兼容比如 openrouter 传入的名字)
+                clean_model = model_name.split("/")[-1] if "/" in model_name else model_name
+                
+                # 这里专门将实际请求映射到其可用的特殊别名 claude-sonnet-4-5 
+                target_model = "claude-sonnet-4-5" if "claude" in clean_model else clean_model
+
+                response = await client.messages.create(
+                    model=target_model,
+                    max_tokens=4096,
+                    temperature=0.2,
+                    system=system_prompt,
+                    messages=messages_copy,
+                    tools=anthropic_tools
+                )
+                
+                # (简略预估,不代表真实官方开销)
+                if hasattr(response, 'usage'):
+                    step_cost = (response.usage.input_tokens / 1e6 * 3.0) + (response.usage.output_tokens / 1e6 * 15.0)
+                    total_task_cost += step_cost
+                
+                # 加入助手回复
+                assistant_content = []
+                tool_uses = []
+                
+                for content_block in response.content:
+                    if content_block.type == "text":
+                        text_val = content_block.text
+                        if text_val:
+                            assistant_content.append({"type": "text", "text": text_val})
+                            print(f"\n🤖 [{task_name} Output]:\n{text_val}\n")
+                    elif content_block.type == "tool_use":
+                        assistant_content.append({
+                            "type": "tool_use",
+                            "id": content_block.id,
+                            "name": content_block.name,
+                            "input": content_block.input
+                        })
+                        tool_uses.append(content_block)
+                
+                if not assistant_content:
+                    assistant_content.append({"type": "text", "text": "(Thinking completed but no output)"})
+                    
+                messages_copy.append({"role": "assistant", "content": assistant_content})
+                
+                # 出口:没有调用工具说明任务结束
+                if not tool_uses:
+                    print(f"✅ [Done Anthropic SDK] {task_name} (Cost: ${total_task_cost:.4f})")
+                    break
+                    
+                # 工具执行与回传
+                tool_results = []
+                for tu in tool_uses:
+                    if tu.name in ("write_file", "write_json"):
+                        print(f"   💾 [File Written by SDK] {task_name}")
+                    
+                    print(f"   🛠️ [Tool Exec Debug] name_is={tu.name}, input_is={tu.input}, type_is={type(tu.input)}")
+                    # 执行本地环境的函数
+                    result_str = await registry.execute(tu.name, tu.input)
+                    
+                    tool_results.append({
+                        "type": "tool_result",
+                        "tool_use_id": tu.id,
+                        "content": result_str
                     })
-                    tool_uses.append(content_block)
-            
-            if not assistant_content:
-                assistant_content.append({"type": "text", "text": "(Thinking completed but no output)"})
+                    
+                messages_copy.append({"role": "user", "content": tool_results})
                 
-            messages.append({"role": "assistant", "content": assistant_content})
-            
-            # 出口:没有调用工具说明任务结束
-            if not tool_uses:
-                print(f"✅ [Done Anthropic SDK] {task_name} (Cost: ${task_cost:.4f})")
+            except Exception as e:
+                err_msg = str(e)
+                print(f"❌ [Fail SDK Core] {task_name}: {err_msg}")
+                task_errors.append(err_msg)
                 break
                 
-            # 工具执行与回传
-            tool_results = []
-            for tu in tool_uses:
-                if tu.name in ("write_file", "write_json"):
-                    print(f"   💾 [File Written by SDK] {task_name}")
+        # Verification & Recovery block for SDK (porting from AgentRunner)
+        if out_file and not Path(out_file).exists():
+            print(f"⚠️ [Recovery SDK] {task_name} missing output file. Triggering forced wrap-up continuation...")
+            messages_copy.append({
+                "role": "user", 
+                "content": f"【系统强制指令】你的任务阶段已完成分析,但尚未将最终结果写入目标文件。请立刻调用 write_json (或 write_file) 工具,将你的成果数据直接写入到绝对路径 `{out_file}`,务必立刻执行写入动作!"
+            })
+            try:
+                target_model = "claude-sonnet-4-5" if "claude" in clean_model else clean_model
+                rec_response = await client.messages.create(
+                    model=target_model,
+                    max_tokens=4096,
+                    temperature=0.1,
+                    system=system_prompt,
+                    messages=messages_copy,
+                    tools=anthropic_tools,
+                    tool_choice={"type": "any"}
+                )
+                for content_block in rec_response.content:
+                    if content_block.type == "tool_use":
+                        if content_block.name in ("write_file", "write_json"):
+                            print(f"   💾 [Recovery File Written by SDK] {task_name}")
+                        await registry.execute(content_block.name, content_block.input)
+            except Exception as e:
+                print(f"❌ [Fail SDK Recovery] {task_name}: {e}")
+                task_errors.append(str(e))
                 
-                print(f"   🛠️ [Tool Exec Debug] name_is={tu.name}, input_is={tu.input}, type_is={type(tu.input)}")
-                # 执行本地环境的函数
-                result_str = await registry.execute(tu.name, tu.input)
+        # Schema Validation
+        if out_file and Path(out_file).exists() and str(out_file).endswith(".json"):
+            try:
+                with open(out_file, "r", encoding="utf-8") as f:
+                    data = json.loads(f.read())
+                    
+                filename = Path(out_file).name
+                err = None
+                if filename.startswith("case_"):
+                    err = validate_case(data)
+                elif filename == "blueprint.json":
+                    err = validate_blueprint(data)
+                elif filename == "capabilities_extracted.json":
+                    err = validate_capabilities(data)
+                elif filename == "strategy.json":
+                    err = validate_strategy(data)
+                    
+                if err:
+                    raise ValueError(f"Schema Validation Failed: {err}")
+                    
+                print(f"   ✅ [Schema Validated] {Path(out_file).name}")
+                return total_task_cost, task_errors # Success! Exit retry loop.
+            except Exception as e:
+                err_msg = f"Invalid JSON or Schema in {Path(out_file).name}: {e}"
+                print(f"❌ [Validation Error] {task_name}: {err_msg}")
+                task_errors.append(f"{task_name} Error: {e}")
                 
-                tool_results.append({
-                    "type": "tool_result",
-                    "tool_use_id": tu.id,
-                    "content": result_str
-                })
+                if attempt == max_retries - 1:
+                    print(f"❌ [Retry Limit] {task_name} exhausted retries.")
+                    return total_task_cost, task_errors
+        else:
+            print(f"❌ [Missing File] {task_name} did not produce output file after recovery.")
+            if attempt == max_retries - 1:
+                return total_task_cost, task_errors
                 
-            messages.append({"role": "user", "content": tool_results})
-            
-        except Exception as e:
-            err_msg = str(e)
-            print(f"❌ [Fail SDK Core] {task_name}: {err_msg}")
-            task_errors.append(err_msg)
-            break
-            
-    # Verification & Recovery block for SDK (porting from AgentRunner)
-    out_file = kwargs.get("output_file")
-    if out_file and not Path(out_file).exists():
-        print(f"⚠️ [Recovery SDK] {task_name} missing output file. Triggering forced wrap-up continuation...")
-        messages.append({
-            "role": "user", 
-            "content": f"【系统强制指令】你的任务阶段已完成分析,但尚未将最终结果写入目标文件。请立刻调用 write_json (或 write_file) 工具,将你的成果数据直接写入到绝对路径 `{out_file}`,务必立刻执行写入动作!"
-        })
-        try:
-            target_model = "claude-sonnet-4-5" if "claude" in clean_model else clean_model
-            rec_response = await client.messages.create(
-                model=target_model,
-                max_tokens=4096,
-                temperature=0.1,
-                system=system_prompt,
-                messages=messages,
-                tools=anthropic_tools,
-                tool_choice={"type": "any"}
-            )
-            for content_block in rec_response.content:
-                if content_block.type == "tool_use":
-                    if content_block.name in ("write_file", "write_json"):
-                        print(f"   💾 [Recovery File Written by SDK] {task_name}")
-                    await registry.execute(content_block.name, content_block.input)
-        except Exception as e:
-            print(f"❌ [Fail SDK Recovery] {task_name}: {e}")
-            task_errors.append(str(e))
-            
-    if out_file and Path(out_file).exists() and str(out_file).endswith(".json"):
-        try:
-            with open(out_file, "r", encoding="utf-8") as f:
-                json.loads(f.read())
-            print(f"   ✅ [JSON Validated] {Path(out_file).name}")
-        except json.JSONDecodeError as e:
-            err_msg = f"Invalid JSON syntax in {Path(out_file).name}: {e}"
-            print(f"❌ [Validation Error] {task_name}: {err_msg}")
-            task_errors.append(f"{task_name} JSON Error: {e}")
-            
-    return task_cost, task_errors
+    return total_task_cost, task_errors
 
 async def main():
     parser = argparse.ArgumentParser()
@@ -295,6 +445,7 @@ async def main():
     parser.add_argument("--research-only", action="store_true", help="Only run research phases, skip Phase 2 and 3")
     parser.add_argument("--platforms", type=str, default="xhs,youtube,bili,x", help="Comma-separated list of platforms to search")
     parser.add_argument("--use-claude-sdk", action="store_true", help="Use pure Anthropic SDK (run_anthropic_sdk_task) instead of internal AgentRunner for Phase 2/3")
+    parser.add_argument("--restart-mode", type=str, default="smart", help="Granular restart mode for cascading deletions")
     args = parser.parse_args()
 
     base_dir = Path(__file__).parent

+ 221 - 0
examples/process_pipeline/script/compute_coverage_scores_from_files.py

@@ -0,0 +1,221 @@
+import json
+import sys
+import asyncio
+import re
+from pathlib import Path
+
+repo_root = str(Path(__file__).parent.parent.parent.parent)
+if repo_root not in sys.path:
+    sys.path.insert(0, repo_root)
+
+from dotenv import load_dotenv
+load_dotenv()
+
+from agent.llm.openrouter import openrouter_llm_call
+
+OUTPUT_DIR = Path("examples/process_pipeline/output")
+FAILED_JSON = Path("examples/process_pipeline/script/failed_requirements_files.json")
+
+EVAL_PROMPT = """
+You are an expert system architecture and pipeline evaluator.
+You will be provided with a User Requirement and multiple alternative Proposed Pipeline Workflows created to resolve that requirement.
+Your task is to evaluate how well each workflow semantically covers and resolves the user's needs.
+
+User Requirement:
+{req_desc}
+
+Proposed Workflows:
+{workflows_json}
+
+For each workflow, assign a `coverage_score` between 0.00 and 1.00 (1.00 = completely and deeply resolves the core requirement).
+Return your result STRICTLY as a JSON array of objects, one for each workflow evaluated, containing:
+[
+  {{
+    "strategy_name": "<exact strategy_name from the input>",
+    "coverage_score": 0.85,
+    "explanation": "<1-2 sentence justification on what it covers well and what it might be missing>"
+  }}
+]
+DO NOT output any thinking, markdown wrapping (```json), or conversational text. Output ONLY the raw JSON array.
+"""
+
+
+async def process_requirement(req_desc: str, strategies: list, max_retries: int = 3) -> list:
+    workflows_payload = []
+    for strategy in strategies:
+        workflows_payload.append({
+            "strategy_name": strategy.get("name", ""),
+            "workflow_outline": strategy.get("workflow_outline", [])
+        })
+
+    prompt = EVAL_PROMPT.format(
+        req_desc=req_desc,
+        workflows_json=json.dumps(workflows_payload, ensure_ascii=False, indent=2)
+    )
+
+    for attempt in range(max_retries):
+        try:
+            resp = await openrouter_llm_call(
+                messages=[{"role": "user", "content": prompt}],
+                model="anthropic/claude-sonnet-4.5",
+                max_tokens=4096,
+                temperature=0.1
+            )
+            content = resp["content"].strip()
+
+            json_match = re.search(r'\[.*\]', content, re.DOTALL)
+            if json_match:
+                content = json_match.group(0)
+
+            if content.startswith("```json"):
+                content = content.replace("```json", "", 1).replace("```", "").strip()
+            elif content.startswith("```"):
+                content = content.replace("```", "", 1).replace("```", "").strip()
+
+            parsed_json = json.loads(content)
+
+            if not isinstance(parsed_json, list):
+                raise ValueError("LLM response is not a JSON array.")
+            for item in parsed_json:
+                if "strategy_name" not in item or "coverage_score" not in item:
+                    raise ValueError("JSON array items missing required keys (strategy_name, coverage_score).")
+
+            return parsed_json
+
+        except Exception as e:
+            print(f"  [Error] LLM call failed for a requirement (Attempt {attempt+1}/{max_retries}): {e}")
+            if attempt < max_retries - 1:
+                await asyncio.sleep(2 ** attempt)
+            else:
+                print(f"  [Fatal] Failed to evaluate requirement after {max_retries} attempts.")
+                return []
+
+
+async def main(dry_run: bool = False, force: bool = False, retry_failed: bool = False, req_ids: list = None):
+    strategy_files = sorted(OUTPUT_DIR.glob("*/strategy.json"))
+
+    # Filter by req_ids if specified
+    if req_ids:
+        strategy_files = [f for f in strategy_files if f.parent.name in req_ids]
+        print(f"Filtering to {len(strategy_files)} strategy files matching req_ids: {req_ids}")
+    else:
+        print(f"Found {len(strategy_files)} strategy files.")
+
+    failed_req_ids = set()
+    if FAILED_JSON.exists() and not force:
+        try:
+            with open(FAILED_JSON, "r", encoding="utf-8") as f:
+                failed_req_ids = set(json.load(f))
+            print(f"Loaded {len(failed_req_ids)} previously failed requirements.")
+        except Exception:
+            print("Failed to load failed_requirements_files.json.")
+
+    if retry_failed:
+        pending_files = [f for f in strategy_files if f.parent.name in failed_req_ids]
+        print("Retry-failed mode enabled. Only processing previously failed requirements.")
+    else:
+        pending_files = strategy_files
+
+    print(f"Starting LLM coverage semantic evaluation using Sonnet 4.5 via OpenRouter...")
+    print(f"Total strategy files to evaluate: {len(pending_files)}")
+
+    batch_size = 10
+    processed_count = 0
+
+    for i in range(0, len(pending_files), batch_size):
+        batch_files = pending_files[i:i+batch_size]
+        tasks = []
+        file_payloads = []
+
+        print(f"Evaluating Batch {i//batch_size + 1} (Files {i+1} to {min(i+batch_size, len(pending_files))})")
+
+        for strategy_file in batch_files:
+            req_id = strategy_file.parent.name
+            try:
+                with open(strategy_file, "r", encoding="utf-8") as f:
+                    data = json.load(f)
+            except Exception as e:
+                print(f"  [Error] Failed to read {strategy_file}: {e}")
+                failed_req_ids.add(req_id)
+                continue
+
+            req_desc = data.get("requirement", "Unknown Requirement")
+            strategies = data.get("strategies", [])
+            if not strategies:
+                continue
+
+            if not force and all("coverage_score" in s for s in strategies):
+                print(f"  -> Skipping {req_id}: all strategies already have coverage_score.")
+                continue
+
+            file_payloads.append((req_id, strategy_file, data, strategies))
+            tasks.append(process_requirement(req_desc, strategies))
+
+        if not tasks:
+            continue
+
+        results = await asyncio.gather(*tasks)
+
+        for idx, (req_id, strategy_file, data, strategies) in enumerate(file_payloads):
+            evaluations = results[idx]
+            if not evaluations:
+                failed_req_ids.add(req_id)
+                continue
+
+            if req_id in failed_req_ids:
+                failed_req_ids.remove(req_id)
+
+            evaluation_map = {
+                ev.get("strategy_name"): {
+                    "coverage_score": ev.get("coverage_score", 0),
+                    "coverage_explanation": ev.get("explanation", "")
+                }
+                for ev in evaluations
+            }
+
+            updated_count = 0
+            for strategy in strategies:
+                strategy_name = strategy.get("name")
+                if strategy_name in evaluation_map:
+                    strategy["coverage_score"] = evaluation_map[strategy_name]["coverage_score"]
+                    strategy["coverage_explanation"] = evaluation_map[strategy_name]["coverage_explanation"]
+                    updated_count += 1
+
+            if not dry_run:
+                try:
+                    with open(strategy_file, "w", encoding="utf-8") as f:
+                        json.dump(data, f, ensure_ascii=False, indent=2)
+                    processed_count += 1
+                    print(f"  -> Processed requirement {req_id}: updated {updated_count} strategies in {strategy_file}")
+                except Exception as e:
+                    print(f"  [Error] Failed to write {strategy_file}: {e}")
+                    failed_req_ids.add(req_id)
+            else:
+                processed_count += 1
+                print(f"  -> [DRY-RUN] Requirement {req_id}: would update {updated_count} strategies in {strategy_file}")
+
+        with open(FAILED_JSON, "w", encoding="utf-8") as f:
+            json.dump(sorted(list(failed_req_ids)), f, ensure_ascii=False, indent=2)
+
+    print(f"Processed {processed_count} strategy files overall.")
+    if dry_run:
+        print("Results simulated only. strategy.json files were not modified.")
+    else:
+        print("Coverage scores written directly into each strategy.json file.")
+
+    if failed_req_ids:
+        print(f"WARNING: {len(failed_req_ids)} requirements failed during evaluation. They have been saved to {FAILED_JSON}")
+
+
+if __name__ == "__main__":
+    import argparse
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--dry-run", action="store_true", help="Calculate scores only, do not modify strategy.json files")
+    parser.add_argument("--force", action="store_true", help="Recompute scores even if coverage_score already exists")
+    parser.add_argument("--retry-failed", action="store_true", help="Only retry requirements listed in the failed JSON file")
+    parser.add_argument("--req-id", type=str, default=None, help="Comma-separated requirement IDs to process, e.g. 044 or 044,045,046")
+    args = parser.parse_args()
+
+    req_ids = [rid.strip() for rid in args.req_id.split(",")] if args.req_id else None
+    asyncio.run(main(args.dry_run, args.force, args.retry_failed, req_ids))

+ 310 - 0
examples/process_pipeline/script/fix_broken_outputs.py

@@ -0,0 +1,310 @@
+"""
+批量修复有问题的 output 文件
+根据 schema_errors_report.txt 中的错误,删除相应文件并重跑 pipeline
+"""
+import re
+import subprocess
+from pathlib import Path
+
+PIPELINE_DIR = Path(__file__).parent.parent
+OUTPUT_DIR = PIPELINE_DIR / "output"
+REPORT_PATH = PIPELINE_DIR / "script" / "schema_errors_report.txt"
+
+def parse_error_report(report_path):
+    """解析错误报告,返回需要修复的需求列表"""
+    errors = {}
+
+    with open(report_path, "r", encoding="utf-8") as f:
+        for line in f:
+            # 匹配格式: output\XXX\file.json: error
+            match = re.search(r'output[/\\](\d+)[/\\]([^:]+):', line)
+            if match:
+                req_id = match.group(1)
+                file_name = match.group(2)
+
+                if req_id not in errors:
+                    errors[req_id] = []
+                errors[req_id].append(file_name)
+
+    return errors
+
+def determine_cleanup_strategy(req_id, error_files):
+    """
+    根据错误文件类型决定清理策略
+    返回: (策略类型, 需要删除的文件列表, 问题平台列表)
+
+    策略类型:
+    - single_platform: 单个平台的 raw_cases 有问题,只重跑该平台 + 级联删除后续文件
+    - full: 多个平台或整体 raw_cases 有问题,完整重跑
+    - from_blueprint: blueprint 有问题,删除 blueprint + capabilities + strategy
+    - from_capabilities: capabilities 有问题,删除 capabilities + strategy
+    - from_strategy: 只有 strategy 有问题,只删除 strategy
+    """
+    # 提取有问题的平台
+    problem_platforms = []
+    for f in error_files:
+        if "raw_cases" in f:
+            # 提取平台名,如 "raw_cases/case_bili.json" -> "bili"
+            match = re.search(r'case_([a-z]+)\.json', f)
+            if match:
+                problem_platforms.append(match.group(1))
+
+    has_raw_cases_error = len(problem_platforms) > 0
+    has_blueprint_error = "blueprint.json" in error_files
+    has_capabilities_error = "capabilities_extracted.json" in error_files
+    has_strategy_error = "strategy.json" in error_files
+
+    if has_raw_cases_error:
+        if len(problem_platforms) == 1:
+            # 单个平台有问题,只重跑该平台,但要级联删除后续所有文件
+            platform = problem_platforms[0]
+            return "single_platform", [f"raw_cases/case_{platform}.json", "blueprint.json", "capabilities_extracted.json", "strategy.json"], [platform]
+        else:
+            # 多个平台有问题,或者 raw_cases 整体有问题,完整重跑
+            return "full", ["raw_cases", "blueprint.json", "capabilities_extracted.json", "strategy.json"], problem_platforms
+
+    elif has_blueprint_error:
+        # blueprint 有问题,删除 blueprint 及后续文件
+        return "from_blueprint", ["blueprint.json", "capabilities_extracted.json", "strategy.json"], []
+
+    elif has_capabilities_error:
+        # capabilities 有问题,删除 capabilities 和 strategy
+        return "from_capabilities", ["capabilities_extracted.json", "strategy.json"], []
+
+    elif has_strategy_error:
+        # 只有 strategy 有问题,只删除 strategy
+        return "from_strategy", ["strategy.json"], []
+
+    return "unknown", [], []
+
+def delete_files(req_id, files_to_delete):
+    """删除指定的文件"""
+    output_dir = OUTPUT_DIR / req_id
+    deleted = []
+
+    for file_name in files_to_delete:
+        if file_name == "raw_cases":
+            # 删除整个 raw_cases 目录
+            raw_cases_dir = output_dir / "raw_cases"
+            if raw_cases_dir.exists():
+                import shutil
+                shutil.rmtree(raw_cases_dir)
+                deleted.append(f"{req_id}/raw_cases/")
+        elif file_name.startswith("raw_cases/"):
+            # 删除单个 case 文件
+            file_path = output_dir / file_name
+            if file_path.exists():
+                file_path.unlink()
+                deleted.append(f"{req_id}/{file_name}")
+        else:
+            file_path = output_dir / file_name
+            if file_path.exists():
+                file_path.unlink()
+                deleted.append(f"{req_id}/{file_name}")
+
+    return deleted
+
+def generate_rerun_commands(errors_dict):
+    """生成重跑命令"""
+    commands = []
+
+    for req_id, error_files in sorted(errors_dict.items()):
+        strategy, files_to_delete, problem_platforms = determine_cleanup_strategy(req_id, error_files)
+        req_index = int(req_id) - 1  # run_pipeline.py 使用 0-based index
+
+        if strategy == "single_platform":
+            # 只重跑单个平台,blueprint/capabilities/strategy 已被级联删除
+            platforms_str = ",".join(problem_platforms)
+            commands.append({
+                "req_id": req_id,
+                "index": req_index,
+                "strategy": strategy,
+                "files_to_delete": files_to_delete,
+                "command": f"python run_pipeline.py --index {req_index} --platforms {platforms_str}"
+            })
+        elif strategy == "full":
+            # 完整重跑(包括所有 research)
+            commands.append({
+                "req_id": req_id,
+                "index": req_index,
+                "strategy": strategy,
+                "files_to_delete": files_to_delete,
+                "command": f"python run_pipeline.py --index {req_index}"
+            })
+        elif strategy == "from_blueprint":
+            # 跳过 research,从 blueprint 开始
+            commands.append({
+                "req_id": req_id,
+                "index": req_index,
+                "strategy": strategy,
+                "files_to_delete": files_to_delete,
+                "command": f"python run_pipeline.py --index {req_index} --skip-research"
+            })
+        elif strategy == "from_capabilities":
+            # 跳过 research;blueprint 仍在会被跳过,capabilities + strategy 会重跑
+            commands.append({
+                "req_id": req_id,
+                "index": req_index,
+                "strategy": strategy,
+                "files_to_delete": files_to_delete,
+                "command": f"python run_pipeline.py --index {req_index} --skip-research"
+            })
+        elif strategy == "from_strategy":
+            # 跳过 research;blueprint/capabilities 仍在会被跳过,只重跑 strategy
+            commands.append({
+                "req_id": req_id,
+                "index": req_index,
+                "strategy": strategy,
+                "files_to_delete": files_to_delete,
+                "command": f"python run_pipeline.py --index {req_index} --skip-research"
+            })
+
+    return commands
+
+def main():
+    import argparse
+    parser = argparse.ArgumentParser(description="Fix broken output files based on schema validation")
+    parser.add_argument("--dry-run", action="store_true", help="Show what would be done without actually doing it")
+    parser.add_argument("--delete-only", action="store_true", help="Only delete broken files, don't rerun")
+    parser.add_argument("--rerun-only", action="store_true", help="Only rerun (assume files already deleted)")
+    parser.add_argument("--workers", type=int, default=4, help="Number of parallel CMD windows to spawn")
+    parser.add_argument("--no-spawn", action="store_true", help="Only generate worker .bat files, don't auto-spawn windows")
+    args = parser.parse_args()
+
+    report_path = REPORT_PATH
+    if not report_path.exists():
+        print(f"[ERROR] {report_path} not found. Run validate_schema.py first.")
+        return
+
+    print("[*] Parsing error report...")
+    errors_dict = parse_error_report(report_path)
+
+    if not errors_dict:
+        print("[OK] No errors found in report!")
+        return
+
+    print(f"Found {len(errors_dict)} requirements with errors:")
+    for req_id, files in sorted(errors_dict.items()):
+        print(f"  - REQ_{req_id}: {', '.join(files)}")
+
+    print("\n" + "="*60)
+    print("[*] Generating cleanup and rerun plan...")
+    print("="*60)
+
+    commands = generate_rerun_commands(errors_dict)
+
+    for cmd_info in commands:
+        print(f"\n[FIX] REQ_{cmd_info['req_id']} ({cmd_info['strategy']}):")
+        print(f"   Delete: {', '.join(cmd_info['files_to_delete'])}")
+        print(f"   Rerun:  {cmd_info['command']}")
+
+    if args.dry_run:
+        print("\n[DRY-RUN] No files were deleted or commands executed.")
+        return
+
+    # 执行删除
+    if not args.rerun_only:
+        print("\n" + "="*60)
+        print("[*] Deleting broken files...")
+        print("="*60)
+
+        for cmd_info in commands:
+            deleted = delete_files(cmd_info['req_id'], cmd_info['files_to_delete'])
+            for f in deleted:
+                print(f"   ✓ Deleted: {f}")
+
+    if args.delete_only:
+        print("\n[DELETE-ONLY] Files deleted. Skipping rerun.")
+        return
+
+    # 生成多窗口并行批处理脚本
+    print("\n" + "="*60)
+    print("[*] Generating multi-window parallel batch scripts...")
+    print("="*60)
+
+    total_tasks = len(commands)
+    num_workers = min(args.workers, total_tasks)
+
+    # 使用 round-robin 分配任务到各个 worker
+    worker_tasks = {i: [] for i in range(num_workers)}
+    for i, cmd_info in enumerate(commands):
+        worker_id = i % num_workers
+        worker_tasks[worker_id].append(cmd_info)
+
+    print(f"Distributing {total_tasks} tasks across {num_workers} workers...")
+
+    # bat 文件生成在 pipeline 根目录(run_pipeline.py 所在目录)
+    bat_dir = PIPELINE_DIR
+
+    # 为每个 worker 生成独立的批处理文件
+    for worker_id, tasks in worker_tasks.items():
+        if not tasks:
+            continue
+
+        bat_path = bat_dir / f"fix_worker_{worker_id}.bat"
+        with open(bat_path, "w", encoding="utf-8") as f:
+            f.write("@echo off\n")
+            f.write("cd /d %~dp0\n")
+            f.write("if exist ..\\..\\venv\\Scripts\\activate.bat call ..\\..\\venv\\Scripts\\activate.bat\n")
+            f.write("if exist ..\\..\\.venv\\Scripts\\activate.bat call ..\\..\\.venv\\Scripts\\activate.bat\n")
+            f.write(f"echo Worker {worker_id} starting with {len(tasks)} tasks...\n")
+            f.write("echo ========================================\n\n")
+
+            for cmd_info in tasks:
+                f.write(f"echo.\n")
+                f.write(f"echo ========================================\n")
+                f.write(f"echo [Worker {worker_id}] Fixing REQ_{cmd_info['req_id']} ({cmd_info['strategy']})\n")
+                f.write(f"echo ========================================\n")
+                f.write(f"{cmd_info['command']}\n")
+                f.write(f"timeout /t 2 > NUL\n\n")
+
+            f.write(f"echo.\n")
+            f.write(f"echo ========================================\n")
+            f.write(f"echo Worker {worker_id} completed all tasks!\n")
+            f.write(f"echo ========================================\n")
+            f.write("pause\n")
+
+        print(f"  Worker {worker_id}: {len(tasks)} tasks -> {bat_path}")
+
+    # 生成主启动脚本
+    launcher_path = bat_dir / "fix_broken_outputs_parallel.bat"
+    with open(launcher_path, "w", encoding="utf-8") as f:
+        f.write("@echo off\n")
+        f.write("cd /d %~dp0\n")
+        f.write(f"echo Spawning {num_workers} parallel workers to fix {total_tasks} broken outputs...\n")
+        f.write("echo ========================================\n\n")
+
+        for worker_id in range(num_workers):
+            if worker_id in worker_tasks and worker_tasks[worker_id]:
+                f.write(f'start "Fix_Worker_{worker_id}" cmd /c fix_worker_{worker_id}.bat\n')
+                f.write(f"timeout /t 1 > NUL\n")
+
+        f.write("\necho.\n")
+        f.write(f"echo All {num_workers} workers have been launched in separate windows.\n")
+        f.write("echo ========================================\n")
+        f.write("pause\n")
+
+    print(f"\n[OK] Parallel batch scripts generated!")
+    print(f"\nTo execute all fixes in parallel, run:")
+    print(f"   {launcher_path}")
+    print(f"\nOr manually launch individual workers:")
+    for worker_id in range(num_workers):
+        if worker_id in worker_tasks and worker_tasks[worker_id]:
+            print(f"   fix_worker_{worker_id}.bat")
+
+    # 如果不是 no-spawn 模式,自动启动窗口
+    if not args.no_spawn:
+        import subprocess
+        print(f"\n[*] Auto-spawning {num_workers} worker windows...")
+        for worker_id in range(num_workers):
+            if worker_id in worker_tasks and worker_tasks[worker_id]:
+                bat_file = bat_dir / f"fix_worker_{worker_id}.bat"
+                subprocess.Popen(
+                    ["cmd.exe", "/c", "start", f"Fix_Worker_{worker_id}", "cmd.exe", "/c", str(bat_file.absolute())],
+                    cwd=str(base_dir)
+                )
+                print(f"  Launched Worker {worker_id}")
+        print(f"\n[OK] All workers launched! Check the new CMD windows.")
+
+if __name__ == "__main__":
+    main()

+ 17 - 0
examples/process_pipeline/script/schema_errors_report.txt

@@ -0,0 +1,17 @@
+[ERROR] Found 16 files with incorrect schemas/formats:
+   - output\008\capabilities_extracted.json: Schema mismatch:  missing keys: ['extracted_capabilities']
+   - output\010\blueprint.json: Schema mismatch:  missing keys: ['distilled_cases', 'blueprints']
+   - output\031\strategy.json: Schema mismatch:  missing keys: ['strategies', 'uncovered_requirements']
+   - output\034\capabilities_extracted.json: Schema mismatch:  missing keys: ['extracted_capabilities']
+   - output\044\capabilities_extracted.json: Schema mismatch: extracted_capabilities[0] missing keys: ['id', 'name', 'description', 'criterion', 'effects', 'implements']
+   - output\045\capabilities_extracted.json: Schema mismatch: extracted_capabilities[0] missing keys: ['effects']
+   - output\046\capabilities_extracted.json: Schema mismatch: extracted_capabilities[0] missing keys: ['effects']
+   - output\048\blueprint.json: Schema mismatch:  missing keys: ['distilled_cases', 'blueprints']
+   - output\053\strategy.json: Schema mismatch:  missing keys: ['strategies', 'uncovered_requirements']
+   - output\066\strategy.json: Schema mismatch:  missing keys: ['strategies', 'uncovered_requirements']
+   - output\070\strategy.json: Schema mismatch:  missing keys: ['strategies', 'uncovered_requirements']
+   - output\079\blueprint.json: Schema mismatch:  missing keys: ['distilled_cases', 'blueprints']
+   - output\012\raw_cases\case_bili.json: Schema mismatch: cases[14] missing keys: ['workflow_process']
+   - output\051\raw_cases\case_bili.json: Schema mismatch: cases[0] missing keys: ['images']
+   - output\087\raw_cases\case_youtube.json: Schema mismatch: cases[3] missing keys: ['workflow_process']
+   - output\098\raw_cases\case_youtube.json: Schema mismatch: Root is not a dict

+ 227 - 0
examples/process_pipeline/script/validate_schema.py

@@ -0,0 +1,227 @@
+import json
+from pathlib import Path
+
+def check_keys(data, expected_keys, path_context=""):
+    missing = [k for k in expected_keys if k not in data]
+    if missing:
+        return f"{path_context} missing keys: {missing}"
+    return None
+
+def validate_case(data):
+    if not isinstance(data, dict): return "Root is not a dict"
+    err = check_keys(data, ["requirement", "cases"])
+    if err: return err
+    if not isinstance(data["cases"], list): return "'cases' is not a list"
+
+    if len(data["cases"]) == 0:
+        return "'cases' array is empty"
+
+    for i, c in enumerate(data["cases"]):
+        err = check_keys(c, ["id", "title", "platform", "source_url", "metrics", "user_feedback", "images", "input_details", "output_details", "workflow_process"], f"cases[{i}]")
+        if err: return err
+        if not isinstance(c.get("images", []), list): return f"cases[{i}].images must be a list"
+
+        # 检查关键字段是否为空
+        if not (c.get("title") or "").strip():
+            return f"cases[{i}].title is empty"
+        wp = c.get("workflow_process")
+        if not wp or (isinstance(wp, str) and not wp.strip()) or (isinstance(wp, list) and len(wp) == 0):
+            return f"cases[{i}].workflow_process is empty"
+    return None
+
+def validate_blueprint(data):
+    if not isinstance(data, dict): return "Root is not a dict"
+    err = check_keys(data, ["requirement", "distilled_cases", "blueprints"])
+    if err: return err
+
+    if not isinstance(data["blueprints"], list): return "'blueprints' is not a list"
+    if len(data["blueprints"]) == 0:
+        return "'blueprints' array is empty"
+
+    for i, bp in enumerate(data["blueprints"]):
+        err = check_keys(bp, ["name", "phases", "reasoning"], f"blueprints[{i}]")
+        if err: return err
+        if not isinstance(bp.get("phases", []), list): return f"blueprints[{i}].phases must be a list"
+
+        # 检查关键字段是否为空
+        if not (bp.get("name") or "").strip():
+            return f"blueprints[{i}].name is empty"
+        if len(bp.get("phases", [])) == 0:
+            return f"blueprints[{i}].phases array is empty"
+
+    if not isinstance(data["distilled_cases"], list): return "'distilled_cases' is not a list"
+    if len(data["distilled_cases"]) == 0:
+        return "'distilled_cases' array is empty"
+
+    for i, dc in enumerate(data["distilled_cases"]):
+        err = check_keys(dc, ["id", "title", "source_url", "user_feedback", "workflow_process"], f"distilled_cases[{i}]")
+        if err: return err
+
+        # 检查关键字段是否为空
+        if not (dc.get("title") or "").strip():
+            return f"distilled_cases[{i}].title is empty"
+    return None
+
+def validate_capabilities(data):
+    if not isinstance(data, dict): return "Root is not a dict"
+    err = check_keys(data, ["extracted_capabilities", "requirement"])
+    if err: return err
+
+    if not isinstance(data["extracted_capabilities"], list): return "'extracted_capabilities' is not a list"
+    if len(data["extracted_capabilities"]) == 0:
+        return "'extracted_capabilities' array is empty"
+
+    for i, cap in enumerate(data["extracted_capabilities"]):
+        err = check_keys(cap, ["id", "name", "description", "criterion", "effects", "implements", "is_new", "case_references"], f"extracted_capabilities[{i}]")
+        if err: return err
+        if not isinstance(cap.get("effects", []), list): return f"extracted_capabilities[{i}].effects must be a list"
+        if not isinstance(cap.get("case_references", []), list): return f"extracted_capabilities[{i}].case_references must be a list"
+
+        # 检查关键字段是否为空
+        if not (cap.get("name") or "").strip():
+            return f"extracted_capabilities[{i}].name is empty"
+        if not (cap.get("description") or "").strip():
+            return f"extracted_capabilities[{i}].description is empty"
+    return None
+
+def validate_strategy(data):
+    if not isinstance(data, dict): return "Root is not a dict"
+    err = check_keys(data, ["requirement", "strategies", "uncovered_requirements"])
+    if err: return err
+
+    if not isinstance(data["strategies"], list): return "'strategies' is not a list"
+    if len(data["strategies"]) == 0:
+        return "'strategies' array is empty"
+
+    for i, strat in enumerate(data["strategies"]):
+        err = check_keys(strat, ["is_selected", "name", "source", "workflow_outline", "highlight_coverage", "baseline_coverage", "reasoning", "why_not", "could_switch_if", "coverage_score", "coverage_explanation"], f"strategies[{i}]")
+        if err: return err
+
+        # 检查关键字段是否为空
+        if not (strat.get("name") or "").strip():
+            return f"strategies[{i}].name is empty"
+        if not (strat.get("reasoning") or "").strip():
+            return f"strategies[{i}].reasoning is empty"
+
+        if isinstance(strat.get("workflow_outline"), list):
+            if len(strat["workflow_outline"]) == 0:
+                return f"strategies[{i}].workflow_outline array is empty"
+
+            for j, wo in enumerate(strat["workflow_outline"]):
+                err = check_keys(wo, ["phase", "description", "capabilities"], f"strategies[{i}].workflow_outline[{j}]")
+                if err: return err
+                if not isinstance(wo.get("capabilities", []), list): return f"strategies[{i}].workflow_outline[{j}].capabilities must be a list"
+
+                # 检查关键字段是否为空
+                if not (wo.get("phase") or "").strip():
+                    return f"strategies[{i}].workflow_outline[{j}].phase is empty"
+                if not (wo.get("description") or "").strip():
+                    return f"strategies[{i}].workflow_outline[{j}].description is empty"
+    return None
+
+def check_missing_files(base_dir):
+    """检查每个需求目录是否缺少必需的文件"""
+    missing_files = []
+
+    # 获取所有需求目录(格式为 001, 002, ...)
+    req_dirs = sorted([d for d in base_dir.iterdir() if d.is_dir() and d.name.isdigit()])
+
+    for req_dir in req_dirs:
+        req_id = req_dir.name
+
+        # 检查必需的文件
+        required_files = {
+            "raw_cases": req_dir / "raw_cases",
+            "blueprint.json": req_dir / "blueprint.json",
+            "capabilities_extracted.json": req_dir / "capabilities_extracted.json",
+            "strategy.json": req_dir / "strategy.json"
+        }
+
+        for file_name, file_path in required_files.items():
+            if file_name == "raw_cases":
+                # raw_cases 是目录,检查是否存在且至少有一个 case 文件
+                if not file_path.exists():
+                    missing_files.append((req_id, f"raw_cases directory missing"))
+                elif not list(file_path.glob("case_*.json")):
+                    missing_files.append((req_id, f"raw_cases directory exists but contains no case files"))
+            else:
+                # 其他是文件
+                if not file_path.exists():
+                    missing_files.append((req_id, f"{file_name} missing"))
+
+    return missing_files
+
+def main():
+    base_dir = Path(__file__).parent.parent / "output"
+    if not base_dir.exists():
+        print(f"Error: {base_dir} does not exist.")
+        return
+
+    # 检查文件缺失
+    print(f"[Start] Checking for missing files...")
+    missing_files = check_missing_files(base_dir)
+
+    if missing_files:
+        print(f"[WARNING] Found {len(missing_files)} missing files:")
+        for req_id, issue in missing_files:
+            print(f"   - REQ_{req_id}: {issue}")
+        print("-" * 50)
+    else:
+        print("[OK] All required files are present.")
+        print("-" * 50)
+
+    # 检查 schema
+    json_files = list(base_dir.rglob("*.json"))
+    total_files = len(json_files)
+
+    format_errors = []
+
+    print(f"[Start] Validating schema for {total_files} JSON files...")
+
+    for file_path in json_files:
+        try:
+            with open(file_path, "r", encoding="utf-8") as f:
+                data = json.load(f)
+        except Exception as e:
+            format_errors.append((file_path, f"JSON Parsing Error: {e}"))
+            continue
+            
+        filename = file_path.name
+        rel_path = file_path.relative_to(base_dir.parent)
+        
+        err = None
+        if filename.startswith("case_"):
+            err = validate_case(data)
+        elif filename == "blueprint.json":
+            err = validate_blueprint(data)
+        elif filename == "capabilities_extracted.json":
+            err = validate_capabilities(data)
+        elif filename == "strategy.json":
+            err = validate_strategy(data)
+        else:
+            # Unknown json file
+            pass
+            
+        if err:
+            format_errors.append((rel_path, f"Schema mismatch: {err}"))
+
+    report_path = Path(__file__).parent / "schema_errors_report.txt"
+
+    print("-" * 50)
+    with open(report_path, "w", encoding="utf-8") as out_f:
+        if not format_errors:
+            msg = f"[OK] All {total_files} JSON files match their expected schemas perfectly!"
+            print(msg)
+            out_f.write(msg + "\n")
+        else:
+            msg = f"[ERROR] Found {len(format_errors)} files with incorrect schemas/formats:"
+            print(msg)
+            out_f.write(msg + "\n")
+            for path, error in format_errors:
+                print(f"   - {path}: {error}")
+                out_f.write(f"   - {path}: {error}\n")
+    print("-" * 50)
+    print(f"Schema error details saved to {report_path}")
+
+if __name__ == "__main__":
+    main()

+ 277 - 0
examples/process_pipeline/server.py

@@ -0,0 +1,277 @@
+import asyncio
+import json
+from pathlib import Path
+from typing import Dict, List, Optional
+from datetime import datetime
+from collections import deque
+
+from fastapi import FastAPI, HTTPException, Request
+from fastapi.staticfiles import StaticFiles
+from fastapi.responses import HTMLResponse
+from pydantic import BaseModel
+import uvicorn
+import sys
+
+app = FastAPI(title="Pipeline Dashboard")
+
+BASE_DIR = Path(__file__).parent
+OUTPUT_DIR = BASE_DIR / "output"
+DB_PATH = BASE_DIR / "db_requirements.json"
+PROMPTS_DIR = BASE_DIR / "prompts"
+
+# In-memory storage for active runs
+class ActiveRun:
+    def __init__(self):
+        self.process: Optional[asyncio.subprocess.Process] = None
+        self.logs: deque = deque(maxlen=200)  # Keep last 200 lines
+        self.status: str = "starting"
+        self.start_time: str = datetime.now().isoformat()
+
+active_runs: Dict[int, ActiveRun] = {}
+
+class RunRequest(BaseModel):
+    skip_research: bool = False
+    research_only: bool = False
+    platforms: str = "xhs,youtube,bili,x"
+    use_claude_sdk: bool = False
+    restart_mode: str = "smart"
+
+class MemoRequest(BaseModel):
+    memo: str
+
+class PromptRequest(BaseModel):
+    content: str
+
+@app.get("/api/requirements")
+def get_requirements():
+    if not DB_PATH.exists():
+        raise HTTPException(status_code=404, detail="db_requirements.json not found")
+        
+    with open(DB_PATH, "r", encoding="utf-8") as f:
+        reqs = json.load(f)
+        
+    results = []
+    for i, req in enumerate(reqs):
+        idx_str = f"{(i+1):03d}"
+        dir_path = OUTPUT_DIR / idx_str
+        
+        has_strategy = (dir_path / "strategy.json").exists()
+        has_blueprint = (dir_path / "blueprint.json").exists()
+        has_caps = (dir_path / "capabilities_extracted.json").exists()
+        
+        raw_cases_count = 0
+        raw_cases_dir = dir_path / "raw_cases"
+        if raw_cases_dir.exists():
+            raw_cases_count = len(list(raw_cases_dir.glob("case_*.json")))
+            
+        status = "completed" if has_strategy else ("partial" if raw_cases_count > 0 else "pending")
+        if i in active_runs and active_runs[i].status == "running":
+            status = "running"
+            
+        memo_content = ""
+        memo_path = dir_path / "memo.txt"
+        if memo_path.exists():
+            with open(memo_path, "r", encoding="utf-8") as fm:
+                memo_content = fm.read().strip()
+
+        results.append({
+            "index": i,
+            "id": idx_str,
+            "requirement": req,
+            "status": status,
+            "has_strategy": has_strategy,
+            "has_blueprint": has_blueprint,
+            "has_caps": has_caps,
+            "raw_cases_count": raw_cases_count,
+            "memo": memo_content
+        })
+        
+    return results
+
+@app.get("/api/requirements/{index}/data")
+def get_requirement_data(index: int):
+    idx_str = f"{(index+1):03d}"
+    dir_path = OUTPUT_DIR / idx_str
+    
+    def safe_load_json(p: Path):
+        if not p.exists():
+            return None
+        try:
+            with open(p, "r", encoding="utf-8") as f:
+                return json.load(f)
+        except Exception:
+            return {"error": "Failed to parse JSON"}
+
+    data = {
+        "strategy": safe_load_json(dir_path / "strategy.json"),
+        "blueprint": safe_load_json(dir_path / "blueprint.json"),
+        "capabilities": safe_load_json(dir_path / "capabilities_extracted.json"),
+        "raw_cases": {}
+    }
+    
+    raw_cases_dir = dir_path / "raw_cases"
+    if raw_cases_dir.exists():
+        for f in raw_cases_dir.glob("case_*.json"):
+            data["raw_cases"][f.stem] = safe_load_json(f)
+            
+    return data
+
+@app.get("/api/requirements/{index}/memo")
+def get_memo(index: int):
+    idx_str = f"{(index+1):03d}"
+    memo_path = OUTPUT_DIR / idx_str / "memo.txt"
+    if memo_path.exists():
+        with open(memo_path, "r", encoding="utf-8") as f:
+            return {"memo": f.read()}
+    return {"memo": ""}
+
+@app.post("/api/requirements/{index}/memo")
+def save_memo(index: int, req: MemoRequest):
+    idx_str = f"{(index+1):03d}"
+    dir_path = OUTPUT_DIR / idx_str
+    dir_path.mkdir(parents=True, exist_ok=True)
+    memo_path = dir_path / "memo.txt"
+    with open(memo_path, "w", encoding="utf-8") as f:
+        f.write(req.memo)
+    return {"status": "ok"}
+
+@app.get("/api/prompts")
+def list_prompts():
+    if not PROMPTS_DIR.exists():
+        return []
+    return [f.name for f in PROMPTS_DIR.glob("*.prompt")]
+
+@app.get("/api/prompts/{name}")
+def get_prompt(name: str):
+    if "/" in name or "\\" in name:
+        raise HTTPException(status_code=400, detail="Invalid prompt name")
+    prompt_path = PROMPTS_DIR / name
+    if not prompt_path.exists() or not prompt_path.is_file():
+        raise HTTPException(status_code=404, detail="Prompt not found")
+    with open(prompt_path, "r", encoding="utf-8") as f:
+        return {"content": f.read()}
+
+@app.post("/api/prompts/{name}")
+def save_prompt(name: str, req: PromptRequest):
+    if "/" in name or "\\" in name:
+        raise HTTPException(status_code=400, detail="Invalid prompt name")
+    prompt_path = PROMPTS_DIR / name
+    if not prompt_path.exists() or not prompt_path.is_file():
+        raise HTTPException(status_code=404, detail="Prompt not found")
+    with open(prompt_path, "w", encoding="utf-8") as f:
+        f.write(req.content)
+    return {"status": "ok"}
+
+async def run_pipeline_task(index: int, run_req: RunRequest):
+    run_state = active_runs[index]
+    run_state.status = "running"
+    
+    dir_path = OUTPUT_DIR / f"{(index+1):03d}"
+    
+    mode = run_req.restart_mode
+    
+    if mode in ['phase1_platforms', 'phase2_capabilities', 'phase2_blueprint', 'phase2_all', 'phase3']:
+        if (dir_path / "strategy.json").exists(): (dir_path / "strategy.json").unlink()
+        
+    if mode in ['phase1_platforms', 'phase2_all', 'phase2_blueprint']:
+        if (dir_path / "blueprint.json").exists(): (dir_path / "blueprint.json").unlink()
+        
+    if mode in ['phase1_platforms', 'phase2_all', 'phase2_capabilities']:
+        if (dir_path / "capabilities_extracted.json").exists(): (dir_path / "capabilities_extracted.json").unlink()
+        
+    if mode == 'phase1_platforms':
+        raw_cases_dir = dir_path / "raw_cases"
+        if raw_cases_dir.exists() and run_req.platforms:
+            plats = [p.strip() for p in run_req.platforms.split(",") if p.strip()]
+            for p in plats:
+                f = raw_cases_dir / f"case_{p}.json"
+                if f.exists():
+                    f.unlink()
+        run_req.skip_research = False
+    elif mode in ['phase2_capabilities', 'phase2_blueprint', 'phase2_all', 'phase3']:
+        run_req.skip_research = True
+    
+    # build command
+    script_path = BASE_DIR / "run_pipeline.py"
+    cmd = [sys.executable, str(script_path), "--index", str(index)]
+    if run_req.skip_research:
+        cmd.append("--skip-research")
+    if run_req.research_only:
+        cmd.append("--research-only")
+    if run_req.platforms:
+        cmd.extend(["--platforms", run_req.platforms])
+    if run_req.use_claude_sdk:
+        cmd.append("--use-claude-sdk")
+        
+    run_state.logs.append(f"Starting command: {' '.join(cmd)}\n")
+    
+    import threading
+    import subprocess
+    import os
+    
+    def run_process():
+        try:
+            process = subprocess.Popen(
+                cmd,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.STDOUT,
+                text=True,
+                encoding="utf-8",
+                bufsize=1,
+                cwd=str(BASE_DIR),
+                env=dict(os.environ, PYTHONIOENCODING="utf-8")
+            )
+            run_state.process = process
+            
+            for line in iter(process.stdout.readline, ''):
+                if line:
+                    run_state.logs.append(line)
+            
+            process.stdout.close()
+            return_code = process.wait()
+            
+            run_state.logs.append(f"\nProcess exited with code {return_code}")
+            run_state.status = "completed" if return_code == 0 else "failed"
+        except Exception as e:
+            run_state.logs.append(f"\nException occurred: {repr(e)}")
+            run_state.status = "failed"
+            
+    thread = threading.Thread(target=run_process)
+    thread.start()
+
+@app.post("/api/pipeline/run/{index}")
+async def trigger_pipeline(index: int, req: RunRequest):
+    if index in active_runs and active_runs[index].status == "running":
+        raise HTTPException(status_code=400, detail="Pipeline already running for this index")
+        
+    active_runs[index] = ActiveRun()
+    asyncio.create_task(run_pipeline_task(index, req))
+    return {"message": "Pipeline started", "index": index}
+
+@app.get("/api/pipeline/status")
+def get_all_status():
+    res = {}
+    for idx, run in active_runs.items():
+        res[idx] = {
+            "status": run.status,
+            "start_time": run.start_time,
+            "logs": list(run.logs)
+        }
+    return res
+
+# Mount UI static files
+ui_dir = BASE_DIR / "ui"
+ui_dir.mkdir(exist_ok=True)
+app.mount("/static", StaticFiles(directory=str(ui_dir)), name="static")
+
+@app.get("/")
+def serve_ui():
+    index_html = ui_dir / "index.html"
+    if not index_html.exists():
+        return HTMLResponse("UI not found. Please create ui/index.html", status_code=404)
+    with open(index_html, "r", encoding="utf-8") as f:
+        return HTMLResponse(f.read())
+
+if __name__ == "__main__":
+    print("Starting Pipeline Dashboard server on http://127.0.0.1:8080")
+    uvicorn.run("server:app", host="0.0.0.0", port=8080, reload=False)

+ 767 - 0
examples/process_pipeline/ui/app.js

@@ -0,0 +1,767 @@
+let requirements = [];
+let currentSelectedIndex = null;
+let activeRuns = {};
+let statusInterval = null;
+
+let currentPromptName = null;
+const modalPrompts = document.getElementById('prompts-modal');
+const elPromptList = document.getElementById('prompt-list');
+const elPromptTextarea = document.getElementById('prompt-textarea');
+const elPromptStatus = document.getElementById('prompt-save-status');
+
+// DOM Elements
+const elTaskList = document.getElementById('task-list');
+const elSearchInput = document.getElementById('search-input');
+const elStatsContainer = document.getElementById('stats-container');
+
+const elMainContent = document.getElementById('main-content');
+const elEmptyState = document.getElementById('empty-state');
+const elDetailView = document.getElementById('detail-view');
+
+const elDetailId = document.getElementById('detail-id');
+const elDetailTitle = document.getElementById('detail-title');
+const elStatusBanner = document.getElementById('status-banner');
+const elStatusText = document.getElementById('status-text');
+
+// Form logic
+const selectForcePhase = document.getElementById('select-force-phase');
+const groupPlatforms = document.getElementById('group-platforms');
+
+if (selectForcePhase && groupPlatforms) {
+    selectForcePhase.addEventListener('change', (e) => {
+        const val = e.target.value;
+        if (val.startsWith('phase2') || val === 'phase3') {
+            groupPlatforms.style.display = 'none';
+        } else {
+            groupPlatforms.style.display = 'block';
+        }
+    });
+}
+
+const jsonStrategy = document.getElementById('json-strategy');
+const jsonBlueprint = document.getElementById('json-blueprint');
+const jsonCaps = document.getElementById('json-caps');
+const jsonRaw = document.getElementById('json-raw');
+
+// Modals
+const modalRun = document.getElementById('run-modal');
+const modalLogs = document.getElementById('logs-modal');
+const terminalLogs = document.getElementById('terminal-logs');
+
+// Initialize
+async function init() {
+    await fetchRequirements();
+    setupEventListeners();
+    startStatusPolling();
+}
+
+// Fetch Data
+async function fetchRequirements() {
+    try {
+        const res = await fetch('/api/requirements');
+        requirements = await res.json();
+        renderTaskList(requirements);
+        updateStats();
+    } catch (e) {
+        console.error("Failed to fetch requirements", e);
+        elTaskList.innerHTML = '<div style="padding:1rem;color:var(--danger)">Error loading data. Is the backend running?</div>';
+    }
+}
+
+function renderJSON(obj) {
+    if (obj === null) return `<span class="json-null">null</span>`;
+    if (typeof obj === 'number') return `<span class="json-number">${obj}</span>`;
+    if (typeof obj === 'boolean') return `<span class="json-boolean">${obj}</span>`;
+    if (typeof obj === 'string') {
+        const escaped = obj.replace(/</g, '&lt;').replace(/>/g, '&gt;');
+        return `<span class="json-string">"${escaped}"</span>`;
+    }
+    
+    if (Array.isArray(obj)) {
+        if (obj.length === 0) return '[]';
+        let html = '<div class="json-array">[<div class="json-children">';
+        obj.forEach((val, i) => {
+            html += `<div class="json-item">${renderJSON(val)}${i < obj.length - 1 ? ',' : ''}</div>`;
+        });
+        html += '</div>]</div>';
+        return html;
+    }
+    
+    if (typeof obj === 'object') {
+        const keys = Object.keys(obj);
+        if (keys.length === 0) return '{}';
+        let html = '<div class="json-object">{<div class="json-children">';
+        keys.forEach((k, i) => {
+            html += `<div class="json-prop"><span class="json-key">"${k}"</span>: ${renderJSON(obj[k])}${i < keys.length - 1 ? ',' : ''}</div>`;
+        });
+        html += '</div>}</div>';
+        return html;
+    }
+    return String(obj);
+}
+
+function renderRawCases(rawCasesObj) {
+    if (!rawCasesObj || Object.keys(rawCasesObj).length === 0) return 'No raw cases data';
+    const platforms = Object.keys(rawCasesObj);
+    let html = `<div class="sub-tabs">`;
+    platforms.forEach((p, i) => {
+        const name = p.replace('case_', '').toUpperCase();
+        html += `<button class="sub-tab-btn ${i === 0 ? 'active' : ''}" onclick="selectSubTab('${p}')">${name}</button>`;
+    });
+    html += `</div><div class="sub-tab-contents">`;
+    platforms.forEach((p, i) => {
+        html += `<div id="sub-tab-${p}" class="sub-tab-pane ${i === 0 ? '' : 'hidden'}">`;
+        const cases = rawCasesObj[p].cases || [];
+        if (cases.length === 0) {
+            html += `<p style="color:var(--text-muted)">No cases found for this platform.</p>`;
+        } else {
+            const platCode = p.replace('case_', '');
+            cases.forEach(c => {
+                html += `<div class="data-card" id="case-card-${platCode}-${c.id}">
+                    <div class="card-header">
+                        <div class="card-title">📝 ${c.title || 'Untitled'}</div>
+                        <a href="${c.source_url}" target="_blank" class="badge-emoji primary" style="text-decoration:none">🔗 View Source</a>
+                    </div>
+                    <div class="card-body">
+                        <div class="tags-container">
+                            <span class="badge-emoji">📱 Platform: ${c.platform || p}</span>
+                            <span class="badge-emoji">❤️ Likes: ${c.metrics?.likes || 0}</span>
+                            <span class="badge-emoji">💬 Comments: ${c.metrics?.comments || 0}</span>
+                            <span class="badge-emoji">🔄 Shares: ${c.metrics?.shares || 0}</span>
+                        </div>`;
+                if (c.user_feedback) {
+                    html += `<div class="card-section"><div class="section-title">🗣️ User Feedback</div>`;
+                    if (Array.isArray(c.user_feedback)) {
+                        html += `<ul>`;
+                        c.user_feedback.forEach(fb => html += `<li>${fb}</li>`);
+                        html += `</ul>`;
+                    } else {
+                        html += `<p>${c.user_feedback}</p>`;
+                    }
+                    html += `</div>`;
+                }
+                if (c.workflow_process) {
+                    html += `<div class="card-section"><div class="section-title">🧱 Workflow Process</div>`;
+                    if (Array.isArray(c.workflow_process)) {
+                        html += `<div class="phase-list">`;
+                        c.workflow_process.forEach(wp => html += `<div class="phase-item">${wp}</div>`);
+                        html += `</div>`;
+                    } else {
+                        html += `<p>${c.workflow_process}</p>`;
+                    }
+                    html += `</div>`;
+                }
+                if (c.images && c.images.length > 0) {
+                    html += `<div class="card-section"><div class="section-title">🖼️ Images</div><div class="image-gallery">`;
+                    c.images.forEach(img => {
+                        const url = typeof img === 'string' ? img : img.url;
+                        const desc = (typeof img === 'object' ? img.description : '') || '';
+                        html += `<div class="image-item"><img src="${url}" alt="${desc}" title="${desc}"><div class="image-caption">${desc}</div></div>`;
+                    });
+                    html += `</div></div>`;
+                }
+                html += `</div></div>`;
+            });
+        }
+        html += `</div>`;
+    });
+    html += `</div>`;
+    return html;
+}
+
+function renderCapabilities(capsObj) {
+    if (!capsObj || !capsObj.extracted_capabilities) return 'No capabilities data';
+    const caps = capsObj.extracted_capabilities;
+    if (caps.length === 0) return '<p>No capabilities extracted.</p>';
+    
+    let html = ``;
+    caps.forEach(cap => {
+        const isNew = cap.is_new ? '<span class="badge-emoji success">✨ New Capability</span>' : '';
+        html += `<div class="data-card">
+            <div class="card-header">
+                <div class="card-title">⚡ [${cap.id || 'N/A'}] ${cap.name || 'Unnamed'}</div>
+                ${isNew}
+            </div>
+            <div class="card-body">
+                <p>${cap.description || ''}</p>
+                <div class="card-section"><div class="section-title">✨ Effects</div><ul>`;
+        if (cap.effects) cap.effects.forEach(eff => html += `<li>${eff}</li>`);
+        html += `</ul></div>`;
+        if (cap.implements && Object.keys(cap.implements).length > 0) {
+            html += `<div class="card-section"><div class="section-title">🛠️ Implements Tools</div><div class="tags-container">`;
+            for (const [tool, args] of Object.entries(cap.implements)) {
+                html += `<span class="badge-emoji primary" title="${args}">🔧 ${tool}</span>`;
+            }
+            html += `</div></div>`;
+        }
+        if (cap.case_references && cap.case_references.length > 0) {
+            html += `<div class="card-section"><div class="section-title">📌 Source Cases</div><div class="tags-container" style="gap:0.8rem">`;
+            cap.case_references.forEach(ref => {
+                let caseId = null;
+                let title = ref;
+                const matchA = ref.match(/^case_([a-z]+)_(\d+)(?:[::\s]+(.*))?/);
+                if (matchA) {
+                    caseId = `${matchA[1]}-case_${matchA[2]}`;
+                    title = matchA[3] || ref;
+                } else {
+                    const matchB = ref.match(/^([a-z]+)[\/\s](case_\d+)(?:[::\s]+(.*))?/);
+                    if (matchB) {
+                        caseId = `${matchB[1]}-${matchB[2]}`;
+                        title = matchB[3] || ref;
+                    } else {
+                        const matchC = ref.match(/^case_(\d+)_([a-z]+)(?:[::\s]+(.*))?/);
+                        if (matchC) {
+                            caseId = `${matchC[2]}-case_${matchC[1]}`;
+                            title = matchC[3] || ref;
+                        }
+                    }
+                }
+                
+                if (caseId) {
+                    html += `<a href="#" onclick="jumpToCase('${caseId}'); return false;" class="badge-emoji primary" style="text-decoration:none; white-space:normal; text-align:left; line-height:1.4">
+                        <strong>🔍 ${caseId.replace('-', ' ')}</strong><br>
+                        <span style="font-size:0.75rem">${title.substring(0, 40) + (title.length>40?'...':'')}</span>
+                    </a>`;
+                } else {
+                    html += `<span class="badge-emoji" style="white-space:normal; text-align:left; line-height:1.4; font-size:0.75rem">${ref}</span>`;
+                }
+            });
+            html += `</div></div>`;
+        }
+        html += `</div></div>`;
+    });
+    return html;
+}
+
+function renderBlueprint(bpObj) {
+    if (!bpObj || !bpObj.blueprints) return 'No blueprint data';
+    let html = ``;
+    
+    if (bpObj.distilled_cases && bpObj.distilled_cases.length > 0) {
+        html += `<div style="margin-bottom: 1.5rem; padding: 1rem; background: rgba(0,0,0,0.2); border-radius: 8px;">
+            <h3 style="color:var(--text-main); margin-bottom:0.8rem">📚 Source Cases for Blueprint</h3>
+            <div class="tags-container" style="gap:0.8rem">`;
+        bpObj.distilled_cases.forEach(c => {
+            let targetId = c.id;
+            const matchA = targetId.match(/^case_([a-z]+)_(\d+)/);
+            if (matchA) {
+                targetId = `${matchA[1]}-case_${matchA[2]}`;
+            } else {
+                const matchB = targetId.match(/^([a-z]+)[\/\s](case_\d+)/);
+                if (matchB) {
+                    targetId = `${matchB[1]}-${matchB[2]}`;
+                } else {
+                    const matchC = targetId.match(/^case_(\d+)_([a-z]+)/);
+                    if (matchC) targetId = `${matchC[2]}-case_${matchC[1]}`;
+                }
+            }
+            
+            html += `<a href="#" onclick="jumpToCase('${targetId}'); return false;" class="badge-emoji primary" style="text-decoration:none; white-space:normal; text-align:left; line-height:1.4">
+                <strong>🔍 ${c.id}</strong><br>
+                <span style="font-size:0.75rem">${c.title ? c.title.substring(0, 40) + (c.title.length>40?'...':'') : 'View Source'}</span>
+            </a>`;
+        });
+        html += `</div></div>`;
+    }
+
+    bpObj.blueprints.forEach(bp => {
+        html += `<div class="data-card">
+            <div class="card-header">
+                <div class="card-title">🗺️ ${bp.name || 'Unnamed'}</div>
+            </div>
+            <div class="card-body">
+                <div class="card-section"><div class="section-title">🧠 Reasoning</div>
+                <p>${bp.reasoning || ''}</p></div>
+                <div class="card-section"><div class="section-title">📍 Phases</div><div class="phase-list">`;
+        if (bp.phases) bp.phases.forEach(ph => {
+            html += `<div class="phase-item">
+                <div class="phase-title">${ph.phase || ''}</div>
+                <div>${ph.description || ''}</div>
+            </div>`;
+        });
+        html += `</div></div></div>`;
+    });
+    return html;
+}
+
+function renderStrategy(stratObj) {
+    if (!stratObj || !stratObj.strategies) return 'No strategy data';
+    let html = `<div style="margin-bottom: 1.5rem; padding: 1rem; background: rgba(0,0,0,0.2); border-radius: 8px;">
+        <h3 style="color:var(--text-main); margin-bottom:0.5rem">🎯 Requirement</h3>
+        <p style="color:var(--text-muted)">${stratObj.requirement || ''}</p>
+    </div>`;
+    
+    stratObj.strategies.sort((a,b) => (b.is_selected === true) - (a.is_selected === true));
+    
+    stratObj.strategies.forEach(strat => {
+        const isSelected = strat.is_selected;
+        const icon = isSelected ? '🎯' : '🥈';
+        const badge = isSelected ? '<span class="badge-emoji success">⭐ Selected Strategy</span>' : '<span class="badge-emoji warning">Alternative</span>';
+        
+        let scoreHtml = '';
+        if (strat.coverage_score !== undefined) {
+            const score = strat.coverage_score;
+            const deg = score * 360;
+            const scoreClass = score >= 0.8 ? '' : (score >= 0.5 ? 'medium' : 'low');
+            scoreHtml = `<div class="score-container">
+                <div class="score-circle ${scoreClass}" style="--score-deg: ${deg}deg"><span>${Math.round(score * 100)}%</span></div>
+                <div class="score-text"><strong>Coverage Score</strong><br>${strat.coverage_explanation || ''}</div>
+            </div>`;
+        }
+        
+        html += `<div class="data-card" style="${isSelected ? 'border-color: var(--accent-primary); box-shadow: 0 0 15px rgba(59,130,246,0.1);' : ''}">
+            <div class="card-header">
+                <div class="card-title">${icon} ${strat.name || 'Unnamed'}</div>
+                ${badge}
+            </div>
+            <div class="card-body">
+                ${scoreHtml}
+                <div class="tags-container" style="margin-bottom:1rem">
+                    <span class="badge-emoji">📥 Source: ${strat.source || 'N/A'}</span>
+                </div>`;
+                
+        if (strat.reasoning) html += `<div class="card-section"><div class="section-title">🧠 Reasoning</div><p>${strat.reasoning}</p></div>`;
+        if (strat.why_not) html += `<div class="card-section"><div class="section-title">❌ Why Not Selected</div><p>${strat.why_not}</p></div>`;
+        
+        if (strat.workflow_outline && strat.workflow_outline.length > 0) {
+            html += `<div class="card-section"><div class="section-title">🧱 Workflow Outline</div><div class="phase-list">`;
+            strat.workflow_outline.forEach(wo => {
+                html += `<div class="phase-item">
+                    <div class="phase-title">${wo.phase}</div>
+                    <div style="margin-bottom:0.5rem">${wo.description}</div>
+                    <div class="tags-container">`;
+                if (wo.capabilities) {
+                    wo.capabilities.forEach(cap => html += `<span class="badge-emoji primary">⚡ ${cap.name}</span>`);
+                }
+                html += `</div></div>`;
+            });
+            html += `</div></div>`;
+        }
+        html += `</div></div>`;
+    });
+    
+    if (stratObj.uncovered_requirements && stratObj.uncovered_requirements.length > 0) {
+        html += `<div class="data-card" style="border-color: var(--danger)">
+            <div class="card-header"><div class="card-title">⚠️ Uncovered Requirements</div></div>
+            <div class="card-body"><ul>`;
+        stratObj.uncovered_requirements.forEach(req => html += `<li>${req}</li>`);
+        html += `</ul></div></div>`;
+    }
+    
+    return html;
+}
+
+window.selectSubTab = function(p) {
+    document.querySelectorAll('.sub-tab-btn').forEach(b => {
+        b.classList.remove('active');
+        if(b.textContent === p.replace('case_', '').toUpperCase()) b.classList.add('active');
+    });
+    document.querySelectorAll('.sub-tab-pane').forEach(pane => pane.classList.add('hidden'));
+    const target = document.getElementById('sub-tab-' + p);
+    if(target) target.classList.remove('hidden');
+};
+
+window.jumpToCase = function(caseId) {
+    // Switch to raw tab
+    document.querySelector('.tab-btn[data-target="tab-raw"]').click();
+    
+    // Find the case card
+    const targetCard = document.getElementById('case-card-' + caseId);
+    if (targetCard) {
+        // Find which sub-tab pane it's inside
+        const pane = targetCard.closest('.sub-tab-pane');
+        if (pane) {
+            const platformId = pane.id.replace('sub-tab-', '');
+            selectSubTab(platformId);
+        }
+        
+        // Scroll into view
+        targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
+        
+        // Add highlight
+        targetCard.classList.remove('highlight-pulse');
+        void targetCard.offsetWidth; // Trigger reflow
+        targetCard.classList.add('highlight-pulse');
+    } else {
+        alert("Case not found in raw cases data.");
+    }
+};
+
+async function fetchMemo(index) {
+    const elTextarea = document.getElementById('memo-textarea');
+    const elStatus = document.getElementById('memo-status');
+    if(!elTextarea) return;
+    elTextarea.value = 'Loading...';
+    elTextarea.disabled = true;
+    try {
+        const res = await fetch(`/api/requirements/${index}/memo`);
+        const data = await res.json();
+        elTextarea.value = data.memo || '';
+        elStatus.textContent = '';
+    } catch (e) {
+        elTextarea.value = '';
+        console.error("Failed to fetch memo", e);
+    }
+    elTextarea.disabled = false;
+}
+
+async function fetchPromptsList() {
+    try {
+        const res = await fetch('/api/prompts');
+        const list = await res.json();
+        if(!elPromptList) return;
+        elPromptList.innerHTML = '';
+        list.forEach((p, idx) => {
+            const div = document.createElement('div');
+            div.className = 'prompt-tab';
+            div.textContent = p;
+            div.onclick = () => selectPrompt(p, div);
+            elPromptList.appendChild(div);
+            if (idx === 0) selectPrompt(p, div);
+        });
+    } catch (e) {
+        console.error("Failed to load prompts", e);
+    }
+}
+
+async function selectPrompt(name, tabEl) {
+    currentPromptName = name;
+    document.querySelectorAll('.prompt-tab').forEach(el => el.classList.remove('active'));
+    tabEl.classList.add('active');
+    
+    elPromptTextarea.value = 'Loading...';
+    elPromptTextarea.disabled = true;
+    try {
+        const res = await fetch(`/api/prompts/${name}`);
+        const data = await res.json();
+        elPromptTextarea.value = data.content || '';
+    } catch (e) {
+        elPromptTextarea.value = 'Error loading prompt.';
+    }
+    elPromptTextarea.disabled = false;
+    elPromptStatus.textContent = '';
+}
+
+async function fetchRequirementData(index) {
+    try {
+        const res = await fetch(`/api/requirements/${index}/data`);
+        const data = await res.json();
+        
+        jsonStrategy.innerHTML = data.strategy ? renderStrategy(data.strategy) : '<p style="color:var(--text-muted)">No strategy data</p>';
+        jsonBlueprint.innerHTML = data.blueprint ? renderBlueprint(data.blueprint) : '<p style="color:var(--text-muted)">No blueprint data</p>';
+        jsonCaps.innerHTML = data.capabilities ? renderCapabilities(data.capabilities) : '<p style="color:var(--text-muted)">No capabilities data</p>';
+        jsonRaw.innerHTML = data.raw_cases ? renderRawCases(data.raw_cases) : '<p style="color:var(--text-muted)">No raw cases data</p>';
+    } catch (e) {
+        console.error("Failed to fetch data", e);
+    }
+}
+
+async function pollStatus() {
+    try {
+        const res = await fetch('/api/pipeline/status');
+        const statusData = await res.json();
+        
+        let needsListUpdate = false;
+
+        // Check if any status changed
+        for(const [idxStr, runInfo] of Object.entries(statusData)) {
+            const idx = parseInt(idxStr);
+            if (!activeRuns[idx] || activeRuns[idx].status !== runInfo.status) {
+                needsListUpdate = true;
+            }
+            activeRuns[idx] = runInfo;
+
+            // Update logs if modal is open for this index
+            if (currentSelectedIndex === idx && !modalLogs.classList.contains('hidden')) {
+                terminalLogs.textContent = runInfo.logs.join('');
+                terminalLogs.scrollTop = terminalLogs.scrollHeight;
+            }
+            
+            // Update detail view banner if this is the selected one
+            if (currentSelectedIndex === idx) {
+                updateDetailBannerStatus(runInfo.status);
+            }
+        }
+        
+        if (needsListUpdate) {
+            // update in requirements array
+            requirements.forEach(req => {
+                if (activeRuns[req.index]) {
+                    req.status = activeRuns[req.index].status;
+                }
+            });
+            renderTaskList(requirements);
+        }
+
+    } catch (e) {
+        console.error("Failed to poll status", e);
+    }
+}
+
+function startStatusPolling() {
+    if (statusInterval) clearInterval(statusInterval);
+    statusInterval = setInterval(pollStatus, 2000);
+}
+
+// Render
+function renderTaskList(list) {
+    elTaskList.innerHTML = '';
+    list.forEach(req => {
+        const div = document.createElement('div');
+        div.className = `task-item ${currentSelectedIndex === req.index ? 'active' : ''}`;
+        div.onclick = () => selectRequirement(req.index);
+
+        let statusTag = '';
+        if (req.status === 'running') statusTag = '<span class="tag running">Running</span>';
+        else if (req.status === 'completed') statusTag = '<span class="tag success">Complete</span>';
+        else if (req.status === 'partial') statusTag = '<span class="tag warning">Partial</span>';
+        else statusTag = '<span class="tag">Pending</span>';
+
+        let memoHtml = '';
+        if (req.memo && req.memo.trim() !== '') {
+            memoHtml = `<div class="task-memo" title="${req.memo}">${req.memo}</div>`;
+        }
+
+        div.innerHTML = `
+            <div class="task-id">#${req.id}</div>
+            <div class="task-req" title="${req.requirement}">${req.requirement}</div>
+            ${memoHtml}
+            <div class="task-tags">
+                ${statusTag}
+                <span class="tag">Cases: ${req.raw_cases_count}</span>
+            </div>
+        `;
+        elTaskList.appendChild(div);
+    });
+}
+
+function updateStats() {
+    const total = requirements.length;
+    const completed = requirements.filter(r => r.status === 'completed').length;
+    const running = requirements.filter(r => r.status === 'running').length;
+    
+    elStatsContainer.innerHTML = `
+        <span>Total: ${total}</span>
+        <span style="color:var(--success)">Done: ${completed}</span>
+        ${running > 0 ? `<span style="color:var(--accent-primary)">Running: ${running}</span>` : ''}
+    `;
+}
+
+function selectRequirement(index) {
+    currentSelectedIndex = index;
+    const req = requirements.find(r => r.index === index);
+    if (!req) return;
+
+    // Update List UI
+    document.querySelectorAll('.task-item').forEach(el => el.classList.remove('active'));
+    // We re-render to be safe, but simple class toggle is better.
+    renderTaskList(requirements);
+
+    // Update Detail UI
+    elEmptyState.classList.add('hidden');
+    elDetailView.classList.remove('hidden');
+    
+    elDetailId.textContent = req.id;
+    elDetailTitle.textContent = req.requirement;
+
+    updateDetailBannerStatus(activeRuns[index] ? activeRuns[index].status : req.status);
+
+    // Fetch data
+    jsonStrategy.textContent = 'Loading...';
+    jsonBlueprint.textContent = 'Loading...';
+    jsonCaps.textContent = 'Loading...';
+    jsonRaw.textContent = 'Loading...';
+    fetchRequirementData(index);
+    fetchMemo(index);
+}
+
+function updateDetailBannerStatus(status) {
+    if (status === 'running') {
+        elStatusBanner.classList.remove('hidden');
+        elStatusBanner.style.background = 'rgba(59, 130, 246, 0.1)';
+        elStatusText.textContent = 'Pipeline is currently running...';
+        elStatusBanner.querySelector('.status-indicator').style.display = 'block';
+        elStatusBanner.querySelector('.status-indicator').style.background = 'var(--accent-primary)';
+    } else if (status === 'failed') {
+        elStatusBanner.classList.remove('hidden');
+        elStatusBanner.style.background = 'rgba(239, 68, 68, 0.1)';
+        elStatusText.textContent = 'Pipeline run failed.';
+        elStatusBanner.querySelector('.status-indicator').style.display = 'block';
+        elStatusBanner.querySelector('.status-indicator').style.background = 'var(--danger)';
+    } else {
+        elStatusBanner.classList.add('hidden');
+    }
+}
+
+// Actions
+async function triggerRun() {
+    if (currentSelectedIndex === null) return;
+    
+    const requestData = {
+        platforms: document.getElementById('input-platforms').value,
+        skip_research: false,
+        research_only: false,
+        use_claude_sdk: document.getElementById('check-claude-sdk').checked,
+        restart_mode: document.getElementById('select-force-phase').value
+    };
+    modalRun.classList.add('hidden');
+    
+    // Optimistic UI update
+    const req = requirements.find(r => r.index === currentSelectedIndex);
+    if (req) req.status = 'running';
+    activeRuns[currentSelectedIndex] = { status: 'running', logs: ['Initializing request...'] };
+    renderTaskList(requirements);
+    updateDetailBannerStatus('running');
+
+    try {
+        const res = await fetch(`/api/pipeline/run/${currentSelectedIndex}`, {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify(requestData)
+        });
+        
+        if (!res.ok) {
+            const err = await res.json();
+            alert("Error: " + err.detail);
+        }
+    } catch (e) {
+        console.error("Run failed", e);
+        alert("Failed to trigger run");
+    }
+}
+
+// Event Listeners
+function setupEventListeners() {
+    // Search
+    elSearchInput.addEventListener('input', (e) => {
+        const query = e.target.value.toLowerCase();
+        const filtered = requirements.filter(r => 
+            r.requirement.toLowerCase().includes(query) || 
+            r.id.includes(query)
+        );
+        renderTaskList(filtered);
+    });
+
+    // Tabs
+    document.querySelectorAll('.tab-btn').forEach(btn => {
+        btn.addEventListener('click', () => {
+            document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
+            document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
+            
+            btn.classList.add('active');
+            document.getElementById(btn.dataset.target).classList.add('active');
+        });
+    });
+
+    // Modals
+    document.getElementById('btn-open-run-modal').addEventListener('click', () => {
+        if (currentSelectedIndex !== null) {
+            modalRun.classList.remove('hidden');
+        }
+    });
+
+    document.getElementById('btn-close-modal').addEventListener('click', () => {
+        modalRun.classList.add('hidden');
+    });
+
+    document.getElementById('btn-cancel-run').addEventListener('click', () => {
+        modalRun.classList.add('hidden');
+    });
+
+    document.getElementById('btn-confirm-run').addEventListener('click', triggerRun);
+
+    document.getElementById('btn-view-logs').addEventListener('click', () => {
+        modalLogs.classList.remove('hidden');
+        if (activeRuns[currentSelectedIndex]) {
+            terminalLogs.textContent = activeRuns[currentSelectedIndex].logs.join('');
+            terminalLogs.scrollTop = terminalLogs.scrollHeight;
+        } else {
+            terminalLogs.textContent = 'No logs available.';
+        }
+    });
+
+    document.getElementById('btn-close-logs').addEventListener('click', () => {
+        modalLogs.classList.add('hidden');
+    });
+
+    const btnSaveMemo = document.getElementById('btn-save-memo');
+    if (btnSaveMemo) {
+        btnSaveMemo.addEventListener('click', async () => {
+            if (currentSelectedIndex === null) return;
+            const elTextarea = document.getElementById('memo-textarea');
+            const elStatus = document.getElementById('memo-status');
+            elStatus.textContent = 'Saving...';
+            elStatus.style.color = 'var(--text-muted)';
+            
+            try {
+                const res = await fetch(`/api/requirements/${currentSelectedIndex}/memo`, {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify({ memo: elTextarea.value })
+                });
+                if (res.ok) {
+                    elStatus.textContent = 'Saved!';
+                    elStatus.style.color = 'var(--success)';
+                    setTimeout(() => elStatus.textContent = '', 2000);
+                    
+                    const req = requirements.find(r => r.index === currentSelectedIndex);
+                    if (req) {
+                        req.memo = elTextarea.value;
+                        renderTaskList(requirements);
+                    }
+                } else {
+                    throw new Error("Bad response");
+                }
+            } catch (e) {
+                console.error("Failed to save memo", e);
+                elStatus.textContent = 'Save failed';
+                elStatus.style.color = 'var(--danger)';
+            }
+        });
+    }
+
+    const btnOpenPrompts = document.getElementById('btn-open-prompts');
+    if (btnOpenPrompts) {
+        btnOpenPrompts.addEventListener('click', () => {
+            modalPrompts.classList.remove('hidden');
+            fetchPromptsList();
+        });
+    }
+
+    const btnClosePrompts = document.getElementById('btn-close-prompts');
+    if (btnClosePrompts) {
+        btnClosePrompts.addEventListener('click', () => {
+            modalPrompts.classList.add('hidden');
+        });
+    }
+
+    const btnSavePrompt = document.getElementById('btn-save-prompt');
+    if (btnSavePrompt) {
+        btnSavePrompt.addEventListener('click', async () => {
+            if (!currentPromptName) return;
+            elPromptStatus.textContent = 'Saving...';
+            elPromptStatus.style.color = 'var(--text-muted)';
+            
+            try {
+                const res = await fetch(`/api/prompts/${currentPromptName}`, {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify({ content: elPromptTextarea.value })
+                });
+                if (res.ok) {
+                    elPromptStatus.textContent = 'Saved!';
+                    elPromptStatus.style.color = 'var(--success)';
+                    setTimeout(() => elPromptStatus.textContent = '', 2000);
+                } else {
+                    throw new Error("Failed to save");
+                }
+            } catch(e) {
+                elPromptStatus.textContent = 'Save failed';
+                elPromptStatus.style.color = 'var(--danger)';
+            }
+        });
+    }
+}
+
+// Boot
+init();

+ 167 - 0
examples/process_pipeline/ui/index.html

@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="referrer" content="no-referrer">
+    <title>Pipeline Dashboard</title>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
+    <link rel="stylesheet" href="/static/style.css">
+</head>
+<body>
+    <div class="app-container">
+        <!-- Sidebar -->
+        <aside class="sidebar glass-panel">
+            <div class="sidebar-header">
+                <h2>⚡ Pipeline Tasks</h2>
+                <div class="stats" id="stats-container">
+                    <!-- Stats injected via JS -->
+                </div>
+            </div>
+            <div class="search-box" style="display: flex; gap: 8px;">
+                <input type="text" id="search-input" placeholder="Search requirements..." style="flex: 1;">
+                <button class="btn btn-secondary btn-small" id="btn-open-prompts" title="Edit Prompts" style="padding: 0 10px;">⚙️</button>
+            </div>
+            <div class="task-list" id="task-list">
+                <!-- Task items injected via JS -->
+                <div class="loading">Loading tasks...</div>
+            </div>
+        </aside>
+
+        <!-- Main Content -->
+        <main class="main-content glass-panel" id="main-content">
+            <div class="empty-state" id="empty-state">
+                <div class="icon">✨</div>
+                <h2>Select a requirement to view details</h2>
+                <p>Monitor pipeline execution and view generated data</p>
+            </div>
+            
+            <div class="detail-view hidden" id="detail-view">
+                <div class="detail-header">
+                    <div class="title-section">
+                        <span class="badge" id="detail-id">001</span>
+                        <h1 id="detail-title">Requirement Title</h1>
+                    </div>
+                    <div class="actions">
+                        <button class="btn btn-primary" id="btn-open-run-modal">🚀 Run Pipeline</button>
+                    </div>
+                </div>
+
+                <div class="status-banner hidden" id="status-banner">
+                    <div class="status-indicator"></div>
+                    <span id="status-text">Running...</span>
+                    <button class="btn btn-small" id="btn-view-logs">View Logs</button>
+                </div>
+
+                <div class="memo-container" id="memo-container">
+                    <div class="memo-header">
+                        <div class="memo-title">📝 Requirement Notes</div>
+                        <div style="display:flex; align-items:center; gap:10px;">
+                            <span class="save-status" id="memo-status"></span>
+                            <button class="btn btn-small btn-primary" id="btn-save-memo" style="padding: 4px 12px; min-width: 60px">Save</button>
+                        </div>
+                    </div>
+                    <textarea class="memo-textarea" id="memo-textarea" placeholder="Add notes, context, or issues for this requirement here..."></textarea>
+                </div>
+
+                <div class="data-tabs">
+                    <button class="tab-btn active" data-target="tab-strategy">Strategy</button>
+                    <button class="tab-btn" data-target="tab-blueprint">Blueprint</button>
+                    <button class="tab-btn" data-target="tab-caps">Capabilities</button>
+                    <button class="tab-btn" data-target="tab-raw">Raw Cases</button>
+                </div>
+
+                <div class="tab-content-container">
+                    <div class="tab-content active" id="tab-strategy">
+                        <div class="content-viewer" id="json-strategy">Loading...</div>
+                    </div>
+                    <div class="tab-content" id="tab-blueprint">
+                        <div class="content-viewer" id="json-blueprint">Loading...</div>
+                    </div>
+                    <div class="tab-content" id="tab-caps">
+                        <div class="content-viewer" id="json-caps">Loading...</div>
+                    </div>
+                    <div class="tab-content" id="tab-raw">
+                        <div class="content-viewer" id="json-raw">Loading...</div>
+                    </div>
+                </div>
+            </div>
+        </main>
+    </div>
+
+    <!-- Run Modal -->
+    <div class="modal-overlay hidden" id="run-modal">
+        <div class="modal glass-panel">
+            <div class="modal-header">
+                <h3>Configure Pipeline Run</h3>
+                <button class="close-btn" id="btn-close-modal">×</button>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label>Restart From</label>
+                    <select id="select-force-phase" class="glass-input" style="background-color: #1e293b; color: white; margin-top: 5px;">
+                        <option value="smart">Smart Resume (Continue where left off)</option>
+                        <option value="phase1_platforms">Phase 1: Regenerate Specific Platforms (Deletes chosen cases + Phase 2/3)</option>
+                        <option value="phase2_blueprint">Phase 2: Regenerate Blueprint Only (Deletes Blueprint + Strategy)</option>
+                        <option value="phase2_capabilities">Phase 2: Extract Capabilities Only (Deletes Caps + Strategy)</option>
+                        <option value="phase2_all">Phase 2: Entire Phase 2 (Deletes Blueprint + Caps + Strategy)</option>
+                        <option value="phase3">Phase 3: Strategy (Deletes Strategy only)</option>
+                    </select>
+                </div>
+                <div class="form-group" id="group-platforms">
+                    <label>Platforms (comma separated)</label>
+                    <input type="text" id="input-platforms" value="xhs,youtube,bili,x" class="glass-input">
+                </div>
+                <div class="form-group checkbox" style="margin-top: 15px;">
+                    <input type="checkbox" id="check-claude-sdk">
+                    <label for="check-claude-sdk">Use Anthropic SDK Core</label>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button class="btn btn-secondary" id="btn-cancel-run">Cancel</button>
+                <button class="btn btn-primary" id="btn-confirm-run">Execute</button>
+            </div>
+        </div>
+    </div>
+
+    <!-- Logs Modal -->
+    <div class="modal-overlay hidden" id="logs-modal">
+        <div class="modal large glass-panel">
+            <div class="modal-header">
+                <h3>Execution Logs</h3>
+                <button class="close-btn" id="btn-close-logs">×</button>
+            </div>
+            <div class="modal-body">
+                <pre class="terminal" id="terminal-logs">Waiting for logs...</pre>
+            </div>
+        </div>
+    </div>
+
+    <!-- Prompts Modal -->
+    <div class="modal-overlay hidden" id="prompts-modal">
+        <div class="modal large glass-panel" style="max-width: 90vw; height: 90vh; display: flex; flex-direction: column;">
+            <div class="modal-header">
+                <h3>⚙️ Prompts Editor</h3>
+                <div style="display:flex; align-items:center; gap:10px;">
+                    <span class="save-status" id="prompt-save-status"></span>
+                    <button class="btn btn-primary btn-small" id="btn-save-prompt">Save Changes</button>
+                    <button class="close-btn" id="btn-close-prompts">×</button>
+                </div>
+            </div>
+            <div class="modal-body" style="flex: 1; display: flex; gap: 1rem; overflow: hidden; padding: 1rem;">
+                <div class="prompt-sidebar" style="width: 250px; border-right: 1px solid rgba(255,255,255,0.1); padding-right: 1rem; overflow-y: auto;">
+                    <h4 style="margin-top:0; margin-bottom:1rem; color:var(--text-muted)">Available Prompts</h4>
+                    <div id="prompt-list" style="display:flex; flex-direction:column; gap:0.5rem;">
+                        <!-- Prompt tabs injected via JS -->
+                    </div>
+                </div>
+                <div class="prompt-editor" style="flex: 1; display: flex; flex-direction: column;">
+                    <textarea id="prompt-textarea" style="width:100%; height:100%; background:rgba(15,23,42,0.8); color:var(--text-main); font-family:monospace; padding:1rem; border:1px solid rgba(255,255,255,0.1); border-radius:6px; resize:none; font-size:14px;" placeholder="Select a prompt to edit..."></textarea>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script src="/static/app.js"></script>
+</body>
+</html>

+ 747 - 0
examples/process_pipeline/ui/style.css

@@ -0,0 +1,747 @@
+:root {
+    --bg-base: #0f172a;
+    --bg-glass: rgba(30, 41, 59, 0.7);
+    --border-glass: rgba(255, 255, 255, 0.1);
+    --text-main: #f8fafc;
+    --text-muted: #94a3b8;
+    --accent-primary: #3b82f6;
+    --accent-hover: #2563eb;
+    --success: #10b981;
+    --warning: #f59e0b;
+    --danger: #ef4444;
+    --card-hover: rgba(59, 130, 246, 0.15);
+    
+    --shadow-glass: 0 4px 30px rgba(0, 0, 0, 0.3);
+    --radius-lg: 16px;
+    --radius-md: 8px;
+    --radius-sm: 4px;
+}
+
+* {
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+}
+
+body {
+    font-family: 'Inter', sans-serif;
+    background: var(--bg-base);
+    background-image: 
+        radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.15) 0px, transparent 50%),
+        radial-gradient(at 100% 100%, rgba(139, 92, 246, 0.15) 0px, transparent 50%);
+    background-attachment: fixed;
+    color: var(--text-main);
+    height: 100vh;
+    overflow: hidden;
+}
+
+/* Glassmorphism Utilities */
+.glass-panel {
+    background: var(--bg-glass);
+    backdrop-filter: blur(16px);
+    -webkit-backdrop-filter: blur(16px);
+    border: 1px solid var(--border-glass);
+    box-shadow: var(--shadow-glass);
+}
+
+/* Layout */
+.app-container {
+    display: flex;
+    height: 100vh;
+    padding: 1rem;
+    gap: 1rem;
+}
+
+/* Sidebar */
+.sidebar {
+    width: 350px;
+    border-radius: var(--radius-lg);
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+}
+
+.sidebar-header {
+    padding: 1.5rem;
+    border-bottom: 1px solid var(--border-glass);
+}
+
+.sidebar-header h2 {
+    font-size: 1.25rem;
+    font-weight: 600;
+    margin-bottom: 0.5rem;
+    background: linear-gradient(to right, #60a5fa, #a78bfa);
+    -webkit-background-clip: text;
+    -webkit-text-fill-color: transparent;
+}
+
+.stats {
+    font-size: 0.85rem;
+    color: var(--text-muted);
+    display: flex;
+    gap: 1rem;
+}
+
+.search-box {
+    padding: 1rem;
+    border-bottom: 1px solid var(--border-glass);
+}
+
+.search-box input {
+    width: 100%;
+    padding: 0.75rem 1rem;
+    border-radius: var(--radius-md);
+    background: rgba(0,0,0,0.2);
+    border: 1px solid var(--border-glass);
+    color: var(--text-main);
+    font-family: inherit;
+    transition: all 0.2s ease;
+}
+
+.search-box input:focus {
+    outline: none;
+    border-color: var(--accent-primary);
+    box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
+}
+
+.task-list {
+    flex: 1;
+    overflow-y: auto;
+    padding: 0.5rem;
+}
+
+/* Scrollbar */
+::-webkit-scrollbar {
+    width: 6px;
+    height: 6px;
+}
+::-webkit-scrollbar-track {
+    background: transparent;
+}
+::-webkit-scrollbar-thumb {
+    background: var(--border-glass);
+    border-radius: 10px;
+}
+::-webkit-scrollbar-thumb:hover {
+    background: var(--text-muted);
+}
+
+/* Task Item */
+.task-item {
+    padding: 1rem;
+    border-radius: var(--radius-md);
+    margin-bottom: 0.5rem;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    border: 1px solid transparent;
+}
+
+.task-item:hover {
+    background: var(--card-hover);
+    border-color: var(--border-glass);
+    transform: translateY(-1px);
+}
+
+.task-item.active {
+    background: var(--card-hover);
+    border-color: var(--accent-primary);
+}
+
+.task-id {
+    font-size: 0.75rem;
+    color: var(--accent-primary);
+    font-weight: 600;
+    margin-bottom: 0.25rem;
+}
+
+.task-req {
+    font-size: 0.9rem;
+    line-height: 1.4;
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
+    margin-bottom: 0.5rem;
+}
+
+.task-tags {
+    display: flex;
+    gap: 0.25rem;
+    flex-wrap: wrap;
+}
+
+.tag {
+    font-size: 0.7rem;
+    padding: 0.2rem 0.4rem;
+    border-radius: 4px;
+    background: rgba(255,255,255,0.1);
+}
+.tag.success { background: rgba(16, 185, 129, 0.2); color: #34d399; }
+.tag.warning { background: rgba(245, 158, 11, 0.2); color: #fbbf24; }
+.tag.danger { background: rgba(239, 68, 68, 0.2); color: #f87171; }
+.tag.running { 
+    background: rgba(59, 130, 246, 0.2); 
+    color: #60a5fa;
+    animation: pulse 2s infinite; 
+}
+
+@keyframes pulse {
+    0% { opacity: 0.6; }
+    50% { opacity: 1; }
+    100% { opacity: 0.6; }
+}
+
+/* Main Content */
+.main-content {
+    flex: 1;
+    border-radius: var(--radius-lg);
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+    position: relative;
+}
+
+.empty-state {
+    position: absolute;
+    top: 0; left: 0; right: 0; bottom: 0;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    text-align: center;
+    color: var(--text-muted);
+}
+.empty-state .icon {
+    font-size: 4rem;
+    margin-bottom: 1rem;
+    opacity: 0.5;
+}
+
+.hidden {
+    display: none !important;
+}
+
+.detail-view {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+}
+
+.detail-header {
+    padding: 1.5rem;
+    border-bottom: 1px solid var(--border-glass);
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    gap: 1rem;
+}
+
+.title-section h1 {
+    font-size: 1.25rem;
+    font-weight: 500;
+    line-height: 1.4;
+    margin-top: 0.5rem;
+}
+
+.badge {
+    background: var(--accent-primary);
+    color: white;
+    padding: 0.25rem 0.5rem;
+    border-radius: var(--radius-sm);
+    font-size: 0.75rem;
+    font-weight: 600;
+}
+
+.btn {
+    padding: 0.6rem 1.2rem;
+    border-radius: var(--radius-md);
+    border: none;
+    font-weight: 500;
+    cursor: pointer;
+    font-family: inherit;
+    transition: all 0.2s ease;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.btn-primary {
+    background: linear-gradient(135deg, var(--accent-primary), #8b5cf6);
+    color: white;
+    box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3);
+}
+
+.btn-primary:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
+}
+
+.btn-secondary {
+    background: rgba(255,255,255,0.1);
+    color: var(--text-main);
+}
+.btn-secondary:hover {
+    background: rgba(255,255,255,0.2);
+}
+
+.btn-small {
+    padding: 0.4rem 0.8rem;
+    font-size: 0.8rem;
+}
+
+/* Status Banner */
+.status-banner {
+    padding: 0.75rem 1.5rem;
+    background: rgba(59, 130, 246, 0.1);
+    border-bottom: 1px solid var(--border-glass);
+    display: flex;
+    align-items: center;
+    gap: 1rem;
+}
+.status-indicator {
+    width: 10px; height: 10px;
+    border-radius: 50%;
+    background: var(--accent-primary);
+    box-shadow: 0 0 10px var(--accent-primary);
+    animation: pulse 1.5s infinite;
+}
+
+/* Tabs */
+.data-tabs {
+    display: flex;
+    padding: 0 1.5rem;
+    border-bottom: 1px solid var(--border-glass);
+    background: rgba(0,0,0,0.1);
+}
+
+.tab-btn {
+    padding: 1rem 1.5rem;
+    background: transparent;
+    border: none;
+    color: var(--text-muted);
+    cursor: pointer;
+    font-family: inherit;
+    font-weight: 500;
+    border-bottom: 2px solid transparent;
+    transition: all 0.2s ease;
+}
+
+.tab-btn:hover {
+    color: var(--text-main);
+}
+
+.tab-btn.active {
+    color: var(--accent-primary);
+    border-bottom-color: var(--accent-primary);
+}
+
+.tab-content-container {
+    flex: 1;
+    overflow: auto;
+    padding: 1.5rem;
+    background: rgba(0,0,0,0.2);
+}
+
+.tab-content {
+    display: none;
+}
+.tab-content.active {
+    display: block;
+}
+
+.json-viewer {
+    font-family: 'Consolas', 'Monaco', monospace;
+    font-size: 0.85rem;
+    line-height: 1.5;
+    color: #e2e8f0;
+    white-space: pre-wrap;
+    word-break: break-all;
+}
+
+/* Modals */
+.modal-overlay {
+    position: fixed;
+    top: 0; left: 0; right: 0; bottom: 0;
+    background: rgba(0,0,0,0.6);
+    backdrop-filter: blur(4px);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 100;
+    opacity: 1;
+    transition: opacity 0.2s ease;
+}
+
+.modal-overlay.hidden {
+    opacity: 0;
+    pointer-events: none;
+}
+
+.modal {
+    width: 100%;
+    max-width: 500px;
+    border-radius: var(--radius-lg);
+    overflow: hidden;
+    transform: translateY(0);
+    transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+}
+.modal.large {
+    max-width: 800px;
+    height: 80vh;
+    display: flex;
+    flex-direction: column;
+}
+.modal-overlay.hidden .modal {
+    transform: translateY(20px);
+}
+
+.modal-header {
+    padding: 1.5rem;
+    border-bottom: 1px solid var(--border-glass);
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+.close-btn {
+    background: transparent;
+    border: none;
+    color: var(--text-muted);
+    font-size: 1.5rem;
+    cursor: pointer;
+    line-height: 1;
+}
+.close-btn:hover { color: white; }
+
+.modal-body {
+    padding: 1.5rem;
+}
+.modal.large .modal-body {
+    flex: 1;
+    overflow: auto;
+    padding: 0;
+}
+
+.form-group {
+    margin-bottom: 1.2rem;
+}
+.form-group label {
+    display: block;
+    margin-bottom: 0.5rem;
+    font-size: 0.9rem;
+    color: var(--text-muted);
+}
+.form-group.checkbox {
+    display: flex;
+    align-items: center;
+    gap: 0.5rem;
+}
+.form-group.checkbox label {
+    margin: 0;
+    cursor: pointer;
+}
+.glass-input {
+    width: 100%;
+    padding: 0.8rem;
+    background: rgba(0,0,0,0.2);
+    border: 1px solid var(--border-glass);
+    border-radius: var(--radius-md);
+    color: white;
+    font-family: inherit;
+}
+
+.modal-footer {
+    padding: 1.2rem 1.5rem;
+    border-top: 1px solid var(--border-glass);
+    display: flex;
+    justify-content: flex-end;
+    gap: 1rem;
+    background: rgba(0,0,0,0.2);
+}
+
+/* Terminal */
+.terminal {
+    background: #000;
+    color: #0f0;
+    font-family: 'Consolas', monospace;
+    padding: 1rem;
+    height: 100%;
+    overflow-y: auto;
+    font-size: 0.85rem;
+    line-height: 1.4;
+    margin: 0;
+}
+
+/* JSON Viewer */
+.json-object, .json-array { margin-left: 1rem; border-left: 1px solid rgba(255,255,255,0.1); padding-left: 0.5rem; }
+.json-key { color: #a78bfa; font-weight: 500; }
+.json-string { color: #34d399; }
+.json-number { color: #fbbf24; }
+.json-boolean { color: #f472b6; }
+.json-null { color: #94a3b8; font-style: italic; }
+.json-prop { margin-bottom: 0.25rem; }
+
+/* Sub Tabs for Raw Cases */
+.sub-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; border-bottom: 1px solid var(--border-glass); padding-bottom: 0.5rem; }
+.sub-tab-btn { background: rgba(255,255,255,0.1); border: none; color: var(--text-main); padding: 0.4rem 1rem; border-radius: var(--radius-sm); cursor: pointer; transition: all 0.2s; font-weight: 600; font-size: 0.8rem; }
+.sub-tab-btn:hover { background: rgba(255,255,255,0.2); }
+.sub-tab-btn.active { background: var(--accent-primary); box-shadow: 0 0 10px rgba(59,130,246,0.3); }
+
+/* Data Cards and Rendered UI */
+.data-card {
+    background: rgba(255, 255, 255, 0.03);
+    border: 1px solid var(--border-glass);
+    border-radius: var(--radius-md);
+    padding: 1.2rem;
+    margin-bottom: 1rem;
+    transition: transform 0.2s;
+}
+.data-card:hover {
+    background: rgba(255, 255, 255, 0.05);
+    border-color: rgba(255, 255, 255, 0.2);
+}
+.card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    margin-bottom: 0.8rem;
+    gap: 1rem;
+}
+.card-title {
+    font-size: 1.1rem;
+    font-weight: 600;
+    color: var(--text-main);
+    line-height: 1.4;
+}
+.badge-emoji {
+    display: inline-flex;
+    align-items: center;
+    gap: 0.3rem;
+    padding: 0.3rem 0.6rem;
+    border-radius: 20px;
+    background: rgba(255, 255, 255, 0.1);
+    font-size: 0.8rem;
+    color: var(--text-muted);
+    border: 1px solid var(--border-glass);
+    white-space: nowrap;
+}
+.badge-emoji.success { background: rgba(16, 185, 129, 0.15); color: #34d399; border-color: rgba(16, 185, 129, 0.3); }
+.badge-emoji.warning { background: rgba(245, 158, 11, 0.15); color: #fbbf24; border-color: rgba(245, 158, 11, 0.3); }
+.badge-emoji.primary { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border-color: rgba(59, 130, 246, 0.3); }
+
+.card-body {
+    font-size: 0.9rem;
+    color: var(--text-muted);
+    line-height: 1.6;
+}
+.card-section {
+    margin-top: 1rem;
+    padding-top: 0.8rem;
+    border-top: 1px dashed var(--border-glass);
+}
+.section-title {
+    font-size: 0.85rem;
+    text-transform: uppercase;
+    letter-spacing: 0.5px;
+    color: #a78bfa;
+    margin-bottom: 0.5rem;
+    font-weight: 600;
+}
+
+/* Image Gallery */
+.image-gallery {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 0.8rem;
+    margin-top: 0.8rem;
+}
+.image-item {
+    position: relative;
+    border-radius: var(--radius-sm);
+    overflow: hidden;
+    border: 1px solid var(--border-glass);
+    background: #000;
+}
+.image-item img {
+    max-height: 180px;
+    max-width: 100%;
+    object-fit: cover;
+    display: block;
+    transition: transform 0.3s;
+}
+.image-item img:hover {
+    transform: scale(1.05);
+}
+.image-caption {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    background: rgba(0, 0, 0, 0.7);
+    color: white;
+    font-size: 0.7rem;
+    padding: 0.3rem 0.5rem;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+/* Phase List */
+.phase-list {
+    display: flex;
+    flex-direction: column;
+    gap: 1rem;
+    margin-top: 0.5rem;
+}
+.phase-item {
+    position: relative;
+    padding-left: 1.5rem;
+    border-left: 2px solid var(--accent-primary);
+}
+.phase-item::before {
+    content: '';
+    position: absolute;
+    left: -6px;
+    top: 5px;
+    width: 10px;
+    height: 10px;
+    border-radius: 50%;
+    background: var(--text-main);
+    border: 2px solid var(--accent-primary);
+}
+.phase-title {
+    font-weight: 600;
+    color: var(--text-main);
+    margin-bottom: 0.3rem;
+}
+
+/* Score Bar */
+.score-container {
+    display: flex;
+    align-items: center;
+    gap: 1rem;
+    margin: 1rem 0;
+    padding: 0.8rem;
+    background: rgba(0, 0, 0, 0.2);
+    border-radius: var(--radius-sm);
+}
+.score-circle {
+    width: 50px;
+    height: 50px;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 1.2rem;
+    font-weight: 700;
+    color: white;
+    background: conic-gradient(var(--success) var(--score-deg, 0deg), rgba(255,255,255,0.1) 0deg);
+}
+.score-circle.medium { background: conic-gradient(var(--warning) var(--score-deg, 0deg), rgba(255,255,255,0.1) 0deg); }
+.score-circle.low { background: conic-gradient(var(--danger) var(--score-deg, 0deg), rgba(255,255,255,0.1) 0deg); }
+.score-text {
+    flex: 1;
+    font-size: 0.9rem;
+    color: var(--text-muted);
+}
+
+.tags-container {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 0.4rem;
+    margin-top: 0.5rem;
+}
+
+@keyframes highlight-pulse {
+    0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); border-color: var(--accent-primary); }
+    70% { box-shadow: 0 0 0 15px rgba(59, 130, 246, 0); border-color: var(--accent-primary); }
+    100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
+}
+
+.highlight-pulse {
+    animation: highlight-pulse 2s cubic-bezier(0.165, 0.84, 0.44, 1);
+}
+
+.memo-container {
+    background: rgba(0, 0, 0, 0.2);
+    border-radius: var(--radius-sm);
+    padding: 1rem;
+    margin-bottom: 1.5rem;
+    border: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.memo-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 0.8rem;
+}
+
+.memo-title {
+    font-size: 0.95rem;
+    font-weight: 600;
+    color: var(--text-main);
+    display: flex;
+    align-items: center;
+    gap: 0.5rem;
+}
+
+.memo-textarea {
+    width: 100%;
+    min-height: 80px;
+    background: rgba(15, 23, 42, 0.6);
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    border-radius: 6px;
+    padding: 0.8rem;
+    color: var(--text-main);
+    font-family: inherit;
+    font-size: 0.9rem;
+    resize: vertical;
+    transition: border-color 0.2s ease;
+}
+
+.memo-textarea:focus {
+    outline: none;
+    border-color: var(--accent-primary);
+}
+
+.save-status {
+    font-size: 0.8rem;
+    color: var(--text-muted);
+    transition: opacity 0.3s;
+}
+
+.prompt-tab {
+    padding: 0.6rem 1rem;
+    border-radius: 6px;
+    background: rgba(255,255,255,0.05);
+    color: var(--text-muted);
+    cursor: pointer;
+    transition: all 0.2s;
+    font-size: 0.9rem;
+    text-align: left;
+    border: 1px solid transparent;
+}
+.prompt-tab:hover {
+    background: rgba(255,255,255,0.1);
+    color: var(--text-main);
+}
+.prompt-tab.active {
+    background: rgba(59, 130, 246, 0.2);
+    color: var(--accent-primary);
+    border-color: rgba(59, 130, 246, 0.4);
+    font-weight: 600;
+}
+
+.task-memo {
+    font-size: 0.75rem;
+    color: var(--text-muted);
+    background: rgba(0,0,0,0.2);
+    padding: 4px 6px;
+    border-radius: 4px;
+    margin-bottom: 0.5rem;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    font-style: italic;
+    border-left: 2px solid var(--accent-primary);
+}