@misterhuydo/sentinel 1.5.38 → 1.5.40

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.5.38",
3
+ "version": "1.5.40",
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.38"
1
+ __version__ = "1.5.40"
@@ -152,15 +152,20 @@ COMPLETE TOOL REFERENCE
152
152
  ── Log Management ─────────────────────────────────────────────────────────────
153
153
 
154
154
  6. fetch_logs Run fetch_log.sh on demand — pull fresh logs from servers now.
155
- Supports --debug and parameter overrides.
156
- "fetch logs", "fetch logs for SSOLWA", "fetch without filter"
155
+ When grep_filter is set, results go to TEMP files (workspace/fetched_temp/)
156
+ and do NOT affect the main rolling logs. Temp files are cleared on every
157
+ custom fetch, and filter_logs searches them automatically.
158
+ Use grep_filter for INFO-level or feature-specific patterns that the default
159
+ WARN/ERROR filter would miss.
160
+ "fetch logs", "fetch SSOLWA with filter provision/phone", "fetch without filter"
157
161
 
158
162
  7. search_logs Live SSH grep on production servers using GREP_FILTER.
159
163
  Falls back to cached files if SSH unavailable.
160
164
  "search logs for illegal PIN in 1881", "find NullPointerException in STS"
161
165
 
162
- 8. filter_logs Instant keyword/regex search on locally-synced logs. No SSH, sub-second.
163
- Supports since_hours, case options.
166
+ 8. filter_logs Instant keyword/regex search on locally-synced logs + any temp fetch results.
167
+ No SSH, sub-second. Also searches workspace/fetched_temp/ if a custom
168
+ fetch was recently run.
164
169
  "filter logs for TryDig", "errors last 6h", "find appid=X in STS logs"
165
170
 
166
171
  9. tail_log Last N lines of a log source live, no filter.
@@ -545,6 +550,9 @@ When to act vs. when to ask:
545
550
  tasks, fixes, releases). Always call get_status or list_recent_commits first to verify live
546
551
  state. Session memory is a snapshot — tasks complete, commits land, queues drain between turns.
547
552
  If you remember "task X was in-flight", check whether it finished before telling the user to wait.
553
+ - NEVER repeat a deployment assertion you made in a prior turn without re-verifying it.
554
+ If you said "version X is not deployed" in a previous message and the user asks again, do NOT
555
+ just echo the same claim — check list_recent_commits or startup logs fresh before answering.
548
556
  - Prefer filter_logs over search_logs when synced logs are available — it's instant and never causes session timeout.
549
557
  Use search_logs (live SSH fetch) when:
550
558
  • The user explicitly wants live/real-time data
