diff --git a/plugins/ansible/commands/ansible-scaffold.md b/plugins/ansible/commands/ansible-scaffold.md index bd86548..b9b63a6 100644 --- a/plugins/ansible/commands/ansible-scaffold.md +++ b/plugins/ansible/commands/ansible-scaffold.md @@ -53,8 +53,8 @@ Create the following structure: ### Step 3: Generate Configuration Files -1. **ansible.cfg** - Copy from `skills/infra/ansible-automation/examples/project-layout/ansible.cfg` -2. **requirements.yml** - Copy from `skills/infra/ansible-automation/examples/project-layout/requirements.yml` +1. **ansible.cfg** - Copy from `plugins/ansible/skills/ansible/examples/project-layout/ansible.cfg` +2. **requirements.yml** - Copy from `plugins/ansible/skills/ansible/examples/project-layout/requirements.yml` 3. **.ansible-lint**: ```yaml @@ -99,17 +99,17 @@ For each environment, create: 1. **hosts.yml** - Basic inventory structure 2. **group_vars/all.yml** - Environment-specific variables -Use `skills/infra/ansible-automation/examples/project-layout/inventories/` as templates. +Use `plugins/ansible/skills/ansible/examples/project-layout/inventories/` as templates. ### Step 5: Create Main Playbook -Create `playbooks/site.yml` from `skills/infra/ansible-automation/examples/playbooks/site.yml`. +Create `playbooks/site.yml` from `plugins/ansible/skills/ansible/examples/playbooks/site.yml`. ### Step 6: Add Baseline Role (if requested) If user selected baseline role: -1. Copy entire `skills/infra/ansible-automation/examples/baseline-role/` to `roles/baseline/` +1. Copy entire `plugins/ansible/skills/ansible/examples/baseline-role/` to `roles/baseline/` 2. Update variable prefix if project has specific naming ### Step 7: Add CI Pipeline (if requested) diff --git a/plugins/flux/commands/flux-refactor.md b/plugins/flux/commands/flux-refactor.md index f21d462..a8d6341 100644 --- a/plugins/flux/commands/flux-refactor.md +++ b/plugins/flux/commands/flux-refactor.md @@ -28,7 +28,7 @@ Use AskUserQuestion to confirm: Execute the analysis script: ```bash -python plugins/skillbox/skills/k8s/flux-gitops-refactor/scripts/analyze-structure.py /path/to/repo --format=markdown +python ${CLAUDE_PLUGIN_ROOT}/skills/refactor/scripts/analyze-structure.py /path/to/repo --format=markdown ``` Present analysis results to user showing: @@ -125,5 +125,5 @@ configMapGenerator: ## References -- Load `skills/k8s/flux-gitops-refactor/references/refactoring-patterns.md` for detailed patterns -- Load `skills/k8s/flux-gitops-scaffold/references/project-structure.md` for target structure +- See `skills/refactor/references/refactoring-patterns.md` for detailed patterns +- See `skills/scaffold/references/project-structure.md` for target structure diff --git a/plugins/flux/skills/scaffold/references/version-matrix.md b/plugins/flux/skills/scaffold/references/version-matrix.md index 8fdaee9..854881a 100644 --- a/plugins/flux/skills/scaffold/references/version-matrix.md +++ b/plugins/flux/skills/scaffold/references/version-matrix.md @@ -6,31 +6,41 @@ 1. SessionStart hook — reminds about Context7 for k8s projects 2. PreToolUse hook — checks versions in HelmRelease files -## Using Context7 for Latest Versions (REQUIRED) +## Getting Chart Versions -**Mandatory workflow** — fetch latest versions via Context7 before scaffolding: +**Recommended workflow** — verify versions before scaffolding: -### Step 1: Resolve Library ID +### Option 1: Context7 (Documentation) ``` Tool: resolve-library-id Parameter: libraryName="cert-manager" -``` - -### Step 2: Query Documentation for Version -``` Tool: query-docs Parameters: - libraryId: "/jetstack/cert-manager" - topic: "helm chart installation version" + libraryId: "/cert-manager/website" + topic: "helm chart install version" ``` +**Note:** Context7 returns documentation examples which may contain older versions. +Use as reference but verify current release. + +### Option 2: Helm Repo (Authoritative) + +```bash +helm repo add jetstack https://charts.jetstack.io +helm search repo jetstack/cert-manager --versions | head -5 +``` + +### Option 3: GitHub Releases + +Check the project's GitHub releases page for latest stable version. + ## Component Library IDs | Component | Library Name | Context7 ID | |-----------|--------------|-------------| -| cert-manager | cert-manager | /jetstack/cert-manager | +| cert-manager | cert-manager | /cert-manager/website | | ingress-nginx | ingress-nginx | /kubernetes/ingress-nginx | | external-secrets | external-secrets | /external-secrets/external-secrets | | external-dns | external-dns | /kubernetes-sigs/external-dns | diff --git a/plugins/go-dev/.claude-plugin/plugin.json b/plugins/go-dev/.claude-plugin/plugin.json index 64e25ed..2d816a8 100644 --- a/plugins/go-dev/.claude-plugin/plugin.json +++ b/plugins/go-dev/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "go-dev", "description": "Go development toolkit: project structure, services, repositories, handlers, OpenAPI code generation", - "version": "1.0.0", + "version": "1.1.1", "author": { "name": "11me" } diff --git a/plugins/go-dev/agents/project-init.md b/plugins/go-dev/agents/project-init.md index 82c78ed..7ecd112 100644 --- a/plugins/go-dev/agents/project-init.md +++ b/plugins/go-dev/agents/project-init.md @@ -143,7 +143,7 @@ This agent uses patterns from `go-development` skill: - **Errors**: Typed errors (EntityNotFound, ValidationFailed, StateConflict) - **Logging**: slog (default) or zap (for large projects) -See skill: `plugins/skillbox/skills/go/go-development/SKILL.md` +See skill: `plugins/go-dev/skills/go-development/SKILL.md` ## Scaffolding Process diff --git a/plugins/go-dev/hooks/hooks.json b/plugins/go-dev/hooks/hooks.json new file mode 100644 index 0000000..957e70a --- /dev/null +++ b/plugins/go-dev/hooks/hooks.json @@ -0,0 +1,39 @@ +{ + "hooks": { + "SessionStart": [ + { + "once": true, + "hooks": [ + { + "type": "command", + "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/session_context.py" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/pretool-go-get-check.py" + } + ] + }, + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/golangci-guard.py" + }, + { + "type": "command", + "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/pretool-serializable-check.py" + } + ] + } + ] + } +} diff --git a/plugins/workflow/scripts/hooks/golangci-guard.py b/plugins/go-dev/scripts/hooks/golangci-guard.py old mode 100644 new mode 100755 similarity index 60% rename from plugins/workflow/scripts/hooks/golangci-guard.py rename to plugins/go-dev/scripts/hooks/golangci-guard.py index bbe9414..7ee1b52 --- a/plugins/workflow/scripts/hooks/golangci-guard.py +++ b/plugins/go-dev/scripts/hooks/golangci-guard.py @@ -6,15 +6,32 @@ import sys from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent)) -from lib.response import allow, ask +# Import response utilities from workflow plugin (avoid duplication) +_workflow_lib = Path(__file__).parent.parent.parent.parent / "workflow/scripts/hooks/lib" +if _workflow_lib.exists(): + sys.path.insert(0, str(_workflow_lib)) + from response import allow, ask +else: + # Minimal fallback + def allow(event: str | None = None) -> None: + if event: + print(json.dumps({"hookSpecificOutput": {"hookEventName": event}})) + + def ask(reason: str, event: str = "PreToolUse", context: str | None = None) -> None: + output: dict = { + "hookSpecificOutput": { + "hookEventName": event, + "permissionDecision": "ask", + "permissionDecisionReason": reason, + } + } + if context: + output["hookSpecificOutput"]["additionalContext"] = context + print(json.dumps(output)) def is_inside_plugin_dir(file_path: str) -> bool: - """Check if file is inside the skillbox plugin directory. - - Uses CLAUDE_PLUGIN_ROOT env var provided by Claude Code. - """ + """Check if file is inside the skillbox plugin directory.""" plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", "") if not plugin_root: return False diff --git a/plugins/workflow/scripts/hooks/pretool-go-get-check.py b/plugins/go-dev/scripts/hooks/pretool-go-get-check.py similarity index 100% rename from plugins/workflow/scripts/hooks/pretool-go-get-check.py rename to plugins/go-dev/scripts/hooks/pretool-go-get-check.py diff --git a/plugins/workflow/scripts/hooks/pretool-serializable-check.py b/plugins/go-dev/scripts/hooks/pretool-serializable-check.py similarity index 100% rename from plugins/workflow/scripts/hooks/pretool-serializable-check.py rename to plugins/go-dev/scripts/hooks/pretool-serializable-check.py diff --git a/plugins/go-dev/scripts/hooks/session_context.py b/plugins/go-dev/scripts/hooks/session_context.py new file mode 100755 index 0000000..00d5ac7 --- /dev/null +++ b/plugins/go-dev/scripts/hooks/session_context.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""SessionStart hook: inject Go-specific guidelines. + +Only activates if go.mod exists in current directory. +""" + +import json +from pathlib import Path + + +def main() -> None: + cwd = Path.cwd() + + # Only activate for Go projects + if not (cwd / "go.mod").exists(): + return + + output_lines: list[str] = [] + + # Try to load GO-GUIDELINES.md from plugin + guidelines_path = Path(__file__).parent.parent.parent / "skills/go-development/GO-GUIDELINES.md" + if guidelines_path.exists(): + guidelines = guidelines_path.read_text().strip() + output_lines.append("## Go Guidelines") + output_lines.append("") + output_lines.append(guidelines) + output_lines.append("") + else: + # Fallback + output_lines.append("**Go Linter enforces:**") + output_lines.append("- `userID` not `userId` (var-naming)") + output_lines.append("- `any` not `interface{}` (use-any)") + output_lines.append("- No `common/helpers/utils/shared/misc` packages") + output_lines.append("") + output_lines.append("→ Run `golangci-lint run` after completing Go tasks") + output_lines.append("") + + output_lines.append("- Dependencies: always use `@latest` (hook enforces)") + output_lines.append( + "- Repository queries: use Filter pattern (`XxxFilter` + `getXxxCondition()`)" + ) + + if output_lines: + print( + json.dumps( + { + "hookSpecificOutput": { + "hookEventName": "SessionStart", + "additionalContext": "\n".join(output_lines), + } + } + ) + ) + + +if __name__ == "__main__": + main() diff --git a/plugins/harness/commands/harness-init.md b/plugins/harness/commands/harness-init.md index c41d869..d52ac48 100644 --- a/plugins/harness/commands/harness-init.md +++ b/plugins/harness/commands/harness-init.md @@ -80,7 +80,10 @@ Ask user for features to track. For each feature: Instead of using Write tool (blocked by guard hook), call initialization script: ```bash -python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/harness_init.py \ +# Find harness_init.py in workflow plugin +# CLAUDE_PLUGIN_ROOT points to current plugin (harness), workflow is sibling +PLUGIN_BASE=$(dirname ${CLAUDE_PLUGIN_ROOT}) +python3 ${PLUGIN_BASE}/workflow/scripts/hooks/harness_init.py \ --project-dir "$(pwd)" \ --features '[ {"id": "auth-login", "description": "User login with JWT"}, diff --git a/plugins/harness/commands/harness-supervisor.md b/plugins/harness/commands/harness-supervisor.md index 7c16861..2151ca3 100644 --- a/plugins/harness/commands/harness-supervisor.md +++ b/plugins/harness/commands/harness-supervisor.md @@ -184,5 +184,5 @@ This ensures: ## See Also -- `skills/core/agent-harness/SKILL.md` — Full documentation +- `agent-harness` skill — Full documentation - Anthropic article: "Effective Harnesses for Long-Running Agents" diff --git a/plugins/harness/skills/agent-harness/SKILL.md b/plugins/harness/skills/agent-harness/SKILL.md index 7edb2bc..07db8f3 100644 --- a/plugins/harness/skills/agent-harness/SKILL.md +++ b/plugins/harness/skills/agent-harness/SKILL.md @@ -278,11 +278,10 @@ Send a SINGLE message with MULTIPLE Task tool calls - they execute in parallel. ## Related Skills -- **workflow-orchestration** — Task-to-code traceability +- **unified-workflow** — Complete workflow from task to delivery - **beads-workflow** — Task tracking details - **tdd-enforcer** — Code-level testing - **context-engineering** — Context management -- **reliable-execution** — Persistence patterns ## Version History diff --git a/plugins/tdd/.claude-plugin/plugin.json b/plugins/tdd/.claude-plugin/plugin.json index 47b9cd9..b9f8e02 100644 --- a/plugins/tdd/.claude-plugin/plugin.json +++ b/plugins/tdd/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "tdd", "description": "Test-Driven Development: TDD workflow enforcement, test analysis, coaching for Go, TypeScript, Python, Rust", - "version": "1.0.0", + "version": "1.1.1", "author": { "name": "11me" } diff --git a/plugins/tdd/hooks/hooks.json b/plugins/tdd/hooks/hooks.json new file mode 100644 index 0000000..6bf353d --- /dev/null +++ b/plugins/tdd/hooks/hooks.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "SessionStart": [ + { + "once": true, + "hooks": [ + { + "type": "command", + "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/session_context.py" + } + ] + } + ] + } +} diff --git a/plugins/tdd/scripts/hooks/session_context.py b/plugins/tdd/scripts/hooks/session_context.py new file mode 100755 index 0000000..8775c44 --- /dev/null +++ b/plugins/tdd/scripts/hooks/session_context.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""SessionStart hook: inject TDD guidelines when TDD mode is enabled. + +TDD mode is detected via: +1. Explicit config in .claude/tdd-enforcer.local.md +2. Auto-detect by presence of test files +""" + +import json +import sys +from pathlib import Path + +# Import shared detection utilities from workflow plugin +# This avoids code duplication between plugins +_workflow_lib = Path(__file__).parent.parent.parent.parent / "workflow/scripts/hooks/lib" +if _workflow_lib.exists(): + sys.path.insert(0, str(_workflow_lib)) + from detector import detect_tdd_mode +else: + # Fallback if workflow plugin not available + def detect_tdd_mode(cwd: Path) -> dict[str, bool]: + """Minimal fallback detection.""" + result = {"enabled": False, "strict": False} + config_path = cwd / ".claude" / "tdd-enforcer.local.md" + if config_path.exists(): + try: + content = config_path.read_text(errors="ignore") + if "enabled: true" in content: + result["enabled"] = True + if "strictMode: true" in content: + result["strict"] = True + except OSError: + pass + return result + + +def main() -> None: + cwd = Path.cwd() + output_lines: list[str] = [] + + tdd_status = detect_tdd_mode(cwd) + if not tdd_status["enabled"]: + return + + # Try to load TDD-GUIDELINES.md from plugin + guidelines_path = Path(__file__).parent.parent.parent / "skills/tdd-enforcer/TDD-GUIDELINES.md" + + mode_label = "STRICT" if tdd_status["strict"] else "ACTIVE" + output_lines.append(f"## TDD Mode ({mode_label})") + output_lines.append("") + + if guidelines_path.exists(): + guidelines = guidelines_path.read_text().strip() + output_lines.append(guidelines) + else: + output_lines.append("**Cycle:** RED -> GREEN -> REFACTOR") + output_lines.append("1. Write failing test FIRST") + output_lines.append("2. Minimal implementation to pass") + output_lines.append("3. Refactor with tests passing") + + output_lines.append("") + + if output_lines: + print( + json.dumps( + { + "hookSpecificOutput": { + "hookEventName": "SessionStart", + "additionalContext": "\n".join(output_lines), + } + } + ) + ) + + +if __name__ == "__main__": + main() diff --git a/plugins/tdd/skills/tdd-enforcer/TDD-GUIDELINES.md b/plugins/tdd/skills/tdd-enforcer/TDD-GUIDELINES.md index bf17e04..e3f94f8 100644 --- a/plugins/tdd/skills/tdd-enforcer/TDD-GUIDELINES.md +++ b/plugins/tdd/skills/tdd-enforcer/TDD-GUIDELINES.md @@ -36,4 +36,4 @@ - Run tests after EVERY change - One test = one behavior -→ Full reference: `skills/core/tdd-enforcer/SKILL.md` +→ Full reference: tdd-enforcer skill diff --git a/plugins/workflow/.claude-plugin/plugin.json b/plugins/workflow/.claude-plugin/plugin.json index cdaa04d..a7e6857 100644 --- a/plugins/workflow/.claude-plugin/plugin.json +++ b/plugins/workflow/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "workflow", "description": "Personal workflow tools: beads task tracking, serena code navigation, conventional commits, discovery, context engineering", - "version": "1.1.0", + "version": "1.2.1", "author": { "name": "11me" } diff --git a/plugins/workflow/commands/workflow-scaffold.md b/plugins/workflow/commands/workflow-scaffold.md index 0fb64c6..1393680 100644 --- a/plugins/workflow/commands/workflow-scaffold.md +++ b/plugins/workflow/commands/workflow-scaffold.md @@ -29,7 +29,7 @@ Create the directory structure and SKILL.md file for a new Agent Skill. - Must be unique in target location 2. **Determine target directory**: - - `skillbox`: `plugins/skillbox/skills//` + - `workflow`: `plugins/workflow/skills//` - `personal`: `~/.claude/skills//` - `project`: `.claude/skills//` diff --git a/plugins/workflow/hooks/hooks.json b/plugins/workflow/hooks/hooks.json index 6e8ba41..fc67ac2 100644 --- a/plugins/workflow/hooks/hooks.json +++ b/plugins/workflow/hooks/hooks.json @@ -19,20 +19,7 @@ }, { "type": "command", - "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/skill_suggester.py" - }, - { - "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/memory_validator.sh" - } - ] - }, - { - "once": true, - "hooks": [ - { - "type": "command", - "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/session_context.py" + "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/memory_validator.py --quiet" } ] } @@ -52,15 +39,6 @@ } ], "PreToolUse": [ - { - "matcher": ".*", - "hooks": [ - { - "type": "command", - "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/tmux-emoji-ops.sh clear" - } - ] - }, { "matcher": "AskUserQuestion", "hooks": [ @@ -80,10 +58,6 @@ { "type": "command", "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/git-push-guard.py" - }, - { - "type": "command", - "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/pretool-go-get-check.py" } ] }, @@ -102,29 +76,11 @@ "type": "command", "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/pretool-features-guard.py" }, - { - "type": "command", - "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/golangci-guard.py" - } - ] - }, - { - "matcher": "Write|Edit", - "hooks": [ { "type": "command", "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/version-guard.py" } ] - }, - { - "matcher": "Write|Edit", - "hooks": [ - { - "type": "command", - "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/hooks/pretool-serializable-check.py" - } - ] } ], "PostToolUse": [ diff --git a/plugins/workflow/scripts/hooks/flow_check.py b/plugins/workflow/scripts/hooks/flow_check.py index fd8d940..39c8a25 100644 --- a/plugins/workflow/scripts/hooks/flow_check.py +++ b/plugins/workflow/scripts/hooks/flow_check.py @@ -45,11 +45,7 @@ def main() -> None: output_lines.append("Missing: .pre-commit-config.yaml") missing_count += 1 - # Check beads initialization - beads_dir = cwd / ".beads" - if not beads_dir.is_dir(): - output_lines.append("Missing: .beads/ (task tracking)") - missing_count += 1 + # Skip beads check - session_context.py handles beads status with ready tasks # Check tests if not has_tests(cwd): diff --git a/plugins/workflow/scripts/hooks/lib/bootstrap.py b/plugins/workflow/scripts/hooks/lib/bootstrap.py index 9a869bc..6f29814 100644 --- a/plugins/workflow/scripts/hooks/lib/bootstrap.py +++ b/plugins/workflow/scripts/hooks/lib/bootstrap.py @@ -122,7 +122,10 @@ def get_default_verification_command(project_dir: Path, feature_id: str) -> str ) if project_type and project_type.startswith("python"): - return f"pytest -k {feature_id.replace('-', '_')}" + # Convert feature-id to CamelCase for class/test pattern matching + # e.g., link-dotfiles → LinkDotfiles (matches TestLinkDotfiles class) + camel_case = _to_test_pattern(feature_id) + return f"pytest -k {camel_case}" return None diff --git a/plugins/workflow/scripts/hooks/lib/checkpoint.py b/plugins/workflow/scripts/hooks/lib/checkpoint.py index f1b72cb..ea62444 100644 --- a/plugins/workflow/scripts/hooks/lib/checkpoint.py +++ b/plugins/workflow/scripts/hooks/lib/checkpoint.py @@ -118,6 +118,32 @@ def _is_likely_file_path(s: str) -> bool: return True +def _cleanup_old_empty_checkpoints(memories_dir: Path, keep_recent_hours: int = 2) -> None: + """Remove old empty auto-checkpoint files. + + Args: + memories_dir: Path to memories directory. + keep_recent_hours: Keep checkpoints created within this many hours. + """ + import time + + now = time.time() + cutoff = now - (keep_recent_hours * 3600) + + for checkpoint in memories_dir.glob("auto-checkpoint-*.md"): + try: + # Skip recent files + if checkpoint.stat().st_mtime > cutoff: + continue + + # Check if empty (template not filled) + content = checkpoint.read_text() + if "(Add summary here)" in content and "(Add next steps here)" in content: + checkpoint.unlink() + except OSError: + continue + + def write_auto_checkpoint( checkpoint_type: str, modified_files: list[tuple[str, str]], @@ -125,6 +151,9 @@ def write_auto_checkpoint( ) -> Path: """Write auto-checkpoint to serena memories. + Uses 10-minute granularity to avoid creating too many checkpoints. + Cleans up old empty checkpoints automatically. + Args: checkpoint_type: "PreCompact" or "SessionEnd" modified_files: List of (path, action) tuples @@ -136,9 +165,21 @@ def write_auto_checkpoint( memories_dir = Path.cwd() / ".serena" / "memories" memories_dir.mkdir(parents=True, exist_ok=True) - timestamp = datetime.now().strftime("%Y-%m-%d-%H%M") + # Cleanup old empty checkpoints + _cleanup_old_empty_checkpoints(memories_dir) + + # Use 10-minute granularity (floor to nearest 10 min) + now = datetime.now() + minute_bucket = (now.minute // 10) * 10 + timestamp = now.strftime(f"%Y-%m-%d-%H{minute_bucket:02d}") filename = f"auto-checkpoint-{timestamp}.md" + # If checkpoint already exists for this bucket, update it instead + checkpoint_path = memories_dir / filename + if checkpoint_path.exists(): + # Return existing path - hook will prompt to update it + return checkpoint_path + # Build content lines = [ "# Auto Checkpoint", diff --git a/plugins/workflow/scripts/hooks/lib/detector.py b/plugins/workflow/scripts/hooks/lib/detector.py index cad38b8..63a7135 100644 --- a/plugins/workflow/scripts/hooks/lib/detector.py +++ b/plugins/workflow/scripts/hooks/lib/detector.py @@ -3,6 +3,28 @@ from pathlib import Path +def find_beads_dir(cwd: Path | None = None) -> Path | None: + """Find .beads directory by searching up the directory tree. + + This matches beads CLI behavior which auto-discovers the database + by walking up from cwd until finding .beads/ or hitting a boundary. + + Returns: + Path to .beads directory if found, None otherwise. + """ + cwd = cwd or Path.cwd() + + for parent in [cwd, *cwd.parents]: + beads_dir = parent / ".beads" + if beads_dir.is_dir(): + return beads_dir + # Stop at git root or home directory (boundaries) + if (parent / ".git").is_dir() or parent == Path.home(): + break + + return None + + def detect_project_types(cwd: Path | None = None) -> dict[str, bool]: """Detect project types based on marker files. @@ -22,7 +44,7 @@ def detect_project_types(cwd: Path | None = None) -> dict[str, bool]: or (cwd / "setup.py").exists(), "node": (cwd / "package.json").exists(), "rust": (cwd / "Cargo.toml").exists(), - "beads": (cwd / ".beads").is_dir(), + "beads": find_beads_dir(cwd) is not None, "serena": (cwd / ".serena").is_dir(), } @@ -66,7 +88,10 @@ def iter_yaml_files(base: Path, depth: int = 0) -> list[Path]: def has_tests(cwd: Path | None = None) -> bool: - """Check if project has tests.""" + """Check if project has tests. + + This is the canonical implementation - TDD plugin imports from here. + """ cwd = cwd or Path.cwd() # Check test directories @@ -74,10 +99,17 @@ def has_tests(cwd: Path | None = None) -> bool: if (cwd / test_dir).is_dir(): return True - # Check test file patterns - patterns = ["*_test.go", "*_test.py", "*.test.ts", "*.spec.ts", "test_*.py"] + # Use recursive glob to find test files in subdirectories + patterns = [ + "**/*_test.go", + "**/*_test.py", + "**/*.test.ts", + "**/*.spec.ts", + "**/test_*.py", + ] for pattern in patterns: - if list(cwd.glob(pattern)): + # Use next() with default to short-circuit on first match + if next(cwd.glob(pattern), None) is not None: return True return False @@ -108,6 +140,8 @@ def detect_python_framework(cwd: Path | None = None) -> str | None: def detect_tdd_mode(cwd: Path | None = None) -> dict[str, bool]: """Detect TDD mode status. + This is the canonical implementation - TDD plugin imports from here. + Priority: 1. Explicit config in .claude/tdd-enforcer.local.md 2. Auto-detect by presence of test files diff --git a/plugins/workflow/scripts/hooks/lib/features.py b/plugins/workflow/scripts/hooks/lib/features.py index 7948a19..d25e2bc 100644 --- a/plugins/workflow/scripts/hooks/lib/features.py +++ b/plugins/workflow/scripts/hooks/lib/features.py @@ -258,8 +258,8 @@ def _create_beads_task(description: str) -> str | None: if result.returncode != 0: return None - # Parse task ID from output (e.g., "Created: skills-abc123") - match = re.search(r"(?:Created|created):\s*(\S+)", result.stdout) + # Parse task ID from output (e.g., "Created issue: claude-skillbox-abc") + match = re.search(r"(?:Created|created)\s+issue:\s*(\S+)", result.stdout) if match: return match.group(1) diff --git a/plugins/workflow/scripts/hooks/lib/response.py b/plugins/workflow/scripts/hooks/lib/response.py index c2f08ae..35f8c3d 100644 --- a/plugins/workflow/scripts/hooks/lib/response.py +++ b/plugins/workflow/scripts/hooks/lib/response.py @@ -2,7 +2,12 @@ import json -from lib.notifier import notify +try: + from .notifier import notify +except ImportError: + # Fallback when imported from other plugins + def notify(title: str, message: str, urgency: str = "normal") -> None: + pass # Silent fallback def session_output(message: str) -> None: diff --git a/plugins/workflow/scripts/hooks/memory_validator.py b/plugins/workflow/scripts/hooks/memory_validator.py index 9bfe457..e843b02 100644 --- a/plugins/workflow/scripts/hooks/memory_validator.py +++ b/plugins/workflow/scripts/hooks/memory_validator.py @@ -313,6 +313,10 @@ def main(): """Main entry point.""" import argparse + # Add lib to path for session_output + sys.path.insert(0, str(Path(__file__).parent)) + from lib.response import session_output + parser = argparse.ArgumentParser(description="Validate memory consistency") parser.add_argument("--json", action="store_true", help="Output as JSON") parser.add_argument("--quiet", action="store_true", help="Only output if issues found") @@ -327,11 +331,12 @@ def main(): if args.json: output = format_json(result) + print(output) else: output = format_report(result) - - if output: - print(output) + if output: + # Use session_output for proper hook formatting + session_output(output) sys.exit(0 if result.is_valid else 1) diff --git a/plugins/workflow/scripts/hooks/memory_validator.sh b/plugins/workflow/scripts/hooks/memory_validator.sh deleted file mode 100755 index 9e8c5ce..0000000 --- a/plugins/workflow/scripts/hooks/memory_validator.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -# memory_validator.sh - SessionStart hook for memory consistency validation -# -# Validates that .serena/memories references match current codebase state. -# Reports stale references to skills, files, or other memories. -# -# Exit 0 = always (non-blocking, informational only) - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -VALIDATOR="$SCRIPT_DIR/memory_validator.py" - -# Check if validator exists -if [[ ! -f "$VALIDATOR" ]]; then - exit 0 -fi - -# Run validation (quiet mode - only output if issues found) -python3 "$VALIDATOR" --quiet 2>/dev/null || true - -exit 0 diff --git a/plugins/workflow/scripts/hooks/pretool-beads-guard.py b/plugins/workflow/scripts/hooks/pretool-beads-guard.py index c93693f..7256638 100644 --- a/plugins/workflow/scripts/hooks/pretool-beads-guard.py +++ b/plugins/workflow/scripts/hooks/pretool-beads-guard.py @@ -41,9 +41,11 @@ def is_workflow_active(project_dir: Path) -> bool: """Check if workflow mode is active (harness or beads).""" + from lib.detector import find_beads_dir + if is_harness_initialized(project_dir): return True - if (project_dir / ".beads").is_dir(): + if find_beads_dir(project_dir) is not None: return True return False @@ -69,11 +71,14 @@ def has_active_beads_task() -> tuple[bool, str | None]: allowing them. This prevents bypassing task tracking on errors. """ try: + # Let bd auto-discover the database - it handles worktrees and daemons + # Run from cwd which should be the main project directory result = subprocess.run( ["bd", "list", "--status", "in_progress", "--json"], capture_output=True, text=True, timeout=5, + cwd=Path.cwd(), # Explicit cwd for clarity ) if result.returncode != 0: # Fail safe: cannot verify task status, block operation diff --git a/plugins/workflow/scripts/hooks/prompt-guard.py b/plugins/workflow/scripts/hooks/prompt-guard.py index ebf69be..923b4fd 100755 --- a/plugins/workflow/scripts/hooks/prompt-guard.py +++ b/plugins/workflow/scripts/hooks/prompt-guard.py @@ -12,10 +12,17 @@ # Add lib to path sys.path.insert(0, str(Path(__file__).parent)) +from lib.detector import detect_project_types from lib.response import allow, block def main() -> None: + # Fast-path: skip for non-Helm/GitOps projects + cwd = Path.cwd() + types = detect_project_types(cwd) + if not types.get("helm") and not types.get("gitops"): + return # Exit silently - not a Helm project + try: data = json.load(sys.stdin) except json.JSONDecodeError: diff --git a/plugins/workflow/scripts/hooks/session_bootstrap.py b/plugins/workflow/scripts/hooks/session_bootstrap.py index 62f8f9f..b13c99f 100644 --- a/plugins/workflow/scripts/hooks/session_bootstrap.py +++ b/plugins/workflow/scripts/hooks/session_bootstrap.py @@ -68,7 +68,7 @@ def main() -> None: "2. Create `features.json` for tracking", "3. Record bootstrap state for future sessions", "", - "Or skip harness with `/init-workflow` for basic setup only.", + "Or skip harness with `/workflow-init` for basic setup only.", ] ) diff --git a/plugins/workflow/scripts/hooks/session_context.py b/plugins/workflow/scripts/hooks/session_context.py index 35d3c37..7d459cd 100644 --- a/plugins/workflow/scripts/hooks/session_context.py +++ b/plugins/workflow/scripts/hooks/session_context.py @@ -20,7 +20,7 @@ sys.path.insert(0, str(Path(__file__).parent)) from lib.bootstrap import is_harness_initialized -from lib.detector import detect_flux, detect_project_types, detect_tdd_mode +from lib.detector import detect_flux, detect_project_types from lib.response import session_output from lib.tmux_state import cleanup_stale_states from lib.tmux_state import save_state as save_tmux_state @@ -191,32 +191,6 @@ def main() -> None: output_lines.append("**Project type:** Go project") output_lines.append("") - # Inject mandatory Go guidelines - guidelines_path = ( - Path(__file__).parent.parent.parent / "skills/go/go-development/GO-GUIDELINES.md" - ) - if guidelines_path.exists(): - guidelines = guidelines_path.read_text().strip() - output_lines.append("## ⛔ MANDATORY - Follow these rules") - output_lines.append("") - output_lines.append(guidelines) - output_lines.append("") - else: - # Fallback if file not found - output_lines.append("**Linter enforces:**") - output_lines.append("- `userID` not `userId` (var-naming)") - output_lines.append("- `any` not `interface{}` (use-any)") - output_lines.append("- No `common/helpers/utils/shared/misc` packages") - output_lines.append("") - output_lines.append("→ Run `golangci-lint run` after completing Go tasks") - output_lines.append("") - - output_lines.append("- Dependencies: always use `@latest` (hook enforces)") - output_lines.append( - "- Repository queries: use Filter pattern (`XxxFilter` + `getXxxCondition()`)" - ) - output_lines.append("") - elif types["python"]: project_type = "python" output_lines.append("**Project type:** Python project") @@ -277,9 +251,11 @@ def main() -> None: output_lines.append(f"**Missing tools:** {' '.join(missing_tools)}") output_lines.append("") - # 4. Beads integration + # 4. Beads integration (search up directory tree like bd CLI does) + from lib.detector import find_beads_dir + bd_installed = check_command_exists("bd") - beads_initialized = (cwd / ".beads").is_dir() + beads_initialized = find_beads_dir(cwd) is not None if bd_installed and beads_initialized: bd_ready = get_beads_ready() @@ -294,32 +270,11 @@ def main() -> None: output_lines.append("→ Run `/init-project` for full setup, or `bd init` for beads only") output_lines.append("") - # 5. TDD mode detection and injection - tdd_status = detect_tdd_mode(cwd) - if tdd_status["enabled"]: - tdd_guidelines_path = ( - Path(__file__).parent.parent.parent / "skills/core/tdd-enforcer/TDD-GUIDELINES.md" - ) - mode_label = "STRICT" if tdd_status["strict"] else "ACTIVE" - output_lines.append(f"## 🧪 TDD Mode ({mode_label})") - output_lines.append("") - - if tdd_guidelines_path.exists(): - guidelines = tdd_guidelines_path.read_text().strip() - output_lines.append(guidelines) - else: - # Fallback if file not found - output_lines.append("**Cycle:** RED → GREEN → REFACTOR") - output_lines.append("1. Write failing test FIRST") - output_lines.append("2. Minimal implementation to pass") - output_lines.append("3. Refactor with tests passing") - output_lines.append("") - - # 5.5 Harness workflow rules (context reinforcement) + # 5. Harness workflow rules (context reinforcement) if is_harness_initialized(cwd): output_lines.extend(get_harness_rules()) - # 5.6 Task enforcement rules and warning (when beads is active) + # 6. Task enforcement rules and warning (when beads is active) if beads_initialized: output_lines.extend(get_task_enforcement_rules()) @@ -328,7 +283,7 @@ def main() -> None: if no_task_warning: output_lines.append(no_task_warning) - # 6. GitOps rules reminder + # 7. GitOps rules reminder if project_type in ("helm", "gitops"): output_lines.append("**Rules:**") output_lines.append("- No literal secrets in values.yaml (use ExternalSecret)") @@ -336,7 +291,7 @@ def main() -> None: output_lines.append("- Validate with /helm-validate before completing work") output_lines.append("") - # 7. K8s/Flux version enforcement via Context7 + # 8. K8s/Flux version enforcement via Context7 is_flux = detect_flux(cwd) if project_type in ("helm", "gitops") or is_flux: output_lines.append("**Flux/K8s project detected**") diff --git a/plugins/workflow/scripts/hooks/skill_suggester.py b/plugins/workflow/scripts/hooks/skill_suggester.py deleted file mode 100644 index 5e21868..0000000 --- a/plugins/workflow/scripts/hooks/skill_suggester.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 -"""SessionStart hook: auto-detect project context and suggest skills. - -Detects: -- Helm charts and GitOps repositories -- Kustomize overlays -- Flux GitOps -- Go, Python, Node.js, Rust projects -- Beads and Serena integration -""" - -import sys -from pathlib import Path - -# Add lib to path -sys.path.insert(0, str(Path(__file__).parent)) - -from lib.detector import detect_flux, detect_project_types, detect_python_framework -from lib.response import session_output - - -def main() -> None: - cwd = Path.cwd() - output_lines: list[str] = [] - suggested_skills: list[str] = [] - - types = detect_project_types(cwd) - - # Helm Chart Detection - if types["helm"]: - suggested_skills.append("skillbox-k8s:helm-chart-developer") - output_lines.append("**Helm Chart detected**") - - # GitOps Repository Detection - if types["gitops"]: - if "skillbox-k8s:helm-chart-developer" not in suggested_skills: - suggested_skills.append("skillbox-k8s:helm-chart-developer") - output_lines.append("**GitOps repository detected**") - output_lines.append(" Use: /helm-scaffold, /helm-validate") - - # Kustomize Detection - if types["kustomize"]: - if "skillbox-k8s:helm-chart-developer" not in suggested_skills: - suggested_skills.append("skillbox-k8s:helm-chart-developer") - output_lines.append("**Kustomize overlay detected**") - - # Flux Detection - if detect_flux(cwd): - output_lines.append("**Flux GitOps detected**") - - # Go Project Detection - if types["go"]: - output_lines.append("**Go project detected**") - # Future: suggested_skills.append("skillbox-golang:go-conventions") - - # Python Project Detection - if types["python"]: - output_lines.append("**Python project detected**") - # Future: suggested_skills.append("skillbox-python:python-conventions") - - framework = detect_python_framework(cwd) - if framework: - output_lines.append(f" Framework: {framework}") - - # Node.js Detection - if types["node"]: - output_lines.append("**Node.js project detected**") - # Future: suggested_skills.append("skillbox-node:node-conventions") - - # Rust Detection - if types["rust"]: - output_lines.append("**Rust project detected**") - # Future: suggested_skills.append("skillbox-rust:rust-conventions") - - # Beads Detection - if types["beads"]: - suggested_skills.append("skillbox:beads-workflow") - - # Serena Detection - if types["serena"]: - suggested_skills.append("skillbox:serena-navigation") - output_lines.append("**Serena project detected**") - output_lines.append(" Use semantic tools: find_symbol, get_symbols_overview") - - # Output suggestions - if output_lines: - output_lines.append("") - - if suggested_skills: - output_lines.append(f"**Suggested skills:** {', '.join(suggested_skills)}") - - if output_lines: - session_output("\n".join(output_lines)) - - -if __name__ == "__main__": - main() diff --git a/plugins/workflow/scripts/hooks/version-guard.py b/plugins/workflow/scripts/hooks/version-guard.py index dc4c2fa..2ed049e 100755 --- a/plugins/workflow/scripts/hooks/version-guard.py +++ b/plugins/workflow/scripts/hooks/version-guard.py @@ -16,7 +16,11 @@ def main() -> None: - data = json.load(sys.stdin) + try: + data = json.load(sys.stdin) + except json.JSONDecodeError: + return # Allow silently on invalid input + tool_input = data.get("tool_input", {}) # Only check Write/Edit to yaml files diff --git a/plugins/workflow/scripts/init-project.py b/plugins/workflow/scripts/init-project.py index 204fb23..ac409c4 100644 --- a/plugins/workflow/scripts/init-project.py +++ b/plugins/workflow/scripts/init-project.py @@ -36,6 +36,14 @@ def init_beads(cwd: Path) -> None: print(" ✓ .beads/ already exists") return + # Check if beads exists in parent directory (worktree scenario) + for parent in cwd.parents: + if (parent / ".beads").is_dir(): + print(f" ✓ Using beads from parent: {parent / '.beads'}") + return + if (parent / ".git").is_dir(): + break # Stop at git root + if not command_exists("bd"): print(" ⚠ beads CLI not found. Install: cargo install beads") return