Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1a56ba1
feat: Implement comprehensive system health checks for #128
hyaku0121 Dec 11, 2025
dc54e53
feat: Add health monitor core logic, CLI integration, and unit tests
hyaku0121 Dec 11, 2025
0889c76
fix: Add timeouts to subprocess calls to improve reliability
hyaku0121 Dec 11, 2025
fd88be0
refactor: Address code review feedback (docstrings, timeouts, complex…
hyaku0121 Dec 11, 2025
f4ced43
refactor: Improve security check complexity and SSH parsing logic
hyaku0121 Dec 11, 2025
f87bd8c
fix: Resolve SonarCloud code smells and reduce complexity
hyaku0121 Dec 11, 2025
2e950b1
docs: Add missing docstrings to HealthMonitor public APIs
hyaku0121 Dec 11, 2025
8240944
fix: Address SonarCloud and CodeRabbit feedback (redundant exceptions…
hyaku0121 Dec 11, 2025
db3bf19
fix: improve CodeQL compliance in health checks
hyaku0121 Dec 17, 2025
6f2d8a3
style: fix ruff linter errors in health module
hyaku0121 Dec 17, 2025
fa3b020
style: fix W292 and W293 ruff errors
hyaku0121 Dec 17, 2025
14b5158
fix: secure verify_ubuntu_compatibility.py and fix ruff errors
hyaku0121 Dec 17, 2025
ee2e8e6
fix: resolve syntax error in cli.py and missing newline
hyaku0121 Dec 17, 2025
9582f3b
style: apply black formatting to remaining files
hyaku0121 Dec 17, 2025
dea2609
fix: correct expected score in test_apt_updates test
hyaku0121 Dec 17, 2025
ef9a253
fix: correct test assertion and restore files to main state
hyaku0121 Dec 17, 2025
7a6d42d
style: fix import sorting in cli.py
Dec 20, 2025
127bb75
style: fix black formatting in intent module
Dec 20, 2025
f974bd6
Merge branch 'main' into feature/health-score-128
hyaku0121 Dec 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 91 additions & 14 deletions cortex/cli.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import argparse
import logging
import os
import sys
import time
from datetime import datetime
from typing import Any

from cortex.branding import VERSION, console, cx_header, cx_print, show_banner
from cortex.coordinator import InstallationCoordinator, StepStatus
from cortex.installation_history import (
InstallationHistory,
InstallationStatus,
InstallationType,
)
from cortex.demo import run_demo
from cortex.installation_history import InstallationHistory, InstallationStatus, InstallationType

Check failure on line 17 in cortex/cli.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (F811)

cortex/cli.py:17:82: F811 Redefinition of unused `InstallationType` from line 14

Check failure on line 17 in cortex/cli.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (F811)

cortex/cli.py:17:62: F811 Redefinition of unused `InstallationStatus` from line 13

Check failure on line 17 in cortex/cli.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (F811)

cortex/cli.py:17:41: F811 Redefinition of unused `InstallationHistory` from line 12
Comment on lines +11 to 17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix duplicate import causing CI failure.

Lines 11-15 import InstallationHistory, InstallationStatus, InstallationType from cortex.installation_history, but line 17 imports the same symbols again. This causes the Ruff I001 error (import block is unsorted/unformatted) and is blocking CI.

🔎 Proposed fix

Remove the duplicate import on line 17:

 from cortex.installation_history import (
     InstallationHistory,
     InstallationStatus,
     InstallationType,
 )
 from cortex.demo import run_demo
-from cortex.installation_history import InstallationHistory, InstallationStatus, InstallationType
 from cortex.llm.interpreter import CommandInterpreter
🧰 Tools
🪛 GitHub Check: Lint

[failure] 17-17: Ruff (F811)
cortex/cli.py:17:82: F811 Redefinition of unused InstallationType from line 14


[failure] 17-17: Ruff (F811)
cortex/cli.py:17:62: F811 Redefinition of unused InstallationStatus from line 13


[failure] 17-17: Ruff (F811)
cortex/cli.py:17:41: F811 Redefinition of unused InstallationHistory from line 12

🤖 Prompt for AI Agents
In cortex/cli.py around lines 11 to 17, there is a duplicate import of
InstallationHistory, InstallationStatus, and InstallationType from
cortex.installation_history (lines 11-15 and again on line 17) causing a Ruff
I001 import-block error; remove the redundant import on line 17 so each symbol
is imported only once and ensure the remaining imports are properly
sorted/formatted.

from cortex.llm.interpreter import CommandInterpreter
from cortex.notification_manager import NotificationManager
from cortex.stack_manager import StackManager
Expand Down Expand Up @@ -101,10 +106,9 @@
sys.stdout.write("\r\033[K")
sys.stdout.flush()

# --- New Notification Method ---
# --- Notification Method ---
def notify(self, args):
"""Handle notification commands"""
# Addressing CodeRabbit feedback: Handle missing subcommand gracefully
if not args.notify_action:
self._print_error("Please specify a subcommand (config/enable/disable/dnd/send)")
return 1
Expand All @@ -127,24 +131,22 @@

elif args.notify_action == "enable":
mgr.config["enabled"] = True
# Addressing CodeRabbit feedback: Ideally should use a public method instead of private _save_config,
# but keeping as is for a simple fix (or adding a save method to NotificationManager would be best).
mgr._save_config()
mgr.save_config()
self._print_success("Notifications enabled")
return 0

elif args.notify_action == "disable":
mgr.config["enabled"] = False
mgr._save_config()
cx_print("Notifications disabled (Critical alerts will still show)", "warning")
mgr.save_config()
self._print_success("Notifications disabled (Critical alerts will still show)")
return 0

elif args.notify_action == "dnd":
if not args.start or not args.end:
self._print_error("Please provide start and end times (HH:MM)")
return 1

# Addressing CodeRabbit feedback: Add time format validation
# Add time format validation
try:
datetime.strptime(args.start, "%H:%M")
datetime.strptime(args.end, "%H:%M")
Expand All @@ -154,7 +156,7 @@

mgr.config["dnd_start"] = args.start
mgr.config["dnd_end"] = args.end
mgr._save_config()
mgr.save_config()
self._print_success(f"DND Window updated: {args.start} - {args.end}")
return 0

Expand All @@ -170,6 +172,76 @@
self._print_error("Unknown notify command")
return 1

# --- Health Command ---
def health(self, args):
"""Run system health checks and show recommendations.

Args:
args: Parsed command line arguments.

Returns:
int: 0 on success, 1 on failure.
"""
try:
from cortex.health.monitor import HealthMonitor

self._print_status("🔍", "Running system health checks...")
monitor = HealthMonitor()
report = monitor.run_all()

# --- Display Results ---
score = report["total_score"]

# Color code the score
score_color = "green"
if score < 60:
score_color = "red"
elif score < 80:
score_color = "yellow"

console.print()
console.print(
f"📊 [bold]System Health Score:[/bold] [{score_color}]{score}/100[/{score_color}]"
)
console.print()

console.print("[bold]Factors:[/bold]")
recommendations = []

for res in report["results"]:
status_icon = "✅"
if res["status"] == "WARNING":
status_icon = "⚠️ "
elif res["status"] == "CRITICAL":
status_icon = "❌"

console.print(
f" {status_icon} {res['name']:<15}: {res['score']}/100 ({res['details']})"
)

if res["recommendation"]:
recommendations.append(res["recommendation"])

console.print()

if recommendations:
console.print("[bold]Recommendations:[/bold]")
for i, rec in enumerate(recommendations, 1):
console.print(f" {i}. {rec}")

console.print()
console.print("[dim]Run suggested commands manually to improve your score.[/dim]")
else:
self._print_success("System is in excellent health! No actions needed.")

return 0
except ImportError as e:
self._print_error(f"Health module not available: {e}")
return 1
except Exception as e:
self._print_error(f"Health check failed: {e}")
return 1

# -------------------------------
def demo(self):
"""
Expand Down Expand Up @@ -462,7 +534,7 @@

coordinator = InstallationCoordinator(
commands=commands,
descriptions=[f"Step {i+1}" for i in range(len(commands))],
descriptions=[f"Step {i + 1}" for i in range(len(commands))],
timeout=300,
stop_on_error=True,
progress_callback=progress_callback,
Expand Down Expand Up @@ -591,7 +663,7 @@
date = r.timestamp[:19].replace("T", " ")
packages = ", ".join(r.packages[:2])
if len(r.packages) > 2:
packages += f" +{len(r.packages)-2}"
packages += f" +{len(r.packages) - 2}"

print(
f"{r.id:<18} {date:<20} {r.operation_type.value:<12} {packages:<30} {r.status.value:<15}"
Expand Down Expand Up @@ -767,9 +839,10 @@
table.add_row("install <pkg>", "Install software")
table.add_row("history", "View history")
table.add_row("rollback <id>", "Undo installation")
table.add_row("notify", "Manage desktop notifications") # Added this line
table.add_row("notify", "Manage desktop notifications")
table.add_row("cache stats", "Show LLM cache statistics")
table.add_row("stack <name>", "Install the stack")
table.add_row("health", "Check system health score")
table.add_row("doctor", "System health check")

console.print(table)
Expand Down Expand Up @@ -858,7 +931,7 @@
edit_pref_parser.add_argument("key", nargs="?")
edit_pref_parser.add_argument("value", nargs="?")

# --- New Notify Command ---
# --- Notify Command ---
notify_parser = subparsers.add_parser("notify", help="Manage desktop notifications")
notify_subs = notify_parser.add_subparsers(dest="notify_action", help="Notify actions")

Expand All @@ -875,6 +948,9 @@
send_parser.add_argument("--title", default="Cortex Notification")
send_parser.add_argument("--level", choices=["low", "normal", "critical"], default="normal")
send_parser.add_argument("--actions", nargs="*", help="Action buttons")

# --- Health Command ---
subparsers.add_parser("health", help="Check system health score")
# --------------------------

# Stack command
Expand Down Expand Up @@ -924,9 +1000,10 @@
return cli.check_pref(key=args.key)
elif args.command == "edit-pref":
return cli.edit_pref(action=args.action, key=args.key, value=args.value)
# Handle the new notify command
elif args.command == "notify":
return cli.notify(args)
elif args.command == "health":
return cli.health(args)
elif args.command == "stack":
return cli.stack(args)
elif args.command == "doctor":
Expand Down
Empty file added cortex/health/__init__.py
Empty file.
61 changes: 61 additions & 0 deletions cortex/health/checks/disk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import shutil

from ..monitor import CheckResult, HealthCheck


class DiskCheck(HealthCheck):
"""Check root filesystem disk usage."""

def run(self) -> CheckResult:
"""Calculate disk usage percentage.

Returns:
CheckResult based on usage thresholds.
"""
try:
# Use _ for unused variable (free space)
total, used, _ = shutil.disk_usage("/")
usage_percent = (used / total) * 100
except Exception as e:
return CheckResult(
name="Disk Usage",
category="disk",
score=0,
status="CRITICAL",
details=f"Check failed: {e}",
recommendation="Check disk mounts and permissions",
weight=0.20,
)

# Explicit early returns to avoid static analysis confusion
if usage_percent > 90:
return CheckResult(
name="Disk Usage",
category="disk",
score=0,
status="CRITICAL",
details=f"{usage_percent:.1f}% used",
recommendation="Clean up disk space immediately",
weight=0.20,
)

if usage_percent > 80:
return CheckResult(
name="Disk Usage",
category="disk",
score=50,
status="WARNING",
details=f"{usage_percent:.1f}% used",
recommendation="Consider cleaning up disk space",
weight=0.20,
)

return CheckResult(
name="Disk Usage",
category="disk",
score=100,
status="OK",
details=f"{usage_percent:.1f}% used",
recommendation=None,
weight=0.20,
)
72 changes: 72 additions & 0 deletions cortex/health/checks/performance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import multiprocessing
import os

from ..monitor import CheckResult, HealthCheck


class PerformanceCheck(HealthCheck):
"""Check system performance metrics including CPU load and memory usage."""

def run(self) -> CheckResult:
"""Check system load and memory usage.

Returns:
CheckResult with performance score.
"""
score = 100
issues = []
rec = None

# 1. Load Average (1min)
try:
load1, _, _ = os.getloadavg()
cores = multiprocessing.cpu_count()
# Load ratio against core count
load_ratio = load1 / cores

if load_ratio > 1.0:
score -= 50
issues.append(f"High Load ({load1:.2f})")
rec = "Check top processes"
except OSError:
pass # Skip on Windows etc.

# 2. Memory Usage (Linux /proc/meminfo)
try:
with open("/proc/meminfo") as f:
meminfo = {}
for line in f:
parts = line.split(":")
if len(parts) == 2:
meminfo[parts[0].strip()] = int(parts[1].strip().split()[0])

if "MemTotal" in meminfo and "MemAvailable" in meminfo:
total = meminfo["MemTotal"]
avail = meminfo["MemAvailable"]
used_percent = ((total - avail) / total) * 100

if used_percent > 80:
penalty = int(used_percent - 80)
score -= penalty
issues.append(f"High Memory ({used_percent:.0f}%)")
except FileNotFoundError:
pass # Non-Linux systems

# Summary of results
status = "OK"
if score < 50:
status = "CRITICAL"
elif score < 90:
status = "WARNING"

details = ", ".join(issues) if issues else "Optimal"

return CheckResult(
name="System Load",
category="performance",
score=max(0, score),
status=status,
details=details,
recommendation=rec,
weight=0.20, # 20%
)
Loading
Loading