更新了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()