skill-automation-package 0.2.0

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.
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ PACKAGE_ROOT = Path(__file__).resolve().parents[1]
10
+ ASSETS_ROOT = PACKAGE_ROOT / "assets"
11
+ TEMPLATES_ROOT = PACKAGE_ROOT / "templates"
12
+ PACKAGE_MANIFEST = PACKAGE_ROOT / "package.json"
13
+ EXECUTABLE_FILE_MODE = 0o755
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class PackageLayout:
18
+ name: str
19
+ version: str
20
+ managed_assets: tuple[Path, ...]
21
+ optional_assets: tuple[Path, ...]
22
+ executable_assets: frozenset[Path]
23
+
24
+ @property
25
+ def all_assets(self) -> list[Path]:
26
+ return [*self.managed_assets, *self.optional_assets]
27
+
28
+ def selected_assets(self, *, include_optional: bool) -> list[Path]:
29
+ if include_optional:
30
+ return self.all_assets
31
+ return list(self.managed_assets)
32
+
33
+
34
+ def load_package_layout(manifest_path: Path = PACKAGE_MANIFEST) -> PackageLayout:
35
+ payload = json.loads(manifest_path.read_text(encoding="utf-8"))
36
+ managed_assets = normalize_manifest_paths(
37
+ payload.get("managed_assets") or payload.get("assets", [])
38
+ )
39
+ optional_assets = normalize_manifest_paths(payload.get("optional_assets", []))
40
+ executable_assets = frozenset(normalize_manifest_paths(payload.get("executable_assets", [])))
41
+ return PackageLayout(
42
+ name=str(payload["name"]),
43
+ version=str(payload["version"]),
44
+ managed_assets=managed_assets,
45
+ optional_assets=optional_assets,
46
+ executable_assets=executable_assets,
47
+ )
48
+
49
+
50
+ def normalize_manifest_paths(values: object) -> tuple[Path, ...]:
51
+ if not isinstance(values, list):
52
+ return ()
53
+ normalized: list[Path] = []
54
+ seen: set[Path] = set()
55
+ for value in values:
56
+ if not isinstance(value, str):
57
+ continue
58
+ path = Path(value)
59
+ if path in seen:
60
+ continue
61
+ normalized.append(path)
62
+ seen.add(path)
63
+ return tuple(normalized)
64
+
65
+
66
+ def iter_asset_files(source_root: Path, asset_paths: list[Path]) -> list[tuple[Path, Path]]:
67
+ resolved_files: list[tuple[Path, Path]] = []
68
+ seen: set[Path] = set()
69
+ for relative_path in asset_paths:
70
+ source_path = source_root / relative_path
71
+ if not source_path.exists():
72
+ raise FileNotFoundError(f"Missing asset: {source_path}")
73
+ if source_path.is_dir():
74
+ for candidate in sorted(source_path.rglob("*")):
75
+ if not candidate.is_file():
76
+ continue
77
+ asset_file = candidate.relative_to(source_root)
78
+ if asset_file in seen:
79
+ continue
80
+ resolved_files.append((candidate, asset_file))
81
+ seen.add(asset_file)
82
+ continue
83
+ if relative_path in seen:
84
+ continue
85
+ resolved_files.append((source_path, relative_path))
86
+ seen.add(relative_path)
87
+ return resolved_files
88
+
89
+
90
+ def copy_assets(
91
+ *,
92
+ source_root: Path,
93
+ destination_root: Path,
94
+ asset_paths: list[Path],
95
+ executable_assets: frozenset[Path],
96
+ dry_run: bool = False,
97
+ ) -> list[Path]:
98
+ copied: list[Path] = []
99
+ for source_path, relative_path in iter_asset_files(source_root, asset_paths):
100
+ destination_path = destination_root / relative_path
101
+ copied.append(destination_path)
102
+ if dry_run:
103
+ continue
104
+ destination_path.parent.mkdir(parents=True, exist_ok=True)
105
+ shutil.copy2(source_path, destination_path)
106
+ if relative_path in executable_assets:
107
+ destination_path.chmod(EXECUTABLE_FILE_MODE)
108
+ return copied
@@ -0,0 +1,20 @@
1
+ <!-- SKILL-AUTOMATION:AGENTS:START -->
2
+ ## Repo Local Skill Automation
3
+
4
+ For any non-trivial task in this repository, start with:
5
+
6
+ ```bash
7
+ python3 .claude/tools/skill_agent.py auto "<task>" --json
8
+ ```
9
+
10
+ - If the action is `reuse`, open the matched skill under `.claude/skills/<skill-name>/SKILL.md` and follow it.
11
+ - If the action is `created`, use the generated skill immediately. The registry is already refreshed.
12
+ - If a step inside the chosen workflow becomes its own repeatable, non-trivial subtask, rerun `python3 .claude/tools/skill_agent.py auto "<sub-task>" --json` for that step, then resume the parent workflow.
13
+ - If you want a preview before writing files, run:
14
+
15
+ ```bash
16
+ python3 .claude/tools/skill_agent.py auto "<task>" --dry-run --json
17
+ ```
18
+
19
+ - Use `suggest`, `search`, `bootstrap`, `create`, and `refresh` only when the default `auto` flow is not enough.
20
+ <!-- SKILL-AUTOMATION:AGENTS:END -->
@@ -0,0 +1,11 @@
1
+ <!-- SKILL-AUTOMATION:CLAUDE:START -->
2
+ ## Local Skill Automation
3
+
4
+ - Store repo-local skills under `.claude/skills/<skill-name>/`.
5
+ - For any non-trivial task, start with `python3 .claude/tools/skill_agent.py auto "<task>" --json`.
6
+ - If `auto` returns `reuse`, open the matched skill and follow it.
7
+ - If `auto` returns `created`, use the generated skill immediately. The registry is updated automatically.
8
+ - If a step inside the current workflow becomes its own repeatable, non-trivial subtask, rerun `python3 .claude/tools/skill_agent.py auto "<sub-task>" --json` for that step before expanding the parent flow manually.
9
+ - To preview a generated skill before writing files, use `python3 .claude/tools/skill_agent.py auto "<task>" --dry-run --json`.
10
+ - Use `suggest`, `search`, `bootstrap`, `create`, and `refresh` only when the default `auto` flow is not enough.
11
+ <!-- SKILL-AUTOMATION:CLAUDE:END -->