From 6ae3783c76c2d149d330529b5ce0b73bc82bc63e Mon Sep 17 00:00:00 2001 From: itdove Date: Mon, 6 Apr 2026 10:49:31 -0400 Subject: [PATCH] test: add comprehensive test coverage for investigate command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add test_investigate_command.py with full test suite - Fix JiraClient import path in investigate command tests - Update AGENTS.md and daf-workflow skill documentation - Enhance investigate_command.py implementation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .wolf/anatomy.md | 13 +- .wolf/buglog.json | 26 +- .wolf/hooks/_session.json | 4 +- .wolf/memory.md | 26 ++ .wolf/token-ledger.json | 153 ++++++++- AGENTS.md | 11 + devflow/cli/commands/investigate_command.py | 191 ++++++++++- devflow/cli/main.py | 18 +- devflow/cli_skills/daf-workflow/SKILL.md | 67 ++++ tests/test_investigate_command.py | 349 ++++++++++++++++++++ 10 files changed, 834 insertions(+), 24 deletions(-) diff --git a/.wolf/anatomy.md b/.wolf/anatomy.md index afb0620..5ec0859 100644 --- a/.wolf/anatomy.md +++ b/.wolf/anatomy.md @@ -1,13 +1,13 @@ # anatomy.md -> Auto-maintained by OpenWolf. Last scanned: 2026-04-06T12:56:30.133Z -> Files: 504 tracked | Anatomy hits: 0 | Misses: 0 +> Auto-maintained by OpenWolf. Last scanned: 2026-04-06T14:36:32.857Z +> Files: 505 tracked | Anatomy hits: 0 | Misses: 0 ## ./ - `.coverage` (~14200 tok) - `.gitignore` — Git ignore rules (~175 tok) -- `AGENTS.md` — Agent Instructions for DevAIFlow (~20993 tok) +- `AGENTS.md` — Agent Instructions for DevAIFlow (~21192 tok) - `CHANGELOG.md` — Change log (~4649 tok) - `CLAUDE.md` — OpenWolf (~139 tok) - `config.schema.json` (~9891 tok) @@ -100,7 +100,7 @@ - `__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) +- `main.py` — Main CLI entry point for DevAIFlow. (~47322 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) @@ -134,7 +134,7 @@ - `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) +- `investigate_command.py` — Command for daf investigate - create investigation-only session without ticket creation. (~12343 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) @@ -230,7 +230,7 @@ ## devflow/cli_skills/daf-workflow/ -- `SKILL.md` — DevAIFlow Workflow Guide (~3274 tok) +- `SKILL.md` — DevAIFlow Workflow Guide (~3832 tok) ## devflow/cli_skills/daf-workspace/ @@ -709,6 +709,7 @@ - `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_investigate_command.py` — Tests for daf investigate command. (~10764 tok) - `test_release_manager_pyproject.py` — Tests for ReleaseManager with pyproject.toml support. (~1968 tok) ## tests/cli/commands/ diff --git a/.wolf/buglog.json b/.wolf/buglog.json index 4136cd6..69c0d01 100644 --- a/.wolf/buglog.json +++ b/.wolf/buglog.json @@ -8,10 +8,34 @@ "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"], + "tags": [ + "release", + "pyproject.toml", + "packaging", + "version-management", + "pep-517", + "pep-518", + "pep-621" + ], "related_bugs": [], "occurrences": 1, "last_seen": "2026-04-06T01:50:00Z" + }, + { + "id": "bug-002", + "timestamp": "2026-04-06T14:34:09.151Z", + "error_message": "Incorrect value in code", + "file": "tests/test_investigate_command.py", + "root_cause": "Had \"devflow.cli.commands.investigate_command.JiraClie", + "fix": "Changed to \"devflow.jira.JiraClient\"", + "tags": [ + "auto-detected", + "wrong-value", + "py" + ], + "related_bugs": [], + "occurrences": 1, + "last_seen": "2026-04-06T14:34:09.151Z" } ] } \ No newline at end of file diff --git a/.wolf/hooks/_session.json b/.wolf/hooks/_session.json index c52a236..946c0f6 100644 --- a/.wolf/hooks/_session.json +++ b/.wolf/hooks/_session.json @@ -1,6 +1,6 @@ { - "session_id": "session-2026-04-06-0913", - "started": "2026-04-06T13:13:57.895Z", + "session_id": "session-2026-04-06-1049", + "started": "2026-04-06T14:49:18.559Z", "files_read": {}, "files_written": [], "edit_counts": {}, diff --git a/.wolf/memory.md b/.wolf/memory.md index 967069a..ddc2a6e 100644 --- a/.wolf/memory.md +++ b/.wolf/memory.md @@ -46,3 +46,29 @@ | Time | Action | File(s) | Outcome | ~Tokens | |------|--------|---------|---------|--------| + +## Session: 2026-04-06 09:44 + +| Time | Action | File(s) | Outcome | ~Tokens | +|------|--------|---------|---------|--------| +| 09:48 | Edited devflow/cli/main.py | modified investigate() | ~920 | +| 09:49 | Edited devflow/cli/commands/investigate_command.py | added 4 import(s) | ~323 | +| 10:01 | Edited devflow/cli/commands/investigate_command.py | modified create_investigation_from_issue() | ~1539 | +| 10:03 | Edited devflow/cli/commands/investigate_command.py | modified create_investigation_session() | ~454 | +| 10:32 | Edited devflow/cli/commands/investigate_command.py | modified Investigate() | ~329 | +| 10:32 | Edited devflow/cli/commands/investigate_command.py | expanded (+6 lines) | ~123 | +| 10:32 | Edited devflow/cli/commands/investigate_command.py | modified _build_investigation_prompt() | ~676 | +| 10:33 | Edited tests/test_investigate_command.py | modified test_investigate_from_jira_issue() | ~4198 | +| 10:34 | Edited tests/test_investigate_command.py | 2→2 lines | ~26 | +| 10:34 | Edited tests/test_investigate_command.py | 6→6 lines | ~112 | +| 10:34 | Edited tests/test_investigate_command.py | 6→6 lines | ~107 | +| 10:35 | Edited tests/test_investigate_command.py | modified test_investigate_from_issue_with_goal_override() | ~421 | +| 10:35 | Edited tests/test_investigate_command.py | modified test_investigate_from_issue_custom_name() | ~420 | +| 10:35 | Edited AGENTS.md | expanded (+11 lines) | ~333 | +| 10:36 | Edited devflow/cli_skills/daf-workflow/SKILL.md | modified scratch() | ~625 | +| 10:38 | Session end: 15 writes across 5 files (main.py, investigate_command.py, test_investigate_command.py, AGENTS.md, SKILL.md) | 7 reads | ~108651 tok | + +## Session: 2026-04-06 10:49 + +| Time | Action | File(s) | Outcome | ~Tokens | +|------|--------|---------|---------|--------| diff --git a/.wolf/token-ledger.json b/.wolf/token-ledger.json index e693eba..3159677 100644 --- a/.wolf/token-ledger.json +++ b/.wolf/token-ledger.json @@ -2,16 +2,151 @@ "version": 1, "created_at": "2026-04-06T12:35:01.489Z", "lifetime": { - "total_tokens_estimated": 0, - "total_reads": 0, - "total_writes": 0, - "total_sessions": 7, - "anatomy_hits": 0, - "anatomy_misses": 0, - "repeated_reads_blocked": 0, - "estimated_savings_vs_bare_cli": 0 + "total_tokens_estimated": 108651, + "total_reads": 7, + "total_writes": 15, + "total_sessions": 9, + "anatomy_hits": 5, + "anatomy_misses": 1, + "repeated_reads_blocked": 14, + "estimated_savings_vs_bare_cli": 190705 }, - "sessions": [], + "sessions": [ + { + "id": "session-2026-04-06-0944", + "started": "2026-04-06T13:44:19.889Z", + "ended": "2026-04-06T14:38:25.390Z", + "reads": [ + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/AGENTS.md", + "tokens_estimated": 20993, + "was_repeated": true, + "anatomy_had_description": false + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/devflow/cli/commands/investigate_command.py", + "tokens_estimated": 11917, + "was_repeated": true, + "anatomy_had_description": false + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/devflow/cli/main.py", + "tokens_estimated": 47109, + "was_repeated": true, + "anatomy_had_description": false + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/devflow/utils/backend_detection.py", + "tokens_estimated": 1809, + "was_repeated": false, + "anatomy_had_description": false + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/devflow/cli/commands/git_open_command.py", + "tokens_estimated": 2094, + "was_repeated": false, + "anatomy_had_description": false + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/tests/test_investigate_command.py", + "tokens_estimated": 10780, + "was_repeated": true, + "anatomy_had_description": false + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/devflow/cli_skills/daf-workflow/SKILL.md", + "tokens_estimated": 3274, + "was_repeated": true, + "anatomy_had_description": false + } + ], + "writes": [ + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/devflow/cli/main.py", + "tokens_estimated": 920, + "action": "edit" + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/devflow/cli/commands/investigate_command.py", + "tokens_estimated": 323, + "action": "edit" + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/devflow/cli/commands/investigate_command.py", + "tokens_estimated": 1539, + "action": "edit" + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/devflow/cli/commands/investigate_command.py", + "tokens_estimated": 454, + "action": "edit" + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/devflow/cli/commands/investigate_command.py", + "tokens_estimated": 329, + "action": "edit" + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/devflow/cli/commands/investigate_command.py", + "tokens_estimated": 123, + "action": "edit" + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/devflow/cli/commands/investigate_command.py", + "tokens_estimated": 676, + "action": "edit" + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/tests/test_investigate_command.py", + "tokens_estimated": 4198, + "action": "edit" + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/tests/test_investigate_command.py", + "tokens_estimated": 26, + "action": "edit" + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/tests/test_investigate_command.py", + "tokens_estimated": 112, + "action": "edit" + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/tests/test_investigate_command.py", + "tokens_estimated": 107, + "action": "edit" + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/tests/test_investigate_command.py", + "tokens_estimated": 421, + "action": "edit" + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/tests/test_investigate_command.py", + "tokens_estimated": 420, + "action": "edit" + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/AGENTS.md", + "tokens_estimated": 357, + "action": "edit" + }, + { + "file": "/Users/dvernier/development/devaiflow/devaiflow/devflow/cli_skills/daf-workflow/SKILL.md", + "tokens_estimated": 670, + "action": "edit" + } + ], + "totals": { + "input_tokens_estimated": 97976, + "output_tokens_estimated": 10675, + "reads_count": 7, + "writes_count": 15, + "repeated_reads_blocked": 14, + "anatomy_lookups": 5 + } + } + ], "daemon_usage": [], "waste_flags": [], "optimization_report": { diff --git a/AGENTS.md b/AGENTS.md index 618d00e..5c7a8ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1706,6 +1706,17 @@ class Session(BaseModel): - Prevents session name collisions during sync - Comprehensive test coverage (2 new tests) - All 3631 tests pass +- ✓ Issue key support for daf investigate command (itdove/devaiflow#363) + - Added optional issue_key positional argument to daf investigate command + - Supports JIRA (PROJ-12345), GitHub (#123, owner/repo#123), and GitLab issue keys + - Automatically fetches issue details and uses summary as investigation goal + - Auto-generates session name from issue key (e.g., investigate-PROJ-12345) + - --goal flag still works and overrides issue summary when both provided + - Investigation prompt includes issue details (summary, description, link) + - Comprehensive error handling (issue not found, auth errors, API failures) + - Examples: daf investigate PROJ-12345, daf investigate owner/repo#123, daf investigate #123 + - Comprehensive test coverage (7 new tests) + - All 22 investigate tests pass ## Release Management diff --git a/devflow/cli/commands/investigate_command.py b/devflow/cli/commands/investigate_command.py index 07cbefe..6ba702a 100644 --- a/devflow/cli/commands/investigate_command.py +++ b/devflow/cli/commands/investigate_command.py @@ -12,6 +12,10 @@ from devflow.cli.utils import console_print, get_workspace_path, is_json_mode, output_json, require_outside_claude, resolve_workspace_path, should_launch_claude_code from devflow.git.utils import GitUtils +from devflow.utils.backend_detection import detect_backend_from_key +from devflow.issue_tracker.factory import create_issue_tracker_client +from devflow.issue_tracker.exceptions import IssueTrackerNotFoundError, IssueTrackerAuthError, IssueTrackerApiError +from devflow.utils.git_remote import GitRemoteDetector # Import unified utilities from devflow.cli.signal_handler import setup_signal_handlers, is_cleanup_done @@ -22,6 +26,133 @@ console = Console() +@require_outside_claude +def create_investigation_from_issue( + issue_key: str, + goal_override: Optional[str] = None, + parent: Optional[str] = None, + name: Optional[str] = None, + path: Optional[str] = None, + workspace: Optional[str] = None, + model_profile: Optional[str] = None, + projects: Optional[str] = None, + temp_clone: Optional[bool] = None, +) -> None: + """Create investigation session from an existing issue tracker ticket. + + This function fetches the issue details and creates an investigation session + with the issue summary as the goal. + + Args: + issue_key: Issue identifier (PROJ-12345, owner/repo#123, #123) + goal_override: Optional goal to override issue summary + parent: Optional parent issue key (for tracking investigation under an epic) + name: Optional session name (auto-generated from issue key if not provided) + path: Optional project path (bypasses interactive selection if provided) + workspace: Optional workspace name (overrides session default and config default) + model_profile: Optional model provider profile to use + projects: Optional comma-separated list of project names for multi-project mode + temp_clone: Whether to clone to temp directory (None = prompt, True = clone, False = no clone) + """ + from devflow.config.loader import ConfigLoader + + config_loader = ConfigLoader() + config = config_loader.load_config() + + if not config: + console_print("[red]✗[/red] No configuration found. Run [cyan]daf init[/cyan] first.") + if is_json_mode(): + output_json(success=False, error={"message": "No configuration found", "code": "NO_CONFIG"}) + sys.exit(1) + + console_print(f"[dim]Fetching issue details for: {issue_key}[/dim]") + + # Detect backend from issue key format + backend = detect_backend_from_key(issue_key, config) + console_print(f"[dim]Detected backend: {backend}[/dim]") + + # Fetch issue details + try: + if backend == "jira": + # JIRA backend + from devflow.jira import JiraClient + client = JiraClient() + issue = client.get_ticket(issue_key) + issue_summary = issue.get("summary", "") + issue_description = issue.get("description", "") + + else: + # GitHub/GitLab backend + detector = GitRemoteDetector() + platform_info = detector.parse_repository_info() + hostname = detector.get_hostname() + + if platform_info: + platform, owner, repo_name = platform_info + actual_backend = "gitlab" if platform == "gitlab" else "github" + repository = f"{owner}/{repo_name}" + else: + # Default to GitHub if can't detect + actual_backend = "github" + repository = None + + # Create appropriate client + client = create_issue_tracker_client(backend=actual_backend, hostname=hostname) + + # Set repository if we have one + if repository and hasattr(client, 'repository'): + client.repository = repository + + # Fetch issue + issue = client.get_ticket(issue_key) + issue_summary = issue.get("title", "") or issue.get("summary", "") + issue_description = issue.get("body", "") or issue.get("description", "") + + console_print(f"[green]✓[/green] Fetched issue: {issue_summary}") + + except IssueTrackerNotFoundError: + console_print(f"[red]✗[/red] Issue not found: {issue_key}") + if is_json_mode(): + output_json(success=False, error={"message": f"Issue not found: {issue_key}", "code": "ISSUE_NOT_FOUND"}) + sys.exit(1) + except IssueTrackerAuthError as e: + console_print(f"[red]✗[/red] Authentication failed: {e}") + if is_json_mode(): + output_json(success=False, error={"message": str(e), "code": "AUTH_ERROR"}) + sys.exit(1) + except IssueTrackerApiError as e: + console_print(f"[red]✗[/red] API error: {e}") + if is_json_mode(): + output_json(success=False, error={"message": str(e), "code": "API_ERROR"}) + sys.exit(1) + except Exception as e: + console_print(f"[red]✗[/red] Failed to fetch issue: {e}") + if is_json_mode(): + output_json(success=False, error={"message": str(e), "code": "FETCH_ERROR"}) + sys.exit(1) + + # Use goal override if provided, otherwise use issue summary + goal = goal_override if goal_override else issue_summary + + # Auto-generate session name from issue key if not provided + if not name: + # Normalize issue key for session name (replace # with issue-) + name = f"investigate-{issue_key.replace('#', 'issue-')}" + console_print(f"[dim]Auto-generated session name: {name}[/dim]") + + # Create investigation session with fetched details + create_investigation_session( + goal=goal, + parent=parent or issue_key, # Track under the issue if no parent provided + name=name, + path=path, + workspace=workspace, + model_profile=model_profile, + projects=projects, + temp_clone=temp_clone, + issue_key=issue_key, + issue_details={"summary": issue_summary, "description": issue_description}, + ) def slugify_goal(goal: str) -> str: @@ -66,6 +197,8 @@ def create_investigation_session( model_profile: Optional[str] = None, projects: Optional[str] = None, temp_clone: Optional[bool] = None, + issue_key: Optional[str] = None, + issue_details: Optional[dict] = None, ) -> None: """Create a new investigation session for codebase analysis. @@ -84,6 +217,8 @@ def create_investigation_session( model_profile: Optional model provider profile to use (e.g., "vertex", "llama-cpp") projects: Optional comma-separated list of project names for multi-project mode temp_clone: Whether to clone to temp directory (None = prompt, True = clone, False = no clone) + issue_key: Optional issue key (for sessions created from issues) + issue_details: Optional issue details dict with 'summary' and 'description' keys """ from devflow.session.manager import SessionManager from devflow.config.loader import ConfigLoader @@ -265,7 +400,10 @@ def create_investigation_session( console_print(f"[dim]User declined temp clone or cloning failed - using current directory[/dim]") # Build the goal string for investigation - if parent: + if issue_key: + # Session created from issue - include issue key in goal + full_goal = f"Investigate {issue_key}: {goal}" + elif parent: full_goal = f"Investigate (under {parent}): {goal}" else: full_goal = f"Investigate: {goal}" @@ -284,8 +422,10 @@ def create_investigation_session( # Set session_type to "investigation" session.session_type = "investigation" - # Set parent for tracking if provided - if parent: + # Set issue_key for tracking (use issue_key or parent) + if issue_key: + session.issue_key = issue_key + elif parent: session.issue_key = parent # AAP-64296: Store selected workspace in session @@ -351,7 +491,13 @@ def create_investigation_session( # Build initial prompt with investigation-only constraints # AAP-64886: Get workspace path from session instead of using default workspace = resolve_workspace_path(config, session.workspace_name) - initial_prompt = _build_investigation_prompt(goal, parent, config, name, project_path=project_path, workspace=workspace) + initial_prompt = _build_investigation_prompt( + goal, parent, config, name, + project_path=project_path, + workspace=workspace, + issue_key=issue_key, + issue_details=issue_details + ) # Note: daf-workflow skill is auto-loaded, no validation needed if not validate_daf_agents_md(session, config_loader): @@ -469,6 +615,8 @@ def _build_investigation_prompt( session_name: str, project_path: Optional[str] = None, workspace: Optional[str] = None, + issue_key: Optional[str] = None, + issue_details: Optional[dict] = None, ) -> str: """Build the initial prompt for investigation sessions. @@ -479,12 +627,16 @@ def _build_investigation_prompt( session_name: Name of the session project_path: Project path workspace: Workspace path + issue_key: Optional issue key (for sessions created from issues) + issue_details: Optional issue details dict with 'summary' and 'description' keys Returns: Initial prompt string with investigation-focused instructions """ # Build the "Work on" line - if parent: + if issue_key: + work_on_line = f"Work on: Investigate {issue_key}: {goal}" + elif parent: work_on_line = f"Work on: Investigate (tracking under {parent}): {goal}" else: work_on_line = f"Work on: Investigate: {goal}" @@ -494,6 +646,35 @@ def _build_investigation_prompt( "", ] + # Add issue details section if session was created from an issue + if issue_key and issue_details: + from devflow.utils.backend_detection import detect_backend_from_key + backend = detect_backend_from_key(issue_key, config) + + # Build issue URL if possible + issue_url = None + if backend == "jira" and config and hasattr(config, 'jira') and config.jira and hasattr(config.jira, 'url'): + issue_url = f"{config.jira.url.rstrip('/')}/browse/{issue_key}" + + prompt_parts.append("## Issue Details") + prompt_parts.append("") + if issue_url: + prompt_parts.append(f"**Issue:** {issue_key} ({issue_url})") + else: + prompt_parts.append(f"**Issue:** {issue_key}") + + if issue_details.get("summary"): + prompt_parts.append(f"**Summary:** {issue_details['summary']}") + + if issue_details.get("description"): + prompt_parts.append("") + prompt_parts.append("**Description:**") + prompt_parts.append(issue_details['description']) + + prompt_parts.append("") + prompt_parts.append("---") + prompt_parts.append("") + # Add context files section default_files = [ ("AGENTS.md", "agent-specific instructions"), diff --git a/devflow/cli/main.py b/devflow/cli/main.py index 97501e2..190808b 100644 --- a/devflow/cli/main.py +++ b/devflow/cli/main.py @@ -1916,6 +1916,7 @@ def git_check_auth(ctx: click.Context, repository: Optional[str]) -> None: @cli.command(name="investigate") @json_option +@click.argument("issue_key", required=False) @click.option("--goal", help="Goal/description for the investigation (auto-detection of file:// paths and http(s):// URLs)") @click.option("--goal-file", help="Explicit file path or URL for goal input (mutually exclusive with --goal)") @click.option("--parent", required=False, help="Optional parent issue key (for tracking investigation under an epic)") @@ -1925,7 +1926,7 @@ def git_check_auth(ctx: click.Context, repository: Optional[str]) -> None: @click.option("--projects", help="Comma-separated list of repository names for multi-project sessions (requires --workspace)") @click.option("--temp-clone/--no-temp-clone", default=None, help="Clone to temporary directory for clean analysis (default: prompt)") @click.option("--model-profile", help="Model provider profile to use (e.g., 'vertex', 'llama-cpp')") -def investigate(ctx: click.Context, goal: str, goal_file: str, parent: Optional[str], name: str, path: str, workspace: str, projects: str, temp_clone: bool, model_profile: str) -> None: +def investigate(ctx: click.Context, issue_key: Optional[str], goal: str, goal_file: str, parent: Optional[str], name: str, path: str, workspace: str, projects: str, temp_clone: bool, model_profile: str) -> None: """Create investigation-only session without ticket creation. Creates a session with session_type="investigation" that: @@ -1936,7 +1937,16 @@ def investigate(ctx: click.Context, goal: str, goal_file: str, parent: Optional[ Use this when you want to explore the codebase before committing to creating a issue tracker ticket. + ISSUE_KEY is optional. If provided, the command will fetch the issue details + and use the issue summary as the investigation goal. Supports: + - JIRA: PROJ-12345 + - GitHub: owner/repo#123 or #123 + - GitLab: owner/repo#123 or #123 + Examples: + daf investigate PROJ-12345 + daf investigate owner/repo#123 + daf investigate #123 daf investigate --goal "Research Redis caching options for subscription API" daf investigate --goal "Investigate timeout issue in backup service" --parent PROJ-59038 daf investigate --goal "file:///path/to/research-notes.md" @@ -1945,6 +1955,12 @@ def investigate(ctx: click.Context, goal: str, goal_file: str, parent: Optional[ from devflow.cli.commands.investigate_command import create_investigation_session from devflow.cli.utils import process_goal_options + # If issue_key provided, fetch issue details and delegate to command + if issue_key: + from devflow.cli.commands.investigate_command import create_investigation_from_issue + create_investigation_from_issue(issue_key, goal, parent, name, path, workspace, model_profile, projects, temp_clone) + return + # Prompt for goal if not provided if not goal and not goal_file: goal = click.prompt("Enter goal/description for the investigation") diff --git a/devflow/cli_skills/daf-workflow/SKILL.md b/devflow/cli_skills/daf-workflow/SKILL.md index b34418b..984e316 100644 --- a/devflow/cli_skills/daf-workflow/SKILL.md +++ b/devflow/cli_skills/daf-workflow/SKILL.md @@ -307,6 +307,73 @@ daf git create {bug|story|task} \ --- +## Workflow: Investigation Sessions + +For sessions opened via `daf investigate` (analysis-only sessions for codebase exploration): + +**Purpose:** Analyze the codebase before committing to creating a ticket or starting implementation + +**Creating investigation sessions:** + +**From scratch (manual goal):** +```bash +daf investigate --goal "Research Redis caching options for subscription API" +daf investigate --goal "Investigate timeout issue" --parent PROJ-59038 +``` + +**From existing issue (auto-fetch details):** +```bash +# JIRA +daf investigate PROJ-12345 + +# GitHub/GitLab +daf investigate owner/repo#123 +daf investigate #123 +``` + +**How it works:** +- When you provide an issue key, daf automatically: + - Fetches the issue details from the issue tracker + - Uses the issue summary as the investigation goal + - Includes issue description and link in the initial prompt + - Auto-generates session name (e.g., `investigate-PROJ-12345`) + - Stores issue key for tracking + +**Context checking:** +- `daf active` is NOT needed in investigation sessions +- You're only reading files, not making changes + +**Constraints:** +- ❌ DO NOT modify code or files +- ❌ DO NOT create git commits or checkout branches +- ✅ ONLY read files, search code, analyze architecture +- ✅ Document findings and recommendations +- ✅ Create issue tracker tickets for discovered bugs/improvements (optional) + +**Workflow:** +1. Analyze the codebase to understand the problem/feature +2. Read relevant files, search for patterns, understand architecture +3. Identify feasibility and implementation approaches +4. Document findings and recommendations +5. (Optional) Create tickets for discovered issues using `daf jira create` or `daf git create` + +**When analysis is complete:** +- Provide a summary of findings +- List key files and components involved +- Suggest implementation approaches +- Note any concerns or blockers +- User saves findings with `daf note` or exports them + +**Overriding issue summary (optional):** +```bash +# Fetch issue details but use custom goal +daf investigate PROJ-12345 --goal "Custom investigation focus" +``` + +**Why:** Investigation sessions provide a safe read-only environment to explore the codebase before committing to changes or ticket creation. + +--- + ## Command Usage Guidelines **See these skills for detailed documentation:** diff --git a/tests/test_investigate_command.py b/tests/test_investigate_command.py index 85d960a..3808878 100644 --- a/tests/test_investigate_command.py +++ b/tests/test_investigate_command.py @@ -609,3 +609,352 @@ def test_multi_project_investigation_prompt_includes_all_projects(self, temp_daf assert "READ-ONLY" in prompt assert "Do NOT modify any code or files in any project" in prompt assert "Investigate API integration" in prompt + + +class TestInvestigateFromIssue: + """Test creating investigation sessions from issue tracker tickets.""" + + def test_investigate_from_jira_issue(self, temp_daf_home, monkeypatch): + """Test creating investigation session from JIRA issue.""" + # Set mock mode + monkeypatch.setenv("DAF_MOCK_MODE", "1") + + # Create config + config_loader = ConfigLoader() + config = config_loader.create_default_config() + from devflow.config.models import WorkspaceDefinition + + config.repos.workspaces = [ + WorkspaceDefinition(name="default", path=str(Path(temp_daf_home) / "workspace")) + ] + config_loader.save_config(config) + + # Create workspace and project + workspace = Path(temp_daf_home) / "workspace" + workspace.mkdir(parents=True, exist_ok=True) + test_project = workspace / "test-project" + test_project.mkdir(exist_ok=True) + + # Mock JIRA client + with patch("devflow.jira.JiraClient") as MockJiraClient: + mock_client = MagicMock() + mock_client.get_ticket.return_value = { + "summary": "Investigate Redis caching for subscription API", + "description": "We need to research Redis caching options to improve performance", + "key": "PROJ-12345", + } + MockJiraClient.return_value = mock_client + + runner = CliRunner() + result = runner.invoke(cli, [ + "investigate", + "PROJ-12345", + "--path", str(test_project), + ]) + + # Should succeed + assert result.exit_code == 0, f"Output: {result.output}" + assert "Fetched issue: Investigate Redis caching for subscription API" in result.output + assert "Created session" in result.output + assert "session_type: investigation" in result.output + + # Verify session was created + session_manager = SessionManager(config_loader=config_loader) + sessions = session_manager.list_sessions() + + created_session = None + for session in sessions: + if session.session_type == "investigation": + created_session = session + break + + assert created_session is not None + assert created_session.session_type == "investigation" + assert created_session.issue_key == "PROJ-12345" + assert "Investigate Redis caching for subscription API" in created_session.goal + assert created_session.name == "investigate-PROJ-12345" + + def test_investigate_from_github_issue(self, temp_daf_home, monkeypatch): + """Test creating investigation session from GitHub issue.""" + # Set mock mode + monkeypatch.setenv("DAF_MOCK_MODE", "1") + + # Create config + config_loader = ConfigLoader() + config = config_loader.create_default_config() + from devflow.config.models import WorkspaceDefinition + + config.repos.workspaces = [ + WorkspaceDefinition(name="default", path=str(Path(temp_daf_home) / "workspace")) + ] + config_loader.save_config(config) + + # Create workspace and project + workspace = Path(temp_daf_home) / "workspace" + workspace.mkdir(parents=True, exist_ok=True) + test_project = workspace / "test-project" + test_project.mkdir(exist_ok=True) + + # Mock GitHub client + with patch("devflow.cli.commands.investigate_command.create_issue_tracker_client") as mock_factory: + mock_client = MagicMock() + mock_client.get_ticket.return_value = { + "title": "Investigate API timeout issues", + "body": "Users are reporting timeout issues with the API", + "number": 123, + } + mock_factory.return_value = mock_client + + # Mock git remote detector + with patch("devflow.cli.commands.investigate_command.GitRemoteDetector") as MockDetector: + mock_detector = MagicMock() + mock_detector.parse_repository_info.return_value = ("github", "owner", "repo") + mock_detector.get_hostname.return_value = None + MockDetector.return_value = mock_detector + + runner = CliRunner() + result = runner.invoke(cli, [ + "investigate", + "#123", + "--path", str(test_project), + ]) + + # Should succeed + assert result.exit_code == 0, f"Output: {result.output}" + assert "Fetched issue: Investigate API timeout issues" in result.output + assert "Created session" in result.output + + # Verify session was created + session_manager = SessionManager(config_loader=config_loader) + sessions = session_manager.list_sessions() + + created_session = None + for session in sessions: + if session.session_type == "investigation": + created_session = session + break + + assert created_session is not None + assert created_session.session_type == "investigation" + assert created_session.issue_key == "#123" + assert "Investigate API timeout issues" in created_session.goal + assert created_session.name == "investigate-issue-123" + + def test_investigate_from_issue_not_found(self, temp_daf_home, monkeypatch): + """Test error handling when issue is not found.""" + # Set mock mode + monkeypatch.setenv("DAF_MOCK_MODE", "1") + + # Create config + config_loader = ConfigLoader() + config = config_loader.create_default_config() + from devflow.config.models import WorkspaceDefinition + + config.repos.workspaces = [ + WorkspaceDefinition(name="default", path=str(Path(temp_daf_home) / "workspace")) + ] + config_loader.save_config(config) + + # Create workspace and project + workspace = Path(temp_daf_home) / "workspace" + workspace.mkdir(parents=True, exist_ok=True) + test_project = workspace / "test-project" + test_project.mkdir(exist_ok=True) + + # Mock JIRA client to raise not found error + from devflow.issue_tracker.exceptions import IssueTrackerNotFoundError + with patch("devflow.jira.JiraClient") as MockJiraClient: + mock_client = MagicMock() + mock_client.get_ticket.side_effect = IssueTrackerNotFoundError("Issue PROJ-99999 not found") + MockJiraClient.return_value = mock_client + + runner = CliRunner() + result = runner.invoke(cli, [ + "investigate", + "PROJ-99999", + "--path", str(test_project), + ]) + + # Should fail + assert result.exit_code == 1 + assert "Issue not found: PROJ-99999" in result.output + + def test_investigate_from_issue_auth_error(self, temp_daf_home, monkeypatch): + """Test error handling when authentication fails.""" + # Set mock mode + monkeypatch.setenv("DAF_MOCK_MODE", "1") + + # Create config + config_loader = ConfigLoader() + config = config_loader.create_default_config() + from devflow.config.models import WorkspaceDefinition + + config.repos.workspaces = [ + WorkspaceDefinition(name="default", path=str(Path(temp_daf_home) / "workspace")) + ] + config_loader.save_config(config) + + # Create workspace and project + workspace = Path(temp_daf_home) / "workspace" + workspace.mkdir(parents=True, exist_ok=True) + test_project = workspace / "test-project" + test_project.mkdir(exist_ok=True) + + # Mock JIRA client to raise auth error + from devflow.issue_tracker.exceptions import IssueTrackerAuthError + with patch("devflow.jira.JiraClient") as MockJiraClient: + mock_client = MagicMock() + mock_client.get_ticket.side_effect = IssueTrackerAuthError("Authentication failed") + MockJiraClient.return_value = mock_client + + runner = CliRunner() + result = runner.invoke(cli, [ + "investigate", + "PROJ-12345", + "--path", str(test_project), + ]) + + # Should fail + assert result.exit_code == 1 + assert "Authentication failed" in result.output + + def test_investigate_from_issue_with_goal_override(self, temp_daf_home, monkeypatch): + """Test that --goal flag overrides issue summary.""" + # Set mock mode + monkeypatch.setenv("DAF_MOCK_MODE", "1") + + # Create config + config_loader = ConfigLoader() + config = config_loader.create_default_config() + from devflow.config.models import WorkspaceDefinition + + config.repos.workspaces = [ + WorkspaceDefinition(name="default", path=str(Path(temp_daf_home) / "workspace")) + ] + config_loader.save_config(config) + + # Create workspace and project + workspace = Path(temp_daf_home) / "workspace" + workspace.mkdir(parents=True, exist_ok=True) + test_project = workspace / "test-project" + test_project.mkdir(exist_ok=True) + + # Mock JIRA client + with patch("devflow.jira.JiraClient") as MockJiraClient: + mock_client = MagicMock() + mock_client.get_ticket.return_value = { + "summary": "Original issue summary", + "description": "Original description", + "key": "PROJ-55555", + } + MockJiraClient.return_value = mock_client + + runner = CliRunner() + result = runner.invoke(cli, [ + "investigate", + "PROJ-55555", + "--goal", "Custom investigation goal", + "--path", str(test_project), + ]) + + # Should succeed + assert result.exit_code == 0, f"Output: {result.output}" + assert "Created session" in result.output + + # Verify session uses custom goal, not issue summary + session_manager = SessionManager(config_loader=config_loader) + sessions = session_manager.list_sessions() + + created_session = None + for session in sessions: + if session.session_type == "investigation": + created_session = session + break + + assert created_session is not None + assert "Custom investigation goal" in created_session.goal + assert "Original issue summary" not in created_session.goal + + def test_investigate_from_issue_prompt_includes_description(self, temp_daf_home, monkeypatch): + """Test that investigation prompt includes issue description.""" + from devflow.cli.commands.investigate_command import _build_investigation_prompt + + # Create config + config_loader = ConfigLoader() + config = config_loader.create_default_config() + + # Build prompt with issue details + prompt = _build_investigation_prompt( + goal="Investigate Redis caching", + parent=None, + config=config, + session_name="test-investigation", + project_path="/test/path", + workspace="/test/workspace", + issue_key="PROJ-12345", + issue_details={ + "summary": "Investigate Redis caching for subscription API", + "description": "We need to research Redis caching options to improve performance" + } + ) + + # Verify prompt includes issue details + assert "PROJ-12345" in prompt + assert "Issue Details" in prompt + assert "Investigate Redis caching for subscription API" in prompt + assert "We need to research Redis caching options to improve performance" in prompt + + def test_investigate_from_issue_custom_name(self, temp_daf_home, monkeypatch): + """Test creating investigation from issue with custom session name.""" + # Set mock mode + monkeypatch.setenv("DAF_MOCK_MODE", "1") + + # Create config + config_loader = ConfigLoader() + config = config_loader.create_default_config() + from devflow.config.models import WorkspaceDefinition + + config.repos.workspaces = [ + WorkspaceDefinition(name="default", path=str(Path(temp_daf_home) / "workspace")) + ] + config_loader.save_config(config) + + # Create workspace and project + workspace = Path(temp_daf_home) / "workspace" + workspace.mkdir(parents=True, exist_ok=True) + test_project = workspace / "test-project" + test_project.mkdir(exist_ok=True) + + # Mock JIRA client + with patch("devflow.jira.JiraClient") as MockJiraClient: + mock_client = MagicMock() + mock_client.get_ticket.return_value = { + "summary": "Test issue", + "description": "Test description", + "key": "PROJ-77777", + } + MockJiraClient.return_value = mock_client + + runner = CliRunner() + result = runner.invoke(cli, [ + "investigate", + "PROJ-77777", + "--name", "custom-investigation-name", + "--path", str(test_project), + ]) + + # Should succeed + assert result.exit_code == 0, f"Output: {result.output}" + + # Verify session uses custom name + session_manager = SessionManager(config_loader=config_loader) + sessions = session_manager.list_sessions() + + created_session = None + for session in sessions: + if session.session_type == "investigation": + created_session = session + break + + assert created_session is not None + assert created_session.name == "custom-investigation-name"