@misterhuydo/sentinel 1.4.43 → 1.4.45
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:19:44.042Z",
|
|
3
|
+
"checkpoint_at": "2026-03-25T10:19:44.043Z",
|
|
4
4
|
"active_files": [],
|
|
5
5
|
"notes": [],
|
|
6
6
|
"mtime_snapshot": {}
|
package/package.json
CHANGED
|
@@ -129,6 +129,29 @@ def _build_prompt(event, repo: RepoConfig, log_file, marker: str, stale_markers:
|
|
|
129
129
|
]
|
|
130
130
|
return "\n".join(lines_out)
|
|
131
131
|
|
|
132
|
+
def _fix_blank_context_lines(patch: str) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Ensure blank context lines inside hunks have a leading space.
|
|
135
|
+
Claude sometimes outputs empty lines in hunk bodies as bare '\\n'
|
|
136
|
+
instead of ' \\n', which causes git-apply to report 'corrupt patch'.
|
|
137
|
+
"""
|
|
138
|
+
lines = patch.splitlines()
|
|
139
|
+
result = []
|
|
140
|
+
in_hunk = False
|
|
141
|
+
for line in lines:
|
|
142
|
+
if line.startswith("@@"):
|
|
143
|
+
in_hunk = True
|
|
144
|
+
result.append(line)
|
|
145
|
+
elif line.startswith(("diff ", "--- ", "+++ ", "index ")):
|
|
146
|
+
in_hunk = False
|
|
147
|
+
result.append(line)
|
|
148
|
+
elif in_hunk and line == "":
|
|
149
|
+
result.append(" ")
|
|
150
|
+
else:
|
|
151
|
+
result.append(line)
|
|
152
|
+
return "\n".join(result) + "\n"
|
|
153
|
+
|
|
154
|
+
|
|
132
155
|
def _validate_patch(patch: str) -> tuple[bool, str]:
|
|
133
156
|
files_changed = len(re.findall(r"^diff --git", patch, re.MULTILINE))
|
|
134
157
|
lines_changed = len([
|
|
@@ -338,6 +361,8 @@ def generate_fix(
|
|
|
338
361
|
logger.warning("No patch found in Claude output for %s", event.fingerprint)
|
|
339
362
|
return "error", None, ""
|
|
340
363
|
|
|
364
|
+
patch = _fix_blank_context_lines(patch)
|
|
365
|
+
|
|
341
366
|
ok, reason = _validate_patch(patch)
|
|
342
367
|
if not ok:
|
|
343
368
|
logger.warning("Patch rejected for %s: %s", event.fingerprint, reason)
|
|
@@ -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
|
|