@misterhuydo/sentinel 1.5.61 → 1.5.63

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.
package/.cairn/.hint-lock CHANGED
@@ -1 +1 @@
1
- 2026-04-21T09:15:18.249Z
1
+ 2026-04-22T05:30:19.725Z
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-04-21T09:17:19.216Z",
3
- "checkpoint_at": "2026-04-21T09:17:19.218Z",
2
+ "message": "Auto-checkpoint at 2026-04-21T11:31:04.976Z",
3
+ "checkpoint_at": "2026-04-21T11:31:04.977Z",
4
4
  "active_files": [
5
5
  "J:\\Projects\\Sentinel\\cli\\bin\\sentinel.js",
6
6
  "J:\\Projects\\Sentinel\\cli\\lib\\test.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.5.61",
3
+ "version": "1.5.63",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -1 +1 @@
1
- __version__ = "1.5.61"
1
+ __version__ = "1.5.63"
@@ -131,7 +131,8 @@ def scan_issues(project_dir: Path) -> list[IssueEvent]:
131
131
 
132
132
  # Parse metadata headers in any order (TARGET_REPO, SUBMITTED_BY, SUBMITTED_AT, etc.)
133
133
  import re as _re
134
- _META = ("TARGET_REPO:", "SUBMITTED_BY:", "SUBMITTED_AT:", "SUPPORT_URL:", "ORIGIN_CHANNEL:")
134
+ _META = ("TARGET_REPO:", "SUBMITTED_BY:", "SUBMITTED_AT:", "SUPPORT_URL:",
135
+ "ORIGIN_CHANNEL:", "SOURCE:", "SLACK_TS:")
135
136
  submitter_user_id = ""
136
137
  origin_channel = ""
137
138
  for i, line in enumerate(lines):
@@ -283,7 +283,7 @@ async def _handle_error(event: ErrorEvent, cfg_loader: ConfigLoader, store: Stat
283
283
  _progress(f":warning: Needs human input — {marker}")
284
284
  notify_fix_blocked(sentinel, event.source, event.message,
285
285
  reason=marker, repo_name=repo.repo_name,
286
- submitter_user_id="")
286
+ submitter_user_id="", body=getattr(event, "body", ""))
287
287
  else:
288
288
  _progress(f":x: Cannot generate fix — Claude returned {status.upper()}")
289
289
  send_failure_notification(sentinel, {
@@ -475,7 +475,8 @@ async def _handle_issue(event: IssueEvent, cfg_loader: ConfigLoader, store: Stat
475
475
  _progress(f":x: Could not generate a safe fix — {reason_text[:120]}")
476
476
  notify_fix_blocked(sentinel, event.source, event.message,
477
477
  reason=reason_text, repo_name=repo.repo_name,
478
- submitter_user_id=submitter_uid)
478
+ submitter_user_id=submitter_uid,
479
+ body=getattr(event, "body", ""))
479
480
  mark_done(event.issue_file)
480
481
  return {"submitter": submitter_uid, "repo_name": repo.repo_name,
481
482
  "status": "blocked", "summary": reason_text[:120], "pr_url": ""}
@@ -490,7 +491,8 @@ async def _handle_issue(event: IssueEvent, cfg_loader: ConfigLoader, store: Stat
490
491
  notify_fix_blocked(sentinel, event.source, event.message,
491
492
  reason="Patch was generated but commit/tests failed",
492
493
  repo_name=repo.repo_name,
493
- submitter_user_id=submitter_uid)
494
+ submitter_user_id=submitter_uid,
495
+ body=getattr(event, "body", ""))
494
496
  mark_done(event.issue_file)
495
497
  return {"submitter": submitter_uid, "repo_name": repo.repo_name,
496
498
  "status": "blocked", "summary": "Commit/tests failed", "pr_url": ""}
@@ -587,7 +589,7 @@ async def _handle_issue(event: IssueEvent, cfg_loader: ConfigLoader, store: Stat
587
589
  notify_fix_blocked(sentinel, event.source, event.message,
588
590
  reason="Patch was generated but commit/tests failed after tool install",
589
591
  repo_name=repo.repo_name, submitter_user_id=submitter_uid,
590
- origin_channel=_origin_channel)
592
+ origin_channel=_origin_channel, body=getattr(event, "body", ""))
591
593
  mark_done(event.issue_file)
592
594
  return {"submitter": submitter_uid, "repo_name": repo.repo_name,
593
595
  "status": "blocked", "summary": "Commit/tests failed after tool install", "pr_url": ""}
@@ -1770,6 +1772,44 @@ def _format_monitor_step_output(tool: str, raw: str) -> str | None:
1770
1772
  text += f"\n_…and {len(matches) - 200} more_"
1771
1773
  return f"```\n{text}\n```"
1772
1774
 
1775
+ # ── search_logs ───────────────────────────────────────────────────────────
1776
+ if tool == "search_logs":
1777
+ hits = data.get("results") or data.get("matches") or []
1778
+ if not hits:
1779
+ return None
1780
+ lines = [
1781
+ f"[{h.get('source','?')}] {h.get('line', h)}" if isinstance(h, dict) else str(h)
1782
+ for h in hits[:200]
1783
+ ]
1784
+ text = "\n".join(lines)
1785
+ if len(hits) > 200:
1786
+ text += f"\n_…and {len(hits) - 200} more_"
1787
+ return f"```\n{text}\n```"
1788
+
1789
+ # ── list_pending_prs ──────────────────────────────────────────────────────
1790
+ if tool == "list_pending_prs":
1791
+ prs = data.get("prs") or data.get("results") or []
1792
+ if not prs:
1793
+ return None
1794
+ lines = [
1795
+ f"• [{p.get('repo','?')}] {p.get('title','?')} — {p.get('url') or p.get('pr_url','')}"
1796
+ if isinstance(p, dict) else str(p)
1797
+ for p in prs
1798
+ ]
1799
+ return "\n".join(lines)
1800
+
1801
+ # ── get_repo_status ───────────────────────────────────────────────────────
1802
+ if tool == "get_repo_status":
1803
+ repos = data.get("repos") or (data.get("repo") and [data]) or []
1804
+ if not repos:
1805
+ return raw.strip() or None
1806
+ lines = []
1807
+ for r in repos:
1808
+ name = r.get("repo") or r.get("name", "?")
1809
+ status = r.get("status") or r.get("summary", "")
1810
+ lines.append(f"• *{name}*: {status}")
1811
+ return "\n".join(lines) or None
1812
+
1773
1813
  # ── get_status / check_health / others — always show ─────────────────────
1774
1814
  if "error" in data:
1775
1815
  return f":warning: `{tool}` error: {data['error']}"
@@ -289,6 +289,7 @@ def notify_fix_blocked(
289
289
  repo_name: str = "",
290
290
  submitter_user_id: str = "",
291
291
  origin_channel: str = "",
292
+ body: str = "",
292
293
  ) -> None:
293
294
  """
294
295
  Notify that a fix needs human intervention.
@@ -297,13 +298,19 @@ def notify_fix_blocked(
297
298
  falls back to cfg.slack_channel. Always emails admins.
298
299
  """
299
300
  short_reason = (reason or "Claude could not determine a safe fix.")[:600]
300
- repo_line = f"\n*Repo:* {repo_name}" if repo_name else ""
301
+ repo_line = f"*Repo:* {repo_name}\n" if repo_name else ""
302
+
303
+ # Show the original report body in a code block so it's readable;
304
+ # fall back to the one-liner message if body isn't available.
305
+ report_content = (body or message).strip()
306
+ report_block = f"```\n{report_content[:1200]}\n```" if report_content else ""
301
307
 
302
308
  slack_text = (
303
309
  f":hand: *Fix blocked — human intervention needed*\n"
304
- f"*Source:* {source}\n"
305
- f"*Issue:* {message[:200]}{repo_line}\n"
306
- f"*Reason:*\n{short_reason}"
310
+ f"{repo_line}"
311
+ f"*What Claude found:* {short_reason}\n\n"
312
+ f"*Original report:*\n{report_block}\n\n"
313
+ f"_Reply `ignore` to dismiss, or assign someone to investigate._"
307
314
  )
308
315
 
309
316
  target_channel = origin_channel or cfg.slack_channel
@@ -374,8 +374,18 @@ reply with a grouped summary like this:
374
374
  – "every X min for Y hours/days" → calculate stop_at = now + duration
375
375
  – "every X min for N times" → set max_runs=N
376
376
  – "every X min until <datetime>" → set stop_at
377
- Minimum interval: 60 seconds. Allowed tools: fetch_logs, filter_logs, get_status,
378
- ask_logs, list_recent_commits, check_health.
377
+ Minimum interval: 60 seconds.
378
+ When a user asks what monitors can do or what tools are available for monitors, list all
379
+ allowed tools with a one-line description of each use case:
380
+ • fetch_logs — pull fresh logs from SSH servers (with optional filter/grep)
381
+ • filter_logs — search already-fetched logs by pattern
382
+ • ask_logs — ask an AI question about fetched log content
383
+ • get_status — Sentinel's error/fix summary for the last N hours
384
+ • check_health — health check on a service or repo
385
+ • list_recent_commits — recent commits across monitored repos
386
+ • search_logs — search indexed log history for a pattern across all sources
387
+ • list_pending_prs — show open Sentinel PRs waiting for review
388
+ • get_repo_status — git status of a repo (behind main, diverged, dirty, etc.)
379
389
  Always confirm to the user with the monitor ID and stop condition before creating.
380
390
  • `stop_monitor` — delete a monitor by ID (stops it if active); pass "all" to delete all in this channel
381
391
  • `list_monitors` — show active monitors plus completed/cancelled ones from the last 24 hours
@@ -1347,14 +1357,16 @@ _TOOLS = [
1347
1357
  "results to this Slack channel. Supports: run indefinitely (until stopped), run for "
1348
1358
  "a fixed duration (stop_at), or run N times (max_runs). "
1349
1359
  "steps is a list of {tool, inputs} objects — most monitors are a single step. "
1350
- "Allowed tools: fetch_logs, filter_logs, get_status, ask_logs, list_recent_commits, check_health. "
1360
+ "Allowed tools: fetch_logs, filter_logs, get_status, ask_logs, list_recent_commits, "
1361
+ "check_health, search_logs, list_pending_prs, get_repo_status. "
1351
1362
  "Boss calculates stop_at from phrases like 'within 2 hours' / 'for 30 minutes' using "
1352
1363
  "the current UTC time in the system prompt. "
1353
1364
  "IMPORTANT: ALWAYS call list_monitors FIRST to get the live DB state before calling "
1354
1365
  "this tool — never infer active monitors from conversation history, as finished monitors "
1355
1366
  "are deleted and context window state will be stale. "
1356
1367
  "Examples: 'fetch SSOLWA logs filtered by provision/phone every 5 min for 2 hours', "
1357
- "'check STS health every 10 min until I say stop', 'get status every hour for 3 times'."
1368
+ "'check STS health every 10 min until I say stop', 'search logs for OOM every 10 min', "
1369
+ "'watch for pending PRs every 30 min', 'monitor repo drift every hour'."
1358
1370
  ),
1359
1371
  "input_schema": {
1360
1372
  "type": "object",
@@ -3534,7 +3546,8 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
3534
3546
  return json.dumps({"error": f"interval_seconds must be >= 60 (minimum 1 minute), got {interval_s}"})
3535
3547
 
3536
3548
  _MONITOR_ALLOWED = {"fetch_logs", "filter_logs", "get_status", "ask_logs",
3537
- "list_recent_commits", "check_health"}
3549
+ "list_recent_commits", "check_health",
3550
+ "search_logs", "list_pending_prs", "get_repo_status"}
3538
3551
  for _step in steps:
3539
3552
  _t = _step.get("tool", "")
3540
3553
  if _t not in _MONITOR_ALLOWED: