diff --git a/.tasks/STATUS.md b/.tasks/STATUS.md index dc22589..7b6bffb 100644 --- a/.tasks/STATUS.md +++ b/.tasks/STATUS.md @@ -397,13 +397,14 @@ needs-claude --- -## 🟡 [agent-neutral-skill-pipeline] — ДОЛГ (идея 19 п.5, буфер claude-to-agents): инсталл/апдейт-пайплайн скилов — claude-only. update-claude-skills ставит только в ~/.claude/skills; pi получил каталог вручную (settings skills array, идея 6). Цель: update-skills обслуживает всех агентов (единый канон + честный per-agent срез). Связано с ренеймом репо (rename-repo-to-skills) и чисткой ~/.agents/skills. Сейчас НЕ исполняем (долг, status=paused) — осознанно отложено. +## 🟢 [agent-neutral-skill-pipeline] — ДОЛГ (идея 19 п.5): инсталл/апдейт-пайплайн скилов — claude-only. update-claude-skills ставил только в ~/.claude/skills; pi получил каталог вручную (settings skills array, идея 6). Цель: update-skills обслуживает всех агентов (единый канон + честный per-agent срез). -**Status:** paused -**Where I stopped:** (not started) -**Next action:** (пауза) После rename-repo-to-skills: спроектировать update-skills с таргетами ~/.claude/skills + ~/.agents/skills + (pi settings), подтвердить у владельца -**Branch:** n/a +**Status:** done +**Where I stopped:** РЕШЕНИЕ ОПЕРАТОРА (2026-08-12, зафиксировано после вскрытия непротоколированного решения вчерашней сессии «канон = ~/.claude/skills»): **dual install target**. install.sh/ps1 + update.sh/ps1 ставят в ОБА каталога: ~/.claude/skills (Claude Code — читает только его нативно, кастомные диры не конфигурируются) + ~/.agents/skills (pi — нативный скан-путь, нейтральный неймспейс .agents/). Из ~/.pi/agent/settings.json убран skills-массив (вендор-лок устранён). update-skills SKILL.md v0.2.1 — секция «Install targets (dual, agent-neutral)», semver-bump, lint 46/0. Оба таргета синхронны (46/46, diff IDENTICAL). Источник истины — git-репо skills/, оба дира — инсталлы. +**Next action:** (none — kept until merged) pi перезапустить чтобы убедиться, что скилы грузятся из ~/.agents/skills (нативный скан без settings-хака). +**Branch:** master + --- diff --git a/dist/update-skills.skill b/dist/update-skills.skill index e302674..5ea5520 100644 Binary files a/dist/update-skills.skill and b/dist/update-skills.skill differ diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 729633e..b7ca16f 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1,6 +1,10 @@ -# Install skills// into ~\.claude\skills\\ (or $env:CLAUDE_SKILLS_DIR) +# Install skills// into BOTH agent skill dirs: +# ~\.claude\skills\\ (Claude Code — native, not configurable) +# ~\.agents\skills\\ (pi — native default scan path, agent-neutral namespace) +# $env:CLAUDE_SKILLS_DIR overrides ONLY the claude target (for testing/CI). +# # Usage: install.ps1 [-Names ,] [-Prune] -# no args = install all skills/* into target +# no args = install all skills/* into both targets # -Prune = after install, remove target// dirs that are NOT in skills/* # (prune always scans full target, ignores -Names filter — it's a global cleanup) # @@ -17,45 +21,54 @@ $ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot $src = Join-Path $root 'skills' +# Dual install targets — the "canon" is the git repo (skills/); both dirs are installs. +$targets = @() if ($env:CLAUDE_SKILLS_DIR) { - $target = $env:CLAUDE_SKILLS_DIR + $targets += $env:CLAUDE_SKILLS_DIR } else { - $target = Join-Path $env:USERPROFILE '.claude\skills' + $targets += (Join-Path $env:USERPROFILE '.claude\skills') } +$targets += (Join-Path $env:USERPROFILE '.agents\skills') -if (-not (Test-Path $target)) { - New-Item -ItemType Directory -Force -Path $target | Out-Null +foreach ($target in $targets) { + if (-not (Test-Path $target)) { + New-Item -ItemType Directory -Force -Path $target | Out-Null + } } if ($Names.Count -eq 0) { $Names = Get-ChildItem -Path $src -Directory | Sort-Object Name | Select-Object -ExpandProperty Name } -foreach ($name in $Names) { - $srcDir = Join-Path $src $name - $dstDir = Join-Path $target $name - if (-not (Test-Path $srcDir -PathType Container)) { - Write-Warning "skip: $name (not found in skills/)" - continue +foreach ($target in $targets) { + foreach ($name in $Names) { + $srcDir = Join-Path $src $name + $dstDir = Join-Path $target $name + if (-not (Test-Path $srcDir -PathType Container)) { + Write-Warning "skip: $name (not found in skills/)" + continue + } + if (-not (Test-Path (Join-Path $srcDir 'SKILL.md'))) { + Write-Warning "skip: $name (missing SKILL.md)" + continue + } + if (Test-Path $dstDir) { + Remove-Item -Recurse -Force $dstDir + } + Copy-Item -Recurse $srcDir $dstDir + Write-Host "installed: $name -> $dstDir" } - if (-not (Test-Path (Join-Path $srcDir 'SKILL.md'))) { - Write-Warning "skip: $name (missing SKILL.md)" - continue - } - if (Test-Path $dstDir) { - Remove-Item -Recurse -Force $dstDir - } - Copy-Item -Recurse $srcDir $dstDir - Write-Host "installed: $name -> $dstDir" } if ($Prune) { $sourceNames = @(Get-ChildItem -Path $src -Directory | Select-Object -ExpandProperty Name) - $installedDirs = Get-ChildItem -Path $target -Directory -ErrorAction SilentlyContinue - foreach ($dir in $installedDirs) { - if ($sourceNames -notcontains $dir.Name) { - Write-Host "pruning: $($dir.Name) (not in skills/) -> $($dir.FullName)" - Remove-Item -Recurse -Force $dir.FullName + foreach ($target in $targets) { + $installedDirs = Get-ChildItem -Path $target -Directory -ErrorAction SilentlyContinue + foreach ($dir in $installedDirs) { + if ($sourceNames -notcontains $dir.Name) { + Write-Host "pruning: $($dir.Name) (not in skills/) -> $($dir.FullName)" + Remove-Item -Recurse -Force $dir.FullName + } } } } diff --git a/scripts/install.sh b/scripts/install.sh index 55b807c..304dc8f 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,14 +1,20 @@ #!/usr/bin/env bash -# Install skills// into ~/.claude/skills// (or $CLAUDE_SKILLS_DIR) +# Install skills// into BOTH agent skill dirs: +# ~/.claude/skills// (Claude Code — native, not configurable) +# ~/.agents/skills// (pi — native default scan path, agent-neutral namespace) +# $CLAUDE_SKILLS_DIR overrides ONLY the claude target (for testing/CI). +# # Usage: install.sh [--prune] [name...] -# no args = install all skills/* into target +# no args = install all skills/* into both targets # --prune = after install, remove target// dirs that are NOT in skills/* # (prune always scans full target, ignores name filter — global cleanup) set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SRC="$ROOT/skills" -TARGET="${CLAUDE_SKILLS_DIR:-$HOME/.claude/skills}" + +# Dual install targets — the "canon" is the git repo (skills/); both dirs are installs. +TARGETS=("${CLAUDE_SKILLS_DIR:-$HOME/.claude/skills}" "$HOME/.agents/skills") prune=0 positional=() @@ -33,31 +39,37 @@ else names=("${positional[@]}") fi -mkdir -p "$TARGET" +for target in "${TARGETS[@]}"; do + mkdir -p "$target" +done -for name in "${names[@]}"; do - src_dir="$SRC/$name" - dst_dir="$TARGET/$name" - if [ ! -d "$src_dir" ]; then - echo "skip: $name (not found in skills/)" >&2 - continue - fi - if [ ! -f "$src_dir/SKILL.md" ]; then - echo "skip: $name (missing SKILL.md)" >&2 - continue - fi - rm -rf "$dst_dir" - cp -R "$src_dir" "$dst_dir" - echo "installed: $name → $dst_dir" +for target in "${TARGETS[@]}"; do + for name in "${names[@]}"; do + src_dir="$SRC/$name" + dst_dir="$target/$name" + if [ ! -d "$src_dir" ]; then + echo "skip: $name (not found in skills/)" >&2 + continue + fi + if [ ! -f "$src_dir/SKILL.md" ]; then + echo "skip: $name (missing SKILL.md)" >&2 + continue + fi + rm -rf "$dst_dir" + cp -R "$src_dir" "$dst_dir" + echo "installed: $name → $dst_dir" + done done if [ "$prune" -eq 1 ]; then - for d in "$TARGET"/*/; do - [ -d "$d" ] || continue - name="$(basename "$d")" - if [ ! -d "$SRC/$name" ]; then - echo "pruning: $name (not in skills/) → $d" - rm -rf "$d" - fi + for target in "${TARGETS[@]}"; do + for d in "$target"/*/; do + [ -d "$d" ] || continue + name="$(basename "$d")" + if [ ! -d "$SRC/$name" ]; then + echo "pruning: $name (not in skills/) → $d" + rm -rf "$d" + fi + done done fi diff --git a/scripts/update.ps1 b/scripts/update.ps1 index 786ec03..caf2759 100644 --- a/scripts/update.ps1 +++ b/scripts/update.ps1 @@ -22,7 +22,10 @@ $ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot $skillsSrc = Join-Path $root 'skills' -$target = if ($env:CLAUDE_SKILLS_DIR) { $env:CLAUDE_SKILLS_DIR } else { Join-Path $env:USERPROFILE '.claude\skills' } +# Dual install targets — the "canon" is the git repo (skills/); both dirs are installs. +$targets = @() +if ($env:CLAUDE_SKILLS_DIR) { $targets += $env:CLAUDE_SKILLS_DIR } else { $targets += (Join-Path $env:USERPROFILE '.claude\skills') } +$targets += (Join-Path $env:USERPROFILE '.agents\skills') $commonRoot = if ($env:COMMON_ROOT) { $env:COMMON_ROOT } else { Join-Path $env:USERPROFILE 'projects\.common' } $metaMcp = Join-Path $commonRoot 'lib\projects-meta-mcp' @@ -31,13 +34,15 @@ $internsMcp = Join-Path $commonRoot 'lib\interns-mcp' # -- Collect before-versions -------------------------------------------------- $beforeVersions = @{} -if (Test-Path $target) { - Get-ChildItem -Path $target -Directory -ErrorAction SilentlyContinue | ForEach-Object { - $vf = Join-Path $_.FullName 'SKILL.md' - if (Test-Path $vf) { - $match = Select-String -Path $vf -Pattern '^version:\s*(\d+\.\d+\.\d+)' -ErrorAction SilentlyContinue | Select-Object -First 1 - $ver = if ($match) { $match.Matches.Groups[1].Value } else { '?' } - $beforeVersions[$_.Name] = $ver +foreach ($target in $targets) { + if (Test-Path $target) { + Get-ChildItem -Path $target -Directory -ErrorAction SilentlyContinue | ForEach-Object { + $vf = Join-Path $_.FullName 'SKILL.md' + if (Test-Path $vf) { + $match = Select-String -Path $vf -Pattern '^version:\s*(\d+\.\d+\.\d+)' -ErrorAction SilentlyContinue | Select-Object -First 1 + $ver = if ($match) { $match.Matches.Groups[1].Value } else { '?' } + $beforeVersions[$_.Name] = $ver + } } } } @@ -144,16 +149,18 @@ Write-Host "[ok] Skills installed." -ForegroundColor Green Write-Host "[update] Version diff:" -ForegroundColor Cyan $changes = $false -Get-ChildItem -Path $target -Directory -ErrorAction SilentlyContinue | ForEach-Object { - $vf = Join-Path $_.FullName 'SKILL.md' - if (Test-Path $vf) { - $match = Select-String -Path $vf -Pattern '^version:\s*(\d+\.\d+\.\d+)' -ErrorAction SilentlyContinue | Select-Object -First 1 - $after = if ($match) { $match.Matches.Groups[1].Value } else { '?' } - $before = $beforeVersions[$_.Name] - if (-not $before) { $before = 'new' } - if ($before -ne $after) { - Write-Host " $before -> $after ($($_.Name))" - $changes = $true +foreach ($target in $targets) { + Get-ChildItem -Path $target -Directory -ErrorAction SilentlyContinue | ForEach-Object { + $vf = Join-Path $_.FullName 'SKILL.md' + if (Test-Path $vf) { + $match = Select-String -Path $vf -Pattern '^version:\s*(\d+\.\d+\.\d+)' -ErrorAction SilentlyContinue | Select-Object -First 1 + $after = if ($match) { $match.Matches.Groups[1].Value } else { '?' } + $before = $beforeVersions[$_.Name] + if (-not $before) { $before = 'new' } + if ($before -ne $after) { + Write-Host " $before -> $after ($($_.Name))" + $changes = $true + } } } } @@ -166,9 +173,11 @@ if (-not $changes) { $newSetup = @() Get-ChildItem -Path $skillsSrc -Directory | ForEach-Object { $name = $_.Name - if ($name -like 'setup-*' -and -not (Test-Path (Join-Path $target $name))) { - $newSetup += $name + $missing = $false + foreach ($target in $targets) { + if ($name -like 'setup-*' -and -not (Test-Path (Join-Path $target $name))) { $missing = $true } } + if ($missing) { $newSetup += $name } } if ($newSetup.Count -gt 0) { diff --git a/scripts/update.sh b/scripts/update.sh index 91638f9..987b154 100644 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -20,7 +20,8 @@ YES_MODE=false ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SKILLS_SRC="$ROOT/skills" -TARGET="${CLAUDE_SKILLS_DIR:-$HOME/.claude/skills}" +# Dual install targets — the "canon" is the git repo (skills/); both dirs are installs. +TARGETS=("${CLAUDE_SKILLS_DIR:-$HOME/.claude/skills}" "$HOME/.agents/skills") COMMON_ROOT="${COMMON_ROOT:-$HOME/projects/.common}" META_MCP="$COMMON_ROOT/lib/projects-meta-mcp" @@ -43,8 +44,9 @@ confirm() { # ── Collect before-versions ──────────────────────────────────────────────── declare -A BEFORE_VERSIONS=() -if [[ -d "$TARGET" ]]; then - for skill_dir in "$TARGET"/*/; do +for target in "${TARGETS[@]}"; do + [[ -d "$target" ]] || continue + for skill_dir in "$target"/*/; do name="$(basename "$skill_dir")" vf="$skill_dir/SKILL.md" if [[ -f "$vf" ]]; then @@ -52,7 +54,7 @@ if [[ -d "$TARGET" ]]; then BEFORE_VERSIONS["$name"]="$ver" fi done -fi +done # ── Step 1: git pull ──────────────────────────────────────────────────────── @@ -133,17 +135,19 @@ ok "Skills installed." info "Version diff:" CHANGES=false -for skill_dir in "$TARGET"/*/; do - name="$(basename "$skill_dir")" - vf="$skill_dir/SKILL.md" - if [[ -f "$vf" ]]; then - after="$(grep -oP '^version:\s*\K[0-9]+\.[0-9]+\.[0-9]+' "$vf" 2>/dev/null || echo '?')" - before="${BEFORE_VERSIONS[$name]:-new}" - if [[ "$before" != "$after" ]]; then - echo " $before → $after ($name)" - CHANGES=true +for target in "${TARGETS[@]}"; do + for skill_dir in "$target"/*/; do + name="$(basename "$skill_dir")" + vf="$skill_dir/SKILL.md" + if [[ -f "$vf" ]]; then + after="$(grep -oP '^version:\s*\K[0-9]+\.[0-9]+\.[0-9]+' "$vf" 2>/dev/null || echo '?')" + before="${BEFORE_VERSIONS[$name]:-new}" + if [[ "$before" != "$after" ]]; then + echo " $before → $after ($name)" + CHANGES=true + fi fi - fi + done done if ! $CHANGES; then echo " (no version changes)" @@ -154,8 +158,12 @@ fi NEW_SETUP=() for skill_dir in "$SKILLS_SRC"/*/; do name="$(basename "$skill_dir")" - if [[ "$name" == setup-* ]] && [[ ! -d "$TARGET/$name" ]]; then - NEW_SETUP+=("$name") + if [[ "$name" == setup-* ]]; then + missing=false + for target in "${TARGETS[@]}"; do + [[ ! -d "$target/$name" ]] && missing=true + done + $missing && NEW_SETUP+=("$name") fi done diff --git a/skills/update-skills/SKILL.md b/skills/update-skills/SKILL.md index 1843fe7..b1adeb0 100644 --- a/skills/update-skills/SKILL.md +++ b/skills/update-skills/SKILL.md @@ -1,7 +1,7 @@ --- name: update-skills author: ours -version: 0.2.0 +version: 0.2.1 description: > Full uplift cycle for an existing skills installation: git pull, conditionally rebuild MCP servers, install skills, show version diff, suggest @@ -54,6 +54,10 @@ If no MCP changes: > Skills updated. Start a new session (or `/clear`) for changes to take full effect. +Both agents pick up the new catalog on their next session: Claude Code reads +`~/.claude/skills` natively; pi reads `~/.agents/skills` natively (default +scan path — no settings needed). See "Install targets" below. + ### 4. Offer to run new setup skills If the script detected new `setup-*` skills that aren't yet installed: @@ -62,6 +66,20 @@ If the script detected new `setup-*` skills that aren't yet installed: The user decides — never auto-run setup skills. +## Install targets (dual, agent-neutral) + +The canon is the git repo — `skills//SKILL.md`. `install.sh` / `install.ps1` +install into **both** agent dirs (identical copies, synced by the script — no drift): + +| Target | Agent | Why | +|---|---|---| +| `~/.claude/skills` | Claude Code | Claude Code reads this natively; custom skill dirs are NOT configurable in Claude Code (only plugin `skillsPaths`) | +| `~/.agents/skills` | pi | pi reads this natively (default scan path); the agent-neutral `.agents/` namespace is our convention | + +`$CLAUDE_SKILLS_DIR` / `$env:CLAUDE_SKILLS_DIR` overrides only the claude target (for testing/CI). +pi must NOT have `~/.claude/skills` in its settings `skills` array — that was the old vendor-locked +setup; pi loads `~/.agents/skills` by default. Verify with `pi --help` / a fresh session listing skills. + ## What the scripts do The update scripts (`scripts/update.sh` and `scripts/update.ps1`) handle: @@ -76,7 +94,7 @@ The update scripts (`scripts/update.sh` and `scripts/update.ps1`) handle: ## Out of scope - **Greenfield bootstrap** — use `project-bootstrap` for first-time setup on a new machine. -- **`--prune` flag** — removing skills not in `skills/` from `~/.claude/skills/`. Tracked separately in `[install-ps1]`. +- **`--prune` flag** — removing skills not in `skills/` from the install targets (both `~/.claude/skills/` and `~/.agents/skills/`). Tracked separately in `[install-ps1]`. - **Hermes-side update** — `hermes-installer-skill` handles the Hermes/Linux factory path. - **Auto-run setup skills** — the user decides which new setup skills to configure.