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:
@@ -117,6 +117,28 @@ function Get-DirFingerprint([string]$Path) {
|
|||||||
return (($lines | Sort-Object) -join "`n")
|
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) {
|
function New-SkillJunction([string]$LinkPath, [string]$TargetPath, [string]$Why) {
|
||||||
if ($DryRun) { Write-Host " (dry-run) would link $LinkPath -> $TargetPath [$Why]"; return }
|
if ($DryRun) { Write-Host " (dry-run) would link $LinkPath -> $TargetPath [$Why]"; return }
|
||||||
New-Item -ItemType Junction -Path $LinkPath -Target $TargetPath | Out-Null
|
New-Item -ItemType Junction -Path $LinkPath -Target $TargetPath | Out-Null
|
||||||
@@ -272,6 +294,17 @@ foreach ($reg in $registries) {
|
|||||||
$name = Split-Path $copyPath -Leaf
|
$name = Split-Path $copyPath -Leaf
|
||||||
$primaryPath = Join-Path $ClaudeSkills $name
|
$primaryPath = Join-Path $ClaudeSkills $name
|
||||||
if (-not (Test-Path $copyPath)) { continue } # already resolved earlier this run
|
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
|
$repairLinks += $copyPath
|
||||||
|
|
||||||
# --- real directory holding a skill ---
|
# --- real directory holding a skill ---
|
||||||
@@ -287,6 +320,15 @@ foreach ($reg in $registries) {
|
|||||||
Write-Host " registry-only skill NOT ingested (-NoIngest): $name"
|
Write-Host " registry-only skill NOT ingested (-NoIngest): $name"
|
||||||
continue
|
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)"
|
Write-Host " INGEST registry-only skill: $name ($copyPath -> $primaryPath)"
|
||||||
if (-not $DryRun) { Move-Item -LiteralPath $copyPath -Destination $primaryPath }
|
if (-not $DryRun) { Move-Item -LiteralPath $copyPath -Destination $primaryPath }
|
||||||
$script:Ingested += $name
|
$script:Ingested += $name
|
||||||
@@ -332,7 +374,10 @@ if ($MirrorRegistry) {
|
|||||||
if (-not $DryRun) { Remove-Item -Recurse -Force $mirrorGit }
|
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) {
|
foreach ($d in $primarySkills) {
|
||||||
$link = Join-Path $MirrorRegistry $d.Name
|
$link = Join-Path $MirrorRegistry $d.Name
|
||||||
if (Test-Path $link) { continue }
|
if (Test-Path $link) { continue }
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ try {
|
|||||||
# A link into the other registry rather than the primary: the drift case.
|
# 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
|
New-Item -ItemType Junction -Path (Join-Path $codex "shared") -Target (Join-Path $agents "shared") | Out-Null
|
||||||
|
|
||||||
|
# Host-installed shape: the real directory lives in a registry and the PRIMARY is the link
|
||||||
|
# (Orca installs this way). Collapsing the registry copy here would make the link point at
|
||||||
|
# itself and delete the only copy - the reconciler must recognise it and keep its hands off.
|
||||||
|
New-Skill (Join-Path $agents "hostowned") "installed by the host"
|
||||||
|
New-Item -ItemType Junction -Path (Join-Path $primary "hostowned") -Target (Join-Path $agents "hostowned") | Out-Null
|
||||||
|
|
||||||
# A local bare repo stands in for origin so Phase 1 completes and Phase 2 actually runs.
|
# A local bare repo stands in for origin so Phase 1 completes and Phase 2 actually runs.
|
||||||
$origin = Join-Path $root "origin.git"
|
$origin = Join-Path $root "origin.git"
|
||||||
& git init --quiet --bare $origin
|
& git init --quiet --bare $origin
|
||||||
@@ -57,6 +63,15 @@ try {
|
|||||||
if ((Get-Content -Raw (Join-Path $primary "forked\SKILL.md")) -notmatch 'primary body') { throw "primary was overwritten by a conflict`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" }
|
if ($output -notmatch 'not a skill dir') { throw "directory without SKILL.md not reported`n$output" }
|
||||||
|
|
||||||
|
# The host-installed skill must survive intact, with the primary still a link at the registry.
|
||||||
|
$hostReal = Get-Item -Force (Join-Path $agents "hostowned")
|
||||||
|
if ($hostReal.LinkType -eq "Junction") { throw "host-installed skill's real dir was collapsed into a link`n$output" }
|
||||||
|
if ((Get-Content -Raw (Join-Path $agents "hostowned\SKILL.md")) -notmatch 'installed by the host') { throw "host-installed skill's content was lost`n$output" }
|
||||||
|
$hostPrimary = Get-Item -Force (Join-Path $primary "hostowned")
|
||||||
|
if (($hostPrimary.Target | Select-Object -First 1) -ne (Join-Path $agents "hostowned")) { throw "primary link to the host-installed skill was repointed`n$output" }
|
||||||
|
# Mirroring it into the registry Codex scans would register the same skill twice.
|
||||||
|
if (Test-Path (Join-Path $codex "hostowned")) { throw "host-installed skill was mirrored, double-registering it`n$output" }
|
||||||
# Mirror registry must expose every primary skill, including the freshly ingested one.
|
# Mirror registry must expose every primary skill, including the freshly ingested one.
|
||||||
foreach ($name in @("shared", "forked", "codexborn")) {
|
foreach ($name in @("shared", "forked", "codexborn")) {
|
||||||
if (-not (Test-Path (Join-Path $codex "$name\SKILL.md"))) { throw "mirror missing $name`n$output" }
|
if (-not (Test-Path (Join-Path $codex "$name\SKILL.md"))) { throw "mirror missing $name`n$output" }
|
||||||
|
|||||||
Reference in New Issue
Block a user