Metadata-Version: 2.4
Name: s-skillkit
Version: 0.3.6
Summary: Локальное ядро управления навыками: install из git/локальной папки, junction/symlink-линковка в scope агента, .skillignore-фильтр, build manifest, project-манифест. 0 завязок на сеть/auth — stdlib + tomli-w + pathspec + platformdirs.
Author: Dmitry
License: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: pathspec>=0.12.0
Requires-Dist: platformdirs>=4.0
Requires-Dist: s-telemetrykit>=0.2.0
Requires-Dist: tomli-w>=1.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# s-skillkit

Локальное ядро управления навыками (skills): только локальная ФС-механика — ни
сети, ни auth. Из `librarykit` берётся ровно один модуль — `librarykit.proc`
(единый запуск подпроцессов: на Windows без всплывающих консольных окон и с
обязательным таймаутом); `uv`, `git` и `npm` кит зовёт только через него.

## Что внутри

- **`SkillStore`** (`skillkit.installer`) — материализация навыка в центральный
  стор и линковка (junction на Windows / symlink на POSIX) в scope агента:
  - `install_from_git` / `install_from_path` / `install` / `materialize`
  - `update` (инкрементальный sha-diff), `link_existing`, `migrate_scope`
  - `remove` (keep-local / purge / force)
  - P0 **stub-would-clobber guard** (stub не затирает живой контент);
  - **гейт принадлежности:** `remove` снимает ТОЛЬКО то, что кит ставил сам —
    каталог с нашей `_skill_meta.json` или ссылку в наш стор. Чужое (руками
    положенный навык в общем каталоге вроде `~/.agents/skills`) не удаляется, а
    попадает в `RemoveResult.skipped_foreign` + `.message`. Обойти можно лишь
    явно: `remove(..., force=True)` — на случай установок доисторических версий
    кита без меты.
- **`targets`** — `IAgentTarget` + `detect_agent` / `get_target` (Claude Code,
  Codex, OpenCode, Antigravity). Форма навыка везде одна: **каталог + `SKILL.md`**
  внутри (плоский `<имя>.md` не читает ни один агент). Раскладка сверена с живым
  опытом 2026-07-21 (навыки-пробы с кодовым словом в `description`):

  | агент | global | project | доказательность |
  |---|---|---|---|
  | `claude_code` | `~/.claude/skills/` | `<project>/.claude/skills/` | канон агента |
  | `codex` | `~/.agents/skills/` | `<project>/.agents/skills/` | **документация** (живьём не проверено: аккаунт 402) |
  | `opencode` | `~/.config/opencode/skills/` | `<project>/.opencode/skills/` | ОПЫТ |
  | `antigravity` | `~/.gemini/config/skills/` | **нет** (`ScopeUnsupported`) | ОПЫТ |

  Antigravity (`agy`) читает РОВНО один каталог; рабочие `.agents/skills` и
  `.gemini/skills` он не читает, поэтому project-scope у него — явная ошибка, а не
  тихая запись в никуда. Чтобы agy вообще видел навыки, в `~/.gemini/settings.json`
  нужен `experimental.skills = true` — этот общий с gemini-cli файл кит НЕ трогает.
  OpenCode читает ещё три места (`~/.opencode/skills`, `~/.agents/skills`,
  `<project>/.agents/skills`), но кит туда не пишет и не убирает: он туда никогда
  и не ставил, а `.agents` вдобавок общий с Codex — у каталога один владелец.

  Агент отдаёт и расположение своих конфигов: `mcp_config()` (MCP-серверы;
  Claude Code — `~/.claude.json`, Codex — `~/.codex/config.toml`, Antigravity —
  `~/.gemini/config/mcp_config.json`, ключ `mcpServers`), `hook_config()` (хуки).
  Смену раскладки закрывает механизм `legacy_dirnames` (ЧТЕНИЕ старых путей +
  `resolve_slug_dir` / `remove` / `migrate_scope`). Из встроенных заявлен у Codex:
  `~/.codex/skills` — канон до 0.3.0, откуда наши установки теперь находятся,
  снимаются и переезжают на новый канон. Соседи по старому каталогу не страдают
  благодаря гейту принадлежности. Объявлять сюда «просто читаемые агентом»
  каталоги нельзя — только те, куда кит РЕАЛЬНО ставил.
- **`hook_register`** — хуки навыка (`[[hooks]]`) в настройки агента:
  `register_hook` / `unregister_hook` / `list_hooks`. Идемпотентно, чужие хуки не
  задеваются, агент с неизвестным форматом → `status=manual` (не падение).
- **`manifest`** — `build_manifest` для publish + ридеры frontmatter /
  `_skill_meta.toml`.
- **`filter`** — `.skillignore` / `files`-allowlist фильтр (pathspec).
- **`project`** — проектный манифест `.skills-hub/skills.toml`.
- **`Paths`** — инъекция каталогов (`store_dir` / `config_dir` / `bin_dir`).
  `Paths.default()` — нативная раскладка через platformdirs.

## Инъекция вместо завязки на конфиг

Кит НЕ читает env/config. Каталоги передаются явным `Paths`; git-учётка —
инъектируемым `credential_resolver` (callable `url -> url`). Потребитель (CLI)
читает env-токен и собирает резолвер:

```python
from skillkit import SkillStore, Paths, get_target

paths = Paths.default()  # или из ClientConfig

def resolver(url: str) -> str:
    token = os.environ.get("SKILLS_HUB_GIT_TOKEN")
    if token and url.startswith("https://") and "@" not in url:
        return url.replace("https://", f"https://oauth2:{token}@", 1)
    return url

store = SkillStore(get_target(None), paths.store_dir, credential_resolver=resolver)
```

