@misterhuydo/sentinel 1.0.32 → 1.0.34
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/session.json +2 -2
- package/package.json +1 -1
- package/python/sentinel/main.py +10 -1
- package/python/sentinel/sentinel_boss.py +154 -0
package/.cairn/session.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"message": "Auto-checkpoint at 2026-03-
|
|
3
|
-
"checkpoint_at": "2026-03-
|
|
2
|
+
"message": "Auto-checkpoint at 2026-03-21T21:16:28.480Z",
|
|
3
|
+
"checkpoint_at": "2026-03-21T21:16:28.481Z",
|
|
4
4
|
"active_files": [],
|
|
5
5
|
"notes": [],
|
|
6
6
|
"mtime_snapshot": {}
|
package/package.json
CHANGED
package/python/sentinel/main.py
CHANGED
|
@@ -571,7 +571,16 @@ async def run_loop(cfg_loader: ConfigLoader, store: StateStore):
|
|
|
571
571
|
await poll_cycle(cfg_loader, store)
|
|
572
572
|
except Exception as e:
|
|
573
573
|
logger.exception("Unhandled error in poll cycle: %s", e)
|
|
574
|
-
|
|
574
|
+
# Sleep in 5s ticks so SENTINEL_POLL_NOW triggers immediately
|
|
575
|
+
elapsed = 0
|
|
576
|
+
while elapsed < interval:
|
|
577
|
+
await asyncio.sleep(5)
|
|
578
|
+
elapsed += 5
|
|
579
|
+
poll_now = Path("SENTINEL_POLL_NOW")
|
|
580
|
+
if poll_now.exists():
|
|
581
|
+
poll_now.unlink(missing_ok=True)
|
|
582
|
+
logger.info("Immediate poll triggered by Sentinel Boss")
|
|
583
|
+
break
|
|
575
584
|
|
|
576
585
|
|
|
577
586
|
def main():
|
|
@@ -122,6 +122,77 @@ _TOOLS = [
|
|
|
122
122
|
),
|
|
123
123
|
"input_schema": {"type": "object", "properties": {}},
|
|
124
124
|
},
|
|
125
|
+
{
|
|
126
|
+
"name": "search_logs",
|
|
127
|
+
"description": (
|
|
128
|
+
"Search fetched log files for a pattern. Use for: 'find activity from appid=X', "
|
|
129
|
+
"'search logs for Y', 'show me log entries containing Z', 'what did user X do?'. "
|
|
130
|
+
"Supports any regex or plain text pattern."
|
|
131
|
+
),
|
|
132
|
+
"input_schema": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"properties": {
|
|
135
|
+
"query": {
|
|
136
|
+
"type": "string",
|
|
137
|
+
"description": "Regex or plain text to search for",
|
|
138
|
+
},
|
|
139
|
+
"source": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"description": "Optional: limit to a specific log source name (partial match)",
|
|
142
|
+
},
|
|
143
|
+
"max_matches": {
|
|
144
|
+
"type": "integer",
|
|
145
|
+
"description": "Max matching lines to return per file (default 20)",
|
|
146
|
+
"default": 20,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
"required": ["query"],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"name": "trigger_poll",
|
|
154
|
+
"description": (
|
|
155
|
+
"Trigger an immediate log-fetch and error-detection cycle without waiting "
|
|
156
|
+
"for the next scheduled interval. Use when: 'check now', 'run now', "
|
|
157
|
+
"'poll immediately', 'don't wait'."
|
|
158
|
+
),
|
|
159
|
+
"input_schema": {"type": "object", "properties": {}},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"name": "get_repo_status",
|
|
163
|
+
"description": (
|
|
164
|
+
"Per-repository breakdown of errors detected and fixes applied. "
|
|
165
|
+
"Use for: 'how is repo X doing?', 'which repo has the most issues?', "
|
|
166
|
+
"'break down by repo'."
|
|
167
|
+
),
|
|
168
|
+
"input_schema": {
|
|
169
|
+
"type": "object",
|
|
170
|
+
"properties": {
|
|
171
|
+
"hours": {
|
|
172
|
+
"type": "integer",
|
|
173
|
+
"description": "Look-back window in hours (default 24)",
|
|
174
|
+
"default": 24,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"name": "list_recent_commits",
|
|
181
|
+
"description": (
|
|
182
|
+
"List recent commits made by Sentinel across all managed repos. "
|
|
183
|
+
"Use for: 'what did Sentinel commit?', 'show recent auto-fixes', 'what was changed?'."
|
|
184
|
+
),
|
|
185
|
+
"input_schema": {
|
|
186
|
+
"type": "object",
|
|
187
|
+
"properties": {
|
|
188
|
+
"limit": {
|
|
189
|
+
"type": "integer",
|
|
190
|
+
"description": "Max commits per repo (default 5)",
|
|
191
|
+
"default": 5,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
125
196
|
]
|
|
126
197
|
|
|
127
198
|
|
|
@@ -262,6 +333,76 @@ def _run_tool(name: str, inputs: dict, cfg_loader, store) -> str:
|
|
|
262
333
|
"workspace_projects": other_projects,
|
|
263
334
|
})
|
|
264
335
|
|
|
336
|
+
if name == "search_logs":
|
|
337
|
+
query = inputs.get("query", "")
|
|
338
|
+
source = inputs.get("source", "").lower()
|
|
339
|
+
max_matches = int(inputs.get("max_matches", 20))
|
|
340
|
+
fetched_dir = Path("workspace/fetched")
|
|
341
|
+
if not fetched_dir.exists():
|
|
342
|
+
return json.dumps({"error": "No fetched logs found — run a poll first"})
|
|
343
|
+
try:
|
|
344
|
+
pattern = re.compile(query, re.IGNORECASE)
|
|
345
|
+
except re.error as e:
|
|
346
|
+
return json.dumps({"error": f"Invalid regex: {e}"})
|
|
347
|
+
results = []
|
|
348
|
+
for log_file in sorted(fetched_dir.glob("*.log")):
|
|
349
|
+
if source and source not in log_file.name.lower():
|
|
350
|
+
continue
|
|
351
|
+
try:
|
|
352
|
+
lines = log_file.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
353
|
+
matches = [
|
|
354
|
+
{"line": i + 1, "text": line[:300]}
|
|
355
|
+
for i, line in enumerate(lines)
|
|
356
|
+
if pattern.search(line)
|
|
357
|
+
][:max_matches]
|
|
358
|
+
if matches:
|
|
359
|
+
results.append({"file": log_file.name, "matches": matches})
|
|
360
|
+
except Exception:
|
|
361
|
+
pass
|
|
362
|
+
total = sum(len(r["matches"]) for r in results)
|
|
363
|
+
return json.dumps({
|
|
364
|
+
"query": query,
|
|
365
|
+
"total_matches": total,
|
|
366
|
+
"files_searched": len(list(fetched_dir.glob("*.log"))),
|
|
367
|
+
"results": results,
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
if name == "trigger_poll":
|
|
371
|
+
Path("SENTINEL_POLL_NOW").touch()
|
|
372
|
+
logger.info("Boss: immediate poll requested")
|
|
373
|
+
return json.dumps({"status": "triggered", "note": "Sentinel will run a poll cycle within seconds"})
|
|
374
|
+
|
|
375
|
+
if name == "get_repo_status":
|
|
376
|
+
hours = int(inputs.get("hours", 24))
|
|
377
|
+
fixes = store.get_recent_fixes(hours)
|
|
378
|
+
errors = store.get_recent_errors(hours)
|
|
379
|
+
by_repo: dict = {}
|
|
380
|
+
for fix in fixes:
|
|
381
|
+
repo = fix.get("repo_name", "unknown")
|
|
382
|
+
s = by_repo.setdefault(repo, {"applied": 0, "pending": 0, "failed": 0, "skipped": 0})
|
|
383
|
+
key = fix.get("status", "failed")
|
|
384
|
+
s[key] = s.get(key, 0) + 1
|
|
385
|
+
return json.dumps({"window_hours": hours, "total_errors": len(errors), "by_repo": by_repo})
|
|
386
|
+
|
|
387
|
+
if name == "list_recent_commits":
|
|
388
|
+
limit = int(inputs.get("limit", 5))
|
|
389
|
+
results = []
|
|
390
|
+
for repo_name, repo in cfg_loader.repos.items():
|
|
391
|
+
local = Path(repo.local_path)
|
|
392
|
+
if not local.exists():
|
|
393
|
+
continue
|
|
394
|
+
try:
|
|
395
|
+
r = subprocess.run(
|
|
396
|
+
["git", "log", "--oneline", "--grep=sentinel", "-n", str(limit)],
|
|
397
|
+
cwd=str(local), capture_output=True, text=True, timeout=10,
|
|
398
|
+
)
|
|
399
|
+
commits = r.stdout.strip().splitlines()
|
|
400
|
+
if commits:
|
|
401
|
+
results.append({"repo": repo_name, "commits": commits})
|
|
402
|
+
except Exception:
|
|
403
|
+
pass
|
|
404
|
+
return json.dumps({"sentinel_commits": results})
|
|
405
|
+
|
|
265
406
|
return json.dumps({"error": f"unknown tool: {name}"})
|
|
266
407
|
|
|
267
408
|
|
|
@@ -280,6 +421,16 @@ async def _handle_with_cli(
|
|
|
280
421
|
status_json = _run_tool("get_status", {"hours": 24}, cfg_loader, store)
|
|
281
422
|
prs_json = _run_tool("list_pending_prs", {}, cfg_loader, store)
|
|
282
423
|
|
|
424
|
+
# Pre-fetch log search if the message is a search request.
|
|
425
|
+
# Use quoted strings as the query, or fall back to the full message.
|
|
426
|
+
# Never hardcode field names — the query is whatever the user said.
|
|
427
|
+
search_json = ""
|
|
428
|
+
_search_kws = ("search", "find", "look for", "show me log", "grep", "entries for")
|
|
429
|
+
if any(kw in message.lower() for kw in _search_kws):
|
|
430
|
+
quoted = re.findall(r'"([^"]+)"', message)
|
|
431
|
+
query = quoted[0] if quoted else message
|
|
432
|
+
search_json = _run_tool("search_logs", {"query": query}, cfg_loader, store)
|
|
433
|
+
|
|
283
434
|
paused = Path("SENTINEL_PAUSE").exists()
|
|
284
435
|
repos = list(cfg_loader.repos.keys())
|
|
285
436
|
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
@@ -304,12 +455,15 @@ async def _handle_with_cli(
|
|
|
304
455
|
+ f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
|
|
305
456
|
+ f"\n\nCurrent status (last 24 h):\n{status_json}"
|
|
306
457
|
+ f"\n\nOpen PRs:\n{prs_json}"
|
|
458
|
+
+ (f"\n\nLog search results:\n{search_json}" if search_json else "")
|
|
307
459
|
+ (f"\n\nConversation so far:{history_text}" if history_text else "")
|
|
308
460
|
+ f"\n\nUSER: {message}"
|
|
309
461
|
+ "\n\nIf you need to take an action, include a line like:\n"
|
|
310
462
|
+ " ACTION: {\"action\": \"pause_sentinel\"}\n"
|
|
311
463
|
+ " ACTION: {\"action\": \"resume_sentinel\"}\n"
|
|
464
|
+
+ " ACTION: {\"action\": \"trigger_poll\"}\n"
|
|
312
465
|
+ " ACTION: {\"action\": \"create_issue\", \"description\": \"...\", \"target_repo\": \"\"}\n"
|
|
466
|
+
+ " ACTION: {\"action\": \"search_logs\", \"query\": \"<whatever the user asked to find>\"}\n"
|
|
313
467
|
+ "End with [DONE] if the request is fully handled."
|
|
314
468
|
)
|
|
315
469
|
|