s-skillkit 0.5.0__tar.gz → 0.6.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/.gitignore +5 -0
  2. s_skillkit-0.6.2/.gitlab-ci.yml +138 -0
  3. s_skillkit-0.5.0/.gitlab-ci.yml → s_skillkit-0.6.2/.gitlab-ci.yml.bak +27 -0
  4. s_skillkit-0.6.2/.importlinter +70 -0
  5. s_skillkit-0.5.0/.importlinter → s_skillkit-0.6.2/.importlinter.bak +3 -0
  6. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/CHANGELOG.md +53 -0
  7. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/PKG-INFO +5 -3
  8. s_skillkit-0.6.2/docs/agent-projections.md +76 -0
  9. s_skillkit-0.6.2/docs/safe-updates.md +41 -0
  10. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/pyproject.toml +20 -3
  11. s_skillkit-0.6.2/scripts/cycles_gate.py +397 -0
  12. s_skillkit-0.6.2/scripts/cycles_gate.py.bak +397 -0
  13. s_skillkit-0.6.2/scripts/lock_gate.py +228 -0
  14. s_skillkit-0.6.2/scripts/pins_gate.py +268 -0
  15. s_skillkit-0.6.2/scripts/pins_gate.py.bak +269 -0
  16. s_skillkit-0.6.2/scripts/release_guard.py +275 -0
  17. s_skillkit-0.6.2/scripts/source_guard.py +104 -0
  18. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/__init__.py +4 -0
  19. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer/__init__.py +189 -20
  20. s_skillkit-0.6.2/skillkit/installer/differ.py +168 -0
  21. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer/fetchers.py +6 -1
  22. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer/models.py +7 -0
  23. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer/ownership.py +35 -0
  24. s_skillkit-0.6.2/skillkit/installer/projections.py +338 -0
  25. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/mcp_register.py +4 -0
  26. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/targets/__init__.py +2 -0
  27. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/targets/antigravity.py +0 -22
  28. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/targets/base.py +0 -55
  29. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/targets/claude_code.py +15 -29
  30. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/targets/codex.py +14 -2
  31. s_skillkit-0.6.2/skillkit/targets/deepseek.py +18 -0
  32. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/targets/detect.py +3 -0
  33. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/conftest.py +7 -0
  34. s_skillkit-0.6.2/tests/test_agent_projections.py +424 -0
  35. s_skillkit-0.6.2/tests/test_deepseek_target.py +19 -0
  36. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_destructive_ownership_gate_2026_07_21.py +43 -3
  37. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_git_install.py +2 -1
  38. s_skillkit-0.6.2/tests/test_git_payload_bytes.py +49 -0
  39. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_installer.py +7 -3
  40. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_legacy_paths_no_orphans_2026_07_21.py +37 -7
  41. s_skillkit-0.6.2/tests/test_mcp_disabled_preference.py +40 -0
  42. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_remove_ownership_guard_2026_07_21.py +10 -2
  43. s_skillkit-0.6.2/tests/test_safe_update_gate.py +304 -0
  44. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_targets.py +2 -1
  45. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_w272_empty_manifest_guard.py +2 -1
  46. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/uv.lock +16 -3
  47. s_skillkit-0.5.0/skillkit/installer/differ.py +0 -44
  48. s_skillkit-0.5.0/skillkit/plugin_register.py +0 -260
  49. s_skillkit-0.5.0/tests/test_plugin_register.py +0 -163
  50. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/.skillgateignore +0 -0
  51. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/AGENTS.md +0 -0
  52. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/LICENSE +0 -0
  53. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/README.md +0 -0
  54. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/docs/adr/0000-template.md +0 -0
  55. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/docs/adr/0001-kanon-navyka-zhivet-v-skillkit.md +0 -0
  56. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/docs/adr/README.md +0 -0
  57. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/scripts/check_dist_leaks.py +0 -0
  58. /s_skillkit-0.5.0/scripts/release_guard.py → /s_skillkit-0.6.2/scripts/release_guard.py.bak +0 -0
  59. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/binary_installer.py +0 -0
  60. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/bootstrap.py +0 -0
  61. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/canon.py +0 -0
  62. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/collections.py +0 -0
  63. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/config.py +0 -0
  64. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/deps_installer.py +0 -0
  65. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/errors.py +0 -0
  66. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/filter.py +0 -0
  67. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/hook_register.py +0 -0
  68. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer/fsops.py +0 -0
  69. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer/meta.py +0 -0
  70. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer_cli/__init__.py +0 -0
  71. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer_cli/_branding.py +0 -0
  72. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer_cli/_detached.py +0 -0
  73. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer_cli/config.py +0 -0
  74. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer_cli/direct.py +0 -0
  75. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer_cli/doctor.py +0 -0
  76. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer_cli/install_scripts.py +0 -0
  77. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer_cli/publish.py +0 -0
  78. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer_cli/self_update.py +0 -0
  79. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/installer_cli/tokens.py +0 -0
  80. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/linker.py +0 -0
  81. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/manifest.py +0 -0
  82. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/path_store.py +0 -0
  83. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/paths.py +0 -0
  84. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/pkg_manager.py +0 -0
  85. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/proc.py +0 -0
  86. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/project.py +0 -0
  87. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/targets/opencode.py +0 -0
  88. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/telemetry.py +0 -0
  89. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/tooling.py +0 -0
  90. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/skillkit/uv_bootstrap.py +0 -0
  91. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_agent_skill_layouts_2026_07_21.py +0 -0
  92. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_artifact_ownership_2272.py +0 -0
  93. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_binary_installer.py +0 -0
  94. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_bootstrap.py +0 -0
  95. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_canon.py +0 -0
  96. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_claude_frontmatter.py +0 -0
  97. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_collections.py +0 -0
  98. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_config.py +0 -0
  99. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_deps_binary.py +0 -0
  100. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_deps_installer.py +0 -0
  101. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_errors.py +0 -0
  102. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_filter.py +0 -0
  103. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_hooks.py +0 -0
  104. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_installer_cli_direct.py +0 -0
  105. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_installer_cli_doctor.py +0 -0
  106. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_installer_cli_publish.py +0 -0
  107. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_installer_cli_smoke.py +0 -0
  108. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_installer_cli_tokens.py +0 -0
  109. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_manifest.py +0 -0
  110. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_mcp_register.py +0 -0
  111. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_no_raw_subprocess.py +0 -0
  112. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_packaging_lock.py +0 -0
  113. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_path_store.py +0 -0
  114. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_paths.py +0 -0
  115. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_pkg_manager.py +0 -0
  116. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_pkgsrc_not_in_agent_zone.py +0 -0
  117. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_proc.py +0 -0
  118. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_project.py +0 -0
  119. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_stub_guard.py +0 -0
  120. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_targets_public_names.py +0 -0
  121. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_telemetry_adapter.py +0 -0
  122. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_tooling.py +0 -0
  123. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_transactional_install.py +0 -0
  124. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_traversal_guard.py +0 -0
  125. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_typed_fields.py +0 -0
  126. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_uv_bootstrap.py +0 -0
  127. {s_skillkit-0.5.0 → s_skillkit-0.6.2}/tests/test_w268_subdir_skill.py +0 -0
@@ -48,3 +48,8 @@ NUL
48
48
 
49
49
  # git worktrees (агентские)
50
50
  .worktrees/
51
+
52
+ # Отметка локального прогона гейта 3 — свидетельство «на ЭТОЙ машине
53
+ # потребители были зелёные». В git ей не место: там она означала бы, что
54
+ # прогон был у всех, а он локальный по построению (раннер не видит соседей).
55
+ .kits-gate3.json
@@ -0,0 +1,138 @@
1
+ # СГЕНЕРИРОВАНО из devcontour/tools/kits/template/gitlab-ci.yml — не правь здесь.
2
+ # Правка идёт в шаблон, затем `python tools/kits/apply_template.py <кит>`.
3
+ # Сверка: `python tools/kits/apply_template.py <кит> --check` (код 1 при расхождении).
4
+ #
5
+ # Автопубликация на PyPI по семвер-тегу vX.Y.Z через Trusted Publishing (OIDC).
6
+ # Три стадии, и это не украшение: версии на PyPI неизменяемы, и шесть китов
7
+ # уезжали туда без единого теста (browserkit 0.0.18, netkit ≤0.0.12 — оба в бою).
8
+ # gates — тесты, линтер, слои, doctor: на каждый push и на тег
9
+ # release-guard — тег == версия во всех местах (pyproject/uv.lock/CHANGELOG)
10
+ # publish-pypi — только после двух первых; версия, уже лежащая на PyPI, не
11
+ # публикуется повторно (тег «догоняет» выпуск, а не роняет CI);
12
+ # после сборки и до обмена токена — check_dist_leaks.py
13
+ # (личные данные разработчика в собранном дистрибутиве)
14
+ # Токены НИГДЕ не хранятся: GitLab выдаёт короткоживущий OIDC-токен (PYPI_ID_TOKEN),
15
+ # он одноразово обменивается на PyPI API-токен. Издатель настраивается на pypi.org:
16
+ # Manage -> Publishing -> GitLab (namespace=S-kits, project=skillkit,
17
+ # top-level pipeline file=.gitlab-ci.yml, environment — пусто).
18
+ # Дистрибутив на PyPI: s-skillkit.
19
+ stages: [test, guard, publish]
20
+
21
+ .uv:
22
+ image: ghcr.io/astral-sh/uv:python3.12-bookworm
23
+
24
+ gates:
25
+ extends: .uv
26
+ stage: test
27
+ rules:
28
+ - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
29
+ - if: $CI_PIPELINE_SOURCE == "push"
30
+ script:
31
+ # dev-зависимости у китов объявлены двумя способами — extra `dev` и группа
32
+ # `[dependency-groups] dev`; --all-extras покрывает первое, группу добавляем
33
+ # только если она объявлена, иначе uv падает «Group dev is not defined».
34
+ # `--locked`: замок обязан соответствовать манифесту, а не тихо
35
+ # пересобираться под него. Без флага правка пина без `uv lock` доезжает до
36
+ # прода незамеченной — CI зелёный, а у потребителя приезжает старая версия.
37
+ - uv sync --locked --all-extras $(python3 -c "import tomllib;print('--group dev' if 'dev' in tomllib.load(open('pyproject.toml','rb')).get('dependency-groups',{}) else '')")
38
+ - uv run pytest tests/ -q
39
+ # Линтер по ВСЕМУ пакету, а не по списку файлов: сужение до «файлов волны 0»
40
+ # держалось девять китов и ни разу не было расширено — сужение приживается,
41
+ # расширение нет.
42
+ - uv run ruff check .
43
+ # Границы слоёв: контракт в .importlinter (ставится тем же шаблоном).
44
+ # — доп. пакеты через --with (пусто у большинства
45
+ # китов; adapterkit — пример кита, которому нужен сосед на прогоне
46
+ # lint-imports, см. apply_template.py --extra-with).
47
+ - uv run --with import-linter lint-imports
48
+ # Гейт 1, часть «согласованность локальных venv» (kitsctl doctor). Скрипт
49
+ # раздаётся шаблоном не всем: в раннере видно ОДИН репозиторий, соседних
50
+ # venv'ов там нет, — поэтому шаг условный и молча пропускается, когда
51
+ # инструмента у кита нет. Настоящее место doctor — локальный прогон перед
52
+ # тегом; см. docs/kits/SAFETY-NET.md, «Известные ограничения».
53
+ - 'if [ -f scripts/kitsctl.py ]; then python3 scripts/kitsctl.py doctor; else echo "kitsctl doctor — инструмента нет в репозитории, пропуск (см. SAFETY-NET.md)"; fi'
54
+ # Гейт 2: у каждой зависимости на кит есть верхняя граница. Значение границы
55
+ # здесь не вычисляется — в раннере соседних репозиториев нет, версий взять
56
+ # неоткуда; проверяется НАЛИЧИЕ. Без этого шага правило остаётся устной
57
+ # дисциплиной, которую забывают: портфель дожил до 248 незакрытых пинов
58
+ # ровно потому, что напоминать о нём было некому (#2800).
59
+ - 'if [ -f scripts/pins_gate.py ]; then python3 scripts/pins_gate.py --self-check --root .; else echo "гейт пинов — инструмента нет в репозитории, пропуск"; fi'
60
+ # Гейт колец: объявленное в `[tool.kit] lazy_deps` и правда грузится лениво.
61
+ # Колец здесь не увидеть (соседей в раннере нет), но видно главное: подъём
62
+ # такого импорта на верхний уровень замыкает кольцо ПРЯМО СЕЙЧАС, а
63
+ # выглядит безобидной уборкой («чего он тут в теле функции») — #2833.
64
+ - 'if [ -f scripts/cycles_gate.py ]; then python3 scripts/cycles_gate.py --self-check --root .; else echo "гейт колец — инструмента нет в репозитории, пропуск"; fi'
65
+ # Гейт замка: объявленная версия кита ДОЕЗЖАЕТ до окружения. `--locked`
66
+ # выше ловит другое — что замок не пересобран под манифест; а если у
67
+ # зависимости переопределён источник (`[tool.uv.sources]` = git/path), то
68
+ # спецификатор версии в разрешении не участвует вовсе, замок считается
69
+ # актуальным, и отставание не видит НИКТО. Так agentskit и issuekit
70
+ # держали clikit 0.1.6 при объявленном >=0.2.1 (#2921).
71
+ - 'if [ -f scripts/lock_gate.py ]; then python3 scripts/lock_gate.py --self-check --root .; else echo "гейт замка — инструмента нет в репозитории, пропуск"; fi'
72
+
73
+ release-guard:
74
+ extends: .uv
75
+ stage: guard
76
+ needs: [gates]
77
+ rules:
78
+ - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
79
+ script:
80
+ - python3 scripts/release_guard.py "$CI_COMMIT_TAG"
81
+
82
+ publish-pypi:
83
+ extends: .uv
84
+ stage: publish
85
+ needs: [release-guard]
86
+ rules:
87
+ - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
88
+ id_tokens:
89
+ PYPI_ID_TOKEN:
90
+ aud: pypi
91
+ script:
92
+ # КУДА уезжает выпуск, решает признак В САМОМ КИТЕ ([tool.kit] visibility),
93
+ # а не память человека и не отдельный список где-то в стороне. Приватный кит
94
+ # (готовый сервис, реализация чужой площадки) уходит в приватный индекс
95
+ # GitLab по ВСТРОЕННОМУ токену задания — новых секретов заводить не нужно.
96
+ # Умолчание — private: забыть пометить приватный кит дороже, чем забыть
97
+ # пометить открытый, и ошибаться надо в безопасную сторону.
98
+ - |
99
+ VIS=$(python3 -c "import tomllib;print((tomllib.load(open('pyproject.toml','rb')).get('tool') or {}).get('kit',{}).get('visibility','private'))")
100
+ echo "видимость кита: $VIS"
101
+ if [ "$VIS" != "public" ]; then
102
+ uv build
103
+ python3 scripts/check_dist_leaks.py dist/
104
+ uv publish --publish-url "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi" --username gitlab-ci-token --password "$CI_JOB_TOKEN"
105
+ echo "выпуск ушёл в приватный индекс проекта, на публичный PyPI НЕ уезжал"
106
+ exit 0
107
+ fi
108
+ - |
109
+ DIST=$(python3 -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['name'])")
110
+ VER="${CI_COMMIT_TAG#v}"
111
+ if python3 - "$DIST" "$VER" <<'PY'
112
+ import json, sys, urllib.request
113
+ dist, ver = sys.argv[1], sys.argv[2]
114
+ try:
115
+ data = json.load(urllib.request.urlopen(f"https://pypi.org/pypi/{dist}/json"))
116
+ except Exception:
117
+ sys.exit(1)
118
+ sys.exit(0 if ver in data.get("releases", {}) else 1)
119
+ PY
120
+ then echo "версия $VER уже на PyPI — публикацию пропускаем"; exit 0; fi
121
+ - uv build
122
+ # Последний рубеж перед необратимым шагом (версии на PyPI неизменяемы,
123
+ # yank — не удаление). Прецедент — leasekit: реальный email и домашний
124
+ # путь разработчика уехали в публичный индекс из тестовых фикстур. Ловит
125
+ # по классам, не по строкам; см. docs/kits/SAFETY-NET.md.
126
+ - python3 scripts/check_dist_leaks.py dist/
127
+ - |
128
+ export UV_PUBLISH_TOKEN=$(python3 - <<'PY'
129
+ import json, os, urllib.request
130
+ req = urllib.request.Request(
131
+ "https://pypi.org/_/oidc/mint-token",
132
+ data=json.dumps({"token": os.environ["PYPI_ID_TOKEN"]}).encode(),
133
+ headers={"Content-Type": "application/json"},
134
+ )
135
+ print(json.load(urllib.request.urlopen(req))["token"])
136
+ PY
137
+ )
138
+ - uv publish
@@ -48,6 +48,17 @@ gates:
48
48
  # инструмента у кита нет. Настоящее место doctor — локальный прогон перед
