diff --git a/fpga-goldenref-flow/SKILL.md b/fpga-goldenref-flow/SKILL.md new file mode 100644 index 0000000..9a16b6b --- /dev/null +++ b/fpga-goldenref-flow/SKILL.md @@ -0,0 +1,219 @@ +--- +name: fpga-goldenref-flow +description: > + Run the fpga-goldenref-flow LangGraph design-and-execution workflow for the + project at E:\LangGraph_Design\fpga-goldenref-flow. Drives the compiled graph + in-process (NO langgraph dev server) via a persistent SQLite checkpointer: + submits a run, surfaces the human-review interrupt (plans are summarised in the + Claude conversation for the user to approve/reject), resumes execution, and + displays the final summary — all within the Claude session. + + Trigger whenever the user says any of: "run FPGA flow", "start FPGA workflow", + "invoke fpga-goldenref-flow", "run goldenref flow", "启动 FPGA 工作流", + "运行 FPGA 设计流程", "跑一下 FPGA flow", "run the FPGA agent", + "execute the FPGA design pipeline", or any request to interact with the + LangGraph agent inside this project. Also trigger when the user wants to approve + or reject the generated plan, or resume a paused fpga-goldenref-flow run. +--- + +## 驱动方式:进程内驱动(无 dev server) + +工作流是一张 LangGraph 图,由 `scripts\run_fpga_local.py` **在本进程内**驱动 +(已移除 langgraph dev server 路径)。直接 `build_graph()` + **SqliteSaver 持久化** +驱动编译好的图,状态存在 `\.fpga_flow_local.sqlite`,按 `thread_id` +持久化,`start`/`resume`/`continue` 各自独立进程调用都能接续。 + +```powershell +conda run -n langgraph python ` + "C:\Users\bowei\.claude\skills\fpga-goldenref-flow\scripts\run_fpga_local.py" ` + start --input-dir "E:\\Notes" --output-dir "E:\\fpga_runs" +``` + +为什么是进程内驱动:`langgraph dev` 是**内存存储的开发服务器**,对本项目这种长耗时、 +多模块、反复中断续跑的执行不可靠——重启丢线程、run 队列被 cancel 残留堵成永久 +`pending`、watchfiles `--reload` 自激。进程内驱动没有这些问题(无队列、无内存丢失、 +无 reload),状态全部落在 SQLite。 + +--- + +## Project info + +| Item | Value | +|---|---| +| Root | `E:\LangGraph_Design\fpga-goldenref-flow` | +| Conda env | `langgraph` (`C:\Users\bowei\.conda\envs\langgraph`) | +| Graph name | `fpga_goldenref_flow` (在 `langgraph.json` 里声明;进程内驱动直接 `build_graph()`) | +| Default input dir | `Notes/` (contains PR00.md, Task00.md) | +| Helper script | `C:\Users\bowei\.claude\skills\fpga-goldenref-flow\scripts\run_fpga_local.py` | + +--- + +## Full workflow + +> **入口约束**:下面每一步都通过 helper 脚本 `run_fpga_local.py` 在 `langgraph` +> conda 环境里执行。不要直接 `import` `app.*` 在别处运行,统一走 helper 脚本以保证 +> SQLite 持久化与退出码一致。 + +### Step 1 — Submit a new run + +**Before calling the script, determine the two key paths:** +- `--input-dir` — the directory that holds the project's requirement files (e.g. `Notes/`). Use an absolute path or a path relative to the current shell's CWD. +- `--output-dir` — the directory where all generated files (task folders, plan.md, summary.md) and the run's checkpoint DB + session file will be written. **This must be inside the caller's project directory, not inside the workflow development directory.** Use the absolute path to the project being worked on, e.g. `E:\MyProject\fpga_runs`. + +Run the helper script using the `langgraph` conda environment: +```powershell +conda run -n langgraph python ` + "C:\Users\bowei\.claude\skills\fpga-goldenref-flow\scripts\run_fpga_local.py" ` + start ` + --input-dir "E:\\Notes" ` + --output-dir "E:\\fpga_runs" +``` + +Optional flag: `--prompt "additional context"` if the user supplied extra requirements. + +> **Rule:** always pass `--output-dir` as an absolute path pointing into the calling project. +> If omitted, files fall back to `/runs` which may or may not be in the project directory. + +The script prints `[INFO] Thread : ` and `[INFO] Output dir : ` (note them +for resume), then node-level progress (`-> node_name`) as the graph runs. + +**Exit codes:** +| Code | Meaning | +|---|---| +| 0 | Workflow completed — jump to Step 4 | +| 1 | Error — read the output, diagnose, report to user | +| 2 | An interrupt fired — read the `[INTERRUPT]` payload's `action` and branch (see below) | +| 3 | Codex unavailable — workflow stopped (no downgrade). Resume from checkpoint later with `continue`. | + +**Two interrupts fire in order — branch on `action` in the `[INTERRUPT]` payload:** +1. `"action": "select_plan_model"` — first, right after decomposition → **Step 1.5**. +2. `"action": "confirm_plan"` — later, after plans are generated → **Step 2**. + +### Step 1.5 — Select plan model (first interrupt) + +When the `[INTERRUPT]` payload has `"action": "select_plan_model"`, the workflow has +finished decomposition and is waiting for the user to pick the Claude model used to +**author/revise** the plans (review stays on Codex). The payload includes `default` +(Opus 4.8) and the decomposition overview. + +Ask the user which model (use AskUserQuestion), offering **Opus 4.8 (default/recommended)**, +**Fable 5**, **Sonnet 4.6**. Then resume with the choice: +```powershell +conda run -n langgraph python ` + "C:\Users\bowei\.claude\skills\fpga-goldenref-flow\scripts\run_fpga_local.py" ` + resume --output-dir "E:\\fpga_runs" ` + --plan-model opus # or: fable | sonnet | a full id like claude-opus-4-8 +``` +The workflow then generates plans with that model and stops again at the +`confirm_plan` interrupt (exit 2) → go to Step 2. + +### Step 2 — Human review (Claude reads plans, asks user) + +When the script exits with code 2 (`confirm_plan`) it prints: +- `[INTERRUPT]` — the raw interrupt payload (may include a message) +- `[PLAN] : ` — absolute paths to generated plan files +- `[REQ] : ` — absolute paths to requirement files + +For each plan path, read the file: +```powershell +Get-Content "" -Encoding utf8 +``` + +Then **summarise the plan for the user** in 3–5 bullet points per task and ask: + +> "FPGA 工作流已生成以下设计计划,请确认是否批准继续执行: +> …bullet points… +> **批准(继续执行)还是拒绝(请说明需要修改的地方)?**" + +Wait for the user's answer before proceeding to Step 3. + +### Step 3 — Resume the workflow + +> **并发安全(MANDATORY)**:`resume` 必须绑定到本次运行的工程,绝不能依赖全局会话文件。 +> **总是带上 `--output-dir`**(与 `start` 时完全相同的绝对路径),这样多个工程并发时 +> 各自的会话文件与 SQLite 互不串。等价地,也可以用 `--thread-id ""` +> 直接指定线程,完全绕过会话文件(最稳)。省略两者只在「同一时间只有一个工程在跑」时才安全。 + +**If the user approves:** +```powershell +conda run -n langgraph python ` + "C:\Users\bowei\.claude\skills\fpga-goldenref-flow\scripts\run_fpga_local.py" ` + resume --output-dir "E:\\fpga_runs" +``` +(Default behaviour is approved — no extra flag needed.) + +**If the user rejects:** +```powershell +conda run -n langgraph python ` + "C:\Users\bowei\.claude\skills\fpga-goldenref-flow\scripts\run_fpga_local.py" ` + resume --output-dir "E:\\fpga_runs" ` + --rejected --feedback "" +``` + +> Use the **same `--output-dir`** you passed at `start` (it is printed on the +> `[INFO] Output dir :` line). Alternatively pass `--thread-id ""` from the +> `[INFO] Thread :` line at start. + +After rejection the workflow loops back to plan generation; you will likely hit +exit code 2 again with new plans — repeat Step 2. + +> **Codex 不可用 (exit 3)**:若任一 resume/start 以 exit 3 退出,说明 codex(gpt-5.5) +> 额度被占满,工作流已停在断点(状态已存 SQLite,不降级)。等 codex 空出来后从断点续跑: +> ```powershell +> conda run -n langgraph python ` +> "C:\Users\bowei\.claude\skills\fpga-goldenref-flow\scripts\run_fpga_local.py" ` +> continue --output-dir "E:\\fpga_runs" +> ``` + +### Step 4 — Show results + +When the script exits with code 0 it prints `[SUMMARY]` followed by the contents +of `\summary.md`. Present this to the user. + +If the user wants to inspect a specific task's plan or requirement: +``` +\\plan.md +\\requirement.md +``` + +The `output-dir` is the value passed to `--output-dir` at start time (printed in the +`[INFO] Output dir :` line) and is also stored in the session file. + +--- + +## Session state (per-project — concurrency safe) + +Between `start` and `resume` the helper writes a small session file **inside the +run's own `output_dir`**, so concurrent projects never share it: +``` +\.fpga_flow_session.json +``` +It holds `thread_id` and `output_dir`. `resume`/`continue` locate it via the +`--output-dir` you pass (hence Step 3 requires it). It is deleted automatically when +the run completes successfully. + +**Isolation guarantees for concurrent projects:** +- Graph state (`MainState`/`FpgaState`, checkpoints) is isolated by `thread_id` + inside that run's own `\.fpga_flow_local.sqlite` — different projects + use different output dirs and different DBs, and cannot see each other's state. +- Generated files are isolated by `output_dir`. +- The session pointer is isolated by living inside each `output_dir`. **Always + pass `--output-dir` (or `--thread-id`) to `resume`** so it never reads another + project's session. + +To abandon a paused run and start fresh, delete that project's +`\.fpga_flow_session.json` (and, if you want a clean checkpoint, +`\.fpga_flow_local.sqlite`) before calling `start`. + +--- + +## Troubleshooting + +| Symptom | Fix | +|---|---| +| `ModuleNotFoundError` | `conda run -n langgraph pip install langgraph langgraph-checkpoint-sqlite` | +| `No session file found` | Must call `start` before `resume` (or pass `--thread-id`) | +| Empty `[PLAN]` lines | Look in `\` for `plan.md` files under each `` folder | +| `interrupted` never advances | Make sure `--rejected` flag is accompanied by `--feedback` text | +| exit 3 (Codex 不可用) | Wait for codex quota, then `continue --output-dir ` from the checkpoint | +| Stale state after editing `app\*` | In-process driver imports fresh each run; just re-run `start`/`continue`. To reset a thread, delete that run's `.fpga_flow_local.sqlite`. | diff --git a/fpga-goldenref-flow/scripts/__pycache__/run_fpga_local.cpython-311.pyc b/fpga-goldenref-flow/scripts/__pycache__/run_fpga_local.cpython-311.pyc new file mode 100644 index 0000000..106e44b Binary files /dev/null and b/fpga-goldenref-flow/scripts/__pycache__/run_fpga_local.cpython-311.pyc differ diff --git a/fpga-goldenref-flow/scripts/run_fpga_local.py b/fpga-goldenref-flow/scripts/run_fpga_local.py new file mode 100644 index 0000000..c825ade --- /dev/null +++ b/fpga-goldenref-flow/scripts/run_fpga_local.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +""" +fpga-goldenref-flow workflow runner — IN-PROCESS driver (NO langgraph dev server). + +Drives the compiled LangGraph graph directly in this Python process with a +persistent SQLite checkpointer. This avoids the langgraph dev server entirely: +no REST, no in-memory store, no background run-queue / enqueue jams, no +watchfiles reload loop. State persists across start/resume/continue invocations +in /.fpga_flow_local.sqlite, keyed by thread_id. + +This is the sole driver for the graph (the langgraph dev server path has been +removed). CLI + exit codes: + + start submit a new run; exits 2 at an interrupt + resume answer an interrupt (--plan-model OR approve/--rejected --feedback) + continue continue a thread paused at a normal node boundary (input=None) + +Exit codes: 0 complete · 1 error · 2 interrupted (human action needed) +""" +import os +import sys +import json +import uuid +import argparse +import sqlite3 + +# Drive the graph in-process: import the app package. +FPGA_FLOW_ROOT = r"E:\LangGraph_Design\fpga-goldenref-flow" +if FPGA_FLOW_ROOT not in sys.path: + sys.path.insert(0, FPGA_FLOW_ROOT) + +RECURSION_LIMIT = int(os.getenv("FPGA_FLOW_RECURSION_LIMIT", "300")) + + +def _session_file(output_dir: str = "") -> str: + base = (output_dir or "").strip() or FPGA_FLOW_ROOT + return os.path.join(base, ".fpga_flow_session.json") + + +def _db_path(output_dir: str) -> str: + base = (output_dir or "").strip() or FPGA_FLOW_ROOT + return os.path.join(base, ".fpga_flow_local.sqlite") + + +def save_session(thread_id, output_dir): + with open(_session_file(output_dir), "w", encoding="utf-8") as f: + json.dump({"thread_id": thread_id, "output_dir": output_dir, "driver": "local"}, f) + + +def load_session(output_dir=""): + path = _session_file(output_dir) + if not os.path.exists(path): + print(f"[FAIL] No session file at {path}. Run 'start' first (or pass --thread-id).", flush=True) + sys.exit(1) + with open(path, encoding="utf-8") as f: + return json.load(f) + + +# ── graph + checkpointer ────────────────────────────────────────────────────── + +def _build(output_dir): + """Compile the graph with a persistent SQLite checkpointer (keeps the conn).""" + from langgraph.checkpoint.sqlite import SqliteSaver + from app.main_graph import build_graph + conn = sqlite3.connect(_db_path(output_dir), check_same_thread=False) + saver = SqliteSaver(conn) + try: + saver.setup() # create checkpoint tables if not present + except Exception: + pass + return build_graph(checkpointer=saver), conn + + +def _config(thread_id): + return {"configurable": {"thread_id": thread_id}, "recursion_limit": RECURSION_LIMIT} + + +def _stream(graph, config, payload): + """Run the graph to its next pause, printing node-level progress.""" + for chunk in graph.stream(payload, config, stream_mode="updates"): + if isinstance(chunk, dict): + for node in chunk: + if node and not str(node).startswith("__"): + print(f" -> {node}", flush=True) + + +def _pending_interrupts(graph, config): + """Return list of interrupt payload values currently pending (if any).""" + snap = graph.get_state(config) + vals = [] + for task in (snap.tasks or ()): + for it in (getattr(task, "interrupts", None) or ()): + v = getattr(it, "value", it) + vals.append(v) + return vals, snap + + +def _print_interrupt(graph, config): + vals, snap = _pending_interrupts(graph, config) + for v in vals: + print(f"\n[INTERRUPT] {json.dumps(v, indent=2, default=str, ensure_ascii=False)}", flush=True) + # plan/requirement paths from state + values = snap.values or {} + for serial in values.get("serial_tasks", []): + for pt in serial.get("parallel_tasks", []): + tid = pt.get("task_id", "?") + if pt.get("plan_path"): + print(f"[PLAN] {tid}: {pt['plan_path']}", flush=True) + if pt.get("req_path"): + print(f"[REQ] {tid}: {pt['req_path']}", flush=True) + print("\n[ACTION REQUIRED] Claude will read these plans and ask for your approval.", flush=True) + + +def _show_summary(output_dir): + summary = os.path.join(output_dir, "summary.md") + if os.path.exists(summary): + with open(summary, encoding="utf-8") as f: + print(f"\n[SUMMARY]\n{f.read()}", flush=True) + else: + print(f"[DONE] Workflow completed. (summary not found at {summary})", flush=True) + + +def _finish(graph, config, output_dir): + """After a stream returns, classify: interrupted (2) / done (0).""" + vals, snap = _pending_interrupts(graph, config) + if vals: + print("\n[STATUS] interrupted", flush=True) + _print_interrupt(graph, config) + sys.exit(2) + if snap.next: + # paused without an interrupt (rare) — treat as needing continue + print(f"\n[STATUS] paused (next={list(snap.next)})", flush=True) + sys.exit(2) + print("\n[STATUS] success", flush=True) + _show_summary(output_dir) + sf = _session_file(output_dir) + if os.path.exists(sf): + os.remove(sf) + sys.exit(0) + + +# ── commands ────────────────────────────────────────────────────────────────── + +def cmd_start(args): + raw_out = (args.output_dir or "").strip() + output_dir = os.path.abspath(raw_out) if raw_out else os.path.join( + os.path.dirname(os.path.abspath(args.input_dir)), "runs") + os.makedirs(output_dir, exist_ok=True) + thread_id = str(uuid.uuid4()) + print(f"[INFO] Driver : in-process (no dev server)", flush=True) + print(f"[INFO] Thread : {thread_id}", flush=True) + print(f"[INFO] Output dir : {output_dir}", flush=True) + print(f"[INFO] Checkpoints : {_db_path(output_dir)}", flush=True) + + graph, conn = _build(output_dir) + try: + inp = {"input_dir": args.input_dir, "output_dir": output_dir} + if args.prompt: + inp["user_prompt"] = args.prompt + print("[INFO] Running fpga_flow graph ...\n", flush=True) + save_session(thread_id, output_dir) + _stream(graph, _config(thread_id), inp) + _finish(graph, _config(thread_id), output_dir) + finally: + conn.close() + + +def _resolve_thread(args): + output_dir = (args.output_dir or "").strip() + thread_id = (getattr(args, "thread_id", "") or "").strip() + if not thread_id: + sess = load_session(output_dir) + thread_id = sess["thread_id"] + output_dir = sess.get("output_dir", output_dir) + return thread_id, output_dir + + +def cmd_resume(args): + from langgraph.types import Command + thread_id, output_dir = _resolve_thread(args) + graph, conn = _build(output_dir) + try: + plan_model = (getattr(args, "plan_model", "") or "").strip() + if plan_model: + resume_val = {"plan_model": plan_model} + print(f"[INFO] Resuming {thread_id} plan_model={plan_model}", flush=True) + else: + resume_val = {"approved": not args.rejected, "feedback": args.feedback or ""} + print(f"[INFO] Resuming {thread_id} approved={not args.rejected}", flush=True) + print("", flush=True) + _stream(graph, _config(thread_id), Command(resume=resume_val)) + _finish(graph, _config(thread_id), output_dir) + finally: + conn.close() + + +def cmd_continue(args): + thread_id, output_dir = _resolve_thread(args) + graph, conn = _build(output_dir) + try: + print(f"[INFO] Continuing {thread_id} from checkpoint ...\n", flush=True) + _stream(graph, _config(thread_id), None) + _finish(graph, _config(thread_id), output_dir) + finally: + conn.close() + + +def main(): + p = argparse.ArgumentParser(description="fpga-goldenref-flow IN-PROCESS runner (no dev server)") + sub = p.add_subparsers(dest="cmd") + + ps = sub.add_parser("start") + ps.add_argument("--input-dir", default="Notes/") + ps.add_argument("--output-dir", default="") + ps.add_argument("--prompt", default="") + + pr = sub.add_parser("resume") + pr.add_argument("--plan-model", default="") + pr.add_argument("--rejected", action="store_true", default=False) + pr.add_argument("--feedback", default="") + pr.add_argument("--output-dir", default="") + pr.add_argument("--thread-id", default="") + + pc = sub.add_parser("continue") + pc.add_argument("--output-dir", default="") + pc.add_argument("--thread-id", default="") + + args = p.parse_args() + if not args.cmd: + p.print_help(); sys.exit(1) + + try: + from app.nodes_design import CodexUnavailable + except Exception: + CodexUnavailable = () # type: ignore + + try: + if args.cmd == "start": + cmd_start(args) + elif args.cmd == "resume": + cmd_resume(args) + elif args.cmd == "continue": + cmd_continue(args) + except SystemExit: + raise + except CodexUnavailable as exc: + out = getattr(args, "output_dir", "") or "" + bar = "=" * 64 + print(f"\n{bar}", flush=True) + print("[ALARM] CODEX 不可用 —— 工作流已停止(不降级)", flush=True) + print(f"原因:{exc}", flush=True) + print("codex(gpt-5.5) 与你其它工作共享额度。请等 codex 空出来后,从断点续跑:", flush=True) + print(f' run_fpga_local.py continue --output-dir "{out}"', flush=True) + print("状态已保存到 SQLite,不会丢失;不会回退到 Claude 或更弱模型。", flush=True) + print(bar, flush=True) + sys.exit(3) + + +if __name__ == "__main__": + main() diff --git a/fpga-imx6-flow/SKILL.md b/fpga-imx6-flow/SKILL.md new file mode 100644 index 0000000..94053db --- /dev/null +++ b/fpga-imx6-flow/SKILL.md @@ -0,0 +1,177 @@ +--- +name: fpga-imx6-flow +description: > + Run the fpga-imx6-flow LangGraph workflow at E:\LangGraph_Design\FPGA-imx6-flow — + the closed-loop SMOD (FPGA + i.MX6) hardware-in-the-loop design/debug pipeline: + power on the board, connect JTAG, program bitstream/flash, restart the imx6 app, + analyze (ILA + COM6 serial), judge against design requirements, and iterate by + modifying RTL/imx6 code. It drives the graph in-process (no langgraph dev) and + hands the 5 interrupts (analyze / meets-requirement / propose / modify-FPGA / + modify-imx6) back to this Claude session to execute. + + ALWAYS use this skill whenever the user wants to run, start, drive, resume, or + continue the fpga-imx6-flow workflow / SMOD FPGA+imx6 design loop, or says things + like "启动 fpga-imx6-flow", "跑 imx6 设计流程", "run the imx6 FPGA flow", "执行 + SMOD 设计任务", "继续 imx6 工作流", "resume the imx6 design loop", even if they + don't name the project path. Before driving the workflow this skill first confirms + the langgraph conda environment from /langgraph-vivado-install is in place. +--- + +## What this skill does + +Drives the **fpga-imx6-flow** graph (24 nodes / 34 flow-edges, 1:1 with +`Notes/fpga-imx6-flow.excalidraw`). The graph's deterministic hardware/check nodes +(power, JTAG, program, restart) run in-process; its **5 interrupt nodes** pause and +hand control to **this Claude session**, which performs the real work (analysis, +judgment, RTL/app edits) and resumes the same thread. Persistence is a SqliteSaver +file, so `start`/`resume` survive across separate process calls. + +| Item | Value | +|---|---| +| Project root | `E:\LangGraph_Design\FPGA-imx6-flow` | +| Conda env | `langgraph` (`C:\Users\bowei\.conda\envs\langgraph`) | +| Graph | `app.main_graph:build_graph` (in-process; no langgraph dev) | +| Driver | `C:\Users\bowei\.claude\skills\fpga-imx6-flow\scripts\run_imx6_flow.py` | +| Checkpoint | `\runs\skill_checkpoint.sqlite` (keyed by `--thread-id`) | +| Design source | `Notes/fpga-imx6-flow.excalidraw`, plan `Notes/Task00.md` | + +## ⚠️ 硬件时序铁律(上电 / USB-Blast / 烧写,务必遵守) + +**每次给 SMOD 上电之前,USB-Blast 必须先断开;上电完成之后再重新连接。** 绝不在 +USB-Blast 处于连接状态时给板卡上电或断电(保护 JTAG 适配器与板卡)。 + +标准时序(图拓扑已按此实现,手动操作或写 `tools_*` / 真机脚本时也必须遵守): + +``` +断电(COM8 power_off) → 确认已断电 → 断开USB-Blast(COM4 blast_off) → 确认已断开 + → 上电(COM8 power_on) → 确认已上电 → 连接USB-Blast(COM4 blast_on) → 连接JTAG/烧写/抓ILA +``` + +**烧写 flash 的完整闭环**(固化后必须断电重启从 flash 加载再验证): + +``` +断电 → 断开USB-Blast → 上电 → 连接USB-Blast → 用 /fpga_flash 烧写flash + → 断电 → 断开USB-Blast → 上电 → 连接USB-Blast → 通过ILA验证flash内容正确 +``` + +- 在 LangGraph 图里:`power_off → check_power_off →(是) blast_off_a → check_blast_off + →(是) power_on → check_power →(是) blast_on → check_hw`。`flash_burn` 烧写后回到 + `power_off` 做整机断电重启,再次走完上述上电时序后才 `analyze`(抓ILA验证)。 +- **强化确认**:`power_on` 节点在上电前调用 `_ensure_blast_off_before_power()` 兜底确认 + USB-Blast 已断开;若发现仍连接会强制断开并在 `history` 标记 `rule_violation=True`。 +- vivado-mcp 工具:`power_on`/`power_off`(COM8 继电器)、`USB_Blast_on`/`USB_Blast_off`(COM4)。 + +> **Run via the env python directly** (not bare `conda run`): on this Windows GBK +> console, `conda run` re-prints the child's UTF-8 stdout through a GBK codec and +> **crashes on the Chinese interrupt payloads**. Use the env `python.exe`, or +> `conda run --no-capture-output`. The driver also re-configures its stdout to UTF-8. + +``` +PY = C:\Users\bowei\.conda\envs\langgraph\python.exe +RUN = C:\Users\bowei\.claude\skills\fpga-imx6-flow\scripts\run_imx6_flow.py +``` + +## Step 0 — Confirm the environment (REQUIRED before driving) + +The workflow runs in the `langgraph` conda env created by **`/langgraph-vivado-install`**. +Verify it before starting, so a missing env surfaces as a clear instruction rather +than a cryptic import error mid-run: + +```powershell +$py = "C:\Users\bowei\.conda\envs\langgraph\python.exe" +if (-not (Test-Path $py)) { Write-Host "[MISSING] langgraph env" } else { + & $py -c "import langgraph; from langgraph.checkpoint.sqlite import SqliteSaver; from langgraph.types import Command; print('[OK] langgraph + checkpoint-sqlite')" +} +``` + +- **If `[OK]`** → proceed to Step 1. +- **If missing / import fails** → tell the user the env isn't ready and to run + **`/langgraph-vivado-install`** first (it creates the `langgraph` env with + `langgraph` + `langgraph-checkpoint-sqlite` and the Vivado link). Do not proceed + until it succeeds. + +**For a real-hardware run** (`--real`), additionally confirm: `env_config.json`'s +`VIVADO_BIN_PATH` is valid; the vivado-mcp `power_on`/`USB_Blast_on` tools are +available; COM4/COM6/COM8 are present (`[System.IO.Ports.SerialPort]::GetPortNames()`). +**Default runs are MOCK** (dry-run of the design loop with no hardware), because the +workflow's `tools_*` real-hardware paths are still TODO — see "Mock vs real" below. + +## Step 1 — Start the workflow + +Ask the user (or infer) the first-pass program mode (`bitstream` default, or `flash`) +and any `--bit-path`/`--flash-path`. Then start: + +```powershell +& $py $RUN start --project-dir "E:\LangGraph_Design\FPGA-imx6-flow" ` + --thread-id "imx6-" --program-mode bitstream +# add --real only when hardware tools are wired and the board is connected +``` + +The driver prints **one JSON line** and exits: + +| Exit | `status` | Meaning | +|---|---|---| +| 0 | `done` | Workflow finished (符合设计要求 / 迭代到上限) → Step 3 | +| 1 | `error` | Read `error`, diagnose, report to user | +| 2 | `interrupt` | An interrupt fired → read `interrupt.action` → Step 2 | + +## Step 2 — Handle the interrupt, then resume + +On exit 2, the JSON's `interrupt.action` tells you what the session must do. Perform +it, then resume the **same `--thread-id`** with a JSON payload. Repeat until exit 0. + +| `action` | excalidraw node | What you (the session) do | Resume JSON | +|---|---|---|---| +| `analyze` | 通过ila/串口分析 | Grab COM6 serial log + pull ILA waveform (vivado-mcp); summarize | `{"analysis_log":"<摘要/路径>"}` | +| `meets_requirement` | 符合设计要求? | Judge the analysis against the design requirement | `{"meets": true}` or `{"meets": false}` | +| `propose` | 分析原因,提出修改方案 | Diagnose root cause; decide what to change + next program mode | `{"reason":"...","need_fpga": true/false,"need_imx6": true/false,"program_mode":"bitstream"或"flash"}` | +| `modify_fpga` | 改 RTL→综合→出 bit/flash | Edit RTL, run Vivado synth/impl, produce bit + flash files | `{"bit_path":"...","flash_path":"...","program_mode":"flash"}` | +| `modify_imx6` | VM 改 app→交叉编译 | In the Ubuntu VM (`bowei@192.168.223.128`) edit app + cross-compile | `{"app_path":"..."}` | + +Resume command: + +```powershell +& $py $RUN resume --project-dir "E:\LangGraph_Design\FPGA-imx6-flow" ` + --thread-id "imx6-" --resume-json '{"meets": false}' +``` + +Notes: +- Always pass the **same `--thread-id`** used at `start` — that's how the sqlite + checkpoint reattaches to the run (concurrency-safe: different tasks → different + thread ids → isolated state). +- `propose` fans out to BOTH `decide_fpga` and `decide_imx6`; with `need_fpga:false` + and `need_imx6:false` the loop goes straight to `round_done` → re-program. Setting + either true fires the corresponding `modify_*` interrupt next. +- After `round_done` the graph re-asks the program-mode decision; `program_mode` you + set in `propose`/`modify_fpga` chooses bitstream (download bit) vs flash (re-burn + + full power/connect re-sequence). This is the big closed loop. + +## Step 3 — Finish + +On exit 0 (`status: done`), report to the user: iterations completed and that the +design met requirements (or hit the iteration cap). Offer to inspect +`\runs\` artifacts or the per-step `history`. + +To abandon a paused run and start clean, delete `\runs\skill_checkpoint.sqlite` +(or use a fresh `--thread-id`). + +## Mock vs real + +- **MOCK (default)** — `FPGA_IMX6_MOCK=1`; hardware/check nodes return success + immediately. Use this to dry-run the *design-loop logic* and the 5 interrupts with + no board attached. Great for demonstrating/validating the flow. +- **Real (`--real`)** — drives actual hardware. Requires the project's + `app/tools_hw.py` / `tools_vivado.py` / `tools_imx6.py` real paths to be implemented + (they currently raise `NotImplementedError` as TODO — they will reuse the proven + vivado-mcp serial/Vivado logic). Until then, `--real` fails at the first hardware + node; tell the user to wire the tools first (see `Notes/Task00.md` §7). + +## Troubleshooting + +| Symptom | Fix | +|---|---| +| `UnicodeEncodeError: 'gbk' ...` | Don't use bare `conda run`; use the env `python.exe` or `conda run --no-capture-output`. | +| `import failed ... Run /langgraph-vivado-install` | The `langgraph` env is missing/incomplete → run `/langgraph-vivado-install`. | +| Exit 1 `NotImplementedError ... tools_*` | A `--real` run hit an unwired hardware tool. Run without `--real` (mock), or implement `tools_*`. | +| `resume` seems to start over | You passed a different `--thread-id` than `start`. Use the same one. | +| Need to see the graph visually | Optional: `langgraph dev` via the project's `langgraph.json` opens Studio — not required to run. | diff --git a/fpga-imx6-flow/scripts/run_imx6_flow.py b/fpga-imx6-flow/scripts/run_imx6_flow.py new file mode 100644 index 0000000..ef3da1e --- /dev/null +++ b/fpga-imx6-flow/scripts/run_imx6_flow.py @@ -0,0 +1,123 @@ +"""In-process driver for the fpga-imx6-flow LangGraph workflow. + +Builds the compiled graph from the project's app package and drives it with a +persistent SqliteSaver — no `langgraph dev` server. Mirrors the fpga-flow helper +convention: exit code 0 = done, 1 = error, 2 = interrupt fired. + +Commands: + start --project-dir [--program-mode bitstream|flash] [--bit-path ..] + [--flash-path ..] [--thread-id ..] [--real] [--max-iterations N] + resume --project-dir --resume-json '' [--thread-id ..] [--real] + status --project-dir [--thread-id ..] + +Output: one JSON line on stdout describing status + the pending interrupt (if any). +Run via the env python directly or `conda run --no-capture-output` to avoid the +Windows GBK console re-encode crash on Chinese interrupt payloads. +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +from pathlib import Path + +try: + sys.stdout.reconfigure(encoding="utf-8") # emit UTF-8 even on a GBK console +except Exception: + pass + +DEFAULT_PROJECT = r"E:\LangGraph_Design\FPGA-imx6-flow" + + +def _emit(obj: dict, code: int) -> None: + print(json.dumps(obj, ensure_ascii=False)) + sys.exit(code) + + +def _pending_interrupt(snap): + for task in snap.tasks: + if task.interrupts: + return task.interrupts[0].value + return None + + +def main() -> None: + ap = argparse.ArgumentParser(description="Drive the fpga-imx6-flow graph in-process.") + ap.add_argument("command", choices=["start", "resume", "status"]) + ap.add_argument("--project-dir", default=DEFAULT_PROJECT) + ap.add_argument("--thread-id", default="imx6-skill-1") + ap.add_argument("--resume-json", default="") + ap.add_argument("--program-mode", default="bitstream", choices=["bitstream", "flash"]) + ap.add_argument("--bit-path", default="") + ap.add_argument("--flash-path", default="") + ap.add_argument("--real", action="store_true", + help="drive real hardware (workflow tools_* must be wired; default is MOCK dry-run)") + ap.add_argument("--max-iterations", type=int, default=8) + ap.add_argument("--max-hw-retry", type=int, default=3) + args = ap.parse_args() + + project = Path(args.project_dir) + if not (project / "app" / "main_graph.py").exists(): + _emit({"status": "error", + "error": f"app/main_graph.py not found under {project}; check --project-dir"}, 1) + + # MOCK by default so the design loop can be dry-run with no hardware attached. + os.environ["FPGA_IMX6_MOCK"] = "0" if args.real else "1" + sys.path.insert(0, str(project)) + + try: + from langgraph.checkpoint.sqlite import SqliteSaver + from langgraph.types import Command + from app.main_graph import build_graph + except Exception as exc: # noqa: BLE001 + _emit({"status": "error", + "error": f"import failed: {exc}. Run /langgraph-vivado-install to set up the env."}, 1) + + runs = project / "runs" + runs.mkdir(exist_ok=True) + db = str(runs / "skill_checkpoint.sqlite") + cfg = {"configurable": {"thread_id": args.thread_id}} + + with SqliteSaver.from_conn_string(db) as cp: + graph = build_graph(checkpointer=cp) + try: + if args.command == "start": + init = { + "iteration": 0, + "program_mode": args.program_mode, + "bit_path": args.bit_path, + "flash_path": args.flash_path, + "max_hw_retry": args.max_hw_retry, + "max_iterations": args.max_iterations, + "history": [], + } + graph.invoke(init, cfg) + elif args.command == "resume": + payload = json.loads(args.resume_json) if args.resume_json else {} + graph.invoke(Command(resume=payload), cfg) + # status: no invoke, just snapshot + snap = graph.get_state(cfg) + except Exception as exc: # noqa: BLE001 + _emit({"status": "error", "error": str(exc)}, 1) + + if snap.next: + _emit({ + "status": "interrupt", + "thread_id": args.thread_id, + "mock": not args.real, + "next": list(snap.next), + "iteration": snap.values.get("iteration"), + "interrupt": _pending_interrupt(snap), + }, 2) + else: + _emit({ + "status": "done", + "thread_id": args.thread_id, + "iteration": snap.values.get("iteration"), + "steps": len(snap.values.get("history", [])), + }, 0) + + +if __name__ == "__main__": + main() diff --git a/fpga_flash/SKILL.md b/fpga_flash/SKILL.md index e39ce0d..9f80834 100644 --- a/fpga_flash/SKILL.md +++ b/fpga_flash/SKILL.md @@ -118,11 +118,18 @@ python "C:/Users/bowei/.claude/skills/fpga_flash/scripts/program_flash.py" \ - `--mcs-file`:第三步生成的 .mcs 路径 - `--ltx-file`:若设计无 ILA,省略此参数(脚本自动跳过 PROBES.FILE 配置) -此脚本在同一个 Vivado batch 会话中完成:硬件连接 → cfgmem 器件创建 → 擦除/编程/验证。 +此脚本在同一个 Vivado batch 会话中完成:硬件连接 → cfgmem 器件创建 → **载入 flash-loader 比特流(关键步骤)** → 擦除/编程/验证 → 回载用户 bit。 -**验证成功的标志:** 输出包含 `End of startup status: HIGH` 且无 `ERROR` 行。 +**验证成功的标志(真正写入 flash):** 输出必须包含 +`Performing Erase Operation... Erase Operation successful`、 +`Program/Verify Operation successful`、`Flash programming completed successfully`, +并能读到非零的 `Mfg ID`(如 Winbond=`ef`、Micron=`20`)。 +> ⚠️ 仅有 `End of startup status: HIGH` **不代表写了 flash**——那只是把 bit 载入 FPGA +> 易失配置。若看到 `Mfg ID : 0 / Unable to read device properties`,说明 flash-loader +> 没接管 SPI 总线(本 tcl 已用 `create_hw_bitstream`+`program_hw_devices` 载入 +> `spi__pullnone.bit` 修复此问题)。 -ILA core 的 `Dropping logic core` WARNING 是正常现象(golden 镜像不含 ILA core),不影响烧写结果。 +ILA core 的 `Dropping logic core` WARNING 是正常现象(载入 loader/golden 镜像时不含 ILA core),不影响烧写结果。 --- diff --git a/fpga_flash/scripts/step4_5_program.tcl b/fpga_flash/scripts/step4_5_program.tcl index e247fde..a246568 100644 --- a/fpga_flash/scripts/step4_5_program.tcl +++ b/fpga_flash/scripts/step4_5_program.tcl @@ -38,6 +38,21 @@ set_property PROGRAM.FILES [list $mcs_file] \ [get_property PROGRAM.HW_CFGMEM [lindex [get_hw_devices xc7a200t_0] 0]] refresh_hw_device [lindex [get_hw_devices xc7a200t_0] 0] +# --- CRITICAL: load Vivado's flash-loader bitstream into the FPGA FIRST so it +# bridges JTAG<->SPI flash and takes over the SPI bus. Without this, the user +# design (or boot image) holds the SPI pins and the flash read returns +# "Mfg ID : 0 / Unable to read device properties" (program fails silently). +# This is exactly what the Vivado GUI "Program Configuration Memory Device" +# does under the hood before program_hw_cfgmem. --- +set loader_bit [get_property PROGRAM.HW_CFGMEM_BITFILE [lindex [get_hw_devices xc7a200t_0] 0]] +create_hw_bitstream -hw_device [lindex [get_hw_devices xc7a200t_0] 0] $loader_bit +program_hw_devices [lindex [get_hw_devices xc7a200t_0] 0] + +# Erase + program + verify the SPI flash (this is the actual flash write). +program_hw_cfgmem -hw_cfgmem [get_property PROGRAM.HW_CFGMEM [lindex [get_hw_devices xc7a200t_0] 0]] + +# Optionally reload the user bit + probes so the device runs the design (with ILA) +# after flashing. A power-cycle will boot the same content from flash. if {$has_ltx} { set_property PROBES.FILE $ltx_file [get_hw_devices xc7a200t_0] set_property FULL_PROBES.FILE $ltx_file [get_hw_devices xc7a200t_0] diff --git a/langgraph-plan-review/.langgraph_api/.langgraph_ops.pckl b/langgraph-plan-review/.langgraph_api/.langgraph_ops.pckl new file mode 100644 index 0000000..c3b8e8e Binary files /dev/null and b/langgraph-plan-review/.langgraph_api/.langgraph_ops.pckl differ diff --git a/langgraph-plan-review/SKILL.md b/langgraph-plan-review/SKILL.md index faf0814..d20d285 100644 --- a/langgraph-plan-review/SKILL.md +++ b/langgraph-plan-review/SKILL.md @@ -7,6 +7,32 @@ description: Use the LangGraph plan-review workflow from the current Claude Code Use this skill to run the LangGraph plan-review workflow. All scripts are bundled under the skill's own `scripts\` directory (`C:\Users\bowei\.claude\skills\langgraph-plan-review\scripts\`) — no dependency on `E:\LangGraph_Design\lab00`. +## ⚠️ 强制执行规则(MANDATORY — read first) + +**plan-review 工作流必须、且只能通过 `langgraph dev` 服务器来驱动。** + +- ✅ **唯一允许的入口**:`run_langgraph.ps1` → `run_workflow.py`。后者是一个**纯 REST/SDK 客户端** + (`langgraph_sdk.get_sync_client(url=http://127.0.0.1:2024)`),每次 start/resume 都把 run + 投递到 `langgraph dev` 服务器执行。先用 `manage_dev_server.ps1 start` 把服务器拉起。 + 这样 LangGraph Studio 才能实时追踪线程、节点、中断与状态。 + +- 🚫 **绝对禁止**:直接 `import` / 在进程内运行本 skill 的图定义模块 + (`scripts\graph.py`、`scripts\workflow.py`、`scripts\agents.py`)来跑工作流。包括但不限于: + - `python -c "from graph import ...; graph.invoke(...)"` 或 `.stream(...)` + - 写临时脚本 build 编译图后在进程内调用 + - 任何绕过 `http://127.0.0.1:2024` 的「就地执行」方式 + + 这类调用在 dev server 进程**之外**运行,Studio 追踪不到状态,**一律不允许**—— + 即使只是想「快速验证一个节点」也不行。 + +- ℹ️ **例外说明**:`review_task.py`(Codex 评审 shim)会直接调 `codex`,这是**合规的**—— + 它是由运行在 dev server 内的图通过 `codex_review_command` 调起的评审步骤,不是绕过 + dev server 去驱动工作流本身。区别在于:驱动图 = 必须走 dev server;图内部调用的工具 = 正常。 + +- 🛠 **改了 `graph.py`/`workflow.py`/`agents.py` 后**:dev server 内存里仍是旧代码。先 + `manage_dev_server.ps1 stop` 再 `start`(或让 `--reload` 生效),再经 `run_langgraph.ps1` + 重跑,使改动在被追踪的运行中生效。 + ## Pre-flight: Environment Setup Before running the workflow for the first time (or when unsure the environment is ready), verify that the required packages are installed in the `langgraph` conda env: @@ -32,6 +58,7 @@ conda run -n langgraph langgraph --version ## Invariants +- **Drive the graph only through the dev server** (see the mandatory rule above): `run_langgraph.ps1` → `run_workflow.py` → `http://127.0.0.1:2024`. Never import or run `graph.py`/`workflow.py`/`agents.py` in-process to execute the workflow — that bypasses the dev server and LangGraph Studio cannot track it. - Use the current Claude Code session for all Claude Code work. Do not launch another `claude` process. - Assume the current Claude Code session already has `--dangerously-skip-permissions`. - Run Python through the recorded conda environment. `run_langgraph.ps1` resolves conda from `-CondaExe`, `CONDA_EXE`, `conda` on PATH, or the user-profile miniconda fallback: diff --git a/langgraph-plan-review/scripts/__pycache__/agents.cpython-311.pyc b/langgraph-plan-review/scripts/__pycache__/agents.cpython-311.pyc new file mode 100644 index 0000000..44ceaa2 Binary files /dev/null and b/langgraph-plan-review/scripts/__pycache__/agents.cpython-311.pyc differ diff --git a/langgraph-plan-review/scripts/__pycache__/graph.cpython-311.pyc b/langgraph-plan-review/scripts/__pycache__/graph.cpython-311.pyc new file mode 100644 index 0000000..150b3d2 Binary files /dev/null and b/langgraph-plan-review/scripts/__pycache__/graph.cpython-311.pyc differ diff --git a/langgraph-plan-review/scripts/__pycache__/workflow.cpython-311.pyc b/langgraph-plan-review/scripts/__pycache__/workflow.cpython-311.pyc new file mode 100644 index 0000000..e61694a Binary files /dev/null and b/langgraph-plan-review/scripts/__pycache__/workflow.cpython-311.pyc differ diff --git a/langgraph-plan-review/scripts/manage_dev_server.ps1 b/langgraph-plan-review/scripts/manage_dev_server.ps1 index 1c55c86..10f7073 100644 --- a/langgraph-plan-review/scripts/manage_dev_server.ps1 +++ b/langgraph-plan-review/scripts/manage_dev_server.ps1 @@ -40,9 +40,9 @@ function Get-StoredPid { } function Test-ServerRunning { - $pid = Get-StoredPid - if ($pid -eq 0) { return $false } - $proc = Get-Process -Id $pid -ErrorAction SilentlyContinue + $storedPid = Get-StoredPid + if ($storedPid -eq 0) { return $false } + $proc = Get-Process -Id $storedPid -ErrorAction SilentlyContinue if (-not $proc) { return $false } return (Test-HttpOk "$ApiUrl/ok") } @@ -107,8 +107,8 @@ switch ($Command) { "start" { if (Test-ServerRunning) { - $pid = Get-StoredPid - Write-Host "[dev-server] Already running (PID $pid) at $ApiUrl" + $storedPid = Get-StoredPid + Write-Host "[dev-server] Already running (PID $storedPid) at $ApiUrl" Write-Host "[dev-server] Studio: https://smith.langchain.com/studio/?baseUrl=$ApiUrl" exit 0 } @@ -122,6 +122,10 @@ switch ($Command) { Remove-Item $StdoutLog -ErrorAction SilentlyContinue Remove-Item $StderrLog -ErrorAction SilentlyContinue + # Force UTF-8 to avoid GBK decode errors on Chinese Windows systems + $env:PYTHONUTF8 = "1" + $env:PYTHONIOENCODING = "utf-8" + $proc = Start-Process -FilePath $lgExe ` -ArgumentList "dev", "--no-browser" ` -WorkingDirectory $SkillDir ` @@ -167,9 +171,9 @@ switch ($Command) { } "stop" { - $pid = Get-StoredPid - if ((Stop-ServerProcess -PidValue $pid)) { - Write-Host "[dev-server] Stopped (PID $pid)" + $storedPid = Get-StoredPid + if ((Stop-ServerProcess -PidValue $storedPid)) { + Write-Host "[dev-server] Stopped (PID $storedPid)" } else { Write-Host "[dev-server] Not running (nothing to stop)" } @@ -178,8 +182,8 @@ switch ($Command) { "status" { if (Test-ServerRunning) { - $pid = Get-StoredPid - Write-Host "[dev-server] Running (PID $pid) at $ApiUrl" + $storedPid = Get-StoredPid + Write-Host "[dev-server] Running (PID $storedPid) at $ApiUrl" Write-Host "[dev-server] Studio: https://smith.langchain.com/studio/?baseUrl=$ApiUrl" } else { Write-Host "[dev-server] Not running"