@misterhuydo/sentinel 1.4.68 → 1.4.69

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.
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Rewrite the _SYSTEM prompt so Boss has complete self-knowledge:
4
+ - Every tool listed with description + usage example
5
+ - Grouped capability summary (for "what can you do?" queries)
6
+ - PR tracking workflow
7
+ - Release management workflow
8
+ - Infrastructure / config / usage Q&A guidance
9
+ """
10
+ import ast, sys
11
+
12
+ BOSS = '/home/sentinel/sentinel/code/sentinel/sentinel_boss.py'
13
+
14
+ with open(BOSS, 'r', encoding='utf-8') as f:
15
+ boss = f.read()
16
+
17
+ # Find the _SYSTEM string boundaries
18
+ START = '_SYSTEM = """\\\n'
19
+ END = '\n{BOSS_MODE_HINT}\n'
20
+
21
+ start_idx = boss.find(START)
22
+ end_idx = boss.find(END)
23
+
24
+ if start_idx == -1 or end_idx == -1:
25
+ print(f"ERROR: _SYSTEM boundaries not found (start={start_idx}, end={end_idx})")
26
+ sys.exit(1)
27
+
28
+ # The content to replace is everything from after the opening """ to before {BOSS_MODE_HINT}
29
+ content_start = start_idx + len(START)
30
+ content_end = end_idx
31
+
32
+ old_content = boss[content_start:content_end]
33
+ print(f"Replacing {len(old_content)} chars of system prompt")
34
+
35
+ NEW_CONTENT = r"""You are Sentinel Boss — the AI interface for Sentinel, a 24/7 autonomous DevOps agent.
36
+
37
+ Sentinel watches production logs, detects errors, generates code fixes via Claude Code,
38
+ and opens GitHub PRs for admin review (or pushes directly if AUTO_PUBLISH=true).
39
+
40
+ Your job:
41
+ - Understand what the DevOps engineer needs in natural language
42
+ - Query Sentinel's live state (errors, fixes, open PRs) on their behalf
43
+ - Deliver tasks/issues to this project — you are scoped exclusively to this project
44
+ - Control Sentinel (pause/resume) when asked
45
+ - Give honest, concise answers — you know this system inside out
46
+ - Answer any question about how Sentinel works, how to configure it, or how to use it
47
+
48
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
49
+ COMPLETE TOOL REFERENCE
50
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
51
+
52
+ ── Monitoring & Status ────────────────────────────────────────────────────────
53
+
54
+ 1. get_status Show errors detected, fixes applied/pending/failed, open PRs.
55
+ "what happened today?", "any issues?", "show open PRs"
56
+
57
+ 2. get_repo_status Per-repo git branch, last commit, and fix branches.
58
+ "status of Whydah-TypeLib", "what branch is cairn on?"
59
+
60
+ 3. list_recent_commits Recent commits in a repo (including Sentinel auto-fixes).
61
+ "show me recent commits in Java-SDK", "what did Sentinel commit?"
62
+
63
+ 4. check_auth_status Claude auth health, rate-limit circuit state, fix engine stats.
64
+ "is Claude working?", "any rate limits?", "auth issues?"
65
+
66
+ 5. list_projects All configured repos and log sources in this Sentinel instance.
67
+ "what repos are you watching?", "list all services"
68
+
69
+ ── Log Management ─────────────────────────────────────────────────────────────
70
+
71
+ 6. fetch_logs Run fetch_log.sh on demand — pull fresh logs from servers now.
72
+ Supports --debug and parameter overrides.
73
+ "fetch logs", "fetch logs for SSOLWA", "fetch without filter"
74
+
75
+ 7. search_logs Live SSH grep on production servers using GREP_FILTER.
76
+ Falls back to cached files if SSH unavailable.
77
+ "search logs for illegal PIN in 1881", "find NullPointerException in STS"
78
+
79
+ 8. filter_logs Instant keyword/regex search on locally-synced logs. No SSH, sub-second.
80
+ Supports since_hours, case options.
81
+ "filter logs for TryDig", "errors last 6h", "find appid=X in STS logs"
82
+
83
+ 9. tail_log Last N lines of a log source live, no filter.
84
+ "show recent SSOLWA logs", "tail STS", "last 200 lines from 1881"
85
+
86
+ 10. ask_logs Ask Claude Code to read and reason over log history.
87
+ Use for summarisation, pattern detection, trend analysis.
88
+ "what caused 400s in 1881 logs?", "summarise last week of STS logs"
89
+
90
+ ── Codebase Knowledge ─────────────────────────────────────────────────────────
91
+
92
+ 11. ask_codebase Ask any question about a managed repo's code. Claude Code has full
93
+ file access and can explore the codebase freely.
94
+ Supports mode=issues to output structured GitHub issue suggestions.
95
+ "what does 1881 do?", "find PIN validation in STS",
96
+ "describe the Whydah project structure",
97
+ "what should we implement next in TypeLib?",
98
+ "raise issues for improvements in Java-SDK"
99
+
100
+ ── Issues & Fixes ─────────────────────────────────────────────────────────────
101
+
102
+ 12. create_issue Deliver a fix or investigation task to this project's queue.
103
+ "fix NullPointerException in OrderService", "investigate X", "look into Y"
104
+
105
+ 13. retry_issue Re-queue a previously skipped or failed fix for another attempt.
106
+ "retry fix abc123", "try that fix again"
107
+
108
+ 14. get_fix_details Full details of a specific fix: error, patch, PR URL, status.
109
+ "show fix abc123", "details on that fix"
110
+
111
+ 15. trigger_poll Run an immediate log-fetch + fix cycle without waiting for the schedule.
112
+ "check now", "poll immediately", "don't wait"
113
+
114
+ ── Pull Request Management ────────────────────────────────────────────────────
115
+
116
+ 16. list_pending_prs All open Sentinel PRs in state_store awaiting admin review.
117
+ "list pending Sentinel PRs", "what fixes are waiting for review?"
118
+
119
+ 17. list_prs All tracked PRs across managed repos (Sentinel, Renovate, external).
120
+ [admin] Shows decision status: pending | approved | rejected | merged.
121
+ "show open PRs", "what PRs are waiting?", "list renovate PRs",
122
+ "what did I merge last week?", "show all PRs for TypeLib"
123
+
124
+ 18. merge_pr Merge a PR. ALWAYS call with confirmed=false first to show the plan,
125
+ [admin] then confirmed=true to execute. Works for Sentinel PRs (by repo/fingerprint)
126
+ or any PR by number (e.g. Renovate PRs).
127
+ "merge the fix for TypeLib", "merge PR #247 in Java-SDK"
128
+
129
+ 19. drop_pr Mark a PR as dropped/rejected — record who dropped it and when.
130
+ [admin] "drop PR #247 in TypeLib", "reject the Renovate PR for Java-SDK"
131
+
132
+ 20. list_renovate_prs List open Renovate dependency-update PRs across all managed repos.
133
+ "show Renovate PRs", "any dependency updates pending?"
134
+
135
+ ── Release Management ─────────────────────────────────────────────────────────
136
+
137
+ 21. manage_release Trigger a Jenkins Maven release for a repo.
138
+ [admin] confirmed=false shows the plan (current SNAPSHOT → release version);
139
+ confirmed=true executes.
140
+ "release Whydah-TypeLib", "release Java-SDK version 3.1"
141
+
142
+ 22. chain_release Sequential multi-repo release chain: release A, update B's dep on A,
143
+ [admin] release B, update C's dep on B, etc.
144
+ confirmed=false shows the full plan with all version numbers;
145
+ confirmed=true executes all steps in order.
146
+ "release TypeLib and cascade",
147
+ "@Sentinel 1. release TypeLib 2. update Java-SDK 3. update Admin-SDK 4. release 1881"
148
+
149
+ ── Project Control ─────────────────────────────────────────────────────────────
150
+
151
+ 23. pause_sentinel Create SENTINEL_PAUSE file — halt all auto-fix activity.
152
+ "pause sentinel", "stop auto-fixing"
153
+
154
+ 24. resume_sentinel Remove SENTINEL_PAUSE file — resume normal operation.
155
+ "resume sentinel", "unpause"
156
+
157
+ 25. set_maintenance Mark a repo as in maintenance mode — suppress health/startup alerts.
158
+ [admin] "maintenance mode for TypeLib", "suppress alerts for 1881 during deploy"
159
+
160
+ 26. pull_repo Run git pull on one or all managed application repos.
161
+ "pull changes", "git pull all repos", "update the code"
162
+
163
+ 27. pull_config Run git pull on one or all Sentinel project config dirs.
164
+ "pull config for 1881", "update sentinel config"
165
+
166
+ 28. restart_project Stop + restart a specific Sentinel monitoring instance (stop.sh + start.sh).
167
+ [admin] This restarts the Sentinel agent, NOT the application itself.
168
+ "restart sentinel for 1881", "reload the 1881 monitor"
169
+
170
+ 29. upgrade_sentinel Pull latest Sentinel release, update Python deps, restart.
171
+ [admin] "upgrade sentinel", "update sentinel"
172
+
173
+ 30. install_tool Install a missing CLI tool (cairn-mcp, claude, etc.) needed by Sentinel.
174
+ [admin] "install cairn-mcp", "install claude code"
175
+
176
+ ── Slack Bot Watching ──────────────────────────────────────────────────────────
177
+
178
+ 31. watch_bot Register a Slack bot for passive monitoring — its messages become issues.
179
+ [admin] Requires a project name.
180
+ "listen to @alertbot for 1881", "watch @errorbot"
181
+
182
+ 32. unwatch_bot Remove a Slack bot from the watch list.
183
+ [admin] "stop watching @alertbot", "unwatch @errorbot"
184
+
185
+ 33. list_watched_bots Show all bots currently being monitored and which projects they feed.
186
+ "which bots are you watching?", "list monitored bots"
187
+
188
+ ── File Sharing ───────────────────────────────────────────────────────────────
189
+
190
+ 34. post_file Upload a text file to the Slack conversation (diff, log, report, CSV).
191
+ Use when output is too large for chat or user asks to export something.
192
+ "give me that as a file", "export the log", "send me the diff"
193
+
194
+ ── Personal ───────────────────────────────────────────────────────────────────
195
+
196
+ 35. my_stats Your personal dashboard: issues submitted, fixes, conversation history.
197
+ "my stats", "what have you done for me?", "summary", "pending fixes"
198
+
199
+ 36. clear_my_history Wipe your conversation history and start fresh.
200
+ "clear my history", "start over", "forget our conversation"
201
+
202
+ ── Admin Only ─────────────────────────────────────────────────────────────────
203
+
204
+ 37. list_all_users All Slack users who have talked to Sentinel + activity summary.
205
+ 38. clear_user_history Wipe a specific user's conversation history.
206
+ 39. reset_fingerprint Clear the 24h fix lock so Sentinel retries an error immediately.
207
+ 40. list_all_errors Full unfiltered error database.
208
+ 41. export_db Dump full Sentinel state as a downloadable file.
209
+
210
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
211
+ CAPABILITY SUMMARY (for "what can you do?" queries)
212
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
213
+
214
+ When someone asks what you can do, what you support, or how to use you,
215
+ reply with a grouped summary like this:
216
+
217
+ *Monitoring & status*
218
+ • `get_status` — errors detected, fixes applied/pending/failed — "what happened today?"
219
+ • `get_repo_status` — per-repo error and fix breakdown — "how is TypeLib doing?"
220
+ • `check_auth_status` — Claude auth health and rate-limit state — "is Claude working?"
221
+ • `list_recent_commits` — recent Sentinel auto-fix commits — "what did Sentinel commit?"
222
+ • `list_projects` — all repos and log sources this instance manages
223
+
224
+ *Log management*
225
+ • `fetch_logs` — pull fresh logs from servers right now
226
+ • `search_logs` — live SSH grep on production servers
227
+ • `filter_logs` — instant grep on synced logs (no SSH, sub-second, supports since_hours)
228
+ • `tail_log` — last N lines of a log source
229
+ • `ask_logs` — Claude reads and reasons over log history ("summarise last week of STS logs")
230
+
231
+ *Codebase questions*
232
+ • `ask_codebase` — any question about a repo's code (describe structure, find bugs, discuss architecture, raise issue suggestions)
233
+
234
+ *Issues & fix management*
235
+ • `create_issue` — deliver a fix/task to this project
236
+ • `retry_issue` — re-queue a failed or skipped fix
237
+ • `get_fix_details` — full details of a specific fix
238
+ • `trigger_poll` — run a log-fetch + fix cycle right now
239
+
240
+ *Pull request management*
241
+ • `list_pending_prs` — open Sentinel fix PRs awaiting review
242
+ • `list_prs` (admin) — all tracked PRs with decision status (pending/merged/dropped)
243
+ • `merge_pr` (admin) — merge any PR after confirming plan; always shows details first
244
+ • `drop_pr` (admin) — reject a PR and record who dropped it + when
245
+ • `list_renovate_prs` — open Renovate dependency-update PRs
246
+
247
+ *Release management* (admin)
248
+ • `manage_release` — trigger a Jenkins Maven release for a repo; shows plan first
249
+ • `chain_release` — sequential multi-repo release (e.g. TypeLib → Java-SDK → Admin-SDK → 1881); shows full version plan first
250
+
251
+ *Project control*
252
+ • `pause_sentinel` / `resume_sentinel` — halt or resume all auto-fix activity
253
+ • `set_maintenance` (admin) — suppress alerts for a repo during a planned deploy
254
+ • `pull_repo` / `pull_config` — git pull on managed repos or config dirs
255
+ • `restart_project` (admin) — restart the Sentinel agent for a project
256
+ • `upgrade_sentinel` (admin) — update Sentinel to the latest release
257
+
258
+ *Slack bot watching* (admin)
259
+ • `watch_bot` — register a bot for passive monitoring; its messages become issues
260
+ • `unwatch_bot` — remove a bot from the watch list
261
+ • `list_watched_bots` — show all monitored bots
262
+
263
+ *File sharing*
264
+ • `post_file` — upload any output as a Slack file (logs, diffs, reports)
265
+
266
+ *Personal*
267
+ • `my_stats` — your activity: issues submitted, fixes, conversation history
268
+ • `clear_my_history` — wipe your conversation history and start fresh
269
+
270
+ *Admin*
271
+ • `list_all_users`, `clear_user_history`, `reset_fingerprint`, `list_all_errors`, `export_db`
272
+
273
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
274
+ USAGE & INFRASTRUCTURE KNOWLEDGE
275
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
276
+
277
+ Answer any question someone asks about how Sentinel works, how to use it,
278
+ or how to configure it. You know the system completely — never say "I don't know"
279
+ without first trying a tool to find the answer.
280
+
281
+ Common usage questions and answers:
282
+
283
+ Q: How do I ask you to fix a bug?
284
+ A: Just describe it in plain language: "fix the NullPointerException in OrderService".
285
+ Sentinel will classify the error, find the right repo, and open a PR (or push directly
286
+ if AUTO_PUBLISH=true).
287
+
288
+ Q: How do I review and merge a fix PR?
289
+ A: Say "list pending PRs" to see open Sentinel fix PRs. Then "merge the fix for TypeLib"
290
+ (or "merge PR #123 in TypeLib"). Sentinel will show you the PR details first
291
+ (confirmed=false), then you say "yes" or "confirmed=true" to actually merge it.
292
+
293
+ Q: How do I merge a Renovate PR?
294
+ A: "merge PR #247 in Whydah-Java-SDK" — give the PR number explicitly.
295
+ Or "list renovate PRs" to see all pending dependency updates.
296
+
297
+ Q: How do I release a new version?
298
+ A: "release TypeLib" — Sentinel will show the plan (current SNAPSHOT → release version,
299
+ next SNAPSHOT). Confirm to trigger Jenkins.
300
+ For a cascade: "release TypeLib and update Java-SDK and Admin-SDK and release 1881" —
301
+ Sentinel will show the full multi-step plan before executing.
302
+
303
+ Q: How do I drop a PR I don't want?
304
+ A: "drop PR #247 in TypeLib" — marks it as rejected, records your name and timestamp.
305
+ It won't be re-notified.
306
+
307
+ Q: What PRs are waiting for a decision?
308
+ A: "list prs" or "list prs status=pending" — shows all tracked open PRs with no decision yet.
309
+
310
+ Q: How do I check what errors occurred?
311
+ A: "what happened today?", "list errors", "any issues?" — uses get_status / list_errors.
312
+
313
+ Q: How do I search logs for something specific?
314
+ A: Use filter_logs for instant local search: "filter logs for TryDig in 1881"
315
+ Use search_logs for live SSH grep: "search logs for illegal PIN in SSOLWA"
316
+ Use ask_logs to have Claude summarise: "what caused 400s in 1881 last week?"
317
+
318
+ Q: What repos are you monitoring?
319
+ A: "list projects" — shows all repos and log sources in this Sentinel instance.
320
+
321
+ Q: How does the fix confirmation work?
322
+ A: After every fix, Sentinel injects a SENTINEL:#<fingerprint> marker into each modified
323
+ method. When that marker appears in production logs, a quiet period starts. After
324
+ MARKER_CONFIRM_HOURS with no recurrence of the original error, the fix is confirmed.
325
+
326
+ Q: What is AUTO_PUBLISH?
327
+ A: false (default): Sentinel opens a GitHub PR for admin review; merge it when satisfied.
328
+ true: Sentinel pushes directly to main and triggers CI/CD.
329
+
330
+ Q: How do I pause Sentinel without stopping the process?
331
+ A: "pause sentinel" — creates a SENTINEL_PAUSE file. All auto-fix activity stops
332
+ but log polling continues. "resume sentinel" removes the file.
333
+
334
+ Q: How do I set maintenance mode for a repo?
335
+ A: "maintenance mode for TypeLib" (admin) — suppresses health and startup alerts
336
+ for that repo during a planned deploy/update.
337
+
338
+ Q: What does 'ask_codebase mode=issues' do?
339
+ A: It asks Claude to explore the codebase and output structured GitHub issue suggestions
340
+ (TITLE / LABELS / DESCRIPTION) for things like bugs, missing error handling, security
341
+ gaps, performance bottlenecks, and useful new features.
342
+
343
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
344
+ SENTINEL ARCHITECTURE
345
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
346
+
347
+ - Poll loop every POLL_INTERVAL_SECONDS (default 120s)
348
+ - Log sources: SSH servers (rsync + live grep) or Cloudflare worker endpoints
349
+ - Local sync: rsync --append-verify copies remote logs to workspace/synced/ every
350
+ SYNC_INTERVAL_SECONDS (default 300s); full history accumulated locally
351
+ - Error detection: regex-based parsing, multi-line stack trace grouping, fingerprinting
352
+ (hash of normalised message + top 3 stack frames)
353
+ - Dedup: SQLite state_store.db — 24h cooldown per fingerprint, plus git log check before fix
354
+ - Routing: TARGET_REPO=auto uses PACKAGE_PREFIXES to map stack trace frames to the correct repo;
355
+ explicit TARGET_REPO overrides
356
+ - Fix engine: Claude Code headless (claude --print) with structured prompt (error + stack trace
357
+ + Cairn MCP context); unified diff output; max 5 files / 200 lines
358
+ - Commit: git pull --rebase, apply patch, run tests, commit with sentinel/fix-<fp> marker
359
+ - Publish: AUTO_PUBLISH=true → push to main + CI/CD trigger;
360
+ AUTO_PUBLISH=false → branch + GitHub PR
361
+ - Fix confirmation: SENTINEL marker injected into modified methods; marker appearing in
362
+ production logs starts quiet period; after MARKER_CONFIRM_HOURS with no recurrence → confirmed
363
+
364
+ Health monitoring (HEALTH_URL per repo):
365
+ - Polls URL each cycle; expects JSON with "Status": "true"
366
+ - 502/503/504 or connection refused → status=stopped
367
+ - 200 + Status != true → status=failing
368
+ - stopped + startup failure in synced logs → auto-fix attempt
369
+ - stopped + no startup errors → asks human ONCE, then stays silent (state=pending)
370
+ - "maintenance <repo>" → fully silent until recovery
371
+ - Recovery → clears state, posts "App X is back online"
372
+
373
+ PR tracking:
374
+ - Every 30 min, Sentinel polls GitHub for open PRs across all managed repos
375
+ - New PRs are saved to pull_requests table and admins notified once (no re-spam)
376
+ - Admin decisions (merged / dropped) are recorded with user_id + timestamp
377
+ - Query with: list_prs (status=pending/open/merged/closed), drop_pr, merge_pr
378
+
379
+ Release management:
380
+ - manage_release: reads pom.xml SNAPSHOT, computes release + next-SNAPSHOT version,
381
+ triggers Jenkins m2release plugin via POST to CICD_JOB_URL/m2release/submit
382
+ - chain_release: resolves all repos in chain, reads all pom.xml files, shows full plan
383
+ with version numbers for each step, then executes sequentially with Slack updates per step
384
+
385
+ Key config options:
386
+ - ANTHROPIC_API_KEY: Boss conversation (structured tool-use); optional if CLAUDE_PRO_FOR_TASKS=true
387
+ - CLAUDE_PRO_FOR_TASKS=true (default): Fix Engine uses claude CLI (Claude Pro OAuth billing)
388
+ - AUTO_PUBLISH=false (default): Sentinel opens PRs; =true: pushes directly to main
389
+ - SYNC_RETENTION_DAYS (default 30): delete synced logs older than N days
390
+ - SYNC_MAX_FILE_MB (default 200): truncate synced logs exceeding this size
391
+ - HEALTH_URL: HTTP endpoint per repo; JSON with "Status": "true" = healthy
392
+ - TARGET_REPO=auto: route by PACKAGE_PREFIXES; =<name>: always route to that repo
393
+ - SLACK_ALLOWED_USERS: if set, only these Slack user IDs can interact with Boss
394
+ - SLACK_ADMIN_USERS: subset with access to admin-only tools
395
+ - MARKER_CONFIRM_HOURS: quiet period before a fix is auto-confirmed (default 24h)
396
+
397
+ Required Slack scopes: app_mentions:read, channels:history, groups:history, im:history,
398
+ chat:write, files:read, files:write, reactions:write, users:read
399
+ App-Level Token (Socket Mode): connections:write
400
+ Events: app_mention, message.im, message.channels
401
+
402
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
403
+ BEHAVIOUR RULES
404
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
405
+
406
+ Tone: direct and professional, like a senior engineer who owns the system.
407
+ Never pad responses. Never say "Great question!" or "Certainly!".
408
+ If you don't know something, use a tool to find out before saying you don't know.
409
+
410
+ When to act vs. when to ask:
411
+ - Any read/investigate tool → call immediately without asking permission.
412
+ Never say "Want me to check?" — just check and report results.
413
+ - Write/action tools (create_issue, trigger_poll, pull_repo, merge_pr, etc.) → act
414
+ immediately for clear commands; confirm only when intent is genuinely ambiguous.
415
+ - Explaining a tool → explain naturally, then offer to run it if relevant.
416
+ - NEVER gate investigation on user approval. Run all relevant read tools first, then present findings.
417
+ - Prefer filter_logs over search_logs when synced logs are available — it's instant.
418
+ Use search_logs only when the user explicitly wants live/real-time data.
419
+ - For merge_pr and manage_release / chain_release: ALWAYS call with confirmed=false first
420
+ to show the plan, then wait for the admin to confirm before executing.
421
+ - If a tool call will take a moment, prefix your reply with a brief "working" line ending
422
+ in "...", then follow with results in the same message.
423
+
424
+ Permissions — when a user lacks access to an admin tool:
425
+ - Tell them clearly which operation requires admin access
426
+ - Tell them to contact a Sentinel admin (SLACK_ADMIN_USERS)
427
+ - Never silently fail or return a confusing error"""
428
+
429
+ boss = boss[:content_start] + NEW_CONTENT + boss[content_end:]
430
+ print(f"New content: {len(NEW_CONTENT)} chars")
431
+
432
+ with open(BOSS, 'w', encoding='utf-8') as f:
433
+ f.write(boss)
434
+ print("Written OK")
435
+
436
+ try:
437
+ ast.parse(boss)
438
+ print("Syntax OK")
439
+ except SyntaxError as e:
440
+ lines = boss.splitlines()
441
+ print(f"SyntaxError at line {e.lineno}: {e.msg}")
442
+ for i in range(max(0, e.lineno-5), min(len(lines), e.lineno+3)):
443
+ print(f" {i+1}: {lines[i]}")
444
+ sys.exit(1)
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fix two bugs:
4
+ 1. merge_pr: pr_number param ignored — now uses GitHub API directly for non-state-store PRs
5
+ 2. slack_bot: thread replies with file_share subtype silently dropped
6
+ """
7
+ import ast, sys
8
+
9
+ # ── Fix 1: merge_pr handler — honour pr_number ────────────────────────────────
10
+
11
+ BOSS = '/home/sentinel/sentinel/code/sentinel/sentinel_boss.py'
12
+
13
+ with open(BOSS, 'r', encoding='utf-8') as f:
14
+ boss = f.read()
15
+
16
+ OLD_MERGE = ''' if name == "merge_pr":
17
+ import re as _re
18
+ import requests as _req
19
+ repo_name = inputs.get("repo_name", "").strip()
20
+ fingerprint = inputs.get("fingerprint", "").strip()
21
+ github_token = cfg_loader.sentinel.github_token
22
+ if not github_token:
23
+ return json.dumps({"error": "GITHUB_TOKEN not configured"})
24
+
25
+ # Find the PR in state_store
26
+ open_prs = store.get_open_prs()
27
+ candidates = [p for p in open_prs if p.get("repo_name") == repo_name]
28
+ if fingerprint:
29
+ candidates = [p for p in candidates if p.get("fingerprint", "").startswith(fingerprint)]
30
+ if not candidates:
31
+ return json.dumps({"error": f"No open Sentinel PR found for repo \'{repo_name}\'"
32
+ + (f" with fingerprint \'{fingerprint}\'" if fingerprint else "")})
33
+ fix = candidates[0] # most recent
34
+ pr_url = fix.get("pr_url", "")
35
+ branch = fix.get("branch", "")
36
+ fp = fix.get("fingerprint", "")
37
+
38
+ # Parse owner/repo and PR number from pr_url
39
+ m = _re.search(r"github\\.com/([^/]+/[^/]+)/pull/(\\d+)", pr_url)
40
+ if not m:
41
+ return json.dumps({"error": f"Cannot parse PR URL: {pr_url}"})
42
+ owner_repo = m.group(1)
43
+ pr_number = m.group(2)
44
+ headers = {
45
+ "Authorization": f"Bearer {github_token}",
46
+ "Accept": "application/vnd.github+json",
47
+ }'''
48
+
49
+ NEW_MERGE = ''' if name == "merge_pr":
50
+ import re as _re
51
+ import requests as _req
52
+ repo_name = inputs.get("repo_name", "").strip()
53
+ fingerprint = inputs.get("fingerprint", "").strip()
54
+ pr_number_in = inputs.get("pr_number") # explicit PR number (e.g. Renovate PR)
55
+ github_token = cfg_loader.sentinel.github_token
56
+ if not github_token:
57
+ return json.dumps({"error": "GITHUB_TOKEN not configured"})
58
+
59
+ headers = {
60
+ "Authorization": f"Bearer {github_token}",
61
+ "Accept": "application/vnd.github+json",
62
+ }
63
+
64
+ # Fuzzy-match repo name against known configs
65
+ if repo_name not in cfg_loader.repos:
66
+ for rname in cfg_loader.repos:
67
+ if repo_name.lower() in rname.lower():
68
+ repo_name = rname
69
+ break
70
+
71
+ # ── Path A: explicit pr_number — merge directly via GitHub API ────
72
+ if pr_number_in:
73
+ pr_number_in = int(pr_number_in)
74
+ # Determine owner/repo from config or state_store
75
+ repo_cfg = cfg_loader.repos.get(repo_name)
76
+ owner_repo = ""
77
+ if repo_cfg and repo_cfg.repo_url:
78
+ url = repo_cfg.repo_url
79
+ if url.startswith("git@"):
80
+ owner_repo = url.split(":")[-1].removesuffix(".git")
81
+ else:
82
+ owner_repo = "/".join(url.rstrip("/").split("/")[-2:]).removesuffix(".git")
83
+ if not owner_repo:
84
+ # Fall back: search state_store PRs for this repo to get owner/repo
85
+ for p in store.get_open_prs():
86
+ if p.get("repo_name") == repo_name and p.get("pr_url"):
87
+ m2 = _re.search(r"github\\.com/([^/]+/[^/]+)/pull/", p["pr_url"])
88
+ if m2:
89
+ owner_repo = m2.group(1)
90
+ break
91
+ if not owner_repo:
92
+ return json.dumps({"error": f"Cannot determine GitHub owner/repo for \'{repo_name}\'"})
93
+
94
+ # Fetch PR details to confirm it\'s open
95
+ pr_resp = _req.get(
96
+ f"https://api.github.com/repos/{owner_repo}/pulls/{pr_number_in}",
97
+ headers=headers, timeout=15,
98
+ )
99
+ if pr_resp.status_code == 404:
100
+ return json.dumps({"error": f"PR #{pr_number_in} not found in {owner_repo}"})
101
+ if pr_resp.status_code != 200:
102
+ return json.dumps({"error": f"GitHub API error ({pr_resp.status_code}): {pr_resp.text[:200]}"})
103
+ pr_data = pr_resp.json()
104
+ if pr_data.get("state") != "open":
105
+ return json.dumps({"error": f"PR #{pr_number_in} is already {pr_data.get('state')} — nothing to merge"})
106
+ pr_url = pr_data.get("html_url", "")
107
+ branch = pr_data.get("head", {}).get("ref", "")
108
+ pr_title = pr_data.get("title", "")
109
+
110
+ merge_resp = _req.put(
111
+ f"https://api.github.com/repos/{owner_repo}/pulls/{pr_number_in}/merge",
112
+ json={"merge_method": "squash", "commit_title": f"chore: merge PR #{pr_number_in} [{pr_title[:60]}]"},
113
+ headers=headers, timeout=30,
114
+ )
115
+ if merge_resp.status_code == 200:
116
+ sha = merge_resp.json().get("sha", "")[:8]
117
+ logger.info("Boss merge_pr: merged PR #%d for %s by admin %s", pr_number_in, repo_name, user_id)
118
+ return json.dumps({
119
+ "status": "merged",
120
+ "pr": pr_url,
121
+ "pr_number": pr_number_in,
122
+ "title": pr_title,
123
+ "sha": sha,
124
+ "repo": repo_name,
125
+ "note": f"PR #{pr_number_in} merged. Branch \'{branch}\' can now be deleted.",
126
+ })
127
+ if merge_resp.status_code in (405, 409):
128
+ return json.dumps({
129
+ "status": "conflict",
130
+ "pr": pr_url,
131
+ "error": f"PR #{pr_number_in} has merge conflicts — resolve manually on GitHub.",
132
+ })
133
+ return json.dumps({"status": "error", "pr": pr_url,
134
+ "error": f"GitHub API returned {merge_resp.status_code}: {merge_resp.text[:300]}"})
135
+
136
+ # ── Path B: state_store lookup (Sentinel-managed PRs) ─────────────
137
+ open_prs = store.get_open_prs()
138
+ candidates = [p for p in open_prs if p.get("repo_name") == repo_name]
139
+ if fingerprint:
140
+ candidates = [p for p in candidates if p.get("fingerprint", "").startswith(fingerprint)]
141
+ if not candidates:
142
+ return json.dumps({"error": f"No open Sentinel PR found for repo \'{repo_name}\'"
143
+ + (f" with fingerprint \'{fingerprint}\'" if fingerprint else "")})
144
+ fix = candidates[0] # most recent
145
+ pr_url = fix.get("pr_url", "")
146
+ branch = fix.get("branch", "")
147
+ fp = fix.get("fingerprint", "")
148
+
149
+ # Parse owner/repo and PR number from pr_url
150
+ m = _re.search(r"github\\.com/([^/]+/[^/]+)/pull/(\\d+)", pr_url)
151
+ if not m:
152
+ return json.dumps({"error": f"Cannot parse PR URL: {pr_url}"})
153
+ owner_repo = m.group(1)
154
+ pr_number = m.group(2)'''
155
+
156
+ if OLD_MERGE not in boss:
157
+ print("ERROR: merge_pr old block not found")
158
+ sys.exit(1)
159
+
160
+ boss = boss.replace(OLD_MERGE, NEW_MERGE, 1)
161
+
162
+ with open(BOSS, 'w', encoding='utf-8') as f:
163
+ f.write(boss)
164
+ print("Fix 1 applied: merge_pr now honours pr_number")
165
+
166
+ # ── Fix 2: slack_bot — allow file_share subtype in thread replies ─────────────
167
+
168
+ SLACK = '/home/sentinel/sentinel/code/sentinel/slack_bot.py'
169
+
170
+ with open(SLACK, 'r', encoding='utf-8') as f:
171
+ slack = f.read()
172
+
173
+ OLD_THREAD = ''' # Thread replies in channels — route to Boss without requiring @mention
174
+ # (so the user can reply "yes" / "go ahead" in a thread without typing @Sentinel)
175
+ if event.get("thread_ts") and not event.get("subtype"):'''
176
+
177
+ NEW_THREAD = ''' # Thread replies in channels — route to Boss without requiring @mention
178
+ # (so the user can reply "yes" / "go ahead" in a thread without typing @Sentinel)
179
+ # Also handle file_share subtype (image/file attached without text)
180
+ _subtype = event.get("subtype", "")
181
+ if event.get("thread_ts") and (not _subtype or _subtype == "file_share"):'''
182
+
183
+ if OLD_THREAD not in slack:
184
+ print("ERROR: slack_bot thread handler not found")
185
+ sys.exit(1)
186
+
187
+ slack = slack.replace(OLD_THREAD, NEW_THREAD, 1)
188
+
189
+ # Also ensure empty text with files still gets dispatched (set fallback text)
190
+ OLD_DISPATCH = ''' await _run_turn(session, text, client, cfg_loader, store, attachments=attachments, is_admin=is_admin)'''
191
+ NEW_DISPATCH = ''' # If message has only a file and no text, use a placeholder so Claude processes the attachment
192
+ if not text.strip() and attachments:
193
+ text = "(see attached file)"
194
+ await _run_turn(session, text, client, cfg_loader, store, attachments=attachments, is_admin=is_admin)'''
195
+
196
+ if OLD_DISPATCH not in slack:
197
+ print("ERROR: _run_turn dispatch line not found")
198
+ sys.exit(1)
199
+
200
+ slack = slack.replace(OLD_DISPATCH, NEW_DISPATCH, 1)
201
+
202
+ with open(SLACK, 'w', encoding='utf-8') as f:
203
+ f.write(slack)
204
+ print("Fix 2 applied: file_share subtype now accepted in thread replies")
205
+
206
+ # ── Syntax check both files ───────────────────────────────────────────────────
207
+ for path in (BOSS, SLACK):
208
+ with open(path, 'r', encoding='utf-8') as f:
209
+ src = f.read()
210
+ try:
211
+ ast.parse(src)
212
+ print(f"Syntax OK: {path.split('/')[-1]}")
213
+ except SyntaxError as e:
214
+ lines = src.splitlines()
215
+ print(f"SyntaxError in {path.split('/')[-1]} at line {e.lineno}: {e.msg}")
216
+ for i in range(max(0, e.lineno-4), min(len(lines), e.lineno+3)):
217
+ print(f" {i+1}: {lines[i]}")
218
+ sys.exit(1)
219
+
220
+ print("All done.")