Compare commits
4
Commits
24b9c271e1
...
58b90f16a6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58b90f16a6 | ||
|
|
f665448376 | ||
|
|
aca7c799a8 | ||
|
|
d631dfef5c |
@@ -369,5 +369,7 @@ __pycache__/
|
|||||||
/vercel-react-native-skills/
|
/vercel-react-native-skills/
|
||||||
/vercel-react-view-transitions/
|
/vercel-react-view-transitions/
|
||||||
/web-design-guidelines/
|
/web-design-guidelines/
|
||||||
|
/orca-cli/
|
||||||
|
/orchestration/
|
||||||
/.stfolder/
|
/.stfolder/
|
||||||
/.claude/
|
/.claude/
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
---
|
||||||
|
name: dpd-mig-flow
|
||||||
|
description: >
|
||||||
|
Run the dpd-mig-flow LangGraph workflow (checkout path 见 `~\skills_env.md` 的
|
||||||
|
`DPD_MIG_FLOW_ROOT`) — the
|
||||||
|
pipeline that ports the DPD + CFR functionality (FPGA RTL + i.MX6 app) from the ATSC
|
||||||
|
reference project (SMOD_ATSC\my_exciter) into **another SMOD project**, then verifies it
|
||||||
|
on real hardware: survey the reference design, port FPGA and imx6 sides in parallel,
|
||||||
|
build the bitstream and cross-compile the app (both via the fpga-imx6-flow skill),
|
||||||
|
debug on the board with the fake-PA distortion model, and gate on two objective
|
||||||
|
measurements — DPD trains without error and settles to Hold with shoulder ≥ 40 dB, and
|
||||||
|
the shoulder survives a power cycle. It drives the graph in-process (no langgraph dev)
|
||||||
|
and hands its 6 interrupts back to this Claude session to execute.
|
||||||
|
|
||||||
|
ALWAYS use this skill whenever the user wants to port, migrate, or transplant DPD/CFR
|
||||||
|
into another SMOD project, or wants to run/start/drive/resume/continue this workflow —
|
||||||
|
including phrasings like "移植 DPD", "把 DPD 和 CFR 移到 DAB/DVBT2 工程", "跑 dpd-mig-flow",
|
||||||
|
"启动 DPD 移植流程", "继续 DPD 移植", "resume the DPD migration", "port DPD to the DVB-T2
|
||||||
|
exciter", "DPD 带肩不达标怎么迭代", "验证 DPD 断电保持", even when they don't name the
|
||||||
|
project path or say the word "skill". Because the target project is never fixed, this
|
||||||
|
skill always asks the user which Vivado project to port INTO before starting.
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this skill does
|
||||||
|
|
||||||
|
Drives the **dpd-mig-flow** graph (10 nodes / 12 flow-edges, 1:1 with
|
||||||
|
`Notes/dpd-mig-flow.drawio`; plan in `Notes/Task00.md`). Two node classes:
|
||||||
|
|
||||||
|
- **6 interrupt nodes** pause the graph and hand control to **this Claude session**,
|
||||||
|
which does the real work (read reference RTL/C, copy and rewire code, invoke the
|
||||||
|
`fpga-imx6-flow` skill to synthesize / cross-compile / debug on the board).
|
||||||
|
- **2 measurement nodes** run in-process and decide objectively: read DPD state over
|
||||||
|
HTTP/SSH, measure shoulder with the N9320B, power-cycle the board via the COM relay.
|
||||||
|
|
||||||
|
Persistence is a SqliteSaver file, so `start` / `resume` survive across separate process
|
||||||
|
calls, and each target project gets its own thread.
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|---|---|
|
||||||
|
| Project root | 见 `~\skills_env.md` 的 `DPD_MIG_FLOW_ROOT` |
|
||||||
|
| Conda env | `langgraph` (path 见 `~\skills_env.md` 的 `CONDA_LANGGRAPH_PYTHON`) |
|
||||||
|
| Graph | `app.main_graph:build_graph` (in-process, no langgraph dev) |
|
||||||
|
| Driver | `<project>\run.py`(`start` / `resume` / `status` 三个子命令) |
|
||||||
|
| Checkpoint | `<project>\runs\checkpoint.sqlite`(按 `thread_id` 分区) |
|
||||||
|
| Reference project | 见 `~\skills_env.md` 的 `SMOD_ATSC_REF`,顶层 `...\sources_1\top\smod_top.v` |
|
||||||
|
| Target project | **每次运行由用户指定**,见 Step 1 |
|
||||||
|
| Design source | `Notes/dpd-mig-flow.drawio`,方案 `Notes/Task00.md` |
|
||||||
|
| Self-test | `<project>\smoke_test.py`(mock,68 项检查) |
|
||||||
|
|
||||||
|
```
|
||||||
|
PY = <CONDA_LANGGRAPH_PYTHON — 见 ~\skills_env.md>
|
||||||
|
PROJ = <DPD_MIG_FLOW_ROOT — 见 ~\skills_env.md>
|
||||||
|
RUN = %PROJ%\run.py
|
||||||
|
```
|
||||||
|
|
||||||
|
> **直接用环境里的 python.exe 跑,别用裸 `conda run`**:本机 GBK 控制台下 `conda run` 会用
|
||||||
|
> GBK 重新编码子进程的 UTF-8 stdout,**遇到中文 interrupt payload 会崩**。用环境
|
||||||
|
> `python.exe`,或 `conda run --no-capture-output`。
|
||||||
|
|
||||||
|
## ⚠️ 硬件时序铁律
|
||||||
|
|
||||||
|
**绝不在 USB-Blast 处于连接状态时给板卡上电或断电。** 本工作流的 `check_power_cycle`
|
||||||
|
节点会真的断电重启,其内部顺序已固定为:
|
||||||
|
|
||||||
|
```
|
||||||
|
断开 USB-Blast → 断电 → 等待 → 上电 → 等 app 就绪
|
||||||
|
```
|
||||||
|
|
||||||
|
`power_on()` / `power_off()` 各自还会兜底确认一次 USB-Blast 已断开。断电保持判定不需要
|
||||||
|
JTAG,所以默认**不再接回** USB-Blast(需要接回时把 `env_config.json` 的
|
||||||
|
`power.reconnect_blast` 置 `true`)。手工介入板卡时同样遵守这条。
|
||||||
|
|
||||||
|
继电器 COM 号**唯一来源是 `~/vivado_mcp_usb.conf`**(`[power]` / `[usb_blast]`,逐机而异、
|
||||||
|
不入库),与 vivado-mcp 的 `power_on`/`USB_Blast_on` 共用同一份配置;文件缺失时调 vivado-mcp
|
||||||
|
的 **`detect_serial_relays`** 工具自动探测生成。
|
||||||
|
|
||||||
|
## Step 0 — 确认环境(驱动前必做)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$py = @("$env:USERPROFILE\miniconda3\envs\langgraph\python.exe","$env:USERPROFILE\.conda\envs\langgraph\python.exe") | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||||
|
if (-not $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')"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`[OK]` → 进 Step 1。缺失/导入失败 → 告诉用户先跑 **`/langgraph-vivado-install`**,别硬上。
|
||||||
|
|
||||||
|
想先确认工作流本身没坏(不碰任何硬件):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& $py "$PROJ\smoke_test.py" # 期望 ALL PASS (68 checks)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1 — 问用户:移植到哪个工程?(这一步不能跳过)
|
||||||
|
|
||||||
|
设计图里全程只说"当前 vivado 工程",因为**它本来就不固定**——这条流水线是为"把 ATSC 的
|
||||||
|
DPD/CFR 搬到任意 SMOD 工程"设计的。所以目标工程**必须每次运行由用户显式给出**:没有默认值、
|
||||||
|
不沿用上次、不从配置文件里读、不替用户猜。`run.py` 会强制校验,缺参数直接非零退出。
|
||||||
|
|
||||||
|
`XILINX_SMOD_ROOT`(见 `~\skills_env.md`)下的候选通常有 `SMOD_DAB`、`SMOD_DVBT2`、`SMOD_DTMB_CG`、
|
||||||
|
`SMOD_DVBS`、`SMOD_SAT`。用 AskUserQuestion 或直接问,把这 7 项凑齐:
|
||||||
|
|
||||||
|
| 参数 | 含义 | 怎么定 |
|
||||||
|
|---|---|---|
|
||||||
|
| `--target-vivado` | 目标 Vivado 工程目录 | 用户指定 |
|
||||||
|
| `--target-xpr` | 目标工程 `.xpr` | 在上面目录里找,找到多个就让用户选 |
|
||||||
|
| `--target-imx6` | 目标配套 imx6 工程目录 | 通常是工程下的 `imx6` |
|
||||||
|
| `--standard` | 调制标准 | DAB / DVBT2 / DTMB / DVBS / SAT |
|
||||||
|
| `--symbol-clk-mhz` | 调制核输出符号采样率时钟 | **决定 DPD/CFR 挂在哪条数据链上**,问用户或从目标顶层读出来核实 |
|
||||||
|
| `--center-hz` | 频谱测量中心频率 | 决定带肩怎么测 |
|
||||||
|
| `--ch-bw-hz` | 信道带宽 | 同上 |
|
||||||
|
|
||||||
|
参数多,可以先写成档案文件再点名(**档案名仍须每次显式给出**,不许自动挑最新的):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& $py $RUN start --target-profile "$PROJ\targets\smod_dab.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
`start` 会校验目录/`.xpr` 真实存在、目标不是参考工程(防止误改参考工程 `my_exciter`),派生
|
||||||
|
`thread_id`(含工程指纹,不同工程绝不共用 checkpoint),并把参数落到
|
||||||
|
`runs/<thread_id>/launch.json` 留痕。
|
||||||
|
|
||||||
|
## Step 2 — 启动
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& $py $RUN start `
|
||||||
|
--target-vivado "<目标工程目录>" `
|
||||||
|
--target-xpr "<目标工程目录>\<name>.xpr" `
|
||||||
|
--target-imx6 "<目标工程目录>\imx6" `
|
||||||
|
--standard DAB --symbol-clk-mhz 8.192 --center-hz 214e6 --ch-bw-hz 1.536e6
|
||||||
|
```
|
||||||
|
|
||||||
|
输出一行 JSON:`{"status":"waiting","thread_id":...,"interrupts":[{"id":...,"task":...,"value":{...}}]}`。
|
||||||
|
**记下 `thread_id`**,后面每次 resume 都要用。
|
||||||
|
|
||||||
|
想先空跑一遍看流程(不碰仪器/串口/板卡):设 `$env:DPD_MIG_MOCK="1"` 再 start。
|
||||||
|
|
||||||
|
## Step 3 — 执行 interrupt,然后 resume(主循环)
|
||||||
|
|
||||||
|
每个 interrupt 的 `value` 里带 `action`、`instructions`(这一步的完整任务书)、`target`
|
||||||
|
(本次运行指定的目标工程,**据此决定往哪个工程写**,别靠记忆)、`expected_resume`(该回填什么)。
|
||||||
|
|
||||||
|
### ⚠️ 并行阶段会同时抛出 2 个 interrupt
|
||||||
|
|
||||||
|
`study_ref` 之后图分成 FPGA 侧和 imx6 侧两条并行支路,`invoke` 会**同时**返回 2 个
|
||||||
|
interrupt(构建阶段同理)。因此 resume **统一用 `{interrupt_id: payload}` 映射**,
|
||||||
|
单值形式会因为无法指定目标而报错卡死。可以分批恢复:只给一个 id,另一个保持挂起。
|
||||||
|
|
||||||
|
### resume 的正确姿势:写文件,别贴 JSON
|
||||||
|
|
||||||
|
Windows PowerShell 5.1 会**吞掉**传给原生 exe 的双引号,直接把 JSON 贴到命令行必然变成非法
|
||||||
|
JSON。用 `--mapping-file`(最省事,Write 出一个文件即可)或 `--mapping-b64`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# resume.json 内容形如:
|
||||||
|
# { "<id_of_port_fpga_rtl>": { "fpga_port_done": true, ... },
|
||||||
|
# "<id_of_port_imx6_src>": { "imx6_port_done": true, ... } }
|
||||||
|
& $py $RUN resume --thread-id "<thread_id>" --mapping-file "<scratch>\resume.json"
|
||||||
|
|
||||||
|
# 随时看卡在哪
|
||||||
|
& $py $RUN status --thread-id "<thread_id>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6 个 interrupt 各自要做什么
|
||||||
|
|
||||||
|
| action | 你要做的事 | resume 回填 |
|
||||||
|
|---|---|---|
|
||||||
|
| `survey_reference`<br>(#3) | 通读参考工程的 DPD/CFR 用法,产出调研 md + 待移植文件清单。重点:旁路宏 `DPDENABLE`/`CFR_ENABLE`;发射链 `digital_rms_agc → CFR_cfr_conf → dacfr → DPD`;ORx 反馈链 `120M → rx1_dec3_120to40 → orx_dec_hb_40to10 → RAM2 → orx_iq_udp_tx`;SPI3 仲裁 `spi3_agc_arbiter`;DPD 系数存取 `spi_slave_2_DPD_SL.v` | `ref_survey_md`, `ref_file_list` |
|
||||||
|
| `port_fpga`<br>(#4) | 复制 DPD/CFR 源码进目标工程并加入 `.xpr`;参考 `smod_top.v` 接进目标顶层;**做速率转换**(参考工程 DPD/CFR 跑 40 MHz,目标符号率不同,必须接在调制核输出的正确符号采样率链上并与后续 204b 接口时序匹配);宏旁路;**ORx AGC + SPI3 仲裁与参考工程完全一致**;**保留 fake-PA**;建独立 DPD 寄存器模块(对标 `reg_map_dpd.v`) | `fpga_port_done`, `fpga_ported_files`, `rate_conv_scheme`, `reg_map_dpd_path`, `bypass_macros`, `fake_pa_kept`, `orx_agc_ported` |
|
||||||
|
| `port_imx6`<br>(#5) | 整理并移植 6 项功能:①寄存器控制 ②**DPD 控制状态流程 md(含 mermaid)** ③前面板键盘+液晶 ④SPI3 仲裁 ⑤网页控制+状态显示 ⑥隐藏菜单参数。入口:`bsp/dpd_proc.c`、`bsp/eeprom.c`、`task/lcd_task.c`、`task/uart_user.c`、`adrv9009/adrv9009_config.c`、`web/www*/sys_set.html`。**顺手把 `env_config.json` 的 `board.endpoints` 按实际接口填实** | `imx6_port_done`, `imx6_ported_files`, `dpd_state_md`, `imx6_feature_md`, `imx6_features_ok`(6 项全 true 才放行) |
|
||||||
|
| `build_fpga`<br>(#11) | 调 **`fpga-imx6-flow` skill** 综合实现出 bitstream + flash 文件;重点看速率转换到 204b 那段时序有没有违例 | `build_fpga_ok`, `bit_path`(必须真实存在), `flash_path` |
|
||||||
|
| `build_imx6`<br>(#6) | 调 **`fpga-imx6-flow` skill** 交叉编译出 app | `build_imx6_ok`, `app_path` |
|
||||||
|
| `hw_debug`<br>(#10) | 调 **`fpga-imx6-flow` skill** 下 bit、部署 app、重启;**开 fake-PA**;网页 DPD start 训练;看 ILA/串口;用 N9320B 看 DPD 前后带肩。离开本节点前确保 fake-PA 已开、DPD 已 start、板卡与频谱仪在线——下一个节点会自动读状态+测带肩来判定 | `debug_log`, `fake_pa_on`(必须 true), `dpd_started`(必须 true), `shoulder_before_db`(可选,用于判定改善量) |
|
||||||
|
|
||||||
|
第 2 轮起 `hw_debug` 的 payload 里会带 `previous_failure`:**先看它再动手**。带肩不达标 →
|
||||||
|
查 DPD 收敛 / ORx AGC / 速率转换;断电后带肩掉了 → 查 DPD 系数 save/load 与 eeprom 持久化。
|
||||||
|
|
||||||
|
### 两个判定节点(不需要你介入,除非降级)
|
||||||
|
|
||||||
|
- `check_dpd_train`(#15):等训练收敛 → 读 DPD 状态 → 测带肩。四个条件全过才算通过:
|
||||||
|
训练无报错、状态为 `Hold`、带肩 ≥ 40 dB、(若已知 DPD 前带肩)确有改善。否则回 `hw_debug`。
|
||||||
|
- `check_power_cycle`(#19):测重启前带肩 → 按铁律断电重启 → 等 app 起来 → 复测 →
|
||||||
|
变化 ≤ 1.5 dB 且仍 ≥ 40 dB 且状态仍为 Hold 才算通过。否则回 `hw_debug`。
|
||||||
|
|
||||||
|
**降级裁决**:仪器/板卡够不着或读数矛盾时,节点会抛 `action: "adjudicate"` 的 interrupt,
|
||||||
|
payload 的 `evidence` 里带着已采到的数据。你据此给结论:`{"pass": true/false, "reason": "..."}`。
|
||||||
|
别为了让流程往下走而随手判 true——这两个判据就是整个任务的验收标准。
|
||||||
|
|
||||||
|
> 测量结果按 `(thread, iteration)` 落盘缓存(`runs/<thread>/iter<N>/verify_*.json`)。
|
||||||
|
> interrupt-resume 会让节点从头重跑,缓存保证**一轮之内绝不会把板子断电两次**。
|
||||||
|
|
||||||
|
## Step 4 — 结束
|
||||||
|
|
||||||
|
两个判定都过 → `task_done`,`status` 变 `done`,汇总落 `runs/<thread_id>/summary.json`
|
||||||
|
(含最终带肩、断电前后读数、bit/app 路径、产出的几份 md)。把这些回报给用户。
|
||||||
|
|
||||||
|
迭代超过 `max_iterations`(默认 10)会优雅报错退出——这是让人介入判断"是移植方案的问题还是
|
||||||
|
指标定义的问题",不是让你调大上限硬撑。
|
||||||
|
|
||||||
|
## 真机运行前的检查清单
|
||||||
|
|
||||||
|
`DPD_MIG_MOCK` **不设**(或设为 0)就是真机模式。开跑前确认:
|
||||||
|
|
||||||
|
| 项 | 怎么确认 | 缺了怎么办 |
|
||||||
|
|---|---|---|
|
||||||
|
| 继电器 COM 号 | `~/vivado_mcp_usb.conf` 有 `[power]` 和 `[usb_blast]` | 调 vivado-mcp 的 `detect_serial_relays` |
|
||||||
|
| 板卡 DPD 接口 | `env_config.json` 的 `board.endpoints`(或 `ssh_commands`)已按目标工程实际接口填实 | 还是占位模板时 `tools_board` 会**明确报错**,不会对着不存在的接口测出假结论;先完成 `port_imx6` 再来 |
|
||||||
|
| N9320B | `spectrum.host`(默认取 `~\skills_env.md` 的 `N9320B_HOST`)能 ping 通 | 检查网络/仪器电源 |
|
||||||
|
| 板卡 | `board.ip`(默认取 `~\skills_env.md` 的 `SMOD_BOARD_IP`)能通 | 先上电 |
|
||||||
|
| 频谱判据 | `center_hz` / `ch_bw_hz` 是本次目标标准的正确值 | 问用户 |
|
||||||
|
|
||||||
|
带肩定义(DPD 前后、断电前后必须用**完全相同**的仪器设置,否则数值不可比):带内参考取
|
||||||
|
信道内 PSD 均值,肩部取 `±(ch_bw/2 + ch_bw × shoulder_offset_ratio)` 处 PSD,
|
||||||
|
`shoulder_db = 带内 − 肩部`,取上下边带较差者。RBW/VBW/检测器/平均/参考电平/衰减全部由
|
||||||
|
`env_config.json` 固定。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
| 现象 | 原因 / 处理 |
|
||||||
|
|---|---|
|
||||||
|
| `缺少必填的目标工程参数` | 按设计如此(每次必须显式指定),把 7 个参数补齐 |
|
||||||
|
| `目标工程不能是参考工程` | 防误改 `my_exciter` 的保护,检查 `--target-vivado` |
|
||||||
|
| `目标工程参数与本 thread 已记录的不一致` | 中途改了参数。换工程会自动换 thread;只是想改测量频点/带宽就用 `--tag` 开新 thread,别在半途改判据 |
|
||||||
|
| `resume 参数不是合法 JSON` 且引号丢了 | PowerShell 吞引号,改用 `--mapping-file` / `--mapping-b64` |
|
||||||
|
| `未知的 interrupt id` | id 过期了(已被恢复过)。先 `status` 拿当前 id |
|
||||||
|
| `board 接口仍是占位模板` | `port_imx6` 还没把实际网页/命令行接口填进 `env_config.json` |
|
||||||
|
| 控制台中文乱码/崩溃 | 别用裸 `conda run`,直接用环境 `python.exe` |
|
||||||
|
| 改了 `app/*.py` 后行为没变 | 无 dev server 缓存,直接重跑即可;若图结构改到与旧 checkpoint 不兼容,换 `--tag` 或删 `runs/checkpoint.sqlite` |
|
||||||
|
|
||||||
|
## 相关
|
||||||
|
|
||||||
|
- **`fpga-imx6-flow`** — 综合/交叉编译/下载/上板调试都转交它(本图不嵌套调用另一张图,
|
||||||
|
两张图各有 checkpointer 与 interrupt 语义,进程内嵌套会互相污染)
|
||||||
|
- **`smod_atsc_dev`** — SMOD 平台事实、两条烧写路径的选择、上电时序
|
||||||
|
- **`fpga_flash`** / **`fpga_update`** — JTAG 直烧 / 远程升级 mcs
|
||||||
|
- **`langgraph-vivado-install`** — 创建 `langgraph` conda 环境
|
||||||
Binary file not shown.
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: sim-rtl-flow
|
name: sim-rtl-flow
|
||||||
description: >
|
description: >
|
||||||
Run the sim-rtl-flow LangGraph workflow at E:\LangGraph_Design\sim-rtl-flow — the
|
Run the sim-rtl-flow LangGraph workflow (checkout path 见 `~\skills_env.md` 的
|
||||||
|
`SIM_RTL_FLOW_ROOT`) — the
|
||||||
Simulink-to-RTL-to-hardware design pipeline: read the previous run's design version-tree
|
Simulink-to-RTL-to-hardware design pipeline: read the previous run's design version-tree
|
||||||
doc xxx_tree.md to decide what may be reused → Simulink -x principle sim
|
doc xxx_tree.md to decide what may be reused → Simulink -x principle sim
|
||||||
(sim-golden) → HUMAN sign-off on simulation performance → optional -q fixed-point →
|
(sim-golden) → HUMAN sign-off on simulation performance → optional -q fixed-point →
|
||||||
@@ -228,7 +229,7 @@ regenerated tree has lost last round's attributes. Default, as everywhere here,
|
|||||||
`<work_dir>/.sim_rtl_flow/`** — that is where `resume`/`record`/`status` will look.
|
`<work_dir>/.sim_rtl_flow/`** — that is where `resume`/`record`/`status` will look.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat > /c/Users/Administrator/AppData/Local/Temp/claude/input.json <<'EOF'
|
cat > $TMPDIR/sim_rtl_flow_input.json <<'EOF'
|
||||||
{
|
{
|
||||||
"requirements_path": "Notes/requirements.md",
|
"requirements_path": "Notes/requirements.md",
|
||||||
"work_dir": "E:/Xilinx_design/my_project",
|
"work_dir": "E:/Xilinx_design/my_project",
|
||||||
@@ -257,7 +258,7 @@ EOF
|
|||||||
### 2. Start — exits 10 with a handoff
|
### 2. Start — exits 10 with a handoff
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
"$PY" "$RUN" --thread-id my-design-001 start --input /c/Users/Administrator/AppData/Local/Temp/claude/input.json
|
"$PY" "$RUN" --thread-id my-design-001 start --input $TMPDIR/sim_rtl_flow_input.json
|
||||||
```
|
```
|
||||||
|
|
||||||
`start` takes `work_dir` from the input JSON and prints where state landed. Verified:
|
`start` takes `work_dir` from the input JSON and prints where state landed. Verified:
|
||||||
|
|||||||
+96
-38
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: tools-sync
|
name: tools-sync
|
||||||
description: 在本机统一同步并配置 Claude Code 与 Codex 的 skills、MCP server 和开发环境:将 ~\.claude\skills 和 ~\.codex\skills 与 git.rpchannel.com 上的 bowei/skills-win 仓库保持同步,审计 skills 中的硬编码参数并迁移到本机 ~\skills_env.md,校验 langgraph conda 环境,双向发现并补齐 Claude Code 与 Codex 的用户 MCP 注册,同时确保 E:\MCP_design 下所有 MCP 均已注册。同步的传输一旦出问题,本 skill 还负责登录 gitea 服务器(上海 mac 的 Docker 容器)诊断并修复,直到同步真正成功。当用户提到"同步skills"、"skills同步"、"tools sync"、"tools-sync"、"同步codex"、"同步MCP"、"环境同步"、"skills-win"、"把skills推上去/拉下来"、"新电脑配置claude/codex环境"、"检查MCP注册"、"skills_env",或者 git.rpchannel.com 出现"clone/fetch 失败"、"bad pack header"、"early EOF"、"RPC failed"、"推不上去/拉不下来"、"gitea 坏了/连不上/要修"时,必须使用此技能,即使用户没有明确说"skill"二字。
|
description: 在本机统一同步并配置 Claude Code 与 Codex 的 skills(~\.claude\skills / ~\.codex\skills / ~\.agents\skills)、用户级 MCP 注册和开发环境:主副本与 git.rpchannel.com 上的 bowei/skills-win 仓库保持同步(其余目录一律是 junction),审计 skills 中的硬编码参数并迁移到本机 ~\skills_env.md,校验 langgraph conda 环境,双向发现并补齐 Claude Code 与 Codex 的用户 MCP 注册,同时确保 E:\MCP_design 下所有 MCP 均已注册。同步的传输一旦出问题,本 skill 还负责登录 gitea 服务器(上海 mac 的 Docker 容器)诊断并修复,直到同步真正成功。当用户提到“同步skills”、“skills同步”、“tools sync”、“tools-sync”、“同步codex”、“codex 里的 skill 怎么进仓库”、“把 codex 新写的 skill 收进来”、“同步MCP”、“MCP 注册/更新/补齐”、“检查MCP注册”、“codex mcp 没了”、“环境同步”、“skills-win”、“把skills推上去/拉下来”、“新电脑配置claude/codex环境”、“skills_env”、“skills 硬编码路径”,或者 git.rpchannel.com 出现“clone/fetch 失败”、“bad pack header”、“early EOF”、“RPC failed”、“推不上去/拉不下来”、“gitea 坏了/连不上/要修”时,必须使用此技能,即使用户没有明确说“skill”二字。也用于处理 junction 指错、两侧 skill 分叉、以及 skills 里的硬编码路径/密码需要迁进 ~\skills_env.md 的场合。
|
||||||
---
|
---
|
||||||
|
|
||||||
# tools-sync:Claude Code / Codex 工具链同步
|
# tools-sync:Claude Code / Codex 工具链同步
|
||||||
@@ -11,12 +11,22 @@ description: 在本机统一同步并配置 Claude Code 与 Codex 的 skills、M
|
|||||||
注册表。这个 skill 的职责是让它们保持一致,并且可以在一台新电脑上从零复原整套配置:
|
注册表。这个 skill 的职责是让它们保持一致,并且可以在一台新电脑上从零复原整套配置:
|
||||||
|
|
||||||
- **skills 的唯一真源**是 `http://git.rpchannel.com/bowei/skills-win.git`(remote 名 `origin`)。
|
- **skills 的唯一真源**是 `http://git.rpchannel.com/bowei/skills-win.git`(remote 名 `origin`)。
|
||||||
`~\.claude\skills` 是唯一的 git 检出(主副本);`~\.codex\skills` 下的每个 skill
|
`~\.claude\skills` 是唯一的 git 检出(主副本);其余目录里的每个 skill 都是指向主副本
|
||||||
是指向主副本对应目录的 **junction(目录联接)**——两个工具共享同一份物理文件,
|
对应目录的 **junction(目录联接)**——所有工具共享同一份物理文件,从机制上杜绝漂移,
|
||||||
从机制上杜绝两侧漂移,git 操作只发生在主副本一处。
|
git 操作只发生在主副本一处。
|
||||||
Codex 没有注册命令——它在会话启动时自动扫描 `~\.codex\skills` 下所有含 `SKILL.md`
|
受管的 registry 有两个:`~\.codex\skills`(Codex 自己的目录,主副本在这里做全量镜像)和
|
||||||
的目录(junction 对它透明),所以"注册到 Codex" = "junction 存在且指向的目录含
|
`~\.agents\skills`(Codex / Copilot CLI / Gemini CLI 都会扫的跨工具别名目录)。
|
||||||
SKILL.md",新开的 Codex 会话才会扫到新 skill。
|
**`~\.agents\skills` 只做发现与修复,不做全量镜像**:Codex 两个目录都扫,全量铺进去会让
|
||||||
|
同一个 skill 注册两遍。
|
||||||
|
- **同步是双向的**。Codex 侧新建的 skill(真实目录、含 SKILL.md、主副本里没有同名)会被
|
||||||
|
**自动 ingest**:移进 `~\.claude\skills` 成为主副本,原地换成 junction,随后跟着阶段 1 入库。
|
||||||
|
这是"Codex 里写的 skill 如何进仓库"的唯一正规路径,不需要人工搬运。
|
||||||
|
Codex 没有注册命令——它在会话启动时自动扫描上述目录下所有含 `SKILL.md` 的目录
|
||||||
|
(junction 对它透明),所以"注册到 Codex" = "junction 存在且指向的目录含 SKILL.md",
|
||||||
|
新开的 Codex 会话才会扫到新 skill。
|
||||||
|
- **Codex 插件(`codex plugin list`)自带的 skill 和 MCP 归 marketplace 所有**,不属于可移植
|
||||||
|
用户配置,任何阶段都不要 ingest、不要复制到 Claude 侧。同理 `~\.agents\skills` 里指向
|
||||||
|
`~\.matlab\agentic-toolkits` 的 junction 是 MathWorks 装的,脚本只计数不动它。
|
||||||
- **机器相关的参数不进 git**。每台电脑的软件安装路径、IP、账号等都记录在
|
- **机器相关的参数不进 git**。每台电脑的软件安装路径、IP、账号等都记录在
|
||||||
`~\skills_env.md`(用户主目录下,天然在两个仓库检出之外,不会被 git 同步)。
|
`~\skills_env.md`(用户主目录下,天然在两个仓库检出之外,不会被 git 同步)。
|
||||||
skills 里引用参数时写"见 ~\skills_env.md 的 XXX 条目",不写死路径。
|
skills 里引用参数时写"见 ~\skills_env.md 的 XXX 条目",不写死路径。
|
||||||
@@ -137,23 +147,38 @@ powershell -File <skill目录>\scripts\gitea_doctor.ps1 -Fix # 检查→登
|
|||||||
第三步能直接看出是 gitea 丢了 pack 还是 git 自己没产出——上次就是这一步定的案。
|
第三步能直接看出是 gitea 丢了 pack 还是 git 自己没产出——上次就是这一步定的案。
|
||||||
定位到新根因后,把它补进 `gitea_triage.sh` 的检查表,别只修这一次。
|
定位到新根因后,把它补进 `gitea_triage.sh` 的检查表,别只修这一次。
|
||||||
|
|
||||||
## 阶段 2 — 对齐 ~\.codex\skills(junction 对账)
|
|
||||||
|
|
||||||
同一个脚本接着处理 codex 侧。它对主副本的每个顶层 skill 目录做三件事:
|
## 阶段 2 — registry 对账(ingest + 修复 + 镜像)
|
||||||
|
|
||||||
- codex 侧没有同名条目 → 创建 junction(`New-Item -ItemType Junction`,
|
同一个脚本接着扫 `~\.codex\skills` 和 `~\.agents\skills`,对每个顶层条目分类处理:
|
||||||
Windows 上不需要管理员权限,Codex 对 junction 完全透明)。
|
|
||||||
- 已是 junction → 跳过(内容天然一致,无需任何拷贝)。
|
|
||||||
- 是**真实目录**(挡住了链接)→ 脚本停下来列出这些目录。停的原因:真实目录
|
|
||||||
里可能有 codex 独有的、还没入库的内容。处理原则——有价值的内容先复制进
|
|
||||||
`~\.claude\skills` 走阶段 1 入库,确认无需保留后用 `-Force` 重跑,脚本会
|
|
||||||
删掉真实目录并建 junction。
|
|
||||||
|
|
||||||
脚本还会清理"指向已删除主副本目录"的失效 junction(skill 在仓库里被删除或
|
| 现状 | 脚本动作 |
|
||||||
改名后残留的链接)。codex 自带的系统 skills(`.system\`)永远不动。
|
|------|----------|
|
||||||
|
| junction 指向主副本,目标存在 | 跳过(正确状态) |
|
||||||
|
| junction 指向主副本,目标已消失 | 删除失效链接(skill 在仓库里被删或改名后的残留) |
|
||||||
|
| junction 指向受管 registry 之外(如 `~\.matlab\agentic-toolkits`) | 只计数,不动(宿主自己装的) |
|
||||||
|
| junction 指向**另一个 registry** 里的副本 | 报 MISDIRECTED,解析那份副本后把链接改指主副本 |
|
||||||
|
| 真实目录,含 SKILL.md,主副本**无**同名 | **INGEST**:移进主副本 + 原地建 junction(`-NoIngest` 可关闭) |
|
||||||
|
| 真实目录,与主副本同名且内容**相同** | 直接塌缩成 junction(丢不了东西) |
|
||||||
|
| 真实目录,与主副本同名但内容**不同** | 报 CONFLICT,两边都不动,退出码 2 |
|
||||||
|
| 目录里没有 SKILL.md | 忽略并说明 |
|
||||||
|
|
||||||
校验注册:主副本每个 skill 目录在 codex 侧都应有同名 junction 且透出
|
最后把主副本每个 skill 镜像进 `-MirrorRegistry`(默认 `~\.codex\skills`)。
|
||||||
`SKILL.md`(脚本已内置该检查)。提醒用户:Codex 要开新会话才会扫到新 skills。
|
`.system\` 等点开头目录永远不动。
|
||||||
|
|
||||||
|
判断力要点:
|
||||||
|
|
||||||
|
- **CONFLICT 是唯一需要人裁决的分支。** 内容指纹排除了 `.git`、`__pycache__`、
|
||||||
|
`.langgraph_api`、`*.pckl`、`*.sqlite` 这类运行时产物,所以报出来的分叉是真分叉。
|
||||||
|
处理方式:把想留的内容并进主副本,再带 `-Force` 重跑(`-Force` 一律主副本胜出)。
|
||||||
|
- **ingest 进来的 skill 可能命中 `.gitignore` 的 local-only 段**(仓库里有一段
|
||||||
|
`/orca-cli/` 之类的本机专用规则)。脚本会 `git check-ignore` 并在报告里点名——
|
||||||
|
这种 skill 留在本机、不会推上远端,换电脑时不会出现。要它进仓库就删掉那条规则。
|
||||||
|
- **registry 自带 `.git` 是历史遗留的竞争检出。** 本机 `~\.agents\skills` 就有一个指向
|
||||||
|
github 镜像的检出。脚本只报告不动手;等它的独有内容都 ingest 完,再由用户决定删。
|
||||||
|
|
||||||
|
校验注册:主副本每个 skill 在镜像 registry 都应有同名 junction 且透出 `SKILL.md`
|
||||||
|
(脚本已内置该检查)。提醒用户:Codex 要开新会话才会扫到新 skills。
|
||||||
|
|
||||||
## 阶段 3 — 硬编码审计与 skills_env.md 回填
|
## 阶段 3 — 硬编码审计与 skills_env.md 回填
|
||||||
|
|
||||||
@@ -194,8 +219,31 @@ commit + push 一次(junction 模型下 codex 侧自动同步,无需额外
|
|||||||
|
|
||||||
## 阶段 5 — MCP server 双向发现、注册与校验(Claude Code ↔ Codex)
|
## 阶段 5 — MCP server 双向发现、注册与校验(Claude Code ↔ Codex)
|
||||||
|
|
||||||
|
### Codex 的 MCP 存储模型(codex-cli 0.150.1 实测,官方无文档)
|
||||||
|
|
||||||
|
动手前必须知道这三条,否则会写出"看起来成功、实际无效"的配置:
|
||||||
|
|
||||||
|
- **`codex mcp list --json` / `add` / `remove` 是唯一有效接口。** 审计读它,注册写它。
|
||||||
|
- **`~\.codex\config.toml` 的 `[mcp_servers.*]` 节是惰性的,Codex 不读。** 实测:把
|
||||||
|
`node_repl` 的 `startup_timeout_sec` 从 120 改成 137,`codex mcp list` 仍报 120;
|
||||||
|
手写一整节则完全不出现。config.toml 里现存的那几节是历史遗留。
|
||||||
|
**本 skill 旧版"缺失的补写 TOML 节"是错的,别再那样做——它静默地什么也没干。**
|
||||||
|
- **这个存储不跨 CLI 升级存活。** 2026-08-29 注册好的 7 个用户 MCP,在升级到 0.150.1
|
||||||
|
后全部消失。所以每次同步都要重新审计 Codex 侧,`~\skills_env.md` 的 MCP 配置节
|
||||||
|
才是可重建的真源。
|
||||||
|
|
||||||
|
由此还有一个已知缺口:`codex mcp add` 没有 `--tool-timeout-sec` / `env_vars` 之类的参数,
|
||||||
|
config.toml 又不生效,所以这两个键**目前无任何设置入口**。需要它们的 MCP 记在
|
||||||
|
`references/codex_mcp_wanted_keys.json`,脚本注册完会单独报"registered, but incomplete",
|
||||||
|
不要把它当失败,也不要假装已配好。每次 codex-cli 升级后复查是否已有新接口。
|
||||||
|
|
||||||
|
另:这两个脚本都要用 PowerShell 跑,不要用 git-bash——MSYS 会改写 `/c` 形式的参数和
|
||||||
|
Windows 路径(实测 `codex mcp add ... /c echo hi` 的 `/c` 被改成了 `C:/`)。
|
||||||
|
|
||||||
|
### 双向审计
|
||||||
|
|
||||||
先运行双向审计脚本。它读取 Claude Code 用户级 `~\.claude.json` 的 `mcpServers` 和
|
先运行双向审计脚本。它读取 Claude Code 用户级 `~\.claude.json` 的 `mcpServers` 和
|
||||||
Codex 用户级 `~\.codex\config.toml` 的 `[mcp_servers.*]`,对用户可配置 MCP 做并集:
|
`codex mcp list --json`,对用户可配置 MCP 做并集:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# 只审计;发现差集或漂移时退出码为 2,不修改配置
|
# 只审计;发现差集或漂移时退出码为 2,不修改配置
|
||||||
@@ -231,17 +279,17 @@ powershell -File <skill目录>\scripts\sync_mcp_registrations.ps1 -Apply -Confli
|
|||||||
1. **Claude Code 侧**:`claude mcp list` 或读 `~\.claude.json` 的 `mcpServers`,
|
1. **Claude Code 侧**:`claude mcp list` 或读 `~\.claude.json` 的 `mcpServers`,
|
||||||
确认存在对应条目。缺失的用 `claude mcp add <name> ...` 注册,命令、解释器
|
确认存在对应条目。缺失的用 `claude mcp add <name> ...` 注册,命令、解释器
|
||||||
路径和 env 变量都取自 `~\skills_env.md` 的 MCP 配置节。
|
路径和 env 变量都取自 `~\skills_env.md` 的 MCP 配置节。
|
||||||
2. **Codex 侧**:读 `~\.codex\config.toml` 的 `[mcp_servers.<name>]` 节。
|
2. **Codex 侧**:`codex mcp list --json` 确认存在对应条目。缺失的用
|
||||||
缺失的按 skills_env.md 里的同一份配置补写 TOML 节(stdio 类型,
|
`codex mcp add <name> --env K=V -- <command> <args...>` 注册,配置取自 skills_env.md
|
||||||
记得带 `PYTHONIOENCODING=utf-8`、`PYTHONUTF8=1`,Codex 在 Windows 上
|
的同一份表(stdio 类型记得带 `PYTHONIOENCODING=utf-8`、`PYTHONUTF8=1`,Codex 在
|
||||||
没有这两个变量时中文输出会乱码)。
|
Windows 上没有这两个变量时中文输出会乱码)。
|
||||||
3. **可启动性抽查**:对新注册的 MCP,直接运行其 command + args 数秒,
|
3. **可启动性抽查**:对新注册的 MCP,直接运行其 command + args 数秒,
|
||||||
确认进程能起来、没有立刻报 import/路径错误(stdio MCP 起来后会静默等
|
确认进程能起来、没有立刻报 import/路径错误(stdio MCP 起来后会静默等
|
||||||
输入,这就算通过,Ctrl-C 掉即可)。
|
输入,这就算通过,Ctrl-C 掉即可)。
|
||||||
|
|
||||||
注意两侧配置语义相同但格式不同(JSON vs TOML)。先由双向脚本发现差集和漂移,再用
|
先由双向脚本发现差集和漂移,再用 `mcp_list.md` 查漏,最后以 skills_env.md 和实际可启动
|
||||||
`mcp_list.md` 查漏,最后以 skills_env.md 和实际可启动结果判定正确值。不要因为某项当前
|
结果判定正确值。不要因为某项当前只在一侧出现,就假设另一侧没有安装需求——尤其 Codex 侧
|
||||||
只在一侧出现,就假设另一侧没有安装需求。
|
可能只是被上一次 CLI 升级清空了。
|
||||||
|
|
||||||
### MATLAB / Simulink Agentic Toolkit 专项检查与安装
|
### MATLAB / Simulink Agentic Toolkit 专项检查与安装
|
||||||
|
|
||||||
@@ -256,13 +304,17 @@ powershell -File <skill目录>\scripts\sync_mcp_registrations.ps1 -Apply -Confli
|
|||||||
且已安装 Simulink。实际 MATLAB 根目录记录到 `~\skills_env.md` 的 `MATLAB_ROOT`。
|
且已安装 Simulink。实际 MATLAB 根目录记录到 `~\skills_env.md` 的 `MATLAB_ROOT`。
|
||||||
2. `~\.matlab\agentic-toolkits\bin\matlab-mcp-server.exe` 存在且 `--version` 可运行;
|
2. `~\.matlab\agentic-toolkits\bin\matlab-mcp-server.exe` 存在且 `--version` 可运行;
|
||||||
`config.json` 的 `toolkits` 同时包含 `matlab` 与 `simulink`。
|
`config.json` 的 `toolkits` 同时包含 `matlab` 与 `simulink`。
|
||||||
3. Claude `~\.claude.json` 与 Codex `~\.codex\config.toml` 均有用户级 `matlab` MCP,
|
3. Claude `~\.claude.json` 与 `codex mcp list --json` 均有用户级 `matlab` MCP,
|
||||||
command 指向同一二进制,args 同时包含 `--matlab-session-mode=existing` 和正确的
|
command 指向同一二进制,args 同时包含 `--matlab-session-mode=existing` 和正确的
|
||||||
`--extension-file`。Claude 条目应有 `alwaysLoad=true`。
|
`--extension-file`。Claude 条目应有 `alwaysLoad=true`。
|
||||||
4. Codex 的 `[mcp_servers.matlab]` 还必须有 `tool_timeout_sec = 600` 和 Windows 所需的
|
4. Codex 侧本应还有 `tool_timeout_sec = 600` 和 Windows 所需的 `env_vars = ["WINDIR"]`
|
||||||
`env_vars = ["WINDIR"]`;缺少 `WINDIR` 时 Simulink 子进程可能无法正常工作。
|
(缺 `WINDIR` 时 Simulink 子进程可能起不来),但 0.150.1 **没有任何接口能设置它们**
|
||||||
5. `~\.agents\skills` 中存在指向两套 Toolkit 的 junction;Claude Code 的 MATLAB /
|
(见本阶段开头的存储模型)。所以这一条是**已知缺口,不作为安装完整性的判定项**:
|
||||||
Simulink plugin 或等效 skill 注册存在。
|
脚本报出 "registered, but incomplete" 即可,写进报告的遗留事项,不要标记为失败,
|
||||||
|
也不要试图靠改 config.toml 绕过——那不生效。
|
||||||
|
5. `~\.agents\skills` 中存在指向两套 Toolkit 的 junction(阶段 2 会把这类指向
|
||||||
|
`~\.matlab\agentic-toolkits` 的 junction 识别为宿主自有,只计数不动);Claude Code 的
|
||||||
|
MATLAB / Simulink plugin 或等效 skill 注册存在。
|
||||||
|
|
||||||
缺失时使用 MathWorks 官方统一安装器,不使用旧版 agent-driven 安装法:
|
缺失时使用 MathWorks 官方统一安装器,不使用旧版 agent-driven 安装法:
|
||||||
|
|
||||||
@@ -290,8 +342,9 @@ setupAgenticToolkit("configure", Agents=["claude-code","codex"], ...
|
|||||||
Setup” 与 “MATLAB MCP Server Toolbox” 两个官方 Toolbox 根目录加入 path 并
|
Setup” 与 “MATLAB MCP Server Toolbox” 两个官方 Toolbox 根目录加入 path 并
|
||||||
`savepath`,再用全新 MATLAB 进程通过 `which setupAgenticToolkit`、
|
`savepath`,再用全新 MATLAB 进程通过 `which setupAgenticToolkit`、
|
||||||
`which shareMATLABSession` 验证。
|
`which shareMATLABSession` 验证。
|
||||||
- 安装后重新运行双向审计脚本;`matlab` 必须显示为 Matched。再直接解析两边配置,确认
|
- 安装后重新运行双向审计脚本;`matlab` 必须显示为 Matched。再直接比对两边的 command/args,
|
||||||
Simulink extension、Codex timeout 与 `WINDIR` 未被规范化比较遗漏。
|
确认 Simulink extension 未被规范化比较遗漏(Codex timeout 与 `WINDIR` 见上面第 4 条,
|
||||||
|
当前无法设置,不必核对)。
|
||||||
- 使用 Simulink 工具前,每个 MATLAB 会话运行
|
- 使用 Simulink 工具前,每个 MATLAB 会话运行
|
||||||
`addpath("~/.matlab/agentic-toolkits/simulink"); satk_initialize`,然后重启 Claude Code /
|
`addpath("~/.matlab/agentic-toolkits/simulink"); satk_initialize`,然后重启 Claude Code /
|
||||||
Codex 会话。MCP 健康检查在没有共享 MATLAB 会话时可能显示连接失败,这不等于未安装。
|
Codex 会话。MCP 健康检查在没有共享 MATLAB 会话时可能显示连接失败,这不等于未安装。
|
||||||
@@ -306,15 +359,17 @@ setupAgenticToolkit("configure", Agents=["claude-code","codex"], ...
|
|||||||
- ~\.claude\skills: <pull/push 结果,commit 摘要>
|
- ~\.claude\skills: <pull/push 结果,commit 摘要>
|
||||||
- 远端校验: origin/main = <sha>,与本地一致 / **不一致:N 个 commit 滞留本机**
|
- 远端校验: origin/main = <sha>,与本地一致 / **不一致:N 个 commit 滞留本机**
|
||||||
- 服务端: 传输两面均正常 / 医生修复了 <哪几项> / **仍不可用:<RESULT 行>**
|
- 服务端: 传输两面均正常 / 医生修复了 <哪几项> / **仍不可用:<RESULT 行>**
|
||||||
- ~\.codex\skills: <pull 结果,注册差集>
|
- 从 registry ingest: <被自动收进主副本的 codex/agents 侧 skill;没有则写"无">
|
||||||
|
- junction 对账: <修复的 MISDIRECTED、塌缩的重复、清理的失效链接、镜像新建数>
|
||||||
## 硬编码审计
|
## 硬编码审计
|
||||||
- 本次修复: <清单>
|
- 本次修复: <清单>
|
||||||
- 遗留待改: <清单>
|
- 遗留待改: <清单>
|
||||||
## 环境
|
## 环境
|
||||||
- langgraph env: OK / 已重装 / 失败原因
|
- langgraph env: OK / 已重装 / 失败原因
|
||||||
- MCP 注册: <Claude 独有、Codex 独有、同名漂移、已补齐、排除项、清单查漏、MATLAB/Simulink 专项检查、抽查结果>
|
- MCP 注册: <Claude 独有、Codex 独有、同名漂移、已补齐、排除项、清单查漏、MATLAB/Simulink 专项检查、抽查结果>
|
||||||
|
- MCP 已注册但不完整: <codex-cli 无接口可设的键;没有则写"无">
|
||||||
## 需要用户决定的事项
|
## 需要用户决定的事项
|
||||||
- <冲突、密码泄露、codex 独有 skill 去留……没有则写"无">
|
- <CONFLICT 分叉、密码泄露、local-only 的 ingest 项、registry 里的竞争检出……没有则写"无">
|
||||||
```
|
```
|
||||||
|
|
||||||
## 边界与安全
|
## 边界与安全
|
||||||
@@ -324,6 +379,9 @@ setupAgenticToolkit("configure", Agents=["claude-code","codex"], ...
|
|||||||
- `~\skills_env.md` 含凭据,任何情况下不将其内容或凭据字段写入仓库内文件、
|
- `~\skills_env.md` 含凭据,任何情况下不将其内容或凭据字段写入仓库内文件、
|
||||||
commit message 或对外输出。
|
commit message 或对外输出。
|
||||||
- 不动 `~\.codex\skills\.system\`(Codex 自带系统 skills)和
|
- 不动 `~\.codex\skills\.system\`(Codex 自带系统 skills)和
|
||||||
`~\.claude\skills\.system\`。
|
`~\.claude\skills\.system\`;也不动 `codex plugin` 装的 marketplace 插件。
|
||||||
- 修改 `~\.codex\config.toml` 前先备份为 `config.toml.bak-<日期>`,TOML 写坏
|
- 修改 `~\.codex\config.toml` 前先备份为 `config.toml.bak-<日期>`,TOML 写坏
|
||||||
会导致 Codex 起不来。
|
会导致 Codex 起不来。
|
||||||
|
- **ingest 会 `Move-Item`,`-Force` 会 `Remove-Item -Recurse`。** 先 `-DryRun` 看清单,
|
||||||
|
确认没有误判再实跑;CONFLICT 项在用户裁决前脚本不会碰。
|
||||||
|
- 两个脚本都从 PowerShell 运行,不要用 git-bash(MSYS 会改写参数里的 `/c` 和 Windows 路径)。
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Codex-side [mcp_servers.<name>] keys certain MCPs need that codex-cli 0.150.1 offers NO way to set: `codex mcp add` has no flag for them, and config.toml's [mcp_servers.*] tables are inert (measured -- editing one there does not change what `codex mcp list` reports). sync_mcp_registrations.ps1 reports these after registering so the gap stays visible instead of looking complete. Re-check on each codex-cli upgrade; delete an entry once a real interface exists and the key is set.",
|
||||||
|
|
||||||
|
"matlab": {
|
||||||
|
"tool_timeout_sec": 600,
|
||||||
|
"env_vars": ["WINDIR"],
|
||||||
|
"_why": "Simulink model_edit/model_test round-trips exceed the default tool timeout; the Simulink subprocess needs WINDIR inherited from the parent environment on Windows."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,9 @@
|
|||||||
SKILLS_REPO_URL = http://git.rpchannel.com/bowei/skills-win.git
|
SKILLS_REPO_URL = http://git.rpchannel.com/bowei/skills-win.git
|
||||||
SKILLS_REPO_USER = bowei
|
SKILLS_REPO_USER = bowei
|
||||||
SKILLS_REPO_PASS = <填写,或留空表示已存入 Windows 凭据管理器>
|
SKILLS_REPO_PASS = <填写,或留空表示已存入 Windows 凭据管理器>
|
||||||
CLAUDE_SKILLS_DIR = C:\Users\<user>\.claude\skills
|
CLAUDE_SKILLS_DIR = C:\Users\<user>\.claude\skills # 唯一 git 检出(主副本)
|
||||||
CODEX_SKILLS_DIR = C:\Users\<user>\.codex\skills
|
CODEX_SKILLS_DIR = C:\Users\<user>\.codex\skills # 全量镜像,全是 junction
|
||||||
|
AGENTS_SKILLS_DIR = C:\Users\<user>\.agents\skills # 跨工具别名目录;只做发现/修复,不全量铺
|
||||||
```
|
```
|
||||||
|
|
||||||
凭据说明:首次 clone 成功后运行 `git config --global credential.helper manager`,
|
凭据说明:首次 clone 成功后运行 `git config --global credential.helper manager`,
|
||||||
@@ -93,11 +94,13 @@ CROSS_VM_PASS = <填写>
|
|||||||
MCP_DESIGN_ROOT = E:\MCP_design
|
MCP_DESIGN_ROOT = E:\MCP_design
|
||||||
```
|
```
|
||||||
|
|
||||||
每个 MCP 在 Claude Code(`~\.claude.json` 的 `mcpServers`)和 Codex
|
每个 MCP 在 Claude Code(`~\.claude.json` 的 `mcpServers`)和 Codex(`codex mcp add`,
|
||||||
(`~\.codex\config.toml` 的 `[mcp_servers.*]`)中应使用**同一份** command/args/env。
|
读用 `codex mcp list --json`)中应使用**同一份** command/args/env。注意 Codex 侧
|
||||||
同步时还要对两侧用户级注册表取并集,确保任一侧新增的可移植 MCP 都同步到另一侧;
|
`~\.codex\config.toml` 的 `[mcp_servers.*]` 节**不生效**,手写无用;且它的注册存储
|
||||||
|
不跨 codex-cli 升级存活,所以下表是 Codex 侧唯一可重建的真源,每次同步都要核对。
|
||||||
|
同步时对两侧用户级注册表取并集,确保任一侧新增的可移植 MCP 都同步到另一侧;
|
||||||
`mcp_list.md` 继续用于发现“两侧都漏装”的本地工程。托管连接器、插件动态 MCP、
|
`mcp_list.md` 继续用于发现“两侧都漏装”的本地工程。托管连接器、插件动态 MCP、
|
||||||
Codex 内置运行时 MCP 不进入这个并集。
|
Codex 内置运行时 MCP(node_repl / cua_repl)不进入这个并集。
|
||||||
标准配置如下(路径变量引用上文条目):
|
标准配置如下(路径变量引用上文条目):
|
||||||
|
|
||||||
| name | command | args | env |
|
| name | command | args | env |
|
||||||
@@ -110,8 +113,10 @@ Codex 内置运行时 MCP 不进入这个并集。
|
|||||||
| matlab | {MATLAB_MCP_EXE} | --matlab-session-mode=existing --extension-file=...\simulink\tools\tools.json | |
|
| matlab | {MATLAB_MCP_EXE} | --matlab-session-mode=existing --extension-file=...\simulink\tools\tools.json | |
|
||||||
|
|
||||||
Codex 侧所有 stdio MCP 额外加 `PYTHONIOENCODING=utf-8`、`PYTHONUTF8=1`。
|
Codex 侧所有 stdio MCP 额外加 `PYTHONIOENCODING=utf-8`、`PYTHONUTF8=1`。
|
||||||
MathWorks `matlab` MCP 是特例:Codex 还要设置 `tool_timeout_sec=600` 与
|
MathWorks `matlab` MCP 是特例:Claude 侧要设 `alwaysLoad=true`;Codex 侧本应有
|
||||||
`env_vars=["WINDIR"]`;Claude 侧要设置 `alwaysLoad=true`。MATLAB 与 Simulink 共用该条目。
|
`tool_timeout_sec=600` 与 `env_vars=["WINDIR"]`,但 codex-cli 0.150.1 无接口可设,
|
||||||
|
属已知缺口(记录在 tools-sync 的 `references/codex_mcp_wanted_keys.json`)。
|
||||||
|
MATLAB 与 Simulink 共用该条目。
|
||||||
|
|
||||||
## 本机状态备注(同步时更新)
|
## 本机状态备注(同步时更新)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
# tools-sync: audit and reconcile user-configured MCP registrations between
|
# tools-sync: audit and reconcile user-configured MCP registrations between
|
||||||
# Claude Code and Codex. Managed connectors, plugins, and Codex runtime MCPs are
|
# Claude Code and Codex. Managed connectors, plugins, and Codex runtime MCPs are
|
||||||
# intentionally excluded because they are owned by their host application.
|
# intentionally excluded because they are owned by their host application.
|
||||||
|
#
|
||||||
|
# Codex storage model (measured on codex-cli 0.150.1, not documented anywhere):
|
||||||
|
# * `codex mcp list --json` / `add` / `remove` are the ONLY working interface.
|
||||||
|
# * config.toml's [mcp_servers.*] tables are INERT. Editing node_repl's startup_timeout_sec
|
||||||
|
# there from 120 to 137 left `codex mcp list` reporting 120, and a section written by hand
|
||||||
|
# never appeared at all. Hand-writing TOML sections -- what this skill used to instruct --
|
||||||
|
# silently does nothing. The sections still in config.toml are legacy leftovers.
|
||||||
|
# * `codex mcp add` persists outside config.toml and has no flag for tool_timeout_sec or
|
||||||
|
# env_vars, so those keys currently have no setting entry point at all.
|
||||||
|
# * That store is not durable across CLI upgrades: seven user MCPs registered on 2026-08-29
|
||||||
|
# were gone after the upgrade to 0.150.1. Re-audit Codex MCPs on every sync; ~/skills_env.md
|
||||||
|
# is the durable record to re-register from.
|
||||||
|
#
|
||||||
|
# Run from PowerShell, NOT git-bash: MSYS rewrites `/c`-style args and Windows paths.
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
@@ -10,12 +24,21 @@ param(
|
|||||||
[ValidateSet("None", "Claude", "Codex")]
|
[ValidateSet("None", "Claude", "Codex")]
|
||||||
[string]$ConflictAuthority = "None",
|
[string]$ConflictAuthority = "None",
|
||||||
[string[]]$ExcludeName = @(),
|
[string[]]$ExcludeName = @(),
|
||||||
[string]$PythonCommand = "python"
|
[string]$PythonCommand = "python",
|
||||||
|
# Where to read Codex's registrations. Auto = `codex mcp list --json`, falling back to
|
||||||
|
# parsing -CodexConfig when the CLI is unavailable (older codex, or a test fixture).
|
||||||
|
# Toml forces the fixture path so tests can isolate from the real machine.
|
||||||
|
[ValidateSet("Auto", "Cli", "Toml")]
|
||||||
|
[string]$CodexSource = "Auto",
|
||||||
|
# Codex-side keys some MCPs need that codex-cli offers no way to set (see the note above).
|
||||||
|
# Resolved after the param block: $PSScriptRoot is not yet bound in default expressions here.
|
||||||
|
[string]$OverridesFile
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$script:CodexBackupMade = $false
|
$script:CodexBackupMade = $false
|
||||||
$script:Failures = @()
|
$script:Failures = @()
|
||||||
|
$script:Unsupported = @()
|
||||||
$CodexOnlyEnv = @("PYTHONIOENCODING", "PYTHONUTF8")
|
$CodexOnlyEnv = @("PYTHONIOENCODING", "PYTHONUTF8")
|
||||||
|
|
||||||
function Test-TomllibPython([string]$Candidate) {
|
function Test-TomllibPython([string]$Candidate) {
|
||||||
@@ -53,6 +76,19 @@ function Resolve-PythonCommand([string]$Requested, [bool]$Explicit) {
|
|||||||
|
|
||||||
$PythonCommand = Resolve-PythonCommand $PythonCommand $PSBoundParameters.ContainsKey('PythonCommand')
|
$PythonCommand = Resolve-PythonCommand $PythonCommand $PSBoundParameters.ContainsKey('PythonCommand')
|
||||||
|
|
||||||
|
if (-not $OverridesFile) {
|
||||||
|
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$OverridesFile = Join-Path (Split-Path -Parent $here) "references\codex_mcp_wanted_keys.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:Overrides = @{}
|
||||||
|
if (Test-Path -LiteralPath $OverridesFile) {
|
||||||
|
$raw = Get-Content -Raw -LiteralPath $OverridesFile | ConvertFrom-Json
|
||||||
|
foreach ($property in $raw.psobject.Properties) {
|
||||||
|
if ($property.Name -notlike "_*") { $script:Overrides[$property.Name] = $property.Value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ConvertTo-StringMap($Object) {
|
function ConvertTo-StringMap($Object) {
|
||||||
$result = [ordered]@{}
|
$result = [ordered]@{}
|
||||||
if ($null -eq $Object) { return $result }
|
if ($null -eq $Object) { return $result }
|
||||||
@@ -105,7 +141,35 @@ function Get-ClaudeMcps {
|
|||||||
return $result
|
return $result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-CodexMcpsFromCli {
|
||||||
|
# Codex >= 26.8 no longer persists user MCP registrations in config.toml;
|
||||||
|
# `codex mcp list --json` is the authoritative view. Returns $null when the
|
||||||
|
# CLI is unavailable so the caller can fall back to TOML parsing.
|
||||||
|
$codexExe = Get-Command codex -ErrorAction SilentlyContinue
|
||||||
|
if (-not $codexExe) { return $null }
|
||||||
|
$raw = @(& $codexExe.Source mcp list --json 2>$null)
|
||||||
|
if ($LASTEXITCODE -ne 0 -or -not $raw) { return $null }
|
||||||
|
try { $servers = ($raw -join "`n") | ConvertFrom-Json } catch { return $null }
|
||||||
|
if ($null -eq $servers) { return $null }
|
||||||
|
$result = @{}
|
||||||
|
foreach ($s in @($servers)) {
|
||||||
|
$t = $s.transport
|
||||||
|
$transport = if ($t.type) { [string]$t.type } elseif ($t.url) { "http" } else { "stdio" }
|
||||||
|
if ($transport -eq "streamable_http") { $transport = "http" }
|
||||||
|
$result[[string]$s.name] = New-CanonicalMcp ([string]$s.name) $transport ([string]$t.command) `
|
||||||
|
@($t.args) (ConvertTo-StringMap $t.env) ([string]$t.url) `
|
||||||
|
(ConvertTo-StringMap $t.http_headers) ([string]$t.bearer_token_env_var) "Codex"
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
function Get-CodexMcps {
|
function Get-CodexMcps {
|
||||||
|
if ($CodexSource -ne "Toml") {
|
||||||
|
$fromCli = Get-CodexMcpsFromCli
|
||||||
|
if ($null -ne $fromCli) { return $fromCli }
|
||||||
|
if ($CodexSource -eq "Cli") { throw "codex mcp list --json unavailable and -CodexSource Cli forbids the TOML fallback" }
|
||||||
|
}
|
||||||
|
|
||||||
if (-not (Test-Path -LiteralPath $CodexConfig)) {
|
if (-not (Test-Path -LiteralPath $CodexConfig)) {
|
||||||
throw "Codex config not found: $CodexConfig"
|
throw "Codex config not found: $CodexConfig"
|
||||||
}
|
}
|
||||||
@@ -209,6 +273,7 @@ function Add-ToCodex($Mcp) {
|
|||||||
$arguments = @("mcp", "add", $Mcp.name)
|
$arguments = @("mcp", "add", $Mcp.name)
|
||||||
$envMap = [ordered]@{}
|
$envMap = [ordered]@{}
|
||||||
foreach ($key in $Mcp.env.Keys) { $envMap[$key] = $Mcp.env[$key] }
|
foreach ($key in $Mcp.env.Keys) { $envMap[$key] = $Mcp.env[$key] }
|
||||||
|
# Without these, Codex mangles non-ASCII stdio output on Windows.
|
||||||
$envMap["PYTHONIOENCODING"] = "utf-8"
|
$envMap["PYTHONIOENCODING"] = "utf-8"
|
||||||
$envMap["PYTHONUTF8"] = "1"
|
$envMap["PYTHONUTF8"] = "1"
|
||||||
foreach ($key in $envMap.Keys) { $arguments += @("--env", "$key=$($envMap[$key])") }
|
foreach ($key in $envMap.Keys) { $arguments += @("--env", "$key=$($envMap[$key])") }
|
||||||
@@ -216,6 +281,7 @@ function Add-ToCodex($Mcp) {
|
|||||||
$arguments += $Mcp.command
|
$arguments += $Mcp.command
|
||||||
$arguments += @($Mcp.args)
|
$arguments += @($Mcp.args)
|
||||||
Invoke-External "codex add $($Mcp.name)" "codex" $arguments
|
Invoke-External "codex add $($Mcp.name)" "codex" $arguments
|
||||||
|
Report-UnsupportedKeys $Mcp.name
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ($Mcp.headers.Count -gt 0) {
|
if ($Mcp.headers.Count -gt 0) {
|
||||||
@@ -223,6 +289,17 @@ function Add-ToCodex($Mcp) {
|
|||||||
}
|
}
|
||||||
Backup-CodexConfig
|
Backup-CodexConfig
|
||||||
Invoke-External "codex add $($Mcp.name)" "codex" @("mcp", "add", $Mcp.name, "--url", $Mcp.url)
|
Invoke-External "codex add $($Mcp.name)" "codex" @("mcp", "add", $Mcp.name, "--url", $Mcp.url)
|
||||||
|
Report-UnsupportedKeys $Mcp.name
|
||||||
|
}
|
||||||
|
|
||||||
|
# Some MCPs need Codex keys that `codex mcp add` has no flag for and that config.toml
|
||||||
|
# cannot supply either (see the storage-model note at the top). Surface them rather than
|
||||||
|
# pretending the registration is complete.
|
||||||
|
function Report-UnsupportedKeys([string]$Name) {
|
||||||
|
if (-not $script:Overrides.ContainsKey($Name)) { return }
|
||||||
|
$keys = @($script:Overrides[$Name].psobject.Properties | Where-Object { $_.Name -notlike "_*" } |
|
||||||
|
ForEach-Object { "$($_.Name)=$($_.Value)" })
|
||||||
|
if ($keys) { $script:Unsupported += "$Name wants $($keys -join ', ') - no interface in codex-cli to set these" }
|
||||||
}
|
}
|
||||||
|
|
||||||
function Add-ToClaude($Mcp) {
|
function Add-ToClaude($Mcp) {
|
||||||
@@ -319,6 +396,20 @@ if ($Apply) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($Apply) {
|
||||||
|
# Codex re-reads config.toml on every invocation, so this confirms what it will actually load.
|
||||||
|
$after = Get-PortableMcps (Get-CodexMcps) "Codex"
|
||||||
|
$stillMissing = @($claude.Keys | Where-Object { -not $after.ContainsKey($_) } | Sort-Object)
|
||||||
|
Write-Host "=== post-apply verification (codex mcp list) ==="
|
||||||
|
Write-Host "Codex now serves: $(@($after.Keys | Sort-Object) -join ', ')"
|
||||||
|
if ($stillMissing) { $script:Failures += "still absent from Codex after apply: $($stillMissing -join ', ')" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($script:Unsupported.Count -gt 0) {
|
||||||
|
Write-Host "=== registered, but incomplete (codex-cli cannot express these keys) ==="
|
||||||
|
$script:Unsupported | ForEach-Object { Write-Host "- $_" }
|
||||||
|
}
|
||||||
|
|
||||||
if ($script:Failures.Count -gt 0) {
|
if ($script:Failures.Count -gt 0) {
|
||||||
Write-Host "=== failures ==="
|
Write-Host "=== failures ==="
|
||||||
$script:Failures | ForEach-Object { Write-Host "- $_" }
|
$script:Failures | ForEach-Object { Write-Host "- $_" }
|
||||||
|
|||||||
@@ -1,29 +1,51 @@
|
|||||||
# tools-sync: mechanical sync of skills between Claude Code and Codex.
|
# tools-sync: mechanical sync of skills across Claude Code, Codex, and the
|
||||||
# Phase 1 -- git sync of the primary checkout (~\.claude\skills): commit -> pull --rebase -> push.
|
# cross-runtime ~\.agents\skills directory.
|
||||||
# Phase 2 -- junction reconciliation: every top-level skill dir in the primary gets a
|
#
|
||||||
# directory junction of the same name in ~\.codex\skills, so both tools share
|
# Phase 1 — git sync of the primary checkout (~\.claude\skills): commit -> pull --rebase -> push.
|
||||||
# one physical copy and can never drift. Codex auto-registers any dir there
|
# Phase 2 — registry reconciliation. Every skill has exactly ONE physical home (the primary
|
||||||
# containing SKILL.md (new sessions only).
|
# checkout); every other registry holds a directory junction pointing at it, so the
|
||||||
# Judgment calls (rebase conflicts, codex-local dirs worth keeping) are left to the caller.
|
# tools cannot drift. This phase is bidirectional:
|
||||||
|
# * a real skill dir that exists ONLY in a registry is INGESTED into the primary
|
||||||
|
# (this is how a skill created from the Codex side enters the repo), then linked back;
|
||||||
|
# * a link that points somewhere other than the primary is repaired;
|
||||||
|
# * a real dir duplicating a primary skill collapses to a junction when the content
|
||||||
|
# is identical, and is reported as a conflict when it differs.
|
||||||
|
# Judgment calls (rebase conflicts, divergent duplicates) are left to the caller.
|
||||||
|
#
|
||||||
|
# Exit codes: 0 = clean, 1 = hard failure, 2 = conflicts需要人工裁决.
|
||||||
|
#
|
||||||
|
# Run from PowerShell, NOT git-bash: MSYS rewrites `/c`-style arguments and Windows paths.
|
||||||
|
#
|
||||||
|
# A server-side transport fault (bad pack header / early EOF / RPC failed) is not fatal:
|
||||||
|
# gitea_doctor.ps1 is invoked to repair the gitea host and the push is retried once (-NoDoctor opts out).
|
||||||
|
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)][string]$RepoUrl,
|
[Parameter(Mandatory = $true)][string]$RepoUrl,
|
||||||
[string]$ClaudeSkills = "$env:USERPROFILE\.claude\skills",
|
[string]$ClaudeSkills = "$env:USERPROFILE\.claude\skills",
|
||||||
[string]$CodexSkills = "$env:USERPROFILE\.codex\skills",
|
# Registries mirroring the primary. ~\.codex\skills is Codex's own directory;
|
||||||
|
# ~\.agents\skills is the cross-runtime alias Codex / Copilot CLI / Gemini CLI also scan.
|
||||||
|
[string[]]$Registry = @("$env:USERPROFILE\.codex\skills", "$env:USERPROFILE\.agents\skills"),
|
||||||
|
# Registry that gets a junction for EVERY primary skill. The others are only scanned for
|
||||||
|
# ingest/repair, because linking a skill into two directories Codex scans registers it twice.
|
||||||
|
[string]$MirrorRegistry = "$env:USERPROFILE\.codex\skills",
|
||||||
[string]$Branch = "main",
|
[string]$Branch = "main",
|
||||||
# A real (non-junction) dir in the codex side that shadows a primary skill blocks
|
# Resolve divergent duplicates by declaring the primary authoritative (deletes the registry copy).
|
||||||
# linking; -Force deletes it and links (caller must have rescued unique content first).
|
|
||||||
[switch]$Force,
|
[switch]$Force,
|
||||||
# Report what would happen without committing/pushing/linking.
|
|
||||||
[switch]$DryRun,
|
|
||||||
# A transport failure normally aborts the run and leaves commits stranded here. By
|
# A transport failure normally aborts the run and leaves commits stranded here. By
|
||||||
# default we instead log into the gitea host, repair it, and retry once. -NoDoctor
|
# default we instead log into the gitea host, repair it, and retry once. -NoDoctor
|
||||||
# keeps the old abort-and-report behaviour.
|
# keeps the old abort-and-report behaviour.
|
||||||
[switch]$NoDoctor,
|
[switch]$NoDoctor,
|
||||||
[switch]$AllowDestructive
|
[switch]$AllowDestructive,
|
||||||
|
# Leave registry-only skills where they are instead of moving them into the primary checkout.
|
||||||
|
[switch]$NoIngest,
|
||||||
|
# Report what would happen without committing/pushing/linking/moving.
|
||||||
|
[switch]$DryRun
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
$script:Conflicts = @()
|
||||||
|
$script:Ingested = @()
|
||||||
|
$script:Notes = @()
|
||||||
|
|
||||||
function Invoke-Git {
|
function Invoke-Git {
|
||||||
param([string]$Dir, [string[]]$GitArgs)
|
param([string]$Dir, [string[]]$GitArgs)
|
||||||
@@ -68,8 +90,41 @@ function Invoke-Doctor([string]$Why) {
|
|||||||
return $ok
|
return $ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-LinkTarget($Item) {
|
||||||
|
if (-not $Item.Target) { return $null }
|
||||||
|
return (@($Item.Target) | Select-Object -First 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-IsLink($Item) {
|
||||||
|
return [bool]($Item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-UnderPath([string]$Path, [string]$Root) {
|
||||||
|
if (-not $Path -or -not $Root) { return $false }
|
||||||
|
return $Path.TrimEnd('\').ToLowerInvariant().StartsWith($Root.TrimEnd('\').ToLowerInvariant() + '\')
|
||||||
|
}
|
||||||
|
|
||||||
|
# Content fingerprint used to decide whether a duplicate is a harmless copy or a real fork.
|
||||||
|
# Runtime junk that legitimately differs between machines is excluded, mirroring .gitignore.
|
||||||
|
function Get-DirFingerprint([string]$Path) {
|
||||||
|
$skip = '(\\\.git\\|\\__pycache__\\|\\\.langgraph_api\\|\.pyc$|\.pckl$|\.sqlite(-shm|-wal)?$)'
|
||||||
|
$files = Get-ChildItem -LiteralPath $Path -Recurse -File -Force -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.FullName -notmatch $skip }
|
||||||
|
$lines = foreach ($f in $files) {
|
||||||
|
$rel = $f.FullName.Substring($Path.Length).TrimStart('\').ToLowerInvariant()
|
||||||
|
"$rel $((Get-FileHash -LiteralPath $f.FullName -Algorithm SHA256).Hash)"
|
||||||
|
}
|
||||||
|
return (($lines | Sort-Object) -join "`n")
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-SkillJunction([string]$LinkPath, [string]$TargetPath, [string]$Why) {
|
||||||
|
if ($DryRun) { Write-Host " (dry-run) would link $LinkPath -> $TargetPath [$Why]"; return }
|
||||||
|
New-Item -ItemType Junction -Path $LinkPath -Target $TargetPath | Out-Null
|
||||||
|
Write-Host " linked: $LinkPath -> $TargetPath [$Why]"
|
||||||
|
}
|
||||||
|
|
||||||
# ---------- Phase 1: primary checkout (~\.claude\skills) ----------
|
# ---------- Phase 1: primary checkout (~\.claude\skills) ----------
|
||||||
Write-Host "=== [1/2] primary git sync: $ClaudeSkills ==="
|
Write-Host "=== [1/3] primary git sync: $ClaudeSkills ==="
|
||||||
if (-not (Test-Path (Join-Path $ClaudeSkills ".git"))) {
|
if (-not (Test-Path (Join-Path $ClaudeSkills ".git"))) {
|
||||||
Fail "$ClaudeSkills is not a git repo. Clone $RepoUrl there first (new machine: git clone $RepoUrl `"$ClaudeSkills`")."
|
Fail "$ClaudeSkills is not a git repo. Clone $RepoUrl there first (new machine: git clone $RepoUrl `"$ClaudeSkills`")."
|
||||||
}
|
}
|
||||||
@@ -162,61 +217,152 @@ if ($DryRun) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------- Phase 2: junction reconciliation (~\.codex\skills) ----------
|
# ---------- Phase 2: ingest + repair every registry ----------
|
||||||
Write-Host "=== [2/2] codex junctions: $CodexSkills ==="
|
# Walk registries FIRST so skills that only exist on the Codex side land in the primary
|
||||||
if (-not (Test-Path $CodexSkills)) { New-Item -ItemType Directory -Force $CodexSkills | Out-Null }
|
# before Phase 3 mirrors the primary back out.
|
||||||
|
$registries = @($Registry | Where-Object { $_ } | ForEach-Object { $_.TrimEnd('\') })
|
||||||
|
Write-Host "=== [2/3] registry ingest + repair ==="
|
||||||
|
|
||||||
# Left over from the older mirror-checkout approach; junctions need no repo here.
|
foreach ($reg in $registries) {
|
||||||
if (Test-Path (Join-Path $CodexSkills ".git")) {
|
Write-Host "-- $reg"
|
||||||
Write-Host "removing stale .git from codex dir (junction model needs no second checkout)"
|
if (-not (Test-Path $reg)) { Write-Host " (absent, skipped)"; continue }
|
||||||
if (-not $DryRun) { Remove-Item -Recurse -Force (Join-Path $CodexSkills ".git") }
|
$foreign = 0
|
||||||
}
|
|
||||||
|
|
||||||
$primarySkills = Get-ChildItem $ClaudeSkills -Directory | Where-Object { $_.Name -notlike ".*" }
|
# A registry carrying its own .git is a competing checkout; the junction model needs only one.
|
||||||
$blocked = @()
|
if (Test-Path (Join-Path $reg ".git")) {
|
||||||
foreach ($d in $primarySkills) {
|
$remote = (Invoke-Git $reg @("remote", "get-url", "origin")).Out
|
||||||
$link = Join-Path $CodexSkills $d.Name
|
$script:Notes += "$reg holds its own git checkout (origin: $remote). The junction model needs only the primary checkout; review and remove it once its unique content is ingested."
|
||||||
if (Test-Path $link) {
|
Write-Host " NOTE: competing git checkout here (origin: $remote) - reported, not touched"
|
||||||
$item = Get-Item $link
|
|
||||||
if ($item.LinkType -eq "Junction") { continue }
|
|
||||||
# A real dir shadows the primary skill; its content may be unique.
|
|
||||||
if ($Force) {
|
|
||||||
if ($DryRun) { Write-Host "(dry-run) would replace real dir with junction: $($d.Name)" ; continue }
|
|
||||||
Remove-Item -Recurse -Force $link
|
|
||||||
New-Item -ItemType Junction -Path $link -Target $d.FullName | Out-Null
|
|
||||||
Write-Host "replaced real dir with junction: $($d.Name)"
|
|
||||||
} else {
|
|
||||||
$blocked += $d.Name
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if ($DryRun) { Write-Host "(dry-run) would link: $($d.Name)"; continue }
|
foreach ($entry in Get-ChildItem $reg -Directory -Force | Where-Object { $_.Name -notlike ".*" }) {
|
||||||
New-Item -ItemType Junction -Path $link -Target $d.FullName | Out-Null
|
# $copyPath is the real directory to resolve; $repairLinks are registry entries that must
|
||||||
Write-Host "linked: $($d.Name)"
|
# end up pointing at the primary once it does.
|
||||||
|
$copyPath = $entry.FullName
|
||||||
|
$repairLinks = @()
|
||||||
|
|
||||||
|
if (Test-IsLink $entry) {
|
||||||
|
$target = Get-LinkTarget $entry
|
||||||
|
if (Test-UnderPath $target $ClaudeSkills) {
|
||||||
|
if (-not (Test-Path $target)) {
|
||||||
|
# Skill deleted/renamed upstream; the dangling link confuses Codex.
|
||||||
|
if ($DryRun) { Write-Host " (dry-run) would remove stale link: $($entry.Name)"; continue }
|
||||||
|
$entry.Delete()
|
||||||
|
Write-Host " removed stale link: $($entry.Name)"
|
||||||
}
|
}
|
||||||
}
|
continue # correctly pointed at the primary
|
||||||
|
|
||||||
# Junctions whose primary target is gone (skill deleted/renamed upstream) confuse Codex.
|
|
||||||
foreach ($item in Get-ChildItem $CodexSkills -Directory | Where-Object { $_.LinkType -eq "Junction" }) {
|
|
||||||
$target = ($item.Target | Select-Object -First 1)
|
|
||||||
if ($target -and $target.StartsWith($ClaudeSkills) -and -not (Test-Path $target)) {
|
|
||||||
if ($DryRun) { Write-Host "(dry-run) would remove stale junction: $($item.Name)"; continue }
|
|
||||||
# A junction is just a link -- removing it never touches target content.
|
|
||||||
$item.Delete()
|
|
||||||
Write-Host "removed stale junction: $($item.Name)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$inAnotherRegistry = $false
|
||||||
|
foreach ($other in $registries) { if (Test-UnderPath $target $other) { $inAnotherRegistry = $true } }
|
||||||
|
if (-not $inAnotherRegistry) {
|
||||||
|
# Host-owned catalog (e.g. the MathWorks agentic toolkits). Not ours to manage.
|
||||||
|
# Counted rather than listed: these run into the hundreds and drown the report.
|
||||||
|
$foreign++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Points at a copy living in another registry: the exact drift this model exists to
|
||||||
|
# prevent. Resolve that copy, then re-point this link at the primary.
|
||||||
|
Write-Host " MISDIRECTED link: $($entry.Name) -> $target"
|
||||||
|
$repairLinks += $entry.FullName
|
||||||
|
$copyPath = $target
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = Split-Path $copyPath -Leaf
|
||||||
|
$primaryPath = Join-Path $ClaudeSkills $name
|
||||||
|
if (-not (Test-Path $copyPath)) { continue } # already resolved earlier this run
|
||||||
|
$repairLinks += $copyPath
|
||||||
|
|
||||||
|
# --- real directory holding a skill ---
|
||||||
|
if (-not (Test-Path (Join-Path $copyPath "SKILL.md"))) {
|
||||||
|
Write-Host " not a skill dir (no SKILL.md), ignored: $name"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolved = $false
|
||||||
|
if (-not (Test-Path $primaryPath)) {
|
||||||
|
if ($NoIngest) {
|
||||||
|
$script:Conflicts += "$name exists only in $copyPath; -NoIngest kept it out of the primary."
|
||||||
|
Write-Host " registry-only skill NOT ingested (-NoIngest): $name"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Write-Host " INGEST registry-only skill: $name ($copyPath -> $primaryPath)"
|
||||||
|
if (-not $DryRun) { Move-Item -LiteralPath $copyPath -Destination $primaryPath }
|
||||||
|
$script:Ingested += $name
|
||||||
|
$resolved = $true
|
||||||
|
}
|
||||||
|
elseif ((Get-DirFingerprint $copyPath) -eq (Get-DirFingerprint $primaryPath)) {
|
||||||
|
# Identical copy: collapsing it loses nothing.
|
||||||
|
Write-Host " duplicate identical to primary, collapsing to junction: $name"
|
||||||
|
if (-not $DryRun) { Remove-Item -Recurse -Force -LiteralPath $copyPath }
|
||||||
|
$resolved = $true
|
||||||
|
}
|
||||||
|
elseif ($Force) {
|
||||||
|
Write-Host " -Force: primary wins over divergent copy: $name"
|
||||||
|
if (-not $DryRun) { Remove-Item -Recurse -Force -LiteralPath $copyPath }
|
||||||
|
$resolved = $true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$script:Conflicts += "$name diverges between $primaryPath and $copyPath. Merge what you want to keep into the primary, then re-run with -Force."
|
||||||
|
Write-Host " CONFLICT: $name diverges from primary (registry copy left untouched)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($resolved) {
|
||||||
|
foreach ($lnk in ($repairLinks | Select-Object -Unique)) {
|
||||||
|
if ((-not $DryRun) -and (Test-Path $lnk)) { (Get-Item $lnk).Delete() }
|
||||||
|
New-SkillJunction $lnk $primaryPath "repaired"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($foreign -gt 0) { Write-Host " $foreign host-owned links left alone (targets outside the managed registries)" }
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($blocked) {
|
# ---------- Phase 3: mirror the primary into the mirror registry ----------
|
||||||
Write-Host "these codex dirs are REAL directories shadowing primary skills (not linked):"
|
Write-Host "=== [3/3] mirror primary -> $MirrorRegistry ==="
|
||||||
$blocked | ForEach-Object { Write-Host " $_" }
|
if ($MirrorRegistry) {
|
||||||
Fail "rescue any unique content into the primary checkout, then re-run with -Force."
|
if (-not (Test-Path $MirrorRegistry)) {
|
||||||
}
|
if ($DryRun) { Write-Host "(dry-run) would create $MirrorRegistry" }
|
||||||
|
else { New-Item -ItemType Directory -Force $MirrorRegistry | Out-Null }
|
||||||
|
}
|
||||||
|
# Left over from the older mirror-checkout approach; junctions need no repo here.
|
||||||
|
$mirrorGit = Join-Path $MirrorRegistry ".git"
|
||||||
|
if ((Test-Path $mirrorGit) -and $MirrorRegistry -ne $ClaudeSkills) {
|
||||||
|
Write-Host "removing stale .git from mirror registry (junction model needs no second checkout)"
|
||||||
|
if (-not $DryRun) { Remove-Item -Recurse -Force $mirrorGit }
|
||||||
|
}
|
||||||
|
|
||||||
# Registration sanity: every linked skill must expose a SKILL.md for Codex to register it.
|
$primarySkills = Get-ChildItem $ClaudeSkills -Directory | Where-Object { $_.Name -notlike ".*" }
|
||||||
$missing = $primarySkills | Where-Object { -not (Test-Path (Join-Path $CodexSkills "$($_.Name)\SKILL.md")) }
|
foreach ($d in $primarySkills) {
|
||||||
if ($missing) {
|
$link = Join-Path $MirrorRegistry $d.Name
|
||||||
|
if (Test-Path $link) { continue }
|
||||||
|
New-SkillJunction $link $d.FullName "mirror"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Codex only registers a directory that exposes SKILL.md.
|
||||||
|
$missing = $primarySkills | Where-Object { -not (Test-Path (Join-Path $MirrorRegistry "$($_.Name)\SKILL.md")) }
|
||||||
|
if ($missing -and -not $DryRun) {
|
||||||
Write-Host "WARNING: no SKILL.md visible through these links (Codex will ignore them):"
|
Write-Host "WARNING: no SKILL.md visible through these links (Codex will ignore them):"
|
||||||
$missing | ForEach-Object { Write-Host " $($_.Name)" }
|
$missing | ForEach-Object { Write-Host " $($_.Name)" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# An ingested skill matching a local-only .gitignore rule never reaches the remote —
|
||||||
|
# it would silently fail to appear on the next machine.
|
||||||
|
foreach ($name in $script:Ingested) {
|
||||||
|
$ci = Invoke-Git $ClaudeSkills @("check-ignore", "-v", $name)
|
||||||
|
if ($ci.Code -eq 0) {
|
||||||
|
$script:Notes += "ingested '$name' is excluded by .gitignore ($($ci.Out)); it stays local-only until you remove that rule."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "=== summary ==="
|
||||||
|
if ($script:Ingested) { Write-Host "ingested into primary: $($script:Ingested -join ', ')" }
|
||||||
|
foreach ($n in $script:Notes) { Write-Host "NOTE: $n" }
|
||||||
|
if ($script:Conflicts) {
|
||||||
|
Write-Host "conflicts needing a decision:"
|
||||||
|
$script:Conflicts | ForEach-Object { Write-Host " - $_" }
|
||||||
|
Write-Host "=== sync_repos finished with conflicts ==="
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
Write-Host "=== sync_repos done (Codex picks up changes on its next new session) ==="
|
Write-Host "=== sync_repos done (Codex picks up changes on its next new session) ==="
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ command = "C:\\Users\\tester\\.codex\\runtimes\\node_repl.exe"
|
|||||||
|
|
||||||
$oldErrorActionPreference = $ErrorActionPreference
|
$oldErrorActionPreference = $ErrorActionPreference
|
||||||
$ErrorActionPreference = "Continue"
|
$ErrorActionPreference = "Continue"
|
||||||
|
# -CodexSource Toml pins the read to the fixture; the default Auto path would query the
|
||||||
|
# real `codex mcp list` and make this test measure the machine instead of the fixtures.
|
||||||
$output = & powershell -NoProfile -ExecutionPolicy Bypass -File $scriptUnderTest `
|
$output = & powershell -NoProfile -ExecutionPolicy Bypass -File $scriptUnderTest `
|
||||||
-ClaudeConfig $claudeConfig -CodexConfig $codexConfig -Verbose 2>&1 | Out-String
|
-ClaudeConfig $claudeConfig -CodexConfig $codexConfig -CodexSource Toml -Verbose 2>&1 | Out-String
|
||||||
$code = $LASTEXITCODE
|
$code = $LASTEXITCODE
|
||||||
$ErrorActionPreference = $oldErrorActionPreference
|
$ErrorActionPreference = $oldErrorActionPreference
|
||||||
if ($code -ne 2) { throw "expected audit exit 2, got $code`n$output" }
|
if ($code -ne 2) { throw "expected audit exit 2, got $code`n$output" }
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Covers Phase 2 of sync_repos.ps1 -- the registry reconciliation that decides whether a
|
||||||
|
# directory found on the Codex side gets ingested, collapsed, or reported. Runs against a
|
||||||
|
# throwaway primary + registries + bare origin under TEMP; the real machine is never touched.
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$scriptUnderTest = Join-Path (Split-Path $PSScriptRoot -Parent) "scripts\sync_repos.ps1"
|
||||||
|
$root = Join-Path ([IO.Path]::GetTempPath()) ("tools-sync-repos-test-" + [guid]::NewGuid().ToString("N"))
|
||||||
|
|
||||||
|
function New-Skill([string]$Path, [string]$Body) {
|
||||||
|
New-Item -ItemType Directory -Force -Path $Path | Out-Null
|
||||||
|
Set-Content -LiteralPath (Join-Path $Path "SKILL.md") -Value $Body -Encoding utf8
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$primary = Join-Path $root "primary"
|
||||||
|
$codex = Join-Path $root "codex"
|
||||||
|
$agents = Join-Path $root "agents"
|
||||||
|
New-Item -ItemType Directory -Force -Path $primary, $codex, $agents | Out-Null
|
||||||
|
|
||||||
|
# One skill of each shape the reconciler must tell apart.
|
||||||
|
New-Skill (Join-Path $primary "shared") "shared body"
|
||||||
|
New-Skill (Join-Path $primary "forked") "primary body"
|
||||||
|
New-Skill (Join-Path $agents "shared") "shared body" # identical duplicate -> collapse
|
||||||
|
New-Skill (Join-Path $agents "forked") "registry body" # divergent -> conflict
|
||||||
|
New-Skill (Join-Path $agents "codexborn") "born on the codex side" # registry-only -> ingest
|
||||||
|
New-Item -ItemType Directory -Force -Path (Join-Path $agents "notaskill") | Out-Null # no SKILL.md
|
||||||
|
# A link into the other registry rather than the primary: the drift case.
|
||||||
|
New-Item -ItemType Junction -Path (Join-Path $codex "shared") -Target (Join-Path $agents "shared") | Out-Null
|
||||||
|
|
||||||
|
# A local bare repo stands in for origin so Phase 1 completes and Phase 2 actually runs.
|
||||||
|
$origin = Join-Path $root "origin.git"
|
||||||
|
& git init --quiet --bare $origin
|
||||||
|
& git init --quiet --initial-branch=main $primary
|
||||||
|
& git -C $primary remote add origin $origin
|
||||||
|
& git -C $primary -c user.email=t@t -c user.name=t commit --quiet --allow-empty -m init
|
||||||
|
& git -C $primary push --quiet -u origin main
|
||||||
|
|
||||||
|
# Invoked via -Command, not -File: -File stringifies every argument, so a [string[]]
|
||||||
|
# parameter silently takes only its first element and the rest slide into later positions.
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
$invocation = "& '$scriptUnderTest' -RepoUrl '$origin' -ClaudeSkills '$primary' " +
|
||||||
|
"-Registry @('$codex','$agents') -MirrorRegistry '$codex'"
|
||||||
|
$output = & powershell -NoProfile -ExecutionPolicy Bypass -Command $invocation 2>&1 | Out-String
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
if ($output -notmatch 'INGEST registry-only skill:\s*codexborn') { throw "codex-side skill was not ingested`n$output" }
|
||||||
|
if (-not (Test-Path (Join-Path $primary "codexborn\SKILL.md"))) { throw "ingested skill is not in the primary`n$output" }
|
||||||
|
if ((Get-Item (Join-Path $agents "codexborn")).LinkType -ne "Junction") { throw "ingested skill was not linked back`n$output" }
|
||||||
|
|
||||||
|
if ($output -notmatch 'MISDIRECTED link:\s*shared') { throw "link into another registry not detected`n$output" }
|
||||||
|
$sharedTarget = (Get-Item (Join-Path $codex "shared")).Target | Select-Object -First 1
|
||||||
|
if ($sharedTarget -ne (Join-Path $primary "shared")) { throw "misdirected link not repointed at the primary, got $sharedTarget`n$output" }
|
||||||
|
if ((Get-Item (Join-Path $agents "shared")).LinkType -ne "Junction") { throw "identical duplicate did not collapse`n$output" }
|
||||||
|
|
||||||
|
if ($output -notmatch 'CONFLICT:\s*forked') { throw "divergent duplicate not reported`n$output" }
|
||||||
|
if ((Get-Item (Join-Path $agents "forked")).LinkType -eq "Junction") { throw "divergent duplicate must be left untouched`n$output" }
|
||||||
|
if ((Get-Content -Raw (Join-Path $primary "forked\SKILL.md")) -notmatch 'primary body') { throw "primary was overwritten by a conflict`n$output" }
|
||||||
|
|
||||||
|
if ($output -notmatch 'not a skill dir') { throw "directory without SKILL.md not reported`n$output" }
|
||||||
|
# Mirror registry must expose every primary skill, including the freshly ingested one.
|
||||||
|
foreach ($name in @("shared", "forked", "codexborn")) {
|
||||||
|
if (-not (Test-Path (Join-Path $codex "$name\SKILL.md"))) { throw "mirror missing $name`n$output" }
|
||||||
|
}
|
||||||
|
# Phase 1's success line is the ls-remote gate's verdict (upstream wording).
|
||||||
|
if ($output -notmatch 'verified: origin/main =') { throw "Phase 1 did not complete`n$output" }
|
||||||
|
|
||||||
|
Write-Host "PASS: registry ingest / repair / conflict fixtures"
|
||||||
|
} finally {
|
||||||
|
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user