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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions devflow/cli/commands/complete_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from devflow.issue_tracker.factory import create_issue_tracker_client
from devflow.issue_tracker.exceptions import IssueTrackerApiError, IssueTrackerAuthError, IssueTrackerNotFoundError, IssueTrackerValidationError
from devflow.utils.backend_detection import get_issue_tracker_backend
from devflow.utils.model_provider import get_co_authored_by_line
from devflow.session.manager import SessionManager
from devflow.utils import strip_code_fences
from devflow.utils.dependencies import require_tool
Expand Down Expand Up @@ -316,11 +317,12 @@ def complete_session(
commit_message_short = _prompt_for_commit_message(auto_message, config)

if commit_message_short:
co_authored_by = get_co_authored_by_line(config, session.model_profile)
full_message = f"""{commit_message_short}

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>"""
{co_authored_by}"""

success, error_msg = GitUtils.commit_all(working_dir, full_message)
if success:
Expand Down Expand Up @@ -482,11 +484,12 @@ def complete_session(
commit_message_short = _prompt_for_commit_message(auto_message, config)

if commit_message_short:
co_authored_by = get_co_authored_by_line(config, session.model_profile)
full_message = f"""{commit_message_short}

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>"""
{co_authored_by}"""

success, error_msg = GitUtils.commit_all(working_dir, full_message)
if success:
Expand Down Expand Up @@ -620,11 +623,12 @@ def complete_session(

if commit_message_short:
# Create commit with standard format
co_authored_by = get_co_authored_by_line(config, session.model_profile)
full_message = f"""{commit_message_short}

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>"""
{co_authored_by}"""
else:
# User cancelled the commit
should_commit = False
Expand Down Expand Up @@ -1319,11 +1323,12 @@ def _sync_branch_for_export(session, issue_key: str, config_loader) -> None:
console.print(f"[dim]Skipping commit - changes will not be included in export[/dim]")
else:
# Create WIP commit
co_authored_by = get_co_authored_by_line(config, session.model_profile)
commit_message = f"""WIP: Session export for {issue_key}

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>"""
{co_authored_by}"""

success, error_msg = GitUtils.commit_all(working_dir, commit_message)
if success:
Expand Down Expand Up @@ -2835,6 +2840,8 @@ def _generate_pr_description(session, working_dir: Path, config_loader: ConfigLo
else:
description_content = f"## Summary\n{summary_bullets}\n"

config = config_loader.load_config()
co_authored_by = get_co_authored_by_line(config, session.model_profile)
description = f"""{jira_section}{description_content}

## Test plan
Expand All @@ -2844,7 +2851,7 @@ def _generate_pr_description(session, working_dir: Path, config_loader: ConfigLo

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>"""
{co_authored_by}"""

return description

Expand Down
16 changes: 13 additions & 3 deletions devflow/cli/commands/export_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from devflow.export.manager import ExportManager
from devflow.git.utils import GitUtils
from devflow.session.manager import SessionManager
from devflow.utils.model_provider import get_co_authored_by_line

console = Console()

Expand Down Expand Up @@ -57,11 +58,13 @@ def export_sessions(
# Now syncs ALL conversations in multi-conversation sessions
# Captures remote URLs for fork support
if not all_sessions and issue_keys:
config = config_loader.load_config()
for identifier in issue_keys:
sessions = session_manager.index.get_sessions(identifier)
if sessions:
for session in sessions:
_sync_all_branches_for_export(session)
co_authored_by = get_co_authored_by_line(config, session.model_profile)
_sync_all_branches_for_export(session, co_authored_by=co_authored_by)
# Save session to persist remote URLs
session_manager.update_session(session)

Expand All @@ -87,7 +90,7 @@ def export_sessions(
raise


def _sync_all_branches_for_export(session) -> None:
def _sync_all_branches_for_export(session, co_authored_by: Optional[str] = None) -> None:
"""Sync all conversation branches before export for team handoff.

For multi-conversation sessions, syncs all branches across all conversations.
Expand All @@ -96,6 +99,7 @@ def _sync_all_branches_for_export(session) -> None:

Args:
session: Session object
co_authored_by: Optional Co-Authored-By line for commit messages
"""
# Check if this is a multi-project session (new architecture)
active_conv = session.active_conversation
Expand All @@ -111,6 +115,7 @@ def _sync_all_branches_for_export(session) -> None:
issue_key=session.issue_key,
working_dir_name=proj_name,
conversation=active_conv, # Pass conversation to update remote_url
co_authored_by=co_authored_by,
)
# Multi-conversation support (old architecture - backward compatibility)
elif session.conversations:
Expand All @@ -127,6 +132,7 @@ def _sync_all_branches_for_export(session) -> None:
issue_key=session.issue_key,
working_dir_name=working_dir,
conversation=active, # Pass active session to update remote_url
co_authored_by=co_authored_by,
)
# Legacy single-conversation support (fallback for sessions without working_directory)
elif session.active_conversation:
Expand All @@ -138,6 +144,7 @@ def _sync_all_branches_for_export(session) -> None:
branch=active_conv.branch,
session_name=session.name,
issue_key=session.issue_key,
co_authored_by=co_authored_by,
)


Expand All @@ -148,6 +155,7 @@ def _sync_single_conversation_branch(
issue_key: Optional[str] = None,
working_dir_name: Optional[str] = None,
conversation = None,
co_authored_by: Optional[str] = None,
) -> None:
"""Sync a single conversation's branch before export.

Expand All @@ -167,6 +175,7 @@ def _sync_single_conversation_branch(
issue_key: Optional issue key
working_dir_name: Optional working directory name (for multi-conversation sessions)
conversation: Optional ConversationContext to update with remote URL
co_authored_by: Optional Co-Authored-By line for commit messages

Raises:
ValueError: If checkout, commit, or push fails
Expand Down Expand Up @@ -224,11 +233,12 @@ def _sync_single_conversation_branch(
# Generate WIP commit message
identifier = issue_key if issue_key else session_name
dir_label = f" ({working_dir_name})" if working_dir_name else ""
attribution = co_authored_by or "Co-Authored-By: Claude <noreply@anthropic.com>"
commit_message = f"""WIP: Export for {identifier}{dir_label}

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>"""
{attribution}"""

# Commit all changes (REQUIRED)
success, error_msg = GitUtils.commit_all(working_dir, commit_message)
Expand Down
73 changes: 73 additions & 0 deletions devflow/utils/model_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""

import os
import re
from typing import Dict, Optional, Any


Expand Down Expand Up @@ -180,3 +181,75 @@ def get_profile_display_name(profile: Optional[Dict[str, Any]]) -> str:
return f"{name} ({base_url})"

return name


def parse_claude_model_display_name(model_id: str) -> str:
"""Parse a Claude model ID into a human-readable display name.

Args:
model_id: Model identifier (e.g., "claude-opus-4-6", "claude-3-5-sonnet-20241022")

Returns:
Human-readable name (e.g., "Claude Opus 4.6") or the original ID if not a Claude model
"""
if not model_id or not model_id.startswith("claude-"):
return model_id or "Claude"

# Strip context marker like [1m]
clean_id = re.sub(r'\[.*?\]$', '', model_id)

# Claude 4.x format: claude-{tier}-{major}-{minor}[-date]
match = re.match(r'^claude-(opus|sonnet|haiku)-(\d+)-(\d+)(?:-\d+)?$', clean_id)
if match:
tier = match.group(1).capitalize()
major = match.group(2)
minor = match.group(3)
return f"Claude {tier} {major}.{minor}"

# Claude 3.x format: claude-{major}[-{minor}]-{tier}[-date]
match = re.match(r'^claude-(\d+)(?:-(\d+))?-(opus|sonnet|haiku)(?:-\d+)?$', clean_id)
if match:
major = match.group(1)
minor = match.group(2)
tier = match.group(3).capitalize()
if minor:
return f"Claude {major}.{minor} {tier}"
return f"Claude {major} {tier}"

return model_id


def get_model_attribution_name(config, model_profile_override: Optional[str] = None) -> str:
"""Resolve the model display name for commit attribution.

Args:
config: Merged configuration object
model_profile_override: Optional profile name from session.model_profile

Returns:
Display name (e.g., "Claude Opus 4.6", "Claude") for use in Co-Authored-By
"""
profile = get_active_profile(config, override_profile_name=model_profile_override)
model_name = get_model_name_from_profile(profile)

if not model_name:
return "Claude"

if model_name.startswith("claude-"):
return parse_claude_model_display_name(model_name)

return model_name


def get_co_authored_by_line(config=None, model_profile_override: Optional[str] = None) -> str:
"""Build the Co-Authored-By attribution line for commit messages.

Args:
config: Merged configuration object (optional)
model_profile_override: Optional profile name from session.model_profile

Returns:
Full attribution string, e.g., "Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"
"""
name = get_model_attribution_name(config, model_profile_override) if config else "Claude"
return f"Co-Authored-By: {name} <noreply@anthropic.com>"
Loading
Loading