python-skills 1.0.0__py3-none-any.whl

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 (105) hide show
  1. python_skills/__init__.py +10 -0
  2. python_skills/__main__.py +6 -0
  3. python_skills/adapters/__init__.py +48 -0
  4. python_skills/adapters/agent_skills.py +415 -0
  5. python_skills/adapters/aider_adapter.py +226 -0
  6. python_skills/adapters/base.py +153 -0
  7. python_skills/adapters/claude.py +474 -0
  8. python_skills/adapters/cline.py +332 -0
  9. python_skills/adapters/codex.py +24 -0
  10. python_skills/adapters/continue_adapter.py +198 -0
  11. python_skills/adapters/cursor.py +327 -0
  12. python_skills/adapters/gemini.py +26 -0
  13. python_skills/adapters/goose.py +26 -0
  14. python_skills/adapters/junie.py +25 -0
  15. python_skills/adapters/kiro.py +382 -0
  16. python_skills/adapters/opencode.py +27 -0
  17. python_skills/adapters/roo.py +25 -0
  18. python_skills/adapters/universal.py +203 -0
  19. python_skills/adapters/vscode.py +27 -0
  20. python_skills/adapters/windsurf.py +26 -0
  21. python_skills/adapters/zed.py +27 -0
  22. python_skills/cli.py +326 -0
  23. python_skills/config.py +160 -0
  24. python_skills/detector.py +152 -0
  25. python_skills/installer.py +163 -0
  26. python_skills/markers.py +115 -0
  27. python_skills/skills/__init__.py +14 -0
  28. python_skills/skills/loader.py +171 -0
  29. python_skills/skills/metadata.py +152 -0
  30. python_skills/skills/registry.py +101 -0
  31. python_skills/state.py +204 -0
  32. python_skills-1.0.0.dist-info/METADATA +99 -0
  33. python_skills-1.0.0.dist-info/RECORD +105 -0
  34. python_skills-1.0.0.dist-info/WHEEL +4 -0
  35. python_skills-1.0.0.dist-info/entry_points.txt +2 -0
  36. python_skills-1.0.0.dist-info/licenses/LICENSE +21 -0
  37. skills/advanced_python.md +239 -0
  38. skills/anti_patterns/index.md +406 -0
  39. skills/comprehensions.md +167 -0
  40. skills/control_flow.md +175 -0
  41. skills/data_structures.md +243 -0
  42. skills/debugging/common_bugs.md +222 -0
  43. skills/debugging/inspection_techniques.md +249 -0
  44. skills/debugging/root_cause.md +203 -0
  45. skills/engineering/application_logging.md +195 -0
  46. skills/engineering/cli_apps.md +207 -0
  47. skills/engineering/configuration.md +218 -0
  48. skills/engineering/database.md +240 -0
  49. skills/engineering/dependency_management.md +205 -0
  50. skills/engineering/http_clients.md +267 -0
  51. skills/engineering/modules_packages.md +211 -0
  52. skills/engineering/packaging.md +197 -0
  53. skills/engineering/project_structure.md +155 -0
  54. skills/engineering/pyproject_toml.md +302 -0
  55. skills/engineering/virtual_environments.md +206 -0
  56. skills/functions.md +244 -0
  57. skills/generation/async_concurrency.md +291 -0
  58. skills/generation/error_handling.md +276 -0
  59. skills/generation/protocols_generics.md +243 -0
  60. skills/generation/type_hints.md +290 -0
  61. skills/generation/validation_pipeline.md +274 -0
  62. skills/generation/workflow.md +190 -0
  63. skills/oop.md +228 -0
  64. skills/quality/abstractions.md +154 -0
  65. skills/quality/comments.md +177 -0
  66. skills/quality/documentation.md +176 -0
  67. skills/quality/duplication.md +137 -0
  68. skills/quality/maintainability.md +142 -0
  69. skills/quality/naming.md +171 -0
  70. skills/quality/quality_functions.md +245 -0
  71. skills/quality/readability.md +239 -0
  72. skills/quality/type_annotations.md +192 -0
  73. skills/refactoring/behavior_preservation.md +157 -0
  74. skills/refactoring/incremental.md +187 -0
  75. skills/refactoring/interface_stability.md +199 -0
  76. skills/refactoring/safe_refactoring.md +206 -0
  77. skills/security/auth_boundaries.md +200 -0
  78. skills/security/command_injection.md +207 -0
  79. skills/security/dependency_risks.md +282 -0
  80. skills/security/file_handling.md +156 -0
  81. skills/security/input_validation.md +190 -0
  82. skills/security/path_traversal.md +172 -0
  83. skills/security/secrets.md +171 -0
  84. skills/security/sql_injection.md +188 -0
  85. skills/security/unsafe_deserialization.md +164 -0
  86. skills/stdlib/argparse.md +178 -0
  87. skills/stdlib/collections.md +212 -0
  88. skills/stdlib/datetime.md +187 -0
  89. skills/stdlib/functools.md +238 -0
  90. skills/stdlib/itertools.md +183 -0
  91. skills/stdlib/json.md +162 -0
  92. skills/stdlib/logging.md +185 -0
  93. skills/stdlib/os_sys.md +184 -0
  94. skills/stdlib/pathlib.md +218 -0
  95. skills/stdlib/re.md +171 -0
  96. skills/stdlib/statistics.md +112 -0
  97. skills/stdlib/subprocess.md +211 -0
  98. skills/testing/async_tests.md +249 -0
  99. skills/testing/coverage.md +168 -0
  100. skills/testing/edge_cases.md +197 -0
  101. skills/testing/fixtures_mocks.md +203 -0
  102. skills/testing/organization.md +205 -0
  103. skills/testing/parameterized.md +174 -0
  104. skills/testing/regression_tests.md +165 -0
  105. skills/variables_types.md +107 -0
@@ -0,0 +1,10 @@
1
+ """Python Skills - Python engineering skills for AI coding agents."""
2
+
3
+ from importlib.metadata import version as _metadata_version
4
+
5
+ try:
6
+ __version__ = _metadata_version("python-skills")
7
+ except Exception:
8
+ __version__ = "0.0.0"
9
+
10
+ __all__ = ["__version__"]
@@ -0,0 +1,6 @@
1
+ """Main entry point for python-skills CLI."""
2
+
3
+ from python_skills.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,48 @@
1
+ """Adapters for python-skills target environments."""
2
+
3
+ from ..config import Target
4
+
5
+
6
+ def get_adapter(target: Target, project_root, skills_registry, lock_manager, config):
7
+ """Factory function to get adapter instance by target."""
8
+ from .aider_adapter import AiderAdapter
9
+ from .claude import ClaudeAdapter
10
+ from .cline import ClineAdapter
11
+ from .codex import CodexAdapter
12
+ from .continue_adapter import ContinueAdapter
13
+ from .cursor import CursorAdapter
14
+ from .gemini import GeminiAdapter
15
+ from .goose import GooseAdapter
16
+ from .junie import JunieAdapter
17
+ from .kiro import KiroAdapter
18
+ from .opencode import OpenCodeAdapter
19
+ from .roo import RooAdapter
20
+ from .universal import UniversalAdapter
21
+ from .vscode import VSCodeAdapter
22
+ from .windsurf import WindsurfAdapter
23
+ from .zed import ZedAdapter
24
+
25
+ adapters = {
26
+ Target.CLAUDE: ClaudeAdapter,
27
+ Target.CURSOR: CursorAdapter,
28
+ Target.KIRO: KiroAdapter,
29
+ Target.CLINE: ClineAdapter,
30
+ Target.UNIVERSAL: UniversalAdapter,
31
+ Target.OPENCODE: OpenCodeAdapter,
32
+ Target.WINDSURF: WindsurfAdapter,
33
+ Target.VSCODE: VSCodeAdapter,
34
+ Target.ROO: RooAdapter,
35
+ Target.GEMINI: GeminiAdapter,
36
+ Target.CODEX: CodexAdapter,
37
+ Target.JETBRAINS: JunieAdapter,
38
+ Target.GOOSE: GooseAdapter,
39
+ Target.ZED: ZedAdapter,
40
+ Target.CONTINUE: ContinueAdapter,
41
+ Target.AIDER: AiderAdapter,
42
+ }
43
+
44
+ adapter_cls = adapters.get(target)
45
+ if adapter_cls is None:
46
+ raise ValueError(f"Unknown target: {target}")
47
+
48
+ return adapter_cls(project_root, skills_registry, lock_manager, config)
@@ -0,0 +1,415 @@
1
+ """Shared Agent Skills adapter for targets supporting the Agent Skills specification.
2
+
3
+ The Agent Skills specification (https://agentskills.io) defines:
4
+ - SKILL.md with YAML frontmatter (name, description required)
5
+ - Directory-based skill structure with optional supporting files
6
+ - Progressive disclosure (metadata first, full content on demand)
7
+
8
+ This base class handles:
9
+ - Converting canonical flat .md skills to Agent Skills SKILL.md format
10
+ - Installing skills to target-specific discovery paths
11
+ - Sync, uninstall, and status operations
12
+ - Ownership tracking via markers
13
+ """
14
+
15
+ import shutil
16
+ from pathlib import Path
17
+
18
+ from ..adapters.base import (
19
+ AgentAdapter,
20
+ DetectionResult,
21
+ InstallResult,
22
+ StatusResult,
23
+ SyncResult,
24
+ UninstallResult,
25
+ )
26
+ from ..skills.metadata import SkillMetadata
27
+ from ..skills.registry import SkillRegistry
28
+ from ..state import LockManager
29
+
30
+
31
+ def _skill_name_to_slug(name: str) -> str:
32
+ """Convert a skill name to an Agent Skills compliant slug.
33
+
34
+ Agent Skills spec: lowercase alphanumeric with single hyphen separators.
35
+ """
36
+ slug = name.lower()
37
+ slug = slug.replace("_", "-")
38
+ # Remove consecutive hyphens
39
+ while "--" in slug:
40
+ slug = slug.replace("--", "-")
41
+ # Strip leading/trailing hyphens
42
+ slug = slug.strip("-")
43
+ # Ensure it's valid
44
+ if not slug:
45
+ slug = "skill"
46
+ return slug
47
+
48
+
49
+ def _generate_skill_md(skill: SkillMetadata, content: str) -> str:
50
+ """Generate Agent Skills compliant SKILL.md from a canonical skill.
51
+
52
+ Adds YAML frontmatter if missing, preserves existing content.
53
+ """
54
+ # Check if content already has frontmatter
55
+ if content.startswith("---"):
56
+ return content
57
+
58
+ # Generate frontmatter
59
+ slug = _skill_name_to_slug(skill.name)
60
+ description = skill.description or f"Python {skill.category} skill: {skill.name}"
61
+
62
+ # Truncate description to 1024 chars (Agent Skills spec limit)
63
+ if len(description) > 1024:
64
+ description = description[:1021] + "..."
65
+
66
+ frontmatter = f"""---
67
+ name: {slug}
68
+ description: {description}
69
+ ---"""
70
+
71
+ return f"{frontmatter}\n\n{content}"
72
+
73
+
74
+ class AgentSkillsAdapter(AgentAdapter):
75
+ """Base adapter for targets supporting the Agent Skills specification.
76
+
77
+ Subclasses define:
78
+ - target_name: Target enum value
79
+ - display_name: Human-readable name
80
+ - agent_skills_dir: Project-level path for SKILL.md files (e.g., ".agents/skills")
81
+ - global_skills_dir: Global path for SKILL.md files
82
+ - detection_project_markers: Files/dirs that indicate project integration
83
+ - detection_app_command: CLI command to check for app detection
84
+ """
85
+
86
+ # Subclasses must set these
87
+ target_name: str = ""
88
+ display_name: str = ""
89
+ agent_skills_dir: str = ".agents/skills"
90
+ global_skills_dir: str = ""
91
+ detection_project_markers: list[str] = []
92
+ detection_app_command: str | None = None
93
+ # Additional target-native skill directory (e.g., ".opencode/skills")
94
+ native_skills_dir: str | None = None
95
+
96
+ def __init__(self, project_root: Path, skills_registry: SkillRegistry,
97
+ lock_manager: LockManager, config):
98
+ super().__init__(project_root, skills_registry, lock_manager, config)
99
+
100
+ def _get_global_path(self) -> Path:
101
+ if self.global_skills_dir:
102
+ return Path.home() / self.global_skills_dir
103
+ return Path.home() / ".agents"
104
+
105
+ def detect(self) -> DetectionResult:
106
+ """Detect if this agent is available and configured."""
107
+ import shutil as _shutil
108
+
109
+ app_detected = False
110
+ if self.detection_app_command:
111
+ app_detected = _shutil.which(self.detection_app_command) is not None
112
+
113
+ project_config = any(
114
+ (self.project_root / marker).exists()
115
+ for marker in self.detection_project_markers
116
+ )
117
+
118
+ global_config = self._get_global_path().exists()
119
+
120
+ return DetectionResult(
121
+ application_detected=app_detected,
122
+ project_config_detected=project_config,
123
+ adapter_available=True,
124
+ details=f"App: {'yes' if app_detected else 'no'}, "
125
+ f"Project: {'yes' if project_config else 'no'}, "
126
+ f"Global: {'yes' if global_config else 'no'}",
127
+ )
128
+
129
+ def install(self, scope: str = "project", dry_run: bool = False) -> InstallResult:
130
+ """Install Agent Skills for this target."""
131
+ result = InstallResult(success=True, target=self.target_name, scope=scope)
132
+ target_path = self._get_scope_path(scope)
133
+
134
+ try:
135
+ # 1. Install skills to .agents/skills/
136
+ skills_dir = target_path / self.agent_skills_dir
137
+ created = self._install_skills_to_dir(skills_dir, dry_run)
138
+ result.files_created.extend(created)
139
+
140
+ # 2. If target has a native skills dir that differs, also install there
141
+ if self.native_skills_dir and self.native_skills_dir != self.agent_skills_dir:
142
+ native_dir = target_path / self.native_skills_dir
143
+ native_created = self._install_skills_to_dir(native_dir, dry_run)
144
+ result.files_created.extend(native_created)
145
+
146
+ # 3. Generate instructions file if the target supports it
147
+ instructions_created = self._install_instructions(target_path, dry_run)
148
+ result.files_created.extend(instructions_created)
149
+
150
+ if not dry_run:
151
+ self._update_lock_state(scope)
152
+
153
+ except Exception as e:
154
+ result.success = False
155
+ result.errors.append(str(e))
156
+
157
+ return result
158
+
159
+ def _install_skills_to_dir(self, skills_dir: Path, dry_run: bool) -> list[str]:
160
+ """Install all canonical skills to a target skills directory."""
161
+ created = []
162
+
163
+ if not self.skills_registry:
164
+ return created
165
+
166
+ skills_dir.mkdir(parents=True, exist_ok=True)
167
+
168
+ for skill in self.skills_registry.get_all_skills():
169
+ slug = _skill_name_to_slug(skill.name)
170
+ target_skill_dir = skills_dir / slug
171
+ target_skill_file = target_skill_dir / "SKILL.md"
172
+
173
+ if dry_run:
174
+ created.append(str(target_skill_file.relative_to(self.project_root)))
175
+ continue
176
+
177
+ # Get canonical content
178
+ content = self.skills_registry.get_skill_content(skill.name)
179
+ if not content:
180
+ continue
181
+
182
+ # Generate Agent Skills compliant SKILL.md
183
+ skill_md = _generate_skill_md(skill, content)
184
+
185
+ # Wrap with ownership markers for status/uninstall detection
186
+ skill_md = self._wrap_managed(skill_md, target_skill_file)
187
+
188
+ # Write to target
189
+ target_skill_dir.mkdir(parents=True, exist_ok=True)
190
+ target_skill_file.write_text(skill_md, encoding="utf-8")
191
+
192
+ created.append(str(target_skill_file.relative_to(self.project_root)))
193
+ self._record_file(self.target_name, "project", target_skill_file)
194
+
195
+ return created
196
+
197
+ def _install_instructions(self, target_path: Path, dry_run: bool) -> list[str]:
198
+ """Generate instructions file for the target. Override in subclasses."""
199
+ return []
200
+
201
+ def sync(self, scope: str = "project", dry_run: bool = False) -> SyncResult:
202
+ """Sync installed skills with canonical source."""
203
+ result = SyncResult(success=True, target=self.target_name, scope=scope)
204
+ target_path = self._get_scope_path(scope)
205
+
206
+ try:
207
+ skills_dir = target_path / self.agent_skills_dir
208
+
209
+ if not skills_dir.exists():
210
+ result.success = False
211
+ result.errors.append(f"No {self.agent_skills_dir} directory found")
212
+ return result
213
+
214
+ # Get current installed skills
215
+ installed = set()
216
+ for item in skills_dir.iterdir():
217
+ if item.is_dir() and (item / "SKILL.md").exists():
218
+ installed.add(item.name)
219
+
220
+ # Get canonical skills
221
+ canonical = set()
222
+ if self.skills_registry:
223
+ for skill in self.skills_registry.get_all_skills():
224
+ slug = _skill_name_to_slug(skill.name)
225
+ canonical.add(slug)
226
+
227
+ # Remove skills no longer in canonical
228
+ for skill_name in installed:
229
+ if skill_name not in canonical:
230
+ skill_dir = skills_dir / skill_name
231
+ if not dry_run:
232
+ shutil.rmtree(skill_dir)
233
+ result.removed.append(str(skill_dir.relative_to(self.project_root)))
234
+
235
+ # Add new skills
236
+ for slug in canonical:
237
+ if slug not in installed:
238
+ skill = self.skills_registry.get_skill(slug.replace("-", "_"))
239
+ if skill:
240
+ target_dir = skills_dir / slug
241
+ if not dry_run:
242
+ content = self.skills_registry.get_skill_content(skill.name)
243
+ if content:
244
+ skill_md = _generate_skill_md(skill, content)
245
+ skill_md = self._wrap_managed(skill_md, target_dir / "SKILL.md")
246
+ target_dir.mkdir(parents=True, exist_ok=True)
247
+ (target_dir / "SKILL.md").write_text(skill_md, encoding="utf-8")
248
+ result.added.append(str((target_dir / "SKILL.md").relative_to(self.project_root)))
249
+
250
+ # Update existing skills
251
+ for slug in canonical & installed:
252
+ skill = self.skills_registry.get_skill(slug.replace("-", "_"))
253
+ if skill:
254
+ skill_file = skills_dir / slug / "SKILL.md"
255
+ if skill_file.exists():
256
+ current = skill_file.read_text(encoding="utf-8")
257
+ content = self.skills_registry.get_skill_content(skill.name)
258
+ if content:
259
+ new_content = _generate_skill_md(skill, content)
260
+ # Wrap with markers for comparison and writing
261
+ new_content_wrapped = self._wrap_managed(new_content, skill_file)
262
+ # Strip markers from current to compare raw content
263
+ begin, end = self._get_markers(skill_file)
264
+ current_raw = current
265
+ if begin in current and end in current:
266
+ start = current.find(begin) + len(begin)
267
+ end_idx = current.find(end, start)
268
+ if end_idx != -1:
269
+ current_raw = current[start:end_idx].strip()
270
+ new_raw = new_content
271
+ if current_raw.strip() != new_raw.strip():
272
+ if not dry_run:
273
+ skill_file.write_text(new_content_wrapped, encoding="utf-8")
274
+ result.modified.append(str(skill_file.relative_to(self.project_root)))
275
+ else:
276
+ result.skipped.append(str(skill_file.relative_to(self.project_root)))
277
+
278
+ if not dry_run:
279
+ self._update_lock_state(scope)
280
+
281
+ except Exception as e:
282
+ result.success = False
283
+ result.errors.append(str(e))
284
+
285
+ return result
286
+
287
+ def uninstall(self, scope: str = "project", dry_run: bool = False) -> UninstallResult:
288
+ """Uninstall skills managed by python-skills.
289
+
290
+ For shared directories (.agents/skills/), only removes the vendor-specific
291
+ native directory. The shared directory is preserved for other consumers.
292
+ """
293
+ result = UninstallResult(success=True, target=self.target_name, scope=scope)
294
+ target_path = self._get_scope_path(scope)
295
+
296
+ try:
297
+ # Remove vendor-specific native skills dir (e.g., .opencode/skills/)
298
+ if self.native_skills_dir and self.native_skills_dir != self.agent_skills_dir:
299
+ native_dir = target_path / self.native_skills_dir
300
+ if native_dir.exists():
301
+ for item in native_dir.iterdir():
302
+ if item.is_dir():
303
+ skill_file = item / "SKILL.md"
304
+ if skill_file.exists():
305
+ content = skill_file.read_text(encoding="utf-8")
306
+ begin, _end = self._get_markers(skill_file)
307
+ if begin in content:
308
+ if not dry_run:
309
+ shutil.rmtree(item)
310
+ result.files_removed.append(
311
+ str(item.relative_to(self.project_root))
312
+ )
313
+
314
+ # For the shared .agents/skills/ dir: only remove if this target
315
+ # is the sole consumer. Check lock file for other consumers.
316
+ skills_dir = target_path / self.agent_skills_dir
317
+ if skills_dir.exists() and self.agent_skills_dir == ".agents/skills":
318
+ # Check if any other target still references .agents/skills/
319
+ other_consumers = self._count_other_consumers(scope)
320
+ if other_consumers > 0:
321
+ # Other consumers exist — don't touch shared dir
322
+ pass
323
+ else:
324
+ # No other consumers — safe to remove shared dir
325
+ for item in skills_dir.iterdir():
326
+ if item.is_dir():
327
+ skill_file = item / "SKILL.md"
328
+ if skill_file.exists():
329
+ content = skill_file.read_text(encoding="utf-8")
330
+ begin, _end = self._get_markers(skill_file)
331
+ if begin in content:
332
+ if not dry_run:
333
+ shutil.rmtree(item)
334
+ result.files_removed.append(
335
+ str(item.relative_to(self.project_root))
336
+ )
337
+ # Remove the shared directory if empty
338
+ if not dry_run and skills_dir.exists():
339
+ remaining = list(skills_dir.iterdir())
340
+ if not remaining:
341
+ skills_dir.rmdir()
342
+ elif skills_dir.exists():
343
+ # Non-shared agent_skills_dir — remove normally
344
+ for item in skills_dir.iterdir():
345
+ if item.is_dir():
346
+ skill_file = item / "SKILL.md"
347
+ if skill_file.exists():
348
+ content = skill_file.read_text(encoding="utf-8")
349
+ begin, _end = self._get_markers(skill_file)
350
+ if begin in content:
351
+ if not dry_run:
352
+ shutil.rmtree(item)
353
+ result.files_removed.append(
354
+ str(item.relative_to(self.project_root))
355
+ )
356
+
357
+ if not dry_run:
358
+ self._update_lock_state(scope)
359
+ # Clear target state after uninstall
360
+ self.lock_manager.clear_target(self.target_name)
361
+
362
+ except Exception as e:
363
+ result.success = False
364
+ result.errors.append(str(e))
365
+
366
+ return result
367
+
368
+ def _count_other_consumers(self, scope: str) -> int:
369
+ """Count how many other targets use the shared .agents/skills/ directory."""
370
+ # Known targets that consume .agents/skills/
371
+ shared_consumers = {
372
+ "opencode", "windsurf", "vscode", "gemini",
373
+ "roo", "codex", "goose", "jetbrains", "zed",
374
+ }
375
+ # Remove self from the count
376
+ other = shared_consumers - {self.target_name}
377
+ # Check which others are actually installed (have lock records)
378
+ count = 0
379
+ for consumer in other:
380
+ status = self.lock_manager.get_target_status(consumer)
381
+ if status and status.files:
382
+ count += 1
383
+ return count
384
+
385
+ def status(self, scope: str = "project") -> StatusResult:
386
+ """Get installation status."""
387
+ target_path = self._get_scope_path(scope)
388
+ installed = False
389
+ files = []
390
+
391
+ skills_dir = target_path / self.agent_skills_dir
392
+ if skills_dir.exists():
393
+ for item in skills_dir.iterdir():
394
+ if item.is_dir():
395
+ skill_file = item / "SKILL.md"
396
+ if skill_file.exists():
397
+ content = skill_file.read_text(encoding="utf-8")
398
+ begin, end = self._get_markers(skill_file)
399
+ if begin in content:
400
+ installed = True
401
+ files.append(str(skill_file.relative_to(self.project_root)))
402
+
403
+ return StatusResult(
404
+ target=self.target_name,
405
+ scope=scope,
406
+ installed=installed,
407
+ files=files,
408
+ version="2.0.0",
409
+ )
410
+
411
+ def _update_lock_state(self, scope: str) -> None:
412
+ """Update lock state after changes."""
413
+ skills_root = self.skills_registry.skills_root
414
+ if skills_root.exists():
415
+ self.lock_manager.update_canonical_hash(skills_root)