aline-ai 0.6.2__py3-none-any.whl → 0.6.4__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.6.2.dist-info → aline_ai-0.6.4.dist-info}/METADATA +1 -1
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/RECORD +38 -37
- realign/__init__.py +1 -1
- realign/adapters/__init__.py +0 -3
- realign/adapters/codex.py +14 -9
- realign/cli.py +42 -236
- realign/codex_detector.py +72 -32
- realign/codex_home.py +85 -0
- realign/codex_terminal_linker.py +172 -0
- realign/commands/__init__.py +2 -2
- realign/commands/add.py +89 -9
- realign/commands/doctor.py +495 -0
- realign/commands/export_shares.py +154 -226
- realign/commands/init.py +66 -4
- realign/commands/watcher.py +30 -80
- realign/config.py +9 -46
- realign/dashboard/app.py +7 -11
- realign/dashboard/screens/event_detail.py +0 -3
- realign/dashboard/screens/session_detail.py +0 -1
- realign/dashboard/tmux_manager.py +129 -4
- realign/dashboard/widgets/config_panel.py +175 -241
- realign/dashboard/widgets/events_table.py +71 -128
- realign/dashboard/widgets/sessions_table.py +77 -136
- realign/dashboard/widgets/terminal_panel.py +349 -27
- realign/dashboard/widgets/watcher_panel.py +0 -2
- realign/db/sqlite_db.py +77 -2
- realign/events/event_summarizer.py +76 -35
- realign/events/session_summarizer.py +73 -32
- realign/hooks.py +334 -647
- realign/llm_client.py +201 -520
- realign/triggers/__init__.py +0 -2
- realign/triggers/next_turn_trigger.py +4 -5
- realign/triggers/registry.py +1 -4
- realign/watcher_core.py +53 -35
- realign/adapters/antigravity.py +0 -159
- realign/triggers/antigravity_trigger.py +0 -140
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"""Aline doctor command - Repair common issues after updates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import io
|
|
7
|
+
import shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from ..config import ReAlignConfig, get_default_config_content
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _clear_python_cache(root: Path, *, verbose: bool) -> Tuple[int, int]:
|
|
21
|
+
pyc_count = 0
|
|
22
|
+
pycache_count = 0
|
|
23
|
+
|
|
24
|
+
for pyc_file in root.rglob("*.pyc"):
|
|
25
|
+
try:
|
|
26
|
+
pyc_file.unlink()
|
|
27
|
+
pyc_count += 1
|
|
28
|
+
if verbose:
|
|
29
|
+
console.print(f" [dim]Removed: {pyc_file}[/dim]")
|
|
30
|
+
except Exception as e:
|
|
31
|
+
if verbose:
|
|
32
|
+
console.print(f" [yellow]Failed to remove {pyc_file}: {e}[/yellow]")
|
|
33
|
+
|
|
34
|
+
for pycache_dir in root.rglob("__pycache__"):
|
|
35
|
+
if not pycache_dir.is_dir():
|
|
36
|
+
continue
|
|
37
|
+
try:
|
|
38
|
+
shutil.rmtree(pycache_dir)
|
|
39
|
+
pycache_count += 1
|
|
40
|
+
if verbose:
|
|
41
|
+
console.print(f" [dim]Removed: {pycache_dir}[/dim]")
|
|
42
|
+
except Exception as e:
|
|
43
|
+
if verbose:
|
|
44
|
+
console.print(f" [yellow]Failed to remove {pycache_dir}: {e}[/yellow]")
|
|
45
|
+
|
|
46
|
+
return pyc_count, pycache_count
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _ensure_global_config(*, force: bool, verbose: bool) -> Path:
|
|
50
|
+
config_path = Path.home() / ".aline" / "config.yaml"
|
|
51
|
+
|
|
52
|
+
if force or not config_path.exists():
|
|
53
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
config_path.write_text(get_default_config_content(), encoding="utf-8")
|
|
55
|
+
if verbose:
|
|
56
|
+
console.print(f" [dim]Wrote config: {config_path}[/dim]")
|
|
57
|
+
|
|
58
|
+
return config_path
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _ensure_database_initialized(config: ReAlignConfig, *, verbose: bool) -> Path:
|
|
62
|
+
db_path = Path(config.sqlite_db_path).expanduser()
|
|
63
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
|
|
65
|
+
from ..db.sqlite_db import SQLiteDatabase
|
|
66
|
+
|
|
67
|
+
db = SQLiteDatabase(str(db_path))
|
|
68
|
+
ok = db.initialize()
|
|
69
|
+
db.close()
|
|
70
|
+
if verbose:
|
|
71
|
+
console.print(f" [dim]Database init: {'ok' if ok else 'failed'}[/dim]")
|
|
72
|
+
|
|
73
|
+
return db_path
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _update_claude_hooks(*, verbose: bool) -> Tuple[list[str], list[str]]:
|
|
77
|
+
hooks_updated: list[str] = []
|
|
78
|
+
hooks_failed: list[str] = []
|
|
79
|
+
|
|
80
|
+
# Stop hook
|
|
81
|
+
try:
|
|
82
|
+
from ..claude_hooks.stop_hook_installer import install_stop_hook, get_settings_path
|
|
83
|
+
|
|
84
|
+
if install_stop_hook(get_settings_path(), quiet=True, force=True):
|
|
85
|
+
hooks_updated.append("Stop")
|
|
86
|
+
if verbose:
|
|
87
|
+
console.print(" [dim]Stop hook updated[/dim]")
|
|
88
|
+
else:
|
|
89
|
+
hooks_failed.append("Stop")
|
|
90
|
+
except Exception as e:
|
|
91
|
+
hooks_failed.append("Stop")
|
|
92
|
+
if verbose:
|
|
93
|
+
console.print(f" [yellow]Stop hook failed: {e}[/yellow]")
|
|
94
|
+
|
|
95
|
+
# UserPromptSubmit hook
|
|
96
|
+
try:
|
|
97
|
+
from ..claude_hooks.user_prompt_submit_hook_installer import (
|
|
98
|
+
install_user_prompt_submit_hook,
|
|
99
|
+
get_settings_path as get_submit_settings_path,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if install_user_prompt_submit_hook(get_submit_settings_path(), quiet=True, force=True):
|
|
103
|
+
hooks_updated.append("UserPromptSubmit")
|
|
104
|
+
if verbose:
|
|
105
|
+
console.print(" [dim]UserPromptSubmit hook updated[/dim]")
|
|
106
|
+
else:
|
|
107
|
+
hooks_failed.append("UserPromptSubmit")
|
|
108
|
+
except Exception as e:
|
|
109
|
+
hooks_failed.append("UserPromptSubmit")
|
|
110
|
+
if verbose:
|
|
111
|
+
console.print(f" [yellow]UserPromptSubmit hook failed: {e}[/yellow]")
|
|
112
|
+
|
|
113
|
+
# PermissionRequest hook
|
|
114
|
+
try:
|
|
115
|
+
from ..claude_hooks.permission_request_hook_installer import (
|
|
116
|
+
install_permission_request_hook,
|
|
117
|
+
get_settings_path as get_permission_settings_path,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if install_permission_request_hook(get_permission_settings_path(), quiet=True, force=True):
|
|
121
|
+
hooks_updated.append("PermissionRequest")
|
|
122
|
+
if verbose:
|
|
123
|
+
console.print(" [dim]PermissionRequest hook updated[/dim]")
|
|
124
|
+
else:
|
|
125
|
+
hooks_failed.append("PermissionRequest")
|
|
126
|
+
except Exception as e:
|
|
127
|
+
hooks_failed.append("PermissionRequest")
|
|
128
|
+
if verbose:
|
|
129
|
+
console.print(f" [yellow]PermissionRequest hook failed: {e}[/yellow]")
|
|
130
|
+
|
|
131
|
+
return hooks_updated, hooks_failed
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _update_skills(*, verbose: bool) -> int:
|
|
135
|
+
from .add import add_skills_command
|
|
136
|
+
|
|
137
|
+
stdout_capture = io.StringIO()
|
|
138
|
+
with contextlib.redirect_stdout(stdout_capture):
|
|
139
|
+
add_skills_command(force=True)
|
|
140
|
+
|
|
141
|
+
output = stdout_capture.getvalue()
|
|
142
|
+
updated_count = output.count("✓")
|
|
143
|
+
if verbose and output.strip():
|
|
144
|
+
for line in output.strip().split("\n"):
|
|
145
|
+
console.print(f" [dim]{line}[/dim]")
|
|
146
|
+
return updated_count
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _check_failed_jobs(
|
|
150
|
+
config: ReAlignConfig,
|
|
151
|
+
*,
|
|
152
|
+
verbose: bool,
|
|
153
|
+
fix: bool,
|
|
154
|
+
) -> Tuple[int, int]:
|
|
155
|
+
"""
|
|
156
|
+
Check for failed turn_summary and session_summary jobs.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
(failed_count, requeued_count)
|
|
160
|
+
"""
|
|
161
|
+
from ..db.sqlite_db import SQLiteDatabase
|
|
162
|
+
|
|
163
|
+
db_path = Path(config.sqlite_db_path).expanduser()
|
|
164
|
+
if not db_path.exists():
|
|
165
|
+
return 0, 0
|
|
166
|
+
|
|
167
|
+
db = SQLiteDatabase(str(db_path))
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
# Count failed jobs in queue
|
|
171
|
+
failed_turn_count = db.count_jobs(kinds=["turn_summary"], statuses=["failed"])
|
|
172
|
+
failed_session_count = db.count_jobs(kinds=["session_summary"], statuses=["failed"])
|
|
173
|
+
total_failed = failed_turn_count + failed_session_count
|
|
174
|
+
|
|
175
|
+
if verbose and total_failed > 0:
|
|
176
|
+
# Show details of failed jobs
|
|
177
|
+
failed_jobs = db.list_jobs(statuses=["failed"], limit=20)
|
|
178
|
+
if failed_jobs:
|
|
179
|
+
table = Table(title="Failed Jobs in Queue", show_header=True, header_style="bold")
|
|
180
|
+
table.add_column("Kind", style="cyan")
|
|
181
|
+
table.add_column("Session", style="dim")
|
|
182
|
+
table.add_column("Turn", style="dim")
|
|
183
|
+
table.add_column("Error", style="red", max_width=40)
|
|
184
|
+
table.add_column("Attempts", justify="right")
|
|
185
|
+
|
|
186
|
+
for job in failed_jobs:
|
|
187
|
+
payload = job.get("payload", {})
|
|
188
|
+
session_id = str(payload.get("session_id", ""))[:8]
|
|
189
|
+
turn_num = str(payload.get("turn_number", "-"))
|
|
190
|
+
error = (job.get("last_error") or "")[:40]
|
|
191
|
+
attempts = str(job.get("attempts", 0))
|
|
192
|
+
table.add_row(job["kind"], session_id, turn_num, error, attempts)
|
|
193
|
+
|
|
194
|
+
console.print(table)
|
|
195
|
+
|
|
196
|
+
requeued = 0
|
|
197
|
+
if fix and total_failed > 0:
|
|
198
|
+
# Requeue failed jobs
|
|
199
|
+
requeued, _ = db.requeue_failed_jobs(kinds=["turn_summary", "session_summary"])
|
|
200
|
+
|
|
201
|
+
return total_failed, requeued
|
|
202
|
+
|
|
203
|
+
finally:
|
|
204
|
+
db.close()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _check_llm_error_turns(
|
|
208
|
+
config: ReAlignConfig,
|
|
209
|
+
*,
|
|
210
|
+
verbose: bool,
|
|
211
|
+
fix: bool,
|
|
212
|
+
) -> Tuple[int, int]:
|
|
213
|
+
"""
|
|
214
|
+
Check for turns with LLM API errors (llm_title contains 'LLM API Error').
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
(error_count, requeued_count)
|
|
218
|
+
"""
|
|
219
|
+
from ..db.sqlite_db import SQLiteDatabase
|
|
220
|
+
|
|
221
|
+
db_path = Path(config.sqlite_db_path).expanduser()
|
|
222
|
+
if not db_path.exists():
|
|
223
|
+
return 0, 0
|
|
224
|
+
|
|
225
|
+
db = SQLiteDatabase(str(db_path))
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
conn = db._get_connection()
|
|
229
|
+
|
|
230
|
+
# Find turns with LLM API Error marker (exact prefix match)
|
|
231
|
+
rows = conn.execute(
|
|
232
|
+
"""
|
|
233
|
+
SELECT t.session_id, t.turn_number, t.llm_title, s.session_file_path, s.workspace_path
|
|
234
|
+
FROM turns t
|
|
235
|
+
JOIN sessions s ON t.session_id = s.id
|
|
236
|
+
WHERE t.llm_title LIKE '⚠ LLM API Error%'
|
|
237
|
+
ORDER BY t.timestamp DESC
|
|
238
|
+
"""
|
|
239
|
+
).fetchall()
|
|
240
|
+
|
|
241
|
+
if not rows:
|
|
242
|
+
return 0, 0
|
|
243
|
+
|
|
244
|
+
if verbose:
|
|
245
|
+
table = Table(title="Turns with LLM API Error", show_header=True, header_style="bold")
|
|
246
|
+
table.add_column("Session", style="dim")
|
|
247
|
+
table.add_column("Turn", justify="right")
|
|
248
|
+
table.add_column("Title", style="yellow", max_width=50)
|
|
249
|
+
|
|
250
|
+
for row in rows[:20]: # Show max 20
|
|
251
|
+
session_id = str(row["session_id"])[:12]
|
|
252
|
+
turn_num = str(row["turn_number"])
|
|
253
|
+
title = (row["llm_title"] or "")[:50]
|
|
254
|
+
table.add_row(session_id, turn_num, title)
|
|
255
|
+
|
|
256
|
+
if len(rows) > 20:
|
|
257
|
+
table.add_row("...", "...", f"({len(rows) - 20} more)")
|
|
258
|
+
|
|
259
|
+
console.print(table)
|
|
260
|
+
|
|
261
|
+
if not fix:
|
|
262
|
+
return len(rows), 0
|
|
263
|
+
|
|
264
|
+
# Requeue turn_summary jobs for these turns
|
|
265
|
+
requeued = 0
|
|
266
|
+
skipped = 0
|
|
267
|
+
for row in rows:
|
|
268
|
+
session_file_path_str = row["session_file_path"] or ""
|
|
269
|
+
|
|
270
|
+
# Skip invalid session file paths
|
|
271
|
+
if not session_file_path_str or session_file_path_str in (".", ".."):
|
|
272
|
+
if verbose:
|
|
273
|
+
console.print(f" [dim]Skip: invalid session path for {row['session_id'][:8]} #{row['turn_number']}[/dim]")
|
|
274
|
+
skipped += 1
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
session_file_path = Path(session_file_path_str)
|
|
278
|
+
workspace_path = Path(row["workspace_path"]) if row["workspace_path"] else None
|
|
279
|
+
|
|
280
|
+
if not session_file_path.exists():
|
|
281
|
+
if verbose:
|
|
282
|
+
console.print(f" [dim]Skip: session file not found: {session_file_path}[/dim]")
|
|
283
|
+
skipped += 1
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
db.enqueue_turn_summary_job(
|
|
288
|
+
session_file_path=session_file_path,
|
|
289
|
+
workspace_path=workspace_path,
|
|
290
|
+
turn_number=row["turn_number"],
|
|
291
|
+
skip_dedup=True, # Force regeneration
|
|
292
|
+
)
|
|
293
|
+
requeued += 1
|
|
294
|
+
except Exception as e:
|
|
295
|
+
if verbose:
|
|
296
|
+
console.print(f" [yellow]Failed to enqueue {row['session_id'][:8]} #{row['turn_number']}: {e}[/yellow]")
|
|
297
|
+
skipped += 1
|
|
298
|
+
|
|
299
|
+
return len(rows), requeued
|
|
300
|
+
|
|
301
|
+
finally:
|
|
302
|
+
db.close()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def run_doctor(
|
|
306
|
+
*,
|
|
307
|
+
restart_daemons: bool,
|
|
308
|
+
start_if_not_running: bool,
|
|
309
|
+
verbose: bool,
|
|
310
|
+
clear_cache: bool,
|
|
311
|
+
) -> int:
|
|
312
|
+
"""
|
|
313
|
+
Core doctor logic.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
restart_daemons: Restart/ensure daemons at the end.
|
|
317
|
+
start_if_not_running: If True and restart_daemons is True, start daemons even if not running.
|
|
318
|
+
verbose: Print details.
|
|
319
|
+
clear_cache: Clear Python bytecode cache for the installed package directory.
|
|
320
|
+
"""
|
|
321
|
+
from ..auth import is_logged_in
|
|
322
|
+
from . import watcher as watcher_cmd
|
|
323
|
+
from . import worker as worker_cmd
|
|
324
|
+
from . import init as init_cmd
|
|
325
|
+
|
|
326
|
+
console.print("\n[bold blue]═══ Aline Doctor ═══[/bold blue]\n")
|
|
327
|
+
|
|
328
|
+
watcher_running, _watcher_pid, _watcher_mode = watcher_cmd.detect_watcher_process()
|
|
329
|
+
worker_running, _worker_pid, _worker_mode = worker_cmd.detect_worker_process()
|
|
330
|
+
|
|
331
|
+
can_start_daemons = is_logged_in()
|
|
332
|
+
if restart_daemons and not can_start_daemons:
|
|
333
|
+
console.print("[yellow]![/yellow] Not logged in; skipping daemon restart/start.")
|
|
334
|
+
console.print("[dim]Run 'aline login' then re-run 'aline doctor'.[/dim]")
|
|
335
|
+
restart_daemons = False
|
|
336
|
+
|
|
337
|
+
# Stop daemons early to avoid DB lock during migrations.
|
|
338
|
+
if restart_daemons:
|
|
339
|
+
if watcher_running:
|
|
340
|
+
if verbose:
|
|
341
|
+
console.print("[dim]Stopping watcher...[/dim]")
|
|
342
|
+
try:
|
|
343
|
+
watcher_cmd.watcher_stop_command()
|
|
344
|
+
except Exception:
|
|
345
|
+
pass
|
|
346
|
+
if worker_running:
|
|
347
|
+
if verbose:
|
|
348
|
+
console.print("[dim]Stopping worker...[/dim]")
|
|
349
|
+
try:
|
|
350
|
+
worker_cmd.worker_stop_command()
|
|
351
|
+
except Exception:
|
|
352
|
+
pass
|
|
353
|
+
|
|
354
|
+
# 1. Clear Python cache (package scope)
|
|
355
|
+
if clear_cache:
|
|
356
|
+
console.print("[bold]1. Clearing Python cache...[/bold]")
|
|
357
|
+
package_root = Path(__file__).resolve().parents[1]
|
|
358
|
+
pyc_count, pycache_count = _clear_python_cache(package_root, verbose=verbose)
|
|
359
|
+
console.print(
|
|
360
|
+
f" [green]✓[/green] Cleared {pyc_count} .pyc files, {pycache_count} __pycache__ directories"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# 2. Ensure global environment (config/db/prompts/tmux)
|
|
364
|
+
console.print("\n[bold]2. Ensuring global environment...[/bold]")
|
|
365
|
+
try:
|
|
366
|
+
config_path = _ensure_global_config(force=False, verbose=verbose)
|
|
367
|
+
config = ReAlignConfig.load(config_path)
|
|
368
|
+
db_path = _ensure_database_initialized(config, verbose=verbose)
|
|
369
|
+
|
|
370
|
+
# Prompts + tmux are safe to re-run (no overwrite for prompts; tmux auto-updates Aline-managed config).
|
|
371
|
+
init_cmd._initialize_prompts_directory()
|
|
372
|
+
tmux_conf = init_cmd._initialize_tmux_config()
|
|
373
|
+
|
|
374
|
+
console.print(f" [green]✓[/green] Config: {config_path}")
|
|
375
|
+
console.print(f" [green]✓[/green] Database: {db_path}")
|
|
376
|
+
console.print(f" [green]✓[/green] Prompts: {Path.home() / '.aline' / 'prompts'}")
|
|
377
|
+
console.print(f" [green]✓[/green] Tmux: {tmux_conf}")
|
|
378
|
+
except Exception as e:
|
|
379
|
+
console.print(f" [red]✗[/red] Failed to ensure global environment: {e}")
|
|
380
|
+
return 1
|
|
381
|
+
|
|
382
|
+
# 3. Update Claude Code hooks
|
|
383
|
+
console.print("\n[bold]3. Updating Claude Code hooks...[/bold]")
|
|
384
|
+
hooks_updated, hooks_failed = _update_claude_hooks(verbose=verbose)
|
|
385
|
+
if hooks_updated:
|
|
386
|
+
console.print(f" [green]✓[/green] Updated hooks: {', '.join(hooks_updated)}")
|
|
387
|
+
if hooks_failed:
|
|
388
|
+
console.print(f" [yellow]![/yellow] Failed hooks: {', '.join(hooks_failed)}")
|
|
389
|
+
|
|
390
|
+
# 4. Update skills
|
|
391
|
+
console.print("\n[bold]4. Updating skills...[/bold]")
|
|
392
|
+
try:
|
|
393
|
+
updated_count = _update_skills(verbose=verbose)
|
|
394
|
+
if updated_count > 0:
|
|
395
|
+
console.print(f" [green]✓[/green] Updated {updated_count} skill(s)")
|
|
396
|
+
else:
|
|
397
|
+
console.print(" [green]✓[/green] Skills are up to date")
|
|
398
|
+
except Exception as e:
|
|
399
|
+
console.print(f" [yellow]![/yellow] Failed to update skills: {e}")
|
|
400
|
+
|
|
401
|
+
# 5. Check/fix failed jobs and LLM error turns
|
|
402
|
+
console.print("\n[bold]5. Checking failed summary jobs...[/bold]")
|
|
403
|
+
try:
|
|
404
|
+
# First pass: check counts without fixing
|
|
405
|
+
failed_count, _ = _check_failed_jobs(config, verbose=verbose, fix=False)
|
|
406
|
+
llm_error_count, _ = _check_llm_error_turns(config, verbose=verbose, fix=False)
|
|
407
|
+
|
|
408
|
+
total_issues = failed_count + llm_error_count
|
|
409
|
+
|
|
410
|
+
if total_issues == 0:
|
|
411
|
+
console.print(" [green]✓[/green] No failed jobs or LLM errors found")
|
|
412
|
+
else:
|
|
413
|
+
# Show what was found
|
|
414
|
+
if failed_count > 0:
|
|
415
|
+
console.print(f" [yellow]![/yellow] Found {failed_count} failed job(s) in queue")
|
|
416
|
+
if llm_error_count > 0:
|
|
417
|
+
console.print(f" [yellow]![/yellow] Found {llm_error_count} turn(s) with LLM API errors")
|
|
418
|
+
|
|
419
|
+
# Ask user if they want to fix
|
|
420
|
+
if typer.confirm("\n Do you want to requeue these for regeneration?", default=True):
|
|
421
|
+
requeued_jobs = 0
|
|
422
|
+
requeued_turns = 0
|
|
423
|
+
|
|
424
|
+
if failed_count > 0:
|
|
425
|
+
_, requeued_jobs = _check_failed_jobs(config, verbose=verbose, fix=True)
|
|
426
|
+
if llm_error_count > 0:
|
|
427
|
+
_, requeued_turns = _check_llm_error_turns(config, verbose=verbose, fix=True)
|
|
428
|
+
|
|
429
|
+
total_requeued = requeued_jobs + requeued_turns
|
|
430
|
+
console.print(f" [green]✓[/green] Requeued {total_requeued} item(s) for regeneration")
|
|
431
|
+
else:
|
|
432
|
+
console.print(" [dim]Skipped fixing failed jobs[/dim]")
|
|
433
|
+
except Exception as e:
|
|
434
|
+
console.print(f" [yellow]![/yellow] Failed to check jobs: {e}")
|
|
435
|
+
|
|
436
|
+
# 6. Restart/ensure daemons
|
|
437
|
+
if restart_daemons:
|
|
438
|
+
console.print("\n[bold]6. Checking daemons...[/bold]")
|
|
439
|
+
|
|
440
|
+
should_start_watcher = watcher_running or start_if_not_running
|
|
441
|
+
should_start_worker = worker_running or start_if_not_running
|
|
442
|
+
|
|
443
|
+
if should_start_watcher:
|
|
444
|
+
try:
|
|
445
|
+
exit_code = watcher_cmd.watcher_start_command()
|
|
446
|
+
if exit_code == 0:
|
|
447
|
+
console.print(" [green]✓[/green] Watcher is running")
|
|
448
|
+
else:
|
|
449
|
+
console.print(" [yellow]![/yellow] Failed to start watcher")
|
|
450
|
+
except Exception as e:
|
|
451
|
+
console.print(f" [yellow]![/yellow] Failed to start watcher: {e}")
|
|
452
|
+
else:
|
|
453
|
+
console.print(" [dim]Watcher was not running; leaving it stopped.[/dim]")
|
|
454
|
+
|
|
455
|
+
if should_start_worker:
|
|
456
|
+
try:
|
|
457
|
+
exit_code = worker_cmd.worker_start_command()
|
|
458
|
+
if exit_code == 0:
|
|
459
|
+
console.print(" [green]✓[/green] Worker is running")
|
|
460
|
+
else:
|
|
461
|
+
console.print(" [yellow]![/yellow] Failed to start worker")
|
|
462
|
+
except Exception as e:
|
|
463
|
+
console.print(f" [yellow]![/yellow] Failed to start worker: {e}")
|
|
464
|
+
else:
|
|
465
|
+
console.print(" [dim]Worker was not running; leaving it stopped.[/dim]")
|
|
466
|
+
else:
|
|
467
|
+
console.print("\n[dim]Skipping daemon restart (--no-restart)[/dim]")
|
|
468
|
+
|
|
469
|
+
console.print("\n[green]Done![/green] Aline is ready with the latest code.")
|
|
470
|
+
return 0
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def doctor_command(
|
|
474
|
+
no_restart: bool = typer.Option(False, "--no-restart", help="Only repair files, don't restart daemons"),
|
|
475
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
476
|
+
):
|
|
477
|
+
"""
|
|
478
|
+
Fix common issues after code updates.
|
|
479
|
+
|
|
480
|
+
This command:
|
|
481
|
+
- Clears Python bytecode cache for the installed Aline package
|
|
482
|
+
- Ensures global config/DB/prompts/tmux are present and up to date
|
|
483
|
+
- Updates Claude Code hooks (Stop, UserPromptSubmit, PermissionRequest)
|
|
484
|
+
- Updates Claude Code skills to the latest version
|
|
485
|
+
- Checks for failed summary jobs (prompts to fix if found)
|
|
486
|
+
- Restarts watcher/worker (default) so long-running processes use the latest code
|
|
487
|
+
"""
|
|
488
|
+
exit_code = run_doctor(
|
|
489
|
+
restart_daemons=not no_restart,
|
|
490
|
+
start_if_not_running=True,
|
|
491
|
+
verbose=verbose,
|
|
492
|
+
clear_cache=True,
|
|
493
|
+
)
|
|
494
|
+
raise typer.Exit(code=exit_code)
|
|
495
|
+
|