kin: KIN-083 Healthcheck claude CLI auth: перед запуском pipeline проверять что claude залогинен (быстрый claude -p 'ok' --output-format json, проверить is_error и 'Not logged in'). Если не залогинен — не запускать pipeline, а показать ошибку 'Claude CLI requires login' в GUI с инструкцией.
This commit is contained in:
parent
a80679ae72
commit
bfc8f1c0bb
18 changed files with 1390 additions and 57 deletions
|
|
@ -213,7 +213,7 @@ def detect_modules(project_path: Path) -> list[dict]:
|
|||
if not child.is_dir() or child.name in _SKIP_DIRS or child.name.startswith("."):
|
||||
continue
|
||||
mod = _analyze_module(child, project_path)
|
||||
key = (mod["name"], mod["path"])
|
||||
key = mod["name"]
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
modules.append(mod)
|
||||
|
|
|
|||
109
agents/runner.py
109
agents/runner.py
|
|
@ -68,6 +68,54 @@ from core.context_builder import build_context, format_prompt
|
|||
from core.hooks import run_hooks
|
||||
|
||||
|
||||
class ClaudeAuthError(Exception):
|
||||
"""Raised when Claude CLI is not authenticated or not available."""
|
||||
pass
|
||||
|
||||
|
||||
def check_claude_auth(timeout: int = 10) -> None:
|
||||
"""Check that claude CLI is authenticated before running a pipeline.
|
||||
|
||||
Runs: claude -p 'ok' --output-format json --no-verbose with timeout.
|
||||
Returns None if auth is confirmed.
|
||||
Raises ClaudeAuthError if:
|
||||
- claude CLI not found in PATH (FileNotFoundError)
|
||||
- stdout/stderr contains 'not logged in' (case-insensitive)
|
||||
- returncode != 0
|
||||
- is_error=true in parsed JSON output
|
||||
Returns silently on TimeoutExpired (ambiguous — don't block pipeline).
|
||||
"""
|
||||
claude_cmd = _resolve_claude_cmd()
|
||||
env = _build_claude_env()
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[claude_cmd, "-p", "ok", "--output-format", "json", "--no-verbose"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
env=env,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
raise ClaudeAuthError("claude CLI not found in PATH. Install it or add to PATH.")
|
||||
except subprocess.TimeoutExpired:
|
||||
return # Ambiguous — don't block pipeline on timeout
|
||||
|
||||
stdout = proc.stdout or ""
|
||||
stderr = proc.stderr or ""
|
||||
combined = stdout + stderr
|
||||
|
||||
if "not logged in" in combined.lower():
|
||||
raise ClaudeAuthError("Claude CLI requires login. Run: claude login")
|
||||
|
||||
if proc.returncode != 0:
|
||||
raise ClaudeAuthError("Claude CLI requires login. Run: claude login")
|
||||
|
||||
parsed = _try_parse_json(stdout)
|
||||
if isinstance(parsed, dict) and parsed.get("is_error"):
|
||||
raise ClaudeAuthError("Claude CLI requires login. Run: claude login")
|
||||
|
||||
|
||||
def run_agent(
|
||||
conn: sqlite3.Connection,
|
||||
role: str,
|
||||
|
|
@ -467,6 +515,37 @@ def _is_permission_error(result: dict) -> bool:
|
|||
# Autocommit: git add -A && git commit after successful pipeline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _get_changed_files(project_path: str) -> list[str]:
|
||||
"""Return files changed in the current pipeline run.
|
||||
|
||||
Combines unstaged changes, staged changes, and the last commit diff
|
||||
to cover both autocommit-on and autocommit-off scenarios.
|
||||
Returns [] on any git error (e.g. no git repo, first commit).
|
||||
"""
|
||||
env = _build_claude_env()
|
||||
git_cmd = shutil.which("git", path=env["PATH"]) or "git"
|
||||
files: set[str] = set()
|
||||
for git_args in (
|
||||
["diff", "--name-only"], # unstaged tracked changes
|
||||
["diff", "--cached", "--name-only"], # staged changes
|
||||
["diff", "HEAD~1", "HEAD", "--name-only"], # last commit (post-autocommit)
|
||||
):
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[git_cmd] + git_args,
|
||||
cwd=project_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
env=env,
|
||||
)
|
||||
if r.returncode == 0:
|
||||
files.update(f.strip() for f in r.stdout.splitlines() if f.strip())
|
||||
except Exception:
|
||||
pass
|
||||
return list(files)
|
||||
|
||||
|
||||
def _run_autocommit(
|
||||
conn: sqlite3.Connection,
|
||||
task_id: str,
|
||||
|
|
@ -582,7 +661,7 @@ def _save_sysadmin_output(
|
|||
if not m_name:
|
||||
continue
|
||||
try:
|
||||
models.add_module(
|
||||
m = models.add_module(
|
||||
conn,
|
||||
project_id=project_id,
|
||||
name=m_name,
|
||||
|
|
@ -591,7 +670,10 @@ def _save_sysadmin_output(
|
|||
description=item.get("description"),
|
||||
owner_role="sysadmin",
|
||||
)
|
||||
modules_added += 1
|
||||
if m.get("_created", True):
|
||||
modules_added += 1
|
||||
else:
|
||||
modules_skipped += 1
|
||||
except Exception:
|
||||
modules_skipped += 1
|
||||
|
||||
|
|
@ -739,6 +821,18 @@ def run_pipeline(
|
|||
|
||||
Returns {success, steps_completed, total_cost, total_tokens, total_duration, results}
|
||||
"""
|
||||
# Auth check — skip for dry_run (dry_run never calls claude CLI)
|
||||
if not dry_run:
|
||||
try:
|
||||
check_claude_auth()
|
||||
except ClaudeAuthError as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "claude_auth_required",
|
||||
"message": str(exc),
|
||||
"instructions": "Run: claude login",
|
||||
}
|
||||
|
||||
task = models.get_task(conn, task_id)
|
||||
if not task:
|
||||
return {"success": False, "error": f"Task '{task_id}' not found"}
|
||||
|
|
@ -979,6 +1073,14 @@ def run_pipeline(
|
|||
|
||||
task_modules = models.get_modules(conn, project_id)
|
||||
|
||||
# Compute changed files for hook filtering (frontend build trigger)
|
||||
changed_files: list[str] | None = None
|
||||
project = models.get_project(conn, project_id)
|
||||
if project and project.get("path"):
|
||||
p_path = Path(project["path"]).expanduser()
|
||||
if p_path.is_dir():
|
||||
changed_files = _get_changed_files(str(p_path))
|
||||
|
||||
last_role = steps[-1].get("role", "") if steps else ""
|
||||
auto_eligible = last_role in {"tester", "reviewer"}
|
||||
|
||||
|
|
@ -1018,7 +1120,8 @@ def run_pipeline(
|
|||
# Run post-pipeline hooks (failures don't affect pipeline status)
|
||||
try:
|
||||
run_hooks(conn, project_id, task_id,
|
||||
event="pipeline_completed", task_modules=task_modules)
|
||||
event="pipeline_completed", task_modules=task_modules,
|
||||
changed_files=changed_files)
|
||||
except Exception:
|
||||
pass # Hook errors must never block pipeline completion
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue