@misterhuydo/sentinel 1.5.11 → 1.5.12
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/.cairn/session.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"message": "Auto-checkpoint at 2026-04-08T13:
|
|
3
|
-
"checkpoint_at": "2026-04-08T13:
|
|
2
|
+
"message": "Auto-checkpoint at 2026-04-08T13:37:58.168Z",
|
|
3
|
+
"checkpoint_at": "2026-04-08T13:37:58.252Z",
|
|
4
4
|
"active_files": [
|
|
5
5
|
"J:\\Projects\\Sentinel\\cli\\bin\\sentinel.js",
|
|
6
6
|
"J:\\Projects\\Sentinel\\cli\\lib\\test.js",
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
"[2026-04-08] git-snapshot: .cairn/session.json | 20 ++-\n .claude/settings.local.json | 47 ++++++-\n cli/.cairn/.hint-lock | 2 +-\n cli/.cairn/minify-map.json | 8 +-\n cli/.cairn/session.json | 22 +++-\n cli/.cairn/views/62a614_bundle.js | 5 +-\n cli/lib/.cairn/minify-map.json | 6 +\n cli/lib/.cairn/views/fb78ac_upgrade.js | 37 +++++-\n cli/lib/.cairn/views/fc4a1a_add.js | 215 +++++++++++++++++++++++++--------\n 9 files changed, 296 insertions(+), 66 deletions(-) | status: M ../.cairn/session.json\n M ../.claude/settings.local.json\n M .cairn/.hint-lock\n M .cairn/minify-map.json\n M .cairn/session.json\n M .cairn/views/62a614_bundle.js\n M lib/.cairn/minify-map.json\n M lib/.cairn/views/fb78ac_upgrade.js\n M lib/.cairn/views/fc4a1a_add.js\n?? ../.cairn/.cairn-project\n?? ../.cairn/memory/\n?? ../.cairn/minify-map.json\n?? ../.cairn/views/\n?? .cairn/views/23edf4_sentinel_boss.py\n?? .cairn/views/7802b9_cicd_trigger.py\n?? .cairn/views/ac3df4_repo_task_engine.py\n?? lib/.cairn/views/2a85cc_init.js\n?? lib/.cairn/views/e26996_slack-setup.js\n?? ../scripts/fix_ask_codebase_context.py\n?? ../scripts/fix_ask_codebase_stdin.py\n?? ../scripts/fix_chain_slack.py\n?? ../scripts/fix_fstring.py\n?? ../scripts/fix_knowledge_cache.py\n?? ../scripts/fix_knowledge_cache_staleness.py\n?? ../scripts/fix_merge_confirm.py\n?? ../scripts/fix_permission_messages.py\n?? ../scripts/fix_pr_check_head_detect.py\n?? ../scripts/fix_pr_msg_newlines.py\n?? ../scripts/fix_pr_tracking_boss.py\n?? ../scripts/fix_pr_tracking_db.py\n?? ../scripts/fix_pr_tracking_main.py\n?? ../scripts/fix_project_isolation.py\n?? ../scripts/fix_system_prompt.py\n?? ../scripts/fix_two_bugs.py\n?? ../scripts/patch_chain_release.py",
|
|
12
12
|
"[2026-04-08] git-snapshot: .cairn/session.json | 20 ++-\n .claude/settings.local.json | 47 ++++++-\n cli/.cairn/.hint-lock | 2 +-\n cli/.cairn/minify-map.json | 8 +-\n cli/.cairn/session.json | 21 +++-\n cli/.cairn/views/62a614_bundle.js | 5 +-\n cli/lib/.cairn/minify-map.json | 6 +\n cli/lib/.cairn/views/fb78ac_upgrade.js | 37 +++++-\n cli/lib/.cairn/views/fc4a1a_add.js | 215 +++++++++++++++++++++++++--------\n 9 files changed, 295 insertions(+), 66 deletions(-) | status: M ../.cairn/session.json\n M ../.claude/settings.local.json\n M .cairn/.hint-lock\n M .cairn/minify-map.json\n M .cairn/session.json\n M .cairn/views/62a614_bundle.js\n M lib/.cairn/minify-map.json\n M lib/.cairn/views/fb78ac_upgrade.js\n M lib/.cairn/views/fc4a1a_add.js\n?? ../.cairn/.cairn-project\n?? ../.cairn/memory/\n?? ../.cairn/minify-map.json\n?? ../.cairn/views/\n?? .cairn/views/23edf4_sentinel_boss.py\n?? .cairn/views/7802b9_cicd_trigger.py\n?? .cairn/views/ac3df4_repo_task_engine.py\n?? lib/.cairn/views/2a85cc_init.js\n?? lib/.cairn/views/e26996_slack-setup.js\n?? ../scripts/fix_ask_codebase_context.py\n?? ../scripts/fix_ask_codebase_stdin.py\n?? ../scripts/fix_chain_slack.py\n?? ../scripts/fix_fstring.py\n?? ../scripts/fix_knowledge_cache.py\n?? ../scripts/fix_knowledge_cache_staleness.py\n?? ../scripts/fix_merge_confirm.py\n?? ../scripts/fix_permission_messages.py\n?? ../scripts/fix_pr_check_head_detect.py\n?? ../scripts/fix_pr_msg_newlines.py\n?? ../scripts/fix_pr_tracking_boss.py\n?? ../scripts/fix_pr_tracking_db.py\n?? ../scripts/fix_pr_tracking_main.py\n?? ../scripts/fix_project_isolation.py\n?? ../scripts/fix_system_prompt.py\n?? ../scripts/fix_two_bugs.py\n?? ../scripts/patch_chain_release.py",
|
|
13
13
|
"[2026-04-08] git-snapshot: .cairn/session.json | 20 ++-\n .claude/settings.local.json | 47 ++++++-\n cli/.cairn/.hint-lock | 2 +-\n cli/.cairn/minify-map.json | 8 +-\n cli/.cairn/session.json | 22 +++-\n cli/.cairn/views/62a614_bundle.js | 5 +-\n cli/lib/.cairn/minify-map.json | 6 +\n cli/lib/.cairn/views/fb78ac_upgrade.js | 37 +++++-\n cli/lib/.cairn/views/fc4a1a_add.js | 215 +++++++++++++++++++++++++--------\n 9 files changed, 296 insertions(+), 66 deletions(-) | status: M ../.cairn/session.json\n M ../.claude/settings.local.json\n M .cairn/.hint-lock\n M .cairn/minify-map.json\n M .cairn/session.json\n M .cairn/views/62a614_bundle.js\n M lib/.cairn/minify-map.json\n M lib/.cairn/views/fb78ac_upgrade.js\n M lib/.cairn/views/fc4a1a_add.js\n?? ../.cairn/.cairn-project\n?? ../.cairn/memory/\n?? ../.cairn/minify-map.json\n?? ../.cairn/views/\n?? .cairn/views/23edf4_sentinel_boss.py\n?? .cairn/views/7802b9_cicd_trigger.py\n?? .cairn/views/ac3df4_repo_task_engine.py\n?? lib/.cairn/views/2a85cc_init.js\n?? lib/.cairn/views/e26996_slack-setup.js\n?? ../scripts/fix_ask_codebase_context.py\n?? ../scripts/fix_ask_codebase_stdin.py\n?? ../scripts/fix_chain_slack.py\n?? ../scripts/fix_fstring.py\n?? ../scripts/fix_knowledge_cache.py\n?? ../scripts/fix_knowledge_cache_staleness.py\n?? ../scripts/fix_merge_confirm.py\n?? ../scripts/fix_permission_messages.py\n?? ../scripts/fix_pr_check_head_detect.py\n?? ../scripts/fix_pr_msg_newlines.py\n?? ../scripts/fix_pr_tracking_boss.py\n?? ../scripts/fix_pr_tracking_db.py\n?? ../scripts/fix_pr_tracking_main.py\n?? ../scripts/fix_project_isolation.py\n?? ../scripts/fix_system_prompt.py\n?? ../scripts/fix_two_bugs.py\n?? ../scripts/patch_chain_release.py",
|
|
14
|
-
"[2026-04-08] git-snapshot: .cairn/session.json | 29 ++++-\n .claude/settings.local.json | 47 ++++++-\n cli/.cairn/.hint-lock | 2 +-\n cli/.cairn/minify-map.json | 8 +-\n cli/.cairn/session.json | 23 +++-\n cli/.cairn/views/62a614_bundle.js | 5 +-\n cli/lib/.cairn/minify-map.json | 6 +\n cli/lib/.cairn/views/fb78ac_upgrade.js | 37 +++++-\n cli/lib/.cairn/views/fc4a1a_add.js | 215 +++++++++++++++++++++++++--------\n 9 files changed, 306 insertions(+), 66 deletions(-) | status: M ../.cairn/session.json\n M ../.claude/settings.local.json\n M .cairn/.hint-lock\n M .cairn/minify-map.json\n M .cairn/session.json\n M .cairn/views/62a614_bundle.js\n M lib/.cairn/minify-map.json\n M lib/.cairn/views/fb78ac_upgrade.js\n M lib/.cairn/views/fc4a1a_add.js\n?? ../.cairn/.cairn-project\n?? ../.cairn/memory/\n?? ../.cairn/minify-map.json\n?? ../.cairn/views/\n?? .cairn/views/23edf4_sentinel_boss.py\n?? .cairn/views/7802b9_cicd_trigger.py\n?? .cairn/views/ac3df4_repo_task_engine.py\n?? lib/.cairn/views/2a85cc_init.js\n?? lib/.cairn/views/e26996_slack-setup.js\n?? ../scripts/fix_ask_codebase_context.py\n?? ../scripts/fix_ask_codebase_stdin.py\n?? ../scripts/fix_chain_slack.py\n?? ../scripts/fix_fstring.py\n?? ../scripts/fix_knowledge_cache.py\n?? ../scripts/fix_knowledge_cache_staleness.py\n?? ../scripts/fix_merge_confirm.py\n?? ../scripts/fix_permission_messages.py\n?? ../scripts/fix_pr_check_head_detect.py\n?? ../scripts/fix_pr_msg_newlines.py\n?? ../scripts/fix_pr_tracking_boss.py\n?? ../scripts/fix_pr_tracking_db.py\n?? ../scripts/fix_pr_tracking_main.py\n?? ../scripts/fix_project_isolation.py\n?? ../scripts/fix_system_prompt.py\n?? ../scripts/fix_two_bugs.py\n?? ../scripts/patch_chain_release.py"
|
|
14
|
+
"[2026-04-08] git-snapshot: .cairn/session.json | 29 ++++-\n .claude/settings.local.json | 47 ++++++-\n cli/.cairn/.hint-lock | 2 +-\n cli/.cairn/minify-map.json | 8 +-\n cli/.cairn/session.json | 23 +++-\n cli/.cairn/views/62a614_bundle.js | 5 +-\n cli/lib/.cairn/minify-map.json | 6 +\n cli/lib/.cairn/views/fb78ac_upgrade.js | 37 +++++-\n cli/lib/.cairn/views/fc4a1a_add.js | 215 +++++++++++++++++++++++++--------\n 9 files changed, 306 insertions(+), 66 deletions(-) | status: M ../.cairn/session.json\n M ../.claude/settings.local.json\n M .cairn/.hint-lock\n M .cairn/minify-map.json\n M .cairn/session.json\n M .cairn/views/62a614_bundle.js\n M lib/.cairn/minify-map.json\n M lib/.cairn/views/fb78ac_upgrade.js\n M lib/.cairn/views/fc4a1a_add.js\n?? ../.cairn/.cairn-project\n?? ../.cairn/memory/\n?? ../.cairn/minify-map.json\n?? ../.cairn/views/\n?? .cairn/views/23edf4_sentinel_boss.py\n?? .cairn/views/7802b9_cicd_trigger.py\n?? .cairn/views/ac3df4_repo_task_engine.py\n?? lib/.cairn/views/2a85cc_init.js\n?? lib/.cairn/views/e26996_slack-setup.js\n?? ../scripts/fix_ask_codebase_context.py\n?? ../scripts/fix_ask_codebase_stdin.py\n?? ../scripts/fix_chain_slack.py\n?? ../scripts/fix_fstring.py\n?? ../scripts/fix_knowledge_cache.py\n?? ../scripts/fix_knowledge_cache_staleness.py\n?? ../scripts/fix_merge_confirm.py\n?? ../scripts/fix_permission_messages.py\n?? ../scripts/fix_pr_check_head_detect.py\n?? ../scripts/fix_pr_msg_newlines.py\n?? ../scripts/fix_pr_tracking_boss.py\n?? ../scripts/fix_pr_tracking_db.py\n?? ../scripts/fix_pr_tracking_main.py\n?? ../scripts/fix_project_isolation.py\n?? ../scripts/fix_system_prompt.py\n?? ../scripts/fix_two_bugs.py\n?? ../scripts/patch_chain_release.py",
|
|
15
|
+
"[2026-04-08] git-snapshot: .cairn/session.json | 29 ++++-\n .claude/settings.local.json | 47 ++++++-\n cli/.cairn/.hint-lock | 2 +-\n cli/.cairn/minify-map.json | 8 +-\n cli/.cairn/session.json | 24 +++-\n cli/.cairn/views/62a614_bundle.js | 5 +-\n cli/lib/.cairn/minify-map.json | 6 +\n cli/lib/.cairn/views/fb78ac_upgrade.js | 37 +++++-\n cli/lib/.cairn/views/fc4a1a_add.js | 215 +++++++++++++++++++++++++--------\n 9 files changed, 307 insertions(+), 66 deletions(-) | status: M ../.cairn/session.json\n M ../.claude/settings.local.json\n M .cairn/.hint-lock\n M .cairn/minify-map.json\n M .cairn/session.json\n M .cairn/views/62a614_bundle.js\n M lib/.cairn/minify-map.json\n M lib/.cairn/views/fb78ac_upgrade.js\n M lib/.cairn/views/fc4a1a_add.js\n?? ../.cairn/.cairn-project\n?? ../.cairn/memory/\n?? ../.cairn/minify-map.json\n?? ../.cairn/views/\n?? .cairn/views/23edf4_sentinel_boss.py\n?? .cairn/views/7802b9_cicd_trigger.py\n?? .cairn/views/ac3df4_repo_task_engine.py\n?? lib/.cairn/views/2a85cc_init.js\n?? lib/.cairn/views/e26996_slack-setup.js\n?? ../scripts/fix_ask_codebase_context.py\n?? ../scripts/fix_ask_codebase_stdin.py\n?? ../scripts/fix_chain_slack.py\n?? ../scripts/fix_fstring.py\n?? ../scripts/fix_knowledge_cache.py\n?? ../scripts/fix_knowledge_cache_staleness.py\n?? ../scripts/fix_merge_confirm.py\n?? ../scripts/fix_permission_messages.py\n?? ../scripts/fix_pr_check_head_detect.py\n?? ../scripts/fix_pr_msg_newlines.py\n?? ../scripts/fix_pr_tracking_boss.py\n?? ../scripts/fix_pr_tracking_db.py\n?? ../scripts/fix_pr_tracking_main.py\n?? ../scripts/fix_project_isolation.py\n?? ../scripts/fix_system_prompt.py\n?? ../scripts/fix_two_bugs.py\n?? ../scripts/patch_chain_release.py",
|
|
16
|
+
"[2026-04-08] git-snapshot: .cairn/session.json | 29 ++++-\n .claude/settings.local.json | 47 ++++++-\n cli/.cairn/.hint-lock | 2 +-\n cli/.cairn/minify-map.json | 8 +-\n cli/.cairn/session.json | 25 +++-\n cli/.cairn/views/62a614_bundle.js | 5 +-\n cli/lib/.cairn/minify-map.json | 6 +\n cli/lib/.cairn/views/fb78ac_upgrade.js | 37 +++++-\n cli/lib/.cairn/views/fc4a1a_add.js | 215 +++++++++++++++++++++++++--------\n 9 files changed, 308 insertions(+), 66 deletions(-) | status: M ../.cairn/session.json\n M ../.claude/settings.local.json\n M .cairn/.hint-lock\n M .cairn/minify-map.json\n M .cairn/session.json\n M .cairn/views/62a614_bundle.js\n M lib/.cairn/minify-map.json\n M lib/.cairn/views/fb78ac_upgrade.js\n M lib/.cairn/views/fc4a1a_add.js\n?? ../.cairn/.cairn-project\n?? ../.cairn/memory/\n?? ../.cairn/minify-map.json\n?? ../.cairn/views/\n?? .cairn/views/23edf4_sentinel_boss.py\n?? .cairn/views/7802b9_cicd_trigger.py\n?? .cairn/views/ac3df4_repo_task_engine.py\n?? lib/.cairn/views/2a85cc_init.js\n?? lib/.cairn/views/e26996_slack-setup.js\n?? ../scripts/fix_ask_codebase_context.py\n?? ../scripts/fix_ask_codebase_stdin.py\n?? ../scripts/fix_chain_slack.py\n?? ../scripts/fix_fstring.py\n?? ../scripts/fix_knowledge_cache.py\n?? ../scripts/fix_knowledge_cache_staleness.py\n?? ../scripts/fix_merge_confirm.py\n?? ../scripts/fix_permission_messages.py\n?? ../scripts/fix_pr_check_head_detect.py\n?? ../scripts/fix_pr_msg_newlines.py\n?? ../scripts/fix_pr_tracking_boss.py\n?? ../scripts/fix_pr_tracking_db.py\n?? ../scripts/fix_pr_tracking_main.py\n?? ../scripts/fix_project_isolation.py\n?? ../scripts/fix_system_prompt.py\n?? ../scripts/fix_two_bugs.py\n?? ../scripts/patch_chain_release.py",
|
|
17
|
+
"[2026-04-08] git-snapshot: .cairn/session.json | 29 ++++-\n .claude/settings.local.json | 47 ++++++-\n cli/.cairn/.hint-lock | 2 +-\n cli/.cairn/minify-map.json | 8 +-\n cli/.cairn/session.json | 26 +++-\n cli/.cairn/views/62a614_bundle.js | 5 +-\n cli/lib/.cairn/minify-map.json | 6 +\n cli/lib/.cairn/views/fb78ac_upgrade.js | 37 +++++-\n cli/lib/.cairn/views/fc4a1a_add.js | 215 +++++++++++++++++++++++++--------\n 9 files changed, 309 insertions(+), 66 deletions(-) | status: M ../.cairn/session.json\n M ../.claude/settings.local.json\n M .cairn/.hint-lock\n M .cairn/minify-map.json\n M .cairn/session.json\n M .cairn/views/62a614_bundle.js\n M lib/.cairn/minify-map.json\n M lib/.cairn/views/fb78ac_upgrade.js\n M lib/.cairn/views/fc4a1a_add.js\n?? ../.cairn/.cairn-project\n?? ../.cairn/memory/\n?? ../.cairn/minify-map.json\n?? ../.cairn/views/\n?? .cairn/views/23edf4_sentinel_boss.py\n?? .cairn/views/7802b9_cicd_trigger.py\n?? .cairn/views/ac3df4_repo_task_engine.py\n?? lib/.cairn/views/2a85cc_init.js\n?? lib/.cairn/views/e26996_slack-setup.js\n?? ../scripts/fix_ask_codebase_context.py\n?? ../scripts/fix_ask_codebase_stdin.py\n?? ../scripts/fix_chain_slack.py\n?? ../scripts/fix_fstring.py\n?? ../scripts/fix_knowledge_cache.py\n?? ../scripts/fix_knowledge_cache_staleness.py\n?? ../scripts/fix_merge_confirm.py\n?? ../scripts/fix_permission_messages.py\n?? ../scripts/fix_pr_check_head_detect.py\n?? ../scripts/fix_pr_msg_newlines.py\n?? ../scripts/fix_pr_tracking_boss.py\n?? ../scripts/fix_pr_tracking_db.py\n?? ../scripts/fix_pr_tracking_main.py\n?? ../scripts/fix_project_isolation.py\n?? ../scripts/fix_system_prompt.py\n?? ../scripts/fix_two_bugs.py\n?? ../scripts/patch_chain_release.py",
|
|
18
|
+
"[2026-04-08] git-snapshot: .cairn/session.json | 29 ++++-\n .claude/settings.local.json | 47 ++++++-\n cli/.cairn/.hint-lock | 2 +-\n cli/.cairn/minify-map.json | 8 +-\n cli/.cairn/session.json | 27 ++++-\n cli/.cairn/views/62a614_bundle.js | 5 +-\n cli/lib/.cairn/minify-map.json | 6 +\n cli/lib/.cairn/views/fb78ac_upgrade.js | 37 +++++-\n cli/lib/.cairn/views/fc4a1a_add.js | 215 +++++++++++++++++++++++++--------\n 9 files changed, 310 insertions(+), 66 deletions(-) | status: M ../.cairn/session.json\n M ../.claude/settings.local.json\n M .cairn/.hint-lock\n M .cairn/minify-map.json\n M .cairn/session.json\n M .cairn/views/62a614_bundle.js\n M lib/.cairn/minify-map.json\n M lib/.cairn/views/fb78ac_upgrade.js\n M lib/.cairn/views/fc4a1a_add.js\n?? ../.cairn/.cairn-project\n?? ../.cairn/memory/\n?? ../.cairn/minify-map.json\n?? ../.cairn/views/\n?? .cairn/views/23edf4_sentinel_boss.py\n?? .cairn/views/7802b9_cicd_trigger.py\n?? .cairn/views/ac3df4_repo_task_engine.py\n?? lib/.cairn/views/2a85cc_init.js\n?? lib/.cairn/views/e26996_slack-setup.js\n?? ../scripts/fix_ask_codebase_context.py\n?? ../scripts/fix_ask_codebase_stdin.py\n?? ../scripts/fix_chain_slack.py\n?? ../scripts/fix_fstring.py\n?? ../scripts/fix_knowledge_cache.py\n?? ../scripts/fix_knowledge_cache_staleness.py\n?? ../scripts/fix_merge_confirm.py\n?? ../scripts/fix_permission_messages.py\n?? ../scripts/fix_pr_check_head_detect.py\n?? ../scripts/fix_pr_msg_newlines.py\n?? ../scripts/fix_pr_tracking_boss.py\n?? ../scripts/fix_pr_tracking_db.py\n?? ../scripts/fix_pr_tracking_main.py\n?? ../scripts/fix_project_isolation.py\n?? ../scripts/fix_system_prompt.py\n?? ../scripts/fix_two_bugs.py\n?? ../scripts/patch_chain_release.py"
|
|
15
19
|
],
|
|
16
20
|
"mtime_snapshot": {
|
|
17
21
|
"J:\\Projects\\Sentinel\\cli\\bin\\sentinel.js": 1774252515044.4768,
|
package/package.json
CHANGED
package/python/sentinel/main.py
CHANGED
|
@@ -176,23 +176,59 @@ def _run_cascade(repo, sentinel, cfg_loader):
|
|
|
176
176
|
|
|
177
177
|
|
|
178
178
|
async def _handle_error(event: ErrorEvent, cfg_loader: ConfigLoader, store: StateStore):
|
|
179
|
+
from .notify import notify_error_detected, slack_thread_reply as _thread_reply
|
|
179
180
|
sentinel = cfg_loader.sentinel
|
|
180
181
|
|
|
181
182
|
repo = route(event, cfg_loader.repos)
|
|
182
183
|
if not repo:
|
|
183
184
|
return
|
|
184
185
|
|
|
186
|
+
# ── Dismissed errors — skip silently (human explicitly said so) ───────────
|
|
187
|
+
if store.is_dismissed(event.fingerprint):
|
|
188
|
+
logger.debug("Error %s is dismissed — skipping", event.fingerprint)
|
|
189
|
+
return
|
|
190
|
+
|
|
185
191
|
if Path("SENTINEL_PAUSE").exists():
|
|
186
192
|
logger.info("SENTINEL_PAUSE present — fix activity halted")
|
|
187
193
|
return
|
|
188
194
|
|
|
195
|
+
# ── Determine what we can do before alerting ──────────────────────────────
|
|
196
|
+
cannot_fix = event.is_infra_issue or (event.severity == "CRITICAL" and repo.auto_publish)
|
|
197
|
+
fix_status = "cannot_fix" if cannot_fix else ("will_fix_pr" if not repo.auto_publish else "attempting")
|
|
198
|
+
|
|
199
|
+
stack_lines = getattr(event, "stack_trace", None)
|
|
200
|
+
stack_preview = ""
|
|
201
|
+
if isinstance(stack_lines, list):
|
|
202
|
+
stack_preview = "\n".join(stack_lines[:5])
|
|
203
|
+
elif isinstance(stack_lines, str):
|
|
204
|
+
stack_preview = "\n".join(stack_lines.splitlines()[:5])
|
|
205
|
+
|
|
206
|
+
# ── Alert the channel on every new/recurring occurrence ───────────────────
|
|
207
|
+
thread_ts = notify_error_detected(
|
|
208
|
+
sentinel,
|
|
209
|
+
fingerprint=event.fingerprint,
|
|
210
|
+
source=event.source,
|
|
211
|
+
message=event.message,
|
|
212
|
+
severity=event.severity,
|
|
213
|
+
repo_name=repo.repo_name,
|
|
214
|
+
fix_status=fix_status,
|
|
215
|
+
stack_preview=stack_preview,
|
|
216
|
+
)
|
|
217
|
+
if thread_ts:
|
|
218
|
+
store.store_alert_thread(event.fingerprint, thread_ts, sentinel.slack_channel)
|
|
219
|
+
|
|
220
|
+
def _progress(msg: str) -> None:
|
|
221
|
+
if thread_ts:
|
|
222
|
+
_thread_reply(sentinel.slack_bot_token, sentinel.slack_channel, thread_ts, msg)
|
|
223
|
+
|
|
224
|
+
# ── Cases where we alert but don't attempt a fix ──────────────────────────
|
|
189
225
|
if event.is_infra_issue:
|
|
190
|
-
logger.info("Infra issue for %s —
|
|
226
|
+
logger.info("Infra issue for %s — alerted, no fix", event.fingerprint)
|
|
191
227
|
store.record_fix(event.fingerprint, "skipped", repo_name=repo.repo_name)
|
|
192
228
|
return
|
|
193
229
|
|
|
194
230
|
if event.severity == "CRITICAL" and repo.auto_publish:
|
|
195
|
-
logger.warning("CRITICAL in auto-publish repo '%s' —
|
|
231
|
+
logger.warning("CRITICAL in auto-publish repo '%s' — alerted, human review needed", repo.repo_name)
|
|
196
232
|
store.record_fix(event.fingerprint, "skipped", repo_name=repo.repo_name)
|
|
197
233
|
return
|
|
198
234
|
|
|
@@ -200,18 +236,21 @@ async def _handle_error(event: ErrorEvent, cfg_loader: ConfigLoader, store: Stat
|
|
|
200
236
|
logger.debug("Fix already attempted recently for %s", event.fingerprint)
|
|
201
237
|
return
|
|
202
238
|
|
|
239
|
+
# ── Generate fix ──────────────────────────────────────────────────────────
|
|
240
|
+
_progress(":brain: Analyzing with Claude Code...")
|
|
203
241
|
patches_dir = Path(sentinel.workspace_dir).resolve() / "patches"
|
|
204
242
|
status, patch_path, marker = generate_fix(event, repo, sentinel, patches_dir, store)
|
|
205
243
|
|
|
206
244
|
if status != "patch" or patch_path is None:
|
|
207
245
|
outcome = "skipped" if status in ("skip", "needs_human") else "failed"
|
|
208
246
|
store.record_fix(event.fingerprint, outcome, repo_name=repo.repo_name)
|
|
209
|
-
# For log-detected errors: NEEDS_HUMAN -> DM/channel; SKIP -> email only (not spam)
|
|
210
247
|
if status == "needs_human":
|
|
248
|
+
_progress(f":warning: Needs human input — {marker}")
|
|
211
249
|
notify_fix_blocked(sentinel, event.source, event.message,
|
|
212
250
|
reason=marker, repo_name=repo.repo_name,
|
|
213
251
|
submitter_user_id="")
|
|
214
252
|
else:
|
|
253
|
+
_progress(f":x: Cannot generate fix — Claude returned {status.upper()}")
|
|
215
254
|
send_failure_notification(sentinel, {
|
|
216
255
|
"source": event.source,
|
|
217
256
|
"message": event.message,
|
|
@@ -221,6 +260,8 @@ async def _handle_error(event: ErrorEvent, cfg_loader: ConfigLoader, store: Stat
|
|
|
221
260
|
})
|
|
222
261
|
return
|
|
223
262
|
|
|
263
|
+
# ── Apply fix ─────────────────────────────────────────────────────────────
|
|
264
|
+
_progress(":gear: Applying patch and running tests...")
|
|
224
265
|
try:
|
|
225
266
|
commit_status, commit_hash = apply_and_commit(event, patch_path, repo, sentinel)
|
|
226
267
|
except MavenAuthError as e:
|
|
@@ -228,6 +269,7 @@ async def _handle_error(event: ErrorEvent, cfg_loader: ConfigLoader, store: Stat
|
|
|
228
269
|
logger.error("Nexus auth failure applying fix for %s: %s", event.fingerprint, str(e))
|
|
229
270
|
notify_nexus_auth_failure(sentinel, repo.repo_name, f"fix {event.fingerprint[:8]}", e.output)
|
|
230
271
|
store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
|
|
272
|
+
_progress(":x: Nexus auth failure — check credentials")
|
|
231
273
|
return
|
|
232
274
|
except MissingToolError as e:
|
|
233
275
|
logger.warning("Missing tool for %s: %s", event.source, e)
|
|
@@ -238,13 +280,17 @@ async def _handle_error(event: ErrorEvent, cfg_loader: ConfigLoader, store: Stat
|
|
|
238
280
|
logger.error("Still missing tool after auto-install: %s", e2)
|
|
239
281
|
notify_missing_tool(sentinel, e2.tool, repo.repo_name, event.source, "")
|
|
240
282
|
store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
|
|
283
|
+
_progress(f":x: Missing tool `{e2.tool}` — auto-install failed")
|
|
241
284
|
return
|
|
242
285
|
else:
|
|
243
286
|
notify_missing_tool(sentinel, e.tool, repo.repo_name, event.source, "")
|
|
244
287
|
store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
|
|
288
|
+
_progress(f":x: Missing tool `{e.tool}` — cannot apply fix")
|
|
245
289
|
return
|
|
290
|
+
|
|
246
291
|
if commit_status != "committed":
|
|
247
292
|
store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
|
|
293
|
+
_progress(":x: Patch generated but commit/tests failed")
|
|
248
294
|
send_failure_notification(sentinel, {
|
|
249
295
|
"source": event.source,
|
|
250
296
|
"message": event.message,
|
|
@@ -266,6 +312,11 @@ async def _handle_error(event: ErrorEvent, cfg_loader: ConfigLoader, store: Stat
|
|
|
266
312
|
sentinel_marker=marker,
|
|
267
313
|
)
|
|
268
314
|
|
|
315
|
+
if pr_url:
|
|
316
|
+
_progress(f":white_check_mark: Fix committed — PR opened: {pr_url}")
|
|
317
|
+
else:
|
|
318
|
+
_progress(f":white_check_mark: Fix pushed to `{branch}` (`{commit_hash[:8]}`)")
|
|
319
|
+
|
|
269
320
|
send_fix_notification(sentinel, {
|
|
270
321
|
"source": event.source,
|
|
271
322
|
"severity": event.severity,
|
|
@@ -336,7 +387,7 @@ async def _handle_issue(event: IssueEvent, cfg_loader: ConfigLoader, store: Stat
|
|
|
336
387
|
|
|
337
388
|
def _progress(msg: str) -> None:
|
|
338
389
|
"""Post a threaded reply under the 'Working on' message."""
|
|
339
|
-
_slack_reply(sentinel.slack_bot_token,
|
|
390
|
+
_slack_reply(sentinel.slack_bot_token, _origin_channel, _thread_ts, msg)
|
|
340
391
|
|
|
341
392
|
try:
|
|
342
393
|
patches_dir = Path(sentinel.workspace_dir).resolve() / "patches"
|
|
@@ -1292,15 +1343,16 @@ async def _handle_repo_task(task, repo_cfg, cfg_loader: ConfigLoader, store: Sta
|
|
|
1292
1343
|
|
|
1293
1344
|
sentinel = cfg_loader.sentinel
|
|
1294
1345
|
_submitter = task.submitter_user_id
|
|
1346
|
+
_origin_channel = task.origin_channel or sentinel.slack_channel
|
|
1295
1347
|
_started_msg = (
|
|
1296
1348
|
f":hammer: Working on *<@{_submitter}>*'s request for `{task.repo_name}`\n_{task.message[:120]}_"
|
|
1297
1349
|
) if _submitter else (
|
|
1298
1350
|
f":hammer: Working on repo task for `{task.repo_name}`\n_{task.message[:120]}_"
|
|
1299
1351
|
)
|
|
1300
|
-
_thread_ts = _slack_alert(sentinel.slack_bot_token,
|
|
1352
|
+
_thread_ts = _slack_alert(sentinel.slack_bot_token, _origin_channel, _started_msg)
|
|
1301
1353
|
|
|
1302
1354
|
def _progress(msg: str) -> None:
|
|
1303
|
-
_slack_reply(sentinel.slack_bot_token,
|
|
1355
|
+
_slack_reply(sentinel.slack_bot_token, _origin_channel, _thread_ts, msg)
|
|
1304
1356
|
|
|
1305
1357
|
_loop = asyncio.get_event_loop()
|
|
1306
1358
|
try:
|
|
@@ -1330,34 +1382,34 @@ async def _handle_repo_task(task, repo_cfg, cfg_loader: ConfigLoader, store: Sta
|
|
|
1330
1382
|
if detail and detail.startswith("__cicd__"):
|
|
1331
1383
|
cicd_name = detail[len("__cicd__"):]
|
|
1332
1384
|
_slack_alert(
|
|
1333
|
-
sentinel.slack_bot_token,
|
|
1385
|
+
sentinel.slack_bot_token, _origin_channel,
|
|
1334
1386
|
f"{mentions}:white_check_mark: Done — pushed to `{task.repo_name}/{repo_cfg.branch}` and triggered `{cicd_name}` release.",
|
|
1335
1387
|
)
|
|
1336
1388
|
elif detail: # PR URL
|
|
1337
1389
|
_slack_alert(
|
|
1338
|
-
sentinel.slack_bot_token,
|
|
1390
|
+
sentinel.slack_bot_token, _origin_channel,
|
|
1339
1391
|
f"{mentions}:white_check_mark: Done — PR opened for `{task.repo_name}`: {detail}",
|
|
1340
1392
|
)
|
|
1341
1393
|
else:
|
|
1342
1394
|
_slack_alert(
|
|
1343
|
-
sentinel.slack_bot_token,
|
|
1395
|
+
sentinel.slack_bot_token, _origin_channel,
|
|
1344
1396
|
f"{mentions}:white_check_mark: Done — changes pushed to `{task.repo_name}/{repo_cfg.branch}`.",
|
|
1345
1397
|
)
|
|
1346
1398
|
elif status == "needs_human":
|
|
1347
1399
|
qualified = _boss_qualify_dev_reason(detail, sentinel)
|
|
1348
1400
|
_slack_alert(
|
|
1349
|
-
sentinel.slack_bot_token,
|
|
1401
|
+
sentinel.slack_bot_token, _origin_channel,
|
|
1350
1402
|
f"{mentions}:warning: *Task needs human input* (`{task.repo_name}`)\n{qualified}",
|
|
1351
1403
|
)
|
|
1352
1404
|
elif status == "skip":
|
|
1353
1405
|
qualified = _boss_qualify_dev_reason(detail, sentinel)
|
|
1354
1406
|
_slack_alert(
|
|
1355
|
-
sentinel.slack_bot_token,
|
|
1407
|
+
sentinel.slack_bot_token, _origin_channel,
|
|
1356
1408
|
f"{mentions}:fast_forward: Task skipped for `{task.repo_name}` — {qualified}",
|
|
1357
1409
|
)
|
|
1358
1410
|
else:
|
|
1359
1411
|
_slack_alert(
|
|
1360
|
-
sentinel.slack_bot_token,
|
|
1412
|
+
sentinel.slack_bot_token, _origin_channel,
|
|
1361
1413
|
f"{mentions}:x: Task error for `{task.repo_name}` — {(detail or '')[:200]}",
|
|
1362
1414
|
)
|
|
1363
1415
|
|
|
@@ -222,6 +222,64 @@ def notify_nexus_auth_failure(cfg, repo_name: str, context: str, mvn_output: str
|
|
|
222
222
|
slack_dm(cfg.slack_bot_token, cfg.slack_admin_users[0], "\n".join(lines))
|
|
223
223
|
|
|
224
224
|
|
|
225
|
+
def notify_error_detected(
|
|
226
|
+
cfg,
|
|
227
|
+
fingerprint: str,
|
|
228
|
+
source: str,
|
|
229
|
+
message: str,
|
|
230
|
+
severity: str,
|
|
231
|
+
repo_name: str,
|
|
232
|
+
fix_status: str = "attempting", # "attempting" | "cannot_fix" | "will_fix_pr" | reason string
|
|
233
|
+
stack_preview: str = "",
|
|
234
|
+
) -> str:
|
|
235
|
+
"""
|
|
236
|
+
Post an error-detected alert to the configured channel.
|
|
237
|
+
|
|
238
|
+
Mentions @admin_users + @allowed_users for ERROR/CRITICAL.
|
|
239
|
+
WARN posts without @mention (informational only).
|
|
240
|
+
Returns the Slack thread_ts so progress can be threaded under it.
|
|
241
|
+
"""
|
|
242
|
+
severity_upper = severity.upper()
|
|
243
|
+
|
|
244
|
+
# Severity icon
|
|
245
|
+
icon = {
|
|
246
|
+
"CRITICAL": ":rotating_light:",
|
|
247
|
+
"ERROR": ":red_circle:",
|
|
248
|
+
"WARN": ":large_yellow_circle:",
|
|
249
|
+
}.get(severity_upper, ":large_yellow_circle:")
|
|
250
|
+
|
|
251
|
+
# Mention string — only for ERROR/CRITICAL
|
|
252
|
+
mention_ids = []
|
|
253
|
+
if severity_upper in ("ERROR", "CRITICAL"):
|
|
254
|
+
mention_ids = list(cfg.slack_admin_users or [])
|
|
255
|
+
for uid in (cfg.slack_allowed_users or []):
|
|
256
|
+
if uid not in mention_ids:
|
|
257
|
+
mention_ids.append(uid)
|
|
258
|
+
mention_str = (" ".join(f"<@{uid}>" for uid in mention_ids) + " ") if mention_ids else ""
|
|
259
|
+
|
|
260
|
+
# Fix-status footer
|
|
261
|
+
fix_footer = {
|
|
262
|
+
"attempting": ":hammer: Sentinel is attempting a fix...",
|
|
263
|
+
"will_fix_pr": ":hammer: Sentinel is preparing a fix PR...",
|
|
264
|
+
"cannot_fix": ":no_entry_sign: Sentinel cannot auto-fix this (infra/CRITICAL — human review needed).",
|
|
265
|
+
}.get(fix_status, f":information_source: {fix_status}")
|
|
266
|
+
|
|
267
|
+
lines = [
|
|
268
|
+
f"{mention_str}{icon} *New {severity_upper} — `{source}`*",
|
|
269
|
+
f"`{message[:200]}`",
|
|
270
|
+
]
|
|
271
|
+
if stack_preview:
|
|
272
|
+
lines.append(f"```{stack_preview[:300]}```")
|
|
273
|
+
lines += [
|
|
274
|
+
f"Repo: *{repo_name}* | Fingerprint: `{fingerprint[:8]}`",
|
|
275
|
+
fix_footer,
|
|
276
|
+
f"_Reply \"ignore {fingerprint[:8]}\" to suppress future alerts for this error._",
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
thread_ts = slack_alert(cfg.slack_bot_token, cfg.slack_channel, "\n".join(lines))
|
|
280
|
+
return thread_ts or ""
|
|
281
|
+
|
|
282
|
+
|
|
225
283
|
def notify_fix_blocked(
|
|
226
284
|
cfg,
|
|
227
285
|
source: str,
|
|
@@ -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:")
|
|
37
|
+
_META_PREFIXES = ("REPO:", "TYPE:", "SUBMITTED_BY:", "SUBMITTED_AT:", "RUN_AT:", "NOTIFY:", "ORIGIN_CHANNEL:")
|
|
38
38
|
_TASK_TIMEOUT = 900 # 15 minutes
|
|
39
39
|
|
|
40
40
|
|
|
@@ -50,6 +50,7 @@ class RepoTask:
|
|
|
50
50
|
fingerprint: str = ""
|
|
51
51
|
timestamp: str = ""
|
|
52
52
|
run_at: datetime | None = None # UTC; task is held until this time if set
|
|
53
|
+
origin_channel: str = "" # Slack channel ID where task was requested (DM or group)
|
|
53
54
|
|
|
54
55
|
def __post_init__(self):
|
|
55
56
|
if not self.fingerprint:
|
|
@@ -334,6 +335,7 @@ def drop_repo_task(
|
|
|
334
335
|
submitter_user_id: str = "",
|
|
335
336
|
notify_user_ids: list | None = None,
|
|
336
337
|
run_at: datetime | None = None,
|
|
338
|
+
origin_channel: str = "",
|
|
337
339
|
) -> Path:
|
|
338
340
|
"""Drop a repo task file into <project_dir>/repo-tasks/.
|
|
339
341
|
|
|
@@ -357,6 +359,8 @@ def drop_repo_task(
|
|
|
357
359
|
lines.append(f"RUN_AT: {run_at.astimezone(timezone.utc).isoformat()}")
|
|
358
360
|
if notify_user_ids:
|
|
359
361
|
lines.append(f"NOTIFY: {','.join(notify_user_ids)}")
|
|
362
|
+
if origin_channel:
|
|
363
|
+
lines.append(f"ORIGIN_CHANNEL: {origin_channel}")
|
|
360
364
|
lines += ["", description]
|
|
361
365
|
fpath.write_text("\n".join(lines), encoding="utf-8")
|
|
362
366
|
if run_at:
|
|
@@ -388,6 +392,7 @@ def scan_repo_tasks(project_dir: Path) -> list[RepoTask]:
|
|
|
388
392
|
submitter_user_id = ""
|
|
389
393
|
notify_user_ids: list = []
|
|
390
394
|
run_at: datetime | None = None
|
|
395
|
+
origin_channel = ""
|
|
391
396
|
body_start = 0
|
|
392
397
|
|
|
393
398
|
for i, line in enumerate(lines):
|
|
@@ -415,6 +420,9 @@ def scan_repo_tasks(project_dir: Path) -> list[RepoTask]:
|
|
|
415
420
|
elif upper.startswith("NOTIFY:"):
|
|
416
421
|
notify_user_ids = [u.strip() for u in stripped[7:].split(",") if u.strip()]
|
|
417
422
|
body_start = i + 1
|
|
423
|
+
elif upper.startswith("ORIGIN_CHANNEL:"):
|
|
424
|
+
origin_channel = stripped[15:].strip()
|
|
425
|
+
body_start = i + 1
|
|
418
426
|
elif any(upper.startswith(p) for p in _META_PREFIXES) or not stripped:
|
|
419
427
|
body_start = i + 1
|
|
420
428
|
else:
|
|
@@ -449,6 +457,7 @@ def scan_repo_tasks(project_dir: Path) -> list[RepoTask]:
|
|
|
449
457
|
submitter_user_id=submitter_user_id,
|
|
450
458
|
notify_user_ids=notify_user_ids,
|
|
451
459
|
run_at=run_at,
|
|
460
|
+
origin_channel=origin_channel,
|
|
452
461
|
))
|
|
453
462
|
logger.info("Found repo task: %s → %s (type=%s)", f.name, repo_name, task_type)
|
|
454
463
|
|
|
@@ -237,6 +237,14 @@ COMPLETE TOOL REFERENCE
|
|
|
237
237
|
25. set_maintenance Mark a repo as in maintenance mode — suppress health/startup alerts.
|
|
238
238
|
[admin] "maintenance mode for TypeLib", "suppress alerts for 1881 during deploy"
|
|
239
239
|
|
|
240
|
+
26. dismiss_error Suppress alerts + fix attempts for a specific error fingerprint permanently.
|
|
241
|
+
[admin] "ignore abc12345", "false positive abc12345", "that's just a warning — suppress it"
|
|
242
|
+
reasons: false_positive | known_warning | infra_only | wont_fix
|
|
243
|
+
Use undismiss_error to re-enable.
|
|
244
|
+
|
|
245
|
+
27. undismiss_error Re-enable alerting/fixing for a previously dismissed error fingerprint.
|
|
246
|
+
[admin] "re-enable abc12345", "undismiss that error"
|
|
247
|
+
|
|
240
248
|
26. pull_repo Run git pull on one or all managed application repos.
|
|
241
249
|
"pull changes", "git pull all repos", "update the code"
|
|
242
250
|
|
|
@@ -1642,6 +1650,59 @@ _TOOLS = [
|
|
|
1642
1650
|
"required": ["operation", "source_repo"],
|
|
1643
1651
|
},
|
|
1644
1652
|
},
|
|
1653
|
+
{
|
|
1654
|
+
"name": "dismiss_error",
|
|
1655
|
+
"description": (
|
|
1656
|
+
"Suppress future alerts and fix attempts for a specific error fingerprint. "
|
|
1657
|
+
"Use when a user says the error is a false positive, a known warning, infra-only, "
|
|
1658
|
+
"or simply not worth auto-fixing. "
|
|
1659
|
+
"Examples: 'ignore abc12345', 'false positive abc12345', 'that's just a warning — suppress it', "
|
|
1660
|
+
"'stop alerting on that timeout error'. "
|
|
1661
|
+
"Admin-only. Use undismiss_error to re-enable alerting."
|
|
1662
|
+
),
|
|
1663
|
+
"input_schema": {
|
|
1664
|
+
"type": "object",
|
|
1665
|
+
"properties": {
|
|
1666
|
+
"fingerprint": {
|
|
1667
|
+
"type": "string",
|
|
1668
|
+
"description": "Full or short (8-char prefix) fingerprint from the error alert.",
|
|
1669
|
+
},
|
|
1670
|
+
"reason": {
|
|
1671
|
+
"type": "string",
|
|
1672
|
+
"enum": ["false_positive", "known_warning", "infra_only", "wont_fix"],
|
|
1673
|
+
"description": (
|
|
1674
|
+
"false_positive: not a real error. "
|
|
1675
|
+
"known_warning: expected, no action needed. "
|
|
1676
|
+
"infra_only: infrastructure issue outside code scope. "
|
|
1677
|
+
"wont_fix: real error but not worth auto-fixing."
|
|
1678
|
+
),
|
|
1679
|
+
},
|
|
1680
|
+
"note": {
|
|
1681
|
+
"type": "string",
|
|
1682
|
+
"description": "Optional explanation — stored for future reference.",
|
|
1683
|
+
},
|
|
1684
|
+
},
|
|
1685
|
+
"required": ["fingerprint", "reason"],
|
|
1686
|
+
},
|
|
1687
|
+
},
|
|
1688
|
+
{
|
|
1689
|
+
"name": "undismiss_error",
|
|
1690
|
+
"description": (
|
|
1691
|
+
"Re-enable alerting and auto-fix for a previously dismissed error fingerprint. "
|
|
1692
|
+
"Use when a dismissed error turns out to be real after all. "
|
|
1693
|
+
"Admin-only."
|
|
1694
|
+
),
|
|
1695
|
+
"input_schema": {
|
|
1696
|
+
"type": "object",
|
|
1697
|
+
"properties": {
|
|
1698
|
+
"fingerprint": {
|
|
1699
|
+
"type": "string",
|
|
1700
|
+
"description": "Full or short (8-char prefix) fingerprint to re-enable.",
|
|
1701
|
+
},
|
|
1702
|
+
},
|
|
1703
|
+
"required": ["fingerprint"],
|
|
1704
|
+
},
|
|
1705
|
+
},
|
|
1645
1706
|
{
|
|
1646
1707
|
"name": "set_maintenance",
|
|
1647
1708
|
"description": (
|
|
@@ -2403,6 +2464,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
2403
2464
|
submitter_user_id=user_id,
|
|
2404
2465
|
notify_user_ids=notify_ids,
|
|
2405
2466
|
run_at=run_at_dt,
|
|
2467
|
+
origin_channel=channel,
|
|
2406
2468
|
)
|
|
2407
2469
|
logger.info(
|
|
2408
2470
|
"Boss repo_task: dropped %s for user %s (repo=%s, run_at=%s)",
|
|
@@ -3490,6 +3552,66 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
3490
3552
|
logger.info("Boss admin: cleared history for user %s (%s) by admin %s", target, display, user_id)
|
|
3491
3553
|
return json.dumps({"status": "cleared", "target_user_id": target, "display_name": display})
|
|
3492
3554
|
|
|
3555
|
+
if name == "dismiss_error":
|
|
3556
|
+
if not is_admin:
|
|
3557
|
+
return json.dumps({"error": "Admin access required to dismiss errors."})
|
|
3558
|
+
raw_fp = inputs.get("fingerprint", "").strip()
|
|
3559
|
+
reason = inputs.get("reason", "wont_fix").strip()
|
|
3560
|
+
note = inputs.get("note", "").strip()
|
|
3561
|
+
|
|
3562
|
+
# Resolve short prefix to full fingerprint
|
|
3563
|
+
fingerprint = raw_fp
|
|
3564
|
+
if len(raw_fp) < 32:
|
|
3565
|
+
full = store.find_fingerprint_by_prefix(raw_fp)
|
|
3566
|
+
if not full:
|
|
3567
|
+
return json.dumps({"error": f"No error found with fingerprint prefix '{raw_fp}'. "
|
|
3568
|
+
"Check recent alerts for the correct fingerprint."})
|
|
3569
|
+
fingerprint = full
|
|
3570
|
+
|
|
3571
|
+
store.dismiss_error(fingerprint, reason, dismissed_by=user_id, note=note)
|
|
3572
|
+
logger.info("Boss: error %s dismissed by %s (reason=%s)", fingerprint[:8], user_id, reason)
|
|
3573
|
+
|
|
3574
|
+
# Reply to the original alert thread if we have it
|
|
3575
|
+
thread_ts, alert_channel = store.get_alert_thread(fingerprint)
|
|
3576
|
+
if thread_ts and alert_channel:
|
|
3577
|
+
from .notify import slack_thread_reply as _tr
|
|
3578
|
+
_tr(
|
|
3579
|
+
cfg_loader.sentinel.slack_bot_token,
|
|
3580
|
+
alert_channel,
|
|
3581
|
+
thread_ts,
|
|
3582
|
+
f":white_check_mark: Dismissed by <@{user_id}> — reason: `{reason}`"
|
|
3583
|
+
+ (f"\n_{note}_" if note else ""),
|
|
3584
|
+
)
|
|
3585
|
+
|
|
3586
|
+
return json.dumps({
|
|
3587
|
+
"status": "dismissed",
|
|
3588
|
+
"fingerprint": fingerprint[:8],
|
|
3589
|
+
"reason": reason,
|
|
3590
|
+
"note": note or None,
|
|
3591
|
+
"message": f"Error `{fingerprint[:8]}` suppressed — Sentinel will not alert or fix it again.",
|
|
3592
|
+
})
|
|
3593
|
+
|
|
3594
|
+
if name == "undismiss_error":
|
|
3595
|
+
if not is_admin:
|
|
3596
|
+
return json.dumps({"error": "Admin access required."})
|
|
3597
|
+
raw_fp = inputs.get("fingerprint", "").strip()
|
|
3598
|
+
|
|
3599
|
+
fingerprint = raw_fp
|
|
3600
|
+
if len(raw_fp) < 32:
|
|
3601
|
+
full = store.find_fingerprint_by_prefix(raw_fp)
|
|
3602
|
+
if full:
|
|
3603
|
+
fingerprint = full
|
|
3604
|
+
|
|
3605
|
+
removed = store.undismiss_error(fingerprint)
|
|
3606
|
+
if removed:
|
|
3607
|
+
logger.info("Boss: error %s undismissed by %s", fingerprint[:8], user_id)
|
|
3608
|
+
return json.dumps({
|
|
3609
|
+
"status": "undismissed",
|
|
3610
|
+
"fingerprint": fingerprint[:8],
|
|
3611
|
+
"message": f"Error `{fingerprint[:8]}` re-enabled — Sentinel will alert and attempt fixes again.",
|
|
3612
|
+
})
|
|
3613
|
+
return json.dumps({"error": f"No dismissed error found for fingerprint '{raw_fp}'."})
|
|
3614
|
+
|
|
3493
3615
|
if name == "set_maintenance":
|
|
3494
3616
|
repo_name = inputs.get("repo_name", "").strip()
|
|
3495
3617
|
note = inputs.get("note", "").strip()
|
|
@@ -69,6 +69,14 @@ class StateStore:
|
|
|
69
69
|
recipient_count INTEGER NOT NULL DEFAULT 0,
|
|
70
70
|
summary_json TEXT
|
|
71
71
|
);
|
|
72
|
+
|
|
73
|
+
CREATE TABLE IF NOT EXISTS dismissed_errors (
|
|
74
|
+
fingerprint TEXT PRIMARY KEY,
|
|
75
|
+
reason TEXT NOT NULL, -- false_positive|known_warning|infra_only|wont_fix
|
|
76
|
+
dismissed_by TEXT, -- Slack user ID
|
|
77
|
+
dismissed_at TEXT NOT NULL,
|
|
78
|
+
note TEXT -- optional free-text explanation
|
|
79
|
+
);
|
|
72
80
|
""")
|
|
73
81
|
self._migrate()
|
|
74
82
|
logger.debug("StateStore initialised at %s", self.db_path)
|
|
@@ -81,6 +89,8 @@ class StateStore:
|
|
|
81
89
|
("add_fix_outcome", "ALTER TABLE fixes ADD COLUMN fix_outcome TEXT"),
|
|
82
90
|
("add_marker_seen_at", "ALTER TABLE fixes ADD COLUMN marker_seen_at TEXT"),
|
|
83
91
|
("add_watched_bots_project", "ALTER TABLE watched_bots ADD COLUMN project_name TEXT"),
|
|
92
|
+
("add_alert_thread_ts", "ALTER TABLE errors ADD COLUMN alert_thread_ts TEXT"),
|
|
93
|
+
("add_alert_channel", "ALTER TABLE errors ADD COLUMN alert_channel TEXT"),
|
|
84
94
|
]
|
|
85
95
|
with self._conn() as conn:
|
|
86
96
|
done = {r[0] for r in conn.execute("SELECT name FROM _sentinel_migrations").fetchall()}
|
|
@@ -129,6 +139,68 @@ class StateStore:
|
|
|
129
139
|
).fetchall()
|
|
130
140
|
return [dict(r) for r in rows]
|
|
131
141
|
|
|
142
|
+
def store_alert_thread(self, fingerprint: str, thread_ts: str, channel: str):
|
|
143
|
+
"""Store the Slack thread_ts + channel for the error alert so we can thread replies under it."""
|
|
144
|
+
with self._conn() as conn:
|
|
145
|
+
conn.execute(
|
|
146
|
+
"UPDATE errors SET alert_thread_ts=?, alert_channel=? WHERE fingerprint=?",
|
|
147
|
+
(thread_ts, channel, fingerprint),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def get_alert_thread(self, fingerprint: str) -> tuple[str, str]:
|
|
151
|
+
"""Return (thread_ts, channel) for the original alert, or ('', '') if not stored."""
|
|
152
|
+
with self._conn() as conn:
|
|
153
|
+
row = conn.execute(
|
|
154
|
+
"SELECT alert_thread_ts, alert_channel FROM errors WHERE fingerprint=?",
|
|
155
|
+
(fingerprint,),
|
|
156
|
+
).fetchone()
|
|
157
|
+
if row and row["alert_thread_ts"]:
|
|
158
|
+
return row["alert_thread_ts"], row["alert_channel"] or ""
|
|
159
|
+
return "", ""
|
|
160
|
+
|
|
161
|
+
# ── Dismissals ────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
def dismiss_error(self, fingerprint: str, reason: str, dismissed_by: str = "", note: str = ""):
|
|
164
|
+
"""Permanently suppress alerts and fix attempts for this fingerprint."""
|
|
165
|
+
with self._conn() as conn:
|
|
166
|
+
conn.execute(
|
|
167
|
+
"INSERT OR REPLACE INTO dismissed_errors "
|
|
168
|
+
"(fingerprint, reason, dismissed_by, dismissed_at, note) VALUES (?, ?, ?, ?, ?)",
|
|
169
|
+
(fingerprint, reason, dismissed_by or None, _now(), note or None),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def is_dismissed(self, fingerprint: str) -> bool:
|
|
173
|
+
with self._conn() as conn:
|
|
174
|
+
return conn.execute(
|
|
175
|
+
"SELECT 1 FROM dismissed_errors WHERE fingerprint=?", (fingerprint,)
|
|
176
|
+
).fetchone() is not None
|
|
177
|
+
|
|
178
|
+
def get_dismissal(self, fingerprint: str) -> dict | None:
|
|
179
|
+
with self._conn() as conn:
|
|
180
|
+
row = conn.execute(
|
|
181
|
+
"SELECT * FROM dismissed_errors WHERE fingerprint=?", (fingerprint,)
|
|
182
|
+
).fetchone()
|
|
183
|
+
return dict(row) if row else None
|
|
184
|
+
|
|
185
|
+
def undismiss_error(self, fingerprint: str) -> bool:
|
|
186
|
+
"""Remove a dismissal — Sentinel will alert and attempt fixes again."""
|
|
187
|
+
with self._conn() as conn:
|
|
188
|
+
cur = conn.execute(
|
|
189
|
+
"DELETE FROM dismissed_errors WHERE fingerprint=?", (fingerprint,)
|
|
190
|
+
)
|
|
191
|
+
return cur.rowcount > 0
|
|
192
|
+
|
|
193
|
+
def find_fingerprint_by_prefix(self, prefix: str) -> str | None:
|
|
194
|
+
"""Return the full fingerprint matching a short prefix, or None if not found / ambiguous."""
|
|
195
|
+
with self._conn() as conn:
|
|
196
|
+
rows = conn.execute(
|
|
197
|
+
"SELECT fingerprint FROM errors WHERE fingerprint LIKE ? LIMIT 2",
|
|
198
|
+
(f"{prefix}%",),
|
|
199
|
+
).fetchall()
|
|
200
|
+
if len(rows) == 1:
|
|
201
|
+
return rows[0]["fingerprint"]
|
|
202
|
+
return None
|
|
203
|
+
|
|
132
204
|
# ── Fixes ─────────────────────────────────────────────────────────────────
|
|
133
205
|
|
|
134
206
|
def record_fix(
|