49
49
  # тегом; см. docs/kits/SAFETY-NET.md, «Известные ограничения».
50
50
  - 'if [ -f scripts/kitsctl.py ]; then python3 scripts/kitsctl.py doctor; else echo "kitsctl doctor — инструмента нет в репозитории, пропуск (см. SAFETY-NET.md)"; fi'
51
+ # Гейт 2: у каждой зависимости на кит есть верхняя граница. Значение границы
52
+ # здесь не вычисляется — в раннере соседних репозиториев нет, версий взять
53
+ # неоткуда; проверяется НАЛИЧИЕ. Без этого шага правило остаётся устной
54
+ # дисциплиной, которую забывают: портфель дожил до 248 незакрытых пинов
55
+ # ровно потому, что напоминать о нём было некому (#2800).
56
+ - 'if [ -f scripts/pins_gate.py ]; then python3 scripts/pins_gate.py --self-check --root .; else echo "гейт пинов — инструмента нет в репозитории, пропуск"; fi'
57
+ # Гейт колец: объявленное в `[tool.kit] lazy_deps` и правда грузится лениво.
58
+ # Колец здесь не увидеть (соседей в раннере нет), но видно главное: подъём
59
+ # такого импорта на верхний уровень замыкает кольцо ПРЯМО СЕЙЧАС, а
60
+ # выглядит безобидной уборкой («чего он тут в теле функции») — #2833.
61
+ - 'if [ -f scripts/cycles_gate.py ]; then python3 scripts/cycles_gate.py --self-check --root .; else echo "гейт колец — инструмента нет в репозитории, пропуск"; fi'
51
62
 
