elspais 0.11.1__py3-none-any.whl → 0.11.2__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.
Files changed (53) hide show
  1. elspais/__init__.py +1 -1
  2. elspais/cli.py +29 -10
  3. elspais/commands/analyze.py +5 -6
  4. elspais/commands/changed.py +2 -6
  5. elspais/commands/config_cmd.py +4 -4
  6. elspais/commands/edit.py +32 -36
  7. elspais/commands/hash_cmd.py +24 -18
  8. elspais/commands/index.py +8 -7
  9. elspais/commands/init.py +4 -4
  10. elspais/commands/reformat_cmd.py +32 -43
  11. elspais/commands/rules_cmd.py +6 -2
  12. elspais/commands/trace.py +23 -19
  13. elspais/commands/validate.py +8 -10
  14. elspais/config/defaults.py +7 -1
  15. elspais/core/content_rules.py +0 -1
  16. elspais/core/git.py +4 -10
  17. elspais/core/parser.py +55 -56
  18. elspais/core/patterns.py +2 -6
  19. elspais/core/rules.py +10 -15
  20. elspais/mcp/__init__.py +2 -0
  21. elspais/mcp/context.py +1 -0
  22. elspais/mcp/serializers.py +1 -1
  23. elspais/mcp/server.py +54 -39
  24. elspais/reformat/__init__.py +13 -13
  25. elspais/reformat/detector.py +9 -16
  26. elspais/reformat/hierarchy.py +8 -7
  27. elspais/reformat/line_breaks.py +36 -38
  28. elspais/reformat/prompts.py +22 -12
  29. elspais/reformat/transformer.py +43 -41
  30. elspais/sponsors/__init__.py +0 -2
  31. elspais/testing/__init__.py +1 -1
  32. elspais/testing/result_parser.py +25 -21
  33. elspais/trace_view/__init__.py +4 -3
  34. elspais/trace_view/coverage.py +5 -5
  35. elspais/trace_view/generators/__init__.py +1 -1
  36. elspais/trace_view/generators/base.py +17 -12
  37. elspais/trace_view/generators/csv.py +2 -6
  38. elspais/trace_view/generators/markdown.py +3 -8
  39. elspais/trace_view/html/__init__.py +4 -2
  40. elspais/trace_view/html/generator.py +423 -289
  41. elspais/trace_view/models.py +25 -0
  42. elspais/trace_view/review/__init__.py +21 -18
  43. elspais/trace_view/review/branches.py +114 -121
  44. elspais/trace_view/review/models.py +232 -237
  45. elspais/trace_view/review/position.py +53 -71
  46. elspais/trace_view/review/server.py +264 -288
  47. elspais/trace_view/review/status.py +43 -58
  48. elspais/trace_view/review/storage.py +48 -72
  49. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/METADATA +1 -1
  50. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/RECORD +53 -53
  51. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/WHEEL +0 -0
  52. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/entry_points.txt +0 -0
  53. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/licenses/LICENSE +0 -0
@@ -23,18 +23,18 @@ from datetime import datetime, timezone
23
23
  from pathlib import Path
24
24
  from typing import List, Optional, Tuple
25
25
 
26
-
27
26
  # =============================================================================
28
27
  # Constants
29
28
  # =============================================================================
30
29
 
31
- REVIEW_BRANCH_PREFIX = 'reviews/'
30
+ REVIEW_BRANCH_PREFIX = "reviews/"
32
31
 
33
32
 
34
33
  # =============================================================================
35
34
  # Data Classes (REQ-tv-d00013)
36
35
  # =============================================================================
37
36
 
37
+
38
38
  @dataclass
39
39
  class BranchInfo:
40
40
  """
@@ -42,13 +42,14 @@ class BranchInfo:
42
42
 
43
43
  REQ-tv-d00013: Branch info data class for listing and cleanup operations.
44
44
  """
45
- name: str # Full branch name: reviews/{pkg}/{user}
46
- package_id: str # Package identifier
47
- username: str # User who owns the branch
48
- last_commit_date: datetime # Date of last commit on branch
49
- is_current: bool # True if this is the current branch
50
- has_remote: bool # True if remote tracking branch exists
51
- is_merged: bool # True if merged into main
45
+
46
+ name: str # Full branch name: reviews/{pkg}/{user}
47
+ package_id: str # Package identifier
48
+ username: str # User who owns the branch
49
+ last_commit_date: datetime # Date of last commit on branch
50
+ is_current: bool # True if this is the current branch
51
+ has_remote: bool # True if remote tracking branch exists
52
+ is_merged: bool # True if merged into main
52
53
 
53
54
  @property
54
55
  def age_days(self) -> int:
@@ -67,6 +68,7 @@ class BranchInfo:
67
68
  # Branch Naming (REQ-tv-d00013-A, B)
68
69
  # =============================================================================
69
70
 
71
+
70
72
  def get_review_branch_name(package_id: str, user: str) -> str:
71
73
  """
72
74
  Generate a review branch name from package and user.
@@ -101,11 +103,11 @@ def _sanitize_branch_name(name: str) -> str:
101
103
  Replaces spaces with hyphens and removes invalid characters.
102
104
  """
103
105
  # Replace spaces with hyphens
104
- name = name.replace(' ', '-')
106
+ name = name.replace(" ", "-")
105
107
  # Remove invalid characters (keep alphanumeric, hyphen, underscore)
106
- name = re.sub(r'[^a-zA-Z0-9_-]', '', name)
108
+ name = re.sub(r"[^a-zA-Z0-9_-]", "", name)
107
109
  # Remove leading/trailing hyphens
108
- name = name.strip('-')
110
+ name = name.strip("-")
109
111
  # Convert to lowercase
110
112
  return name.lower()
111
113
 
@@ -114,6 +116,7 @@ def _sanitize_branch_name(name: str) -> str:
114
116
  # Branch Parsing (REQ-tv-d00013-C, D)
115
117
  # =============================================================================
116
118
 
119
+
117
120
  def parse_review_branch_name(branch_name: str) -> Optional[Tuple[str, str]]:
118
121
  """
119
122
  Parse a review branch name into (package_id, user).
@@ -139,8 +142,8 @@ def parse_review_branch_name(branch_name: str) -> Optional[Tuple[str, str]]:
139
142
  return None
140
143
 
141
144
  # Remove prefix
142
- remainder = branch_name[len(REVIEW_BRANCH_PREFIX):]
143
- parts = remainder.split('/', 1)
145
+ remainder = branch_name[len(REVIEW_BRANCH_PREFIX) :]
146
+ parts = remainder.split("/", 1)
144
147
 
145
148
  if len(parts) != 2 or not parts[0] or not parts[1]:
146
149
  return None
@@ -175,8 +178,8 @@ def is_review_branch(branch_name: str) -> bool:
175
178
  if not branch_name.startswith(REVIEW_BRANCH_PREFIX):
176
179
  return False
177
180
 
178
- remainder = branch_name[len(REVIEW_BRANCH_PREFIX):]
179
- parts = remainder.split('/', 1)
181
+ remainder = branch_name[len(REVIEW_BRANCH_PREFIX) :]
182
+ parts = remainder.split("/", 1)
180
183
 
181
184
  # Must have both package and user
182
185
  return len(parts) == 2 and bool(parts[0]) and bool(parts[1])
@@ -186,8 +189,8 @@ def is_review_branch(branch_name: str) -> bool:
186
189
  # Git Utilities
187
190
  # =============================================================================
188
191
 
189
- def _run_git(repo_root: Path, args: List[str],
190
- check: bool = False) -> subprocess.CompletedProcess:
192
+
193
+ def _run_git(repo_root: Path, args: List[str], check: bool = False) -> subprocess.CompletedProcess:
191
194
  """
192
195
  Run a git command in the repository.
193
196
 
@@ -201,19 +204,12 @@ def _run_git(repo_root: Path, args: List[str],
201
204
  """
202
205
  try:
203
206
  return subprocess.run(
204
- ['git'] + args,
205
- cwd=repo_root,
206
- capture_output=True,
207
- text=True,
208
- check=check
207
+ ["git"] + args, cwd=repo_root, capture_output=True, text=True, check=check
209
208
  )
210
209
  except (subprocess.CalledProcessError, FileNotFoundError, OSError):
211
210
  # Return a fake failed result
212
211
  return subprocess.CompletedProcess(
213
- args=['git'] + args,
214
- returncode=1,
215
- stdout='',
216
- stderr='Error running git'
212
+ args=["git"] + args, returncode=1, stdout="", stderr="Error running git"
217
213
  )
218
214
 
219
215
 
@@ -227,7 +223,7 @@ def get_current_branch(repo_root: Path) -> Optional[str]:
227
223
  Returns:
228
224
  Branch name or None if not in a git repo
229
225
  """
230
- result = _run_git(repo_root, ['rev-parse', '--abbrev-ref', 'HEAD'])
226
+ result = _run_git(repo_root, ["rev-parse", "--abbrev-ref", "HEAD"])
231
227
  if result.returncode != 0:
232
228
  return None
233
229
  return result.stdout.strip()
@@ -243,17 +239,18 @@ def get_remote_name(repo_root: Path) -> Optional[str]:
243
239
  Returns:
244
240
  Remote name or None if no remotes configured
245
241
  """
246
- result = _run_git(repo_root, ['remote'])
242
+ result = _run_git(repo_root, ["remote"])
247
243
  if result.returncode != 0 or not result.stdout.strip():
248
244
  return None
249
245
  # Return first remote
250
- return result.stdout.strip().split('\n')[0]
246
+ return result.stdout.strip().split("\n")[0]
251
247
 
252
248
 
253
249
  # =============================================================================
254
250
  # Git Audit Trail (REQ-d00098)
255
251
  # =============================================================================
256
252
 
253
+
257
254
  def get_head_commit_hash(repo_root: Path) -> Optional[str]:
258
255
  """
259
256
  Get the current HEAD commit hash (full 40 characters).
@@ -267,7 +264,7 @@ def get_head_commit_hash(repo_root: Path) -> Optional[str]:
267
264
  Returns:
268
265
  Full commit hash (40 chars) or None if not in a git repo
269
266
  """
270
- result = _run_git(repo_root, ['rev-parse', 'HEAD'])
267
+ result = _run_git(repo_root, ["rev-parse", "HEAD"])
271
268
  if result.returncode != 0:
272
269
  return None
273
270
  return result.stdout.strip()
@@ -284,7 +281,7 @@ def get_short_commit_hash(repo_root: Path, length: int = 7) -> Optional[str]:
284
281
  Returns:
285
282
  Short commit hash or None if not in a git repo
286
283
  """
287
- result = _run_git(repo_root, ['rev-parse', f'--short={length}', 'HEAD'])
284
+ result = _run_git(repo_root, ["rev-parse", f"--short={length}", "HEAD"])
288
285
  if result.returncode != 0:
289
286
  return None
290
287
  return result.stdout.strip()
@@ -303,8 +300,8 @@ def get_git_context(repo_root: Path) -> dict:
303
300
  Dictionary with 'branchName' and 'commitHash' keys (values may be None)
304
301
  """
305
302
  return {
306
- 'branchName': get_current_branch(repo_root),
307
- 'commitHash': get_head_commit_hash(repo_root)
303
+ "branchName": get_current_branch(repo_root),
304
+ "commitHash": get_head_commit_hash(repo_root),
308
305
  }
309
306
 
310
307
 
@@ -324,8 +321,8 @@ def commit_exists(repo_root: Path, commit_hash: str) -> bool:
324
321
  Returns:
325
322
  True if commit exists, False otherwise
326
323
  """
327
- result = _run_git(repo_root, ['cat-file', '-t', commit_hash])
328
- return result.returncode == 0 and result.stdout.strip() == 'commit'
324
+ result = _run_git(repo_root, ["cat-file", "-t", commit_hash])
325
+ return result.returncode == 0 and result.stdout.strip() == "commit"
329
326
 
330
327
 
331
328
  def branch_exists(repo_root: Path, branch_name: str) -> bool:
@@ -339,12 +336,11 @@ def branch_exists(repo_root: Path, branch_name: str) -> bool:
339
336
  Returns:
340
337
  True if branch exists locally
341
338
  """
342
- result = _run_git(repo_root, ['rev-parse', '--verify', f'refs/heads/{branch_name}'])
339
+ result = _run_git(repo_root, ["rev-parse", "--verify", f"refs/heads/{branch_name}"])
343
340
  return result.returncode == 0
344
341
 
345
342
 
346
- def remote_branch_exists(repo_root: Path, branch_name: str,
347
- remote: str = 'origin') -> bool:
343
+ def remote_branch_exists(repo_root: Path, branch_name: str, remote: str = "origin") -> bool:
348
344
  """
349
345
  Check if a remote branch exists.
350
346
 
@@ -356,7 +352,7 @@ def remote_branch_exists(repo_root: Path, branch_name: str,
356
352
  Returns:
357
353
  True if branch exists on remote
358
354
  """
359
- result = _run_git(repo_root, ['rev-parse', '--verify', f'refs/remotes/{remote}/{branch_name}'])
355
+ result = _run_git(repo_root, ["rev-parse", "--verify", f"refs/remotes/{remote}/{branch_name}"])
360
356
  return result.returncode == 0
361
357
 
362
358
 
@@ -364,6 +360,7 @@ def remote_branch_exists(repo_root: Path, branch_name: str,
364
360
  # Branch Metadata (REQ-tv-d00013 Service Layer)
365
361
  # =============================================================================
366
362
 
363
+
367
364
  def get_branch_last_commit_date(repo_root: Path, branch_name: str) -> Optional[datetime]:
368
365
  """
369
366
  Get the date of the last commit on a branch.
@@ -382,9 +379,7 @@ def get_branch_last_commit_date(repo_root: Path, branch_name: str) -> Optional[d
382
379
  datetime.datetime(2025, 1, 8, 12, 30, 45, tzinfo=timezone.utc)
383
380
  """
384
381
  # Use git log to get the commit date in ISO format
385
- result = _run_git(repo_root, [
386
- 'log', '-1', '--format=%cI', branch_name
387
- ])
382
+ result = _run_git(repo_root, ["log", "-1", "--format=%cI", branch_name])
388
383
  if result.returncode != 0 or not result.stdout.strip():
389
384
  return None
390
385
 
@@ -400,8 +395,7 @@ def get_branch_last_commit_date(repo_root: Path, branch_name: str) -> Optional[d
400
395
  return None
401
396
 
402
397
 
403
- def is_branch_merged(repo_root: Path, branch_name: str,
404
- target_branch: str = 'main') -> bool:
398
+ def is_branch_merged(repo_root: Path, branch_name: str, target_branch: str = "main") -> bool:
405
399
  """
406
400
  Check if a branch has been merged into the target branch.
407
401
 
@@ -422,22 +416,21 @@ def is_branch_merged(repo_root: Path, branch_name: str,
422
416
  False # bob's branch not merged to develop
423
417
  """
424
418
  # git branch --merged <target> lists branches merged into target
425
- result = _run_git(repo_root, ['branch', '--merged', target_branch])
419
+ result = _run_git(repo_root, ["branch", "--merged", target_branch])
426
420
  if result.returncode != 0:
427
421
  return False
428
422
 
429
423
  # Parse branch list and check if our branch is in it
430
424
  merged_branches = []
431
- for line in result.stdout.strip().split('\n'):
432
- branch = line.strip().lstrip('* ')
425
+ for line in result.stdout.strip().split("\n"):
426
+ branch = line.strip().lstrip("* ")
433
427
  if branch:
434
428
  merged_branches.append(branch)
435
429
 
436
430
  return branch_name in merged_branches
437
431
 
438
432
 
439
- def has_unpushed_commits(repo_root: Path, branch_name: str,
440
- remote: str = 'origin') -> bool:
433
+ def has_unpushed_commits(repo_root: Path, branch_name: str, remote: str = "origin") -> bool:
441
434
  """
442
435
  Check if a branch has commits not pushed to remote.
443
436
 
@@ -460,10 +453,7 @@ def has_unpushed_commits(repo_root: Path, branch_name: str,
460
453
  return True # Remote exists but branch not pushed
461
454
 
462
455
  # Compare local and remote
463
- result = _run_git(repo_root, [
464
- 'rev-list', '--count',
465
- f'{remote}/{branch_name}..{branch_name}'
466
- ])
456
+ result = _run_git(repo_root, ["rev-list", "--count", f"{remote}/{branch_name}..{branch_name}"])
467
457
  if result.returncode != 0:
468
458
  return True # Assume unpushed if we can't check
469
459
 
@@ -513,7 +503,7 @@ def get_branch_info(repo_root: Path, branch_name: str) -> Optional[BranchInfo]:
513
503
 
514
504
  # Check if current branch
515
505
  current = get_current_branch(repo_root)
516
- is_current = (current == branch_name)
506
+ is_current = current == branch_name
517
507
 
518
508
  # Check remote existence
519
509
  has_remote = remote_branch_exists(repo_root, branch_name)
@@ -528,7 +518,7 @@ def get_branch_info(repo_root: Path, branch_name: str) -> Optional[BranchInfo]:
528
518
  last_commit_date=last_commit,
529
519
  is_current=is_current,
530
520
  has_remote=has_remote,
531
- is_merged=is_merged
521
+ is_merged=is_merged,
532
522
  )
533
523
 
534
524
 
@@ -536,6 +526,7 @@ def get_branch_info(repo_root: Path, branch_name: str) -> Optional[BranchInfo]:
536
526
  # Package Context (REQ-tv-d00013-F)
537
527
  # =============================================================================
538
528
 
529
+
539
530
  def get_current_package_context(repo_root: Path) -> Tuple[Optional[str], Optional[str]]:
540
531
  """
541
532
  Get current (package_id, user) from branch name.
@@ -569,6 +560,7 @@ def get_current_package_context(repo_root: Path) -> Tuple[Optional[str], Optiona
569
560
  # Branch Discovery (REQ-tv-d00013-E)
570
561
  # =============================================================================
571
562
 
563
+
572
564
  def list_package_branches(repo_root: Path, package_id: str) -> List[str]:
573
565
  """
574
566
  List all local review branches for a specific package.
@@ -603,21 +595,20 @@ def _list_branches_by_pattern(repo_root: Path, pattern: str) -> List[str]:
603
595
  Returns:
604
596
  List of matching branch names
605
597
  """
606
- result = _run_git(repo_root, ['branch', '--list', pattern])
598
+ result = _run_git(repo_root, ["branch", "--list", pattern])
607
599
  if result.returncode != 0:
608
600
  return []
609
601
 
610
602
  branches = []
611
- for line in result.stdout.strip().split('\n'):
612
- branch = line.strip().lstrip('* ')
603
+ for line in result.stdout.strip().split("\n"):
604
+ branch = line.strip().lstrip("* ")
613
605
  if branch and is_review_branch(branch):
614
606
  branches.append(branch)
615
607
 
616
608
  return branches
617
609
 
618
610
 
619
- def list_local_review_branches(repo_root: Path,
620
- user: Optional[str] = None) -> List[str]:
611
+ def list_local_review_branches(repo_root: Path, user: Optional[str] = None) -> List[str]:
621
612
  """
622
613
  List all local review branches.
623
614
 
@@ -628,14 +619,14 @@ def list_local_review_branches(repo_root: Path,
628
619
  Returns:
629
620
  List of branch names
630
621
  """
631
- result = _run_git(repo_root, ['branch', '--list', 'reviews/*'])
622
+ result = _run_git(repo_root, ["branch", "--list", "reviews/*"])
632
623
  if result.returncode != 0:
633
624
  return []
634
625
 
635
626
  branches = []
636
- for line in result.stdout.strip().split('\n'):
627
+ for line in result.stdout.strip().split("\n"):
637
628
  # Remove leading * and whitespace
638
- branch = line.strip().lstrip('* ')
629
+ branch = line.strip().lstrip("* ")
639
630
  if branch and is_review_branch(branch):
640
631
  if user is None:
641
632
  branches.append(branch)
@@ -652,6 +643,7 @@ def list_local_review_branches(repo_root: Path,
652
643
  # Branch Operations
653
644
  # =============================================================================
654
645
 
646
+
655
647
  def create_review_branch(repo_root: Path, package_id: str, user: str) -> str:
656
648
  """
657
649
  Create a new review branch.
@@ -673,7 +665,7 @@ def create_review_branch(repo_root: Path, package_id: str, user: str) -> str:
673
665
  if branch_exists(repo_root, branch_name):
674
666
  raise ValueError(f"Branch already exists: {branch_name}")
675
667
 
676
- result = _run_git(repo_root, ['branch', branch_name])
668
+ result = _run_git(repo_root, ["branch", branch_name])
677
669
  if result.returncode != 0:
678
670
  raise RuntimeError(f"Failed to create branch: {result.stderr}")
679
671
 
@@ -697,7 +689,7 @@ def checkout_review_branch(repo_root: Path, package_id: str, user: str) -> bool:
697
689
  if not branch_exists(repo_root, branch_name):
698
690
  return False
699
691
 
700
- result = _run_git(repo_root, ['checkout', branch_name])
692
+ result = _run_git(repo_root, ["checkout", branch_name])
701
693
  return result.returncode == 0
702
694
 
703
695
 
@@ -705,6 +697,7 @@ def checkout_review_branch(repo_root: Path, package_id: str, user: str) -> bool:
705
697
  # Change Detection
706
698
  # =============================================================================
707
699
 
700
+
708
701
  def has_uncommitted_changes(repo_root: Path) -> bool:
709
702
  """
710
703
  Check if there are uncommitted changes.
@@ -717,7 +710,7 @@ def has_uncommitted_changes(repo_root: Path) -> bool:
717
710
  Returns:
718
711
  True if there are uncommitted changes (staged or unstaged)
719
712
  """
720
- result = _run_git(repo_root, ['status', '--porcelain'])
713
+ result = _run_git(repo_root, ["status", "--porcelain"])
721
714
  return bool(result.stdout.strip())
722
715
 
723
716
 
@@ -731,11 +724,11 @@ def has_reviews_changes(repo_root: Path) -> bool:
731
724
  Returns:
732
725
  True if .reviews/ has uncommitted changes
733
726
  """
734
- reviews_dir = repo_root / '.reviews'
727
+ reviews_dir = repo_root / ".reviews"
735
728
  if not reviews_dir.exists():
736
729
  return False
737
730
 
738
- result = _run_git(repo_root, ['status', '--porcelain', '.reviews/'])
731
+ result = _run_git(repo_root, ["status", "--porcelain", ".reviews/"])
739
732
  return bool(result.stdout.strip())
740
733
 
741
734
 
@@ -752,19 +745,19 @@ def has_conflicts(repo_root: Path) -> bool:
752
745
  True if there are unresolved merge conflicts
753
746
  """
754
747
  # Check for merge in progress
755
- git_dir = repo_root / '.git'
756
- if (git_dir / 'MERGE_HEAD').exists():
748
+ git_dir = repo_root / ".git"
749
+ if (git_dir / "MERGE_HEAD").exists():
757
750
  # Merge in progress, check for conflict markers
758
- result = _run_git(repo_root, ['diff', '--check'])
751
+ result = _run_git(repo_root, ["diff", "--check"])
759
752
  return result.returncode != 0
760
753
 
761
754
  # Check for conflict markers in staged files
762
- result = _run_git(repo_root, ['diff', '--cached', '--check'])
755
+ result = _run_git(repo_root, ["diff", "--cached", "--check"])
763
756
  if result.returncode != 0:
764
757
  return True
765
758
 
766
759
  # Also check working tree
767
- result = _run_git(repo_root, ['diff', '--check'])
760
+ result = _run_git(repo_root, ["diff", "--check"])
768
761
  return result.returncode != 0
769
762
 
770
763
 
@@ -772,7 +765,8 @@ def has_conflicts(repo_root: Path) -> bool:
772
765
  # Commit and Push Operations (REQ-tv-d00013-G)
773
766
  # =============================================================================
774
767
 
775
- def commit_reviews(repo_root: Path, message: str, user: str = 'system') -> bool:
768
+
769
+ def commit_reviews(repo_root: Path, message: str, user: str = "system") -> bool:
776
770
  """
777
771
  Commit changes to .reviews/ directory.
778
772
 
@@ -789,21 +783,18 @@ def commit_reviews(repo_root: Path, message: str, user: str = 'system') -> bool:
789
783
  return True # No changes, success
790
784
 
791
785
  # Stage .reviews/ changes
792
- result = _run_git(repo_root, ['add', '.reviews/'])
786
+ result = _run_git(repo_root, ["add", ".reviews/"])
793
787
  if result.returncode != 0:
794
788
  return False
795
789
 
796
790
  # Commit with message
797
791
  full_message = f"[review] {message}\n\nBy: {user}"
798
- result = _run_git(repo_root, ['commit', '-m', full_message])
792
+ result = _run_git(repo_root, ["commit", "-m", full_message])
799
793
  return result.returncode == 0
800
794
 
801
795
 
802
796
  def commit_and_push_reviews(
803
- repo_root: Path,
804
- message: str,
805
- user: str = 'system',
806
- remote: str = 'origin'
797
+ repo_root: Path, message: str, user: str = "system", remote: str = "origin"
807
798
  ) -> Tuple[bool, str]:
808
799
  """
809
800
  Commit changes to .reviews/ and push to remote.
@@ -822,42 +813,42 @@ def commit_and_push_reviews(
822
813
  """
823
814
  # Check if there are changes
824
815
  if not has_reviews_changes(repo_root):
825
- return (True, 'No changes to commit')
816
+ return (True, "No changes to commit")
826
817
 
827
818
  # Stage .reviews/ changes
828
- result = _run_git(repo_root, ['add', '.reviews/'])
819
+ result = _run_git(repo_root, ["add", ".reviews/"])
829
820
  if result.returncode != 0:
830
- return (False, f'Failed to stage changes: {result.stderr}')
821
+ return (False, f"Failed to stage changes: {result.stderr}")
831
822
 
832
823
  # Commit with message
833
824
  full_message = f"[review] {message}\n\nBy: {user}"
834
- result = _run_git(repo_root, ['commit', '-m', full_message])
825
+ result = _run_git(repo_root, ["commit", "-m", full_message])
835
826
  if result.returncode != 0:
836
- return (False, f'Failed to commit: {result.stderr}')
827
+ return (False, f"Failed to commit: {result.stderr}")
837
828
 
838
829
  # Check if remote exists
839
830
  if get_remote_name(repo_root) is None:
840
- return (True, 'Committed locally (no remote configured)')
831
+ return (True, "Committed locally (no remote configured)")
841
832
 
842
833
  # Push to remote
843
834
  current_branch = get_current_branch(repo_root)
844
835
  if current_branch:
845
- push_result = _run_git(repo_root, ['push', remote, current_branch])
836
+ push_result = _run_git(repo_root, ["push", remote, current_branch])
846
837
  if push_result.returncode == 0:
847
- return (True, 'Committed and pushed successfully')
838
+ return (True, "Committed and pushed successfully")
848
839
  else:
849
840
  # Commit succeeded but push failed - still return success for commit
850
- return (True, f'Committed locally (push failed: {push_result.stderr})')
841
+ return (True, f"Committed locally (push failed: {push_result.stderr})")
851
842
 
852
- return (True, 'Committed locally')
843
+ return (True, "Committed locally")
853
844
 
854
845
 
855
846
  # =============================================================================
856
847
  # Fetch Operations (REQ-tv-d00013-I)
857
848
  # =============================================================================
858
849
 
859
- def fetch_package_branches(repo_root: Path, package_id: str,
860
- remote: str = 'origin') -> List[str]:
850
+
851
+ def fetch_package_branches(repo_root: Path, package_id: str, remote: str = "origin") -> List[str]:
861
852
  """
862
853
  Fetch all remote branches for a package.
863
854
 
@@ -877,17 +868,23 @@ def fetch_package_branches(repo_root: Path, package_id: str,
877
868
  return []
878
869
 
879
870
  sanitized_package = _sanitize_branch_name(package_id)
880
- refspec = f'refs/heads/{REVIEW_BRANCH_PREFIX}{sanitized_package}/*:refs/remotes/{remote}/{REVIEW_BRANCH_PREFIX}{sanitized_package}/*'
871
+ refspec = (
872
+ f"refs/heads/{REVIEW_BRANCH_PREFIX}{sanitized_package}/*:"
873
+ f"refs/remotes/{remote}/{REVIEW_BRANCH_PREFIX}{sanitized_package}/*"
874
+ )
881
875
 
882
876
  # Fetch the specific package branches
883
- result = _run_git(repo_root, ['fetch', remote, refspec])
877
+ _run_git(repo_root, ["fetch", remote, refspec])
884
878
 
885
879
  # Even if fetch fails (e.g., no matching refs), list what we have
886
880
  branches = []
887
- list_result = _run_git(repo_root, ['branch', '-r', '--list', f'{remote}/{REVIEW_BRANCH_PREFIX}{sanitized_package}/*'])
881
+ list_result = _run_git(
882
+ repo_root,
883
+ ["branch", "-r", "--list", f"{remote}/{REVIEW_BRANCH_PREFIX}{sanitized_package}/*"],
884
+ )
888
885
 
889
886
  if list_result.returncode == 0:
890
- for line in list_result.stdout.strip().split('\n'):
887
+ for line in list_result.stdout.strip().split("\n"):
891
888
  branch = line.strip()
892
889
  if branch:
893
890
  branches.append(branch)
@@ -895,7 +892,7 @@ def fetch_package_branches(repo_root: Path, package_id: str,
895
892
  return branches
896
893
 
897
894
 
898
- def fetch_review_branches(repo_root: Path, remote: str = 'origin') -> bool:
895
+ def fetch_review_branches(repo_root: Path, remote: str = "origin") -> bool:
899
896
  """
900
897
  Fetch all review branches from remote.
901
898
 
@@ -909,7 +906,7 @@ def fetch_review_branches(repo_root: Path, remote: str = 'origin') -> bool:
909
906
  if get_remote_name(repo_root) is None:
910
907
  return False
911
908
 
912
- result = _run_git(repo_root, ['fetch', remote, '--prune'])
909
+ result = _run_git(repo_root, ["fetch", remote, "--prune"])
913
910
  return result.returncode == 0
914
911
 
915
912
 
@@ -917,6 +914,7 @@ def fetch_review_branches(repo_root: Path, remote: str = 'origin') -> bool:
917
914
  # Branch Listing and Cleanup (REQ-tv-d00013 CLI Service Layer)
918
915
  # =============================================================================
919
916
 
917
+
920
918
  @dataclass
921
919
  class CleanupResult:
922
920
  """
@@ -924,18 +922,17 @@ class CleanupResult:
924
922
 
925
923
  REQ-tv-d00013: Cleanup result for CLI feedback.
926
924
  """
927
- deleted_local: List[str] # Branches deleted locally
928
- deleted_remote: List[str] # Branches deleted from remote
929
- skipped_current: List[str] # Skipped because current branch
930
- skipped_unpushed: List[str] # Skipped because has unpushed commits
931
- skipped_unmerged: List[str] # Skipped because not merged
932
- errors: List[Tuple[str, str]] # (branch, error_message) pairs
925
+
926
+ deleted_local: List[str] # Branches deleted locally
927
+ deleted_remote: List[str] # Branches deleted from remote
928
+ skipped_current: List[str] # Skipped because current branch
929
+ skipped_unpushed: List[str] # Skipped because has unpushed commits
930
+ skipped_unmerged: List[str] # Skipped because not merged
931
+ errors: List[Tuple[str, str]] # (branch, error_message) pairs
933
932
 
934
933
 
935
934
  def list_review_branches_with_info(
936
- repo_root: Path,
937
- package_id: Optional[str] = None,
938
- user: Optional[str] = None
935
+ repo_root: Path, package_id: Optional[str] = None, user: Optional[str] = None
939
936
  ) -> List[BranchInfo]:
940
937
  """
941
938
  List all review branches with their metadata.
@@ -988,7 +985,7 @@ def delete_review_branch(
988
985
  branch_name: str,
989
986
  delete_remote: bool = False,
990
987
  force: bool = False,
991
- remote: str = 'origin'
988
+ remote: str = "origin",
992
989
  ) -> Tuple[bool, str]:
993
990
  """
994
991
  Delete a review branch with safety checks.
@@ -1037,14 +1034,14 @@ def delete_review_branch(
1037
1034
  return (False, f"Branch is not merged into main: {branch_name}")
1038
1035
 
1039
1036
  # Delete local branch
1040
- delete_flag = '-D' if force else '-d'
1041
- result = _run_git(repo_root, ['branch', delete_flag, branch_name])
1037
+ delete_flag = "-D" if force else "-d"
1038
+ result = _run_git(repo_root, ["branch", delete_flag, branch_name])
1042
1039
  if result.returncode != 0:
1043
1040
  return (False, f"Failed to delete local branch: {result.stderr}")
1044
1041
 
1045
1042
  # Delete remote branch if requested
1046
1043
  if delete_remote and remote_branch_exists(repo_root, branch_name, remote):
1047
- result = _run_git(repo_root, ['push', remote, '--delete', branch_name])
1044
+ result = _run_git(repo_root, ["push", remote, "--delete", branch_name])
1048
1045
  if result.returncode != 0:
1049
1046
  return (True, f"Deleted local branch, but failed to delete remote: {result.stderr}")
1050
1047
  return (True, f"Deleted local and remote branch: {branch_name}")
@@ -1059,7 +1056,7 @@ def cleanup_review_branches(
1059
1056
  only_merged: bool = True,
1060
1057
  delete_remote: bool = False,
1061
1058
  dry_run: bool = False,
1062
- remote: str = 'origin'
1059
+ remote: str = "origin",
1063
1060
  ) -> CleanupResult:
1064
1061
  """
1065
1062
  Delete review branches matching criteria.
@@ -1092,7 +1089,7 @@ def cleanup_review_branches(
1092
1089
  skipped_current=[],
1093
1090
  skipped_unpushed=[],
1094
1091
  skipped_unmerged=[],
1095
- errors=[]
1092
+ errors=[],
1096
1093
  )
1097
1094
 
1098
1095
  # Get branches to consider
@@ -1126,15 +1123,13 @@ def cleanup_review_branches(
1126
1123
  result.deleted_remote.append(branch.name)
1127
1124
  else:
1128
1125
  # Delete local branch
1129
- delete_result = _run_git(repo_root, ['branch', '-d', branch.name])
1126
+ delete_result = _run_git(repo_root, ["branch", "-d", branch.name])
1130
1127
  if delete_result.returncode == 0:
1131
1128
  result.deleted_local.append(branch.name)
1132
1129
 
1133
1130
  # Delete remote if requested
1134
1131
  if delete_remote and branch.has_remote:
1135
- remote_result = _run_git(
1136
- repo_root, ['push', remote, '--delete', branch.name]
1137
- )
1132
+ remote_result = _run_git(repo_root, ["push", remote, "--delete", branch.name])
1138
1133
  if remote_result.returncode == 0:
1139
1134
  result.deleted_remote.append(branch.name)
1140
1135
  else:
@@ -1142,8 +1137,6 @@ def cleanup_review_branches(
1142
1137
  (branch.name, f"Failed to delete remote: {remote_result.stderr}")
1143
1138
  )
1144
1139
  else:
1145
- result.errors.append(
1146
- (branch.name, f"Failed to delete: {delete_result.stderr}")
1147
- )
1140
+ result.errors.append((branch.name, f"Failed to delete: {delete_result.stderr}"))
1148
1141
 
1149
1142
  return result