@misterhuydo/sentinel 1.5.30 → 1.5.32

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.30",
3
+ "version": "1.5.32",
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.30"
1
+ __version__ = "1.5.32"
@@ -34,7 +34,7 @@ from .git_manager import _git_env
34
34
 
35
35
  logger = logging.getLogger(__name__)
36
36
 
37
- _META_PREFIXES = ("REPO:", "TYPE:", "SUBMITTED_BY:", "SUBMITTED_AT:", "RUN_AT:", "NOTIFY:", "ORIGIN_CHANNEL:")
37
+ _META_PREFIXES = ("REPO:", "TYPE:", "SUBMITTED_BY:", "SUBMITTED_AT:", "RUN_AT:", "NOTIFY:", "ORIGIN_CHANNEL:", "SKIP_TESTS:")
38
38
  _TASK_TIMEOUT = 900 # 15 minutes
39
39
 
40
40
 
@@ -51,6 +51,7 @@ class RepoTask:
51
51
  timestamp: str = ""
52
52
  run_at: datetime | None = None # UTC; task is held until this time if set
53
53
  origin_channel: str = "" # Slack channel ID where task was requested (DM or group)
54
+ skip_tests: bool = False # skip test suite (safe for trivial/non-logic changes)
54
55
 
55
56
  def __post_init__(self):
56
57
  if not self.fingerprint:
@@ -62,6 +63,20 @@ class RepoTask:
62
63
 
63
64
  def _build_repo_prompt(task: RepoTask, repo: RepoConfig) -> str:
64
65
  submitted = f"Requested by: <@{task.submitter_user_id}>" if task.submitter_user_id else ""
66
+ if task.skip_tests:
67
+ test_instruction = (
68
+ "4. The user explicitly requested to skip tests — syntax check only, "
69
+ "do NOT run the test suite."
70
+ )
71
+ else:
72
+ test_instruction = (
73
+ "4. After making your changes, run: git diff HEAD to review the diff, then decide:\n"
74
+ " - If ALL changes are non-logic (log statements, comments, string literals,\n"
75
+ " config values, whitespace/formatting): syntax check only — skip the test suite.\n"
76
+ " - If ANY logic changed (conditionals, data flow, new/removed code paths,\n"
77
+ " method behaviour): run the full test suite (mvn test / npm test / gradlew test)\n"
78
+ " and commit only if it passes."
79
+ )
65
80
  return (
66
81
  f"You are implementing a requested change in the repository at {repo.local_path}.\n"
67
82
  f"Repository: {repo.repo_name}\n"
@@ -76,7 +91,7 @@ def _build_repo_prompt(task: RepoTask, repo: RepoConfig) -> str:
76
91
  f" before making any changes.\n"
77
92
  f"2. Implement the requested change following the repo's existing code style.\n"
78
93
  f"3. Syntax/compile check modified files.\n"
79
- f"4. Run tests if available (Maven, Gradle, npm test) — commit only if passing.\n"
94
+ f"{test_instruction}\n"
80
95
  f"5. Commit all changes:\n"
81
96
  f" git add -A\n"
82
97
  f" git commit -m \"{task.task_type}(<scope>): <concise summary> [sentinel-task]\"\n"
@@ -347,6 +362,7 @@ def drop_repo_task(
347
362
  notify_user_ids: list | None = None,
348
363
  run_at: datetime | None = None,
349
364
  origin_channel: str = "",
365
+ skip_tests: bool = False,
350
366
  ) -> Path:
351
367
  """Drop a repo task file into <project_dir>/repo-tasks/.
352
368
 
@@ -372,6 +388,8 @@ def drop_repo_task(
372
388
  lines.append(f"NOTIFY: {','.join(notify_user_ids)}")
373
389
  if origin_channel:
374
390
  lines.append(f"ORIGIN_CHANNEL: {origin_channel}")
391
+ if skip_tests:
392
+ lines.append("SKIP_TESTS: true")
375
393
  lines += ["", description]
376
394
  fpath.write_text("\n".join(lines), encoding="utf-8")
377
395
  if run_at:
@@ -404,6 +422,7 @@ def scan_repo_tasks(project_dir: Path) -> list[RepoTask]:
404
422
  notify_user_ids: list = []
405
423
  run_at: datetime | None = None
406
424
  origin_channel = ""
425
+ skip_tests = False
407
426
  body_start = 0
408
427
 
409
428
  for i, line in enumerate(lines):
@@ -434,6 +453,9 @@ def scan_repo_tasks(project_dir: Path) -> list[RepoTask]:
434
453
  elif upper.startswith("ORIGIN_CHANNEL:"):
435
454
  origin_channel = stripped[15:].strip()
436
455
  body_start = i + 1
456
+ elif upper.startswith("SKIP_TESTS:"):
457
+ skip_tests = stripped[11:].strip().lower() in ("true", "yes", "1")
458
+ body_start = i + 1
437
459
  elif any(upper.startswith(p) for p in _META_PREFIXES) or not stripped:
438
460
  body_start = i + 1
439
461
  else:
@@ -469,6 +491,7 @@ def scan_repo_tasks(project_dir: Path) -> list[RepoTask]:
469
491
  notify_user_ids=notify_user_ids,
470
492
  run_at=run_at,
471
493
  origin_channel=origin_channel,
494
+ skip_tests=skip_tests,
472
495
  ))
473
496
  logger.info("Found repo task: %s → %s (type=%s)", f.name, repo_name, task_type)
474
497
 
@@ -116,6 +116,9 @@ BEFORE CALLING dev_task OR repo_task — GATHER A COMPLETE SPEC:
116
116
  Ask follow-up questions until you have enough to write an unambiguous task description.
117
117
  Typical questions to resolve:
118
118
  - For repo_task: which repo exactly? (confirm if multiple match)
119
+ - For repo_task: Claude Code auto-judges whether to run tests by inspecting its own diff.
120
+ Only set skip_tests=true when the user explicitly says "skip tests", "don't run tests",
121
+ or similar — never set it on your own judgment.
119
122
  - What exactly should happen? (specific behaviour, not vague intent)
120
123
  - Any config values, credentials, or external dependencies involved?
121
124
  - How should it be triggered? (schedule, event, API call, etc.)
@@ -532,6 +535,10 @@ When to act vs. when to ask:
532
535
  - Explaining a tool ("what does X do?") → explain naturally, then offer to run it if relevant.
533
536
  - NEVER gate investigation on user approval. If diagnosing a problem, run all relevant read tools
534
537
  first, then present findings. Asking "Want me to look?" wastes a round trip.
538
+ - NEVER answer from session memory alone when the question is about current state (commits,
539
+ tasks, fixes, releases). Always call get_status or list_recent_commits first to verify live
540
+ state. Session memory is a snapshot — tasks complete, commits land, queues drain between turns.
541
+ If you remember "task X was in-flight", check whether it finished before telling the user to wait.
535
542
  - Prefer filter_logs over search_logs when synced logs are available — it's instant and never causes session timeout.
536
543
  Use search_logs only when the user explicitly wants live/real-time data or synced logs are not yet available.
537
544
  - 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.
@@ -916,6 +923,15 @@ _TOOLS = [
916
923
  "If omitted the task runs immediately on the next poll cycle."
917
924
  ),
918
925
  },
926
+ "skip_tests": {
927
+ "type": "boolean",
928
+ "description": (
929
+ "Only set to true when the user explicitly requests skipping tests "
930
+ "(e.g. 'skip tests', 'don\\'t run tests', 'just commit it'). "
931
+ "When false (default), Claude Code automatically decides whether to run "
932
+ "tests by inspecting its own diff — non-logic changes skip tests on their own."
933
+ ),
934
+ },
919
935
  },
920
936
  "required": ["repo_name", "description"],
921
937
  },
@@ -2473,6 +2489,8 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2473
2489
  if not _project_dirs:
2474
2490
  return json.dumps({"error": "No project directory found."})
2475
2491
 
2492
+ skip_tests = bool(inputs.get("skip_tests", False))
2493
+
2476
2494
  from .repo_task_engine import drop_repo_task as _drop_repo_task
2477
2495
  task_file = _drop_repo_task(
2478
2496
  _project_dirs[0],
@@ -2483,6 +2501,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2483
2501
  notify_user_ids=notify_ids,
2484
2502
  run_at=run_at_dt,
2485
2503
  origin_channel=channel,
2504
+ skip_tests=skip_tests,
2486
2505
  )
2487
2506
  logger.info(
2488
2507
  "Boss repo_task: dropped %s for user %s (repo=%s, run_at=%s)",
@@ -125,10 +125,12 @@ async def run_slack_bot(cfg_loader, store) -> None:
125
125
 
126
126
  @app.event("app_mention")
127
127
  async def on_mention(event, client):
128
+ # Resolve allowed channel lazily (used for passive message filtering only).
129
+ # @mentions are always handled regardless of SLACK_CHANNEL — Boss responds
130
+ # in whatever channel the user mentioned it from.
128
131
  if cfg.slack_channel and not _allowed_id:
129
132
  await _resolve_allowed(client)
130
- if _allowed(event.get("channel", "")):
131
- await _dispatch(event, client, cfg_loader, store)
133
+ await _dispatch(event, client, cfg_loader, store)
132
134
 
133
135
  # ── Passive bot watcher — seed DB from config on startup ─────────────────
134
136
  for bot_id_cfg in cfg.slack_watch_bot_ids:
@@ -139,8 +141,13 @@ async def run_slack_bot(cfg_loader, store) -> None:
139
141
  @app.event("message")
140
142
  async def on_message(event, client):
141
143
  if event.get("bot_id"):
142
- # Passive bot watcher — bot messages from DB-registered bots
143
- if event.get("channel") and store.is_watched_bot(event["bot_id"]):
144
+ # Passive bot watcher — match on bot_id (B-prefix) OR user (U-prefix)
145
+ # watch_bot stores the U-prefixed user ID; Slack events send B-prefixed bot_id
146
+ _is_watched = (
147
+ store.is_watched_bot(event["bot_id"])
148
+ or store.is_watched_bot(event.get("user", ""))
149
+ )
150
+ if event.get("channel") and _is_watched:
144
151
  await _handle_bot_message(event, client, cfg_loader, store)
145
152
  return
146
153
 
@@ -199,8 +206,13 @@ async def _handle_bot_message(event: dict, client, cfg_loader, store) -> None:
199
206
  return
200
207
 
201
208
  # Find the project this bot is registered to
209
+ # Match on either the B-prefixed bot_id or the U-prefixed user ID
210
+ _user_id = event.get("user", "")
202
211
  bots = store.get_watched_bots()
203
- bot_info = next((b for b in bots if b["bot_id"] == bot_id), None)
212
+ bot_info = next(
213
+ (b for b in bots if b["bot_id"] in (bot_id, _user_id)),
214
+ None,
215
+ )
204
216
  project_name = (bot_info or {}).get("project_name") or ""
205
217
 
206
218
  # Resolve the project issues directory