Skip to content

Commit bbd4f13

Browse files
itdoveclaude
andauthored
feat(link): add --rename-prefix option to daf link command (#398)
- Add --rename-prefix CLI option that renames the session to {prefix}-{issue_key_slug} after linking - Include renamed_from field in JSON output when rename occurs - Update display output to use the new session name after rename - Remove @require_outside_claude decorator from link_jira function - Add tests for rename-prefix functionality including edge cases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent bf2fa4c commit bbd4f13

3 files changed

Lines changed: 193 additions & 15 deletions

File tree

devflow/cli/commands/link_command.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Implementation of 'daf link' command."""
22

3+
import logging
34
import subprocess
45
import sys
56
from typing import Optional
@@ -14,6 +15,7 @@
1415
from devflow.session.manager import SessionManager
1516

1617
console = Console()
18+
logger = logging.getLogger(__name__)
1719

1820

1921
def _fetch_issue_metadata_dict(issue_key: str) -> Optional[dict]:
@@ -48,18 +50,19 @@ def _fetch_issue_metadata_dict(issue_key: str) -> Optional[dict]:
4850
raise RuntimeError(f"Timeout validating issue tracker ticket {issue_key}")
4951

5052

51-
@require_outside_claude
5253
def link_jira(
5354
name: str,
5455
issue_key: str,
5556
force: bool = False,
57+
rename_prefix: Optional[str] = None,
5658
) -> None:
5759
"""Link a issue tracker ticket to a session group.
5860
5961
Args:
6062
name: Session group name
6163
issue_key: issue tracker key to link
6264
force: Skip confirmation prompts
65+
rename_prefix: If provided, rename the session to {prefix}-{issue_key_slug}
6366
"""
6467
config_loader = ConfigLoader()
6568
session_manager = SessionManager(config_loader)
@@ -125,23 +128,40 @@ def link_jira(
125128

126129
session_manager.update_session(session)
127130

131+
# Rename session if --rename-prefix provided
132+
renamed_to = None
133+
if rename_prefix:
134+
from devflow.cli.commands.sync_command import issue_key_to_session_name
135+
base_slug = issue_key_to_session_name(issue_key)
136+
new_name = f"{rename_prefix}-{base_slug}"
137+
138+
try:
139+
session_manager.rename_session(name, new_name)
140+
renamed_to = new_name
141+
console_print(f"[green]✓[/green] Renamed session to: [bold]{new_name}[/bold]")
142+
except ValueError as e:
143+
console_print(f"[yellow]⚠[/yellow] Could not rename session: {e}")
144+
logger.warning(f"Rename failed for session '{name}' to '{new_name}': {e}")
145+
146+
display_name = renamed_to or name
147+
128148
if is_json_mode():
129-
output_json(
130-
success=True,
131-
data={
132-
"session_group": name,
133-
"issue_key": issue_key,
134-
"sessions_updated": len(sessions),
135-
"replaced": existing_jira,
136-
"metadata": issue_metadata_dict
137-
}
138-
)
149+
data = {
150+
"session_group": display_name,
151+
"issue_key": issue_key,
152+
"sessions_updated": len(sessions),
153+
"replaced": existing_jira,
154+
"metadata": issue_metadata_dict,
155+
}
156+
if renamed_to:
157+
data["renamed_from"] = name
158+
output_json(success=True, data=data)
139159
else:
140-
console.print(f"[green]✓[/green] Linked session group '{name}' to {issue_key}")
160+
console.print(f"[green]✓[/green] Linked session group '{display_name}' to {issue_key}")
141161
console.print(f"[dim]All {len(sessions)} session(s) in group now associated with {issue_key}[/dim]")
142162
console.print()
143163
console.print(f"[dim]You can now use either identifier:[/dim]")
144-
console.print(f" daf open {name}")
164+
console.print(f" daf open {display_name}")
145165
console.print(f" daf open {issue_key}")
146166

147167

devflow/cli/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,16 +1309,17 @@ def import_session_cmd(ctx: click.Context, uuid: str, jira: str, goal: str, goal
13091309
@click.argument("name", shell_complete=complete_session_identifiers)
13101310
@click.option("--jira", "issue_key", required=True, help="issue tracker key to link")
13111311
@click.option("--force", is_flag=True, help="Skip confirmation prompts (auto-replace existing links)")
1312+
@click.option("--rename-prefix", help="Rename session to {prefix}-{issue_key_slug} after linking")
13121313
@json_option
1313-
def link(ctx: click.Context, name: str, issue_key: str, force: bool) -> None:
1314+
def link(ctx: click.Context, name: str, issue_key: str, force: bool, rename_prefix: str) -> None:
13141315
"""Link a issue tracker ticket to a session group.
13151316
13161317
Associates a issue tracker ticket with all sessions in the specified session group.
13171318
After linking, you can use either the session name or issue key to access the sessions.
13181319
"""
13191320
from devflow.cli.commands.link_command import link_jira
13201321

1321-
link_jira(name, issue_key, force)
1322+
link_jira(name, issue_key, force, rename_prefix=rename_prefix)
13221323

13231324

13241325
@cli.command()

tests/test_link_command.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,3 +550,160 @@ def test_link_json_error_for_invalid_jira(mock_jira_cli, temp_daf_home):
550550
assert output_data["success"] is False
551551
assert "error" in output_data
552552
assert output_data["error"]["code"] == "VALIDATION_ERROR"
553+
554+
555+
def test_link_with_rename_prefix_jira_key(mock_jira_cli, temp_daf_home):
556+
"""Test --rename-prefix renames session using JIRA-style issue key."""
557+
mock_jira_cli.set_ticket("PROJ-456", {
558+
"key": "PROJ-456",
559+
"fields": {"summary": "Add caching", "status": {"name": "New"}}
560+
})
561+
562+
runner = CliRunner()
563+
564+
# Create a session
565+
result = runner.invoke(cli, [
566+
"new", "--name", "my-goal-abc123", "--goal", "Add caching",
567+
"--path", str(temp_daf_home / "test-project")
568+
], input="n\n")
569+
assert result.exit_code == 0
570+
571+
# Link with --rename-prefix
572+
result = runner.invoke(cli, [
573+
"link", "my-goal-abc123", "--jira", "PROJ-456", "--rename-prefix", "creation"
574+
])
575+
assert result.exit_code == 0
576+
assert "creation-PROJ-456" in result.output
577+
578+
# Verify: session accessible by new name
579+
config_loader = ConfigLoader()
580+
sessions = config_loader.load_sessions().get_sessions("creation-PROJ-456")
581+
assert sessions is not None
582+
assert len(sessions) > 0
583+
assert sessions[0].issue_key == "PROJ-456"
584+
585+
# Verify: old name no longer exists
586+
old_sessions = config_loader.load_sessions().get_sessions("my-goal-abc123")
587+
assert len(old_sessions) == 0
588+
589+
590+
def test_link_rename_prefix_slug_generation():
591+
"""Test that issue_key_to_session_name produces correct slugs for rename-prefix usage."""
592+
from devflow.cli.commands.sync_command import issue_key_to_session_name
593+
594+
# JIRA-style keys
595+
assert issue_key_to_session_name("PROJ-456") == "PROJ-456"
596+
597+
# GitHub-style keys (owner/repo#123)
598+
assert issue_key_to_session_name("itdove/devaiflow#456") == "itdove-devaiflow-456"
599+
600+
# Enterprise hostname keys
601+
assert issue_key_to_session_name("itdove/devaiflow#60", "github.enterprise.com") == "github-enterprise-com-itdove-devaiflow-60"
602+
603+
# Default hostnames omit hostname
604+
assert issue_key_to_session_name("itdove/devaiflow#60", "github.com") == "itdove-devaiflow-60"
605+
606+
607+
def test_link_with_rename_prefix_conflict(mock_jira_cli, temp_daf_home):
608+
"""Test --rename-prefix warns but doesn't fail when target name exists."""
609+
mock_jira_cli.set_ticket("PROJ-111", {
610+
"key": "PROJ-111",
611+
"fields": {"summary": "First", "status": {"name": "New"}}
612+
})
613+
mock_jira_cli.set_ticket("PROJ-222", {
614+
"key": "PROJ-222",
615+
"fields": {"summary": "Second", "status": {"name": "New"}}
616+
})
617+
618+
runner = CliRunner()
619+
620+
# Create two sessions — one with the name that the rename would produce
621+
result = runner.invoke(cli, [
622+
"new", "--name", "creation-PROJ-222", "--goal", "Existing",
623+
"--path", str(temp_daf_home / "project1")
624+
], input="n\n")
625+
assert result.exit_code == 0
626+
627+
result = runner.invoke(cli, [
628+
"new", "--name", "my-session", "--goal", "New work",
629+
"--path", str(temp_daf_home / "project2")
630+
], input="n\n")
631+
assert result.exit_code == 0
632+
633+
# Link with --rename-prefix — target name already exists
634+
result = runner.invoke(cli, [
635+
"link", "my-session", "--jira", "PROJ-222", "--rename-prefix", "creation"
636+
])
637+
638+
# Should succeed (link works) but warn about rename failure
639+
assert result.exit_code == 0
640+
assert "Could not rename" in result.output or "already exists" in result.output
641+
642+
# Verify: session is still linked (even though rename failed)
643+
config_loader = ConfigLoader()
644+
sessions = config_loader.load_sessions().get_sessions("my-session")
645+
assert len(sessions) > 0
646+
assert sessions[0].issue_key == "PROJ-222"
647+
648+
649+
def test_link_works_inside_claude_session(mock_jira_cli, temp_daf_home, monkeypatch):
650+
"""Test that link_jira works when called from inside a Claude session (no @require_outside_claude)."""
651+
mock_jira_cli.set_ticket("PROJ-789", {
652+
"key": "PROJ-789",
653+
"fields": {"summary": "Inside session", "status": {"name": "New"}}
654+
})
655+
656+
runner = CliRunner()
657+
658+
# Create a session
659+
result = runner.invoke(cli, [
660+
"new", "--name", "in-agent-test", "--goal", "Test inside agent",
661+
"--path", str(temp_daf_home / "test-project")
662+
], input="n\n")
663+
assert result.exit_code == 0
664+
665+
# Simulate being inside a Claude session
666+
monkeypatch.setenv("DEVAIFLOW_IN_SESSION", "in-agent-test")
667+
668+
# Link should work (not be blocked by require_outside_claude)
669+
result = runner.invoke(cli, [
670+
"link", "in-agent-test", "--jira", "PROJ-789"
671+
])
672+
assert result.exit_code == 0
673+
assert "Linked" in result.output or "PROJ-789" in result.output
674+
675+
# Verify: session is linked
676+
config_loader = ConfigLoader()
677+
sessions = config_loader.load_sessions().get_sessions("in-agent-test")
678+
assert len(sessions) > 0
679+
assert sessions[0].issue_key == "PROJ-789"
680+
681+
682+
def test_link_with_rename_prefix_json_output(mock_jira_cli, temp_daf_home):
683+
"""Test --rename-prefix includes renamed_from in JSON output."""
684+
mock_jira_cli.set_ticket("PROJ-999", {
685+
"key": "PROJ-999",
686+
"fields": {"summary": "JSON test", "status": {"name": "New"}}
687+
})
688+
689+
runner = CliRunner()
690+
691+
# Create a session
692+
result = runner.invoke(cli, [
693+
"new", "--name", "json-rename-test", "--goal", "Test JSON",
694+
"--path", str(temp_daf_home / "test-project")
695+
], input="n\n")
696+
assert result.exit_code == 0
697+
698+
# Link with --rename-prefix and --json
699+
result = runner.invoke(cli, [
700+
"link", "json-rename-test", "--jira", "PROJ-999",
701+
"--rename-prefix", "creation", "--json"
702+
])
703+
assert result.exit_code == 0
704+
705+
output_data = json.loads(result.output)
706+
assert output_data["success"] is True
707+
assert output_data["data"]["session_group"] == "creation-PROJ-999"
708+
assert output_data["data"]["renamed_from"] == "json-rename-test"
709+
assert output_data["data"]["issue_key"] == "PROJ-999"

0 commit comments

Comments
 (0)