From 2e50786cde78d06e91fd360410c4079e94f6b4c9 Mon Sep 17 00:00:00 2001 From: itdove Date: Mon, 6 Apr 2026 09:02:44 -0400 Subject: [PATCH 1/3] fix(release): support pyproject.toml for version management (#360) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The daf release command was failing because it expected version to be in setup.py, but the project migrated to pyproject.toml (PEP 517/518/621). Changes: - ReleaseManager now checks pyproject.toml first, falls back to setup.py - Update version in pyproject.toml when it exists - Skip empty setup.py files (modern format after migration) - Show correct file names in error messages and commits - Added 8 comprehensive tests for pyproject.toml support - Maintains full backward compatibility with setup.py-only projects The fix ensures daf release works for both modern (pyproject.toml) and legacy (setup.py) Python projects. Test Results: - All 8 new tests pass - All 3654 existing tests pass - Manual verification: daf release 2.2.0 --dry-run works correctly Fixes: itdove/devaiflow#360 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .wolf/OPENWOLF.md | 135 +++++ .wolf/anatomy.md | 718 ++++++++++++++++++++++++ .wolf/buglog.json | 17 + .wolf/cerebrum.md | 23 + .wolf/config.json | 73 +++ .wolf/cron-manifest.json | 97 ++++ .wolf/cron-state.json | 7 + .wolf/designqc-report.json | 6 + .wolf/hooks/_session.json | 12 + .wolf/hooks/package.json | 3 + .wolf/hooks/post-read.js | 69 +++ .wolf/hooks/post-write.js | 503 +++++++++++++++++ .wolf/hooks/pre-read.js | 80 +++ .wolf/hooks/pre-write.js | 121 ++++ .wolf/hooks/session-start.js | 77 +++ .wolf/hooks/shared.js | 614 ++++++++++++++++++++ .wolf/hooks/stop.js | 147 +++++ .wolf/identity.md | 9 + .wolf/memory.md | 38 ++ .wolf/reframe-frameworks.md | 597 ++++++++++++++++++++ .wolf/suggestions.json | 4 + .wolf/token-ledger.json | 21 + devflow/cli/commands/release_command.py | 9 +- devflow/release/manager.py | 91 ++- tests/test_release_manager_pyproject.py | 197 +++++++ 25 files changed, 3639 insertions(+), 29 deletions(-) create mode 100644 .wolf/OPENWOLF.md create mode 100644 .wolf/anatomy.md create mode 100644 .wolf/buglog.json create mode 100644 .wolf/cerebrum.md create mode 100644 .wolf/config.json create mode 100644 .wolf/cron-manifest.json create mode 100644 .wolf/cron-state.json create mode 100644 .wolf/designqc-report.json create mode 100644 .wolf/hooks/_session.json create mode 100644 .wolf/hooks/package.json create mode 100644 .wolf/hooks/post-read.js create mode 100644 .wolf/hooks/post-write.js create mode 100644 .wolf/hooks/pre-read.js create mode 100644 .wolf/hooks/pre-write.js create mode 100644 .wolf/hooks/session-start.js create mode 100644 .wolf/hooks/shared.js create mode 100644 .wolf/hooks/stop.js create mode 100644 .wolf/identity.md create mode 100644 .wolf/memory.md create mode 100644 .wolf/reframe-frameworks.md create mode 100644 .wolf/suggestions.json create mode 100644 .wolf/token-ledger.json create mode 100644 tests/test_release_manager_pyproject.py diff --git a/.wolf/OPENWOLF.md b/.wolf/OPENWOLF.md new file mode 100644 index 0000000..aae2a47 --- /dev/null +++ b/.wolf/OPENWOLF.md @@ -0,0 +1,135 @@ +# OpenWolf Operating Protocol + +You are working in an OpenWolf-managed project. These rules apply every turn. + +## File Navigation + +1. Check `.wolf/anatomy.md` BEFORE reading any file. It has a 2-3 line description and token estimate for every file in the project. +2. If the description in anatomy.md is sufficient for your task, do NOT read the full file. +3. If a file is not in anatomy.md, search with Grep/Glob, then update anatomy.md with the new entry. + +## Code Generation + +1. Before generating code, read `.wolf/cerebrum.md` and respect every entry. +2. Check the `## Do-Not-Repeat` section — these are past mistakes that must not recur. +3. Follow all conventions in `## Key Learnings` and `## User Preferences`. + +## After Actions + +1. After every significant action, append a one-line entry to `.wolf/memory.md`: + `| HH:MM | description | file(s) | outcome | ~tokens |` +2. After creating, deleting, or renaming files: update `.wolf/anatomy.md`. + +## Cerebrum Learning (MANDATORY — every session) + +OpenWolf's value comes from learning across sessions. You MUST update `.wolf/cerebrum.md` whenever you learn something useful. This is not optional. + +**Update `## User Preferences` when the user:** +- Corrects your approach ("no, do it this way instead") +- Expresses a style preference (naming, structure, formatting) +- Shows a preferred workflow or tool choice +- Rejects a suggestion — record what they preferred instead +- Asks for more/less detail, verbosity, explanation + +**Update `## Key Learnings` when you discover:** +- A project convention not obvious from the code (e.g., "tests go in __tests__/ not test/") +- A framework-specific pattern this project uses +- An API behavior that surprised you +- A dependency quirk or version constraint +- How modules connect or data flows through the system + +**Update `## Do-Not-Repeat` (with date) when:** +- The user corrects a mistake you made +- You try something that fails and find the right approach +- You discover a gotcha that would trip up a fresh session + +**Update `## Decision Log` when:** +- A significant architectural or technical choice is made +- The user explains why they chose approach A over B +- A trade-off is explicitly discussed + +**The bar is LOW.** If in doubt, add it. A cerebrum entry that's slightly redundant costs nothing. A missing entry means the next session repeats the same discovery process. + +## Bug Logging (MANDATORY) + +**Log a bug to `.wolf/buglog.json` whenever ANY of these happen:** +- The user reports an error, bug, or problem +- A test fails or a command produces an error +- You fix something that was broken +- You edit a file more than twice to get it right +- An import, module, or dependency is missing or wrong +- A runtime error, type error, or syntax error occurs +- A build or lint command fails +- A feature doesn't work as expected +- You change error handling, try/catch blocks, or validation logic +- The user says something "doesn't work", "is broken", or "shows wrong X" + +**Before fixing:** Read `.wolf/buglog.json` first — the fix may already be known. + +**After fixing:** ALWAYS append to `.wolf/buglog.json` with this structure: +```json +{ + "id": "bug-NNN", + "timestamp": "ISO date", + "error_message": "exact error or user complaint", + "file": "file that was fixed", + "root_cause": "why it broke", + "fix": "what you changed to fix it", + "tags": ["relevant", "keywords"], + "related_bugs": [], + "occurrences": 1, + "last_seen": "ISO date" +} +``` + +**The threshold is LOW.** When in doubt, log it. A false positive in the bug log costs nothing. A missed bug means repeating the same mistake later. + +## Token Discipline + +- Never re-read a file already read this session unless it was modified since. +- Prefer anatomy.md descriptions over full file reads when possible. +- Prefer targeted Grep over full file reads when searching for specific code. +- If appending to a file, do not read the entire file first. + +## Design QC + +When the user asks you to check, evaluate, or improve the design/UI of their app: + +1. Run `openwolf designqc` via Bash to capture screenshots. + - The command auto-detects a running dev server, or starts one from package.json if needed + - Use `--url ` only if auto-detection fails + - The command saves compressed JPEG screenshots to `.wolf/designqc-captures/` + - Full pages are captured as sectioned viewport-height images (top, section2, ..., bottom) +2. Read the captured screenshot images from `.wolf/designqc-captures/` using the Read tool. +3. Evaluate the design against modern standards (Shadcn UI, Tailwind, clean React patterns): + - Spacing and whitespace consistency + - Typography hierarchy and readability + - Color contrast and accessibility (WCAG) + - Visual hierarchy and focal points + - Component consistency + - Whether the design looks "dull" or "white-coded" (generic, no personality) +4. Provide specific, actionable feedback with fix suggestions. +5. If the user approves, implement the fixes directly in their code. +6. After fixes, re-run `openwolf designqc` to capture new screenshots and verify improvement. + +**Token awareness:** Each screenshot costs ~2500 tokens. The command compresses images (JPEG quality 70, max width 1200px) to minimize cost. For large apps, use `--routes / /specific-page` to limit captures. + +## Reframe — UI Framework Selection + +When the user asks to change, pick, migrate, or "reframe" their project's UI framework: + +1. Read `.wolf/reframe-frameworks.md` for the full framework knowledge base. +2. Ask the user the decision questions from the file (current stack, priority, Tailwind usage, theme preference, app type). Stop early once the choice narrows to 1-2 options. +3. Present a recommendation with reasoning based on the comparison matrix. +4. Once the user confirms, use the selected framework's prompt from the file — **adapted to the actual project** using `.wolf/anatomy.md` for real file paths, routes, and components. +5. Execute the migration: install dependencies, update config, refactor components. +6. After migration, run `openwolf designqc` to verify the new look. + +**Do NOT read the entire reframe-frameworks.md into context upfront.** Read the decision questions and comparison matrix first (~50 lines). Only read the specific framework's prompt section after the user chooses. + +## Session End + +Before ending or when asked to wrap up: + +1. Write a session summary to `.wolf/memory.md`. +2. Review the session: did you learn anything? Did the user correct you? Did you fix a bug? If yes, update `.wolf/cerebrum.md` and/or `.wolf/buglog.json`. diff --git a/.wolf/anatomy.md b/.wolf/anatomy.md new file mode 100644 index 0000000..afb0620 --- /dev/null +++ b/.wolf/anatomy.md @@ -0,0 +1,718 @@ +# anatomy.md + +> Auto-maintained by OpenWolf. Last scanned: 2026-04-06T12:56:30.133Z +> Files: 504 tracked | Anatomy hits: 0 | Misses: 0 + +## ./ + +- `.coverage` (~14200 tok) +- `.gitignore` — Git ignore rules (~175 tok) +- `AGENTS.md` — Agent Instructions for DevAIFlow (~20993 tok) +- `CHANGELOG.md` — Change log (~4649 tok) +- `CLAUDE.md` — OpenWolf (~139 tok) +- `config.schema.json` (~9891 tok) +- `CONTRIBUTING.md` — Contributing to DevAIFlow (~2458 tok) +- `coverage.json` (~12790 tok) +- `demo_branch_selection.sh` — Demo script showing the new branch source selection feature (~846 tok) +- `LICENSE` — Project license (~3029 tok) +- `pyproject.toml` — Python project configuration (~702 tok) +- `QUICKREF.md` — DevAIFlow Quick Reference (~1384 tok) +- `README.md` — Project documentation (~6603 tok) +- `RELEASING.md` — Release Management Process (~2707 tok) +- `requirements-dev.txt` (~28 tok) +- `requirements.txt` — Python dependencies (~72 tok) +- `SECURITY.md` — Security Policy (~1380 tok) +- `setup.py` — Python package setup (~80 tok) +- `T-1.md` — Session: T-1 - G (~44 tok) + +## .claude/ + +- `settings.json` (~441 tok) +- `settings.local.json` (~305 tok) + +## .claude/rules/ + +- `openwolf.md` (~313 tok) + +## .github/ISSUE_TEMPLATE/ + +- `agent-validation.md` — Environment (~360 tok) + +## .github/workflows/ + +- `integration-tests.yml` — GitHub Actions CI/CD - Integration Tests Workflow (~387 tok) +- `lint.yml` — GitHub Actions CI/CD - Lint Workflow (~534 tok) +- `publish.yml` — GitHub Actions CI/CD - PyPI Publish Workflow (~582 tok) +- `test.yml` — GitHub Actions CI/CD - Test Workflow (~467 tok) + +## .pytest_cache/ + +- `.gitignore` — Git ignore rules (~10 tok) +- `CACHEDIR.TAG` (~51 tok) +- `README.md` — Project documentation (~76 tok) + +## .pytest_cache/v/cache/ + +- `lastfailed` (~4039 tok) +- `nodeids` (~92198 tok) +- `stepwise` (~1 tok) + +## demos/ + +- `demo-commands.txt` (~524 tok) +- `demo-github-workflow-20260308-172110.log` (~46804 tok) +- `demo-github-workflow-20260308-172750.log` (~45925 tok) +- `demo-github-workflow-20260308-173242.log` (~56119 tok) +- `demo-github-workflow-20260308-185816.log` (~58075 tok) +- `demo-github-workflow.sh` — Real demo using tmux to automate DevAIFlow GitHub workflow (~3720 tok) + +## devflow/ + +- `__init__.py` — DevAIFlow - AI-Powered Development Workflow Manager with JIRA integration. (~31 tok) +- `exceptions.py` — Custom exceptions for daf tool. (~300 tok) + +## devflow/agent/ + +- `__init__.py` — Agent interface abstraction for DevAIFlow. (~420 tok) +- `aider_agent.py` — Aider agent implementation. (~3231 tok) +- `claude_agent.py` — Claude Code agent implementation. (~7196 tok) +- `continue_agent.py` — Continue agent implementation. (~3229 tok) +- `crush_agent.py` — Crush agent implementation. (~3484 tok) +- `cursor_agent.py` — Cursor agent implementation. (~2906 tok) +- `factory.py` — Factory for creating AI agent clients. (~1097 tok) +- `github_copilot_agent.py` — GitHub Copilot agent implementation. (~2800 tok) +- `interface.py` — Abstract interface for AI agent backends. (~2273 tok) +- `ollama_claude_agent.py` — Ollama + Claude Code agent implementation. (~4956 tok) +- `skill_directories.py` — get_agent_global_skills_dir, get_agent_project_skills_dir, get_skill_install_paths, validate_agent_names (~2859 tok) +- `windsurf_agent.py` — Windsurf agent implementation. (~2906 tok) + +## devflow/archive/ + +- `__init__.py` — Archive management for backup and export operations. (~42 tok) +- `base.py` — Base class for archive operations (backup/export). (~1155 tok) + +## devflow/backup/ + +- `__init__.py` — Backup and restore functionality for DevAIFlow. (~16 tok) +- `manager.py` — Backup and restore manager for DevAIFlow. (~3370 tok) + +## devflow/cli/ + +- `__init__.py` — CLI commands for DevAIFlow. (~10 tok) +- `completion.py` — Shell completion support for DevAIFlow. (~1235 tok) +- `main.py` — Main CLI entry point for DevAIFlow. (~47109 tok) +- `signal_handler.py` — Unified signal handler for CLI commands that launch Claude sessions. (~2578 tok) +- `skills_discovery.py` — Utility for discovering skills from all hierarchical locations. (~1331 tok) +- `utils.py` — Common utility functions for CLI commands. (~15760 tok) + +## devflow/cli/commands/ + +- `__init__.py` — CLI command implementations. (~10 tok) +- `active_command.py` — Implementation of 'daf active' command. (~3418 tok) +- `agent_commands.py` — Agent management commands for DevAIFlow. (~5482 tok) +- `backup_command.py` — Implementation of 'daf backup' command. (~449 tok) +- `check_command.py` — Implementation of 'daf check' command. (~1058 tok) +- `cleanup_command.py` — Implementation of 'daf cleanup-conversation' command. (~6300 tok) +- `cleanup_sessions_command.py` — Implementation of 'daf cleanup-sessions' command. (~1484 tok) +- `complete_command.py` — Implementation of 'daf complete' command. (~51802 tok) +- `config_export_command.py` — Implementation of 'daf config export' command. (~476 tok) +- `config_import_command.py` — Implementation of 'daf config import' command. (~619 tok) +- `context_commands.py` — Implementation of 'daf config context' commands. (~2136 tok) +- `delete_command.py` — Implementation of 'daf delete' command. (~2272 tok) +- `discover_command.py` — Implementation of 'daf discover' command. (~1365 tok) +- `export_command.py` — Implementation of 'daf export' command. (~5784 tok) +- `export_md_command.py` — Implementation of 'daf export-md' command. (~917 tok) +- `feature_command.py` — Feature orchestration CLI commands. (~28130 tok) +- `git_add_comment_command.py` — Implementation of 'daf git add-comment' command. (~911 tok) +- `git_check_auth_command.py` — Implementation of 'daf git check-auth' command. (~1411 tok) +- `git_commands.py` — Git-based issue tracker CLI command group for DevAIFlow. (~164 tok) +- `git_create_command.py` — Implementation of 'daf git create' command. (~5406 tok) +- `git_new_command.py` — Command for daf git new - create GitHub/GitLab issue with session-type for ticket creation workflow. (~14122 tok) +- `git_open_command.py` — Command for daf git open - open or create session from GitHub/GitLab issue. (~2094 tok) +- `git_update_command.py` — Implementation of 'daf git update' command. (~2244 tok) +- `git_view_command.py` — Implementation of 'daf git view' command. (~2215 tok) +- `import_command.py` — Implementation of 'daf import' command. (~3262 tok) +- `import_session_command.py` — Implementation of 'daf import-session' command. (~2195 tok) +- `info_command.py` — Implementation of 'daf info' command. (~6561 tok) +- `investigate_command.py` — Command for daf investigate - create investigation-only session without ticket creation. (~10149 tok) +- `jira_add_comment_command.py` — Implementation of 'daf jira add-comment' command. (~1756 tok) +- `jira_create_commands.py` — Implementation of 'daf jira create' command. (~18107 tok) +- `jira_create_dynamic.py` — Dynamic command builder for daf jira create with field discovery. (~3074 tok) +- `jira_field_utils.py` — Common utilities for JIRA field processing in dynamic commands. (~1410 tok) +- `jira_new_command.py` — Command for daf jira new - create issue tracker ticket with session-type for ticket creation workflow. (~14348 tok) +- `jira_open_command.py` — Command for daf jira open - open or create session from issue tracker ticket. (~2270 tok) +- `jira_update_command.py` — Implementation of 'daf jira update' command. (~7088 tok) +- `jira_update_dynamic.py` — Dynamic command builder for daf jira update with field discovery. (~1988 tok) +- `jira_view_command.py` — Implementation of 'daf jira view' command. (~4432 tok) +- `link_command.py` — Implementation of 'daf link' command. (~2314 tok) +- `list_command.py` — Implementation of 'daf list' command. (~4987 tok) +- `new_command_multiproject.py` — Multi-project session creation logic for DevAIFlow (Issue #149). (~4292 tok) +- `new_command.py` — Implementation of 'daf new' command. (~27498 tok) +- `note_command.py` — Implementation of 'daf note' and 'daf notes' commands. (~1903 tok) +- `open_command.py` — Implementation of 'daf open' command. (~50903 tok) +- `pause_command.py` — Implementation of 'daf pause' command. (~619 tok) +- `provider_commands.py` — Implementation of 'daf model' commands for managing model provider profiles. (~6464 tok) +- `rebuild_index_command.py` — Implementation of 'daf rebuild-index' command. (~2992 tok) +- `release_command.py` — Implementation of 'daf release' command. (~7071 tok) +- `repair_conversation_command.py` — CLI command for repairing corrupted Claude Code conversation files. (~3410 tok) +- `restore_command.py` — Implementation of 'daf restore' command. (~485 tok) +- `resume_command.py` — Implementation of 'daf resume' command. (~603 tok) +- `search_command.py` — Implementation of 'daf search' command. (~959 tok) +- `session_project_command.py` — Commands for managing projects/conversations in sessions. (~3135 tok) +- `sessions_list_command.py` — Implementation of 'daf list' command (formerly 'daf sessions list'). (~1463 tok) +- `skills_command.py` — Implementation of 'daf skills' command group. (~8572 tok) +- `skills_discovery_command.py` — Implementation of 'daf skills' command for discovery and inspection. (~4658 tok) +- `status_command.py` — Implementation of 'daf status' command. (~3926 tok) +- `summary_command.py` — Implementation of 'daf summary' command. (~2358 tok) +- `sync_command.py` — Implementation of 'daf sync' command. (~16114 tok) +- `template_commands.py` — Implementation of template management commands. (~1880 tok) +- `ticket_creation_multiproject.py` — Multi-project ticket creation session logic for DevAIFlow (Issue #179). (~1273 tok) +- `time_command.py` — Implementation of 'daf time' command. (~982 tok) +- `update_command.py` — Implementation of 'daf update' command. (~656 tok) +- `upgrade_command.py` — Implementation of 'daf upgrade' command. (~2669 tok) +- `workspace_commands.py` — Implementation of 'daf workspace' commands (AAP-63377). (~4987 tok) + +## devflow/cli_skills/daf-active/ + +- `SKILL.md` (~542 tok) + +## devflow/cli_skills/daf-cli/ + +- `SKILL.md` — DAF Quick Reference (~1373 tok) + +## devflow/cli_skills/daf-config/ + +- `SKILL.md` (~585 tok) + +## devflow/cli_skills/daf-git/ + +- `SKILL.md` — Quick Start (~2060 tok) + +## devflow/cli_skills/daf-help/ + +- `SKILL.md` — Work in current repository (~473 tok) + +## devflow/cli_skills/daf-info/ + +- `SKILL.md` (~527 tok) + +## devflow/cli_skills/daf-jira-fields/ + +- `SKILL.md` — JIRA Field Intelligence for DevAIFlow (~1813 tok) + +## devflow/cli_skills/daf-jira-mcp/ + +- `SKILL.md` — MCP JIRA Integration with DevAIFlow Intelligence (~3420 tok) + +## devflow/cli_skills/daf-jira/ + +- `SKILL.md` — MCP Alternative Available (~1489 tok) + +## devflow/cli_skills/daf-list-conversations/ + +- `SKILL.md` (~209 tok) + +## devflow/cli_skills/daf-list/ + +- `SKILL.md` (~381 tok) + +## devflow/cli_skills/daf-notes/ + +- `SKILL.md` (~463 tok) + +## devflow/cli_skills/daf-read-conversation/ + +- `SKILL.md` — Get session info to find conversation details (~502 tok) + +## devflow/cli_skills/daf-status/ + +- `SKILL.md` (~463 tok) + +## devflow/cli_skills/daf-workflow/ + +- `SKILL.md` — DevAIFlow Workflow Guide (~3274 tok) + +## devflow/cli_skills/daf-workspace/ + +- `SKILL.md` (~407 tok) + +## devflow/cli_skills/gh-cli/ + +- `SKILL.md` — GitHub CLI (gh) Reference for daf tool (~3332 tok) + +## devflow/cli_skills/git-cli/ + +- `SKILL.md` — Git CLI Reference for daf tool (~2092 tok) + +## devflow/cli_skills/glab-cli/ + +- `SKILL.md` — GitLab CLI (glab) Reference for daf tool (~3185 tok) + +## devflow/config/ + +- `__init__.py` — Configuration management for DevAIFlow. (~14 tok) +- `exporter.py` — Configuration export functionality. (~2951 tok) +- `importer.py` — Configuration import functionality. (~2580 tok) +- `init_wizard.py` — Interactive configuration wizard for daf init. (~12648 tok) +- `loader.py` — Configuration file loading and management. (~11941 tok) +- `models.py` — Configuration models for DevAIFlow. (~21910 tok) +- `schema.py` — JSON Schema generation and validation for configuration. (~914 tok) +- `validator.py` — Configuration validation for detecting placeholder values and completeness issues. (~2212 tok) + +## devflow/config/schemas/ + +- `__init__.py` (~0 tok) +- `enterprise.schema.json` (~949 tok) +- `organization.schema.json` (~1838 tok) +- `team.schema.json` (~931 tok) +- `user.schema.json` (~1809 tok) + +## devflow/config/schemas/backends/ + +- `__init__.py` (~0 tok) +- `jira.schema.json` (~703 tok) + +## devflow/config/templates/ + +- `__init__.py` — Model provider templates for DevAIFlow. (~113 tok) +- `model_providers.py` — Model provider templates for TUI configuration. (~4081 tok) + +## devflow/config/validators/ + +- `__init__.py` — Configuration validators for DevAIFlow. (~187 tok) +- `base.py` — Base validator class for configuration files. (~3356 tok) +- `enterprise.py` — Validator for enterprise.json configuration file. (~564 tok) +- `organization.py` — Validator for organization.json configuration file. (~1409 tok) +- `team.py` — Validator for team.json configuration file. (~857 tok) +- `user.py` — Validator for config.json (user) configuration file. (~990 tok) + +## devflow/config/validators/backends/ + +- `__init__.py` — Backend configuration validators. (~74 tok) +- `base.py` — Base validator for backend configuration files. (~146 tok) +- `jira.py` — Validator for backends/jira.json configuration file. (~1696 tok) + +## devflow/export/ + +- `__init__.py` — Export and import functionality for DevAIFlow. (~16 tok) +- `manager.py` — Export and import manager for DevAIFlow. (~8292 tok) +- `markdown.py` — Markdown export functionality for DevAIFlow. (~5216 tok) + +## devflow/git/ + +- `__init__.py` — Git integration utilities for DevAIFlow. (~14 tok) +- `pr_template.py` — AI-powered PR/MR template parsing and filling. (~2974 tok) +- `utils.py` — Git utilities for branch management. (~15440 tok) + +## devflow/github/ + +- `__init__.py` — GitHub Issues integration for DevAIFlow. (~123 tok) +- `auth.py` — GitHub authentication and pre-flight checks. (~2001 tok) +- `field_mapper.py` — GitHub field mapper for converting between GitHub Issues and DevAIFlow interface. (~3675 tok) +- `issues_client.py` — GitHub Issues client implementing IssueTrackerClient interface. (~8675 tok) +- `transitions.py` — GitHub issue state transitions for DevAIFlow sessions. (~2728 tok) + +## devflow/gitlab/ + +- `__init__.py` — GitLab integration for DevAIFlow. (~119 tok) +- `field_mapper.py` — GitLab field mapper for converting between GitLab Issues and DevAIFlow interface. (~4120 tok) +- `issues_client.py` — GitLab Issues client implementing IssueTrackerClient interface. (~9175 tok) + +## devflow/issue_tracker/ + +- `__init__.py` — Issue tracker abstraction layer for DevAIFlow. (~231 tok) +- `exceptions.py` — Custom exceptions for issue tracker client operations. (~1788 tok) +- `factory.py` — Factory for creating issue tracker client instances. (~1531 tok) +- `interface.py` — Abstract interface for issue tracking systems. (~3578 tok) +- `mock_client.py` — Mock implementation of IssueTrackerClient for testing. (~3176 tok) +- `README.md` — Project documentation (~1449 tok) + +## devflow/jira/ + +- `__init__.py` — JIRA integration for DevAIFlow. (~111 tok) +- `client.py` — JIRA REST API client for DevAIFlow. (~25990 tok) +- `exceptions.py` — Custom exceptions for JIRA client operations. (~485 tok) +- `field_mapper.py` — JIRA custom field mapper for discovering and caching field metadata. (~6763 tok) +- `transitions.py` — issue tracker ticket transition management. (~3618 tok) +- `utils.py` — Utility functions for JIRA operations. (~4182 tok) +- `validation.py` — JIRA field validation based on config.jira rules. (~4122 tok) + +## devflow/mocks/ + +- `__init__.py` — Mock services infrastructure for integration testing. (~112 tok) +- `claude_mock.py` — Mock Claude Code service for integration testing. (~2230 tok) +- `github_mock.py` — Mock GitHub service for integration testing. (~1725 tok) +- `gitlab_mock.py` — Mock GitLab service for integration testing. (~2366 tok) +- `jira_mock.py` — Mock JIRA service for integration testing. (~3404 tok) +- `persistence.py` — Thread-safe persistent storage for mock services data. (~4392 tok) + +## devflow/orchestration/ + +- `__init__.py` — Feature orchestration module for DevAIFlow. (~111 tok) +- `feature.py` — Feature orchestration manager for DevAIFlow. (~7221 tok) +- `parent_discovery.py` — Parent ticket discovery for feature orchestration. (~5487 tok) +- `storage.py` — Feature orchestration storage backend. (~2215 tok) + +## devflow/release/ + +- `__init__.py` — Release management utilities. (~133 tok) +- `manager.py` — Release manager for automating release mechanics. (~14891 tok) +- `permissions.py` — Permission checking for release operations. (~2884 tok) +- `version.py` — Version parsing and comparison utilities for release management. (~1692 tok) + +## devflow/session/ + +- `__init__.py` — Session management for Claude Code sessions. (~15 tok) +- `capture.py` — Session ID capture logic for detecting new AI agent sessions. (~1357 tok) +- `discovery.py` — Discover existing Claude Code sessions. (~1265 tok) +- `manager.py` — Session management for Claude Code sessions. (~5997 tok) +- `repair.py` — Conversation file repair utilities for Claude Code sessions. (~3704 tok) +- `summary.py` — Session summary extraction from Claude Code conversation files. (~7501 tok) + +## devflow/storage/ + +- `__init__.py` — Storage abstraction layer for session persistence. (~66 tok) +- `base.py` — Abstract base class for storage backends. (~980 tok) +- `file_backend.py` — File-based storage backend for sessions. (~3352 tok) +- `filters.py` — Session filter criteria for querying sessions. (~286 tok) + +## devflow/suggestions/ + +- `__init__.py` — Repository suggestion system for DevAIFlow. (~67 tok) +- `models.py` — Data models for repository suggestion system. (~1442 tok) +- `suggester.py` — Repository suggestion engine with learning capabilities. (~4212 tok) + +## devflow/templates/ + +- `__init__.py` — Template management for DevAIFlow. (~89 tok) +- `manager.py` — Template manager for DevAIFlow. (~1390 tok) +- `models.py` — Template data models for DevAIFlow. (~1722 tok) + +## devflow/ui/ + +- `__init__.py` — UI components for DevAIFlow. (~10 tok) +- `config_tui.py` — Text User Interface for DevAIFlow configuration. (~44286 tok) +- `session_editor_tui.py` — Text User Interface for editing Claude Session metadata. (~10933 tok) + +## devflow/utils/ + +- `__init__.py` — Utility functions for DevAIFlow. (~60 tok) +- `audit_log.py` — Audit logging for DevAIFlow operations. (~1444 tok) +- `backend_detection.py` — Utilities for detecting issue tracker backend from session metadata and issue keys. (~1809 tok) +- `claude_commands.py` — Utilities for managing bundled Claude Code skills. (~6464 tok) +- `context_files.py` — Utility for loading hierarchical context files from DEVAIFLOW_HOME. (~931 tok) +- `daf_agents_validation.py` — Migration utilities for DAF_AGENTS.md to daf-workflow skill transition. (~4940 tok) +- `dependencies.py` — Dependency checking utilities for external tools. (~1072 tok) +- `git_remote.py` — Git remote detection utilities for issue tracker integration. (~2750 tok) +- `hierarchical_skills.py` — Utilities for managing hierarchical skills from config files. (~15589 tok) +- `model_provider.py` — Utilities for managing model provider configuration and profiles. (~1813 tok) +- `paths.py` — Path utilities for DevAIFlow. (~659 tok) +- `ssl_helper.py` — SSL verification helper for HTTP requests. (~840 tok) +- `temp_directory.py` — Temporary directory utilities for issue tracker ticket creation sessions. (~2726 tok) +- `time_parser.py` — Time expression parser for filtering. (~1026 tok) +- `update_checker.py` — Update checker for DevAIFlow. (~2612 tok) +- `url_parser.py` — URL parser for issue tracker URLs. (~1411 tok) +- `user.py` — User detection utilities. (~192 tok) +- `workspace_utils.py` — Utilities for workspace management and auto-upgrade. (~766 tok) + +## devflow/verification/ + +- `__init__.py` — Verification module for feature orchestration. (~149 tok) +- `artifact_validator.py` — Artifact validator for feature verification. (~1004 tok) +- `criteria_checker.py` — Acceptance criteria checker for feature verification. (~2411 tok) +- `report_generator.py` — Verification report generator for feature orchestration. (~2438 tok) +- `test_runner.py` — Test runner for feature verification. (~1424 tok) + +## docs/ + +- `experimental-agents-validation-tracking.md` — Experimental Agents Validation Tracking (~635 tok) +- `experimental-agents.md` — Experimental AI Agents (~4906 tok) +- `NON_INTERACTIVE_PARAMETERS.md` — Non-Interactive CLI Parameters (~2687 tok) +- `NOTEBOOKLM.md` — DevAIFlow - AI-Optimized Summary (~1840 tok) +- `README.md` — Project documentation (~1316 tok) + +## docs/config-templates/ + +- `enterprise.json` (~452 tok) +- `organization.json` (~795 tok) +- `README.md` — Project documentation (~2101 tok) +- `team.json` — Declares is (~230 tok) +- `user.json` (~604 tok) + +## docs/config-templates/backends/ + +- `jira.json` (~252 tok) + +## docs/context-templates/ + +- `CONFIG.md` — Personal Development Notes (~615 tok) +- `JIRA.md` — JIRA Backend Integration Rules (~440 tok) +- `ORGANIZATION.md` — Organization Coding Standards (~1313 tok) +- `README.md` — Project documentation (~1910 tok) +- `TEAM.md` — Team Conventions and Workflows (~642 tok) + +## docs/developer/ + +- `feature-orchestration-architecture.md` — Feature Orchestration Implementation Summary (~3896 tok) +- `feature-orchestration.md` — Feature Orchestration Flow (~4079 tok) +- `issue-tracker-architecture.md` — Issue Tracker Architecture (~2650 tok) +- `publishing-to-pypi.md` — Publishing DevAIFlow to PyPI (~3596 tok) +- `release-management.md` — Release Management (~4727 tok) + +## docs/experimental/ + +- `feature-orchestration.md` — Feature Orchestration (EXPERIMENTAL) (~6420 tok) +- `README.md` — Project documentation (~867 tok) + +## docs/getting-started/ + +- `installation.md` — Installation Guide (~6151 tok) +- `overview.md` — Overview (~4475 tok) +- `quick-start.md` — Quick Start Guide (~3849 tok) +- `uninstall.md` — Uninstall DevAIFlow (~1806 tok) + +## docs/guides/ + +- `enterprise-model-provider-enforcement.md` — Enterprise Model Provider Enforcement Guide (~4231 tok) +- `hierarchical-skills.md` — Hierarchical Skills Architecture (~9300 tok) +- `multi-agent-skill-installation.md` — Multi-Agent Skill Installation Guide (~2690 tok) +- `session-management.md` — Session Management (~8306 tok) +- `skills-management.md` — Skills Management Guide (~2626 tok) +- `ssl-configuration.md` — SSL Certificate Verification Configuration (~1344 tok) +- `troubleshooting.md` — Troubleshooting Guide (~15008 tok) + +## docs/reference/ + +- `ai-agent-support-matrix.md` — AI Agent Support Matrix (~7493 tok) +- `alternative-model-providers.md` — Alternative Model Providers (~11083 tok) +- `commands.md` — Commands Reference (~48930 tok) +- `configuration.md` — Configuration Reference (~16658 tok) + +## docs/tutorials/ + +- `local-llama-cpp-setup.md` — Tutorial: Run Claude Code with Local Models Using llama.cpp (~3550 tok) +- `README.md` — Project documentation (~194 tok) + +## docs/workflows/ + +- `github-gitlab-integration.md` — GitHub Issue Integration (~4604 tok) +- `jira-integration.md` — JIRA Integration (~10139 tok) +- `WORKFLOWS.md` — DevAIFlow Complete Workflows (~3100 tok) + +## htmlcov/ + +- `.gitignore` — Git ignore rules (~8 tok) +- `class_index.html` — Coverage report (~42121 tok) +- `coverage_html_cb_bcae5fc4.js` — For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt (~7272 tok) +- `function_index.html` — Coverage report (~177316 tok) +- `index.html` — Coverage report (~17252 tok) +- `status.json` (~13599 tok) +- `style_cb_8432e98f.css` — Styles: 116 rules, 51 media queries (~4553 tok) +- `z_06a117fdecd41ea4___init___py.html` — Coverage for devflow/cli/__init__.py: 100% (~1271 tok) +- `z_06a117fdecd41ea4_completion_py.html` — Coverage for devflow/cli/completion.py: 100% (~8842 tok) +- `z_06a117fdecd41ea4_main_py.html` — Coverage for devflow/cli/main.py: 55% (~223488 tok) +- `z_06a117fdecd41ea4_signal_handler_py.html` — Coverage for devflow/cli/signal_handler.py: 86% (~15496 tok) +- `z_06a117fdecd41ea4_skills_discovery_py.html` — Coverage for devflow/cli/skills_discovery.py: 85% (~8819 tok) +- `z_06a117fdecd41ea4_utils_py.html` — Coverage for devflow/cli/utils.py: 77% (~76642 tok) +- `z_07a4164db4a08fc5___init___py.html` — Coverage for devflow/agent/__init__.py: 100% (~3022 tok) +- `z_07a4164db4a08fc5_claude_agent_py.html` — Coverage for devflow/agent/claude_agent.py: 100% (~15514 tok) +- `z_07a4164db4a08fc5_cursor_agent_py.html` — Coverage for devflow/agent/cursor_agent.py: 85% (~15142 tok) +- `z_07a4164db4a08fc5_factory_py.html` — Coverage for devflow/agent/factory.py: 100% (~5635 tok) +- `z_07a4164db4a08fc5_github_copilot_agent_py.html` — Coverage for devflow/agent/github_copilot_agent.py: 78% (~14244 tok) +- `z_07a4164db4a08fc5_interface_py.html` — Coverage for devflow/agent/interface.py: 71% (~10062 tok) +- `z_07a4164db4a08fc5_windsurf_agent_py.html` — Coverage for devflow/agent/windsurf_agent.py: 85% (~15134 tok) +- `z_10fec4e5b72b7f39___init___py.html` — Coverage for devflow/jira/__init__.py: 100% (~1851 tok) +- `z_10fec4e5b72b7f39_client_py.html` — Coverage for devflow/jira/client.py: 77% (~130187 tok) +- `z_10fec4e5b72b7f39_exceptions_py.html` — Coverage for devflow/jira/exceptions.py: 98% (~10205 tok) +- `z_10fec4e5b72b7f39_field_mapper_py.html` — Coverage for devflow/jira/field_mapper.py: 89% (~38602 tok) +- `z_10fec4e5b72b7f39_transitions_py.html` — Coverage for devflow/jira/transitions.py: 95% (~19776 tok) +- `z_10fec4e5b72b7f39_utils_py.html` — Coverage for devflow/jira/utils.py: 95% (~23772 tok) +- `z_10fec4e5b72b7f39_validation_py.html` — Coverage for devflow/jira/validation.py: 88% (~27717 tok) +- `z_1949ad9a47d2925e___init___py.html` — Coverage for devflow/suggestions/__init__.py: 100% (~1632 tok) +- `z_1949ad9a47d2925e_models_py.html` — Coverage for devflow/suggestions/models.py: 100% (~11175 tok) +- `z_1949ad9a47d2925e_suggester_py.html` — Coverage for devflow/suggestions/suggester.py: 96% (~32285 tok) +- `z_2eb326c2d41ac0bc___init___py.html` — Coverage for devflow/config/schemas/__init__.py: 100% (~1231 tok) +- `z_3cdcc195ef6adc92___init___py.html` — Coverage for devflow/mocks/__init__.py: 100% (~2107 tok) +- `z_3cdcc195ef6adc92_claude_mock_py.html` — Coverage for devflow/mocks/claude_mock.py: 99% (~18365 tok) +- `z_3cdcc195ef6adc92_github_mock_py.html` — Coverage for devflow/mocks/github_mock.py: 97% (~15538 tok) +- `z_3cdcc195ef6adc92_gitlab_mock_py.html` — Coverage for devflow/mocks/gitlab_mock.py: 91% (~20201 tok) +- `z_3cdcc195ef6adc92_jira_mock_py.html` — Coverage for devflow/mocks/jira_mock.py: 87% (~26506 tok) +- `z_3cdcc195ef6adc92_persistence_py.html` — Coverage for devflow/mocks/persistence.py: 92% (~36444 tok) +- `z_506f96be02ce2c1e___init___py.html` — Coverage for devflow/config/validators/__init__.py: 100% (~2633 tok) +- `z_506f96be02ce2c1e_base_py.html` — Coverage for devflow/config/validators/base.py: 100% (~23998 tok) +- `z_506f96be02ce2c1e_enterprise_py.html` — Coverage for devflow/config/validators/enterprise.py: 88% (~4994 tok) +- `z_506f96be02ce2c1e_organization_py.html` — Coverage for devflow/config/validators/organization.py: 88% (~8442 tok) +- `z_506f96be02ce2c1e_team_py.html` — Coverage for devflow/config/validators/team.py: 85% (~6781 tok) +- `z_506f96be02ce2c1e_user_py.html` — Coverage for devflow/config/validators/user.py: 93% (~7743 tok) +- `z_5a3a4486c526beea___init___py.html` — Coverage for devflow/__init__.py: 100% (~1359 tok) +- `z_5a3a4486c526beea_exceptions_py.html` — Coverage for devflow/exceptions.py: 100% (~3165 tok) +- `z_5cd312b2b760ee18___init___py.html` — Coverage for devflow/archive/__init__.py: 100% (~1524 tok) +- `z_5cd312b2b760ee18_base_py.html` — Coverage for devflow/archive/base.py: 98% (~9448 tok) +- `z_69b1b828517c762c___init___py.html` — Coverage for devflow/ui/__init__.py: 100% (~1272 tok) +- `z_69b1b828517c762c_config_tui_py.html` — Coverage for devflow/ui/config_tui.py: 21% (~208454 tok) +- `z_69b1b828517c762c_session_editor_tui_py.html` — Coverage for devflow/ui/session_editor_tui.py: 49% (~82479 tok) +- `z_7ea0fb1e64e286a0___init___py.html` — Coverage for devflow/cli/commands/__init__.py: 100% (~1284 tok) +- `z_7ea0fb1e64e286a0_active_command_py.html` — Coverage for devflow/cli/commands/active_command.py: 100% (~18165 tok) +- `z_7ea0fb1e64e286a0_backup_command_py.html` — Coverage for devflow/cli/commands/backup_command.py: 100% (~4696 tok) +- `z_7ea0fb1e64e286a0_check_command_py.html` — Coverage for devflow/cli/commands/check_command.py: 100% (~9790 tok) +- `z_7ea0fb1e64e286a0_cleanup_command_py.html` — Coverage for devflow/cli/commands/cleanup_command.py: 19% (~45958 tok) +- `z_7ea0fb1e64e286a0_cleanup_sessions_command_py.html` — Coverage for devflow/cli/commands/cleanup_sessions_command.py: 98% (~11265 tok) +- `z_7ea0fb1e64e286a0_complete_command_py.html` — Coverage for devflow/cli/commands/complete_command.py: 46% (~185903 tok) +- `z_7ea0fb1e64e286a0_context_commands_py.html` — Coverage for devflow/cli/commands/context_commands.py: 85% (~16900 tok) +- `z_7ea0fb1e64e286a0_delete_command_py.html` — Coverage for devflow/cli/commands/delete_command.py: 86% (~16608 tok) +- `z_7ea0fb1e64e286a0_discover_command_py.html` — Coverage for devflow/cli/commands/discover_command.py: 99% (~11164 tok) +- `z_7ea0fb1e64e286a0_export_command_py.html` — Coverage for devflow/cli/commands/export_command.py: 93% (~19664 tok) +- `z_7ea0fb1e64e286a0_export_md_command_py.html` — Coverage for devflow/cli/commands/export_md_command.py: 86% (~8114 tok) +- `z_7ea0fb1e64e286a0_import_command_py.html` — Coverage for devflow/cli/commands/import_command.py: 92% (~8967 tok) +- `z_7ea0fb1e64e286a0_import_session_command_py.html` — Coverage for devflow/cli/commands/import_session_command.py: 98% (~15859 tok) +- `z_7ea0fb1e64e286a0_info_command_py.html` — Coverage for devflow/cli/commands/info_command.py: 83% (~37572 tok) +- `z_7ea0fb1e64e286a0_investigate_command_py.html` — Coverage for devflow/cli/commands/investigate_command.py: 45% (~33623 tok) +- `z_7ea0fb1e64e286a0_jira_add_comment_command_py.html` — Coverage for devflow/cli/commands/jira_add_comment_command.py: 90% (~14612 tok) +- `z_7ea0fb1e64e286a0_jira_create_commands_py.html` — Coverage for devflow/cli/commands/jira_create_commands.py: 54% (~104224 tok) +- `z_7ea0fb1e64e286a0_jira_create_dynamic_py.html` — Coverage for devflow/cli/commands/jira_create_dynamic.py: 87% (~16495 tok) +- `z_7ea0fb1e64e286a0_jira_field_utils_py.html` — Coverage for devflow/cli/commands/jira_field_utils.py: 87% (~10481 tok) +- `z_7ea0fb1e64e286a0_jira_new_command_py.html` — Coverage for devflow/cli/commands/jira_new_command.py: 59% (~66095 tok) +- `z_7ea0fb1e64e286a0_jira_open_command_py.html` — Coverage for devflow/cli/commands/jira_open_command.py: 86% (~14179 tok) +- `z_7ea0fb1e64e286a0_jira_update_command_py.html` — Coverage for devflow/cli/commands/jira_update_command.py: 59% (~46487 tok) +- `z_7ea0fb1e64e286a0_jira_update_dynamic_py.html` — Coverage for devflow/cli/commands/jira_update_dynamic.py: 73% (~12793 tok) +- `z_7ea0fb1e64e286a0_jira_view_command_py.html` — Coverage for devflow/cli/commands/jira_view_command.py: 94% (~33872 tok) +- `z_7ea0fb1e64e286a0_link_command_py.html` — Coverage for devflow/cli/commands/link_command.py: 85% (~18020 tok) +- `z_7ea0fb1e64e286a0_list_command_py.html` — Coverage for devflow/cli/commands/list_command.py: 88% (~31566 tok) +- `z_7ea0fb1e64e286a0_new_command_py.html` — Coverage for devflow/cli/commands/new_command.py: 68% (~92566 tok) +- `z_7ea0fb1e64e286a0_note_command_py.html` — Coverage for devflow/cli/commands/note_command.py: 89% (~11029 tok) +- `z_7ea0fb1e64e286a0_open_command_py.html` — Coverage for devflow/cli/commands/open_command.py: 57% (~250185 tok) +- `z_7ea0fb1e64e286a0_pause_command_py.html` — Coverage for devflow/cli/commands/pause_command.py: 100% (~5699 tok) +- `z_7ea0fb1e64e286a0_rebuild_index_command_py.html` — Coverage for devflow/cli/commands/rebuild_index_command.py: 0% (~22269 tok) +- `z_7ea0fb1e64e286a0_release_command_py.html` — Coverage for devflow/cli/commands/release_command.py: 53% (~51449 tok) +- `z_7ea0fb1e64e286a0_repair_conversation_command_py.html` — Coverage for devflow/cli/commands/repair_conversation_command.py: 98% (~25108 tok) +- `z_7ea0fb1e64e286a0_restore_command_py.html` — Coverage for devflow/cli/commands/restore_command.py: 97% (~4992 tok) +- `z_7ea0fb1e64e286a0_resume_command_py.html` — Coverage for devflow/cli/commands/resume_command.py: 100% (~5507 tok) +- `z_7ea0fb1e64e286a0_search_command_py.html` — Coverage for devflow/cli/commands/search_command.py: 94% (~8872 tok) +- `z_7ea0fb1e64e286a0_sessions_list_command_py.html` — Coverage for devflow/cli/commands/sessions_list_command.py: 100% (~11990 tok) +- `z_7ea0fb1e64e286a0_status_command_py.html` — Coverage for devflow/cli/commands/status_command.py: 92% (~29329 tok) +- `z_7ea0fb1e64e286a0_summary_command_py.html` — Coverage for devflow/cli/commands/summary_command.py: 100% (~15753 tok) +- `z_7ea0fb1e64e286a0_sync_command_py.html` — Coverage for devflow/cli/commands/sync_command.py: 86% (~18792 tok) +- `z_7ea0fb1e64e286a0_template_commands_py.html` — Coverage for devflow/cli/commands/template_commands.py: 91% (~16320 tok) +- `z_7ea0fb1e64e286a0_time_command_py.html` — Coverage for devflow/cli/commands/time_command.py: 100% (~9055 tok) +- `z_7ea0fb1e64e286a0_update_command_py.html` — Coverage for devflow/cli/commands/update_command.py: 94% (~5706 tok) +- `z_7ea0fb1e64e286a0_upgrade_command_py.html` — Coverage for devflow/cli/commands/upgrade_command.py: 99% (~17762 tok) +- `z_7ea0fb1e64e286a0_workspace_commands_py.html` — Coverage for devflow/cli/commands/workspace_commands.py: 0% (~37827 tok) +- `z_9071fac653a1ac58___init___py.html` — Coverage for devflow/config/__init__.py: 100% (~1277 tok) +- `z_9071fac653a1ac58_init_wizard_py.html` — Coverage for devflow/config/init_wizard.py: 57% (~28366 tok) +- `z_9071fac653a1ac58_loader_py.html` — Coverage for devflow/config/loader.py: 82% (~66203 tok) +- `z_9071fac653a1ac58_models_py.html` — Coverage for devflow/config/models.py: 94% (~80633 tok) +- `z_9071fac653a1ac58_schema_py.html` — Coverage for devflow/config/schema.py: 95% (~8094 tok) +- `z_9071fac653a1ac58_validator_py.html` — Coverage for devflow/config/validator.py: 94% (~16343 tok) +- `z_9b9d793c7de35baf___init___py.html` — Coverage for devflow/issue_tracker/__init__.py: 100% (~1792 tok) +- `z_9b9d793c7de35baf_factory_py.html` — Coverage for devflow/issue_tracker/factory.py: 100% (~7684 tok) +- `z_9b9d793c7de35baf_interface_py.html` — Coverage for devflow/issue_tracker/interface.py: 69% (~21176 tok) +- `z_9b9d793c7de35baf_mock_client_py.html` — Coverage for devflow/issue_tracker/mock_client.py: 96% (~22186 tok) +- `z_9bdbb123e371d481___init___py.html` — Coverage for devflow/config/validators/backends/__init__.py: 100% (~1830 tok) +- `z_9bdbb123e371d481_base_py.html` — Coverage for devflow/config/validators/backends/base.py: 100% (~2042 tok) +- `z_9bdbb123e371d481_jira_py.html` — Coverage for devflow/config/validators/backends/jira.py: 87% (~9804 tok) +- `z_abdf1e2aecaac275___init___py.html` — Coverage for devflow/backup/__init__.py: 100% (~1276 tok) +- `z_abdf1e2aecaac275_manager_py.html` — Coverage for devflow/backup/manager.py: 87% (~21029 tok) +- `z_af49c4d607f8b748___init___py.html` — Coverage for devflow/templates/__init__.py: 100% (~1745 tok) +- `z_af49c4d607f8b748_manager_py.html` — Coverage for devflow/templates/manager.py: 100% (~11559 tok) +- `z_af49c4d607f8b748_models_py.html` — Coverage for devflow/templates/models.py: 100% (~13438 tok) +- `z_b2fa99fdd7fa1861___init___py.html` — Coverage for devflow/git/__init__.py: 100% (~1277 tok) +- `z_b2fa99fdd7fa1861_pr_template_py.html` — Coverage for devflow/git/pr_template.py: 100% (~20316 tok) +- `z_b2fa99fdd7fa1861_utils_py.html` — Coverage for devflow/git/utils.py: 68% (~94883 tok) +- `z_b85cf9881b04dc76___init___py.html` — Coverage for devflow/storage/__init__.py: 100% (~1681 tok) +- `z_b85cf9881b04dc76_base_py.html` — Coverage for devflow/storage/base.py: 74% (~8717 tok) +- `z_b85cf9881b04dc76_file_backend_py.html` — Coverage for devflow/storage/file_backend.py: 94% (~22838 tok) +- `z_b85cf9881b04dc76_filters_py.html` — Coverage for devflow/storage/filters.py: 100% (~2794 tok) +- `z_bc5e5d4277adbed7___init___py.html` — Coverage for devflow/utils/__init__.py: 100% (~1673 tok) +- `z_bc5e5d4277adbed7_claude_commands_py.html` — Coverage for devflow/utils/claude_commands.py: 80% (~30805 tok) +- `z_bc5e5d4277adbed7_context_files_py.html` — Coverage for devflow/utils/context_files.py: 100% (~5508 tok) +- `z_bc5e5d4277adbed7_dependencies_py.html` — Coverage for devflow/utils/dependencies.py: 100% (~9700 tok) +- `z_bc5e5d4277adbed7_hierarchical_skills_py.html` — Coverage for devflow/utils/hierarchical_skills.py: 4% (~43936 tok) +- `z_bc5e5d4277adbed7_paths_py.html` — Coverage for devflow/utils/paths.py: 100% (~3782 tok) +- `z_bc5e5d4277adbed7_temp_directory_py.html` — Coverage for devflow/utils/temp_directory.py: 100% (~9719 tok) +- `z_bc5e5d4277adbed7_time_parser_py.html` — Coverage for devflow/utils/time_parser.py: 100% (~9634 tok) +- `z_bc5e5d4277adbed7_update_checker_py.html` — Coverage for devflow/utils/update_checker.py: 93% (~20242 tok) +- `z_bc5e5d4277adbed7_user_py.html` — Coverage for devflow/utils/user.py: 100% (~2819 tok) +- `z_bc5e5d4277adbed7_workspace_utils_py.html` — Coverage for devflow/utils/workspace_utils.py: 100% (~5932 tok) +- `z_c41e04e05e7d0bb4___init___py.html` — Coverage for devflow/export/__init__.py: 100% (~1279 tok) +- `z_c41e04e05e7d0bb4_manager_py.html` — Coverage for devflow/export/manager.py: 83% (~49323 tok) +- `z_c41e04e05e7d0bb4_markdown_py.html` — Coverage for devflow/export/markdown.py: 85% (~34916 tok) +- `z_c580db977815c015___init___py.html` — Coverage for devflow/session/__init__.py: 100% (~1278 tok) +- `z_c580db977815c015_capture_py.html` — Coverage for devflow/session/capture.py: 100% (~10159 tok) +- `z_c580db977815c015_discovery_py.html` — Coverage for devflow/session/discovery.py: 100% (~10158 tok) +- `z_c580db977815c015_manager_py.html` — Coverage for devflow/session/manager.py: 88% (~33842 tok) +- `z_c580db977815c015_repair_py.html` — Coverage for devflow/session/repair.py: 83% (~27875 tok) +- `z_c580db977815c015_summary_py.html` — Coverage for devflow/session/summary.py: 93% (~48874 tok) +- `z_c61090a831b66cbd___init___py.html` — Coverage for devflow/config/schemas/backends/__init__.py: 100% (~1242 tok) +- `z_ffa21e95d58bba3e___init___py.html` — Coverage for devflow/release/__init__.py: 100% (~2484 tok) +- `z_ffa21e95d58bba3e_manager_py.html` — Coverage for devflow/release/manager.py: 52% (~101372 tok) +- `z_ffa21e95d58bba3e_permissions_py.html` — Coverage for devflow/release/permissions.py: 84% (~22324 tok) +- `z_ffa21e95d58bba3e_version_py.html` — Coverage for devflow/release/version.py: 100% (~14305 tok) + +## integration-tests/ + +- `configure_test_prompts.py` — Configure prompt settings for integration tests. (~867 tok) +- `README.md` — Project documentation (~2689 tok) +- `run_all_integration_tests.sh` — run_all_integration_tests.sh (~2656 tok) +- `setup_test_config.py` — Create a minimal test configuration for integration tests. (~1690 tok) +- `TEST_COLLABORATION_SCENARIO.md` — Testing Collaboration Workflow: 2-Developer Emulation (~7342 tok) +- `test_collaboration_workflow.sh` — Integration test for collaboration workflow (2-developer emulation) (~7786 tok) +- `test_config.sh` (~88 tok) +- `test_cursor_agent.sh` — test_cursor_agent.sh (~3228 tok) +- `test_devaiflow_home.sh` (~141 tok) +- `test_error_handling.sh` — test_error_handling.sh (~4812 tok) +- `test_feature_orchestration.sh` — test_feature_orchestration.sh (~2462 tok) +- `test_git_sync.sh` — test_git_sync.sh (~2555 tok) +- `test_github_copilot_agent.sh` — test_github_copilot_agent.sh (~3310 tok) +- `test_github_green_path.sh` — test_github_green_path.sh (~3646 tok) +- `test_installation_auto_close.sh` — test_installation_auto_close.sh (~10088 tok) +- `test_investigation.sh` — test_investigation.sh (~4549 tok) +- `TEST_JIRA_GREEN_PATH.md` — JIRA Green Path Integration Test (~2066 tok) +- `test_jira_green_path.sh` — test_jira_green_path.sh (~6030 tok) +- `test_jira_sync.sh` — test_jira_sync.sh (~4579 tok) +- `test_multi_project_workflow.sh` — test_multi_project_workflow.sh (~6131 tok) +- `test_multi_repo.sh` — test_multi_repo.sh (~3855 tok) +- `test_readonly_commands.sh` — test_readonly_commands.sh (~3678 tok) +- `test_session_lifecycle.sh` — test_session_lifecycle.sh (~3853 tok) +- `test_templates.sh` — test_templates.sh (~4062 tok) +- `test_time_tracking.sh` — test_time_tracking.sh (~3736 tok) +- `test_upgrade_project_path.sh` — Integration test for daf upgrade --project-path feature (~1860 tok) +- `test_windsurf_agent.sh` — test_windsurf_agent.sh (~3266 tok) +- `test_workspace_mismatch.sh` — test_workspace_mismatch.sh (~462 tok) + +## tests/ + +- `__init__.py` — Tests for DevAIFlow. (~8 tok) +- `.coverage` (~22896 tok) +- `conftest.py` — Pytest configuration and fixtures. (~10028 tok) +- `coverage.json` (~233462 tok) +- `README.md` — Project documentation (~1090 tok) +- `test_active_command.py` — Tests for daf active command. (~5910 tok) +- `test_agent_commands.py` — Tests for agent management commands (Story 7). (~5984 tok) +- `test_agent_interface.py` — Tests for agent interface abstraction. (~17615 tok) +- `test_auto_create_pr_status.py` — Tests for auto_create_pr_status feature. (~432 tok) +- `test_auto_template.py` — Tests for auto-template creation and usage functionality. (~3330 tok) +- `test_backend_detection.py` — Tests for backend detection utilities. (~2987 tok) +- `test_backup_command.py` — Tests for daf backup command. (~2285 tok) +- `test_backup_manager.py` — Tests for backup/restore manager. (~4663 tok) +- `test_branch_conflict.py` — Tests for branch conflict resolution in _handle_branch_creation. (~4533 tok) +- `test_branch_creation_no_sync_prompt.py` — Tests for issue #139: Unnecessary sync strategy prompt after creating new branch. (~3359 tok) +- `test_branch_from_specific_source.py` — Tests for branch creation from specific source branch. (~3176 tok) +- `test_branch_import_sync.py` — Tests for PROJ-61023: Branch sync on import workflow. (~3361 tok) +- `test_branch_sync_fixes.py` — Tests for branch sync fixes from issue #324. (~3442 tok) +- `test_branch_workflow_ux_fixes.py` — Tests for issue #331: Branch workflow UX issues. (~5629 tok) +- `test_check_command.py` — Tests for daf check command. (~3490 tok) +- `test_claude_agent_skills_filtering.py` — Tests for Claude agent skills filtering in launch_with_prompt. (~4144 tok) +- `test_claude_commands.py` — Tests for devflow/utils/claude_commands.py - bundled skills installation. (~6382 tok) +- `test_claude_config_dir_integration.py` — Integration tests for CLAUDE_CONFIG_DIR environment variable support. (~1166 tok) +- `test_claude_mock.py` — Tests for MockClaudeCode. (~2214 tok) +- `test_cleanup_conversation.py` — Tests for cleanup_conversation command. (~1601 tok) +- `test_cleanup_sessions_command.py` — Tests for daf cleanup-sessions command. (~3720 tok) +- `test_cli_utils_extended.py` — Extended tests for CLI utility functions. (~3673 tok) +- `test_cli_utils.py` — Tests for CLI utility functions. (~8996 tok) +- `test_release_manager_pyproject.py` — Tests for ReleaseManager with pyproject.toml support. (~1968 tok) + +## tests/cli/commands/ + +- `test_export_command.py` — Tests for daf export command branch sync functionality (PROJ-60772). (~4725 tok) +- `test_feature_command.py` — Tests for daf feature CLI commands. (~9324 tok) +- `test_import_command.py` — Tests for daf import command with improved prompt clarity (PROJ-61022). (~2004 tok) diff --git a/.wolf/buglog.json b/.wolf/buglog.json new file mode 100644 index 0000000..4136cd6 --- /dev/null +++ b/.wolf/buglog.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "bugs": [ + { + "id": "bug-360", + "timestamp": "2026-04-06T01:50:00Z", + "error_message": "daf release command fails with pyproject.toml - expects setup.py version field - ValueError: Could not find version in setup.py", + "file": "devflow/release/manager.py, devflow/cli/commands/release_command.py", + "root_cause": "ReleaseManager was hardcoded to read/write version from setup.py, but project migrated to pyproject.toml (PEP 517/518/621) format where setup.py is just an empty setup() call with no version field", + "fix": "Modified ReleaseManager to: (1) Check pyproject.toml first for version field, fall back to setup.py for backward compatibility; (2) Update version in pyproject.toml when it exists; (3) Skip empty setup.py files during updates; (4) Show correct package file names (pyproject.toml vs setup.py) in error messages and commit messages; (5) Added comprehensive test coverage (8 new tests)", + "tags": ["release", "pyproject.toml", "packaging", "version-management", "pep-517", "pep-518", "pep-621"], + "related_bugs": [], + "occurrences": 1, + "last_seen": "2026-04-06T01:50:00Z" + } + ] +} \ No newline at end of file diff --git a/.wolf/cerebrum.md b/.wolf/cerebrum.md new file mode 100644 index 0000000..4cfcfb7 --- /dev/null +++ b/.wolf/cerebrum.md @@ -0,0 +1,23 @@ +# Cerebrum + +> OpenWolf's learning memory. Updated automatically as the AI learns from interactions. +> Do not edit manually unless correcting an error. +> Last updated: 2026-04-06 + +## User Preferences + + + +## Key Learnings + +- **Project:** devaiflow +- **Description:** [![PyPI version](https://badge.fury.io/py/devaiflow.svg)](https://badge.fury.io/py/devaiflow) + +## Do-Not-Repeat + + + + +## Decision Log + + diff --git a/.wolf/config.json b/.wolf/config.json new file mode 100644 index 0000000..d2c76ae --- /dev/null +++ b/.wolf/config.json @@ -0,0 +1,73 @@ +{ + "version": 1, + "openwolf": { + "enabled": true, + "anatomy": { + "auto_scan_on_init": true, + "rescan_interval_hours": 6, + "max_description_length": 100, + "max_files": 500, + "exclude_patterns": [ + "node_modules", + ".git", + "dist", + "build", + ".wolf", + ".next", + ".nuxt", + "coverage", + "__pycache__", + ".cache", + "target", + ".vscode", + ".idea", + ".turbo", + ".vercel", + ".netlify", + ".output", + "*.min.js", + "*.min.css" + ] + }, + "token_audit": { + "enabled": true, + "report_frequency": "weekly", + "waste_threshold_percent": 15, + "chars_per_token_code": 3.5, + "chars_per_token_prose": 4.0 + }, + "cron": { + "enabled": true, + "max_retry_attempts": 3, + "dead_letter_enabled": true, + "heartbeat_interval_minutes": 30, + "use_claude_p": true, + "api_key_env": null + }, + "memory": { + "consolidation_after_days": 7, + "max_entries_before_consolidation": 200 + }, + "cerebrum": { + "max_tokens": 2000, + "reflection_frequency": "weekly" + }, + "daemon": { + "port": 18790, + "log_level": "info" + }, + "dashboard": { + "enabled": true, + "port": 18791 + }, + "designqc": { + "enabled": true, + "viewports": [ + { "name": "desktop", "width": 1440, "height": 900 }, + { "name": "mobile", "width": 375, "height": 812 } + ], + "max_screenshots": 6, + "chrome_path": null + } + } +} diff --git a/.wolf/cron-manifest.json b/.wolf/cron-manifest.json new file mode 100644 index 0000000..7e2d7f3 --- /dev/null +++ b/.wolf/cron-manifest.json @@ -0,0 +1,97 @@ +{ + "version": 1, + "tasks": [ + { + "id": "anatomy-rescan", + "name": "Full anatomy rescan", + "schedule": "0 */6 * * *", + "description": "Re-scans project filesystem and reconciles anatomy.md", + "action": { "type": "scan_project" }, + "retry": { + "max_attempts": 3, + "backoff": "exponential", + "base_delay_seconds": 30 + }, + "failsafe": { + "on_failure": "log_and_continue", + "alert_after_consecutive_failures": 2, + "dead_letter": true + }, + "enabled": true + }, + { + "id": "memory-consolidation", + "name": "Consolidate old memory", + "schedule": "0 2 * * *", + "description": "Compress memory.md entries older than 7 days", + "action": { + "type": "consolidate_memory", + "params": { "older_than_days": 7 } + }, + "retry": { + "max_attempts": 2, + "backoff": "exponential", + "base_delay_seconds": 60 + }, + "failsafe": { + "on_failure": "skip_and_retry_next_cycle", + "dead_letter": false + }, + "enabled": true + }, + { + "id": "token-audit", + "name": "Token audit report", + "schedule": "0 0 * * 1", + "description": "Weekly waste pattern detection", + "action": { "type": "generate_token_report" }, + "retry": { + "max_attempts": 2, + "backoff": "linear", + "base_delay_seconds": 60 + }, + "failsafe": { "on_failure": "log_and_continue", "dead_letter": true }, + "enabled": true + }, + { + "id": "cerebrum-reflection", + "name": "Cerebrum reflection", + "schedule": "0 3 * * 0", + "description": "Weekly AI review of cerebrum.md — prune stale entries, consolidate duplicates", + "action": { + "type": "ai_task", + "params": { + "prompt": "Review this cerebrum.md. Remove duplicate preferences (keep newer). Remove Do-Not-Repeat entries older than 90 days if no longer relevant. Consolidate Key Learnings that overlap. Keep the file under 2000 tokens. Return the cleaned file content only.", + "context_files": [".wolf/cerebrum.md"] + } + }, + "retry": { + "max_attempts": 1, + "backoff": "none", + "base_delay_seconds": 0 + }, + "failsafe": { "on_failure": "log_and_continue", "dead_letter": false }, + "enabled": true + }, + { + "id": "project-suggestions", + "name": "AI suggestions", + "schedule": "0 4 * * 1", + "description": "Weekly AI analysis with project improvement suggestions", + "action": { + "type": "ai_task", + "params": { + "prompt": "Based on the recent memory entries and current project structure, provide: 1) Key achievements this week, 2) Code improvements to consider, 3) Logical next tasks, 4) Technical debt or risks. Be specific and actionable. Return as JSON: {achievements:[], improvements:[], next_tasks:[], risks:[]}", + "context_files": [".wolf/memory.md", ".wolf/anatomy.md"] + } + }, + "retry": { + "max_attempts": 1, + "backoff": "none", + "base_delay_seconds": 0 + }, + "failsafe": { "on_failure": "log_and_continue", "dead_letter": false }, + "enabled": true + } + ] +} diff --git a/.wolf/cron-state.json b/.wolf/cron-state.json new file mode 100644 index 0000000..ce4dcb7 --- /dev/null +++ b/.wolf/cron-state.json @@ -0,0 +1,7 @@ +{ + "last_heartbeat": null, + "engine_status": "initialized", + "execution_log": [], + "dead_letter_queue": [], + "upcoming": [] +} diff --git a/.wolf/designqc-report.json b/.wolf/designqc-report.json new file mode 100644 index 0000000..8f658ee --- /dev/null +++ b/.wolf/designqc-report.json @@ -0,0 +1,6 @@ +{ + "captured_at": null, + "captures": [], + "total_size_kb": 0, + "estimated_tokens": 0 +} \ No newline at end of file diff --git a/.wolf/hooks/_session.json b/.wolf/hooks/_session.json new file mode 100644 index 0000000..abfed3d --- /dev/null +++ b/.wolf/hooks/_session.json @@ -0,0 +1,12 @@ +{ + "session_id": "session-2026-04-06-0859", + "started": "2026-04-06T12:59:51.371Z", + "files_read": {}, + "files_written": [], + "edit_counts": {}, + "anatomy_hits": 0, + "anatomy_misses": 0, + "repeated_reads_warned": 0, + "cerebrum_warnings": 0, + "stop_count": 1 +} \ No newline at end of file diff --git a/.wolf/hooks/package.json b/.wolf/hooks/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/.wolf/hooks/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/.wolf/hooks/post-read.js b/.wolf/hooks/post-read.js new file mode 100644 index 0000000..5d22fbb --- /dev/null +++ b/.wolf/hooks/post-read.js @@ -0,0 +1,69 @@ +import * as path from "node:path"; +import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, estimateTokens, readStdin, normalizePath } from "./shared.js"; +async function main() { + ensureWolfDir(); + const wolfDir = getWolfDir(); + const hooksDir = path.join(wolfDir, "hooks"); + const sessionFile = path.join(hooksDir, "_session.json"); + const raw = await readStdin(); + let input; + try { + input = JSON.parse(raw); + } + catch { + process.exit(0); + return; + } + const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? ""; + const content = input.tool_output?.content ?? ""; + if (!filePath) { + process.exit(0); + return; + } + const normalizedFile = normalizePath(filePath); + // Skip tracking for .wolf/ internal files — consistent with pre-read + const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd()); + const relToProject = normalizedFile.startsWith(projectDir) + ? normalizedFile.slice(projectDir.length).replace(/^\//, "") + : ""; + if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) { + process.exit(0); + return; + } + const ext = path.extname(filePath).toLowerCase(); + const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".rs", ".go", ".java", ".c", ".cpp", ".css", ".json", ".yaml", ".yml"]); + const proseExts = new Set([".md", ".txt", ".rst"]); + const type = codeExts.has(ext) ? "code" : proseExts.has(ext) ? "prose" : "mixed"; + let tokens = content ? estimateTokens(content, type) : 0; + // Fallback: if tool_output had no content, use anatomy token estimate + if (tokens === 0) { + const anatomyContent = readMarkdown(path.join(wolfDir, "anatomy.md")); + const sections = parseAnatomy(anatomyContent); + for (const [sectionKey, entries] of sections) { + for (const entry of entries) { + const entryRelPath = normalizePath(path.join(sectionKey, entry.file)); + if (normalizedFile.endsWith(entryRelPath) || normalizedFile.endsWith("/" + entryRelPath)) { + tokens = entry.tokens; + break; + } + } + if (tokens > 0) + break; + } + } + const session = readJSON(sessionFile, { files_read: {} }); + if (session.files_read[normalizedFile]) { + session.files_read[normalizedFile].tokens = tokens; + } + else { + session.files_read[normalizedFile] = { + count: 1, + tokens, + first_read: new Date().toISOString(), + }; + } + writeJSON(sessionFile, session); + process.exit(0); +} +main().catch(() => process.exit(0)); +//# sourceMappingURL=post-read.js.map \ No newline at end of file diff --git a/.wolf/hooks/post-write.js b/.wolf/hooks/post-write.js new file mode 100644 index 0000000..9a5ce97 --- /dev/null +++ b/.wolf/hooks/post-write.js @@ -0,0 +1,503 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as crypto from "node:crypto"; +import { getWolfDir, ensureWolfDir, readJSON, writeJSON, parseAnatomy, serializeAnatomy, extractDescription, estimateTokens, appendMarkdown, timeShort, readStdin, normalizePath } from "./shared.js"; +async function main() { + ensureWolfDir(); + const wolfDir = getWolfDir(); + const hooksDir = path.join(wolfDir, "hooks"); + const sessionFile = path.join(hooksDir, "_session.json"); + const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd(); + const raw = await readStdin(); + let input; + try { + input = JSON.parse(raw); + } + catch { + process.exit(0); + return; + } + const toolName = input.tool_name ?? "Write"; + const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? ""; + if (!filePath) { + process.exit(0); + return; + } + const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath); + // Skip processing for .wolf/ internal files to avoid slow self-referential updates + const relPath = normalizePath(path.relative(projectRoot, absolutePath)); + if (relPath.startsWith(".wolf/")) { + process.exit(0); + return; + } + // Never track .env files in anatomy — they contain secrets + const baseName = path.basename(absolutePath); + if (baseName === ".env" || baseName.startsWith(".env.")) { + process.exit(0); + return; + } + const oldStr = input.tool_input?.old_string ?? ""; + const newStr = input.tool_input?.new_string ?? ""; + // 1. Update anatomy.md + try { + const anatomyPath = path.join(wolfDir, "anatomy.md"); + let anatomyContent; + try { + anatomyContent = fs.readFileSync(anatomyPath, "utf-8"); + } + catch { + anatomyContent = "# anatomy.md\n\n> Auto-maintained by OpenWolf.\n"; + } + const sections = parseAnatomy(anatomyContent); + const relPathLocal = normalizePath(path.relative(projectRoot, absolutePath)); + const dir = path.dirname(relPathLocal); + const fileName = path.basename(relPathLocal); + const sectionKey = dir === "." ? "./" : dir + "/"; + let fileContent = ""; + try { + fileContent = fs.readFileSync(absolutePath, "utf-8"); + } + catch { + fileContent = input.tool_input?.content ?? ""; + } + const desc = extractDescription(absolutePath).slice(0, 100); + const ext = path.extname(absolutePath).toLowerCase(); + const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".json", ".yaml", ".yml", ".css"]); + const proseExts = new Set([".md", ".txt", ".rst"]); + const type = codeExts.has(ext) ? "code" : proseExts.has(ext) ? "prose" : "mixed"; + const tokens = estimateTokens(fileContent, type); + if (!sections.has(sectionKey)) + sections.set(sectionKey, []); + const entries = sections.get(sectionKey); + const idx = entries.findIndex((e) => e.file === fileName); + if (idx !== -1) { + entries[idx] = { file: fileName, description: desc, tokens }; + } + else { + entries.push({ file: fileName, description: desc, tokens }); + } + let fileCount = 0; + for (const [, list] of sections) + fileCount += list.length; + const serialized = serializeAnatomy(sections, { + lastScanned: new Date().toISOString(), + fileCount, + hits: 0, + misses: 0, + }); + const tmp = anatomyPath + "." + crypto.randomBytes(4).toString("hex") + ".tmp"; + try { + fs.writeFileSync(tmp, serialized, "utf-8"); + fs.renameSync(tmp, anatomyPath); + } + catch { + try { + fs.writeFileSync(anatomyPath, serialized, "utf-8"); + } + catch { } + try { + fs.unlinkSync(tmp); + } + catch { } + } + } + catch { } + // 2. Append richer entry to memory.md + try { + const action = toolName === "Write" ? "Created" : toolName === "MultiEdit" ? "Multi-edited" : "Edited"; + const relFile = normalizePath(path.relative(projectRoot, absolutePath)); + const fileContent = input.tool_input?.content ?? ""; + const ext = path.extname(absolutePath).toLowerCase(); + const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".json", ".yaml", ".yml", ".css"]); + const type = codeExts.has(ext) ? "code" : "mixed"; + const writeTokens = estimateTokens(fileContent || newStr, type); + let changeDesc = ""; + if (oldStr && newStr) { + changeDesc = summarizeEdit(oldStr, newStr, baseName); + } + const memoryPath = path.join(wolfDir, "memory.md"); + const outcome = changeDesc || "—"; + appendMarkdown(memoryPath, `| ${timeShort()} | ${action} ${relFile} | ${outcome} | ~${writeTokens} |\n`); + } + catch { } + // 3. Record in session tracker + track edit counts + try { + const session = readJSON(sessionFile, { files_written: [], edit_counts: {} }); + if (!session.edit_counts) + session.edit_counts = {}; + const normalizedFile = normalizePath(filePath); + const action = toolName === "Write" ? "create" : "edit"; + const fileContent = input.tool_input?.content ?? ""; + const tokens = estimateTokens(fileContent || newStr, "code"); + session.files_written.push({ + file: normalizedFile, + action, + tokens, + at: new Date().toISOString(), + }); + const editKey = normalizePath(path.relative(projectRoot, absolutePath)); + session.edit_counts[editKey] = (session.edit_counts[editKey] || 0) + 1; + writeJSON(sessionFile, session); + if (session.edit_counts[editKey] >= 3) { + process.stderr.write(`⚠️ OpenWolf: ${baseName} has been edited ${session.edit_counts[editKey]} times this session. If you're fixing a bug, remember to log it to .wolf/buglog.json.\n`); + } + } + catch { } + // 4. Auto-detect bug-fix patterns and log them + try { + if (oldStr && newStr) { + autoDetectBugFix(wolfDir, absolutePath, projectRoot, oldStr, newStr); + } + } + catch { } + process.exit(0); +} +// ─── Edit Summarizer ───────────────────────────────────────────── +function summarizeEdit(oldStr, newStr, filename) { + const oldLines = oldStr.split("\n"); + const newLines = newStr.split("\n"); + const oldCount = oldLines.length; + const newCount = newLines.length; + const ext = path.extname(filename).toLowerCase(); + // --- Structural fixes --- + if (newStr.includes("try") && newStr.includes("catch") && !oldStr.includes("catch")) { + return "added error handling"; + } + if (newStr.includes("?.") && !oldStr.includes("?.")) + return "added optional chaining"; + if (newStr.includes("?? ") && !oldStr.includes("?? ")) + return "added nullish coalescing"; + // --- Deleted code --- + if (!newStr.trim() || newStr.trim().length < oldStr.trim().length * 0.2) { + return `removed ${oldCount} lines`; + } + // --- Import changes --- + const oldImports = oldLines.filter(l => /^\s*(import|require|use |from )/.test(l)).length; + const newImports = newLines.filter(l => /^\s*(import|require|use |from )/.test(l)).length; + if (newImports > oldImports && Math.abs(newCount - oldCount) <= newImports - oldImports + 1) { + return `added ${newImports - oldImports} import(s)`; + } + // --- Value/string replacement (common bug fix: wrong value) --- + if (oldCount === 1 && newCount === 1) { + const o = oldStr.trim(); + const n = newStr.trim(); + // String literal change + const oStr = o.match(/['"`]([^'"`]+)['"`]/); + const nStr = n.match(/['"`]([^'"`]+)['"`]/); + if (oStr && nStr && oStr[1] !== nStr[1]) { + return `"${oStr[1].slice(0, 25)}" → "${nStr[1].slice(0, 25)}"`; + } + // Number change + const oNum = o.match(/\b(\d+\.?\d*)\b/); + const nNum = n.match(/\b(\d+\.?\d*)\b/); + if (oNum && nNum && oNum[1] !== nNum[1] && o.replace(oNum[1], "") === n.replace(nNum[1], "")) { + return `${oNum[1]} → ${nNum[1]}`; + } + return "inline fix"; + } + // --- Method/function call changes --- + const oldCalls = extractCalls(oldStr); + const newCalls = extractCalls(newStr); + const addedCalls = newCalls.filter(c => !oldCalls.includes(c)); + const removedCalls = oldCalls.filter(c => !newCalls.includes(c)); + if (removedCalls.length === 1 && addedCalls.length === 1) { + return `${removedCalls[0]}() → ${addedCalls[0]}()`; + } + // --- CSS/style changes --- + if (ext === ".css" || ext === ".scss" || ext === ".vue" || ext === ".tsx" || ext === ".jsx") { + const oldProps = (oldStr.match(/[\w-]+\s*:/g) || []).map(p => p.replace(/\s*:/, "")); + const newProps = (newStr.match(/[\w-]+\s*:/g) || []).map(p => p.replace(/\s*:/, "")); + const changed = newProps.filter(p => !oldProps.includes(p)); + if (changed.length > 0 && changed.length <= 3) { + return `CSS: ${changed.join(", ")}`; + } + } + // --- Condition changes --- + const oldConds = (oldStr.match(/if\s*\(([^)]+)\)/g) || []); + const newConds = (newStr.match(/if\s*\(([^)]+)\)/g) || []); + if (newConds.length > oldConds.length) { + return `added ${newConds.length - oldConds.length} condition(s)`; + } + // --- Function modified --- + const fnMatch = newStr.match(/(?:function|def|fn|func|async\s+function)\s+(\w+)/); + if (fnMatch) { + return `modified ${fnMatch[1]}()`; + } + // --- Class/method context --- + const methodMatch = newStr.match(/(?:public|private|protected)?\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/); + if (methodMatch) { + return `modified ${methodMatch[1]}()`; + } + // --- Size-based fallback --- + if (newCount > oldCount + 5) + return `expanded (+${newCount - oldCount} lines)`; + if (oldCount > newCount + 5) + return `reduced (-${oldCount - newCount} lines)`; + return `${oldCount}→${newCount} lines`; +} +function extractCalls(code) { + return [...new Set((code.match(/(\w+)\s*\(/g) || []) + .map(m => m.match(/(\w+)/)?.[1] || "") + .filter(n => n.length > 2 && !["if", "for", "while", "switch", "catch", "function", "return", "new", "typeof", "instanceof", "const", "let", "var"].includes(n)))]; +} +// ─── Auto Bug Detection ────────────────────────────────────────── +function autoDetectBugFix(wolfDir, absolutePath, projectRoot, oldStr, newStr) { + const bugLogPath = path.join(wolfDir, "buglog.json"); + const bugLog = readJSON(bugLogPath, { version: 1, bugs: [] }); + const relFile = normalizePath(path.relative(projectRoot, absolutePath)); + const basename = path.basename(absolutePath); + const ext = path.extname(basename).toLowerCase(); + // Detect what kind of fix this is + const detection = detectFixPattern(oldStr, newStr, ext); + if (!detection) + return; + // Check for recent duplicate (same file + same category within 5 min) + const recentDupe = bugLog.bugs.find(b => { + if (path.basename(b.file) !== basename) + return false; + if (!b.tags.includes("auto-detected")) + return false; + if (!b.tags.includes(detection.category)) + return false; + const bugTime = new Date(b.last_seen).getTime(); + return (Date.now() - bugTime) < 5 * 60 * 1000; + }); + if (recentDupe) { + recentDupe.occurrences++; + recentDupe.last_seen = new Date().toISOString(); + // Append additional context + if (detection.context && !recentDupe.fix.includes(detection.context)) { + recentDupe.fix += ` | Also: ${detection.context}`; + } + writeJSON(bugLogPath, bugLog); + return; + } + const nextId = `bug-${String(bugLog.bugs.length + 1).padStart(3, "0")}`; + bugLog.bugs.push({ + id: nextId, + timestamp: new Date().toISOString(), + error_message: detection.summary, + file: relFile, + root_cause: detection.rootCause, + fix: detection.fix, + tags: ["auto-detected", detection.category, ext.replace(".", "") || "unknown"], + related_bugs: [], + occurrences: 1, + last_seen: new Date().toISOString(), + }); + writeJSON(bugLogPath, bugLog); +} +function detectFixPattern(oldStr, newStr, ext) { + const oldLines = oldStr.split("\n"); + const newLines = newStr.split("\n"); + // --- Error handling added --- + if (newStr.includes("catch") && !oldStr.includes("catch")) { + const fn = newStr.match(/(?:function|def|async)\s+(\w+)/)?.[1] || "unknown"; + return { + category: "error-handling", + summary: `Missing error handling in ${path.basename(fn)}`, + rootCause: "Code path had no error handling — exceptions would propagate uncaught", + fix: `Added try/catch block`, + context: extractChangedLines(oldStr, newStr), + }; + } + // --- Null/undefined safety --- + if ((newStr.includes("?.") && !oldStr.includes("?.")) || + (newStr.includes("?? ") && !oldStr.includes("?? ")) || + (/!==?\s*(null|undefined)/.test(newStr) && !/!==?\s*(null|undefined)/.test(oldStr))) { + return { + category: "null-safety", + summary: `Null/undefined access in ${path.basename(path.basename(""))}`, + rootCause: "Property access on potentially null/undefined value", + fix: `Added null safety (optional chaining or null check)`, + context: extractChangedLines(oldStr, newStr), + }; + } + // --- Guard clause / early return added --- + if (/if\s*\([^)]*\)\s*(return|throw|continue|break)/.test(newStr) && + !/if\s*\([^)]*\)\s*(return|throw|continue|break)/.test(oldStr)) { + const condition = newStr.match(/if\s*\(([^)]+)\)/)?.[1]?.trim().slice(0, 60) || "condition"; + return { + category: "guard-clause", + summary: `Missing guard clause`, + rootCause: `No early return/throw for edge case: ${condition}`, + fix: `Added guard clause: if (${condition.slice(0, 40)})`, + }; + } + // --- Wrong value / string fix (very common bug) --- + if (oldLines.length <= 3 && newLines.length <= 3) { + const oldJoined = oldStr.trim(); + const newJoined = newStr.trim(); + // String literal changed + const oStrs = oldJoined.match(/['"`]([^'"`]{2,})['"`]/g) || []; + const nStrs = newJoined.match(/['"`]([^'"`]{2,})['"`]/g) || []; + if (oStrs.length > 0 && nStrs.length > 0) { + for (let i = 0; i < Math.min(oStrs.length, nStrs.length); i++) { + if (oStrs[i] !== nStrs[i]) { + return { + category: "wrong-value", + summary: `Incorrect value in code`, + rootCause: `Had ${oStrs[i].slice(0, 50)}`, + fix: `Changed to ${nStrs[i].slice(0, 50)}`, + }; + } + } + } + // Variable name / method call changed + const oldTokens = tokenizeCode(oldJoined); + const newTokens = tokenizeCode(newJoined); + const changed = []; + for (let i = 0; i < Math.min(oldTokens.length, newTokens.length); i++) { + if (oldTokens[i] !== newTokens[i]) { + changed.push([oldTokens[i], newTokens[i]]); + } + } + if (changed.length === 1 && changed[0][0].length > 2) { + return { + category: "wrong-reference", + summary: `Wrong reference: ${changed[0][0]} should be ${changed[0][1]}`, + rootCause: `Used "${changed[0][0]}" instead of "${changed[0][1]}"`, + fix: `Changed ${changed[0][0]} → ${changed[0][1]}`, + }; + } + } + // --- Logic fix (condition changed) --- + const oldCond = oldStr.match(/if\s*\(([^)]+)\)/)?.[1]; + const newCond = newStr.match(/if\s*\(([^)]+)\)/)?.[1]; + if (oldCond && newCond && oldCond !== newCond && oldLines.length <= 5) { + return { + category: "logic-fix", + summary: `Wrong condition in logic`, + rootCause: `Condition was: if (${oldCond.slice(0, 50)})`, + fix: `Changed to: if (${newCond.slice(0, 50)})`, + }; + } + // --- Operator fix (=== vs ==, > vs >=, etc.) --- + const opChange = findOperatorChange(oldStr, newStr); + if (opChange) { + return { + category: "operator-fix", + summary: `Wrong operator: ${opChange.old} should be ${opChange.new}`, + rootCause: `Used "${opChange.old}" instead of "${opChange.new}"`, + fix: `Changed operator ${opChange.old} → ${opChange.new}`, + }; + } + // --- Missing import/require --- + const oldImports = new Set((oldStr.match(/(?:import|require)\s*\(?['"]([^'"]+)['"]\)?/g) || []).map(m => m)); + const newImports = (newStr.match(/(?:import|require)\s*\(?['"]([^'"]+)['"]\)?/g) || []); + const addedImports = newImports.filter(i => !oldImports.has(i)); + if (addedImports.length > 0 && newLines.length - oldLines.length <= addedImports.length + 2) { + const modules = addedImports.map(i => i.match(/['"]([^'"]+)['"]/)?.[1] || "").filter(Boolean); + return { + category: "missing-import", + summary: `Missing import: ${modules.join(", ")}`, + rootCause: `Module(s) not imported: ${modules.join(", ")}`, + fix: `Added import(s) for ${modules.join(", ")}`, + }; + } + // --- Return value fix --- + const oldReturn = oldStr.match(/return\s+(.+)/)?.[1]?.trim(); + const newReturn = newStr.match(/return\s+(.+)/)?.[1]?.trim(); + if (oldReturn && newReturn && oldReturn !== newReturn && oldLines.length <= 5) { + return { + category: "return-value", + summary: `Wrong return value`, + rootCause: `Was returning: ${oldReturn.slice(0, 50)}`, + fix: `Now returns: ${newReturn.slice(0, 50)}`, + }; + } + // --- Async/await fix --- + if (newStr.includes("await ") && !oldStr.includes("await ")) { + return { + category: "async-fix", + summary: `Missing await`, + rootCause: `Async call without await — returned Promise instead of value`, + fix: `Added await to async call`, + context: extractChangedLines(oldStr, newStr), + }; + } + if (newStr.includes("async ") && !oldStr.includes("async ")) { + return { + category: "async-fix", + summary: `Function not marked async`, + rootCause: `Function uses await but wasn't declared async`, + fix: `Added async modifier`, + }; + } + // --- Type annotation/cast fix --- + if (ext === ".ts" || ext === ".tsx") { + if ((newStr.includes(" as ") && !oldStr.includes(" as ")) || + (newStr.includes(": ") && !oldStr.includes(": ") && oldLines.length <= 3)) { + return { + category: "type-fix", + summary: `Type error`, + rootCause: `Missing or incorrect type annotation`, + fix: `Added type assertion/annotation`, + context: extractChangedLines(oldStr, newStr), + }; + } + } + // --- CSS/style fix --- + if (ext === ".css" || ext === ".scss" || ext === ".vue" || ext === ".tsx" || ext === ".jsx") { + const oldProps = extractCSSProps(oldStr); + const newProps = extractCSSProps(newStr); + const changedProps = [...newProps.entries()].filter(([k, v]) => oldProps.get(k) !== v && oldProps.has(k)); + if (changedProps.length > 0 && changedProps.length <= 3) { + const desc = changedProps.map(([k, v]) => `${k}: ${oldProps.get(k)} → ${v}`).join("; "); + return { + category: "style-fix", + summary: `CSS fix: ${changedProps.map(([k]) => k).join(", ")}`, + rootCause: desc, + fix: `Changed ${desc}`, + }; + } + } + // --- Significant diff (catch-all for substantial edits) --- + const diffRatio = Math.abs(newStr.length - oldStr.length) / Math.max(oldStr.length, 1); + if (diffRatio > 0.3 && oldLines.length >= 3 && newLines.length >= 3) { + // Only log if there's meaningful structural change, not just additions + const removedLines = oldLines.filter(l => l.trim() && !newLines.some(nl => nl.trim() === l.trim())); + if (removedLines.length >= 2) { + return { + category: "refactor", + summary: `Significant refactor of ${path.basename("")}`, + rootCause: `${removedLines.length} lines replaced/restructured`, + fix: `Rewrote ${oldLines.length}→${newLines.length} lines (${removedLines.length} removed)`, + context: removedLines.slice(0, 2).map(l => l.trim().slice(0, 50)).join("; "), + }; + } + } + return null; +} +function extractChangedLines(oldStr, newStr) { + const oldLines = new Set(oldStr.split("\n").map(l => l.trim()).filter(Boolean)); + const newLines = newStr.split("\n").map(l => l.trim()).filter(Boolean); + const added = newLines.filter(l => !oldLines.has(l)); + return added.slice(0, 2).map(l => l.slice(0, 60)).join("; "); +} +function tokenizeCode(code) { + return code.replace(/[^\w$]/g, " ").split(/\s+/).filter(t => t.length > 0); +} +function findOperatorChange(oldStr, newStr) { + const operators = ["===", "!==", "==", "!=", ">=", "<=", ">>", "<<", "&&", "||", "??"]; + for (const op of operators) { + if (oldStr.includes(op) && !newStr.includes(op)) { + for (const op2 of operators) { + if (op2 !== op && newStr.includes(op2) && !oldStr.includes(op2)) { + return { old: op, new: op2 }; + } + } + } + } + return null; +} +function extractCSSProps(code) { + const props = new Map(); + const matches = code.matchAll(/([\w-]+)\s*:\s*([^;}\n]+)/g); + for (const m of matches) { + props.set(m[1].trim(), m[2].trim()); + } + return props; +} +main().catch(() => process.exit(0)); +//# sourceMappingURL=post-write.js.map \ No newline at end of file diff --git a/.wolf/hooks/pre-read.js b/.wolf/hooks/pre-read.js new file mode 100644 index 0000000..6ba67d0 --- /dev/null +++ b/.wolf/hooks/pre-read.js @@ -0,0 +1,80 @@ +import * as path from "node:path"; +import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, readStdin, normalizePath } from "./shared.js"; +async function main() { + ensureWolfDir(); + const wolfDir = getWolfDir(); + const hooksDir = path.join(wolfDir, "hooks"); + const sessionFile = path.join(hooksDir, "_session.json"); + const raw = await readStdin(); + let input; + try { + input = JSON.parse(raw); + } + catch { + process.exit(0); + return; + } + const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? ""; + if (!filePath) { + process.exit(0); + return; + } + const normalizedFile = normalizePath(filePath); + // Skip tracking for .wolf/ internal files — they're infrastructure, not project files. + // Counting them inflates anatomy miss rates since .wolf/ is excluded from anatomy scanning. + const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd()); + const relToProject = normalizedFile.startsWith(projectDir) + ? normalizedFile.slice(projectDir.length).replace(/^\//, "") + : ""; + if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) { + process.exit(0); + return; + } + const session = readJSON(sessionFile, { + session_id: "", files_read: {}, anatomy_hits: 0, anatomy_misses: 0, + repeated_reads_warned: 0, + }); + // Check if already read this session + if (session.files_read[normalizedFile]) { + const prev = session.files_read[normalizedFile]; + process.stderr.write(`⚡ OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tokens). Consider using your existing knowledge of this file.\n`); + session.files_read[normalizedFile].count++; + session.repeated_reads_warned++; + writeJSON(sessionFile, session); + process.exit(0); + return; + } + // Check anatomy.md for this file + const anatomyContent = readMarkdown(path.join(wolfDir, "anatomy.md")); + const sections = parseAnatomy(anatomyContent); + let found = false; + for (const [sectionKey, entries] of sections) { + for (const entry of entries) { + // Build the full relative path from the section key + filename for accurate matching + const entryRelPath = normalizePath(path.join(sectionKey, entry.file)); + if (normalizedFile.endsWith(entryRelPath) || normalizedFile.endsWith("/" + entryRelPath)) { + process.stderr.write(`📋 OpenWolf anatomy: ${entry.file} — ${entry.description} (~${entry.tokens} tok)\n`); + found = true; + break; + } + } + if (found) + break; + } + if (found) { + session.anatomy_hits++; + } + else { + session.anatomy_misses++; + } + // Record initial read entry (tokens will be updated in post-read) + session.files_read[normalizedFile] = { + count: 1, + tokens: 0, + first_read: new Date().toISOString(), + }; + writeJSON(sessionFile, session); + process.exit(0); +} +main().catch(() => process.exit(0)); +//# sourceMappingURL=pre-read.js.map \ No newline at end of file diff --git a/.wolf/hooks/pre-write.js b/.wolf/hooks/pre-write.js new file mode 100644 index 0000000..2e84303 --- /dev/null +++ b/.wolf/hooks/pre-write.js @@ -0,0 +1,121 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { getWolfDir, ensureWolfDir, readJSON, readMarkdown, readStdin } from "./shared.js"; +async function main() { + ensureWolfDir(); + const wolfDir = getWolfDir(); + const raw = await readStdin(); + let input; + try { + input = JSON.parse(raw); + } + catch { + process.exit(0); + return; + } + // For Edit tool, the meaningful content is old_string + new_string + const content = input.tool_input?.content ?? ""; + const oldStr = input.tool_input?.old_string ?? ""; + const newStr = input.tool_input?.new_string ?? ""; + const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? ""; + const allContent = [content, oldStr, newStr].join("\n"); + if (!allContent.trim()) { + process.exit(0); + return; + } + // 1. Cerebrum Do-Not-Repeat check + checkCerebrum(wolfDir, allContent); + // 2. Bug log: search for similar past bugs when editing code + // This fires when Claude is about to edit a file — if the edit looks like a fix + // (changing error handling, modifying catch blocks, etc.), check the bug log + if (filePath && (oldStr || content)) { + checkBugLog(wolfDir, filePath, oldStr, newStr, content); + } + process.exit(0); +} +function checkCerebrum(wolfDir, content) { + const cerebrumContent = readMarkdown(path.join(wolfDir, "cerebrum.md")); + const doNotRepeatSection = cerebrumContent.split("## Do-Not-Repeat")[1]; + if (!doNotRepeatSection) + return; + const entries = doNotRepeatSection.split("## ")[0]; + const lines = entries.split("\n").filter((l) => l.trim().startsWith("[") || l.trim().startsWith("-")); + for (const line of lines) { + const trimmed = line.trim().replace(/^[-*]\s*/, "").replace(/^\[[\d-]+\]\s*/, ""); + if (!trimmed) + continue; + const patterns = []; + const quotedMatches = trimmed.match(/"([^"]+)"/g) || trimmed.match(/'([^']+)'/g) || trimmed.match(/`([^`]+)`/g); + if (quotedMatches) { + for (const qm of quotedMatches) { + patterns.push(qm.replace(/["'`]/g, "")); + } + } + const neverMatch = trimmed.match(/(?:never use|avoid|don't use|do not use)\s+(\w+)/i); + if (neverMatch) + patterns.push(neverMatch[1]); + for (const pattern of patterns) { + try { + const regex = new RegExp(`\\b${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i"); + if (regex.test(content)) { + process.stderr.write(`⚠️ OpenWolf cerebrum warning: "${trimmed}" — check your code before proceeding.\n`); + } + } + catch { } + } + } +} +// Common words that appear in most code — must be excluded from similarity matching +const STOP_WORDS = new Set([ + "error", "function", "return", "const", "this", "that", "with", "from", + "import", "export", "class", "interface", "type", "undefined", "null", + "true", "false", "string", "number", "object", "array", "value", + "file", "path", "name", "data", "response", "request", "result", + "should", "must", "does", "have", "been", "will", "would", "could", + "when", "then", "else", "each", "some", "every", "only", +]); +function checkBugLog(wolfDir, filePath, oldStr, newStr, content) { + const bugLogPath = path.join(wolfDir, "buglog.json"); + if (!fs.existsSync(bugLogPath)) + return; + const bugLog = readJSON(bugLogPath, { version: 1, bugs: [] }); + if (bugLog.bugs.length === 0) + return; + const basename = path.basename(filePath); + // ONLY surface bugs that match the SAME file being edited. + // Cross-file matching is too noisy and risks misdirecting Claude. + const fileMatches = bugLog.bugs.filter(b => { + const bugBasename = path.basename(b.file); + return bugBasename === basename; + }); + if (fileMatches.length === 0) + return; + // Further filter: require tag or error_message overlap with the edit content + const editText = (oldStr + " " + newStr + " " + content).toLowerCase(); + const editTokens = tokenize(editText); + const relevant = fileMatches.filter(bug => { + // Check if any bug tag appears in the edit content + const tagHit = bug.tags.some(t => editText.includes(t.toLowerCase())); + if (tagHit) + return true; + // Check meaningful word overlap (excluding stop words) + const bugTokens = tokenize(bug.error_message + " " + bug.root_cause); + const overlap = [...editTokens].filter(t => bugTokens.has(t)); + // Require at least 3 meaningful overlapping words + return overlap.length >= 3; + }); + if (relevant.length === 0) + return; + // Surface as a FYI, not a directive — Claude should evaluate, not blindly apply + process.stderr.write(`📋 OpenWolf buglog: ${relevant.length} past bug(s) found for ${basename} — review for context, do NOT apply blindly:\n`); + for (const bug of relevant.slice(0, 2)) { + process.stderr.write(` [${bug.id}] "${bug.error_message.slice(0, 70)}"\n Cause: ${bug.root_cause.slice(0, 80)}\n Fix: ${bug.fix.slice(0, 80)}\n`); + } +} +function tokenize(text) { + return new Set(text.replace(/[^\w\s]/g, " ").split(/\s+/) + .filter(w => w.length > 3 && !STOP_WORDS.has(w.toLowerCase())) + .map(w => w.toLowerCase())); +} +main().catch(() => process.exit(0)); +//# sourceMappingURL=pre-write.js.map \ No newline at end of file diff --git a/.wolf/hooks/session-start.js b/.wolf/hooks/session-start.js new file mode 100644 index 0000000..7da38f3 --- /dev/null +++ b/.wolf/hooks/session-start.js @@ -0,0 +1,77 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, timestamp, timeShort } from "./shared.js"; +async function main() { + ensureWolfDir(); + const wolfDir = getWolfDir(); + // Clean up stale .tmp files left from failed atomic writes + try { + const files = fs.readdirSync(wolfDir); + for (const f of files) { + if (f.endsWith(".tmp")) { + try { + fs.unlinkSync(path.join(wolfDir, f)); + } + catch { } + } + } + } + catch { } + const hooksDir = path.join(wolfDir, "hooks"); + const sessionFile = path.join(hooksDir, "_session.json"); + const now = new Date(); + const sessionId = `session-${now.toISOString().slice(0, 10)}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`; + // Create fresh session state + writeJSON(sessionFile, { + session_id: sessionId, + started: timestamp(), + files_read: {}, + files_written: [], + edit_counts: {}, + anatomy_hits: 0, + anatomy_misses: 0, + repeated_reads_warned: 0, + cerebrum_warnings: 0, + stop_count: 0, + }); + // Append session header to memory.md + const memoryPath = path.join(wolfDir, "memory.md"); + const header = `\n## Session: ${now.toISOString().slice(0, 10)} ${timeShort()}\n\n| Time | Action | File(s) | Outcome | ~Tokens |\n|------|--------|---------|---------|--------|\n`; + appendMarkdown(memoryPath, header); + // Check cerebrum freshness — remind Claude to learn + try { + const cerebrumPath = path.join(wolfDir, "cerebrum.md"); + const cerebrumContent = fs.readFileSync(cerebrumPath, "utf-8"); + const stat = fs.statSync(cerebrumPath); + const daysSinceUpdate = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60 * 24); + // Count actual entries (non-comment, non-empty lines in content sections) + const entryLines = cerebrumContent.split("\n").filter(l => { + const t = l.trim(); + return t.startsWith("- ") || t.startsWith("* ") || (t.startsWith("[") && t.includes("]")); + }); + if (entryLines.length < 3) { + process.stderr.write(`💡 OpenWolf: cerebrum.md has only ${entryLines.length} entries. Learn from this session — record user preferences, project conventions, and mistakes to .wolf/cerebrum.md.\n`); + } + else if (daysSinceUpdate > 3) { + process.stderr.write(`💡 OpenWolf: cerebrum.md hasn't been updated in ${Math.floor(daysSinceUpdate)} days. Look for opportunities to add learnings this session.\n`); + } + } + catch { } + // Check buglog — remind if empty + try { + const buglogPath = path.join(wolfDir, "buglog.json"); + const buglog = readJSON(buglogPath, { bugs: [] }); + if (buglog.bugs.length === 0) { + process.stderr.write(`📋 OpenWolf: buglog.json is empty. If you encounter or fix any bugs, errors, or failed tests this session, log them to .wolf/buglog.json.\n`); + } + } + catch { } + // Increment total_sessions in token-ledger + const ledgerPath = path.join(wolfDir, "token-ledger.json"); + const ledger = readJSON(ledgerPath, { version: 1, lifetime: { total_sessions: 0 } }); + ledger.lifetime.total_sessions++; + writeJSON(ledgerPath, ledger); + process.exit(0); +} +main().catch(() => process.exit(0)); +//# sourceMappingURL=session-start.js.map \ No newline at end of file diff --git a/.wolf/hooks/shared.js b/.wolf/hooks/shared.js new file mode 100644 index 0000000..e402d75 --- /dev/null +++ b/.wolf/hooks/shared.js @@ -0,0 +1,614 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as crypto from "node:crypto"; +export function getWolfDir() { + // Prefer CLAUDE_PROJECT_DIR so hooks work even if CWD changes during a session + const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); + return path.join(projectDir, ".wolf"); +} +/** + * Bail out silently if .wolf/ directory doesn't exist in the current project. + * Call this at the top of every hook to avoid crashes in non-OpenWolf projects. + */ +export function ensureWolfDir() { + const wolfDir = getWolfDir(); + if (!fs.existsSync(wolfDir)) { + process.exit(0); + } +} +export function readJSON(filePath, fallback) { + try { + return JSON.parse(fs.readFileSync(filePath, "utf-8")); + } + catch { + return fallback; + } +} +export function writeJSON(filePath, data) { + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) + fs.mkdirSync(dir, { recursive: true }); + const tmp = filePath + "." + crypto.randomBytes(4).toString("hex") + ".tmp"; + try { + fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8"); + fs.renameSync(tmp, filePath); + } + catch { + // On Windows, rename can fail if another process holds a handle. + // Fall back to direct write and clean up the tmp file. + try { + fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8"); + } + catch { } + try { + fs.unlinkSync(tmp); + } + catch { } + } +} +export function readMarkdown(filePath) { + try { + return fs.readFileSync(filePath, "utf-8"); + } + catch { + return ""; + } +} +export function appendMarkdown(filePath, line) { + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) + fs.mkdirSync(dir, { recursive: true }); + fs.appendFileSync(filePath, line, "utf-8"); +} +export function parseAnatomy(content) { + const sections = new Map(); + let currentSection = ""; + for (const line of content.split("\n")) { + const sm = line.match(/^## (.+)/); + if (sm) { + currentSection = sm[1].trim(); + if (!sections.has(currentSection)) + sections.set(currentSection, []); + continue; + } + if (!currentSection) + continue; + const em = line.match(/^- `([^`]+)`(?:\s+—\s+(.+?))?\s*\(~(\d+)\s+tok\)$/); + if (em) { + sections.get(currentSection).push({ + file: em[1], + description: em[2] || "", + tokens: parseInt(em[3], 10), + }); + } + } + return sections; +} +export function serializeAnatomy(sections, metadata) { + const lines = [ + "# anatomy.md", + "", + `> Auto-maintained by OpenWolf. Last scanned: ${metadata.lastScanned}`, + `> Files: ${metadata.fileCount} tracked | Anatomy hits: ${metadata.hits} | Misses: ${metadata.misses}`, + "", + ]; + const keys = [...sections.keys()].sort(); + for (const key of keys) { + lines.push(`## ${key}`); + lines.push(""); + const entries = sections.get(key).sort((a, b) => a.file.localeCompare(b.file)); + for (const e of entries) { + const desc = e.description ? ` — ${e.description}` : ""; + lines.push(`- \`${e.file}\`${desc} (~${e.tokens} tok)`); + } + lines.push(""); + } + return lines.join("\n"); +} +export function extractDescription(filePath) { + const MAX_DESC = 150; + const basename = path.basename(filePath); + const ext = path.extname(basename).toLowerCase(); + const known = { + "package.json": "Node.js package manifest", + "tsconfig.json": "TypeScript configuration", + ".gitignore": "Git ignore rules", + "README.md": "Project documentation", + "composer.json": "PHP package manifest", + "requirements.txt": "Python dependencies", + "schema.sql": "Database schema", + "Dockerfile": "Docker container definition", + "docker-compose.yml": "Docker Compose services", + "Cargo.toml": "Rust package manifest", + "go.mod": "Go module definition", + "Gemfile": "Ruby dependencies", + "pubspec.yaml": "Dart/Flutter package manifest", + }; + if (known[basename]) + return known[basename]; + let content; + try { + const fd = fs.openSync(filePath, "r"); + const buf = Buffer.alloc(12288); // 12KB + const n = fs.readSync(fd, buf, 0, 12288, 0); + fs.closeSync(fd); + content = buf.subarray(0, n).toString("utf-8"); + } + catch { + return ""; + } + if (!content.trim()) + return ""; + const cap = (s) => s.length <= MAX_DESC ? s : s.slice(0, MAX_DESC - 3) + "..."; + // Markdown heading + if (ext === ".md" || ext === ".mdx") { + const m = content.match(/^#{1,2}\s+(.+)$/m); + if (m) + return cap(m[1].trim()); + } + // HTML title + if (ext === ".html" || ext === ".htm") { + const m = content.match(/]*>([^<]+)<\/title>/i); + if (m) + return cap(m[1].trim()); + } + // JSDoc / PHPDoc / Javadoc — first meaningful line + const jm = content.match(/\/\*\*\s*\n?\s*\*?\s*(.+)/); + if (jm) { + const l = jm[1].replace(/\*\/$/, "").trim(); + if (l && !l.startsWith("@") && l.length > 5) + return cap(l); + } + // Python docstring + if (ext === ".py") { + const dm = content.match(/^(?:#[^\n]*\n)*\s*(?:"""(.+?)"""|'''(.+?)''')/s); + if (dm) { + const first = (dm[1] || dm[2]).split("\n")[0].trim(); + if (first && first.length > 3) + return cap(first); + } + } + // Rust doc comments + if (ext === ".rs") { + const lines = content.split("\n"); + for (const line of lines.slice(0, 20)) { + const m = line.match(/^\s*(?:\/\/\/|\/\/!)\s*(.+)/); + if (m && m[1].length > 5) + return cap(m[1].trim()); + } + } + // Go package comment + if (ext === ".go") { + const m = content.match(/\/\/\s*Package\s+\w+\s+(.*)/); + if (m) + return cap(m[1].trim()); + } + // C# XML doc + if (ext === ".cs") { + const m = content.match(/\s*([\s\S]*?)\s*<\/summary>/); + if (m) { + const text = m[1].replace(/\/\/\/\s*/g, "").replace(/\s+/g, " ").trim(); + if (text.length > 5) + return cap(text); + } + } + // Elixir @moduledoc + if (ext === ".ex" || ext === ".exs") { + const m = content.match(/@moduledoc\s+"""\s*\n\s*(.*)/); + if (m) + return cap(m[1].trim()); + } + // Header comment (skip generic ones) + const hdrLines = content.split("\n"); + for (const line of hdrLines.slice(0, 15)) { + const t = line.trim(); + if (!t || t === " 5 && !lower.startsWith("copyright") && !lower.startsWith("license") && !lower.startsWith("@") && !lower.startsWith("strict") && !lower.startsWith("generated") && !lower.startsWith("eslint-") && !lower.startsWith("nolint")) { + return cap(text); + } + } + if (!t.startsWith("//") && !t.startsWith("#") && !t.startsWith("/*") && !t.startsWith("*") && !t.startsWith("--")) + break; + } + // ─── PHP / Laravel ─────────────────────────────────────── + if (ext === ".php") { + if (basename.endsWith(".blade.php")) { + const ext2 = content.match(/@extends\(\s*['"]([^'"]+)['"]\s*\)/); + const sections = (content.match(/@section\(\s*['"](\w+)['"]/g) || []).map(s => s.match(/['"](\w+)['"]/)?.[1]).filter(Boolean); + const parts = []; + if (ext2) + parts.push(`extends ${ext2[1]}`); + if (sections.length) + parts.push(`sections: ${sections.join(", ")}`); + return cap(parts.length ? `Blade: ${parts.join(", ")}` : "Blade template"); + } + const classM = content.match(/class\s+(\w+)(?:\s+extends\s+(\w+))?/); + const className = classM?.[1] || ""; + const parent = classM?.[2] || ""; + const pubMethods = (content.match(/public\s+function\s+(\w+)/g) || []) + .map(m => m.match(/public\s+function\s+(\w+)/)?.[1]) + .filter(n => n && n !== "__construct" && n !== "middleware"); + if (basename.endsWith("Controller.php") || parent === "Controller") { + if (pubMethods.length > 0) { + const display = pubMethods.slice(0, 5).join(", "); + return cap(pubMethods.length > 5 ? `${display} + ${pubMethods.length - 5} more` : display); + } + } + if (parent === "Model" || parent === "Authenticatable") { + const parts = []; + const tbl = content.match(/\$table\s*=\s*['"]([^'"]+)['"]/); + if (tbl) + parts.push(`table: ${tbl[1]}`); + const fill = content.match(/\$fillable\s*=\s*\[([^\]]*)\]/s); + if (fill) { + const c = (fill[1].match(/['"]/g) || []).length / 2; + parts.push(`${Math.floor(c)} fields`); + } + const rels = (content.match(/\$this->(hasMany|hasOne|belongsTo|belongsToMany|morphMany|morphTo)\(/g) || []).length; + if (rels) + parts.push(`${rels} rels`); + return cap(parts.length ? `Model — ${parts.join(", ")}` : `Model: ${className}`); + } + if (basename.match(/^\d{4}_\d{2}_\d{2}/)) { + const create = content.match(/Schema::create\(\s*['"]([^'"]+)['"]/); + if (create) + return `Migration: create ${create[1]} table`; + const alter = content.match(/Schema::table\(\s*['"]([^'"]+)['"]/); + if (alter) + return `Migration: alter ${alter[1]} table`; + return "Database migration"; + } + if (className && pubMethods.length > 0) { + const display = pubMethods.slice(0, 4).join(", "); + return cap(pubMethods.length > 4 ? `${className}: ${display} + ${pubMethods.length - 4} more` : `${className}: ${display}`); + } + } + // ─── TS/JS/React/Next.js ───────────────────────────────── + if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs") { + // React component + if (ext === ".tsx" || ext === ".jsx") { + const comp = content.match(/(?:export\s+(?:default\s+)?)?(?:function|const)\s+(\w+)/); + const parts = []; + if (comp) + parts.push(comp[1]); + const renders = []; + if (/<(?:form|Form)/i.test(content)) + renders.push("form"); + if (/<(?:table|Table|DataTable)/i.test(content)) + renders.push("table"); + if (/<(?:dialog|Dialog|Modal|Drawer)/i.test(content)) + renders.push("modal"); + if (renders.length) + parts.push(`renders ${renders.join(", ")}`); + if (parts.length) + return cap(parts.join(" — ")); + } + // Next.js conventions + if (basename === "page.tsx" || basename === "page.js") + return "Next.js page component"; + if (basename === "layout.tsx" || basename === "layout.js") + return "Next.js layout"; + if (basename === "route.ts" || basename === "route.js") { + const methods = [...new Set((content.match(/export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)/g) || []) + .map(m => m.match(/(GET|POST|PUT|PATCH|DELETE)/)?.[1]))].filter(Boolean); + return methods.length ? `Next.js API route: ${methods.join(", ")}` : "Next.js API route"; + } + // Express/Fastify routes + const routeHits = content.match(/\.(get|post|put|patch|delete)\s*\(\s*['"`]/g); + if (routeHits && routeHits.length > 0) { + const methods = [...new Set(routeHits.map(r => r.match(/\.(get|post|put|patch|delete)/)?.[1]?.toUpperCase()))]; + return cap(`API routes: ${methods.join(", ")} (${routeHits.length} endpoints)`); + } + // tRPC router + if (content.includes("createTRPCRouter") || content.includes("publicProcedure")) { + const procs = (content.match(/\.(query|mutation|subscription)\s*\(/g) || []).length; + return procs ? `tRPC router: ${procs} procedures` : "tRPC router"; + } + // Zod schemas + if (content.includes("z.object") || content.includes("z.string")) { + const schemas = (content.match(/(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*z\./g) || []) + .map(s => s.match(/(?:const|let)\s+(\w+)/)?.[1]).filter(Boolean); + if (schemas.length) + return cap(`Zod schemas: ${schemas.slice(0, 4).join(", ")}${schemas.length > 4 ? ` + ${schemas.length - 4} more` : ""}`); + } + // Exports summary + const exports = (content.match(/export\s+(?:async\s+)?(?:function|class|const|interface|type|enum)\s+(\w+)/g) || []) + .map(e => e.match(/(\w+)$/)?.[1]).filter(Boolean); + if (exports.length > 0 && exports.length <= 5) + return `Exports ${exports.join(", ")}`; + if (exports.length > 5) + return cap(`Exports ${exports.slice(0, 4).join(", ")} + ${exports.length - 4} more`); + } + // ─── Python / Django / FastAPI / Flask ──────────────────── + if (ext === ".py") { + // Django model + if (content.includes("models.Model")) { + const cls = content.match(/class\s+(\w+)\(.*models\.Model\)/); + const fields = (content.match(/^\s+\w+\s*=\s*models\.\w+/gm) || []).length; + return cap(`Model: ${cls?.[1] || "unknown"}, ${fields} fields`); + } + // FastAPI/Flask routes + if (content.includes("@router.") || content.includes("@app.")) { + const routes = (content.match(/@(?:router|app)\.(get|post|put|patch|delete)\s*\(/g) || []); + return cap(routes.length ? `API: ${routes.length} endpoints` : "API router"); + } + // Pydantic + if (content.includes("BaseModel") && content.includes("Field(")) { + const cls = content.match(/class\s+(\w+)\(.*BaseModel\)/); + return cls ? `Pydantic: ${cls[1]}` : "Pydantic model"; + } + // Celery + if (content.includes("@shared_task") || content.includes("@app.task")) { + const tasks = (content.match(/def\s+(\w+)/g) || []).map(m => m.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_")); + return cap(tasks.length ? `Celery tasks: ${tasks.join(", ")}` : "Celery task"); + } + // Generic + const pyClass = content.match(/class\s+(\w+)/); + const funcs = (content.match(/def\s+(\w+)/g) || []).map(f => f.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_")); + if (pyClass && funcs.length > 0) + return cap(funcs.length > 4 ? `${pyClass[1]}: ${funcs.slice(0, 4).join(", ")} + ${funcs.length - 4} more` : `${pyClass[1]}: ${funcs.join(", ")}`); + if (funcs.length > 0) + return cap(funcs.slice(0, 4).join(", ")); + } + // ─── Go ────────────────────────────────────────────────── + if (ext === ".go") { + const handlers = (content.match(/func\s+(\w+)\s*\(\s*\w+\s+http\.ResponseWriter/g) || []) + .map(m => m.match(/func\s+(\w+)/)?.[1]).filter(Boolean); + if (handlers.length) + return cap(`HTTP handlers: ${handlers.slice(0, 5).join(", ")}`); + const iface = content.match(/type\s+(\w+)\s+interface\s*\{/); + if (iface) + return `Interface: ${iface[1]}`; + const structM = content.match(/type\s+(\w+)\s+struct\s*\{/); + if (structM) + return `Struct: ${structM[1]}`; + const funcs = (content.match(/^func\s+(\w+)/gm) || []).map(m => m.match(/func\s+(\w+)/)?.[1]).filter(n => n && n[0] === n[0].toUpperCase()); + if (funcs.length) + return cap(funcs.slice(0, 5).join(", ")); + } + // ─── Rust ──────────────────────────────────────────────── + if (ext === ".rs") { + const structM = content.match(/pub\s+struct\s+(\w+)/); + if (structM) { + const methods = (content.match(/pub\s+(?:async\s+)?fn\s+(\w+)/g) || []).map(m => m.match(/fn\s+(\w+)/)?.[1]).filter(Boolean); + return cap(methods.length ? `${structM[1]}: ${methods.slice(0, 4).join(", ")}` : `Struct: ${structM[1]}`); + } + const traitM = content.match(/pub\s+trait\s+(\w+)/); + if (traitM) + return `Trait: ${traitM[1]}`; + const enumM = content.match(/pub\s+enum\s+(\w+)/); + if (enumM) + return `Enum: ${enumM[1]}`; + const fns = (content.match(/pub\s+(?:async\s+)?fn\s+(\w+)/g) || []).map(m => m.match(/fn\s+(\w+)/)?.[1]).filter(Boolean); + if (fns.length) + return cap(fns.slice(0, 5).join(", ")); + } + // ─── Java / Spring ─────────────────────────────────────── + if (ext === ".java") { + const cls = content.match(/(?:public\s+)?class\s+(\w+)/); + const className = cls?.[1] || basename.replace(".java", ""); + const annotations = (content.match(/@(RestController|Controller|Service|Repository|Component|Entity|Configuration)/g) || []).map(a => a.slice(1)); + const mappings = (content.match(/@(?:Get|Post|Put|Patch|Delete|Request)Mapping/g) || []).length; + if (mappings) + return cap(`${annotations[0] || "Spring"}: ${className} (${mappings} endpoints)`); + if (annotations.length) + return `${annotations[0]}: ${className}`; + if (content.includes("@Entity")) + return `Entity: ${className}`; + const methods = (content.match(/public\s+(?:static\s+)?(?:\w+(?:<[\w,\s]+>)?)\s+(\w+)\s*\(/g) || []) + .map(m => m.match(/(\w+)\s*\(/)?.[1]).filter(n => n && n !== className); + if (methods.length) + return cap(`${className}: ${methods.slice(0, 4).join(", ")}`); + return className ? `Class: ${className}` : ""; + } + // ─── Kotlin ────────────────────────────────────────────── + if (ext === ".kt" || ext === ".kts") { + const cls = content.match(/(?:data\s+)?class\s+(\w+)/); + if (content.match(/data\s+class/)) + return `Data class: ${cls?.[1] || basename.replace(/\.kts?$/, "")}`; + if (content.includes("routing {")) + return "Ktor routing"; + const fns = (content.match(/fun\s+(\w+)/g) || []).map(m => m.match(/fun\s+(\w+)/)?.[1]).filter(Boolean); + if (cls && fns.length) + return cap(`${cls[1]}: ${fns.slice(0, 4).join(", ")}`); + if (fns.length) + return cap(fns.slice(0, 5).join(", ")); + } + // ─── C# / .NET ─────────────────────────────────────────── + if (ext === ".cs") { + const cls = content.match(/(?:public\s+)?(?:partial\s+)?class\s+(\w+)(?:\s*:\s*(\w+))?/); + const className = cls?.[1] || basename.replace(".cs", ""); + const parent = cls?.[2] || ""; + if (parent === "Controller" || parent === "ControllerBase" || content.includes("[ApiController]")) { + const actions = (content.match(/\[Http(Get|Post|Put|Patch|Delete)\]/g) || []).map(a => a.match(/Http(\w+)/)?.[1]).filter(Boolean); + return cap(actions.length ? `API Controller: ${className} (${[...new Set(actions)].join(", ")})` : `Controller: ${className}`); + } + if (parent === "DbContext" || content.includes("DbSet<")) { + const sets = (content.match(/DbSet<(\w+)>/g) || []).map(s => s.match(/<(\w+)>/)?.[1]).filter(Boolean); + return cap(sets.length ? `DbContext: ${sets.join(", ")}` : `DbContext: ${className}`); + } + return className ? `Class: ${className}` : ""; + } + // ─── Ruby / Rails ──────────────────────────────────────── + if (ext === ".rb") { + const cls = content.match(/class\s+(\w+)(?:\s*<\s*(\w+(?:::\w+)?))?/); + const className = cls?.[1] || ""; + const parent = cls?.[2] || ""; + if (parent?.includes("Controller")) { + const actions = (content.match(/def\s+(index|show|new|create|edit|update|destroy|\w+)/g) || []) + .map(m => m.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_")); + return cap(actions.length ? `Controller: ${actions.join(", ")}` : `Controller: ${className}`); + } + if (parent === "ApplicationRecord" || parent === "ActiveRecord::Base") + return `Model: ${className}`; + if (basename.match(/^\d{14}_/)) { + const create = content.match(/create_table\s+:(\w+)/); + return create ? `Migration: create ${create[1]}` : "Database migration"; + } + const methods = (content.match(/def\s+(\w+)/g) || []).map(m => m.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_")); + if (cls && methods.length) + return cap(`${className}: ${methods.slice(0, 4).join(", ")}`); + } + // ─── Swift ─────────────────────────────────────────────── + if (ext === ".swift") { + if (content.includes(": View") || content.includes("some View")) { + const name = content.match(/struct\s+(\w+)\s*:\s*View/); + return name ? `SwiftUI view: ${name[1]}` : "SwiftUI view"; + } + const proto = content.match(/protocol\s+(\w+)/); + if (proto) + return `Protocol: ${proto[1]}`; + const struct = content.match(/(?:public\s+)?struct\s+(\w+)/); + const cls = content.match(/(?:public\s+)?class\s+(\w+)/); + const name = struct?.[1] || cls?.[1] || ""; + if (name) + return `${struct ? "Struct" : "Class"}: ${name}`; + } + // ─── Dart / Flutter ────────────────────────────────────── + if (ext === ".dart") { + if (content.includes("StatefulWidget") || content.includes("StatelessWidget")) { + const name = content.match(/class\s+(\w+)\s+extends\s+(?:Stateful|Stateless)Widget/); + return name ? `${content.includes("StatefulWidget") ? "Stateful" : "Stateless"} widget: ${name[1]}` : "Flutter widget"; + } + const cls = content.match(/class\s+(\w+)/); + if (cls) + return `Class: ${cls[1]}`; + } + // ─── Vue / Svelte / Astro ──────────────────────────────── + if (ext === ".vue") { + const name = content.match(/name:\s*['"]([^'"]+)['"]/); + const setup = content.includes("