mirror of
https://github.com/alirezarezvani/claude-skills.git
synced 2026-09-07 08:26:02 +00:00
Same root cause as #587/#591 — Claude Code's runtime loader rejects array-form skills paths like ["./content-production", "./ai-seo", ...] even when each entry is a valid subdirectory containing SKILL.md. `claude plugin validate` accepts them but the loader does not. The proven canonical layout (used by self-improving-agent in #536): <plugin>/ ├── .claude-plugin/plugin.json skills: "./skills" └── skills/ ├── <skill-1>/SKILL.md ├── <skill-2>/SKILL.md └── ... Restructured 9 multi-skill domain plugins: - business-growth (4 skills moved) - c-level-advisor (28) - engineering (36) - engineering-team (32) - finance (2) - marketing-skill (43) - product-team (12) - project-management (8) - ra-qm-team (13) Also fixed standalone plugins that had root SKILL.md + ./skills/ subdir (agenthub, autoresearch-agent, executive-mentor, playwright-pro). The loader rejected them despite skills="./skills" because of the conflicting root SKILL.md (compare self-improving-agent which works because PR #536 moved its root SKILL.md). Moved each root SKILL.md into ./skills/<name>/. Restored standalone plugin folders to their original paths after the multi-skill restructure swept them into parent skills/ directories (marketplace.json source paths require original locations). Removed 7 orphaned marketplace entries that pointed to skill folders without their own plugin.json (content-creator, demand-gen, fullstack-engineer, aws-architect, product-manager, scrum-master, skill-security-auditor) — these were already non-functional. Bumped patch versions on every changed plugin and synced marketplace.json. Marketplace now lists 29 working plugins (down from 36). After merge: users run `/plugin marketplace update claude-code-skills` followed by `/plugin update --all` to pick up the working layout.
196 lines
6.7 KiB
Python
196 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""TC Init — Initialize TC tracking inside a project.
|
|
|
|
Creates docs/TC/ with tc_config.json, tc_registry.json, records/, and evidence/.
|
|
Idempotent: re-running on an already-initialized project reports current stats
|
|
and exits cleanly.
|
|
|
|
Usage:
|
|
python3 tc_init.py --project "My Project" --root .
|
|
python3 tc_init.py --project "My Project" --root /path/to/project --json
|
|
|
|
Exit codes:
|
|
0 = initialized OR already initialized
|
|
1 = warnings (e.g. partial state)
|
|
2 = bad CLI args / I/O error
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
VALID_STATUSES = ("planned", "in_progress", "blocked", "implemented", "tested", "deployed")
|
|
VALID_SCOPES = ("feature", "bugfix", "refactor", "infrastructure", "documentation", "hotfix", "enhancement")
|
|
VALID_PRIORITIES = ("critical", "high", "medium", "low")
|
|
|
|
|
|
def now_iso() -> str:
|
|
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
|
|
|
|
def detect_project_name(root: Path) -> str:
|
|
"""Try CLAUDE.md heading, package.json name, pyproject.toml name, then directory basename."""
|
|
claude_md = root / "CLAUDE.md"
|
|
if claude_md.exists():
|
|
try:
|
|
for line in claude_md.read_text(encoding="utf-8").splitlines():
|
|
line = line.strip()
|
|
if line.startswith("# "):
|
|
return line[2:].strip()
|
|
except OSError:
|
|
pass
|
|
|
|
pkg = root / "package.json"
|
|
if pkg.exists():
|
|
try:
|
|
data = json.loads(pkg.read_text(encoding="utf-8"))
|
|
name = data.get("name")
|
|
if isinstance(name, str) and name.strip():
|
|
return name.strip()
|
|
except (OSError, json.JSONDecodeError):
|
|
pass
|
|
|
|
pyproject = root / "pyproject.toml"
|
|
if pyproject.exists():
|
|
try:
|
|
for line in pyproject.read_text(encoding="utf-8").splitlines():
|
|
stripped = line.strip()
|
|
if stripped.startswith("name") and "=" in stripped:
|
|
value = stripped.split("=", 1)[1].strip().strip('"').strip("'")
|
|
if value:
|
|
return value
|
|
except OSError:
|
|
pass
|
|
|
|
return root.resolve().name
|
|
|
|
|
|
def build_config(project_name: str) -> dict:
|
|
return {
|
|
"project_name": project_name,
|
|
"tc_root": "docs/TC",
|
|
"created": now_iso(),
|
|
"auto_track": True,
|
|
"default_author": "Claude",
|
|
"categories": list(VALID_SCOPES),
|
|
}
|
|
|
|
|
|
def build_registry(project_name: str) -> dict:
|
|
return {
|
|
"project_name": project_name,
|
|
"created": now_iso(),
|
|
"updated": now_iso(),
|
|
"next_tc_number": 1,
|
|
"records": [],
|
|
"statistics": {
|
|
"total": 0,
|
|
"by_status": {s: 0 for s in VALID_STATUSES},
|
|
"by_scope": {s: 0 for s in VALID_SCOPES},
|
|
"by_priority": {p: 0 for p in VALID_PRIORITIES},
|
|
},
|
|
}
|
|
|
|
|
|
def write_json_atomic(path: Path, data: dict) -> None:
|
|
"""Write JSON to a temp file and rename, to avoid partial writes."""
|
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
tmp.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
tmp.replace(path)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Initialize TC tracking in a project.")
|
|
parser.add_argument("--root", default=".", help="Project root directory (default: current directory)")
|
|
parser.add_argument("--project", help="Project name (auto-detected if omitted)")
|
|
parser.add_argument("--force", action="store_true", help="Re-initialize even if config exists (preserves registry)")
|
|
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
args = parser.parse_args()
|
|
|
|
root = Path(args.root).resolve()
|
|
if not root.exists() or not root.is_dir():
|
|
msg = f"Project root does not exist or is not a directory: {root}"
|
|
print(json.dumps({"status": "error", "error": msg}) if args.json else f"ERROR: {msg}")
|
|
return 2
|
|
|
|
tc_dir = root / "docs" / "TC"
|
|
config_path = tc_dir / "tc_config.json"
|
|
registry_path = tc_dir / "tc_registry.json"
|
|
|
|
if config_path.exists() and not args.force:
|
|
try:
|
|
cfg = json.loads(config_path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError) as e:
|
|
msg = f"Existing tc_config.json is unreadable: {e}"
|
|
print(json.dumps({"status": "error", "error": msg}) if args.json else f"ERROR: {msg}")
|
|
return 2
|
|
|
|
stats = {}
|
|
if registry_path.exists():
|
|
try:
|
|
reg = json.loads(registry_path.read_text(encoding="utf-8"))
|
|
stats = reg.get("statistics", {})
|
|
except (OSError, json.JSONDecodeError):
|
|
stats = {}
|
|
|
|
result = {
|
|
"status": "already_initialized",
|
|
"project_name": cfg.get("project_name"),
|
|
"tc_root": str(tc_dir),
|
|
"statistics": stats,
|
|
}
|
|
if args.json:
|
|
print(json.dumps(result, indent=2))
|
|
else:
|
|
print(f"TC tracking already initialized for project '{cfg.get('project_name')}'.")
|
|
print(f" TC root: {tc_dir}")
|
|
if stats:
|
|
print(f" Total TCs: {stats.get('total', 0)}")
|
|
return 0
|
|
|
|
project_name = args.project or detect_project_name(root)
|
|
|
|
try:
|
|
tc_dir.mkdir(parents=True, exist_ok=True)
|
|
(tc_dir / "records").mkdir(exist_ok=True)
|
|
(tc_dir / "evidence").mkdir(exist_ok=True)
|
|
write_json_atomic(config_path, build_config(project_name))
|
|
if not registry_path.exists() or args.force:
|
|
write_json_atomic(registry_path, build_registry(project_name))
|
|
except OSError as e:
|
|
msg = f"Failed to create TC directories or files: {e}"
|
|
print(json.dumps({"status": "error", "error": msg}) if args.json else f"ERROR: {msg}")
|
|
return 2
|
|
|
|
result = {
|
|
"status": "initialized",
|
|
"project_name": project_name,
|
|
"tc_root": str(tc_dir),
|
|
"files_created": [
|
|
str(config_path),
|
|
str(registry_path),
|
|
str(tc_dir / "records"),
|
|
str(tc_dir / "evidence"),
|
|
],
|
|
}
|
|
if args.json:
|
|
print(json.dumps(result, indent=2))
|
|
else:
|
|
print(f"Initialized TC tracking for project '{project_name}'")
|
|
print(f" TC root: {tc_dir}")
|
|
print(f" Config: {config_path}")
|
|
print(f" Registry: {registry_path}")
|
|
print(f" Records: {tc_dir / 'records'}")
|
|
print(f" Evidence: {tc_dir / 'evidence'}")
|
|
print()
|
|
print("Next: python3 tc_create.py --root . --name <slug> --title <title> --scope <scope> ...")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|