@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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-04-08T13:16:44.531Z",
3
- "checkpoint_at": "2026-04-08T13:16:44.613Z",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.5.11",
3
+ "version": "1.5.12",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -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 — log only", event.fingerprint)
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' — flagging for human review", repo.repo_name)
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, sentinel.slack_channel, _thread_ts, msg)
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, sentinel.slack_channel, _started_msg)
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, sentinel.slack_channel, _thread_ts, msg)
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, sentinel.slack_channel,
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, sentinel.slack_channel,
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, sentinel.slack_channel,
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, sentinel.slack_channel,
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, sentinel.slack_channel,
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, sentinel.slack_channel,
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(