@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 +1 -1
- package/.cairn/session.json +2 -2
- package/package.json +1 -1
- package/python/sentinel/__init__.py +1 -1
- package/python/sentinel/issue_watcher.py +2 -1
- package/python/sentinel/main.py +44 -4
- package/python/sentinel/notify.py +11 -4
- package/python/sentinel/sentinel_boss.py +18 -5
package/.cairn/.hint-lock
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-04-
|
|
1
|
+
2026-04-22T05:30:19.725Z
|
package/.cairn/session.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"message": "Auto-checkpoint at 2026-04-
|
|
3
|
-
"checkpoint_at": "2026-04-
|
|
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 +1 @@
|
|
|
1
|
-
__version__ = "1.5.
|
|
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:",
|
|
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):
|
package/python/sentinel/main.py
CHANGED
|
@@ -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"
|
|
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"
|
|
305
|
-
f"*
|
|
306
|
-
f"*
|
|
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.
|
|
378
|
-
|
|
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,
|
|
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', '
|
|
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:
|