sync: BOWEI 2026-09-07 tools-sync 改为 junction 模型(registry 双向 ingest + 修复)

- sync_repos.ps1: 每个 skill 只在 ~\.claude\skills 有一份物理副本,其余 registry
  一律 junction;新增 registry-only skill 的自动 ingest、MISDIRECTED 链接修复、
  同名内容分叉的 CONFLICT 报告,以及只对 -MirrorRegistry 做全量镜像。
- SKILL.md: 重写阶段 1/2,补 Codex MCP 存储模型(config.toml 惰性,
  codex mcp list --json 是唯一权威口径)与 CLI 升级会清空注册表的事实。
- sync_mcp_registrations.ps1 + references/codex_mcp_wanted_keys.json:
  报告 codex-cli 无接口可设的键(tool_timeout_sec / env_vars)为
  "registered, but incomplete" 而非失败。
- tests/test_sync_repos_registries.ps1: registry 对账的回归测试。
- .gitignore: orchestration(Orca 自带的发现桩,同 orca-cli)列为 local-only。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01438Zbcx49e4o46uSA71mFF
This commit is contained in:
Myfro9
2026-09-07 16:22:09 +08:00
co-authored by Claude Opus 5
parent aca7c799a8
commit f665448376
8 changed files with 467 additions and 111 deletions
+69 -3
View File
@@ -1,6 +1,20 @@
# tools-sync: audit and reconcile user-configured MCP registrations between
# Claude Code and Codex. Managed connectors, plugins, and Codex runtime MCPs are
# 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()]
param(
@@ -10,12 +24,21 @@ param(
[ValidateSet("None", "Claude", "Codex")]
[string]$ConflictAuthority = "None",
[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"
$script:CodexBackupMade = $false
$script:Failures = @()
$script:Unsupported = @()
$CodexOnlyEnv = @("PYTHONIOENCODING", "PYTHONUTF8")
function Test-TomllibPython([string]$Candidate) {
@@ -53,6 +76,19 @@ function Resolve-PythonCommand([string]$Requested, [bool]$Explicit) {
$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) {
$result = [ordered]@{}
if ($null -eq $Object) { return $result }
@@ -128,8 +164,11 @@ function Get-CodexMcpsFromCli {
}
function Get-CodexMcps {
$fromCli = Get-CodexMcpsFromCli
if ($null -ne $fromCli) { return $fromCli }
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)) {
throw "Codex config not found: $CodexConfig"
@@ -234,6 +273,7 @@ function Add-ToCodex($Mcp) {
$arguments = @("mcp", "add", $Mcp.name)
$envMap = [ordered]@{}
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["PYTHONUTF8"] = "1"
foreach ($key in $envMap.Keys) { $arguments += @("--env", "$key=$($envMap[$key])") }
@@ -241,6 +281,7 @@ function Add-ToCodex($Mcp) {
$arguments += $Mcp.command
$arguments += @($Mcp.args)
Invoke-External "codex add $($Mcp.name)" "codex" $arguments
Report-UnsupportedKeys $Mcp.name
return
}
if ($Mcp.headers.Count -gt 0) {
@@ -248,6 +289,17 @@ function Add-ToCodex($Mcp) {
}
Backup-CodexConfig
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) {
@@ -344,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) {
Write-Host "=== failures ==="
$script:Failures | ForEach-Object { Write-Host "- $_" }