Self-contained skill for tracking technical changes with structured JSON records, an enforced state machine, and a session handoff format that lets a new AI session resume work cleanly when a previous one expires. Includes: - 5 stdlib-only Python scripts (init, create, update, status, validator) all supporting --help and --json - 3 reference docs (lifecycle state machine, JSON schema, handoff format) - /tc dispatcher in commands/tc.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
197 lines
6.7 KiB
Python
197 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())
|