aline-ai 0.5.3__py3-none-any.whl → 0.5.5__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 (80) hide show
  1. {aline_ai-0.5.3.dist-info → aline_ai-0.5.5.dist-info}/METADATA +1 -1
  2. aline_ai-0.5.5.dist-info/RECORD +93 -0
  3. realign/__init__.py +1 -1
  4. realign/adapters/antigravity.py +28 -20
  5. realign/adapters/base.py +46 -50
  6. realign/adapters/claude.py +14 -14
  7. realign/adapters/codex.py +7 -7
  8. realign/adapters/gemini.py +11 -11
  9. realign/adapters/registry.py +14 -10
  10. realign/claude_detector.py +2 -2
  11. realign/claude_hooks/__init__.py +3 -3
  12. realign/claude_hooks/permission_request_hook.py +35 -0
  13. realign/claude_hooks/permission_request_hook_installer.py +31 -32
  14. realign/claude_hooks/stop_hook.py +4 -1
  15. realign/claude_hooks/stop_hook_installer.py +30 -31
  16. realign/cli.py +24 -0
  17. realign/codex_detector.py +11 -11
  18. realign/commands/add.py +361 -35
  19. realign/commands/config.py +3 -12
  20. realign/commands/context.py +3 -1
  21. realign/commands/export_shares.py +86 -127
  22. realign/commands/import_shares.py +145 -155
  23. realign/commands/init.py +166 -30
  24. realign/commands/restore.py +18 -6
  25. realign/commands/search.py +14 -42
  26. realign/commands/upgrade.py +155 -11
  27. realign/commands/watcher.py +98 -219
  28. realign/commands/worker.py +29 -6
  29. realign/config.py +25 -20
  30. realign/context.py +1 -3
  31. realign/dashboard/app.py +4 -4
  32. realign/dashboard/screens/create_event.py +3 -1
  33. realign/dashboard/screens/event_detail.py +14 -6
  34. realign/dashboard/screens/session_detail.py +3 -1
  35. realign/dashboard/screens/share_import.py +7 -3
  36. realign/dashboard/tmux_manager.py +91 -22
  37. realign/dashboard/widgets/config_panel.py +85 -1
  38. realign/dashboard/widgets/events_table.py +3 -1
  39. realign/dashboard/widgets/header.py +1 -0
  40. realign/dashboard/widgets/search_panel.py +37 -27
  41. realign/dashboard/widgets/sessions_table.py +24 -15
  42. realign/dashboard/widgets/terminal_panel.py +207 -17
  43. realign/dashboard/widgets/watcher_panel.py +6 -2
  44. realign/dashboard/widgets/worker_panel.py +10 -1
  45. realign/db/__init__.py +1 -1
  46. realign/db/base.py +5 -15
  47. realign/db/locks.py +0 -1
  48. realign/db/migration.py +82 -76
  49. realign/db/schema.py +2 -6
  50. realign/db/sqlite_db.py +23 -41
  51. realign/events/__init__.py +0 -1
  52. realign/events/event_summarizer.py +27 -15
  53. realign/events/session_summarizer.py +29 -15
  54. realign/file_lock.py +1 -0
  55. realign/hooks.py +150 -60
  56. realign/logging_config.py +12 -15
  57. realign/mcp_server.py +30 -51
  58. realign/mcp_watcher.py +0 -1
  59. realign/models/event.py +29 -20
  60. realign/prompts/__init__.py +7 -7
  61. realign/prompts/presets.py +15 -11
  62. realign/redactor.py +99 -59
  63. realign/triggers/__init__.py +9 -9
  64. realign/triggers/antigravity_trigger.py +30 -28
  65. realign/triggers/base.py +4 -3
  66. realign/triggers/claude_trigger.py +104 -85
  67. realign/triggers/codex_trigger.py +15 -5
  68. realign/triggers/gemini_trigger.py +57 -47
  69. realign/triggers/next_turn_trigger.py +3 -1
  70. realign/triggers/registry.py +6 -2
  71. realign/triggers/turn_status.py +3 -1
  72. realign/watcher_core.py +306 -131
  73. realign/watcher_daemon.py +8 -8
  74. realign/worker_core.py +3 -1
  75. realign/worker_daemon.py +3 -1
  76. aline_ai-0.5.3.dist-info/RECORD +0 -93
  77. {aline_ai-0.5.3.dist-info → aline_ai-0.5.5.dist-info}/WHEEL +0 -0
  78. {aline_ai-0.5.3.dist-info → aline_ai-0.5.5.dist-info}/entry_points.txt +0 -0
  79. {aline_ai-0.5.3.dist-info → aline_ai-0.5.5.dist-info}/licenses/LICENSE +0 -0
  80. {aline_ai-0.5.3.dist-info → aline_ai-0.5.5.dist-info}/top_level.txt +0 -0
realign/hooks.py CHANGED
@@ -25,12 +25,13 @@ from .llm_client import call_llm, call_llm_json, extract_json
25
25
 
26
26
  try:
27
27
  from .redactor import check_and_redact_session, save_original_session
28
+
28
29
  REDACTOR_AVAILABLE = True
29
30
  except ImportError:
30
31
  REDACTOR_AVAILABLE = False
31
32
 
32
33
  # Initialize logger for hooks
33
- logger = setup_logger('realign.hooks', 'hooks.log')
34
+ logger = setup_logger("realign.hooks", "hooks.log")
34
35
 
35
36
  DEFAULT_METADATA_PROMPT_TEXT = """You are a metadata classifier for AI-assisted git commits.
36
37
  Determine two fields for the current turn:
@@ -61,7 +62,9 @@ def get_last_llm_error() -> Optional[str]:
61
62
  return _last_llm_error
62
63
 
63
64
 
64
- def _emit_llm_debug(callback: Optional[Callable[[Dict[str, Any]], None]], payload: Dict[str, Any]) -> None:
65
+ def _emit_llm_debug(
66
+ callback: Optional[Callable[[Dict[str, Any]], None]], payload: Dict[str, Any]
67
+ ) -> None:
65
68
  if not callback:
66
69
  return
67
70
  try:
@@ -133,7 +136,9 @@ def _normalize_if_last_task(raw_value: Any) -> str:
133
136
  return "yes"
134
137
  if lower in ("no", "false", "0", "new", "fresh"):
135
138
  return "no"
136
- if any(keyword in lower for keyword in ["still", "again", "continue", "follow-up", "follow up"]):
139
+ if any(
140
+ keyword in lower for keyword in ["still", "again", "continue", "follow-up", "follow up"]
141
+ ):
137
142
  return "yes"
138
143
  return "no"
139
144
 
@@ -147,9 +152,39 @@ def _normalize_satisfaction(raw_value: Any) -> str:
147
152
  lower = raw_value.lower().strip()
148
153
  if lower in ("good", "fine", "bad"):
149
154
  return lower
150
- positive = ["great", "perfect", "excellent", "works", "thanks", "done", "awesome", "success", "completed"]
151
- negative = ["bad", "fail", "error", "broken", "didn't work", "not working", "no,", "no ", "still broken"]
152
- mixed = ["but", "still", "however", "almost", "not quite", "partial", "some", "kinda", "kind of"]
155
+ positive = [
156
+ "great",
157
+ "perfect",
158
+ "excellent",
159
+ "works",
160
+ "thanks",
161
+ "done",
162
+ "awesome",
163
+ "success",
164
+ "completed",
165
+ ]
166
+ negative = [
167
+ "bad",
168
+ "fail",
169
+ "error",
170
+ "broken",
171
+ "didn't work",
172
+ "not working",
173
+ "no,",
174
+ "no ",
175
+ "still broken",
176
+ ]
177
+ mixed = [
178
+ "but",
179
+ "still",
180
+ "however",
181
+ "almost",
182
+ "not quite",
183
+ "partial",
184
+ "some",
185
+ "kinda",
186
+ "kind of",
187
+ ]
153
188
  if any(keyword in lower for keyword in positive):
154
189
  return "good"
155
190
  if any(keyword in lower for keyword in negative):
@@ -198,10 +233,17 @@ def _classify_task_metadata(
198
233
  logger.debug(f"Loaded user-customized metadata prompt from {user_prompt_path}")
199
234
  return text
200
235
  except Exception:
201
- logger.debug("Failed to load user-customized metadata prompt, falling back", exc_info=True)
236
+ logger.debug(
237
+ "Failed to load user-customized metadata prompt, falling back", exc_info=True
238
+ )
202
239
 
203
240
  # Fall back to built-in prompt (tools/commit_message_prompts/metadata_default.md)
204
- candidate = Path(__file__).resolve().parents[2] / "tools" / "commit_message_prompts" / "metadata_default.md"
241
+ candidate = (
242
+ Path(__file__).resolve().parents[2]
243
+ / "tools"
244
+ / "commit_message_prompts"
245
+ / "metadata_default.md"
246
+ )
205
247
  try:
206
248
  text = candidate.read_text(encoding="utf-8").strip()
207
249
  if text:
@@ -270,6 +312,7 @@ def _classify_task_metadata(
270
312
  # Message Cleaning Utilities
271
313
  # ============================================================================
272
314
 
315
+
273
316
  def clean_user_message(text: str) -> str:
274
317
  """
275
318
  Clean user message by removing IDE context tags and other system noise.
@@ -298,16 +341,18 @@ def clean_user_message(text: str) -> str:
298
341
  return ""
299
342
 
300
343
  # Remove IDE opened file tags
301
- text = re.sub(r'<ide_opened_file>.*?</ide_opened_file>\s*', '', text, flags=re.DOTALL)
344
+ text = re.sub(r"<ide_opened_file>.*?</ide_opened_file>\s*", "", text, flags=re.DOTALL)
302
345
 
303
346
  # Remove IDE selection tags
304
- text = re.sub(r'<ide_selection>.*?</ide_selection>\s*', '', text, flags=re.DOTALL)
347
+ text = re.sub(r"<ide_selection>.*?</ide_selection>\s*", "", text, flags=re.DOTALL)
305
348
 
306
349
  # Remove other common system tags if needed
307
350
  # text = re.sub(r'<system_context>.*?</system_context>\s*', '', text, flags=re.DOTALL)
308
351
 
309
352
  # Clean up extra whitespace
310
- text = re.sub(r'\n\s*\n\s*\n+', '\n\n', text) # Replace multiple blank lines with double newline
353
+ text = re.sub(
354
+ r"\n\s*\n\s*\n+", "\n\n", text
355
+ ) # Replace multiple blank lines with double newline
311
356
  text = text.strip()
312
357
 
313
358
  return text
@@ -356,7 +401,9 @@ def get_new_content_from_git_diff(repo_root: Path, session_relpath: str) -> str:
356
401
  new_lines.append(line[1:])
357
402
 
358
403
  content = "\n".join(new_lines)
359
- logger.info(f"Extracted {len(new_lines)} new lines ({len(content)} bytes) from {session_relpath}")
404
+ logger.info(
405
+ f"Extracted {len(new_lines)} new lines ({len(content)} bytes) from {session_relpath}"
406
+ )
360
407
  return content
361
408
 
362
409
  except subprocess.TimeoutExpired:
@@ -377,6 +424,7 @@ def get_claude_project_name(project_path: Path) -> str:
377
424
  For example: /Users/alice/Projects/MyApp -> -Users-alice-Projects-MyApp
378
425
  """
379
426
  from .claude_detector import get_claude_project_name as _get_name
427
+
380
428
  return _get_name(project_path)
381
429
 
382
430
 
@@ -413,7 +461,12 @@ def find_codex_latest_session(project_path: Path, days_back: int = 7) -> Optiona
413
461
  # Search through recent days
414
462
  for days_ago in range(days_back + 1):
415
463
  target_date = datetime.now() - timedelta(days=days_ago)
416
- date_path = codex_sessions_base / str(target_date.year) / f"{target_date.month:02d}" / f"{target_date.day:02d}"
464
+ date_path = (
465
+ codex_sessions_base
466
+ / str(target_date.year)
467
+ / f"{target_date.month:02d}"
468
+ / f"{target_date.day:02d}"
469
+ )
417
470
 
418
471
  if not date_path.exists():
419
472
  continue
@@ -422,12 +475,12 @@ def find_codex_latest_session(project_path: Path, days_back: int = 7) -> Optiona
422
475
  for session_file in date_path.glob("rollout-*.jsonl"):
423
476
  try:
424
477
  # Read first line to get session metadata
425
- with open(session_file, 'r', encoding='utf-8') as f:
478
+ with open(session_file, "r", encoding="utf-8") as f:
426
479
  first_line = f.readline()
427
480
  if first_line:
428
481
  data = json.loads(first_line)
429
- if data.get('type') == 'session_meta':
430
- session_cwd = data.get('payload', {}).get('cwd', '')
482
+ if data.get("type") == "session_meta":
483
+ session_cwd = data.get("payload", {}).get("cwd", "")
431
484
  # Match the project path
432
485
  if session_cwd == abs_project_path:
433
486
  matching_sessions.append(session_file)
@@ -440,7 +493,9 @@ def find_codex_latest_session(project_path: Path, days_back: int = 7) -> Optiona
440
493
  matching_sessions.sort(key=lambda p: p.stat().st_mtime, reverse=True)
441
494
 
442
495
  if matching_sessions:
443
- logger.info(f"Found {len(matching_sessions)} Codex session(s), using latest: {matching_sessions[0]}")
496
+ logger.info(
497
+ f"Found {len(matching_sessions)} Codex session(s), using latest: {matching_sessions[0]}"
498
+ )
444
499
  else:
445
500
  logger.debug("No matching Codex sessions found")
446
501
 
@@ -450,7 +505,7 @@ def find_codex_latest_session(project_path: Path, days_back: int = 7) -> Optiona
450
505
  def find_all_claude_sessions() -> List[Path]:
451
506
  """
452
507
  Find all active Claude Code sessions from ALL projects.
453
-
508
+
454
509
  (Legacy wrapper for ClaudeAdapter)
455
510
  """
456
511
  adapter = get_adapter_registry().get_adapter("claude")
@@ -475,7 +530,7 @@ def find_all_codex_sessions(days_back: int = 1) -> List[Path]:
475
530
  def find_all_antigravity_sessions() -> List[Path]:
476
531
  """
477
532
  Find all active Antigravity IDE sessions.
478
-
533
+
479
534
  (Legacy wrapper for AntigravityAdapter)
480
535
  """
481
536
  adapter = get_adapter_registry().get_adapter("antigravity")
@@ -487,7 +542,7 @@ def find_all_antigravity_sessions() -> List[Path]:
487
542
  def find_all_gemini_cli_sessions() -> List[Path]:
488
543
  """
489
544
  Find all active Gemini CLI sessions.
490
-
545
+
491
546
  (Legacy wrapper for GeminiAdapter)
492
547
  """
493
548
  adapter = get_adapter_registry().get_adapter("gemini")
@@ -497,35 +552,40 @@ def find_all_gemini_cli_sessions() -> List[Path]:
497
552
 
498
553
 
499
554
  def find_all_active_sessions(
500
- config: ReAlignConfig,
501
- project_path: Optional[Path] = None
555
+ config: ReAlignConfig, project_path: Optional[Path] = None
502
556
  ) -> List[Path]:
503
557
  """
504
558
  Find all active session files based on enabled auto-detection options.
505
-
559
+
506
560
  Uses current adapter-based architecture.
507
561
  """
508
562
  logger.info("Searching for active AI sessions")
509
- logger.debug(f"Config: auto_detect_codex={config.auto_detect_codex}, auto_detect_claude={config.auto_detect_claude}")
563
+ logger.debug(
564
+ f"Config: auto_detect_codex={config.auto_detect_codex}, auto_detect_claude={config.auto_detect_claude}"
565
+ )
510
566
  logger.debug(f"Project path: {project_path}")
511
567
 
512
568
  sessions = []
513
569
 
514
570
  # 1. Use AdapterRegistry for discovery
515
571
  registry = get_adapter_registry()
516
-
572
+
517
573
  # Map config options to adapter names
518
574
  enabled_adapters = []
519
- if config.auto_detect_claude: enabled_adapters.append("claude")
520
- if config.auto_detect_codex: enabled_adapters.append("codex")
521
- if config.auto_detect_gemini: enabled_adapters.append("gemini")
522
- if config.auto_detect_antigravity: enabled_adapters.append("antigravity")
575
+ if config.auto_detect_claude:
576
+ enabled_adapters.append("claude")
577
+ if config.auto_detect_codex:
578
+ enabled_adapters.append("codex")
579
+ if config.auto_detect_gemini:
580
+ enabled_adapters.append("gemini")
581
+ if config.auto_detect_antigravity:
582
+ enabled_adapters.append("antigravity")
523
583
 
524
584
  for name in enabled_adapters:
525
585
  adapter = registry.get_adapter(name)
526
586
  if not adapter:
527
587
  continue
528
-
588
+
529
589
  try:
530
590
  if project_path:
531
591
  # Single-project mode
@@ -564,7 +624,9 @@ def find_latest_session(history_path: Path, explicit_path: Optional[str] = None)
564
624
  return None
565
625
 
566
626
  # Expand user path
567
- history_path = Path(os.path.expanduser(history_path)) if isinstance(history_path, str) else history_path
627
+ history_path = (
628
+ Path(os.path.expanduser(history_path)) if isinstance(history_path, str) else history_path
629
+ )
568
630
 
569
631
  if not history_path.exists():
570
632
  return None
@@ -634,13 +696,14 @@ def _parse_antigravity_sections(content: str) -> Dict[str, str]:
634
696
 
635
697
  # Split by section markers
636
698
  import re
637
- pattern = r'--- (task\.md|walkthrough\.md|implementation_plan\.md) ---\n?'
699
+
700
+ pattern = r"--- (task\.md|walkthrough\.md|implementation_plan\.md) ---\n?"
638
701
  parts = re.split(pattern, content)
639
702
 
640
703
  # parts will be: ['', 'task.md', '<content>', 'walkthrough.md', '<content>', ...]
641
704
  i = 1
642
705
  while i < len(parts) - 1:
643
- filename = parts[i].replace('.md', '')
706
+ filename = parts[i].replace(".md", "")
644
707
  section_content = parts[i + 1].strip()
645
708
  if section_content:
646
709
  sections[filename] = section_content
@@ -717,7 +780,12 @@ def _generate_antigravity_summary(
717
780
  if _COMMIT_MESSAGE_PROMPT_CACHE is not None:
718
781
  system_prompt_to_use = _COMMIT_MESSAGE_PROMPT_CACHE
719
782
  else:
720
- candidate = Path(__file__).resolve().parents[2] / "tools" / "commit_message_prompts" / "default.md"
783
+ candidate = (
784
+ Path(__file__).resolve().parents[2]
785
+ / "tools"
786
+ / "commit_message_prompts"
787
+ / "default.md"
788
+ )
721
789
  try:
722
790
  text = candidate.read_text(encoding="utf-8").strip()
723
791
  if text:
@@ -871,12 +939,18 @@ def filter_session_content(content: str) -> Tuple[str, str, str]:
871
939
  # Extract code changes from tool results
872
940
  elif item.get("type") == "tool_result":
873
941
  tool_use_result = obj.get("toolUseResult", {})
874
- if "oldString" in tool_use_result and "newString" in tool_use_result:
942
+ if (
943
+ "oldString" in tool_use_result
944
+ and "newString" in tool_use_result
945
+ ):
875
946
  # This is an Edit operation
876
947
  new_string = tool_use_result.get("newString", "")
877
948
  if new_string:
878
949
  code_changes.append(f"Edit: {new_string[:300]}")
879
- elif "content" in tool_use_result and "filePath" in tool_use_result:
950
+ elif (
951
+ "content" in tool_use_result
952
+ and "filePath" in tool_use_result
953
+ ):
880
954
  # This is a Write operation
881
955
  new_content = tool_use_result.get("content", "")
882
956
  if new_content:
@@ -938,7 +1012,10 @@ def filter_session_content(content: str) -> Tuple[str, str, str]:
938
1012
  content_list = payload.get("content", [])
939
1013
  if isinstance(content_list, list):
940
1014
  for content_item in content_list:
941
- if isinstance(content_item, dict) and content_item.get("type") == "input_text":
1015
+ if (
1016
+ isinstance(content_item, dict)
1017
+ and content_item.get("type") == "input_text"
1018
+ ):
942
1019
  text = content_item.get("text", "").strip()
943
1020
  if text:
944
1021
  user_messages.append(text)
@@ -1105,13 +1182,19 @@ def generate_summary_with_llm(
1105
1182
  text = user_prompt_path.read_text(encoding="utf-8").strip()
1106
1183
  if text:
1107
1184
  _COMMIT_MESSAGE_PROMPT_CACHE = text
1108
- logger.debug(f"Loaded user-customized commit message prompt from {user_prompt_path}")
1185
+ logger.debug(
1186
+ f"Loaded user-customized commit message prompt from {user_prompt_path}"
1187
+ )
1109
1188
  return text
1110
1189
  except Exception:
1111
- logger.debug("Failed to load user-customized commit message prompt, falling back", exc_info=True)
1190
+ logger.debug(
1191
+ "Failed to load user-customized commit message prompt, falling back", exc_info=True
1192
+ )
1112
1193
 
1113
1194
  # Fall back to built-in prompt (tools/commit_message_prompts/default.md)
1114
- candidate = Path(__file__).resolve().parents[2] / "tools" / "commit_message_prompts" / "default.md"
1195
+ candidate = (
1196
+ Path(__file__).resolve().parents[2] / "tools" / "commit_message_prompts" / "default.md"
1197
+ )
1115
1198
  try:
1116
1199
  text = candidate.read_text(encoding="utf-8").strip()
1117
1200
  if text:
@@ -1201,6 +1284,7 @@ Respond with JSON only."""
1201
1284
 
1202
1285
  # Try to extract partial information from the broken JSON
1203
1286
  import re
1287
+
1204
1288
  record_match = re.search(r'"(?:record|title)"\s*:\s*"([^"]{10,})"', response_text)
1205
1289
  extracted_content = record_match.group(1)[:80] if record_match else None
1206
1290
 
@@ -1223,7 +1307,11 @@ Respond with JSON only."""
1223
1307
  error_detail += f"\n\nPartial content extracted: {extracted_content}..."
1224
1308
 
1225
1309
  # Add response preview for debugging
1226
- response_preview = response_text[:200].replace('\n', '\\n') if len(response_text) > 200 else response_text.replace('\n', '\\n')
1310
+ response_preview = (
1311
+ response_text[:200].replace("\n", "\\n")
1312
+ if len(response_text) > 200
1313
+ else response_text.replace("\n", "\\n")
1314
+ )
1227
1315
  error_detail += f"\n\nResponse preview: {response_preview}"
1228
1316
  if len(response_text) > 200:
1229
1317
  error_detail += "..."
@@ -1297,7 +1385,7 @@ def extract_codex_rollout_hash(filename: str) -> Optional[str]:
1297
1385
  # Normalize filename (strip extension) and remove prefix
1298
1386
  stem = Path(filename).stem
1299
1387
  if stem.startswith("rollout-"):
1300
- stem = stem[len("rollout-"):]
1388
+ stem = stem[len("rollout-") :]
1301
1389
 
1302
1390
  if not stem:
1303
1391
  return None
@@ -1384,10 +1472,7 @@ def get_username(session_relpath: str = "") -> str:
1384
1472
 
1385
1473
 
1386
1474
  def copy_session_to_repo(
1387
- session_file: Path,
1388
- repo_root: Path,
1389
- user: str,
1390
- config: Optional[ReAlignConfig] = None
1475
+ session_file: Path, repo_root: Path, user: str, config: Optional[ReAlignConfig] = None
1391
1476
  ) -> Tuple[Path, str, bool, int]:
1392
1477
  """
1393
1478
  Copy session file to repository sessions/ directory (in ~/.aline/{project_name}/).
@@ -1400,6 +1485,7 @@ def copy_session_to_repo(
1400
1485
  logger.debug(f"Source: {session_file}, Repo root: {repo_root}, User: {user}")
1401
1486
 
1402
1487
  from realign import get_realign_dir
1488
+
1403
1489
  realign_dir = get_realign_dir(repo_root)
1404
1490
  sessions_dir = realign_dir / "sessions"
1405
1491
  sessions_dir.mkdir(parents=True, exist_ok=True)
@@ -1410,16 +1496,14 @@ def copy_session_to_repo(
1410
1496
  # UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.jsonl
1411
1497
  stem = session_file.stem # filename without extension
1412
1498
  is_uuid_format = (
1413
- '-' in stem and
1414
- '_' not in stem and
1415
- len(stem) == 36 # UUID is 36 chars including hyphens
1499
+ "-" in stem and "_" not in stem and len(stem) == 36 # UUID is 36 chars including hyphens
1416
1500
  )
1417
1501
  # Codex rollout exports always start with rollout-<timestamp>-
1418
1502
  is_codex_rollout = original_filename.startswith("rollout-")
1419
1503
 
1420
1504
  # Read session content first to detect agent type
1421
1505
  try:
1422
- with open(session_file, 'r', encoding='utf-8') as f:
1506
+ with open(session_file, "r", encoding="utf-8") as f:
1423
1507
  content = f.read()
1424
1508
  logger.debug(f"Session file read: {len(content)} bytes")
1425
1509
  except Exception as e:
@@ -1427,7 +1511,7 @@ def copy_session_to_repo(
1427
1511
  print(f"Warning: Could not read session file: {e}", file=sys.stderr)
1428
1512
  # Fallback to simple copy with unknown agent
1429
1513
  if is_uuid_format:
1430
- short_id = stem.split('-')[0]
1514
+ short_id = stem.split("-")[0]
1431
1515
  user_short = user.split()[0].lower() if user else "unknown"
1432
1516
  new_filename = f"{user_short}_unknown_{short_id}.jsonl"
1433
1517
  dest_path = sessions_dir / new_filename
@@ -1462,7 +1546,8 @@ def copy_session_to_repo(
1462
1546
  agent_type = "unknown"
1463
1547
  try:
1464
1548
  import json
1465
- for line in content.split('\n')[:10]: # Check first 10 lines
1549
+
1550
+ for line in content.split("\n")[:10]: # Check first 10 lines
1466
1551
  if not line.strip():
1467
1552
  continue
1468
1553
  try:
@@ -1489,7 +1574,7 @@ def copy_session_to_repo(
1489
1574
  # If it's UUID format, rename to include username and agent type
1490
1575
  if is_uuid_format:
1491
1576
  # Extract short ID from UUID (first 8 chars)
1492
- short_id = stem.split('-')[0]
1577
+ short_id = stem.split("-")[0]
1493
1578
  user_short = user.split()[0].lower() if user else "unknown"
1494
1579
  # Format: username_agent_shortid.jsonl (no timestamp for consistency)
1495
1580
  new_filename = f"{user_short}_{agent_type}_{short_id}.jsonl"
@@ -1526,8 +1611,7 @@ def copy_session_to_repo(
1526
1611
 
1527
1612
  # Perform redaction
1528
1613
  redacted_content, has_secrets, secrets = check_and_redact_session(
1529
- content,
1530
- redact_mode="auto"
1614
+ content, redact_mode="auto"
1531
1615
  )
1532
1616
 
1533
1617
  if has_secrets:
@@ -1544,7 +1628,7 @@ def copy_session_to_repo(
1544
1628
  # Write content to destination (redacted or original)
1545
1629
  temp_path = dest_path.with_suffix(".tmp")
1546
1630
  try:
1547
- with open(temp_path, 'w', encoding='utf-8') as f:
1631
+ with open(temp_path, "w", encoding="utf-8") as f:
1548
1632
  f.write(content)
1549
1633
  temp_path.rename(dest_path)
1550
1634
  try:
@@ -1580,6 +1664,7 @@ def save_session_metadata(repo_root: Path, session_relpath: str, content_size: i
1580
1664
  content_size: Size of session content when processed
1581
1665
  """
1582
1666
  from realign import get_realign_dir
1667
+
1583
1668
  realign_dir = get_realign_dir(repo_root)
1584
1669
  metadata_dir = realign_dir / ".metadata"
1585
1670
  metadata_dir.mkdir(parents=True, exist_ok=True)
@@ -1595,7 +1680,7 @@ def save_session_metadata(repo_root: Path, session_relpath: str, content_size: i
1595
1680
  }
1596
1681
 
1597
1682
  try:
1598
- with open(metadata_file, 'w', encoding='utf-8') as f:
1683
+ with open(metadata_file, "w", encoding="utf-8") as f:
1599
1684
  json.dump(metadata, f)
1600
1685
  logger.debug(f"Saved metadata for {session_relpath}: {content_size} bytes")
1601
1686
  except Exception as e:
@@ -1614,6 +1699,7 @@ def get_session_metadata(repo_root: Path, session_relpath: str) -> Optional[Dict
1614
1699
  Metadata dictionary or None if not found
1615
1700
  """
1616
1701
  from realign import get_realign_dir
1702
+
1617
1703
  realign_dir = get_realign_dir(repo_root)
1618
1704
  metadata_dir = realign_dir / ".metadata"
1619
1705
  session_name = Path(session_relpath).name
@@ -1623,7 +1709,7 @@ def get_session_metadata(repo_root: Path, session_relpath: str) -> Optional[Dict
1623
1709
  return None
1624
1710
 
1625
1711
  try:
1626
- with open(metadata_file, 'r', encoding='utf-8') as f:
1712
+ with open(metadata_file, "r", encoding="utf-8") as f:
1627
1713
  metadata = json.load(f)
1628
1714
  logger.debug(f"Loaded metadata for {session_relpath}: {metadata.get('content_size')} bytes")
1629
1715
  return metadata
@@ -1708,7 +1794,9 @@ def generate_summary_with_llm_from_turn_context(
1708
1794
  payload_lines = []
1709
1795
  if user_message:
1710
1796
  payload_lines.append(
1711
- json.dumps({"type": "user", "message": {"content": user_message}}, ensure_ascii=False)
1797
+ json.dumps(
1798
+ {"type": "user", "message": {"content": user_message}}, ensure_ascii=False
1799
+ )
1712
1800
  )
1713
1801
 
1714
1802
  # Build a single assistant message with all components
@@ -1720,7 +1808,9 @@ def generate_summary_with_llm_from_turn_context(
1720
1808
 
1721
1809
  # Add the final conclusion from trigger (which is the authoritative summary)
1722
1810
  if assistant_summary:
1723
- assistant_parts.append(f"Turn status: {turn_status}\n\nFinal conclusion:\n{assistant_summary}")
1811
+ assistant_parts.append(
1812
+ f"Turn status: {turn_status}\n\nFinal conclusion:\n{assistant_summary}"
1813
+ )
1724
1814
 
1725
1815
  # Add previous records context (new format) or fallback to recent_commit_context
1726
1816
  if previous_records is not None:
realign/logging_config.py CHANGED
@@ -22,15 +22,15 @@ def get_log_level() -> int:
22
22
  Returns:
23
23
  int: Logging level constant from logging module
24
24
  """
25
- level_name = os.getenv('REALIGN_LOG_LEVEL', 'INFO').upper()
25
+ level_name = os.getenv("REALIGN_LOG_LEVEL", "INFO").upper()
26
26
 
27
27
  # Map string to logging constant
28
28
  level_map = {
29
- 'DEBUG': logging.DEBUG,
30
- 'INFO': logging.INFO,
31
- 'WARNING': logging.WARNING,
32
- 'ERROR': logging.ERROR,
33
- 'CRITICAL': logging.CRITICAL,
29
+ "DEBUG": logging.DEBUG,
30
+ "INFO": logging.INFO,
31
+ "WARNING": logging.WARNING,
32
+ "ERROR": logging.ERROR,
33
+ "CRITICAL": logging.CRITICAL,
34
34
  }
35
35
 
36
36
  return level_map.get(level_name, logging.INFO)
@@ -46,12 +46,12 @@ def get_log_directory() -> Path:
46
46
  Returns:
47
47
  Path: Log directory path
48
48
  """
49
- log_dir_str = os.getenv('REALIGN_LOG_DIR')
49
+ log_dir_str = os.getenv("REALIGN_LOG_DIR")
50
50
 
51
51
  if log_dir_str:
52
52
  log_dir = Path(log_dir_str).expanduser()
53
53
  else:
54
- log_dir = Path.home() / '.aline' / '.logs'
54
+ log_dir = Path.home() / ".aline" / ".logs"
55
55
 
56
56
  # Create directory if it doesn't exist
57
57
  log_dir.mkdir(parents=True, exist_ok=True)
@@ -64,7 +64,7 @@ def setup_logger(
64
64
  log_file: Optional[str] = None,
65
65
  max_bytes: int = 10 * 1024 * 1024, # 10MB
66
66
  backup_count: int = 5,
67
- console_output: bool = False
67
+ console_output: bool = False,
68
68
  ) -> logging.Logger:
69
69
  """
70
70
  Set up a logger with file rotation and optional console output.
@@ -95,8 +95,7 @@ def setup_logger(
95
95
 
96
96
  # Standard formatter with timestamp, level, name, and message
97
97
  formatter = logging.Formatter(
98
- fmt='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s',
99
- datefmt='%Y-%m-%d %H:%M:%S'
98
+ fmt="[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
100
99
  )
101
100
 
102
101
  # File handler with rotation
@@ -106,10 +105,7 @@ def setup_logger(
106
105
  log_path = log_dir / log_file
107
106
 
108
107
  file_handler = RotatingFileHandler(
109
- log_path,
110
- maxBytes=max_bytes,
111
- backupCount=backup_count,
112
- encoding='utf-8'
108
+ log_path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8"
113
109
  )
114
110
  file_handler.setLevel(logging.DEBUG) # Capture all levels to file
115
111
  file_handler.setFormatter(formatter)
@@ -119,6 +115,7 @@ def setup_logger(
119
115
  # If file logging fails, fall back to stderr only
120
116
  # Don't let logging setup break the application
121
117
  import sys
118
+
122
119
  print(f"Warning: Failed to set up file logging: {e}", file=sys.stderr)
123
120
 
124
121
  # Optional console handler (stderr)