更新了skill
This commit is contained in:
@@ -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`. |
|
||||
Binary file not shown.
@@ -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()
|
||||
Reference in New Issue
Block a user