@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 +1 @@
|
|
|
1
|
-
__version__ = "1.5.
|
|
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"
|
|
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
|
-
|
|
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 —
|
|
143
|
-
|
|
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(
|
|
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
|