Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
713 changes: 713 additions & 0 deletions .claude/commands/init-project.md

Large diffs are not rendered by default.

563 changes: 563 additions & 0 deletions .claude/commands/question.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .claude/hooks/log_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import sys
from pathlib import Path
from datetime import datetime
from hook_utils import sanitize_for_logging, sanitize_dict_for_logging
from hook_utils import sanitize_for_logging

# Configure logging to stderr with timestamp
logging.basicConfig(
Expand Down
12 changes: 6 additions & 6 deletions .claude/hooks/post_tool_use_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,19 @@ def format_python(file_path: Path) -> tuple[bool, str]:
Tuple of (success, message)
"""
try:
# Try Black first
# Try Black first (using python -m for better reliability)
result = subprocess.run(
["black", "--quiet", str(file_path)],
[sys.executable, "-m", "black", "--quiet", str(file_path)],
capture_output=True,
text=True,
timeout=3,
)

black_success = result.returncode == 0

# Try Ruff format
# Try Ruff format (using python -m for better reliability)
result = subprocess.run(
["ruff", "format", str(file_path)],
[sys.executable, "-m", "ruff", "format", str(file_path)],
capture_output=True,
text=True,
timeout=3,
Expand All @@ -88,8 +88,8 @@ def format_python(file_path: Path) -> tuple[bool, str]:
logger.warning("Python formatter timed out (3s)")
except subprocess.SubprocessError as e:
logger.warning(f"Formatter subprocess error: {type(e).__name__}")
except FileNotFoundError:
logger.debug("Black/Ruff not found in PATH")
except (FileNotFoundError, ModuleNotFoundError):
logger.debug("Black/Ruff not found as Python modules")

return False, "Formatters not available (Black/Ruff)"

Expand Down
1 change: 0 additions & 1 deletion .claude/hooks/post_tool_use_quality.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import os
import sys
import subprocess
from pathlib import Path

# Configure logging to stderr with timestamp
logging.basicConfig(
Expand Down
4 changes: 2 additions & 2 deletions .claude/hooks/pre_tool_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import sys
from pathlib import Path
from datetime import datetime
from hook_utils import sanitize_for_logging, sanitize_dict_for_logging
from hook_utils import sanitize_dict_for_logging

# Configure logging to stderr with timestamp
logging.basicConfig(
Expand Down Expand Up @@ -445,7 +445,7 @@ def main():
file=sys.stderr,
)
print(
f"Tip: Use Glob or Grep tools instead of bash find/grep for code search.",
"Tip: Use Glob or Grep tools instead of bash find/grep for code search.",
file=sys.stderr,
)
sys.exit(2)
Expand Down
56 changes: 52 additions & 4 deletions .claude/hooks/session_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,48 @@ def initialize_session_state(session_id: str, context: dict) -> None:
logger.warning(f"Unexpected error writing session file: {type(e).__name__}")


def ensure_memory_directory() -> None:
"""
Auto-create .claude/memory/ directory if it doesn't exist.
This ensures MCP Memory server has a place to store data.
"""
memory_dir = Path(".claude/memory")
if not memory_dir.exists():
try:
memory_dir.mkdir(parents=True, exist_ok=True)
logger.info("Created .claude/memory/ directory for MCP Memory")
except OSError as e:
logger.warning(f"Failed to create memory directory: {type(e).__name__}")


def check_environment_variables() -> dict:
"""
Check required environment variables and warn if missing.

Returns:
Dictionary with environment variable status
"""
status = {}

# Check ENRICHMENT_MODEL (required for pre-prompt enrichment)
enrichment_model = os.getenv("ENRICHMENT_MODEL")
if not enrichment_model:
status["ENRICHMENT_MODEL"] = "missing"
logger.warning(
"⚠️ ENRICHMENT_MODEL not set. Pre-prompt enrichment will not work."
)
logger.warning(" Set it with: export ENRICHMENT_MODEL=claude-3-5-haiku")
else:
status["ENRICHMENT_MODEL"] = enrichment_model
logger.info(f"βœ“ ENRICHMENT_MODEL: {enrichment_model}")

# Check MEMORY_FILE_PATH (optional, has default)
memory_path = os.getenv("MEMORY_FILE_PATH", ".claude/memory/memory.jsonl")
status["MEMORY_FILE_PATH"] = memory_path

return status


def log_session_start(session_id: str, context: dict) -> None:
"""
Log session start event.
Expand Down Expand Up @@ -238,6 +280,12 @@ def log_session_start(session_id: str, context: dict) -> None:
def main():
"""Hook entry point."""
try:
# Auto-create memory directory (new feature)
ensure_memory_directory()

# Check environment variables (new feature)
check_environment_variables()

# Read JSON input from stdin
input_data = json.load(sys.stdin)

Expand Down Expand Up @@ -279,10 +327,10 @@ def main():
# Also print user-friendly message to stderr (visible in console)
print("\n=== LAZY-DEV-FRAMEWORK Session Started ===", file=sys.stderr)
print(f"Session ID: {session_id}", file=sys.stderr)
print(f'PRD loaded: {"βœ“" if context["prd"] else "βœ—"}', file=sys.stderr)
print(f'TASKS loaded: {"βœ“" if context["tasks"] else "βœ—"}', file=sys.stderr)
print(f'Git branch: {context["branch"] or "N/A"}', file=sys.stderr)
print(f'Git history: {len(context["git_history"])} commits', file=sys.stderr)
print(f"PRD loaded: {'βœ“' if context['prd'] else 'βœ—'}", file=sys.stderr)
print(f"TASKS loaded: {'βœ“' if context['tasks'] else 'βœ—'}", file=sys.stderr)
print(f"Git branch: {context['branch'] or 'N/A'}", file=sys.stderr)
print(f"Git history: {len(context['git_history'])} commits", file=sys.stderr)
print("==========================================\n", file=sys.stderr)

sys.exit(0)
Expand Down
6 changes: 3 additions & 3 deletions .claude/hooks/stop.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,10 @@ def should_block_completion(

# Enforce minimum tests if configured
if min_tests is not None and test_info["test_count"] < min_tests:
return True, f'Minimum tests not met: {test_info["test_count"]}/{min_tests}'
return True, f"Minimum tests not met: {test_info['test_count']}/{min_tests}"

# Allow if tests passed
return False, f'All tests passing ({test_info["test_count"]} tests)'
return False, f"All tests passing ({test_info['test_count']} tests)"


def log_stop_event(
Expand Down Expand Up @@ -315,7 +315,7 @@ def main():
print(json.dumps(output))

# Print success message
print(f"\nβœ… All tests passing! Ready for review.\n", file=sys.stderr)
print("\nβœ… All tests passing! Ready for review.\n", file=sys.stderr)

sys.exit(0)

Expand Down
29 changes: 18 additions & 11 deletions .claude/hooks/user_prompt_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,13 +479,13 @@ def format_context_injection(git_context: dict, current_task: str | None) -> str

def log_prompt(session_id: str, input_data: dict) -> None:
"""
Log user prompt to logs/user_prompt_submit.json.
Log user prompt to .claude/data/logs/user_prompt_submit.json.

Args:
session_id: Session identifier
input_data: Full hook input data
"""
log_dir = Path("logs")
log_dir = Path(".claude/data/logs")
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / "user_prompt_submit.json"

Expand Down Expand Up @@ -574,15 +574,13 @@ def main():
context_injection = format_context_injection(git_context, current_task)

# Lightweight output-style selection
style_name = None
style_conf = 0.0
style_reason = ""
if os.getenv("LAZYDEV_DISABLE_STYLE") not in {"1", "true", "TRUE"}:
logger.debug("Selecting output style")
sel, conf, reason = choose_output_style(original_prompt)
if sel != "off":
style_name, style_conf, style_reason = sel, conf, reason
logger.info(f"Output style selected: {sel} (confidence: {conf:.2f}, reason: {reason})")
logger.info(
f"Output style selected: {sel} (confidence: {conf:.2f}, reason: {reason})"
)
style_block = (
f"\n\n## Output Style (Auto)\n\n{build_style_block(sel)}\n"
)
Expand Down Expand Up @@ -620,7 +618,9 @@ def main():
logger.debug("Detecting memory intent")
mi = detect_memory_intent(original_prompt)
if mi.get("enabled"):
logger.info(f"Memory graph skill activated: intents={mi['intents']}, mentions={len(mi['mentions'])}")
logger.info(
f"Memory graph skill activated: intents={mi['intents']}, mentions={len(mi['mentions'])}"
)
additional_parts.append(
build_memory_skill_block(mi["intents"], mi["mentions"])
)
Expand All @@ -630,7 +630,9 @@ def main():
logger.info("Memory skill disabled via LAZYDEV_DISABLE_MEMORY_SKILL")

additional_context = "".join(additional_parts)
logger.info(f"Total additional context length: {len(additional_context)} characters")
logger.info(
f"Total additional context length: {len(additional_context)} characters"
)

# Log the prompt
logger.debug("Logging prompt to file")
Expand Down Expand Up @@ -659,15 +661,20 @@ def main():

except json.JSONDecodeError as e:
# Handle JSON decode errors gracefully
logger.error(f"JSON decode error in user_prompt_submit: {type(e).__name__} - {e}")
logger.error(
f"JSON decode error in user_prompt_submit: {type(e).__name__} - {e}"
)
sys.exit(0)
except IOError as e:
# Handle file I/O errors gracefully
logger.error(f"I/O error in user_prompt_submit: {type(e).__name__} - {e}")
sys.exit(0)
except Exception as e:
# Handle any other errors gracefully
logger.error(f"Unexpected error in user_prompt_submit: {type(e).__name__} - {e}", exc_info=True)
logger.error(
f"Unexpected error in user_prompt_submit: {type(e).__name__} - {e}",
exc_info=True,
)
sys.exit(0)


Expand Down
46 changes: 23 additions & 23 deletions .claude/scripts/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,25 @@ def format_code(path: str, session_id: Optional[str] = None) -> int:
"script": "format.py",
"path": str(path_obj),
"session_id": session_id,
"steps": []
"steps": [],
}

print(f"[FORMAT] Running Black on {path}...")
start_time = datetime.utcnow()
black_result = subprocess.run(
["black", str(path_obj)],
capture_output=True,
text=True
["black", str(path_obj)], capture_output=True, text=True
)
black_duration = (datetime.utcnow() - start_time).total_seconds()

log_entry["steps"].append({
"tool": "black",
"duration_seconds": black_duration,
"exit_code": black_result.returncode,
"stdout": black_result.stdout,
"stderr": black_result.stderr
})
log_entry["steps"].append(
{
"tool": "black",
"duration_seconds": black_duration,
"exit_code": black_result.returncode,
"stdout": black_result.stdout,
"stderr": black_result.stderr,
}
)

if black_result.returncode != 0:
print(f"[ERROR] Black failed:\n{black_result.stderr}")
Expand All @@ -71,19 +71,19 @@ def format_code(path: str, session_id: Optional[str] = None) -> int:
print(f"[FORMAT] Running Ruff format on {path}...")
start_time = datetime.utcnow()
ruff_result = subprocess.run(
["ruff", "format", str(path_obj)],
capture_output=True,
text=True
["ruff", "format", str(path_obj)], capture_output=True, text=True
)
ruff_duration = (datetime.utcnow() - start_time).total_seconds()

log_entry["steps"].append({
"tool": "ruff",
"duration_seconds": ruff_duration,
"exit_code": ruff_result.returncode,
"stdout": ruff_result.stdout,
"stderr": ruff_result.stderr
})
log_entry["steps"].append(
{
"tool": "ruff",
"duration_seconds": ruff_duration,
"exit_code": ruff_result.returncode,
"stdout": ruff_result.stdout,
"stderr": ruff_result.stderr,
}
)

if ruff_result.returncode != 0:
print(f"[ERROR] Ruff format failed:\n{ruff_result.stderr}")
Expand Down Expand Up @@ -117,12 +117,12 @@ def _write_log(log_entry: dict, session_id: Optional[str]) -> None:
# Append to existing log
logs = []
if log_file.exists():
with open(log_file, 'r') as f:
with open(log_file, "r") as f:
logs = json.load(f)

logs.append(log_entry)

with open(log_file, 'w') as f:
with open(log_file, "w") as f:
json.dump(logs, f, indent=2)


Expand Down
Loading
Loading