@misterhuydo/sentinel 1.5.31 → 1.5.33

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.31",
3
+ "version": "1.5.33",
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.31"
1
+ __version__ = "1.5.33"
@@ -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
  },
@@ -1165,6 +1181,15 @@ _TOOLS = [
1165
1181
  "type": "string",
1166
1182
  "description": "Project short name this bot's issues should be routed to (e.g. '1881', 'elprint'). Infer from context or ask user before calling.",
1167
1183
  },
1184
+ "target_repo": {
1185
+ "type": "string",
1186
+ "description": (
1187
+ "Optional. Repo name to route this bot's issues to (e.g. '1881-SSOLoginWebApp'). "
1188
+ "Required when the project has multiple repos — without it, routing will fail. "
1189
+ "If the project has only one repo, omit this and it will be auto-selected. "
1190
+ "Ask the user which repo if unclear."
1191
+ ),
1192
+ },
1168
1193
  },
1169
1194
  "required": ["user_ids"],
1170
1195
  },
@@ -2473,6 +2498,8 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2473
2498
  if not _project_dirs:
2474
2499
  return json.dumps({"error": "No project directory found."})
2475
2500
 
2501
+ skip_tests = bool(inputs.get("skip_tests", False))
2502
+
2476
2503
  from .repo_task_engine import drop_repo_task as _drop_repo_task
2477
2504
  task_file = _drop_repo_task(
2478
2505
  _project_dirs[0],
@@ -2483,6 +2510,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
2483
2510
  notify_user_ids=notify_ids,
2484
2511
  run_at=run_at_dt,
2485
2512
  origin_channel=channel,
2513
+ skip_tests=skip_tests,
2486
2514
  )
2487
2515
  logger.info(
2488
2516
  "Boss repo_task: dropped %s for user %s (repo=%s, run_at=%s)",
@@ -3076,6 +3104,18 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
3076
3104
  "action_needed": "Ask the user to specify the project, then retry with project filled in.",
3077
3105
  })
3078
3106
 
3107
+ target_repo_arg = (inputs.get("target_repo") or "").strip()
3108
+ # Fuzzy-match target_repo against known repos
3109
+ resolved_target_repo = ""
3110
+ if target_repo_arg and hasattr(cfg_loader, "repos"):
3111
+ if target_repo_arg in cfg_loader.repos:
3112
+ resolved_target_repo = target_repo_arg
3113
+ else:
3114
+ for rname in cfg_loader.repos:
3115
+ if target_repo_arg.lower() in rname.lower() or rname.lower() in target_repo_arg.lower():
3116
+ resolved_target_repo = rname
3117
+ break
3118
+
3079
3119
  results = []
3080
3120
  for uid in user_ids:
3081
3121
  if not slack_client:
@@ -3088,9 +3128,9 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
3088
3128
  results.append({"user_id": uid, "status": "skipped", "reason": "not a bot — only bots can be watched passively"})
3089
3129
  continue
3090
3130
  bot_name = user.get("real_name") or user.get("name") or uid
3091
- store.add_watched_bot(uid, bot_name, added_by="boss", project_name=resolved_project)
3092
- logger.info("Boss: now watching bot %s (%s) → project '%s'", bot_name, uid, resolved_project or "unset")
3093
- results.append({"user_id": uid, "bot_name": bot_name, "project": resolved_project, "status": "watching"})
3131
+ store.add_watched_bot(uid, bot_name, added_by="boss", project_name=resolved_project, target_repo=resolved_target_repo)
3132
+ logger.info("Boss: now watching bot %s (%s) → project '%s', repo '%s'", bot_name, uid, resolved_project or "unset", resolved_target_repo or "auto")
3133
+ results.append({"user_id": uid, "bot_name": bot_name, "project": resolved_project, "target_repo": resolved_target_repo or "auto", "status": "watching"})
3094
3134
  except Exception as e:
3095
3135
  results.append({"user_id": uid, "status": "error", "reason": str(e)})
3096
3136
  return json.dumps({"results": results})
@@ -214,6 +214,7 @@ async def _handle_bot_message(event: dict, client, cfg_loader, store) -> None:
214
214
  None,
215
215
  )
216
216
  project_name = (bot_info or {}).get("project_name") or ""
217
+ target_repo = (bot_info or {}).get("target_repo") or ""
217
218
 
218
219
  # Resolve the project issues directory
219
220
  workspace = _Path(cfg_loader.sentinel.workspace_dir).parent
@@ -238,9 +239,11 @@ async def _handle_bot_message(event: dict, client, cfg_loader, store) -> None:
238
239
 
239
240
  uid = _uuid.uuid4().hex[:8]
240
241
  fname = f"bot-{project_name or 'unknown'}-{uid}.txt"
242
+ target_line = f"TARGET_REPO: {target_repo}\n" if target_repo else ""
241
243
  content = (
242
244
  f"SOURCE: Slack bot {bot_id} in channel {channel}\n"
243
- f"SLACK_TS: {ts}\n\n"
245
+ f"SLACK_TS: {ts}\n"
246
+ f"{target_line}\n"
244
247
  f"{text}"
245
248
  )
246
249
  (issues_dir / fname).write_text(content, encoding="utf-8")
@@ -99,6 +99,7 @@ class StateStore:
99
99
  ("add_fix_outcome", "ALTER TABLE fixes ADD COLUMN fix_outcome TEXT"),
100
100
  ("add_marker_seen_at", "ALTER TABLE fixes ADD COLUMN marker_seen_at TEXT"),
101
101
  ("add_watched_bots_project", "ALTER TABLE watched_bots ADD COLUMN project_name TEXT"),
102
+ ("add_watched_bots_target_repo", "ALTER TABLE watched_bots ADD COLUMN target_repo TEXT"),
102
103
  ("add_alert_thread_ts", "ALTER TABLE errors ADD COLUMN alert_thread_ts TEXT"),
103
104
  ("add_alert_channel", "ALTER TABLE errors ADD COLUMN alert_channel TEXT"),
104
105
  # knowledge_cache was originally created with a 'cached_at' column; rebuilt with 'expires_at'
@@ -408,13 +409,13 @@ class StateStore:
408
409
  "(bot_id TEXT PRIMARY KEY, bot_name TEXT, added_by TEXT, added_at TEXT)"
409
410
  )
410
411
 
411
- def add_watched_bot(self, bot_id: str, bot_name: str, added_by: str = "config", project_name: str = ""):
412
+ def add_watched_bot(self, bot_id: str, bot_name: str, added_by: str = "config", project_name: str = "", target_repo: str = ""):
412
413
  with self._conn() as conn:
413
414
  self._ensure_watched_bots_table(conn)
414
415
  conn.execute(
415
- "INSERT OR REPLACE INTO watched_bots (bot_id, bot_name, added_by, added_at, project_name) "
416
- "VALUES (?, ?, ?, ?, ?)",
417
- (bot_id, bot_name, added_by, _now(), project_name or None),
416
+ "INSERT OR REPLACE INTO watched_bots (bot_id, bot_name, added_by, added_at, project_name, target_repo) "
417
+ "VALUES (?, ?, ?, ?, ?, ?)",
418
+ (bot_id, bot_name, added_by, _now(), project_name or None, target_repo or None),
418
419
  )
419
420
 
420
421
  def remove_watched_bot(self, bot_id: str) -> bool: