aline-ai 0.5.4__py3-none-any.whl → 0.5.6__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.
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/METADATA +1 -1
- aline_ai-0.5.6.dist-info/RECORD +95 -0
- realign/__init__.py +1 -1
- realign/adapters/antigravity.py +28 -20
- realign/adapters/base.py +46 -50
- realign/adapters/claude.py +14 -14
- realign/adapters/codex.py +7 -7
- realign/adapters/gemini.py +11 -11
- realign/adapters/registry.py +14 -10
- realign/claude_detector.py +2 -2
- realign/claude_hooks/__init__.py +3 -3
- realign/claude_hooks/permission_request_hook_installer.py +31 -32
- realign/claude_hooks/stop_hook.py +4 -1
- realign/claude_hooks/stop_hook_installer.py +30 -31
- realign/cli.py +23 -4
- realign/codex_detector.py +11 -11
- realign/commands/add.py +88 -65
- realign/commands/config.py +3 -12
- realign/commands/context.py +3 -1
- realign/commands/export_shares.py +86 -127
- realign/commands/import_shares.py +145 -155
- realign/commands/init.py +166 -30
- realign/commands/restore.py +18 -6
- realign/commands/search.py +14 -42
- realign/commands/upgrade.py +155 -11
- realign/commands/watcher.py +98 -219
- realign/commands/worker.py +29 -6
- realign/config.py +25 -20
- realign/context.py +1 -3
- realign/dashboard/app.py +34 -24
- realign/dashboard/screens/__init__.py +10 -1
- realign/dashboard/screens/create_agent.py +244 -0
- realign/dashboard/screens/create_event.py +3 -1
- realign/dashboard/screens/event_detail.py +14 -6
- realign/dashboard/screens/help_screen.py +114 -0
- realign/dashboard/screens/session_detail.py +3 -1
- realign/dashboard/screens/share_import.py +7 -3
- realign/dashboard/tmux_manager.py +54 -9
- realign/dashboard/widgets/config_panel.py +85 -1
- realign/dashboard/widgets/events_table.py +314 -70
- realign/dashboard/widgets/header.py +2 -1
- realign/dashboard/widgets/search_panel.py +37 -27
- realign/dashboard/widgets/sessions_table.py +404 -85
- realign/dashboard/widgets/terminal_panel.py +155 -175
- realign/dashboard/widgets/watcher_panel.py +6 -2
- realign/dashboard/widgets/worker_panel.py +10 -1
- realign/db/__init__.py +1 -1
- realign/db/base.py +5 -15
- realign/db/locks.py +0 -1
- realign/db/migration.py +82 -76
- realign/db/schema.py +2 -6
- realign/db/sqlite_db.py +23 -41
- realign/events/__init__.py +0 -1
- realign/events/event_summarizer.py +27 -15
- realign/events/session_summarizer.py +29 -15
- realign/file_lock.py +1 -0
- realign/hooks.py +150 -60
- realign/logging_config.py +12 -15
- realign/mcp_server.py +30 -51
- realign/mcp_watcher.py +0 -1
- realign/models/event.py +29 -20
- realign/prompts/__init__.py +7 -7
- realign/prompts/presets.py +15 -11
- realign/redactor.py +99 -59
- realign/triggers/__init__.py +9 -9
- realign/triggers/antigravity_trigger.py +30 -28
- realign/triggers/base.py +4 -3
- realign/triggers/claude_trigger.py +104 -85
- realign/triggers/codex_trigger.py +15 -5
- realign/triggers/gemini_trigger.py +57 -47
- realign/triggers/next_turn_trigger.py +3 -1
- realign/triggers/registry.py +6 -2
- realign/triggers/turn_status.py +3 -1
- realign/watcher_core.py +306 -131
- realign/watcher_daemon.py +8 -8
- realign/worker_core.py +3 -1
- realign/worker_daemon.py +3 -1
- aline_ai-0.5.4.dist-info/RECORD +0 -93
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/top_level.txt +0 -0
|
@@ -176,13 +176,22 @@ def _get_session_summary_prompt() -> str:
|
|
|
176
176
|
text = user_prompt_path.read_text(encoding="utf-8").strip()
|
|
177
177
|
if text:
|
|
178
178
|
_SESSION_SUMMARY_PROMPT_CACHE = text
|
|
179
|
-
logger.debug(
|
|
179
|
+
logger.debug(
|
|
180
|
+
f"Loaded user-customized session summary prompt from {user_prompt_path}"
|
|
181
|
+
)
|
|
180
182
|
return text
|
|
181
183
|
except Exception:
|
|
182
|
-
logger.debug(
|
|
184
|
+
logger.debug(
|
|
185
|
+
"Failed to load user-customized session summary prompt, falling back", exc_info=True
|
|
186
|
+
)
|
|
183
187
|
|
|
184
188
|
# Fall back to built-in prompt (tools/commit_message_prompts/session_summary.md)
|
|
185
|
-
candidate =
|
|
189
|
+
candidate = (
|
|
190
|
+
Path(__file__).resolve().parents[2]
|
|
191
|
+
/ "tools"
|
|
192
|
+
/ "commit_message_prompts"
|
|
193
|
+
/ "session_summary.md"
|
|
194
|
+
)
|
|
186
195
|
try:
|
|
187
196
|
if candidate.exists():
|
|
188
197
|
text = candidate.read_text(encoding="utf-8").strip()
|
|
@@ -234,19 +243,25 @@ def _generate_session_summary_llm(turns: List[TurnRecord]) -> Tuple[str, str]:
|
|
|
234
243
|
# Build turns payload for prompt
|
|
235
244
|
turns_data = []
|
|
236
245
|
for i, turn in enumerate(turns):
|
|
237
|
-
turns_data.append(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
246
|
+
turns_data.append(
|
|
247
|
+
{
|
|
248
|
+
"turn_number": i + 1,
|
|
249
|
+
"title": turn.llm_title or "(no title)",
|
|
250
|
+
"summary": turn.assistant_summary or "(no summary)",
|
|
251
|
+
"user_request": (turn.user_message or "")[:200], # Truncate long messages
|
|
252
|
+
}
|
|
253
|
+
)
|
|
243
254
|
|
|
244
255
|
system_prompt = _get_session_summary_prompt()
|
|
245
256
|
|
|
246
|
-
user_prompt = json.dumps(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
257
|
+
user_prompt = json.dumps(
|
|
258
|
+
{
|
|
259
|
+
"total_turns": len(turns),
|
|
260
|
+
"turns": turns_data,
|
|
261
|
+
},
|
|
262
|
+
ensure_ascii=False,
|
|
263
|
+
indent=2,
|
|
264
|
+
)
|
|
250
265
|
|
|
251
266
|
try:
|
|
252
267
|
# Use unified LLM client
|
|
@@ -255,7 +270,7 @@ def _generate_session_summary_llm(turns: List[TurnRecord]) -> Tuple[str, str]:
|
|
|
255
270
|
user_prompt=user_prompt,
|
|
256
271
|
provider="auto", # Try Claude first, fallback to OpenAI
|
|
257
272
|
max_tokens=500,
|
|
258
|
-
purpose="session_summary"
|
|
273
|
+
purpose="session_summary",
|
|
259
274
|
)
|
|
260
275
|
|
|
261
276
|
if not response:
|
|
@@ -290,4 +305,3 @@ def _fallback_summary(turns: List[TurnRecord]) -> Tuple[str, str]:
|
|
|
290
305
|
summary = "\n".join(f"- {s}" for s in recent) if recent else ""
|
|
291
306
|
|
|
292
307
|
return title, summary
|
|
293
|
-
|
realign/file_lock.py
CHANGED
|
@@ -109,6 +109,7 @@ def commit_lock(repo_path: Path, timeout: float = 10.0):
|
|
|
109
109
|
True if lock was acquired
|
|
110
110
|
"""
|
|
111
111
|
from realign import get_realign_dir
|
|
112
|
+
|
|
112
113
|
realign_dir = get_realign_dir(repo_path)
|
|
113
114
|
lock_file = realign_dir / ".commit.lock"
|
|
114
115
|
lock = FileLock(lock_file, timeout=timeout)
|
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(
|
|
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(
|
|
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(
|
|
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 = [
|
|
151
|
-
|
|
152
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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,
|
|
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(
|
|
430
|
-
session_cwd = data.get(
|
|
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(
|
|
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(
|
|
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:
|
|
520
|
-
|
|
521
|
-
if config.
|
|
522
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|
|
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(
|
|
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:
|