aline-ai 0.7.2__py3-none-any.whl → 0.7.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.7.2.dist-info → aline_ai-0.7.4.dist-info}/METADATA +1 -1
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.dist-info}/RECORD +32 -27
- realign/__init__.py +1 -1
- realign/adapters/codex.py +30 -2
- realign/claude_hooks/stop_hook.py +176 -21
- realign/codex_home.py +71 -0
- realign/codex_hooks/__init__.py +16 -0
- realign/codex_hooks/notify_hook.py +511 -0
- realign/codex_hooks/notify_hook_installer.py +247 -0
- realign/commands/doctor.py +125 -0
- realign/commands/export_shares.py +188 -65
- realign/commands/import_shares.py +30 -10
- realign/commands/init.py +16 -0
- realign/commands/sync_agent.py +274 -44
- realign/commit_pipeline.py +1024 -0
- realign/config.py +3 -11
- realign/dashboard/app.py +151 -2
- realign/dashboard/diagnostics.py +274 -0
- realign/dashboard/screens/create_agent.py +2 -1
- realign/dashboard/screens/create_agent_info.py +40 -77
- realign/dashboard/tmux_manager.py +348 -33
- realign/dashboard/widgets/agents_panel.py +942 -314
- realign/dashboard/widgets/config_panel.py +34 -121
- realign/dashboard/widgets/header.py +1 -1
- realign/db/sqlite_db.py +59 -1
- realign/logging_config.py +51 -6
- realign/watcher_core.py +742 -393
- realign/worker_core.py +206 -15
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.dist-info}/WHEEL +0 -0
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.7.2.dist-info → aline_ai-0.7.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Codex notify hook installer (best-effort).
|
|
3
|
+
|
|
4
|
+
We primarily support the Rust Codex CLI which reads CODEX_HOME/config.toml and
|
|
5
|
+
supports `notify = "command args..."` to run a script when a turn finishes.
|
|
6
|
+
|
|
7
|
+
For legacy Codex config.yaml/config.json formats, we can only set `notify: true`
|
|
8
|
+
to enable built-in notifications; there is no guaranteed script hook in that format.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from ..logging_config import setup_logger
|
|
22
|
+
|
|
23
|
+
logger = setup_logger("realign.codex_hooks.installer", "codex_hooks_installer.log")
|
|
24
|
+
|
|
25
|
+
ALINE_HOOK_MARKER = "aline-codex-notify-hook"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_notify_hook_script_path() -> Path:
|
|
29
|
+
return Path(__file__).parent / "notify_hook.py"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_notify_hook_command_parts() -> list[str]:
|
|
33
|
+
script_path = get_notify_hook_script_path()
|
|
34
|
+
return [sys.executable, str(script_path)]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _toml_escape(s: str) -> str:
|
|
38
|
+
return s.replace("\\", "\\\\").replace('"', '\\"')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _format_notify_toml(cmd: list[str]) -> str:
|
|
42
|
+
# Codex CLI expects notify as a string (shell command), not an array.
|
|
43
|
+
# NOTE: we intentionally do not attempt complex quoting here; the common case is
|
|
44
|
+
# paths without spaces (e.g. /opt/homebrew/...).
|
|
45
|
+
command_str = " ".join(cmd)
|
|
46
|
+
return f"notify = \"{_toml_escape(command_str)}\" # {ALINE_HOOK_MARKER}\n"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _update_toml_linewise(path: Path, *, cmd: list[str]) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Update config.toml in a minimal, formatting-preserving way (line-based).
|
|
52
|
+
|
|
53
|
+
Returns True if the file was written/updated.
|
|
54
|
+
"""
|
|
55
|
+
desired = _format_notify_toml(cmd)
|
|
56
|
+
existing = ""
|
|
57
|
+
try:
|
|
58
|
+
existing = path.read_text(encoding="utf-8")
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
existing = ""
|
|
61
|
+
except Exception:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
lines = existing.splitlines(keepends=True) if existing else []
|
|
65
|
+
out: list[str] = []
|
|
66
|
+
replaced = False
|
|
67
|
+
|
|
68
|
+
for line in lines:
|
|
69
|
+
stripped = line.lstrip()
|
|
70
|
+
if stripped.startswith("notify ="):
|
|
71
|
+
if not replaced:
|
|
72
|
+
out.append(desired)
|
|
73
|
+
replaced = True
|
|
74
|
+
else:
|
|
75
|
+
# Drop duplicate notify lines.
|
|
76
|
+
continue
|
|
77
|
+
else:
|
|
78
|
+
out.append(line)
|
|
79
|
+
|
|
80
|
+
if not replaced:
|
|
81
|
+
if out and not out[-1].endswith("\n"):
|
|
82
|
+
out[-1] = out[-1] + "\n"
|
|
83
|
+
if out and out[-1].strip():
|
|
84
|
+
out.append("\n")
|
|
85
|
+
out.append(desired)
|
|
86
|
+
|
|
87
|
+
new_text = "".join(out)
|
|
88
|
+
if new_text == existing:
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
path.write_text(new_text, encoding="utf-8")
|
|
94
|
+
return True
|
|
95
|
+
except Exception:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _ensure_legacy_notify_enabled(codex_home: Path) -> list[Path]:
|
|
100
|
+
"""
|
|
101
|
+
Best-effort for legacy Codex config formats (YAML/JSON):
|
|
102
|
+
set `notify: true` / `"notify": true` if the file exists.
|
|
103
|
+
"""
|
|
104
|
+
updated: list[Path] = []
|
|
105
|
+
yaml_path = codex_home / "config.yaml"
|
|
106
|
+
json_path = codex_home / "config.json"
|
|
107
|
+
|
|
108
|
+
if yaml_path.exists():
|
|
109
|
+
try:
|
|
110
|
+
raw = yaml_path.read_text(encoding="utf-8")
|
|
111
|
+
if "notify:" in raw:
|
|
112
|
+
# Minimal replace: notify: <anything> -> notify: true
|
|
113
|
+
out_lines: list[str] = []
|
|
114
|
+
for line in raw.splitlines():
|
|
115
|
+
if line.strip().startswith("notify:"):
|
|
116
|
+
out_lines.append("notify: true")
|
|
117
|
+
else:
|
|
118
|
+
out_lines.append(line)
|
|
119
|
+
new_raw = "\n".join(out_lines) + ("\n" if raw.endswith("\n") else "")
|
|
120
|
+
else:
|
|
121
|
+
new_raw = raw + ("\n" if raw and not raw.endswith("\n") else "") + "notify: true\n"
|
|
122
|
+
if new_raw != raw:
|
|
123
|
+
yaml_path.write_text(new_raw, encoding="utf-8")
|
|
124
|
+
updated.append(yaml_path)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
if json_path.exists():
|
|
129
|
+
try:
|
|
130
|
+
obj = json.loads(json_path.read_text(encoding="utf-8") or "{}")
|
|
131
|
+
if isinstance(obj, dict):
|
|
132
|
+
if obj.get("notify") is not True:
|
|
133
|
+
obj["notify"] = True
|
|
134
|
+
json_path.write_text(json.dumps(obj, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
135
|
+
updated.append(json_path)
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
return updated
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def ensure_notify_hook_installed_for_codex_home(
|
|
143
|
+
codex_home: Path, *, quiet: bool = True
|
|
144
|
+
) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Ensure the notify hook is installed for a given CODEX_HOME.
|
|
147
|
+
|
|
148
|
+
- Rust CLI: writes/updates CODEX_HOME/config.toml notify command.
|
|
149
|
+
- Legacy: enables notify=true if config.yaml/config.json exist.
|
|
150
|
+
"""
|
|
151
|
+
codex_home = Path(codex_home).expanduser()
|
|
152
|
+
cmd = get_notify_hook_command_parts()
|
|
153
|
+
|
|
154
|
+
ok = False
|
|
155
|
+
toml_path = codex_home / "config.toml"
|
|
156
|
+
if _update_toml_linewise(toml_path, cmd=cmd):
|
|
157
|
+
ok = True
|
|
158
|
+
if not quiet:
|
|
159
|
+
print(f"[Aline] Codex notify hook installed: {toml_path}", file=sys.stderr)
|
|
160
|
+
_ensure_legacy_notify_enabled(codex_home)
|
|
161
|
+
return ok
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def ensure_global_codex_notify_hook_installed(*, quiet: bool = True) -> bool:
|
|
165
|
+
"""Best-effort: install notify hook into default global CODEX_HOME (~/.codex)."""
|
|
166
|
+
return ensure_notify_hook_installed_for_codex_home(Path.home() / ".codex", quiet=quiet)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def ensure_all_aline_codex_homes_notify_hook_installed(*, quiet: bool = True) -> int:
|
|
170
|
+
"""
|
|
171
|
+
Best-effort: install notify hook into every Aline-managed CODEX_HOME under ~/.aline/codex_homes.
|
|
172
|
+
Returns number of homes updated.
|
|
173
|
+
"""
|
|
174
|
+
try:
|
|
175
|
+
from ..codex_home import aline_codex_homes_dir
|
|
176
|
+
|
|
177
|
+
root = aline_codex_homes_dir()
|
|
178
|
+
except Exception:
|
|
179
|
+
root = Path.home() / ".aline" / "codex_homes"
|
|
180
|
+
|
|
181
|
+
if not root.exists():
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
updated = 0
|
|
185
|
+
for child in root.iterdir():
|
|
186
|
+
if not child.is_dir():
|
|
187
|
+
continue
|
|
188
|
+
# Layouts:
|
|
189
|
+
# - <terminal_id>/
|
|
190
|
+
# - agent-<id>/<terminal_id>/
|
|
191
|
+
if child.name.startswith("agent-"):
|
|
192
|
+
try:
|
|
193
|
+
for grandchild in child.iterdir():
|
|
194
|
+
if grandchild.is_dir():
|
|
195
|
+
if ensure_notify_hook_installed_for_codex_home(grandchild, quiet=quiet):
|
|
196
|
+
updated += 1
|
|
197
|
+
except Exception:
|
|
198
|
+
continue
|
|
199
|
+
else:
|
|
200
|
+
if ensure_notify_hook_installed_for_codex_home(child, quiet=quiet):
|
|
201
|
+
updated += 1
|
|
202
|
+
return updated
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def codex_cli_supports_notify_hook(*, timeout_seconds: float = 0.5) -> Optional[bool]:
|
|
206
|
+
"""
|
|
207
|
+
Best-effort detect whether the installed `codex` binary supports the Rust notify hook.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
- True: looks like Rust Codex CLI (supports config.toml + notify command)
|
|
211
|
+
- False: looks like legacy Codex (no reliable script notify hook)
|
|
212
|
+
- None: codex binary not found
|
|
213
|
+
"""
|
|
214
|
+
if shutil.which("codex") is None:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
def run(args: list[str]) -> str:
|
|
218
|
+
try:
|
|
219
|
+
proc = subprocess.run(
|
|
220
|
+
args,
|
|
221
|
+
text=True,
|
|
222
|
+
capture_output=True,
|
|
223
|
+
check=False,
|
|
224
|
+
timeout=float(timeout_seconds),
|
|
225
|
+
)
|
|
226
|
+
return f"{proc.stdout}\n{proc.stderr}".strip().lower()
|
|
227
|
+
except Exception:
|
|
228
|
+
return ""
|
|
229
|
+
|
|
230
|
+
help_out = run(["codex", "--help"])
|
|
231
|
+
ver_out = run(["codex", "--version"])
|
|
232
|
+
out = (help_out + "\n" + ver_out).lower()
|
|
233
|
+
|
|
234
|
+
# Strong positive indicators for Rust CLI.
|
|
235
|
+
if "config.toml" in out and "notify" in out:
|
|
236
|
+
return True
|
|
237
|
+
# Avoid false positives (e.g. "trusted" contains the substring "rust").
|
|
238
|
+
if "codex-rs" in out or re.search(r"\brust\b", out):
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
# Legacy indicators (YAML/JSON config keys from older docs/CLI).
|
|
242
|
+
if "config.yaml" in out or "approvalmode" in out or "fullautoerrormode" in out:
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
# Conservative default: if we cannot confirm, treat as unsupported so we don't
|
|
246
|
+
# claim Codex integration works when it won't.
|
|
247
|
+
return False
|
realign/commands/doctor.py
CHANGED
|
@@ -131,6 +131,27 @@ def _update_claude_hooks(*, verbose: bool) -> Tuple[list[str], list[str]]:
|
|
|
131
131
|
return hooks_updated, hooks_failed
|
|
132
132
|
|
|
133
133
|
|
|
134
|
+
def _update_codex_notify_hook(*, verbose: bool) -> Tuple[int, int]:
|
|
135
|
+
"""
|
|
136
|
+
Ensure Codex notify hook is installed:
|
|
137
|
+
- global ~/.codex
|
|
138
|
+
- all Aline-managed ~/.aline/codex_homes/*
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
from ..codex_hooks.notify_hook_installer import (
|
|
142
|
+
ensure_all_aline_codex_homes_notify_hook_installed,
|
|
143
|
+
ensure_global_codex_notify_hook_installed,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
ok_global = 1 if ensure_global_codex_notify_hook_installed(quiet=not verbose) else 0
|
|
147
|
+
ok_homes = int(ensure_all_aline_codex_homes_notify_hook_installed(quiet=not verbose))
|
|
148
|
+
return ok_global, ok_homes
|
|
149
|
+
except Exception as e:
|
|
150
|
+
if verbose:
|
|
151
|
+
console.print(f" [yellow]Codex notify hook update failed: {e}[/yellow]")
|
|
152
|
+
return 0, 0
|
|
153
|
+
|
|
154
|
+
|
|
134
155
|
def _update_skills(*, verbose: bool) -> int:
|
|
135
156
|
from .add import add_skills_command
|
|
136
157
|
|
|
@@ -302,6 +323,66 @@ def _check_llm_error_turns(
|
|
|
302
323
|
db.close()
|
|
303
324
|
|
|
304
325
|
|
|
326
|
+
def _check_watcher_backlog(
|
|
327
|
+
config: ReAlignConfig,
|
|
328
|
+
*,
|
|
329
|
+
verbose: bool,
|
|
330
|
+
fix: bool,
|
|
331
|
+
) -> Tuple[int, int]:
|
|
332
|
+
"""
|
|
333
|
+
Check for backlog sessions that changed since the watcher last run.
|
|
334
|
+
|
|
335
|
+
Uses the same 2-phase startup scan logic as the watcher:
|
|
336
|
+
1) stat previously persisted session paths (fast)
|
|
337
|
+
2) full scan of watch paths (complete)
|
|
338
|
+
"""
|
|
339
|
+
from ..db.sqlite_db import SQLiteDatabase
|
|
340
|
+
from ..watcher_core import DialogueWatcher
|
|
341
|
+
|
|
342
|
+
db_path = Path(config.sqlite_db_path).expanduser()
|
|
343
|
+
if not db_path.exists():
|
|
344
|
+
return 0, 0
|
|
345
|
+
|
|
346
|
+
watcher = DialogueWatcher()
|
|
347
|
+
candidates, _sizes, _mtimes, report = watcher._startup_scan_collect_candidates()
|
|
348
|
+
|
|
349
|
+
if verbose:
|
|
350
|
+
console.print(
|
|
351
|
+
f" [dim]Tracked paths: {report.prev_paths} (missing: {report.prev_missing}, changed: {report.prev_changed})[/dim]"
|
|
352
|
+
)
|
|
353
|
+
console.print(
|
|
354
|
+
f" [dim]Scan paths: {report.scan_paths} (new: {report.scan_new}, changed: {report.scan_changed})[/dim]"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if not candidates:
|
|
358
|
+
return 0, 0
|
|
359
|
+
|
|
360
|
+
if not fix:
|
|
361
|
+
return len(candidates), 0
|
|
362
|
+
|
|
363
|
+
db = SQLiteDatabase(str(db_path))
|
|
364
|
+
try:
|
|
365
|
+
enqueued = 0
|
|
366
|
+
for session_file in candidates:
|
|
367
|
+
try:
|
|
368
|
+
db.enqueue_session_process_job(
|
|
369
|
+
session_file_path=session_file,
|
|
370
|
+
session_id=session_file.stem,
|
|
371
|
+
workspace_path=None,
|
|
372
|
+
session_type=watcher._detect_session_type(session_file),
|
|
373
|
+
source_event="doctor_backlog_scan",
|
|
374
|
+
priority=getattr(watcher, "_startup_scan_priority", 5),
|
|
375
|
+
)
|
|
376
|
+
enqueued += 1
|
|
377
|
+
except Exception as e:
|
|
378
|
+
if verbose:
|
|
379
|
+
console.print(f" [yellow]Failed to enqueue {session_file}: {e}[/yellow]")
|
|
380
|
+
continue
|
|
381
|
+
return len(candidates), enqueued
|
|
382
|
+
finally:
|
|
383
|
+
db.close()
|
|
384
|
+
|
|
385
|
+
|
|
305
386
|
def run_doctor(
|
|
306
387
|
*,
|
|
307
388
|
restart_daemons: bool,
|
|
@@ -389,6 +470,34 @@ def run_doctor(
|
|
|
389
470
|
if hooks_failed:
|
|
390
471
|
console.print(f" [yellow]![/yellow] Failed hooks: {', '.join(hooks_failed)}")
|
|
391
472
|
|
|
473
|
+
# 3b. Update Codex notify hook
|
|
474
|
+
console.print("\n[bold]3b. Updating Codex notify hook...[/bold]")
|
|
475
|
+
try:
|
|
476
|
+
ok_global, ok_homes = _update_codex_notify_hook(verbose=verbose)
|
|
477
|
+
if ok_global:
|
|
478
|
+
console.print(" [green]✓[/green] Updated global Codex config (~/.codex)")
|
|
479
|
+
else:
|
|
480
|
+
console.print(" [dim]Global Codex config not updated (may be missing or unwritable)[/dim]")
|
|
481
|
+
if ok_homes:
|
|
482
|
+
console.print(f" [green]✓[/green] Updated {ok_homes} Aline CODEX_HOME(s)")
|
|
483
|
+
else:
|
|
484
|
+
console.print(" [dim]No Aline CODEX_HOME(s) updated[/dim]")
|
|
485
|
+
|
|
486
|
+
# If Codex exists but is legacy, warn: Aline expects the Rust notify hook.
|
|
487
|
+
try:
|
|
488
|
+
from ..codex_hooks.notify_hook_installer import codex_cli_supports_notify_hook
|
|
489
|
+
|
|
490
|
+
supported = codex_cli_supports_notify_hook()
|
|
491
|
+
if supported is False:
|
|
492
|
+
console.print(" [yellow]![/yellow] Codex CLI does not support notify hook.")
|
|
493
|
+
console.print(
|
|
494
|
+
" [dim]Tip: update to the Rust Codex CLI to enable reliable, event-driven Codex imports.[/dim]"
|
|
495
|
+
)
|
|
496
|
+
except Exception:
|
|
497
|
+
pass
|
|
498
|
+
except Exception as e:
|
|
499
|
+
console.print(f" [yellow]![/yellow] Codex notify hook update failed: {e}")
|
|
500
|
+
|
|
392
501
|
# 4. Update skills
|
|
393
502
|
console.print("\n[bold]4. Updating skills...[/bold]")
|
|
394
503
|
try:
|
|
@@ -446,6 +555,22 @@ def run_doctor(
|
|
|
446
555
|
except Exception as e:
|
|
447
556
|
console.print(f" [yellow]![/yellow] Failed to repair associations: {e}")
|
|
448
557
|
|
|
558
|
+
# 5c. Check backlog sessions (watcher startup scan semantics)
|
|
559
|
+
console.print("\n[bold]5c. Checking watcher backlog sessions...[/bold]")
|
|
560
|
+
try:
|
|
561
|
+
backlog_count, _ = _check_watcher_backlog(config, verbose=verbose, fix=False)
|
|
562
|
+
if backlog_count == 0:
|
|
563
|
+
console.print(" [green]✓[/green] No backlog sessions detected")
|
|
564
|
+
else:
|
|
565
|
+
console.print(f" [yellow]![/yellow] Found {backlog_count} backlog session(s) to process")
|
|
566
|
+
if auto_fix or typer.confirm("\n Do you want to enqueue these for processing now?", default=True):
|
|
567
|
+
_, enqueued = _check_watcher_backlog(config, verbose=verbose, fix=True)
|
|
568
|
+
console.print(f" [green]✓[/green] Enqueued {enqueued} session_process job(s)")
|
|
569
|
+
else:
|
|
570
|
+
console.print(" [dim]Skipped enqueueing backlog sessions[/dim]")
|
|
571
|
+
except Exception as e:
|
|
572
|
+
console.print(f" [yellow]![/yellow] Failed to check watcher backlog: {e}")
|
|
573
|
+
|
|
449
574
|
# 6. Restart/ensure daemons
|
|
450
575
|
if restart_daemons:
|
|
451
576
|
console.print("\n[bold]6. Checking daemons...[/bold]")
|