cc-os/plugins/os-status/hooks/session_start.py

117 lines
3.7 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""session_start.py — aggregated deterministic status pass for the cc-os
plugin family. Runs every registered check in-process (checks.REGISTRY) and
emits at most one warning banner plus any near-zero-token notes.
ok -> silent
note -> additionalContext line (never snoozed or suppressed)
warn -> one line in the single systemMessage banner; snoozed once per day
and permanently suppressible via .cc-os/suppress-<check>
Outside a git project only environment-scoped checks run and no state is
written. Always exits 0 the hook must never block a session.
"""
from __future__ import annotations
import json
import os
import signal
import sys
from datetime import date
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from checks import DEFAULT_VAULT_PATH, REGISTRY, CheckResult, Ctx # noqa: E402
from state import StateDir, find_project_root, read_config # noqa: E402
CHECK_BUDGET_SECONDS = 2
BANNER_HEADER = (
"[os-status] Session preconditions need attention (run /os-status:fix to"
" address all of these):"
)
def run_isolated(check, ctx) -> CheckResult:
"""Failure isolation: an exception or a blown time budget becomes a
generic warn naming the check; the remaining checks keep running."""
def timed_out(signum, frame):
raise TimeoutError
alarmed = hasattr(signal, "SIGALRM")
if alarmed:
previous = signal.signal(signal.SIGALRM, timed_out)
signal.alarm(CHECK_BUDGET_SECONDS)
try:
result = check.fn(ctx)
if not isinstance(result, CheckResult):
raise TypeError("check returned a non-CheckResult")
return result
except Exception:
return CheckResult("warn", f"status check '{check.name}' failed to run")
finally:
if alarmed:
signal.alarm(0)
signal.signal(signal.SIGALRM, previous)
def run(ctx: Ctx, state, today: date, out) -> None:
notes, warns = [], []
for check in REGISTRY:
if check.project_scoped and ctx.project_root is None:
continue
result = run_isolated(check, ctx)
if result.status == "note":
notes.append(result.message)
elif result.status == "warn":
if state and (
state.suppressed(check.name) or state.snoozed(check.name, today)
):
continue
warns.append((check.name, result.message))
payload = {}
if warns:
lines = [BANNER_HEADER]
lines.extend(f" - {message}" for _, message in warns)
if state:
names = ", ".join(name for name, _ in warns)
lines.append(
f" (silence one permanently: touch .cc-os/suppress-<name>; warned: {names})"
)
for name, _ in warns:
state.stamp(name, today)
payload["systemMessage"] = "\n".join(lines)
if notes:
payload["hookSpecificOutput"] = {
"hookEventName": "SessionStart",
"additionalContext": "\n".join(notes),
}
if payload:
out.write(json.dumps(payload))
def main() -> int:
try:
root = find_project_root(Path.cwd())
config = read_config(root) if root else {}
ctx = Ctx(
project_root=root,
settings_path=Path.home() / ".claude" / "settings.json",
vault_path=Path(
os.path.expanduser(config.get("vault_path", DEFAULT_VAULT_PATH))
),
config=config,
environ=dict(os.environ),
)
state = StateDir(root) if root else None
run(ctx, state, date.today(), sys.stdout)
except Exception:
pass # never block a session
return 0
if __name__ == "__main__":
sys.exit(main())