@@ -553,11 +561,15 @@ When to act vs. when to ask:
553
561
  (synced logs may simply be stale — do NOT conclude the change isn't live yet; fetch live first)
554
562
  When filter_logs returns no hits after a recent release, always retry with search_logs before
555
563
  telling the user the log line isn't there.
556
- - NEVER infer deployment status from the absence of a feature log line. A log line only appears
557
- when that specific code path is executed. Zero hits for "provision/phone called by appId" means
558
- nobody has called that endpoint yet it says nothing about whether the release deployed.
559
- To verify whether a release deployed, use list_recent_commits or search_logs for startup/version
560
- log lines (e.g. "Starting", "version", "initialized"), NOT absence of feature-specific lines.
564
+ - NEVER infer deployment status from the absence of a feature log line.
565
+ A specific log line only appears when that exact code path is executed by a real user/request.
566
+ Zero hits means: nobody has triggered that path yet. It says NOTHING about whether the release
567
+ deployed. DO NOT say "release X has not deployed" or "the servers are still on the old version"
568
+ based on this. If you must comment on deployment status, say:
569
+ "The log line hasn't appeared yet — this means the endpoint hasn't been called since the
570
+ release, not that the release is missing."
571
+ To actually verify a release is live, search for startup/version lines:
572
+ search_logs with query "Starting|started in|version|initialized" — do not guess from feature log absence.
561
573
  - If a tool call will take a moment (search, fetch, pull), prefix your reply with a brief "working" line ending in "..." before the results, e.g. "Searching SSOLWA for TryDig activity..." then the actual output.
562
574
  Never just say a working line and stop — always follow it with the results in the same message.
563
575
 
@@ -1149,7 +1161,14 @@ _TOOLS = [
1149
1161
  "Run fetch_log.sh for one or all configured log sources to pull the latest logs "
1150
1162
  "from remote servers right now. Use for: 'fetch logs', 'run fetch_log.sh', "
1151
1163
  "'grab latest logs from SSOLWA', 'try fetch_log.sh for STS', "
1152
- "'pull logs from server', 'get fresh logs'."
1164
+ "'pull logs from server', 'get fresh logs'.\n\n"
1165
+ "IMPORTANT: When a grep_filter is provided, results go to a TEMPORARY location "
1166
+ "(workspace/fetched_temp/) and do NOT overwrite the main rolling logs. "
1167
+ "The temp files are cleared on every custom fetch. "
1168
+ "After a custom fetch, filter_logs will automatically search the temp results too. "
1169
+ "Use grep_filter whenever the user wants to find INFO-level or feature-specific log lines "
1170
+ "(e.g. 'provision/phone', 'appId', startup messages) — the default filter only captures "
1171
+ "WARN|ERROR|FATAL|Exception|Error lines."
1153
1172
  ),
1154
1173
  "input_schema": {
1155
1174
  "type": "object",
@@ -1169,7 +1188,11 @@ _TOOLS = [
1169
1188
  },
1170
1189
  "grep_filter": {
1171
1190
  "type": "string",
1172
- "description": "Override GREP_FILTER (regex). Pass 'none' to disable filtering.",
1191
+ "description": (
1192
+ "Custom grep filter (regex). Results saved to temp files, main logs untouched. "
1193
+ "Pass 'none' to fetch all lines unfiltered. "
1194
+ "Use when searching for INFO-level or feature-specific patterns."
1195
+ ),
1173
1196
  },
1174
1197
  },
1175
1198
  },
@@ -2807,104 +2830,44 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2807
2830
  return json.dumps({"error": f"Invalid regex: {e}"})
2808
2831
 
2809
2832
  synced_base = Path("workspace/synced")
2810
- if not synced_base.exists():
2811
- return json.dumps({
2812
- "error": "No synced logs found.",
2813
- "hint": "Log sync runs every SYNC_INTERVAL_SECONDS (default 300s). "
2814
- "If just started, wait a minute then try again.",
2815
- })
2833
+ temp_base = Path(cfg_loader.sentinel.workspace_dir) / "fetched_temp"
2816
2834
 
2817
2835
  # Build cutoff timestamp for since_hours filter
2818
2836
  cutoff = None
2819
2837
  if since_hours:
2820
2838
  cutoff = _datetime.now(_tz.utc) - timedelta(hours=int(since_hours))
2821
2839
 
2822
- # Determine which source directories to search
2823
- if source_f:
2824
- src_dirs = [d for d in sorted(synced_base.iterdir())
2825
- if d.is_dir() and source_f in d.name.lower()]
2826
- else:
2827
- src_dirs = [d for d in sorted(synced_base.iterdir()) if d.is_dir()]
2828
-
2829
- if not src_dirs:
2830
- available = [d.name for d in synced_base.iterdir() if d.is_dir()]
2831
- return json.dumps({
2832
- "error": f"No synced source matching '{source_f}'",
2833
- "available_sources": available,
2834
- })
2835
-
2836
- results = []
2837
- total_matches = 0
2838
- for src_dir in src_dirs:
2839
- for log_file in sorted(src_dir.glob("*")):
2840
- try:
2841
- lines = log_file.read_text(encoding="utf-8", errors="replace").splitlines()
2842
- matches = []
2843
- for line in lines:
2844
- if not pat.search(line):
2845
- continue
2846
- if cutoff:
2847
- # Try to parse timestamp from line
2848
- from .log_fetcher import _parse_line_ts
2849
- ts = _parse_line_ts(line)
2850
- if ts and ts < cutoff:
2851
- continue
2852
- matches.append(line[:300])
2853
- if len(matches) >= max_matches:
2854
- break
2855
- if matches:
2856
- results.append({
2857
- "source": src_dir.name,
2858
- "file": log_file.name,
2859
- "matches": matches,
2860
- })
2861
- total_matches += len(matches)
2862
- except Exception:
2863
- pass
2864
-
2865
- if not results:
2866
- return json.dumps({
2867
- "query": query_f,
2868
- "total_matches": 0,
2869
- "sources_searched": [d.name for d in src_dirs],
2870
- "note": "No matches found in synced logs.",
2871
- })
2840
+ # Collect candidate directories from both synced/ and fetched_temp/
2841
+ def _collect_dirs(base):
2842
+ if not base.exists():
2843
+ return []
2844
+ if source_f:
2845
+ return [d for d in sorted(base.iterdir()) if d.is_dir() and source_f in d.name.lower()]
2846
+ return [d for d in sorted(base.iterdir()) if d.is_dir()]
2872
2847
 
2848
+ src_dirs = _collect_dirs(synced_base)
2849
+ temp_dirs = _collect_dirs(temp_base)
2850
+ all_search_dirs = src_dirs + [(d, True) for d in temp_dirs] # True = is_temp
2873
2851
 
2874
- try:
2875
- pat = _re.compile(query_f, case_flag)
2876
- except _re.error as e:
2877
- return json.dumps({"error": f"Invalid regex: {e}"})
2852
+ # Flatten to (dir, is_temp) pairs
2853
+ search_pairs = [(d, False) for d in src_dirs] + [(d, True) for d in temp_dirs]
2878
2854
 
2879
- synced_base = Path("workspace/synced")
2880
- if not synced_base.exists():
2855
+ if not search_pairs:
2856
+ available = ([d.name for d in synced_base.iterdir() if d.is_dir()] if synced_base.exists() else [])
2881
2857
  return json.dumps({
2882
- "error": "No synced logs found.",
2858
+ "error": f"No synced or temp source matching '{source_f}'" if source_f else "No logs found.",
2859
+ "available_sources": available,
2883
2860
  "hint": "Log sync runs every SYNC_INTERVAL_SECONDS (default 300s). "
2884
2861
  "If just started, wait a minute then try again.",
2885
2862
  })
2886
2863
 
2887
- cutoff = None
2888
- if since_hours:
2889
- cutoff = _datetime.now(_tz.utc) - timedelta(hours=int(since_hours))
2890
-
2891
- if source_f:
2892
- src_dirs = [d for d in sorted(synced_base.iterdir())
2893
- if d.is_dir() and source_f in d.name.lower()]
2894
- else:
2895
- src_dirs = [d for d in sorted(synced_base.iterdir()) if d.is_dir()]
2896
-
2897
- if not src_dirs:
2898
- available = [d.name for d in synced_base.iterdir() if d.is_dir()]
2899
- return json.dumps({
2900
- "error": f"No synced source matching '{source_f}'",
2901
- "available_sources": available,
2902
- })
2903
-
2904
- all_matches = [] # list of (source_name, line)
2864
+ all_matches = [] # list of (source_label, line)
2905
2865
  sources_hit = set()
2906
- for src_dir in src_dirs:
2907
- for log_file in sorted(src_dir.glob("*")):
2866
+ for src_dir, is_temp in search_pairs:
2867
+ label = src_dir.name + (" [temp]" if is_temp else "")
2868
+ for log_file in sorted(src_dir.glob("**/*")):
2869
+ if not log_file.is_file():
2870
+ continue
2908
2871
  try:
2909
2872
  lines = log_file.read_text(encoding="utf-8", errors="replace").splitlines()
2910
2873
  for line in lines:
@@ -2915,8 +2878,8 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2915
2878
  ts = _parse_line_ts(line)
2916
2879
  if ts and ts < cutoff:
2917
2880
  continue
2918
- all_matches.append((src_dir.name, line[:300]))
2919
- sources_hit.add(src_dir.name)
2881
+ all_matches.append((label, line[:300]))
2882
+ sources_hit.add(label)
2920
2883
  if len(all_matches) >= max_matches:
2921
2884
  break
2922
2885
  except Exception:
@@ -2925,12 +2888,19 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2925
2888
  break
2926
2889
 
2927
2890
  total = len(all_matches)
2891
+ sources_searched = [d.name + (" [temp]" if is_temp else "") for d, is_temp in search_pairs]
2928
2892
  if total == 0:
2893
+ has_temp = bool(temp_dirs)
2929
2894
  return json.dumps({
2930
2895
  "query": query_f,
2931
2896
  "total_matches": 0,
2932
- "sources_searched": [d.name for d in src_dirs],
2933
- "note": "No matches found in synced logs.",
2897
+ "sources_searched": sources_searched,
2898
+ "note": (
2899
+ "No matches found. "
2900
+ + ("Temp fetch results were also checked. " if has_temp else "")
2901
+ + "If searching for a specific log line from a new feature, use fetch_logs "
2902
+ "with a matching grep_filter first — the default filter only captures WARN/ERROR."
2903
+ ),
2934
2904
  })
2935
2905
 
2936
2906
  # Pattern grouping: count occurrences of each error signature
@@ -2976,7 +2946,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2976
2946
  "query": query_f,
2977
2947
  "total_matches": total,
2978
2948
  "sources_hit": sorted(sources_hit),
2979
- "sources_searched": [d.name for d in src_dirs],
2949
+ "sources_searched": sources_searched,
2980
2950
  "top_patterns": top_patterns,
2981
2951
  "sample_lines": sample_lines,
2982
2952
  "time_span": time_span,
@@ -3074,6 +3044,20 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
3074
3044
  if not props_files:
3075
3045
  return json.dumps({"error": f"No log-config found matching '{source_filter}'"})
3076
3046
 
3047
+ # When a custom grep_filter is set, route output to a temp directory
3048
+ # so the main rolling logs are never polluted by user-requested searches.
3049
+ # The temp dir is cleared before each custom fetch.
3050
+ workspace_dir = Path(cfg_loader.sentinel.workspace_dir)
3051
+ temp_base = workspace_dir / "fetched_temp"
3052
+ use_temp = bool(grep_override)
3053
+
3054
+ if use_temp:
3055
+ # Clear old temp results
3056
+ import shutil
3057
+ if temp_base.exists():
3058
+ shutil.rmtree(temp_base)
3059
+ temp_base.mkdir(parents=True, exist_ok=True)
3060
+
3077
3061
  results = []
3078
3062
  for props in props_files:
3079
3063
  env = os.environ.copy()
@@ -3081,6 +3065,9 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
3081
3065
  env["TAIL"] = str(tail_override)
3082
3066
  if grep_override:
3083
3067
  env["SENTINEL_GREP_FILTER_OVERRIDE"] = grep_override
3068
+ if use_temp:
3069
+ # Tell fetch_log.sh where to write output files
3070
+ env["OUTPUT_DIR"] = str(temp_base)
3084
3071
 
3085
3072
  cmd = ["bash", str(script)]
3086
3073
  if debug:
@@ -3093,13 +3080,24 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
3093
3080
  )
3094
3081
  output = (r.stdout or "").strip()
3095
3082
  stderr = (r.stderr or "").strip()
3083
+
3084
+ # Collect lines from temp files for this source
3085
+ temp_lines = []
3086
+ if use_temp:
3087
+ for f in sorted((temp_base / props.stem).glob("*.log")) if (temp_base / props.stem).exists() else []:
3088
+ try:
3089
+ temp_lines.extend(f.read_text(encoding="utf-8", errors="replace").splitlines())
3090
+ except Exception:
3091
+ pass
3092
+
3096
3093
  results.append({
3097
3094
  "source": props.stem,
3098
3095
  "returncode": r.returncode,
3099
3096
  "output": output[-2000:] if output else "",
3100
3097
  "stderr": stderr[-1000:] if stderr else "",
3098
+ **({"lines": temp_lines, "temp_file": str(temp_base / props.stem)} if use_temp else {}),
3101
3099
  })
3102
- logger.info("Boss fetch_logs %s rc=%d", props.stem, r.returncode)
3100
+ logger.info("Boss fetch_logs %s rc=%d (temp=%s)", props.stem, r.returncode, use_temp)
3103
3101
  except subprocess.TimeoutExpired:
3104
3102
  results.append({"source": props.stem, "error": "timed out after 120s"})
3105
3103
  except Exception as e: