更新了skill

This commit is contained in:
Myfro9
2026-06-22 16:41:44 +08:00
parent 48b965ec4e
commit 814dbf1681
13 changed files with 845 additions and 13 deletions
+219
View File
@@ -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 持久化**
驱动编译好的图,状态存在 `<output_dir>\.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:\<proj>\Notes" --output-dir "E:\<proj>\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:\<project-path>\Notes" `
--output-dir "E:\<project-path>\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 `<input-dir-parent>/runs` which may or may not be in the project directory.
The script prints `[INFO] Thread : <id>` and `[INFO] Output dir : <path>` (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:\<project-path>\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] <task_id>: <path>` — absolute paths to generated plan files
- `[REQ] <task_id>: <path>` — absolute paths to requirement files
For each plan path, read the file:
```powershell
Get-Content "<plan_path>" -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 "<start 打印的 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:\<project-path>\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:\<project-path>\fpga_runs" `
--rejected --feedback "<user feedback verbatim>"
```
> Use the **same `--output-dir`** you passed at `start` (it is printed on the
> `[INFO] Output dir :` line). Alternatively pass `--thread-id "<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:\<project-path>\fpga_runs"
> ```
### Step 4 — Show results
When the script exits with code 0 it prints `[SUMMARY]` followed by the contents
of `<output-dir>\summary.md`. Present this to the user.
If the user wants to inspect a specific task's plan or requirement:
```
<output-dir>\<task_id>\plan.md
<output-dir>\<task_id>\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:
```
<output_dir>\.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 `<output_dir>\.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
`<output_dir>\.fpga_flow_session.json` (and, if you want a clean checkpoint,
`<output_dir>\.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 `<output_dir>\` for `plan.md` files under each `<task_id>` folder |
| `interrupted` never advances | Make sure `--rejected` flag is accompanied by `--feedback` text |
| exit 3 (Codex 不可用) | Wait for codex quota, then `continue --output-dir <same path>` 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`. |
@@ -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 <output_dir>/.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()
+177
View File
@@ -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 | `<project>\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-<short-task-name>" --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-<short-task-name>" --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
`<project>\runs\` artifacts or the per-step `history`.
To abandon a paused run and start clean, delete `<project>\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. |
+123
View File
@@ -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 <dir> [--program-mode bitstream|flash] [--bit-path ..]
[--flash-path ..] [--thread-id ..] [--real] [--max-iterations N]
resume --project-dir <dir> --resume-json '<json>' [--thread-id ..] [--real]
status --project-dir <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()
+10 -3
View File
@@ -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_<part>_pullnone.bit` 修复此问题)。
ILA core 的 `Dropping logic core` WARNING 是正常现象(golden 镜像不含 ILA core),不影响烧写结果。
ILA core 的 `Dropping logic core` WARNING 是正常现象(载入 loader/golden 镜像时不含 ILA core),不影响烧写结果。
---
+15
View File
@@ -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]
+27
View File
@@ -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:
@@ -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"