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