diff --git a/bmem/SKILL.md b/bmem/SKILL.md new file mode 100644 index 0000000..ad63817 --- /dev/null +++ b/bmem/SKILL.md @@ -0,0 +1,282 @@ +--- +name: bmem +description: 用 bmem 记忆系统读写跨机器、跨 agent 的长期记忆——判断一条信息该进全局/项目/机器/log 哪一层、把 ./Notes 下的 PRxx/Taskxx/Resultxx 导入记忆、会话收尾该沉淀什么、怎么查以前做过什么、以及 bmem 命令报错时怎么修。当用户提到"记住"、"记一下"、"存到记忆里"、"之前/以前是不是做过"、"上次怎么解决的"、"查一下历史"、"这台机器装了什么"、"这个项目在别的机器上在哪"、"账号密码记下来"、"同步记忆"、"这个项目不做了/从记忆里删掉"、"bmem"、"记忆系统",或者在一个根目录有 bmem_.md 的项目里开始实质性工作、需要知道过去的决策和踩过的坑时,必须使用此技能——即使用户没有说"skill"或"bmem"两个字。用户改完 ./Notes 下的需求/方案/结果文档、或一段实质性工作收尾时也要用它。bmem 命令报错(未加入 bmem / 不在项目内 / PG 不可达 / git 冲突 / 重复注册)时同样用它。 +--- + +# bmem:Claude Code 与 Codex 共享的跨机器长期记忆 + +bmem 把记忆分四层:三层是 Markdown(用 gitea 在多机之间同步),第四层 log 是中心 +PostgreSQL + 本机 SQLite 镜像。**Markdown 是三层记忆的唯一事实源,SQLite 镜像是可 +随时删掉重建的派生物。** + +一句话分工:**Markdown 记知识(我们知道什么、为什么这么定),log 记经历(什么时候、发生了什么)。** + +| 层 | 回答什么 | 存在哪 | 判据 | +|---|---|---|---| +| 全局 global | 我通常怎么工作、跨项目的通用规则 | `~/bmem.md` | 换个项目还成立吗?成立 → global | +| 项目 project | 这个项目是什么、为什么这样设计 | `<项目根>/bmem_.md` | 项目结束后还有意义吗?没有 → project | +| 机器 machine | 这台机器有什么、能做什么、账号配在哪 | `~/bmem_.md` | 换台机器就不成立了吗?是 → machine | +| log | 这个项目发生过什么、Notes 里写了什么 | 中心 PG + `~/.bmem/bmem_local.sqlite` | 这是"发生了一件事"还是"我们知道一件事"?发生 → log | + +`~/bmem.md` 里还有两张注册表:哪些机器加入了系统、每个项目在每台机器上的绝对路径。 +**项目身份是 prjID,不是路径**——同一个项目在不同机器上路径不同,引用项目内文件要用相对路径。 + +## 先确认它能用 + +```bash +bmem help +``` + +`bmem` 不在 PATH 上时,用 `/Scripts/bmem.exe`(路径见 `~/skills_env.md`)。 +本机还没加入系统会提示 `run bmem install first`;当前目录不属于任何项目会提示先 `bmem init`。 + +## 开工之前:先把记忆取回来 + +bmem 没有 `context` 命令——**记忆就是几个 Markdown 文件,直接读**: + +1. `~/bmem.md` 的「全局规则」节:适用于所有项目的硬约束,**先读这个**。 +2. 当前项目根目录的 `bmem_.md`:项目简介、规则/约束、设计决策、当前状态与未解决问题。 +3. 需要了解本机环境(工具装在哪、账号怎么配)时再读 `~/bmem_.md`。 + +要查"以前做过没有、上次怎么解决的",用本 skill 自带的检索脚本查 log 层: + +```bash +python /scripts/bmem_recall.py "时延 扩展" # 语义条目 + 文档章节 + 事件 +python /scripts/bmem_recall.py "CP 长度" --project 52-link +python /scripts/bmem_recall.py --kind decision # 这个项目做过哪些决策 +python /scripts/bmem_recall.py --recent 10 # 最近发生了什么 +``` + +它只读本机镜像,不连中心库,离线可用。中文按 bigram 索引,**两到四个字的词组比整句命中率高**。 + +**为什么值得在动手前花这一分钟**:你即将做的决定可能三个月前已经做过,理由写在某条 +decision 里;你即将踩的坑可能上次已经踩过并记了下来。事后返工的成本远大于开头查一次。 + +命中只是线索,不是依据——**按结果里的 `项目:Notes/xxx.md § N` 回读原文再下结论。** + +## ./Notes 是这个记忆系统的一等公民 + +用户的工作习惯是:`PRxx.md`(需求)→ `Taskxx.md`(方案)→ `Resultxx.md`(结果), +都放在项目的 `./Notes` 下。bmem 按文件名把它们认成一组任务,全文入库、按标题拆成章节。 + +```bash +bmem notes # 扫描 ./Notes,把变化同步进记忆 +bmem notes --check # 只比对差异,不写入任何内容 +bmem notes --sync # 导入后立即同步到中心库 +``` + +每次扫描会报出四种状态:**新增 / 变更 / 一致 / 消失**。判定用的是内容的 canonical +sha256(去 BOM、CRLF→LF、NFC),所以改了空行或换行符不会被误判成变更,反复跑是安全的—— +内容没变就什么都不写。 + +两件值得知道的事: +- **文档从 ./Notes 里删掉后会被标记为"消失"**:它的历史修订全部保留(那是历史), + 但不再计入任务闭环——否则闭环会显示成"由一个已不存在的文件闭合",那是假信息。 + 文件放回来会自动恢复。 +- **项目记忆 `bmem_.md` 里有一个自动维护的「Notes 索引」小节**:任务闭环表 + (含「最近更新」= 日期 + 内容指纹)+ 其它文档清单。它完全由文件名、标题和内容 sha + 确定性推导(不经 LLM),**内容变了它就变、没变它就一动不动**(连日期都不写, + 否则会天天重写文件、每台机器都收到空更新);且只动这一节,手写的「设计决策」等内容不会被碰。 + 好处是:**只读项目记忆就能知道有哪些文档、哪些任务还没闭环、各自是不是最新的**, + 不必查数据库。 + +**目录铁律(这是全局规则 G-0001)**:`./Notes` 根目录只允许放用户手动创建、或用户明确要求 +创建的 md 文档。**你不能把运行过程中自动产生的状态、日志、中间结果写进 `./Notes/` 根目录**, +那些只能写到 `./Notes/auto/`(该子目录不会被导入记忆)。临时脚本和分析产物同理。 + +什么时候跑 `bmem notes`:用户新写或改完一篇 PR/Task/Result 之后、一段实质性工作收尾时。 +重复跑是安全的——内容没变就什么都不做(不脏数据、不写 journal)。 + +### 语义条目:把文档内容变成可检索的结构 + +`bmem notes` 只做到"全文 + 章节"。**把文档内容抽成需求/决策/结论/遗留问题/参数/账号指针, +是你的活**——bmem 自己不调用 LLM。跑完 `bmem notes` 它会打印新修订清单和 sha256, +按这个格式写 JSON 再喂回去: + +```json +{"facts": [ + {"rel_path": "Notes/Task07.md", + "content_sha256": "<该修订的 sha256,从 bmem notes 的输出里抄>", + "items": [ + {"kind": "decision", "title": "N2 的 CP 取 2us", + "body": "1us 余量不足,4us 损失过多有效符号时间;2us 是鲁棒性与效率的折中。", + "status": "done", "event_date": "2026-09-01", "section_no": 3} + ]} +]} +``` + +```bash +bmem notes --facts facts.json --agent claude-code --model <你的模型名> +``` + +`kind` 取值:`requirement` `decision` `result` `issue` `param` `credential-pointer` `lesson` `other`。 +`status` 取值:`open` `done` `superseded`(可省略)。 + +三件事值得知道: +- 条目**挂在具体修订的 sha256 下**,所以文档一改,旧条目自动退出"当前"视图,不会拿过期结论骗人。 +- 同一修订**重复提交 = 重抽**,bmem 会写成新一代 `extraction_gen`,旧代保留但不再是当前—— + 抽得不好可以重来,不用怕污染。 +- 抽取失败不影响主流程:全文和章节已经进库了,检索仍然兜得住。 + +**抽什么值得抽**:设计取舍和它的理由、被否决的方案和否决理由、遗留问题、关键参数、 +踩过的坑。**不值得抽**:文档结构的复述、代码里已经写清楚的东西。 + +## 判断:这条信息该进哪一层 + +这是用 bmem 时唯一真正需要判断力的地方。用最上面那张表的判据问自己一遍。 + +```bash +# 全局:跨项目成立 +bmem add --global "改完 Simulink 原生模型必须同步仿真模型,否则 RTL 已变而仿真还在验证旧行为" + +# 项目:这个项目的约束/结论(默认写进「项目规则/约束」) +bmem add --prj "ADC 采样率固定 61.44MSps,下游所有速率按它推导" + +# 项目:设计决策要写进「设计决策」节,并说清备选项和否决理由 +bmem add --prj --section "设计决策" "N2 的 CP 取 2us:1us 余量不足,4us 损失过多有效符号时间" + +# 机器:只对这台机器成立的事实 +bmem add --machine "ADRV9009 评估板接在这台机器上,JTAG 走 USB-Blaster II" +bmem add --machine --section "账号/服务配置" "gitea 账号 bowei,凭证在 Windows 凭据管理器" +``` + +条目会自动编号(G-/P-/D-/M-)并带日期,追加写入,不会覆盖已有内容。 + +**`--section` 决定它落在文件的哪一节,不给就用默认节。** 各层可用的节和默认值: + +| 层 | 默认节 | 其它可用节 | +|---|---|---| +| `--global` | 全局规则 | (通常就这一个) | +| `--prj` | 项目规则/约束 | 设计决策、当前状态 / 未解决问题、项目简介 | +| `--machine` | 工具环境 | 账号/服务配置、本机项目位置 | + +节名要**原样写全**(含空格,如 `--section "当前状态 / 未解决问题"`);写一个不存在的节名会新建一节。 + +这件事值得留意:**尚未解决的问题不是"规则/约束"**。用默认节会把一条待办混进项目铁律里, +半年后读的人分不清哪些是必须遵守的、哪些只是还没做完的。用 +`bmem add --prj --section "当前状态 / 未解决问题" "..."` 直接放对位置—— +不要写完再去手工编辑 Markdown 搬家,那样绕过了自动编号,也容易破坏表格和已有条目。 + +**凭证的处理**:默认只在机器记忆里记「账号 + 凭证放在哪」,明文口令留在本机 +`~/.bmem/config.toml`(不进任何仓库)。用户明确要求把某条明文写进机器记忆时照做—— +那是他的私有 gitea,由他决定;但不要自作主张替他把口令写进去。 + +## 什么不值得记 + +写太多等于没记,检索会被噪音淹没: + +- **代码和 git 历史已经说清楚的**:文件结构、某次修复的 diff、函数签名。记忆要回答"为什么这么改"。 +- **只对当前这轮对话有意义的中间状态**:临时变量、调试到一半的猜测、下一步计划。 +- **未经验证的猜测**——除非明确写成 `issue` 并标 `status: open`。 +- **会话转录全文**。 + +反过来有一类东西特别值得记而常常被忘记:**被否决的方案和否决它的理由**。 +只记结论,半年后会有人(很可能是你自己)重新提出同一个被否决的方案。 + +## 收尾之前 + +一段实质性工作结束前: + +1. 该写的记忆按层写进去(`bmem add`); +2. 用户新增/改动的 Notes 文档导进来,并附上本次会话摘要: + ```bash + bmem notes --session-summary <摘要文件> --agent claude-code + ``` + 摘要文件写到临时目录或 `./Notes/auto/`,**不要写进 `./Notes/` 根目录**。 +3. 对新修订生成语义条目 JSON 并入库(见上); +4. 同步出去: + ```bash + bmem sync + ``` + +`bmem sync` 做两件事:git 同步三层 Markdown(工作副本 ↔ `~/.bmem/repo` clone ↔ gitea), +以及与中心 PostgreSQL 双向同步 log 层。**它只碰 bmem 自己的文件,不会动你项目的 git 分支。** + +## 多机与本机状态 + +```bash +bmem machines # 所有机器 + 各自的项目和路径 +bmem machines --project 52-link # 这个项目在哪些机器上、各自路径、最近谁动过 +bmem machines --json # 结构化输出 +bmem sqlite # 本机镜像:位置、表结构、项目情况、内容摘要 +bmem sqlite --schema # additionally 每张表的列定义 +``` + +两条都**只读本机镜像、不连中心库**,离线可用。 + +`bmem machines` 会报注册表漂移:某机器只在 `bmem.md` 里有 = 它还没 push 或你还没 pull 到; +只在镜像里有 = **本机的 bmem.md 落后了**。这不是错误,是状态,读的时候要把它当成 +"我看到的可能不是最新的"。 + +## 新项目、新机器 + +```bash +bmem install # 每台机器一次:生成 machineID、拉记忆仓库、装 CLAUDE.md/AGENTS.md adapter +bmem init # 每个项目一次,在项目根目录跑 +``` + +`bmem init` 自己会判断三种情况:本机已注册过就提示不要重复注册;`bmem.md` 里已有这个项目 +(说明别的机器建过)就从记忆仓库把 `bmem_.md` 取回来并登记本机路径;全新项目才新建。 +**所以在第二台机器上直接跑 `bmem init` 是对的**,不会造成身份分裂。 + +`bmem install` 幂等,可以反复跑:它会顺带把机器记忆里的 OS、用户根目录刷新成当前实际值。 + +## 从本机注销一个项目 + +```bash +bmem delete # 只打印会动哪些东西,不做任何改动 +bmem delete --yes --sync # 确认执行并同步给其它机器 +``` + +**它只注销本机**:删掉项目根目录的 `bmem_.md`(**直接删除,不留备份文件**—— +`find_project` 是向上查找的,残留一份会让它下面所有子目录都被误判成这个项目的根; +删除前会先把内容存进记忆仓库 `~/.bmem/repo/projects/`,所以内容不丢)、 +`bmem.md` 里本机那一行、机器记忆的位置行、项目 `CLAUDE.md`/`AGENTS.md` 里的 BMEM 块, +以及中心库里 `project_locations` 中本机那一行。 + +**它不会删**:`./Notes` 下的任何文档、log 层的历史(修订/章节/条目/事件都留在中心库和镜像里)、 +其它机器的登记、记忆仓库里的 `projects/bmem_.md`。所以将来在任意机器上 +`bmem init` 就能重新接上,历史一条不少。 + +三件要注意的事: +- **默认只预览**。不加 `--yes` 什么都不会发生,输出会逐项列出"将删除"与"不会删除"。 + 破坏性操作先看清楚再动手,这是这条命令唯一的使用纪律。 +- 预览会告诉你**还有哪些机器登记着这个项目**。如果显示"本机是最后一台",说明删完就没有 + 任何机器持有它了——不是不能删,但要清楚这一步的含义。 +- 中心库的删除是**排队执行**的:离线也能删,删除意图记在本地并写进 journal, + 下次 `bmem sync` 成功后其它机器才会看到。所以删完记得 sync。 + +## 出问题时 + +| 症状 | 含义与处理 | +|---|---| +| `pg: 不可用(cannot reach PostgreSQL…)` | 中心库连不上。**数据没丢**:留在本机 outbox 和 `~/.bmem/journal/` 里,网络恢复后重跑 `bmem sync` 即可。所有命令本地照常可用 | +| `git: 冲突,已中止 rebase` | 两台机器改了同一个记忆文件。bmem **不会自动合并**(Markdown 是事实源),去 `~/.bmem/repo` 手工合并再 `bmem sync`。本地内容一条没丢 | +| `pull failed before any merge` | 传输失败,不是冲突。本地提交完好,push 仍会照常尝试 | +| 本地库损坏 / 想清空重来 | 直接删 `~/.bmem/bmem_local.sqlite` 再 `bmem sync`:已同步部分从中心库拉回,未推送部分由 journal 回放补齐。**这是设计好的恢复路径,不是下策** | +| `项目 X 已在本机注册,无需重复注册` | 正常提示,不是错误 | +| `facts reference an unknown revision` | facts JSON 里的 sha256 对不上。先跑一次 `bmem notes`,用它打印出来的 sha256 | +| `unknown fact kind` / `unknown status` | 枚举值写错了,对照上面的取值表 | +| 想知道还有多少没推上去 | `bmem sqlite` 底部的「未推送」和 journal 那一行 | + +**一条通则**:bmem 的报告里出现的不都是待办。「pg 不可用」「bmem.md 落后了」描述的是状态, +先判断这个状态该不该改,再决定要不要动手。为了让输出变干净而执行有副作用的命令, +是把手段当成了目的。 + +## 命令速查 + +| 用途 | 命令 | +|---|---| +| 读记忆 | 直接读 `~/bmem.md`、`bmem_.md`、`~/bmem_.md` | +| 查历史 | `scripts/bmem_recall.py <关键词>` / `--kind` / `--recent` | +| 写记忆 | `bmem add --global\|--prj\|--machine [--section 节名] <内容>` | +| 导入 Notes | `bmem notes [--check] [--session-summary F] [--facts F] [--sync]` | +| 看多机/本机状态 | `bmem machines [--project P] [--json]` / `bmem sqlite [--schema] [--json]` | +| 同步 | `bmem sync` | +| 安装与初始化 | `bmem install` / `bmem init` | +| 注销项目 | `bmem delete`(预览)/ `bmem delete --yes --sync`(执行) | +| 帮助 | `bmem help` | + +已知边界:当前 gitea 的 pack 服务故障,**push 正常但 clone/fetch 失败**,所以第二台机器 +暂时拉不到 Markdown 层(log 层走 PostgreSQL 不受影响,Notes 全文在中心库里)。 +看到 `bmem machines` 报"本机的 bmem.md 落后了"时,先想到这条。 diff --git a/bmem/evals/evals.json b/bmem/evals/evals.json new file mode 100644 index 0000000..86eba2d --- /dev/null +++ b/bmem/evals/evals.json @@ -0,0 +1,51 @@ +{ + "skill_name": "bmem", + "notes": "评测在隔离沙箱里跑:BMEM_HOME / BMEM_USER_HOME 指向临时目录,PG 与 git remote 关闭,每个 run 一份独立副本,绝不触碰用户真实记忆。沙箱预置了一个 mine-radio 项目:5 篇 Notes、3 条语义条目(含一条'CP 取 2us'的决策和一条 open issue),以及项目/机器记忆若干条。", + "evals": [ + { + "id": 0, + "name": "recall-before-work", + "prompt": "我准备动手改 mine-radio 的 OFDM 参数,想把 CP 从 2us 改成 1us 来提吞吐。动手之前先帮我确认下有没有什么坑。", + "expected_output": "先查记忆,发现 1us 这个方案半年前已被明确否决(巷道实测多径时延扩展 1.6us,1us 会引入符号间干扰),带来源引用地告诉用户,而不是泛泛讲 CP 原理。理想情况还会提到相关的 open issue(帧 182 CP 边界错位)。不改任何源文件。", + "assertions": [ + {"id": "a0", "text": "查询了记忆系统(跑了 recall 助手,或读了项目记忆 bmem_mine-radio.md / Notes/Task00.md)"}, + {"id": "a1", "text": "明确指出 1us 这个方案此前已被否决,而不只是泛泛讨论 CP 的取舍"}, + {"id": "a2", "text": "给出了否决的具体依据:巷道实测多径时延扩展约 1.6us,1us 小于它"}, + {"id": "a3", "text": "回答里带了可回溯的来源(Notes/Task00.md、bmem_mine-radio.md 或具体条目编号)"}, + {"id": "a4", "text": "提到了记忆里那条 open issue(帧 182 附近 CP 边界错位)"}, + {"id": "a5", "text": "没有修改任何项目源文件或 Notes 文档"} + ], + "files": [] + }, + { + "id": 1, + "name": "session-closeout", + "prompt": "自适应调制那部分做完了,Result01.md 我刚写完。帮我把这次的东西沉淀到记忆里。", + "expected_output": "跑 bmem notes 把 Result01.md 导入(新增 1 条修订),按打印出的 sha256 生成语义条目 JSON 并用 --facts 入库(至少抓到 CQI 上报周期 10ms 偏慢、改 5ms 这个实测发现),附会话摘要,最后 sync。临时文件不得写进 Notes 根目录。", + "assertions": [ + {"id": "b0", "text": "运行了 bmem notes,Result01.md 被导入(本地库 notes_revisions 出现 Notes/Result01.md)"}, + {"id": "b1", "text": "生成了 facts JSON 并通过 bmem notes --facts 入库(doc_facts 行数增加)"}, + {"id": "b2", "text": "facts 里的 content_sha256 与 Result01.md 的真实修订一致(没有伪造或用错 sha)"}, + {"id": "b3", "text": "抽出的条目抓住了实质发现:CQI 上报周期 10ms 偏慢 / 改为 5ms"}, + {"id": "b4", "text": "没有把摘要、JSON 等中间文件写进 Notes/ 根目录(全局规则 G-0001)"}, + {"id": "b5", "text": "记录了本次会话摘要(用了 --session-summary,sessions 表出现记录)"}, + {"id": "b6", "text": "执行了 bmem sync,并正确说明 PG 不可用时数据留在本地 outbox/journal 不会丢"} + ], + "files": [] + }, + { + "id": 2, + "name": "layer-routing", + "prompt": "记两件事:一是这台机器的 Vivado 装在 D:\\Xilinx\\Vivado\\2024.2;二是以后所有项目里,涉及定点位宽的改动都必须先跑一遍定点仿真再上板。", + "expected_output": "两条信息分属不同层:Vivado 安装路径只对这台机器成立 → 机器记忆;定点仿真规则跨项目成立 → 全局记忆。正确路由到 ~/bmem_lab-win-01.md 与 ~/bmem.md,而不是一股脑塞进项目记忆。", + "assertions": [ + {"id": "c0", "text": "Vivado 安装路径写进了机器记忆文件 bmem_lab-win-01.md"}, + {"id": "c1", "text": "定点仿真规则写进了全局记忆文件 bmem.md 的全局规则节"}, + {"id": "c2", "text": "两条都没有被错误地写进项目记忆 bmem_mine-radio.md"}, + {"id": "c3", "text": "用的是 bmem add 命令写入,而不是手工编辑 Markdown 文件"}, + {"id": "c4", "text": "向用户说明了这两条为什么分别进机器层与全局层"} + ], + "files": [] + } + ] +} diff --git a/bmem/scripts/bmem_recall.py b/bmem/scripts/bmem_recall.py new file mode 100644 index 0000000..a6ceceb --- /dev/null +++ b/bmem/scripts/bmem_recall.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python +"""在本机 bmem 镜像里检索历史:语义条目 / 文档章节 / 事件。 + +bmem 的 CLI 目前只负责写入与同步,没有检索命令,但它把 Notes 全文、章节拆分、 +语义条目和事件都存进了本机 SQLite 镜像。这个脚本把 bmem 当库调用,只读地把它们 +查出来——不改任何数据,不连中心库,离线可用。 + +用法: + python bmem_recall.py "多径 时延" # 三类都查 + python bmem_recall.py "CP 长度" --project amem # 限定项目 + python bmem_recall.py "时间戳" --kind decision # 只看某类语义条目 + python bmem_recall.py --recent 10 # 不给关键词=看最近发生了什么 + +中文按 bigram 检索(bmem 写入时做了滑窗补偿),所以两到四个字的词组比整句命中率高。 +""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +from pathlib import Path + + +def _bootstrap() -> None: + """bmem 装在 langgraph conda 环境里,当前解释器没有时换一个再跑。""" + try: + import bmem # noqa: F401 + return + except ImportError: + pass + + home = Path(os.path.expanduser("~")) + candidates = [ + home / "miniconda3" / "envs" / "langgraph" / "python.exe", + home / ".conda" / "envs" / "langgraph" / "python.exe", + home / "miniconda3" / "envs" / "langgraph" / "bin" / "python", + home / ".conda" / "envs" / "langgraph" / "bin" / "python", + ] + for python in candidates: + if python.exists() and Path(sys.executable).resolve() != python.resolve(): + env = dict(os.environ, PYTHONUTF8="1", PYTHONIOENCODING="utf-8") + raise SystemExit( + subprocess.run([str(python), __file__, *sys.argv[1:]], env=env).returncode + ) + raise SystemExit( + "找不到 bmem 包。它以 editable 方式装在 langgraph conda 环境里;\n" + "确认 `bmem help` 能跑,或查 ~/skills_env.md 里记录的解释器路径。" + ) + + +_bootstrap() + +from bmem import config as config_mod # noqa: E402 +from bmem.localdb import Database, fts_query # noqa: E402 + + +def _print_facts(db: Database, query: str, prj_id: str | None, kind: str | None, limit: int) -> int: + sql = ( + "SELECT f.prj_id, f.rel_path, f.kind, f.status, f.title, f.body, f.event_date, " + "f.extraction_gen FROM doc_facts f WHERE 1=1" + ) + params: list[object] = [] + if prj_id: + sql += " AND f.prj_id = ?" + params.append(prj_id) + if kind: + sql += " AND f.kind = ?" + params.append(kind) + if query: + sql += " AND (f.title LIKE ? OR f.body LIKE ?)" + params += [f"%{query}%", f"%{query}%"] + sql += " ORDER BY f.event_date DESC NULLS LAST, f.rel_path, f.fact_no LIMIT ?" + params.append(limit) + + rows = db.rows(sql, params) + if not rows: + return 0 + print("=== 语义条目 (doc_facts) ===") + for row in rows: + status = f" [{row['status']}]" if row["status"] else "" + date = f" {row['event_date']}" if row["event_date"] else "" + print(f"\n[{row['kind']}]{status}{date} {row['title']}") + print(f" 来源 {row['prj_id']}:{row['rel_path']} (第 {row['extraction_gen']} 代抽取)") + body = row["body"].strip().replace("\n", "\n ") + print(f" {body[:400]}{'…' if len(row['body']) > 400 else ''}") + return len(rows) + + +def _print_sections(db: Database, query: str, prj_id: str | None, limit: int) -> int: + """章节级命中:比整篇文档精确,直接告诉你该回读哪一节。""" + sql = ( + "SELECT s.prj_id, s.rel_path, s.section_no, s.heading, s.level, s.body_md " + "FROM note_sections s JOIN (" + " SELECT prj_id, rel_path, content_sha256 FROM notes_revisions r1 " + " WHERE imported_at = (SELECT max(imported_at) FROM notes_revisions r2 " + " WHERE r2.prj_id=r1.prj_id AND r2.rel_path=r1.rel_path)" + ") latest ON latest.prj_id=s.prj_id AND latest.rel_path=s.rel_path " + " AND latest.content_sha256=s.content_sha256 " + "WHERE s.body_md LIKE ?" + ) + params: list[object] = [f"%{query}%"] + if prj_id: + sql += " AND s.prj_id = ?" + params.append(prj_id) + sql += " ORDER BY s.prj_id, s.rel_path, s.section_no LIMIT ?" + params.append(limit) + + rows = db.rows(sql, params) + if not rows: + return 0 + print("\n=== 文档章节 (note_sections,仅最新修订) ===") + for row in rows: + heading = row["heading"] or "(前言)" + print(f"\n{row['prj_id']}:{row['rel_path']} § {row['section_no']} {heading}") + body = row["body_md"] + pos = body.lower().find(query.lower()) + start = max(0, pos - 120) if pos >= 0 else 0 + snippet = body[start : start + 400].strip().replace("\n", "\n ") + print(f" …{snippet}…") + return len(rows) + + +def _print_events(db: Database, query: str, prj_id: str | None, limit: int) -> int: + sql = "SELECT ts, machine_id, prj_id, event_type, result, summary FROM events WHERE 1=1" + params: list[object] = [] + if prj_id: + sql += " AND prj_id = ?" + params.append(prj_id) + if query: + sql += " AND summary LIKE ?" + params.append(f"%{query}%") + sql += " ORDER BY ts DESC LIMIT ?" + params.append(limit) + + rows = db.rows(sql, params) + if not rows: + return 0 + print("\n=== 事件 (events) ===") + for row in rows: + result = f" -> {row['result']}" if row["result"] else "" + print( + f" {row['ts'][:16]} {row['machine_id']} {row['prj_id']} " + f"[{row['event_type']}{result}] {row['summary'][:100]}" + ) + return len(rows) + + +def main() -> int: + parser = argparse.ArgumentParser(description="在本机 bmem 镜像里只读检索历史") + parser.add_argument("query", nargs="?", default="", help="关键词;中文用两到四个字的词组") + parser.add_argument("--project", help="限定 prjID") + parser.add_argument("--kind", help="限定语义条目类型:decision/issue/requirement/result/param/…") + parser.add_argument("--limit", type=int, default=8) + parser.add_argument("--recent", type=int, metavar="N", help="不检索,直接看最近 N 条事件") + args = parser.parse_args() + + try: + cfg = config_mod.load() + except Exception as exc: # noqa: BLE001 + print(f"本机还没加入 bmem:{exc}\n先跑 `bmem install`。", file=sys.stderr) + return 1 + + if not cfg.db_path.exists(): + print(f"本地镜像不存在:{cfg.db_path}\n跑一次 `bmem sync` 会从中心库重建。", file=sys.stderr) + return 1 + + with Database.open(cfg) as db: + if args.recent: + _print_events(db, "", args.project, args.recent) + return 0 + + if not args.query and not args.kind: + parser.error("给一个关键词,或用 --kind 列举某类条目,或用 --recent N 看最近发生了什么") + + hits = _print_facts(db, args.query, args.project, args.kind, args.limit) + if args.query: + # 空关键词配 LIKE '%%' 会把整库倒出来,只在真的有关键词时查这两类 + hits += _print_sections(db, args.query, args.project, args.limit) + hits += _print_events(db, args.query, args.project, args.limit) + + if hits == 0: + print(f"没有命中「{args.query}」。") + print("中文按 bigram 索引,换更短的词组再试;也可能这条历史确实没记过。") + print(f"(FTS 等价查询串:{fts_query(args.query)})") + else: + print(f"\n共 {hits} 条命中。命中只是线索——回读原文再下结论。") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/fpga-imx6-flow/SKILL.md b/fpga-imx6-flow/SKILL.md index 7919c54..0e5ed00 100644 --- a/fpga-imx6-flow/SKILL.md +++ b/fpga-imx6-flow/SKILL.md @@ -135,7 +135,7 @@ it, then resume the **same `--thread-id`** with a JSON payload. Repeat until exi | `action` | excalidraw node | What you (the session) do | Resume JSON | |---|---|---|---| -| `analyze` | 通过ila/串口分析 | Grab imx6 serial log + pull ILA waveform (vivado-mcp); summarize | `{"analysis_log":"<摘要/路径>"}` | +| `analyze` | 通过ila/串口分析 | Grab imx6 serial log + ILA; for RF/TX requirements also capture the requested analyzer spectrum; summarize all evidence | `{"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"}` | @@ -159,6 +159,33 @@ Notes: set in `propose`/`modify_fpga` chooses bitstream (download bit) vs flash (re-burn + full power/connect re-sequence). This is the big closed loop. +### Spectrum-analyzer evidence during `analyze` + +When the requirement includes RF or transmitter output, augment the ILA/console evidence +with a registered spectrum-analyzer MCP: + +- N9320B (tool prefix `mcp__mcp_n9320b__`): call `get_idn`, then `get_settings`, + then `get_spectrum` with `mark_peak=true` and a stable `csv_path`. +- DSA815 (tool prefix `mcp__mcp_dsa815__`): call `dsa_get_identity`, then + `dsa_get_status`, then `dsa_capture_plot`. + +Use the analyzer named by the requirement. If none is named, prefer DSA815. Capture both +only when the requirement asks for a cross-check or results need independent confirmation. +For DSA815 specifically: + +1. Call `dsa_get_identity`, then `dsa_get_status` to verify serial `DSA8A194401321` and + record the current sweep/unit settings. +2. Call `dsa_capture_plot` for the current Trace; use `dsa_capture_spectrum` only when + downstream numerical processing needs all 601 points. +3. Put the PNG/CSV paths, peak frequency/power, span, RBW and VBW in `analysis_log`. +4. Prefer read-only capture. If the requirement forces a frequency/bandwidth change, + save the original status, use the validated setting tools, and restore the original + values after capture. Never enable `dsa_scpi_write` for routine flow work. + +Always put the selected analyzer identity, artifact paths and measured settings in +`analysis_log`. If identity/connectivity fails, record that the RF evidence is unavailable +and do not claim the RF acceptance criterion passed from ILA or console data alone. + ## Step 3 — Finish On exit 0 (`status: done`), report to the user: iterations completed and that the @@ -274,7 +301,8 @@ Gotchas seen on a real install (2026-07-20): `imx_atsc_app.bak_pre_rx1`), md5-verify. 5. Cold-boot (tools_hw blast_off→power_off→power_on, TX-safe) so rc.local runs the new app. 6. Verify: network SSH grep `/var/module_log/imx_atsc_app.log` for `TALISE_radioOn` / `fb init state ok` - / `fpga[0xBB]=0x03` (sync=1/tvalid=1); spectrum for TX; (ILA needs an `.ltx` → skip unless re-impl). + / `fpga[0xBB]=0x03` (sync=1/tvalid=1); for TX use `mcp_dsa815.dsa_capture_plot` + and retain PNG/CSV + peak/settings; (ILA needs an `.ltx` → skip unless re-impl). > The `git HEAD` app (b07d73b) is the known-good **RX1-on-framerA (M=4/lane0x03/RX1RX2) + correct TX** > build. On-device `imx_atsc_app.rx1` and any `M=2/lane0x04` variant **break TX** — do not use them. diff --git a/langgraph-plan-review/SKILL.md b/langgraph-plan-review/SKILL.md index 066950d..d14629a 100644 --- a/langgraph-plan-review/SKILL.md +++ b/langgraph-plan-review/SKILL.md @@ -36,7 +36,24 @@ Use this skill to run the LangGraph plan-review workflow. All scripts are bundle 2. **codex 子进程要能到 chatgpt.com**。`clean_run.py` 会自动读 Windows 系统代理(HKCU Internet Settings 的 `ProxyServer`)设进 `HTTP(S)_PROXY`,让 codex 走代理;也可用 `PLAN_REVIEW_PROXY` 覆盖。 3. **codex 超时**:`review_task.py` 的内层 codex 超时取 argv[5](默认 300s,评审常不够)。用 - `--codex-review-command-b64` 传一条**末尾带超时秒数**的命令(见 Workflow),并把 `--timeout` 设一致。 + `--codex-review-command-b64` 传一条**末尾带超时秒数**的命令(见 Workflow)。**`--timeout`(外层 + agents.py 超时)必须明显大于内层值**(如内层 1500 / 外层 1800)——两者相等时外层会在 codex + 正常收尾前把它杀掉,报 `subprocess.TimeoutExpired`(2026-08-26 踩坑)。 + +- **2026-08-26 踩坑补充**(三条都已实测): + 1. **npm 最新版 codex 也可能太旧**:服务端 models 列表新增 `max` reasoning 档位后,npm 的 + 0.149.1 解析失败(`unknown variant 'max'`)直接退出。VS Code ChatGPT 扩展自带的 codex 更新 + (如 `%USERPROFILE%\.vscode\extensions\openai.chatgpt-<最新版本号>-win32-x64\bin\windows-x86_64\codex.exe`, + 0.150.0-alpha.8 可用)。`review_task.py` 支持 `CODEX_EXE` 环境变量指定该二进制,调用 + `clean_run.py` 前 `$env:CODEX_EXE = "<扩展codex.exe路径>"` 即可(扩展目录版本号会随更新变化, + 用 Get-ChildItem 找最新一个)。 + 2. **Windows elevated 沙箱挂起**:config.toml 有 `[windows] sandbox = "elevated"` 时,codex 在 + ChatGPT 应用之外独立运行会让**每条 shell 命令挂起**,评审读不到文件(表现为评审意见说 + "cannot open file")。`review_task.py` 已固定加 `-c sandbox_mode=read-only` 规避(评审只需读; + 注意 `codex exec resume` 不认 `--sandbox` 旗标,只能用 `-c`)。 + 3. **UTF-8**:直接跑 `clean_run.py` 时先设 `$env:PYTHONUTF8='1'; $env:PYTHONIOENCODING='utf-8'`, + 否则父进程按 cp1252 解码 codex 输出中的中文会 UnicodeDecodeError(旧 dev-server 路径由 + run_langgraph.ps1 代设,clean_run 路径需要自己设)。 - **改了 `workflow.py`/`agents.py`/`clean_run.py` 后**:无 dev server 缓存,下次 `clean_run.py` 直接生效。 若图结构改到与旧 checkpoint 不兼容,删 `scripts\.plan_review_state.sqlite` 重新开始该 thread。 diff --git a/langgraph-plan-review/scripts/review_task.py b/langgraph-plan-review/scripts/review_task.py index 70b22c0..7dec965 100644 --- a/langgraph-plan-review/scripts/review_task.py +++ b/langgraph-plan-review/scripts/review_task.py @@ -121,7 +121,14 @@ def main() -> int: os.close(fd) last_message_path = Path(last_message_name) try: - flags = ["--json", "--skip-git-repo-check", "--output-last-message", str(last_message_path)] + # read-only sandbox: review needs no writes, and on Windows the + # config.toml "elevated" sandbox hangs every shell command when codex + # runs outside the ChatGPT app (probed 2026-08-26). Use -c because + # "codex exec resume" does not accept the --sandbox flag. + # (unquoted value: fails TOML parse -> codex falls back to the raw + # string, which survives cmd /c quoting untouched) + flags = ["--json", "--skip-git-repo-check", "-c", "sandbox_mode=read-only", + "--output-last-message", str(last_message_path)] if codex_session_id: command_args = ["cmd", "/c", codex, "exec", "resume"] + flags + [codex_session_id, "-"] else: