@misterhuydo/sentinel 1.4.49 → 1.4.50

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-25T11:16:59.180Z",
3
- "checkpoint_at": "2026-03-25T11:16:59.181Z",
2
+ "message": "Auto-checkpoint at 2026-03-25T11:24:19.838Z",
3
+ "checkpoint_at": "2026-03-25T11:24:19.839Z",
4
4
  "active_files": [],
5
5
  "notes": [],
6
6
  "mtime_snapshot": {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.4.49",
3
+ "version": "1.4.50",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -21,6 +21,13 @@ logger = logging.getLogger(__name__)
21
21
 
22
22
  GIT_TIMEOUT = 60
23
23
 
24
+
25
+ class MissingToolError(Exception):
26
+ """Raised when a required build tool (e.g. mvn, gradle) is not installed."""
27
+ def __init__(self, tool: str):
28
+ super().__init__(f"Build tool '{tool}' not found on this server")
29
+ self.tool = tool
30
+
24
31
  # Files that must never be modified by Sentinel
25
32
  _PROTECTED_PATHS = {".github/", "Jenkinsfile", "pom.xml"}
26
33
 
@@ -127,8 +134,7 @@ def _run_tests(repo: RepoConfig, local_path: str) -> bool:
127
134
  try:
128
135
  r = subprocess.run(cmd, cwd=local_path, capture_output=True, text=True, timeout=300)
129
136
  except FileNotFoundError:
130
- logger.warning("Test runner '%s' not found on this server — skipping tests for %s", cmd[0], repo.repo_name)
131
- return True
137
+ raise MissingToolError(cmd[0])
132
138
  if r.returncode != 0:
133
139
  logger.error("Tests failed:\n%s", r.stdout[-2000:] + r.stderr[-1000:])
134
140
  return False
@@ -22,14 +22,14 @@ from pathlib import Path
22
22
  from .cairn_client import ensure_installed as cairn_installed, index_repo
23
23
  from .config_loader import ConfigLoader, SentinelConfig
24
24
  from .fix_engine import generate_fix
25
- from .git_manager import apply_and_commit, publish, _git_env
25
+ from .git_manager import apply_and_commit, publish, _git_env, MissingToolError
26
26
  from .cicd_trigger import trigger as cicd_trigger
27
27
  from .log_fetcher import fetch_all
28
28
  from .log_parser import parse_all, scan_all_for_markers, ErrorEvent
29
29
  from .issue_watcher import scan_issues, mark_done, IssueEvent
30
30
  from .repo_router import route
31
31
  from .reporter import build_and_send, send_fix_notification, send_failure_notification, send_confirmed_notification, send_regression_notification, send_startup_notification, send_upgrade_notification
32
- from .notify import notify_fix_blocked, notify_fix_applied
32
+ from .notify import notify_fix_blocked, notify_fix_applied, notify_missing_tool
33
33
  from .health_checker import evaluate_repos
34
34
  from .state_store import StateStore
35
35
 
@@ -106,7 +106,13 @@ async def _handle_error(event: ErrorEvent, cfg_loader: ConfigLoader, store: Stat
106
106
  })
107
107
  return
108
108
 
109
- commit_status, commit_hash = apply_and_commit(event, patch_path, repo, sentinel)
109
+ try:
110
+ commit_status, commit_hash = apply_and_commit(event, patch_path, repo, sentinel)
111
+ except MissingToolError as e:
112
+ logger.warning("Missing tool for %s: %s — notifying admin", event.source, e)
113
+ notify_missing_tool(sentinel, e.tool, repo.repo_name, event.source, "")
114
+ store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
115
+ return
110
116
  if commit_status != "committed":
111
117
  store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
