@misterhuydo/sentinel 1.4.42 → 1.4.44
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/.hint-lock
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-
|
|
1
|
+
2026-03-25T10:11:02.271Z
|
package/.cairn/session.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"message": "Auto-checkpoint at 2026-03-
|
|
3
|
-
"checkpoint_at": "2026-03-
|
|
2
|
+
"message": "Auto-checkpoint at 2026-03-25T10:07:47.873Z",
|
|
3
|
+
"checkpoint_at": "2026-03-25T10:07:47.874Z",
|
|
4
4
|
"active_files": [],
|
|
5
5
|
"notes": [],
|
|
6
6
|
"mtime_snapshot": {}
|
package/package.json
CHANGED
|
@@ -74,6 +74,9 @@ def apply_and_commit(
|
|
|
74
74
|
if _check_protected_paths(patch_path):
|
|
75
75
|
return "failed", ""
|
|
76
76
|
|
|
77
|
+
# Discard any leftover changes from a previous failed fix attempt
|
|
78
|
+
_git(["checkout", "."], cwd=local_path, env=env)
|
|
79
|
+
|
|
77
80
|
r = _git(["pull", "--rebase", "origin", repo.branch], cwd=local_path, env=env)
|
|
78
81
|
if r.returncode != 0:
|
|
79
82
|
logger.error("git pull failed for %s:\n%s", repo.repo_name, r.stderr)
|
|
@@ -906,6 +906,31 @@ _TOOLS = [
|
|
|
906
906
|
),
|
|
907
907
|
"input_schema": {"type": "object", "properties": {}},
|
|
908
908
|
},
|
|
909
|
+
{
|
|
910
|
+
"name": "merge_pr",
|
|
911
|
+
"description": (
|
|
912
|
+
"ADMIN ONLY. Merge an open Sentinel PR into the main branch. "
|
|
913
|
+
"Use when AUTO_PUBLISH=false and you are satisfied with the fix after review. "
|
|
914
|
+
"Handles rebase conflicts automatically if possible. "
|
|
915
|
+
"e.g. 'merge the fix for Whydah-TypeLib', 'sync fix abc123 to main', "
|
|
916
|
+
"'merge the open PR for elprint-sales-core-service'"
|
|
917
|
+
),
|
|
918
|
+
"input_schema": {
|
|
919
|
+
"type": "object",
|
|
920
|
+
"properties": {
|
|
921
|
+
"repo_name": {
|
|
922
|
+
"type": "string",
|
|
923
|
+
"description": "Repository name (must match a repo in config/repos/)",
|
|
924
|
+
},
|
|
925
|
+
"fingerprint": {
|
|
926
|
+
"type": "string",
|
|
927
|
+
"description": "Optional 8-char fingerprint to target a specific fix PR. "
|
|
928
|
+
"If omitted, merges the most recent open PR for the repo.",
|
|
929
|
+
},
|
|
930
|
+
},
|
|
931
|
+
"required": ["repo_name"],
|
|
932
|
+
},
|
|
933
|
+
},
|
|
909
934
|
{
|
|
910
935
|
"name": "set_maintenance",
|
|
911
936
|
"description": (
|
|
@@ -2168,7 +2193,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
2168
2193
|
return json.dumps({"error": "cannot determine user — not clearing"})
|
|
2169
2194
|
|
|
2170
2195
|
# ── Admin-only tools ──────────────────────────────────────────────────────
|
|
2171
|
-
_ADMIN_TOOLS = {"list_all_users", "clear_user_history", "reset_fingerprint", "list_all_errors", "export_db"}
|
|
2196
|
+
_ADMIN_TOOLS = {"list_all_users", "clear_user_history", "reset_fingerprint", "list_all_errors", "export_db", "merge_pr"}
|
|
2172
2197
|
if name in _ADMIN_TOOLS:
|
|
2173
2198
|
if not is_admin:
|
|
2174
2199
|
return json.dumps({"error": "Admin access required. You are not in SLACK_ADMIN_USERS."})
|
|
@@ -2178,9 +2203,9 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
2178
2203
|
return json.dumps({"users": stats, "total": len(stats)})
|
|
2179
2204
|
|
|
2180
2205
|
if name == "clear_user_history":
|
|
2181
|
-
target = inputs.get("target_user_id", "").strip()
|
|
2206
|
+
target = (inputs.get("user_id") or inputs.get("target_user_id", "")).strip()
|
|
2182
2207
|
if not target:
|
|
2183
|
-
return json.dumps({"error": "
|
|
2208
|
+
return json.dumps({"error": "user_id is required"})
|
|
2184
2209
|
store.save_conversation(target, [])
|
|
2185
2210
|
display = store.get_user_name(target)
|
|
2186
2211
|
logger.info("Boss admin: cleared history for user %s (%s) by admin %s", target, display, user_id)
|
|
@@ -2249,6 +2274,112 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
2249
2274
|
except Exception as e:
|
|
2250
2275
|
return json.dumps({"error": str(e)})
|
|
2251
2276
|
|
|
2277
|
+
if name == "merge_pr":
|
|
2278
|
+
import re as _re
|
|
2279
|
+
import requests as _req
|
|
2280
|
+
repo_name = inputs.get("repo_name", "").strip()
|
|
2281
|
+
fingerprint = inputs.get("fingerprint", "").strip()
|
|
2282
|
+
github_token = cfg_loader.sentinel.github_token
|
|
2283
|
+
if not github_token:
|
|
2284
|
+
return json.dumps({"error": "GITHUB_TOKEN not configured"})
|
|
2285
|
+
|
|
2286
|
+
# Find the PR in state_store
|
|
2287
|
+
open_prs = store.get_open_prs()
|
|
2288
|
+
candidates = [p for p in open_prs if p.get("repo_name") == repo_name]
|
|
2289
|
+
if fingerprint:
|
|
2290
|
+
candidates = [p for p in candidates if p.get("fingerprint", "").startswith(fingerprint)]
|
|
2291
|
+
if not candidates:
|
|
2292
|
+
return json.dumps({"error": f"No open Sentinel PR found for repo '{repo_name}'"
|
|
2293
|
+
+ (f" with fingerprint '{fingerprint}'" if fingerprint else "")})
|
|
2294
|
+
fix = candidates[0] # most recent
|
|
2295
|
+
pr_url = fix.get("pr_url", "")
|
|
2296
|
+
branch = fix.get("branch", "")
|
|
2297
|
+
fp = fix.get("fingerprint", "")
|
|
2298
|
+
|
|
2299
|
+
# Parse owner/repo and PR number from pr_url
|
|
2300
|
+
m = _re.search(r"github\.com/([^/]+/[^/]+)/pull/(\d+)", pr_url)
|
|
2301
|
+
if not m:
|
|
2302
|
+
return json.dumps({"error": f"Cannot parse PR URL: {pr_url}"})
|
|
2303
|
+
owner_repo = m.group(1)
|
|
2304
|
+
pr_number = m.group(2)
|
|
2305
|
+
headers = {
|
|
2306
|
+
"Authorization": f"Bearer {github_token}",
|
|
2307
|
+
"Accept": "application/vnd.github+json",
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
# Attempt merge via GitHub API
|
|
2311
|
+
merge_resp = _req.put(
|
|
2312
|
+
f"https://api.github.com/repos/{owner_repo}/pulls/{pr_number}/merge",
|
|
2313
|
+
json={"merge_method": "squash", "commit_title": f"fix(sentinel): merge PR #{pr_number}"},
|
|
2314
|
+
headers=headers, timeout=30,
|
|
2315
|
+
)
|
|
2316
|
+
|
|
2317
|
+
if merge_resp.status_code == 200:
|
|
2318
|
+
# Success
|
|
2319
|
+
sha = merge_resp.json().get("sha", "")[:8]
|
|
2320
|
+
store.record_fix(fp, "applied", branch=branch, pr_url=pr_url,
|
|
2321
|
+
repo_name=repo_name, commit_hash=sha)
|
|
2322
|
+
logger.info("Boss merge_pr: merged PR #%s for %s (fp=%s) by admin %s",
|
|
2323
|
+
pr_number, repo_name, fp[:8], user_id)
|
|
2324
|
+
return json.dumps({
|
|
2325
|
+
"status": "merged",
|
|
2326
|
+
"pr": pr_url,
|
|
2327
|
+
"sha": sha,
|
|
2328
|
+
"repo": repo_name,
|
|
2329
|
+
"note": f"PR #{pr_number} merged into main. Branch '{branch}' can now be deleted.",
|
|
2330
|
+
})
|
|
2331
|
+
|
|
2332
|
+
# Conflict — attempt local rebase then retry
|
|
2333
|
+
if merge_resp.status_code in (405, 409):
|
|
2334
|
+
repo_cfg = cfg_loader.repos.get(repo_name)
|
|
2335
|
+
if not repo_cfg or not repo_cfg.local_path:
|
|
2336
|
+
return json.dumps({"error": f"Merge conflict and no local clone found for '{repo_name}'. Resolve manually: {pr_url}"})
|
|
2337
|
+
import subprocess as _sp
|
|
2338
|
+
from .git_manager import _git_env
|
|
2339
|
+
env = _git_env(repo_cfg)
|
|
2340
|
+
cwd = repo_cfg.local_path
|
|
2341
|
+
base = repo_cfg.branch
|
|
2342
|
+
# fetch + checkout fix branch + rebase
|
|
2343
|
+
_sp.run(["git", "fetch", "origin"], cwd=cwd, env=env, capture_output=True, timeout=60)
|
|
2344
|
+
_sp.run(["git", "checkout", branch], cwd=cwd, env=env, capture_output=True, timeout=30)
|
|
2345
|
+
rb = _sp.run(["git", "rebase", f"origin/{base}"], cwd=cwd, env=env, capture_output=True, timeout=60)
|
|
2346
|
+
if rb.returncode != 0:
|
|
2347
|
+
_sp.run(["git", "rebase", "--abort"], cwd=cwd, env=env, capture_output=True, timeout=30)
|
|
2348
|
+
_sp.run(["git", "checkout", base], cwd=cwd, env=env, capture_output=True, timeout=30)
|
|
2349
|
+
return json.dumps({
|
|
2350
|
+
"status": "conflict",
|
|
2351
|
+
"error": "Rebase failed — conflicts must be resolved manually",
|
|
2352
|
+
"pr": pr_url,
|
|
2353
|
+
"details": rb.stderr.strip()[:500],
|
|
2354
|
+
})
|
|
2355
|
+
_sp.run(["git", "push", "--force-with-lease", "origin", branch],
|
|
2356
|
+
cwd=cwd, env=env, capture_output=True, timeout=60)
|
|
2357
|
+
_sp.run(["git", "checkout", base], cwd=cwd, env=env, capture_output=True, timeout=30)
|
|
2358
|
+
# Retry merge
|
|
2359
|
+
retry_resp = _req.put(
|
|
2360
|
+
f"https://api.github.com/repos/{owner_repo}/pulls/{pr_number}/merge",
|
|
2361
|
+
json={"merge_method": "squash", "commit_title": f"fix(sentinel): merge PR #{pr_number}"},
|
|
2362
|
+
headers=headers, timeout=30,
|
|
2363
|
+
)
|
|
2364
|
+
if retry_resp.status_code == 200:
|
|
2365
|
+
sha = retry_resp.json().get("sha", "")[:8]
|
|
2366
|
+
store.record_fix(fp, "applied", branch=branch, pr_url=pr_url,
|
|
2367
|
+
repo_name=repo_name, commit_hash=sha)
|
|
2368
|
+
logger.info("Boss merge_pr: merged (after rebase) PR #%s for %s by admin %s",
|
|
2369
|
+
pr_number, repo_name, user_id)
|
|
2370
|
+
return json.dumps({
|
|
2371
|
+
"status": "merged",
|
|
2372
|
+
"pr": pr_url,
|
|
2373
|
+
"sha": sha,
|
|
2374
|
+
"repo": repo_name,
|
|
2375
|
+
"note": f"PR #{pr_number} merged after rebase. Branch '{branch}' can now be deleted.",
|
|
2376
|
+
})
|
|
2377
|
+
return json.dumps({"status": "error", "pr": pr_url,
|
|
2378
|
+
"error": f"Retry merge failed ({retry_resp.status_code}): {retry_resp.text[:300]}"})
|
|
2379
|
+
|
|
2380
|
+
return json.dumps({"status": "error", "pr": pr_url,
|
|
2381
|
+
"error": f"GitHub API returned {merge_resp.status_code}: {merge_resp.text[:300]}"})
|
|
2382
|
+
|
|
2252
2383
|
return json.dumps({"error": f"unknown tool: {name}"})
|
|
2253
2384
|
|
|
2254
2385
|
|