doit-toolkit-cli 0.1.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of doit-toolkit-cli might be problematic. Click here for more details.

Files changed (135) hide show
  1. doit_cli/__init__.py +1356 -0
  2. doit_cli/cli/__init__.py +26 -0
  3. doit_cli/cli/analytics_command.py +616 -0
  4. doit_cli/cli/context_command.py +213 -0
  5. doit_cli/cli/diagram_command.py +304 -0
  6. doit_cli/cli/fixit_command.py +641 -0
  7. doit_cli/cli/hooks_command.py +211 -0
  8. doit_cli/cli/init_command.py +613 -0
  9. doit_cli/cli/memory_command.py +293 -0
  10. doit_cli/cli/roadmapit_command.py +10 -0
  11. doit_cli/cli/status_command.py +117 -0
  12. doit_cli/cli/sync_prompts_command.py +248 -0
  13. doit_cli/cli/validate_command.py +196 -0
  14. doit_cli/cli/verify_command.py +204 -0
  15. doit_cli/cli/workflow_mixin.py +224 -0
  16. doit_cli/cli/xref_command.py +555 -0
  17. doit_cli/formatters/__init__.py +8 -0
  18. doit_cli/formatters/base.py +38 -0
  19. doit_cli/formatters/json_formatter.py +126 -0
  20. doit_cli/formatters/markdown_formatter.py +97 -0
  21. doit_cli/formatters/rich_formatter.py +257 -0
  22. doit_cli/main.py +51 -0
  23. doit_cli/models/__init__.py +139 -0
  24. doit_cli/models/agent.py +74 -0
  25. doit_cli/models/analytics_models.py +384 -0
  26. doit_cli/models/context_config.py +464 -0
  27. doit_cli/models/crossref_models.py +182 -0
  28. doit_cli/models/diagram_models.py +363 -0
  29. doit_cli/models/fixit_models.py +355 -0
  30. doit_cli/models/hook_config.py +125 -0
  31. doit_cli/models/project.py +91 -0
  32. doit_cli/models/results.py +121 -0
  33. doit_cli/models/search_models.py +228 -0
  34. doit_cli/models/status_models.py +195 -0
  35. doit_cli/models/sync_models.py +146 -0
  36. doit_cli/models/template.py +77 -0
  37. doit_cli/models/validation_models.py +175 -0
  38. doit_cli/models/workflow_models.py +319 -0
  39. doit_cli/prompts/__init__.py +5 -0
  40. doit_cli/prompts/fixit_prompts.py +344 -0
  41. doit_cli/prompts/interactive.py +390 -0
  42. doit_cli/rules/__init__.py +5 -0
  43. doit_cli/rules/builtin_rules.py +160 -0
  44. doit_cli/services/__init__.py +79 -0
  45. doit_cli/services/agent_detector.py +168 -0
  46. doit_cli/services/analytics_service.py +218 -0
  47. doit_cli/services/architecture_generator.py +290 -0
  48. doit_cli/services/backup_service.py +204 -0
  49. doit_cli/services/config_loader.py +113 -0
  50. doit_cli/services/context_loader.py +1123 -0
  51. doit_cli/services/coverage_calculator.py +142 -0
  52. doit_cli/services/crossref_service.py +237 -0
  53. doit_cli/services/cycle_time_calculator.py +134 -0
  54. doit_cli/services/date_inferrer.py +349 -0
  55. doit_cli/services/diagram_service.py +337 -0
  56. doit_cli/services/drift_detector.py +109 -0
  57. doit_cli/services/entity_parser.py +301 -0
  58. doit_cli/services/er_diagram_generator.py +197 -0
  59. doit_cli/services/fixit_service.py +699 -0
  60. doit_cli/services/github_service.py +192 -0
  61. doit_cli/services/hook_manager.py +258 -0
  62. doit_cli/services/hook_validator.py +528 -0
  63. doit_cli/services/input_validator.py +322 -0
  64. doit_cli/services/memory_search.py +527 -0
  65. doit_cli/services/mermaid_validator.py +334 -0
  66. doit_cli/services/prompt_transformer.py +91 -0
  67. doit_cli/services/prompt_writer.py +133 -0
  68. doit_cli/services/query_interpreter.py +428 -0
  69. doit_cli/services/report_exporter.py +219 -0
  70. doit_cli/services/report_generator.py +256 -0
  71. doit_cli/services/requirement_parser.py +112 -0
  72. doit_cli/services/roadmap_summarizer.py +209 -0
  73. doit_cli/services/rule_engine.py +443 -0
  74. doit_cli/services/scaffolder.py +215 -0
  75. doit_cli/services/score_calculator.py +172 -0
  76. doit_cli/services/section_parser.py +204 -0
  77. doit_cli/services/spec_scanner.py +327 -0
  78. doit_cli/services/state_manager.py +355 -0
  79. doit_cli/services/status_reporter.py +143 -0
  80. doit_cli/services/task_parser.py +347 -0
  81. doit_cli/services/template_manager.py +710 -0
  82. doit_cli/services/template_reader.py +158 -0
  83. doit_cli/services/user_journey_generator.py +214 -0
  84. doit_cli/services/user_story_parser.py +232 -0
  85. doit_cli/services/validation_service.py +188 -0
  86. doit_cli/services/validator.py +232 -0
  87. doit_cli/services/velocity_tracker.py +173 -0
  88. doit_cli/services/workflow_engine.py +405 -0
  89. doit_cli/templates/agent-file-template.md +28 -0
  90. doit_cli/templates/checklist-template.md +39 -0
  91. doit_cli/templates/commands/doit.checkin.md +363 -0
  92. doit_cli/templates/commands/doit.constitution.md +187 -0
  93. doit_cli/templates/commands/doit.documentit.md +485 -0
  94. doit_cli/templates/commands/doit.fixit.md +181 -0
  95. doit_cli/templates/commands/doit.implementit.md +265 -0
  96. doit_cli/templates/commands/doit.planit.md +262 -0
  97. doit_cli/templates/commands/doit.reviewit.md +355 -0
  98. doit_cli/templates/commands/doit.roadmapit.md +389 -0
  99. doit_cli/templates/commands/doit.scaffoldit.md +458 -0
  100. doit_cli/templates/commands/doit.specit.md +521 -0
  101. doit_cli/templates/commands/doit.taskit.md +304 -0
  102. doit_cli/templates/commands/doit.testit.md +277 -0
  103. doit_cli/templates/config/context.yaml +134 -0
  104. doit_cli/templates/config/hooks.yaml +93 -0
  105. doit_cli/templates/config/validation-rules.yaml +64 -0
  106. doit_cli/templates/github-issue-templates/epic.yml +78 -0
  107. doit_cli/templates/github-issue-templates/feature.yml +116 -0
  108. doit_cli/templates/github-issue-templates/task.yml +129 -0
  109. doit_cli/templates/hooks/.gitkeep +0 -0
  110. doit_cli/templates/hooks/post-commit.sh +25 -0
  111. doit_cli/templates/hooks/post-merge.sh +75 -0
  112. doit_cli/templates/hooks/pre-commit.sh +17 -0
  113. doit_cli/templates/hooks/pre-push.sh +18 -0
  114. doit_cli/templates/memory/completed_roadmap.md +50 -0
  115. doit_cli/templates/memory/constitution.md +125 -0
  116. doit_cli/templates/memory/roadmap.md +61 -0
  117. doit_cli/templates/plan-template.md +146 -0
  118. doit_cli/templates/scripts/bash/check-prerequisites.sh +166 -0
  119. doit_cli/templates/scripts/bash/common.sh +156 -0
  120. doit_cli/templates/scripts/bash/create-new-feature.sh +297 -0
  121. doit_cli/templates/scripts/bash/setup-plan.sh +61 -0
  122. doit_cli/templates/scripts/bash/update-agent-context.sh +675 -0
  123. doit_cli/templates/scripts/powershell/check-prerequisites.ps1 +148 -0
  124. doit_cli/templates/scripts/powershell/common.ps1 +137 -0
  125. doit_cli/templates/scripts/powershell/create-new-feature.ps1 +283 -0
  126. doit_cli/templates/scripts/powershell/setup-plan.ps1 +61 -0
  127. doit_cli/templates/scripts/powershell/update-agent-context.ps1 +406 -0
  128. doit_cli/templates/spec-template.md +159 -0
  129. doit_cli/templates/tasks-template.md +313 -0
  130. doit_cli/templates/vscode-settings.json +14 -0
  131. doit_toolkit_cli-0.1.10.dist-info/METADATA +324 -0
  132. doit_toolkit_cli-0.1.10.dist-info/RECORD +135 -0
  133. doit_toolkit_cli-0.1.10.dist-info/WHEEL +4 -0
  134. doit_toolkit_cli-0.1.10.dist-info/entry_points.txt +2 -0
  135. doit_toolkit_cli-0.1.10.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,256 @@
1
+ """Report generator for validation results."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+ from rich.tree import Tree
11
+
12
+ from ..models.validation_models import Severity, ValidationResult, ValidationStatus
13
+
14
+
15
+ class ReportGenerator:
16
+ """Generates validation reports in human-readable and JSON formats."""
17
+
18
+ def __init__(self, console: Optional[Console] = None) -> None:
19
+ """Initialize report generator.
20
+
21
+ Args:
22
+ console: Rich console for output. Creates new one if None.
23
+ """
24
+ self.console = console or Console()
25
+
26
+ def display_result(self, result: ValidationResult) -> None:
27
+ """Display a single validation result with rich formatting.
28
+
29
+ Args:
30
+ result: The validation result to display.
31
+ """
32
+ spec_name = Path(result.spec_path).parent.name
33
+ self.console.print()
34
+ self.console.print(f"[bold]Validating:[/bold] {result.spec_path}")
35
+ self.console.print()
36
+
37
+ if not result.issues:
38
+ self.console.print("[green]No issues found[/green]")
39
+ else:
40
+ # Group issues by severity
41
+ errors = [i for i in result.issues if i.severity == Severity.ERROR]
42
+ warnings = [i for i in result.issues if i.severity == Severity.WARNING]
43
+ infos = [i for i in result.issues if i.severity == Severity.INFO]
44
+
45
+ # Display errors
46
+ if errors:
47
+ self._display_issue_group("ERRORS", errors, "red")
48
+
49
+ # Display warnings
50
+ if warnings:
51
+ self._display_issue_group("WARNINGS", warnings, "yellow")
52
+
53
+ # Display info
54
+ if infos:
55
+ self._display_issue_group("INFO", infos, "blue")
56
+
57
+ # Display summary line
58
+ self.console.print()
59
+ self.console.print("━" * 50)
60
+ self._display_summary_line(result)
61
+
62
+ def _display_issue_group(
63
+ self,
64
+ title: str,
65
+ issues: list,
66
+ color: str,
67
+ ) -> None:
68
+ """Display a group of issues with tree formatting.
69
+
70
+ Args:
71
+ title: Group title (e.g., "ERRORS").
72
+ issues: List of issues to display.
73
+ color: Color for the group.
74
+ """
75
+ tree = Tree(f"[{color}]{title} ({len(issues)})[/{color}]")
76
+
77
+ for i, issue in enumerate(issues):
78
+ is_last = i == len(issues) - 1
79
+ prefix = "└─" if is_last else "├─"
80
+
81
+ line_info = f"Line {issue.line_number}: " if issue.line_number > 0 else ""
82
+ tree.add(f"[{color}]{line_info}{issue.message}[/{color}]")
83
+
84
+ if issue.suggestion:
85
+ # Add suggestion as child
86
+ tree.add(f" [dim]Suggestion: {issue.suggestion}[/dim]")
87
+
88
+ self.console.print(tree)
89
+
90
+ def _display_summary_line(self, result: ValidationResult) -> None:
91
+ """Display the summary line with score and status.
92
+
93
+ Args:
94
+ result: The validation result.
95
+ """
96
+ # Determine status color
97
+ status_colors = {
98
+ ValidationStatus.PASS: "green",
99
+ ValidationStatus.WARN: "yellow",
100
+ ValidationStatus.FAIL: "red",
101
+ }
102
+ color = status_colors.get(result.status, "white")
103
+
104
+ # Build status text
105
+ parts = []
106
+ if result.error_count > 0:
107
+ parts.append(f"{result.error_count} error{'s' if result.error_count != 1 else ''}")
108
+ if result.warning_count > 0:
109
+ parts.append(f"{result.warning_count} warning{'s' if result.warning_count != 1 else ''}")
110
+ if result.info_count > 0:
111
+ parts.append(f"{result.info_count} info")
112
+
113
+ status_detail = ", ".join(parts) if parts else "no issues"
114
+
115
+ self.console.print(
116
+ f"[bold]Quality Score:[/bold] {result.quality_score}/100"
117
+ )
118
+ self.console.print(
119
+ f"[bold]Status:[/bold] [{color}]{result.status.value.upper()}[/{color}] ({status_detail})"
120
+ )
121
+
122
+ def display_summary(
123
+ self,
124
+ results: list[ValidationResult],
125
+ summary: dict,
126
+ ) -> None:
127
+ """Display summary table for multiple validation results.
128
+
129
+ Args:
130
+ results: List of validation results.
131
+ summary: Summary statistics from ValidationService.get_summary().
132
+ """
133
+ self.console.print()
134
+ self.console.print("[bold]Spec Validation Summary[/bold]")
135
+ self.console.print()
136
+
137
+ if not results:
138
+ self.console.print("[yellow]No spec files found to validate[/yellow]")
139
+ return
140
+
141
+ # Create table
142
+ table = Table(show_header=True, header_style="bold cyan")
143
+ table.add_column("Spec", style="dim")
144
+ table.add_column("Score", justify="center")
145
+ table.add_column("Status", justify="center")
146
+ table.add_column("Errors", justify="center")
147
+ table.add_column("Warnings", justify="center")
148
+
149
+ for result in results:
150
+ # Get spec name from path
151
+ spec_name = Path(result.spec_path).parent.name
152
+
153
+ # Determine status color
154
+ status_colors = {
155
+ ValidationStatus.PASS: "green",
156
+ ValidationStatus.WARN: "yellow",
157
+ ValidationStatus.FAIL: "red",
158
+ }
159
+ color = status_colors.get(result.status, "white")
160
+
161
+ table.add_row(
162
+ spec_name,
163
+ str(result.quality_score),
164
+ f"[{color}]{result.status.value.upper()}[/{color}]",
165
+ str(result.error_count),
166
+ str(result.warning_count),
167
+ )
168
+
169
+ self.console.print(table)
170
+
171
+ # Display summary line
172
+ self.console.print()
173
+ self.console.print("━" * 50)
174
+ self.console.print(
175
+ f"[bold]Total:[/bold] {summary['total_specs']} specs | "
176
+ f"[green]Passed: {summary['passed']}[/green] | "
177
+ f"[yellow]Warned: {summary['warned']}[/yellow] | "
178
+ f"[red]Failed: {summary['failed']}[/red] | "
179
+ f"Avg Score: {summary['average_score']}"
180
+ )
181
+
182
+ def to_json(self, result: ValidationResult) -> str:
183
+ """Convert single validation result to JSON string.
184
+
185
+ Args:
186
+ result: The validation result.
187
+
188
+ Returns:
189
+ JSON string representation.
190
+ """
191
+ output = {
192
+ "spec_path": result.spec_path,
193
+ "status": result.status.value,
194
+ "quality_score": result.quality_score,
195
+ "error_count": result.error_count,
196
+ "warning_count": result.warning_count,
197
+ "info_count": result.info_count,
198
+ "issues": [
199
+ {
200
+ "rule_id": issue.rule_id,
201
+ "severity": issue.severity.value,
202
+ "line_number": issue.line_number,
203
+ "message": issue.message,
204
+ "suggestion": issue.suggestion,
205
+ }
206
+ for issue in result.issues
207
+ ],
208
+ "validated_at": result.validated_at.isoformat(),
209
+ }
210
+ return json.dumps(output, indent=2)
211
+
212
+ def to_json_summary(
213
+ self,
214
+ results: list[ValidationResult],
215
+ summary: dict,
216
+ ) -> str:
217
+ """Convert multiple results to JSON summary string.
218
+
219
+ Args:
220
+ results: List of validation results.
221
+ summary: Summary statistics.
222
+
223
+ Returns:
224
+ JSON string representation.
225
+ """
226
+ output = {
227
+ "summary": {
228
+ "total_specs": summary["total_specs"],
229
+ "passed": summary["passed"],
230
+ "warned": summary["warned"],
231
+ "failed": summary["failed"],
232
+ "average_score": summary["average_score"],
233
+ },
234
+ "results": [
235
+ {
236
+ "spec_path": result.spec_path,
237
+ "status": result.status.value,
238
+ "quality_score": result.quality_score,
239
+ "error_count": result.error_count,
240
+ "warning_count": result.warning_count,
241
+ "info_count": result.info_count,
242
+ "issues": [
243
+ {
244
+ "rule_id": issue.rule_id,
245
+ "severity": issue.severity.value,
246
+ "line_number": issue.line_number,
247
+ "message": issue.message,
248
+ "suggestion": issue.suggestion,
249
+ }
250
+ for issue in result.issues
251
+ ],
252
+ }
253
+ for result in results
254
+ ],
255
+ }
256
+ return json.dumps(output, indent=2)
@@ -0,0 +1,112 @@
1
+ """Parser for extracting requirements from spec.md files."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from ..models.crossref_models import Requirement
8
+
9
+
10
+ class RequirementParser:
11
+ """Parses spec.md files to extract functional requirements.
12
+
13
+ This parser extracts FR-XXX requirements from specification files
14
+ using the standard format: `- **FR-XXX**: Description text`
15
+ """
16
+
17
+ # Pattern to match: - **FR-XXX**: Description
18
+ # Captures: group(1) = FR-XXX, group(2) = description
19
+ REQUIREMENT_PATTERN = re.compile(
20
+ r"^\s*-\s*\*\*(?P<id>FR-\d{3})\*\*:\s*(?P<description>.+)$",
21
+ re.MULTILINE,
22
+ )
23
+
24
+ def __init__(self, spec_path: Optional[Path] = None) -> None:
25
+ """Initialize parser with optional spec file path.
26
+
27
+ Args:
28
+ spec_path: Path to spec.md file. Can be set later via parse().
29
+ """
30
+ self.spec_path = spec_path
31
+
32
+ def parse(self, spec_path: Optional[Path] = None) -> list[Requirement]:
33
+ """Parse spec.md and extract all functional requirements.
34
+
35
+ Args:
36
+ spec_path: Path to spec.md file. Overrides constructor path.
37
+
38
+ Returns:
39
+ List of Requirement objects sorted by ID.
40
+
41
+ Raises:
42
+ FileNotFoundError: If spec file doesn't exist.
43
+ ValueError: If no spec path provided.
44
+ """
45
+ path = spec_path or self.spec_path
46
+ if path is None:
47
+ raise ValueError("No spec path provided")
48
+
49
+ path = Path(path)
50
+ if not path.exists():
51
+ raise FileNotFoundError(f"Spec file not found: {path}")
52
+
53
+ content = path.read_text(encoding="utf-8")
54
+ return self.parse_content(content, str(path))
55
+
56
+ def parse_content(self, content: str, spec_path: str) -> list[Requirement]:
57
+ """Parse content string and extract requirements.
58
+
59
+ Args:
60
+ content: Full content of spec.md file.
61
+ spec_path: Path to associate with extracted requirements.
62
+
63
+ Returns:
64
+ List of Requirement objects sorted by ID.
65
+ """
66
+ requirements: list[Requirement] = []
67
+ lines = content.split("\n")
68
+
69
+ for line_num, line in enumerate(lines, start=1):
70
+ match = self.REQUIREMENT_PATTERN.match(line)
71
+ if match:
72
+ req = Requirement(
73
+ id=match.group("id"),
74
+ spec_path=spec_path,
75
+ description=match.group("description").strip(),
76
+ line_number=line_num,
77
+ )
78
+ requirements.append(req)
79
+
80
+ # Sort by requirement ID (FR-001, FR-002, etc.)
81
+ requirements.sort(key=lambda r: r.id)
82
+ return requirements
83
+
84
+ def get_requirement(
85
+ self, requirement_id: str, spec_path: Optional[Path] = None
86
+ ) -> Optional[Requirement]:
87
+ """Get a specific requirement by ID.
88
+
89
+ Args:
90
+ requirement_id: The FR-XXX ID to find.
91
+ spec_path: Path to spec.md file.
92
+
93
+ Returns:
94
+ Requirement if found, None otherwise.
95
+ """
96
+ requirements = self.parse(spec_path)
97
+ for req in requirements:
98
+ if req.id == requirement_id:
99
+ return req
100
+ return None
101
+
102
+ def get_requirement_ids(self, spec_path: Optional[Path] = None) -> list[str]:
103
+ """Get list of all requirement IDs in a spec.
104
+
105
+ Args:
106
+ spec_path: Path to spec.md file.
107
+
108
+ Returns:
109
+ List of FR-XXX IDs.
110
+ """
111
+ requirements = self.parse(spec_path)
112
+ return [req.id for req in requirements]
@@ -0,0 +1,209 @@
1
+ """Roadmap summarization service for AI context injection.
2
+
3
+ This module provides the RoadmapSummarizer service that parses roadmap.md
4
+ and generates condensed summaries with P1/P2 items prioritized.
5
+ """
6
+
7
+ import re
8
+ from typing import Optional
9
+
10
+ from ..models.context_config import (
11
+ RoadmapItem,
12
+ RoadmapSummary,
13
+ SummarizationConfig,
14
+ )
15
+
16
+
17
+ class RoadmapSummarizer:
18
+ """Service for parsing and summarizing roadmap content.
19
+
20
+ Parses roadmap.md by priority sections (P1-P4) and generates condensed
21
+ summaries that include full P1/P2 items with rationale while reducing
22
+ P3/P4 to titles only.
23
+ """
24
+
25
+ def __init__(self, config: Optional[SummarizationConfig] = None) -> None:
26
+ """Initialize with summarization configuration.
27
+
28
+ Args:
29
+ config: Summarization configuration (uses defaults if None).
30
+ """
31
+ self.config = config or SummarizationConfig()
32
+
33
+ def parse_roadmap(self, content: str) -> list[RoadmapItem]:
34
+ """Parse roadmap.md content into structured items.
35
+
36
+ Extracts items from markdown, parsing P1-P4 sections, rationale,
37
+ and feature references.
38
+
39
+ Args:
40
+ content: Raw markdown content of roadmap.md
41
+
42
+ Returns:
43
+ List of RoadmapItem objects
44
+ """
45
+ items: list[RoadmapItem] = []
46
+ lines = content.split("\n")
47
+
48
+ current_priority = "P4" # Default priority
49
+ i = 0
50
+
51
+ while i < len(lines):
52
+ line = lines[i]
53
+
54
+ # Check for priority section headers (### P1 - Critical, ## P2, etc.)
55
+ priority_match = re.match(r"^#{2,3}\s*(P[1-4])\s*[-:]?\s*", line, re.IGNORECASE)
56
+ if priority_match:
57
+ current_priority = priority_match.group(1).upper()
58
+ i += 1
59
+ continue
60
+
61
+ # Check for checklist items (- [ ] or - [x]) or plain list items (- text)
62
+ item_match = re.match(r"^-\s*\[([ xX])\]\s*(.+)$", line)
63
+ plain_match = re.match(r"^-\s+([^[\s].+)$", line) if not item_match else None
64
+
65
+ if item_match:
66
+ completed = item_match.group(1).lower() == "x"
67
+ text = item_match.group(2).strip()
68
+ elif plain_match:
69
+ completed = False
70
+ text = plain_match.group(1).strip()
71
+ else:
72
+ i += 1
73
+ continue
74
+
75
+ # Extract feature reference from text (e.g., `[034-fixit-workflow]` or [034-fixit])
76
+ feature_ref = ""
77
+ feature_match = re.search(r"`?\[?(\d{3}-[\w-]+)\]?`?", text)
78
+ if feature_match:
79
+ feature_ref = feature_match.group(1)
80
+
81
+ # Look for rationale in following lines
82
+ rationale = ""
83
+ j = i + 1
84
+ while j < len(lines):
85
+ next_line = lines[j].strip()
86
+ # Check for rationale line
87
+ rationale_match = re.match(r"^-?\s*\*?\*?Rationale\*?\*?:\s*(.+)$", next_line, re.IGNORECASE)
88
+ if rationale_match:
89
+ rationale = rationale_match.group(1).strip()
90
+ break
91
+ # Stop if we hit another item or section
92
+ if next_line.startswith("- [") or next_line.startswith("-") or next_line.startswith("#"):
93
+ break
94
+ j += 1
95
+
96
+ items.append(RoadmapItem(
97
+ text=text,
98
+ priority=current_priority,
99
+ rationale=rationale,
100
+ feature_ref=feature_ref,
101
+ completed=completed,
102
+ ))
103
+
104
+ i += 1
105
+
106
+ return items
107
+
108
+ def summarize(
109
+ self,
110
+ items: list[RoadmapItem],
111
+ max_tokens: int = 2000,
112
+ current_feature: Optional[str] = None,
113
+ ) -> RoadmapSummary:
114
+ """Generate condensed roadmap summary.
115
+
116
+ Creates a summary with:
117
+ - Full P1/P2 items including rationale
118
+ - P3/P4 items as titles only
119
+ - Current feature highlighted if provided
120
+
121
+ Args:
122
+ items: Parsed roadmap items
123
+ max_tokens: Maximum tokens for output
124
+ current_feature: Current feature branch name for highlighting
125
+
126
+ Returns:
127
+ RoadmapSummary with condensed content
128
+ """
129
+ if not items:
130
+ return RoadmapSummary(
131
+ condensed_text="",
132
+ item_count=0,
133
+ priorities_included=[],
134
+ )
135
+
136
+ sections: list[str] = ["## Roadmap Summary", ""]
137
+ priorities_included: set[str] = set()
138
+ item_count = 0
139
+
140
+ # Group items by priority
141
+ by_priority: dict[str, list[RoadmapItem]] = {"P1": [], "P2": [], "P3": [], "P4": []}
142
+ current_feature_items: list[RoadmapItem] = []
143
+
144
+ for item in items:
145
+ if item.completed:
146
+ continue # Skip completed items in roadmap summary
147
+
148
+ priority = item.priority.upper()
149
+ if priority in by_priority:
150
+ by_priority[priority].append(item)
151
+
152
+ # Check if this is the current feature
153
+ if current_feature and item.feature_ref:
154
+ # Match feature ref against current branch
155
+ if current_feature in item.feature_ref or item.feature_ref in current_feature:
156
+ current_feature_items.append(item)
157
+
158
+ # Add current feature section if we found matches
159
+ if current_feature_items:
160
+ sections.append("### Current Feature")
161
+ for item in current_feature_items:
162
+ sections.append(f"- **[CURRENT]** {item.text}")
163
+ if item.rationale:
164
+ sections.append(f" - *Rationale*: {item.rationale}")
165
+ item_count += 1
166
+ priorities_included.add(item.priority)
167
+ sections.append("")
168
+
169
+ # High priority section (P1/P2) - full details
170
+ high_priority = by_priority["P1"] + by_priority["P2"]
171
+ # Filter out items already shown in current feature
172
+ high_priority = [i for i in high_priority if i not in current_feature_items]
173
+
174
+ if high_priority:
175
+ sections.append("### High Priority (P1-P2)")
176
+ for item in high_priority:
177
+ marker = "[P1]" if item.priority == "P1" else "[P2]"
178
+ sections.append(f"- {marker} {item.text}")
179
+ if item.rationale:
180
+ sections.append(f" - *Rationale*: {item.rationale}")
181
+ item_count += 1
182
+ priorities_included.add(item.priority)
183
+ sections.append("")
184
+
185
+ # Lower priority section (P3/P4) - titles only
186
+ low_priority = by_priority["P3"] + by_priority["P4"]
187
+ # Filter out items already shown
188
+ low_priority = [i for i in low_priority if i not in current_feature_items]
189
+
190
+ if low_priority:
191
+ sections.append("### Other Priorities (P3-P4)")
192
+ for item in low_priority:
193
+ # Extract just the title (first sentence or up to first period)
194
+ title = item.text.split(".")[0].strip()
195
+ if len(title) > 80:
196
+ title = title[:77] + "..."
197
+ marker = "[P3]" if item.priority == "P3" else "[P4]"
198
+ sections.append(f"- {marker} {title}")
199
+ item_count += 1
200
+ priorities_included.add(item.priority)
201
+ sections.append("")
202
+
203
+ condensed_text = "\n".join(sections)
204
+
205
+ return RoadmapSummary(
206
+ condensed_text=condensed_text,
207
+ item_count=item_count,
208
+ priorities_included=sorted(priorities_included),
209
+ )