## Канон «супер-навыка» (навык + CLI + онбординг)

**Супер-навык** — навык, который несёт собственный CLI-инструмент. Канон нужен,
чтобы ОДИН и тот же навык одинаково ставился тремя путями: локальным
install-скриптом, `skillery install` из хаба и `skillery install --path/--from-git`.

### Структура репозитория

```
<repo>/
  pyproject.toml            # пакет CLI-инструмента (публикуется на PyPI)
  src/<tool>/               # исходники CLI
  install/install.sh|.ps1   # локальный установщик (см. ниже)
  skills/<name>/            # САМ НАВЫК — только это материализуется агенту
    SKILL.md                # инструкция для ИИ-агента
    _skill_meta.toml        # ДЕКЛАРАЦИЯ навыка (источник истины)
    references/  agents/
```

Навык может лежать и в корне репо (`SKILL.md` рядом с `_skill_meta.toml`) — тогда
подпапка не нужна. Если навык в подпапке, хаб хранит её в `Skill.skill_path`, и
манифест версии читается ИМЕННО оттуда (иначе tooling-поля теряются).

### `_skill_meta.toml` — полная декларация

```toml
description = "Atlas - local-first PM портфеля проектов и задач."
version = "0.3.0"
kind = "tooling"                 # prompt | comprehensive | tooling
tags = ["pm", "cli"]

# Release notes ЭТОЙ версии — «что изменилось». Опционально: нет поля — нет
# release notes. Хаб сохраняет текст у версии навыка и показывает на странице
# версий. Можно строкой (в т.ч. многострочной """...""") или списком пунктов.
changelog = [
    "Добавлен `atlas backlog` — пул сырых идей.",
    "Починен триаж забытых задач.",
]

# ВАЖНО (TOML): top-level массивы объявляются ДО заголовков [[...]] —
# иначе tomllib отнесёт ключ ВНУТРЬ таблицы, а не на верхний уровень.
runtime_dependencies = [
    { kind = "pip", spec = "atlas-pm==0.3.0" },   # чем ставится CLI
]

[[cli]]                          # какие команды навык приносит
command_name = "atlas"
entrypoint = "atlas.cli:app"

[[hooks]]                        # хук в настройки агента (ставится и СНИМАЕТСЯ)
event = "SessionStart"           # обязателен
command = "atlas session-hook"   # обязателен: встроенная команда CLI
matcher = "startup|resume"       # опц. — когда именно
timeout = 15                     # опц., секунды
status_message = "Atlas triage…" # опц.
marker = "session-hook"          # опц. — по чему узнаём СВОЙ хук (дефолт: command)

[onboarding]                     # что делать ПОСЛЕ установки
summary = "Локальный PM портфеля: проекты, задачи, эпики."
next_steps = [
  "atlas setup          # правила в CLAUDE.md/AGENTS.md + SessionStart-хук",
  "atlas task triage    # что в работе / застряло / забыто",
]
docs = "https://github.com/<owner>/<repo>#readme"
```

### Как ставится: две дороги, один источник истины

**Локальный install-скрипт** (`install/install.sh|.ps1`) — для человека «с нуля»:
ставит `uv` (если нет) → `uv tool install <пакет>` → запускает post-setup
инструмента (напр. `atlas setup`). Скрипты держим **ASCII-only**: их тянут через
`irm | iex` / `curl | sh`, и не-ASCII может побиться.

**`skillery install <skill>`** — та же логика, но декларативно:
1. материализует папку навыка агенту (global или project scope);
2. читает `_skill_meta.toml` **из установленного навыка** и ставит
   `runtime_dependencies` + регистрирует `cli[]`/`mcp[]`/`hooks[]`
   (`apply_tooling_artifacts`);
3. печатает `[onboarding]` — «что делать дальше».

`revert_tooling_artifacts` (disable/remove) снимает ровно эти артефакты — в том
числе хук: осиротевший хук продолжал бы звать удалённую команду на каждом старте
сессии агента. Чужие хуки и чужие настройки в файле не задеваются.

Источник истины для tooling — **декларация в самом навыке**, а не то, что доехало
в манифесте бандла хаба: у навыка в подпапке бандл может прийти без `cli`/
`runtime_dependencies`, поэтому инсталлятор до-читывает `_skill_meta.toml`.

### Детект: не навреди стороннему навыку

Установка CLI запускается **только по явной декларации** (`runtime_dependencies` /
`cli` в `_skill_meta.toml`). Эвристики вида «рядом лежит `pyproject.toml` — значит
надо поставить пакет» ЗАПРЕЩЕНЫ: сторонний навык часто живёт в чужом репо, и такая
догадка поставила бы левый пакет. Навык без `_skill_meta.toml` (просто `SKILL.md`)
материализуется как есть — ничего не выполняется и не ставится.

### Онбординг обязателен для супер-навыка

Навык, приносящий CLI, ОБЯЗАН объявить `[onboarding].next_steps`. Инсталлятор
печатает их сразу после установки, чтобы **ИИ-агент довёл настройку сам**, а не
оставлял пользователя с установленным, но ненастроенным инструментом. Контракт
вывода: в text-режиме — человекочитаемый список, в `--json` — структурные поля
(`next_steps`) в stderr, чтобы не засорять stdout с основным payload'ом.

## Зависимости

`tomli-w`, `pathspec`, `platformdirs`. requires-python `>=3.11`. MIT.
