tools-sync: 修复会删掉宿主自装 skill 的 registry 对账缺陷

本次同步实跑触发了数据丢失:orca-cli 的物理副本在 ~\.agents\skills(Orca 就是
这样装的——真目录放 .agents,再把每个工具链过去,本机 8 个工具全指向它),
而主副本里的同名条目本身是指向它的 junction。对账循环从不检查"主副本条目是不是
链接",于是 fingerprint 比对经链接解析到同一份内容判为"identical",删掉 registry
副本并建 junction → 主副本与 registry 互指成环,唯一的真副本没了。
orchestration 走 INGEST 分支,则把 Orca 的布局反了过来。

两者均已用 `orca skills install --skill <name>` 恢复为 Orca 自己的布局。

修复三处:
- 对账循环:主副本条目本身是链接时,判定该 skill 归宿主所有,计入 foreign 跳过。
- INGEST:新增 Get-SkillDirLinkers,若 ~\.* 下其他工具的 skills 目录有链接指向
  这个目录,说明它是宿主自装 skill 的物理家,不搬进主副本。
- 镜像阶段:跳过本身是链接的主副本条目,否则 Codex 会把同一 skill 注册两遍
  (.agents 与 .codex 都在扫)。

回归测试新增 hostowned 夹具(真目录在 registry、主副本是链接):在修复前的脚本上
以"real dir was collapsed into a link"失败,修复后通过。

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:29:20 +08:00
co-authored by Claude Opus 5
parent 58b90f16a6
commit 8566e23f49
2 changed files with 61 additions and 1 deletions
+46 -1
View File
@@ -117,6 +117,28 @@ function Get-DirFingerprint([string]$Path) {
return (($lines | Sort-Object) -join "`n")
}
# Agent tools each keep their own skills directory and a host installer may pick any one of them
# as a skill's physical home, linking the rest at it. Finding those linkers is how we tell a
# host-installed skill from one authored on the Codex side.
function Get-SkillDirLinkers([string]$TargetPath) {
$roots = @()
foreach ($d in Get-ChildItem $env:USERPROFILE -Directory -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like ".*" }) {
foreach ($sub in @("skills", "agent\skills")) {
$cand = Join-Path $d.FullName $sub
if (Test-Path $cand) { $roots += $cand }
}
}
$hits = @()
foreach ($r in $roots) {
if (Test-UnderPath $TargetPath $r) { continue } # the home itself
$link = Join-Path $r (Split-Path $TargetPath -Leaf)
$item = Get-Item -LiteralPath $link -Force -ErrorAction SilentlyContinue
if ($item -and (Test-IsLink $item) -and ((Get-LinkTarget $item) -eq $TargetPath)) { $hits += $r }
}
return $hits
}
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
@@ -272,6 +294,17 @@ foreach ($reg in $registries) {
$name = Split-Path $copyPath -Leaf
$primaryPath = Join-Path $ClaudeSkills $name
if (-not (Test-Path $copyPath)) { continue } # already resolved earlier this run
# The primary entry is itself a link into a registry: this skill's physical home is the
# registry, and the host installed it that way round (Orca does exactly this - it puts the
# real directory in ~\.agents\skills and links every tool at it). Collapsing the registry
# copy here would point the link at itself and DELETE the only copy. Leave it to its owner.
$primaryItem = Get-Item -LiteralPath $primaryPath -Force -ErrorAction SilentlyContinue
if ($primaryItem -and (Test-IsLink $primaryItem)) {
$foreign++
continue
}
$repairLinks += $copyPath
# --- real directory holding a skill ---
@@ -287,6 +320,15 @@ foreach ($reg in $registries) {
Write-Host " registry-only skill NOT ingested (-NoIngest): $name"
continue
}
# Other agent tools linking at this directory mean it is a host-installed skill's
# physical home, not a skill authored here. Moving it inverts the host's layout and
# its own updater then fights us for the location.
$linkers = Get-SkillDirLinkers $copyPath
if ($linkers) {
$script:Notes += "$name lives in $copyPath and $($linkers.Count) other tool registr$(if ($linkers.Count -eq 1) { 'y links' } else { 'ies link' }) at it; treated as host-installed and left alone."
Write-Host " host-installed skill (linked from $($linkers.Count) other tool(s)), not ingested: $name"
continue
}
Write-Host " INGEST registry-only skill: $name ($copyPath -> $primaryPath)"
if (-not $DryRun) { Move-Item -LiteralPath $copyPath -Destination $primaryPath }
$script:Ingested += $name
@@ -332,7 +374,10 @@ if ($MirrorRegistry) {
if (-not $DryRun) { Remove-Item -Recurse -Force $mirrorGit }
}
$primarySkills = Get-ChildItem $ClaudeSkills -Directory | Where-Object { $_.Name -notlike ".*" }
# A primary entry that is itself a link belongs to whichever host installed it (its real home
# is a registry the host already exposes to Codex). Mirroring it would register it twice.
$primarySkills = Get-ChildItem $ClaudeSkills -Directory -Force |
Where-Object { $_.Name -notlike ".*" -and -not (Test-IsLink $_) }
foreach ($d in $primarySkills) {
$link = Join-Path $MirrorRegistry $d.Name
if (Test-Path $link) { continue }