112
118
  send_failure_notification(sentinel, {
@@ -244,6 +250,14 @@ async def _handle_issue(event: IssueEvent, cfg_loader: ConfigLoader, store: Stat
244
250
  if repo.auto_publish:
245
251
  cicd_trigger(repo, store, event.fingerprint)
246
252
 
253
+ except MissingToolError as e:
254
+ logger.warning("Missing tool for %s: %s — notifying admin", event.source, e)
255
+ submitter_uid = getattr(event, "submitter_user_id", "")
256
+ notify_missing_tool(sentinel, e.tool, repo.repo_name, event.source, submitter_uid)
257
+ # Archive the issue — admin can re-raise it after installing the tool
258
+ store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
259
+ mark_done(event.issue_file)
260
+
247
261
  except Exception:
248
262
  logger.exception("Unexpected error processing issue %s — archiving to prevent retry loop", event.source)
249
263
  store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
@@ -230,6 +230,34 @@ def notify_fix_blocked(
230
230
  logger.warning("notify_fix_blocked: email notification failed: %s", exc)
231
231
 
232
232
 
233
+ def notify_missing_tool(
234
+ cfg,
235
+ tool: str,
236
+ repo_name: str,
237
+ source: str,
238
+ submitter_user_id: str = "",
239
+ ) -> None:
240
+ """
241
+ Notify admins that a build tool is missing on this server.
242
+ Prompts them to ask Boss to install it.
243
+ """
244
+ repo_line = f" for *{repo_name}*" if repo_name else ""
245
+ slack_text = (
246
+ f":wrench: *Build tool missing{repo_line}*\n"
247
+ f"*Source:* {source}\n"
248
+ f"The fix was generated but tests couldn't run because `{tool}` is not installed on this server.\n\n"
249
+ f"Ask me to install it:\n"
250
+ f"> @Sentinel install {tool}\n\n"
251
+ f"Once installed, re-raise the issue to apply the fix."
252
+ )
253
+ if submitter_user_id:
254
+ if getattr(cfg, "slack_dm_submitter", True):
255
+ slack_dm(cfg.slack_bot_token, submitter_user_id, slack_text)
256
+ slack_alert(cfg.slack_bot_token, cfg.slack_channel, f"<@{submitter_user_id}> {slack_text}")
257
+ else:
258
+ slack_alert(cfg.slack_bot_token, cfg.slack_channel, f"<!channel> {slack_text}")
259
+
260
+
233
261
  def notify_fix_applied(
234
262
  cfg,
235
263
  source: str,
@@ -931,6 +931,25 @@ _TOOLS = [
931
931
  "required": ["repo_name"],
932
932
  },
933
933
  },
934
+ {
935
+ "name": "install_tool",
936
+ "description": (
937
+ "ADMIN ONLY. Install a missing build tool (e.g. maven, gradle, node) on this server "
938
+ "using Claude Code with shell execution rights. "
939
+ "Use after Sentinel reports a missing tool error. "
940
+ "e.g. 'install maven', 'install gradle', '@Sentinel install mvn'"
941
+ ),
942
+ "input_schema": {
943
+ "type": "object",
944
+ "properties": {
945
+ "tool_name": {
946
+ "type": "string",
947
+ "description": "The tool to install, e.g. 'maven', 'gradle', 'node'",
948
+ },
949
+ },
950
+ "required": ["tool_name"],
951
+ },
952
+ },
934
953
  {
935
954
  "name": "set_maintenance",
936
955
  "description": (
@@ -2193,7 +2212,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2193
2212
  return json.dumps({"error": "cannot determine user — not clearing"})
2194
2213
 
2195
2214
  # ── Admin-only tools ──────────────────────────────────────────────────────
2196
- _ADMIN_TOOLS = {"list_all_users", "clear_user_history", "reset_fingerprint", "list_all_errors", "export_db", "merge_pr"}
2215
+ _ADMIN_TOOLS = {"list_all_users", "clear_user_history", "reset_fingerprint", "list_all_errors", "export_db", "merge_pr", "install_tool"}
2197
2216
  if name in _ADMIN_TOOLS:
2198
2217
  if not is_admin:
2199
2218
  return json.dumps({"error": "Admin access required. You are not in SLACK_ADMIN_USERS."})
@@ -2380,6 +2399,43 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2380
2399
  return json.dumps({"status": "error", "pr": pr_url,
2381
2400
  "error": f"GitHub API returned {merge_resp.status_code}: {merge_resp.text[:300]}"})
2382
2401
 
2402
+ if name == "install_tool":
2403
+ import subprocess as _sp
2404
+ tool_name = inputs.get("tool_name", "").strip()
2405
+ if not tool_name:
2406
+ return json.dumps({"error": "tool_name is required"})
2407
+
2408
+ bin_path = cfg_loader.sentinel.claude_code_bin
2409
+ api_key = cfg_loader.sentinel.anthropic_api_key
2410
+ env = {**_os.environ}
2411
+ if api_key:
2412
+ env["ANTHROPIC_API_KEY"] = api_key
2413
+
2414
+ prompt = (
2415
+ f"Install '{tool_name}' on this server so it can be used as a build/test tool. "
2416
+ f"Detect the OS and package manager (yum/dnf/apt), then run the appropriate install command. "
2417
+ f"After installing, verify the installation by running '{tool_name} --version' or the equivalent. "
2418
+ f"Report the installed version. Do not explain — just install and report."
2419
+ )
2420
+ logger.info("Boss install_tool: installing '%s' via Claude Code", tool_name)
2421
+ try:
2422
+ result = _sp.run(
2423
+ [bin_path, "--dangerously-skip-permissions", "--bare", "--print", prompt],
2424
+ capture_output=True, text=True, timeout=300, env=env,
2425
+ )
2426
+ output = ((result.stdout or "") + (result.stderr or "")).strip()
2427
+ success = result.returncode == 0
2428
+ logger.info("Boss install_tool: '%s' install %s:\n%s", tool_name, "OK" if success else "FAILED", output[-500:])
2429
+ return json.dumps({
2430
+ "status": "installed" if success else "failed",
2431
+ "tool": tool_name,
2432
+ "output": output[-1000:],
2433
+ })
2434
+ except FileNotFoundError:
2435
+ return json.dumps({"error": f"Claude Code binary not found at '{bin_path}'"})
2436
+ except _sp.TimeoutExpired:
2437
+ return json.dumps({"error": f"Install timed out for '{tool_name}'"})
2438
+
2383
2439
  return json.dumps({"error": f"unknown tool: {name}"})
2384
2440
 
2385
2441