52
63
  release-guard:
53
64
  extends: .uv
@@ -68,6 +79,22 @@ publish-pypi:
68
79
  PYPI_ID_TOKEN:
69
80
  aud: pypi
70
81
  script:
82
+ # КУДА уезжает выпуск, решает признак В САМОМ КИТЕ ([tool.kit] visibility),
83
+ # а не память человека и не отдельный список где-то в стороне. Приватный кит
84
+ # (готовый сервис, реализация чужой площадки) уходит в приватный индекс
85
+ # GitLab по ВСТРОЕННОМУ токену задания — новых секретов заводить не нужно.
86
+ # Умолчание — private: забыть пометить приватный кит дороже, чем забыть
87
+ # пометить открытый, и ошибаться надо в безопасную сторону.
88
+ - |
89
+ VIS=$(python3 -c "import tomllib;print((tomllib.load(open('pyproject.toml','rb')).get('tool') or {}).get('kit',{}).get('visibility','private'))")
90
+ echo "видимость кита: $VIS"
91
+ if [ "$VIS" != "public" ]; then
92
+ uv build
93
+ python3 scripts/check_dist_leaks.py dist/
94
+ uv publish --publish-url "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi" --username gitlab-ci-token --password "$CI_JOB_TOKEN"
95
+ echo "выпуск ушёл в приватный индекс проекта, на публичный PyPI НЕ уезжал"
96
+ exit 0
97
+ fi
71
98
  - |
72
99
  DIST=$(python3 -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['name'])")
73
100
  VER="${CI_COMMIT_TAG#v}"
@@ -0,0 +1,70 @@
1
+ # СГЕНЕРИРОВАНО из devcontour/tools/kits/template/importlinter.ini — не правь
2
+ # здесь. Правка идёт в шаблон, затем `python tools/kits/apply_template.py <кит>`.
3
+ #
4
+ # Заготовка контракта слоёв. Запуск: `uv run --with import-linter lint-imports`.
5
+ #
6
+ # Контракт ниже — минимум, который годится ЛЮБОМУ киту и не требует ручной
7
+ # настройки: «skillkit не импортирует те киты экосистемы, которых нет в его
8
+ # объявленных зависимостях». Скрытая зависимость — это кит, который работает у
9
+ # автора (сосед стоит в venv) и разваливается у потребителя на первом импорте;
10
+ # именно так ломались pypi-publish и gemini-chat.
11
+ #
12
+ # Список forbidden_modules подставлен установщиком из реестра китов на момент
13
+ # установки: все известные пакеты китов МИНУС собственный МИНУС объявленные в
14
+ # pyproject зависимости. Появилась новая честная зависимость — объяви её в
15
+ # pyproject и переустанови шаблон, а не правь этот файл.
16
+ #
17
+ # Собственные контракты кита (layers/eager_forbidden/declared_dependencies,
18
+ # как у adapterkit) дописываются НИЖЕ отдельными секциями прямо в этом файле.
19
+ # Установщик сверяет файл целиком, поэтому `--check` после такой правки
20
+ # закономерно покажет расхождение с каноном — это ожидаемо и осознанно
21
+ # игнорируется в отчёте ревью, см. docs/kits/SAFETY-NET.md, «Что делать при
22
+ # расхождении» (п.2).
23
+
24
+ [importlinter]
25
+ # include_external_packages нужен: forbidden_modules перечисляет пакеты,
26
+ # внешние относительно root_packages.
27
+ include_external_packages = True
28
+ root_packages =
29
+ skillkit
30
+ exclude_type_checking_imports = True
31
+
32
+ [importlinter:contract:no-undeclared-kits]
33
+ name = skillkit не импортирует киты, которых нет в его зависимостях
34
+ type = forbidden
35
+ source_modules =
36
+ skillkit
37
+ forbidden_modules =
38
+ accountpoolkit
39
+ adapterkit
40
+ agentskit
41
+ aichatkit
42
+ authkit_client
43
+ authkit_contracts
44
+ authkit_server
45
+ b24kit
46
+ browserkit
47
+ chatkit
48
+ clientkit
49
+ corekit
50
+ emojikit
51
+ gitkit
52
+ installerkit
53
+ iokit
54
+ issuekit
55
+ leasekit
56
+ maxkit
57
+ modelkit
58
+ netkit
59
+ orchestrationkit
60
+ ormkit
61
+ persistkit
62
+ pluginkit
63
+ replicationkit
64
+ servicekit
65
+ sessionkit
66
+ socialkit
67
+ storagekit
68
+ totp_client
69
+ vaultkit
70
+ worker_service
@@ -56,8 +56,11 @@ forbidden_modules =
56
56
  maxkit
57
57
  modelkit
58
58
  netkit
59
+ orchestrationkit
59
60
  ormkit
60
61
  persistkit
62
+ pluginkit
63
+ replicationkit
61
64
  servicekit
62
65
  sessionkit
63
66
  socialkit
@@ -4,6 +4,59 @@
4
4
  [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/); версионирование —
5
5
  [SemVer](https://semver.org/lang/ru/).
6
6
 
7
+ ## [0.6.2] - 2026-10-14
8
+
9
+ ### Fixed
10
+
11
+ - Verify published payload bytes before hub updates and again before committing staged content; preserve local edits and refuse unknown baselines even with force.
12
+ - Preserve disabled managed MCP entries when updating their commands.
13
+ - Skip filesystem writes for verified current copy projections.
14
+ - Fetch Git releases without inherited newline conversion so Windows and Linux receive the same published bytes.
15
+
16
+ ### Added
17
+
18
+ - Native DeepSeek Harness skill installation roots with DSH_HOME support.
19
+ - Managed copy projections for agents running under Windows redirection restrictions, with conflict protection and explicit CODEX_HOME support.
20
+
21
+ ## [0.6.1] — 2026-09-09
22
+
23
+ ### Исправлено
24
+
25
+ - **`ClaudeCodeTarget.mcp_config()` больше не игнорирует подменённый `root`.**
26
+ Global-путь считался от `Path.home()`, поэтому прогон тестов кита писал
27
+ «чужой» образец `["важные", "данные", "пользователя"]` в НАСТОЯЩИЙ
28
+ `~/.claude.json` разработчика. Claude Code читал массив, превращал его в
29
+ ключи `"0"/"1"/"2"` и дописывал своё — список доверенных проектов пропадал, и
30
+ диалог «Trust this workspace?» возвращался на уже доверенных папках. На живой
31
+ машине это случилось дважды за час. Боевое поведение не меняется: по
32
+ умолчанию `root` = `~/.claude`, его родитель — тот же дом.
33
+ - **Сторож песочницы** (`test_podmenennyi_root_uvodit_vse_puti_v_pesochnitsu`):
34
+ при подменённом `root` ни один путь цели — ни `base_dir`, ни `slug_dir`, ни
35
+ дескрипторы MCP и хуков — не ведёт наружу. Проверяются ВСЕ цели разом:
36
+ соседний `hook_config` правило соблюдал и даже объяснял его в докстринге, так
37
+ что подвела именно помодульная бдительность.
38
+
39
+ ## [0.6.0] — 2026-09-09
40
+
41
+ ### Удалено (ломающее)
42
+
43
+ - **`skillkit.plugin_register`** — движок «плагин как единица поставки»
44
+ (манифест `plugin.json` + запись в реестр агента). Ноль потребителей во всём
45
+ портфеле, ноль публичной поверхности: нет ни в README, ни в `__all__`, ни в
46
+ `__init__`, ни в entry-points — причём групп entry-points у кита нет вовсе.
47
+ Написан 30.08.2026 одним коммитом и больше не трогался. Нишу занял живой
48
+ `pluginkit` с четырьмя потребителями (gateway, aichatkit, adapterkit, atlas):
49
+ формально он про python-пакеты, а не про `plugin.json` в каталоге агента, —
50
+ но второй незанятый движок в той же нише дороже, чем отсутствие обоих.
51
+ - **`PluginDescriptor` и методы `plugin_config`** в `targets/base.py`,
52
+ `targets/claude_code.py`, `targets/antigravity.py` — хвост, который держался
53
+ только на удалённом модуле. Убран тем же комплектом: иначе в ките осталась бы
54
+ раскладка плагинов, которую некому прочитать.
55
+
56
+ Ломающее для тех, кто аннотировал `PluginDescriptor` или переопределял
57
+ `plugin_config` в своей цели. Таких в портфеле нет (проверено грепом), но
58
+ поверхность целей публична, поэтому минор, а не патч (Atlas #2835).
59
+
7
60
  ## [0.5.0] — 2026-09-08
8
61
 
9
62
  ### Добавлено
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: s-skillkit
3
- Version: 0.5.0
3
+ Version: 0.6.2
4
4
  Summary: Локальное ядро управления навыками: install из git/локальной папки, junction/symlink-линковка в scope агента, .skillignore-фильтр, build manifest, project-манифест. 0 завязок на сеть/auth — stdlib + tomli-w + pathspec + platformdirs.
5
5
  Author: Dmitry
6
6
  License: MIT
@@ -8,11 +8,13 @@ License-File: LICENSE
8
8
  Requires-Python: >=3.11
9
9
  Requires-Dist: pathspec>=0.12.0
10
10
  Requires-Dist: platformdirs>=4.0
11
- Requires-Dist: s-telemetrykit>=0.2.0
11
+ Requires-Dist: s-telemetrykit<0.4,>=0.2.0
12
12
  Requires-Dist: tomli-w>=1.0
13
+ Requires-Dist: typer>=0.15
13
14
  Provides-Extra: installer-cli
14
- Requires-Dist: s-clikit>=0.1.6; extra == 'installer-cli'
15
+ Requires-Dist: s-clikit<0.3,>=0.1.6; extra == 'installer-cli'
15
16
  Requires-Dist: s-librarykit<0.9; extra == 'installer-cli'
17
+ Requires-Dist: s-skillgate<0.2,>=0.1.0; extra == 'installer-cli'
16
18
  Description-Content-Type: text/markdown
17
19
 
18
20
  # s-skillkit
@@ -0,0 +1,76 @@
1
+ # Readable skill projections
2
+
3
+ `SkillStore(target, store_dir, projection="auto")` accepts `auto`, `link`, and
4
+ `copy`. An explicit `InstallRequest.projection` overrides the installer default;
5
+ `None` inherits it. The legacy `install`, `install_from_path`,
6
+ `install_from_snapshot`, and `link_existing` keyword APIs accept the same override.
7
+ `materialize` still writes content to the store only.
8
+
9
+ `auto` preserves the existing link-first behavior for new installations. A skill
10
+ already installed as a managed copy remains a copy on later automatic updates.
11
+ `link` reports link creation failures without silently falling back to a copy.
12
+ `copy` creates a regular skill directory readable by processes that cannot follow
13
+ Windows junctions, including SSH descendants enforcing RedirectionGuard. It does
14
+ not change any Windows security policy.
15
+
16
+ Each copy has independent metadata under `_skill_meta.json`:
17
+
18
+ ```json
19
+ {
20
+ "projection": {
21
+ "kind": "copy",
22
+ "store_dir": "absolute/source/store/skill",
23
+ "source_revision": "source-commit",
24
+ "preserved_paths": ["_local/", "browser_profiles/"],
25
+ "baseline": {
26
+ "files": [{"path": "SKILL.md", "sha256": "sha256-of-original-payload"}],
27
+ "sha256": "sha256-of-canonical-files-index"
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ The baseline comes from a successful fresh materialization or a verified previous
34
+ release manifest. Existing files without a verifiable baseline are never adopted
35
+ as original content. Copy and source payload checks run before source updates;
36
+ the scope is checked again before the transactional swap. Local edits, missing
37
+ files, untracked payload, malformed baselines, runtime links, and unmanaged scope
38
+ entries cause an explicit refusal. `force` does not bypass these checks.
39
+
40
+ Copy payload excludes `.git`, `.pkgsrc`, `.venv`, bytecode caches, `.env`, and
41
+ declared preserved state. CLI dependencies remain in their independently managed
42
+ runtime. Existing ordinary preserved state in the scope is kept locally; it is
43
+ not copied from another source installation or included in a payload fingerprint.
44
+ Previously declared preserved paths remain protected when a later release omits
45
+ them. Runtime links are rejected instead of following their targets.
46
+
47
+ The new copy is staged beside the provider directory and activated through
48
+ `PathGuard.swap_in_scope`. Only the exact owned scope entry is renamed or removed;
49
+ link targets are retained. A failed activation restores the previous entry. If a
50
+ foreign entry appears during rollback, both the foreign entry and the original
51
+ backup are retained and the error identifies the recovery blocker. Store
52
+ materialization and scope activation are separate transactions: a failed scope
53
+ activation can leave the verified new source version in the store while the old
54
+ copy remains readable. Retrying updates the scope from that verified source.
55
+
56
+ Direct `link_existing` uses the same checks. `migrate_scope` retains readable
57
+ managed copy projections and reports them in `skipped_projection`. Legacy copy
58
+ migration requires ownership metadata and a prior release manifest whose raw
59
+ hashes match every payload file. Unknown bytes or namesake metadata are left
60
+ unchanged. Recovery requires a verified pinned previous hub manifest/bundle or an
61
+ explicit manual review and export; `force` cannot establish a baseline.
62
+
63
+ ## Codex runtime home
64
+
65
+ With an explicit absolute `CODEX_HOME`, the Codex target places global skills at
66
+ `$CODEX_HOME/skills` and global MCP settings at `$CODEX_HOME/config.toml`. It does
67
+ not claim unrelated sibling legacy directories. An injected `root=` continues to
68
+ override the environment for isolated callers and tests, and `config_root=` can
69
+ explicitly override the configuration path.
70
+
71
+ When `CODEX_HOME` is unset, the existing global `.agents/skills`, `.codex` config,
72
+ and managed legacy lookup remain unchanged. Project skills still use
73
+ `<project>/.agents/skills`, and project MCP configuration still uses
74
+ `<project>/.codex/config.toml`. Paths must be absolute in the current operating
75
+ system's syntax; a Windows drive path is not silently treated as a Linux relative
76
+ directory.
@@ -0,0 +1,41 @@
1
+ # Safe updates of hub-managed skills
2
+
3
+ Existing hub payload is checked against the SHA-256 inventory of its installed
4
+ published version before materialization and again before the staged version
5
+ replaces the store. The same check protects a managed copy in the agent scope.
6
+ An unchanged symlink or Windows junction to the store is a projection and is
7
+ checked through the store instead.
8
+
9
+ Modified or deleted files, file type changes, payload links, and untracked
10
+ payload that could be overwritten or filtered out stop the complete update.
11
+ An update also stops when its old inventory is missing, malformed, or cannot
12
+ be read. A new manifest cannot hide edited payload by declaring it preserved.
13
+ The `force` option does not bypass these checks.
14
+
15
+ For `SkillStore.install(...)`, the result has `skipped=True`, one of
16
+ `skip_reason="local-modifications"` or `"unverifiable-baseline"`, and safe
17
+ relative `conflict_paths`. A path of `.` denotes an unavailable baseline or
18
+ conflict at the installation root. No version, metadata, scope link, or payload
19
+ is replaced. Consumers must skip tooling and must report the reason to the
20
+ user. `SkillStore.materialize(...)` uses the same guard and returns a dictionary
21
+ with `skipped`, `skip_reason`, and `conflict_paths` on refusal; consumers should
22
+ check this before reading the normal success fields.
23
+
24
+ The comparison is byte-exact. LF/CRLF conversion is a modification and is not
25
+ silently normalized. Existing Git-based Windows installs with line-ending
26
+ conversion need a separately verified recovery process. A trusted old baseline
27
+ must come from the immutable installed version or its verified snapshot, never
28
+ from hashing the currently edited installation. New local edits should be
29
+ committed and released from an author's source repository rather than made in
30
+ the managed store.
31
+
32
+ Declared runtime state and installer/runtime artifacts (`.git`, `.pkgsrc`,
33
+ `.venv`, `__pycache__`, and `_skill_meta.json`) are outside this payload hash
34
+ check. Their existing lifecycle is unchanged. A malformed or missing baseline
35
+ is not repaired automatically. Generated stub-over-stub updates keep their
36
+ existing behavior because they have no published payload inventory.
37
+
38
+ This guard checks state again immediately before swapping staged content. It
39
+ does not provide a filesystem transaction against an uncoordinated writer
40
+ changing files during the final check/swap or against other concurrent
41
+ installers; callers still need to serialize installation work.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "s-skillkit"
3
- version = "0.5.0"
3
+ version = "0.6.2"
4
4
  description = "Локальное ядро управления навыками: install из git/локальной папки, junction/symlink-линковка в scope агента, .skillignore-фильтр, build manifest, project-манифест. 0 завязок на сеть/auth — stdlib + tomli-w + pathspec + platformdirs."
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -25,10 +25,15 @@ authors = [{ name = "Dmitry" }]
25
25
  # ответственность ИМЕННО этого кита. Зависимость лёгкая: telemetrykit
26
26
  # stdlib-only и своих зависимостей не имеет.
27
27
  dependencies = [
28
+ # Объявлено ПРЯМО (гейт зависимостей, #2834): кит импортирует пакет сам,
29
+ # а приезжал он транзитивно. Прямой импорт на чужой зависимости живёт до
30
+ # дня, когда сосед сменит свой набор, — и падает ModuleNotFoundError в
31
+ # бою при зелёной сборке.
32
+ "typer>=0.15",
28
33
  "tomli-w>=1.0",
29
34
  "pathspec>=0.12.0",
30
35
  "platformdirs>=4.0",
31
- "s-telemetrykit>=0.2.0",
36
+ "s-telemetrykit>=0.2.0,<0.4",
32
37
  ]
33
38
 
34
39
  #: installer_cli (волна 5, #2710) — бывший псевдокит installerkit, консолидирован
@@ -40,7 +45,11 @@ dependencies = [
40
45
  #: `pip install s-skillkit[installer_cli]`. Модуль и не импортируется eagerly
41
46
  #: из ``skillkit/__init__.py`` (тот же приём, что у ``skillkit.telemetry``).
42
47
  [project.optional-dependencies]
43
- installer_cli = ["s-clikit>=0.1.6", "s-librarykit<0.9"]
48
+ # s-skillgate объявлен здесь же: `installer_cli/publish.py` зовёт
49
+ # `from skillgate import run_gate` — секрет-скан перед публикацией.
50
+ # Импорт ленивый, но без пакета публикация падает ровно в момент,
51
+ # когда её и запускают (гейт #2834).
52
+ installer_cli = ["s-clikit>=0.1.6,<0.3", "s-librarykit<0.9", "s-skillgate>=0.1.0,<0.2"]
44
53
 
45
54
  #: Инструменты прогона объявлены ГРУППОЙ, а не extra, намеренно (skl#2545).
46
55
  #: extra ставится только по явному `--extra dev`; забыли флаг — pytest в
@@ -94,3 +103,11 @@ ignore = ["RUF001", "RUF002", "RUF003", "RUF022", "RUF059"]
94
103
  asyncio_mode = "auto"
95
104
  testpaths = ["tests"]
96
105
  pythonpath = ["."]
106
+
107
+ [tool.kit]
108
+ # ОТКРЫТЫЙ. Это вспомогательная механика: из неё собирают, но сама по себе она
109
+ # ничьей площадки и ничьего продукта не раскрывает.
110
+ #
111
+ # Признак читает шаг публикации: открытый кит уезжает на PyPI, приватный — в
112
+ # приватный индекс GitLab. Граница держится машиной, а не памятью.
113
+ visibility = "public"