@misterhuydo/sentinel 1.4.89 → 1.4.91
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 +1 -1
- package/.cairn/session.json +2 -2
- package/lib/.cairn/minify-map.json +8 -1
- package/lib/.cairn/views/ff8fde_test.js +172 -0
- package/lib/add.js +8 -20
- package/package.json +1 -1
- package/python/sentinel/config_loader.py +12 -4
- package/python/sentinel/dev_watcher.py +288 -0
- package/python/sentinel/fix_engine.py +25 -0
- package/python/sentinel/git_manager.py +51 -1
- package/python/sentinel/main.py +268 -2
- package/python/sentinel/repo_task_engine.py +381 -0
- package/python/sentinel/sentinel_boss.py +373 -6
- package/python/sentinel/sentinel_dev.py +448 -0
- package/python/sentinel/state_store.py +121 -0
- package/templates/log-configs/_example.properties +21 -32
- package/templates/sentinel.properties +5 -6
|
@@ -358,6 +358,21 @@ def publish(
|
|
|
358
358
|
return branch, pr_url
|
|
359
359
|
|
|
360
360
|
|
|
361
|
+
def _alert_github_token_error(cfg: "SentinelConfig", owner_repo: str, status: int, hint: str = "") -> None:
|
|
362
|
+
"""Fire a Slack alert when GitHub API rejects the token during PR creation."""
|
|
363
|
+
msg = (
|
|
364
|
+
f":warning: *Sentinel — GitHub token error ({status})*\n"
|
|
365
|
+
f"Could not open a PR for `{owner_repo}`.\n"
|
|
366
|
+
f"{hint}\n"
|
|
367
|
+
f"Check `GITHUB_TOKEN` in `sentinel.properties` — it may be expired, revoked, "
|
|
368
|
+
f"or not scoped to this org/repo."
|
|
369
|
+
)
|
|
370
|
+
logger.error("GitHub token error %s for %s: %s", status, owner_repo, hint)
|
|
371
|
+
if cfg.slack_bot_token and cfg.slack_channel:
|
|
372
|
+
from .notify import slack_alert
|
|
373
|
+
slack_alert(cfg.slack_bot_token, cfg.slack_channel, msg)
|
|
374
|
+
|
|
375
|
+
|
|
361
376
|
def _open_github_pr(
|
|
362
377
|
event: ErrorEvent,
|
|
363
378
|
repo: RepoConfig,
|
|
@@ -407,10 +422,31 @@ def _open_github_pr(
|
|
|
407
422
|
return pr_url
|
|
408
423
|
else:
|
|
409
424
|
logger.error("Failed to open PR (%s): %s", resp.status_code, resp.text[:300])
|
|
425
|
+
if resp.status_code in (401, 403, 404):
|
|
426
|
+
hint = "Token may lack access to this org/repo." if resp.status_code == 404 else resp.json().get("message", "")
|
|
427
|
+
_alert_github_token_error(cfg, owner_repo, resp.status_code, hint)
|
|
410
428
|
return ""
|
|
411
429
|
|
|
412
430
|
|
|
413
|
-
def
|
|
431
|
+
def _delete_github_branch(owner_repo: str, branch: str, headers: dict) -> None:
|
|
432
|
+
"""Delete a remote branch from GitHub after its PR is merged or closed."""
|
|
433
|
+
try:
|
|
434
|
+
resp = requests.delete(
|
|
435
|
+
f"https://api.github.com/repos/{owner_repo}/git/refs/heads/{branch}",
|
|
436
|
+
headers=headers,
|
|
437
|
+
timeout=15,
|
|
438
|
+
)
|
|
439
|
+
if resp.status_code == 204:
|
|
440
|
+
logger.info("Deleted branch %s from %s", branch, owner_repo)
|
|
441
|
+
elif resp.status_code == 422:
|
|
442
|
+
logger.debug("Branch %s already deleted on %s", branch, owner_repo)
|
|
443
|
+
else:
|
|
444
|
+
logger.warning("Could not delete branch %s (%s): %s", branch, resp.status_code, resp.text[:200])
|
|
445
|
+
except Exception as e:
|
|
446
|
+
logger.warning("Failed to delete branch %s: %s", branch, e)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def poll_open_prs(store, github_token: str, cfg: "SentinelConfig | None" = None) -> list[dict]:
|
|
414
450
|
"""
|
|
415
451
|
Check GitHub for the current state of all pending PRs in the state store.
|
|
416
452
|
|
|
@@ -463,6 +499,14 @@ def poll_open_prs(store, github_token: str) -> list[dict]:
|
|
|
463
499
|
if resp.status_code == 404:
|
|
464
500
|
logger.warning("poll_open_prs: PR not found (deleted?): %s", pr_url)
|
|
465
501
|
continue
|
|
502
|
+
if resp.status_code in (401, 403):
|
|
503
|
+
logger.error(
|
|
504
|
+
"poll_open_prs: GitHub token rejected (%s) for %s — "
|
|
505
|
+
"org may require a fine-grained PAT. Set GITHUB_TOKEN in sentinel.properties.",
|
|
506
|
+
resp.status_code, pr_url,
|
|
507
|
+
)
|
|
508
|
+
_alert_github_token_error(cfg, pr_url, resp.status_code, resp.json().get("message", ""))
|
|
509
|
+
break # No point retrying other PRs with the same bad token
|
|
466
510
|
if resp.status_code != 200:
|
|
467
511
|
logger.warning("poll_open_prs: unexpected %s for %s", resp.status_code, pr_url)
|
|
468
512
|
continue
|
|
@@ -475,6 +519,7 @@ def poll_open_prs(store, github_token: str) -> list[dict]:
|
|
|
475
519
|
continue # still pending
|
|
476
520
|
|
|
477
521
|
new_status = "merged" if merged else "skipped"
|
|
522
|
+
branch = fix.get("branch", "")
|
|
478
523
|
|
|
479
524
|
with store._conn() as conn:
|
|
480
525
|
conn.execute(
|
|
@@ -486,6 +531,11 @@ def poll_open_prs(store, github_token: str) -> list[dict]:
|
|
|
486
531
|
"poll_open_prs: PR #%s %s → %s (fp=%s)",
|
|
487
532
|
pr_number, owner_repo, new_status, fingerprint[:8],
|
|
488
533
|
)
|
|
534
|
+
|
|
535
|
+
# Delete the fix branch from GitHub when PR is closed (merged or rejected)
|
|
536
|
+
if branch:
|
|
537
|
+
_delete_github_branch(owner_repo, branch, headers)
|
|
538
|
+
|
|
489
539
|
changes.append({
|
|
490
540
|
"fingerprint": fingerprint,
|
|
491
541
|
"pr_url": pr_url,
|
package/python/sentinel/main.py
CHANGED
|
@@ -347,7 +347,8 @@ async def _handle_issue(event: IssueEvent, cfg_loader: ConfigLoader, store: Stat
|
|
|
347
347
|
if status != "patch" or patch_path is None:
|
|
348
348
|
store.record_fix(event.fingerprint, "skipped" if status in ("skip", "needs_human") else "failed",
|
|
349
349
|
repo_name=repo.repo_name)
|
|
350
|
-
|
|
350
|
+
raw_reason = marker if status == "needs_human" else f"Claude Code returned {status.upper()}"
|
|
351
|
+
reason_text = _boss_qualify_dev_reason(raw_reason, sentinel) if status == "needs_human" else raw_reason
|
|
351
352
|
_progress(f":x: Could not generate a safe fix — {reason_text[:120]}")
|
|
352
353
|
notify_fix_blocked(sentinel, event.source, event.message,
|
|
353
354
|
reason=reason_text, repo_name=repo.repo_name,
|
|
@@ -548,7 +549,7 @@ async def poll_cycle(cfg_loader: ConfigLoader, store: StateStore):
|
|
|
548
549
|
send_confirmed_notification(cfg_loader.sentinel, confirmed)
|
|
549
550
|
|
|
550
551
|
# ── PR status sync — detect merges/rejections done via GitHub UI ─────────
|
|
551
|
-
pr_changes = poll_open_prs(store, cfg_loader.sentinel.github_token)
|
|
552
|
+
pr_changes = poll_open_prs(store, cfg_loader.sentinel.github_token, cfg=cfg_loader.sentinel)
|
|
552
553
|
for ch in pr_changes:
|
|
553
554
|
if ch["new_status"] == "merged":
|
|
554
555
|
logger.info("PR merged externally: %s (fp=%s)", ch["pr_url"], ch["fingerprint"][:8])
|
|
@@ -924,6 +925,268 @@ async def _sync_loop(cfg_loader: ConfigLoader):
|
|
|
924
925
|
await asyncio.sleep(cfg_loader.sentinel.sync_interval_seconds)
|
|
925
926
|
|
|
926
927
|
|
|
928
|
+
# ── Patch agent (Sentinel self-improvement) ──────────────────────────────────
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def _boss_qualify_dev_reason(raw: str, sentinel) -> str:
|
|
932
|
+
"""
|
|
933
|
+
Boss-qualify a raw Patch reason string (from NEEDS_HUMAN: or SKIP:).
|
|
934
|
+
|
|
935
|
+
Passes the raw text through the Boss LLM to produce a clean, concise,
|
|
936
|
+
user-friendly explanation — so users never see verbose Claude output
|
|
937
|
+
directly. Falls back to a truncated version if the API call fails.
|
|
938
|
+
"""
|
|
939
|
+
if not raw.strip():
|
|
940
|
+
return "(no reason given)"
|
|
941
|
+
if not sentinel.anthropic_api_key:
|
|
942
|
+
# No API key — just truncate and clean up
|
|
943
|
+
return raw[:280].strip()
|
|
944
|
+
try:
|
|
945
|
+
import anthropic as _anthropic
|
|
946
|
+
_client = _anthropic.Anthropic(api_key=sentinel.anthropic_api_key)
|
|
947
|
+
_resp = _client.messages.create(
|
|
948
|
+
model="claude-haiku-4-5-20251001",
|
|
949
|
+
max_tokens=200,
|
|
950
|
+
system=(
|
|
951
|
+
"You are Sentinel Boss, a DevOps agent assistant. "
|
|
952
|
+
"Patch (an autonomous dev agent) produced the following explanation for why it "
|
|
953
|
+
"could not complete a task. Rewrite it as a clear, concise (1-3 sentences), "
|
|
954
|
+
"user-friendly message suitable for a Slack channel. "
|
|
955
|
+
"Be direct and specific. Do not pad with pleasantries. "
|
|
956
|
+
"Do not start with 'I' or mention 'Patch' by name. "
|
|
957
|
+
"Output only the qualified message, nothing else."
|
|
958
|
+
),
|
|
959
|
+
messages=[{"role": "user", "content": f"Patch said:\n{raw[:1000]}"}],
|
|
960
|
+
)
|
|
961
|
+
qualified = _resp.content[0].text.strip() if _resp.content else raw[:280]
|
|
962
|
+
return qualified[:400]
|
|
963
|
+
except Exception as _e:
|
|
964
|
+
logger.warning("Boss: could not qualify dev reason via API: %s", _e)
|
|
965
|
+
return raw[:280].strip()
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
async def _handle_dev_task(task, cfg_loader: ConfigLoader, store: StateStore):
|
|
969
|
+
"""Execute a single dev task via Patch, post progress to Slack."""
|
|
970
|
+
from .sentinel_dev import run_dev_task
|
|
971
|
+
from .dev_watcher import mark_dev_done
|
|
972
|
+
from .notify import slack_alert as _slack_alert, slack_thread_reply as _slack_reply
|
|
973
|
+
|
|
974
|
+
sentinel = cfg_loader.sentinel
|
|
975
|
+
_submitter = task.submitter_user_id
|
|
976
|
+
_started_msg = (
|
|
977
|
+
f":wrench: Patch working on *<@{_submitter}>*'s request\n_{task.message[:120]}_"
|
|
978
|
+
) if _submitter else (
|
|
979
|
+
f":wrench: Patch working on dev task\n_{task.message[:120]}_"
|
|
980
|
+
)
|
|
981
|
+
_thread_ts = _slack_alert(sentinel.slack_bot_token, sentinel.slack_channel, _started_msg)
|
|
982
|
+
|
|
983
|
+
def _progress(msg: str) -> None:
|
|
984
|
+
_slack_reply(sentinel.slack_bot_token, sentinel.slack_channel, _thread_ts, msg)
|
|
985
|
+
|
|
986
|
+
_loop = asyncio.get_event_loop()
|
|
987
|
+
try:
|
|
988
|
+
status, detail = await _loop.run_in_executor(
|
|
989
|
+
None, run_dev_task, task, sentinel, store, _progress
|
|
990
|
+
)
|
|
991
|
+
except Exception:
|
|
992
|
+
logger.exception("Patch: unexpected error on task %s", task.fingerprint[:8])
|
|
993
|
+
_progress(":x: Patch hit an unexpected error — check logs")
|
|
994
|
+
mark_dev_done(task.task_file)
|
|
995
|
+
return
|
|
996
|
+
|
|
997
|
+
mark_dev_done(task.task_file)
|
|
998
|
+
|
|
999
|
+
# Build mention string: submitter + any extra notify users
|
|
1000
|
+
_notify_ids = list(task.notify_user_ids or [])
|
|
1001
|
+
if task.submitter_user_id:
|
|
1002
|
+
mentions = f"<@{task.submitter_user_id}> " + " ".join(
|
|
1003
|
+
f"<@{u}>" for u in _notify_ids if u != task.submitter_user_id
|
|
1004
|
+
)
|
|
1005
|
+
mentions = mentions.strip() + " "
|
|
1006
|
+
else:
|
|
1007
|
+
mentions = " ".join(f"<@{u}>" for u in _notify_ids)
|
|
1008
|
+
mentions = (mentions + " ") if mentions else ""
|
|
1009
|
+
|
|
1010
|
+
if status == "done":
|
|
1011
|
+
_slack_alert(
|
|
1012
|
+
sentinel.slack_bot_token, sentinel.slack_channel,
|
|
1013
|
+
f"{mentions}:white_check_mark: *Patch finished* — changes committed to Sentinel source.",
|
|
1014
|
+
)
|
|
1015
|
+
elif status == "needs_human":
|
|
1016
|
+
# Boss qualifies the raw Patch explanation before surfacing to users
|
|
1017
|
+
qualified = _boss_qualify_dev_reason(detail, sentinel)
|
|
1018
|
+
_slack_alert(
|
|
1019
|
+
sentinel.slack_bot_token, sentinel.slack_channel,
|
|
1020
|
+
f"{mentions}:warning: *Dev task needs human input*\n{qualified}",
|
|
1021
|
+
)
|
|
1022
|
+
elif status == "skip":
|
|
1023
|
+
qualified = _boss_qualify_dev_reason(detail, sentinel)
|
|
1024
|
+
_slack_alert(
|
|
1025
|
+
sentinel.slack_bot_token, sentinel.slack_channel,
|
|
1026
|
+
f"{mentions}:fast_forward: *Dev task skipped* — {qualified}",
|
|
1027
|
+
)
|
|
1028
|
+
else:
|
|
1029
|
+
_slack_alert(
|
|
1030
|
+
sentinel.slack_bot_token, sentinel.slack_channel,
|
|
1031
|
+
f"{mentions}:x: *Patch error* on task `{task.fingerprint[:8]}` — {detail[:200]}",
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
async def _dev_poll_loop(cfg_loader: ConfigLoader, store: StateStore):
|
|
1036
|
+
"""
|
|
1037
|
+
Background task: poll dev-tasks/ every 60 s and dispatch to Patch.
|
|
1038
|
+
Also scans Sentinel's own log for errors and auto-queues self-repair tasks.
|
|
1039
|
+
"""
|
|
1040
|
+
from .dev_watcher import (
|
|
1041
|
+
scan_dev_tasks, purge_old_dev_tasks,
|
|
1042
|
+
scan_sentinel_errors, drop_self_repair_task,
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
# Tracks fingerprints already queued this session (in-memory dedup across polls)
|
|
1046
|
+
_seen_self_fps: set = set()
|
|
1047
|
+
|
|
1048
|
+
# Pre-populate from existing done/cancelled tasks so we don't re-queue on restart
|
|
1049
|
+
project_dir = Path(".")
|
|
1050
|
+
for done_dir in [
|
|
1051
|
+
project_dir / "dev-tasks" / ".done",
|
|
1052
|
+
project_dir / "dev-tasks" / ".cancelled",
|
|
1053
|
+
]:
|
|
1054
|
+
if done_dir.exists():
|
|
1055
|
+
for f in done_dir.iterdir():
|
|
1056
|
+
if f.stem.startswith("self-"):
|
|
1057
|
+
parts = f.stem.split("-")
|
|
1058
|
+
if len(parts) >= 2:
|
|
1059
|
+
_seen_self_fps.add(parts[1])
|
|
1060
|
+
|
|
1061
|
+
# Wait a bit so the main loop and Boss are fully up first
|
|
1062
|
+
await asyncio.sleep(15)
|
|
1063
|
+
|
|
1064
|
+
while True:
|
|
1065
|
+
try:
|
|
1066
|
+
if cfg_loader.sentinel.sentinel_dev_repo_path:
|
|
1067
|
+
purge_old_dev_tasks(project_dir / "dev-tasks")
|
|
1068
|
+
|
|
1069
|
+
# ── Self-repair: scan Sentinel's own log for new errors ────────
|
|
1070
|
+
log_path = project_dir / "logs" / "sentinel.log"
|
|
1071
|
+
new_errors = scan_sentinel_errors(log_path, seen_fps=_seen_self_fps)
|
|
1072
|
+
for fp, task_body in new_errors:
|
|
1073
|
+
logger.info("Dev agent: self-repair task queued for error %s", fp[:8])
|
|
1074
|
+
drop_self_repair_task(project_dir, fp, task_body)
|
|
1075
|
+
|
|
1076
|
+
# ── Process all pending dev tasks (self-repair + human-submitted) ─
|
|
1077
|
+
tasks = scan_dev_tasks(project_dir)
|
|
1078
|
+
if tasks:
|
|
1079
|
+
logger.info("Dev agent: %d task(s) found", len(tasks))
|
|
1080
|
+
for task in tasks:
|
|
1081
|
+
await _handle_dev_task(task, cfg_loader, store)
|
|
1082
|
+
except Exception as e:
|
|
1083
|
+
logger.warning("Dev poll loop error: %s", e)
|
|
1084
|
+
await asyncio.sleep(60)
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
# ── Repo task agent (human-requested changes to managed repos) ────────────────
|
|
1088
|
+
|
|
1089
|
+
async def _handle_repo_task(task, repo_cfg, cfg_loader: ConfigLoader, store: StateStore):
|
|
1090
|
+
"""Execute a single repo task via Claude Code, post progress to Slack."""
|
|
1091
|
+
from .repo_task_engine import run_repo_task, mark_repo_task_done
|
|
1092
|
+
from .notify import slack_alert as _slack_alert, slack_thread_reply as _slack_reply
|
|
1093
|
+
|
|
1094
|
+
sentinel = cfg_loader.sentinel
|
|
1095
|
+
_submitter = task.submitter_user_id
|
|
1096
|
+
_started_msg = (
|
|
1097
|
+
f":hammer: Working on *<@{_submitter}>*'s request for `{task.repo_name}`\n_{task.message[:120]}_"
|
|
1098
|
+
) if _submitter else (
|
|
1099
|
+
f":hammer: Working on repo task for `{task.repo_name}`\n_{task.message[:120]}_"
|
|
1100
|
+
)
|
|
1101
|
+
_thread_ts = _slack_alert(sentinel.slack_bot_token, sentinel.slack_channel, _started_msg)
|
|
1102
|
+
|
|
1103
|
+
def _progress(msg: str) -> None:
|
|
1104
|
+
_slack_reply(sentinel.slack_bot_token, sentinel.slack_channel, _thread_ts, msg)
|
|
1105
|
+
|
|
1106
|
+
_loop = asyncio.get_event_loop()
|
|
1107
|
+
try:
|
|
1108
|
+
status, detail = await _loop.run_in_executor(
|
|
1109
|
+
None, run_repo_task, task, repo_cfg, sentinel, store, _progress,
|
|
1110
|
+
)
|
|
1111
|
+
except Exception:
|
|
1112
|
+
logger.exception("Repo task: unexpected error on task %s", task.fingerprint[:8])
|
|
1113
|
+
_progress(":x: Unexpected error — check logs")
|
|
1114
|
+
mark_repo_task_done(task.task_file)
|
|
1115
|
+
return
|
|
1116
|
+
|
|
1117
|
+
mark_repo_task_done(task.task_file)
|
|
1118
|
+
|
|
1119
|
+
# Build mention string: submitter + extra notify users
|
|
1120
|
+
_notify_ids = list(task.notify_user_ids or [])
|
|
1121
|
+
if task.submitter_user_id:
|
|
1122
|
+
mentions = f"<@{task.submitter_user_id}> " + " ".join(
|
|
1123
|
+
f"<@{u}>" for u in _notify_ids if u != task.submitter_user_id
|
|
1124
|
+
)
|
|
1125
|
+
mentions = mentions.strip() + " "
|
|
1126
|
+
else:
|
|
1127
|
+
mentions = " ".join(f"<@{u}>" for u in _notify_ids)
|
|
1128
|
+
mentions = (mentions + " ") if mentions else ""
|
|
1129
|
+
|
|
1130
|
+
if status == "done":
|
|
1131
|
+
if detail: # PR URL
|
|
1132
|
+
_slack_alert(
|
|
1133
|
+
sentinel.slack_bot_token, sentinel.slack_channel,
|
|
1134
|
+
f"{mentions}:white_check_mark: Done — PR opened for `{task.repo_name}`: {detail}",
|
|
1135
|
+
)
|
|
1136
|
+
else:
|
|
1137
|
+
_slack_alert(
|
|
1138
|
+
sentinel.slack_bot_token, sentinel.slack_channel,
|
|
1139
|
+
f"{mentions}:white_check_mark: Done — changes pushed to `{task.repo_name}/{repo_cfg.branch}`.",
|
|
1140
|
+
)
|
|
1141
|
+
elif status == "needs_human":
|
|
1142
|
+
qualified = _boss_qualify_dev_reason(detail, sentinel)
|
|
1143
|
+
_slack_alert(
|
|
1144
|
+
sentinel.slack_bot_token, sentinel.slack_channel,
|
|
1145
|
+
f"{mentions}:warning: *Task needs human input* (`{task.repo_name}`)\n{qualified}",
|
|
1146
|
+
)
|
|
1147
|
+
elif status == "skip":
|
|
1148
|
+
qualified = _boss_qualify_dev_reason(detail, sentinel)
|
|
1149
|
+
_slack_alert(
|
|
1150
|
+
sentinel.slack_bot_token, sentinel.slack_channel,
|
|
1151
|
+
f"{mentions}:fast_forward: Task skipped for `{task.repo_name}` — {qualified}",
|
|
1152
|
+
)
|
|
1153
|
+
else:
|
|
1154
|
+
_slack_alert(
|
|
1155
|
+
sentinel.slack_bot_token, sentinel.slack_channel,
|
|
1156
|
+
f"{mentions}:x: Task error for `{task.repo_name}` — {(detail or '')[:200]}",
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
async def _repo_task_poll_loop(cfg_loader: ConfigLoader, store: StateStore):
|
|
1161
|
+
"""Background task: poll repo-tasks/ every 60s and dispatch to Claude."""
|
|
1162
|
+
from .repo_task_engine import scan_repo_tasks, mark_repo_task_done
|
|
1163
|
+
|
|
1164
|
+
await asyncio.sleep(20)
|
|
1165
|
+
|
|
1166
|
+
while True:
|
|
1167
|
+
try:
|
|
1168
|
+
project_dir = Path(".")
|
|
1169
|
+
tasks = scan_repo_tasks(project_dir)
|
|
1170
|
+
if tasks:
|
|
1171
|
+
logger.info("Repo task: %d task(s) found", len(tasks))
|
|
1172
|
+
for task in tasks:
|
|
1173
|
+
# Resolve repo config — exact match then fuzzy
|
|
1174
|
+
repo_cfg = cfg_loader.repos.get(task.repo_name)
|
|
1175
|
+
if not repo_cfg:
|
|
1176
|
+
for rname, rcfg in cfg_loader.repos.items():
|
|
1177
|
+
if task.repo_name.lower() in rname.lower():
|
|
1178
|
+
repo_cfg = rcfg
|
|
1179
|
+
break
|
|
1180
|
+
if not repo_cfg:
|
|
1181
|
+
logger.warning("Repo task: no config for repo '%s' — skipping", task.repo_name)
|
|
1182
|
+
mark_repo_task_done(task.task_file)
|
|
1183
|
+
continue
|
|
1184
|
+
await _handle_repo_task(task, repo_cfg, cfg_loader, store)
|
|
1185
|
+
except Exception as e:
|
|
1186
|
+
logger.warning("Repo task poll loop error: %s", e)
|
|
1187
|
+
await asyncio.sleep(60)
|
|
1188
|
+
|
|
1189
|
+
|
|
927
1190
|
# ── Entry point ──────────────────────────────────────────────────────────────────────────────────
|
|
928
1191
|
|
|
929
1192
|
def _log_auth_status(cfg: SentinelConfig) -> None:
|
|
@@ -995,6 +1258,9 @@ async def run_loop(cfg_loader: ConfigLoader, store: StateStore):
|
|
|
995
1258
|
if cfg_loader.sentinel.slack_bot_token:
|
|
996
1259
|
from .slack_bot import run_slack_bot
|
|
997
1260
|
asyncio.ensure_future(run_slack_bot(cfg_loader, store))
|
|
1261
|
+
if cfg_loader.sentinel.sentinel_dev_repo_path:
|
|
1262
|
+
asyncio.ensure_future(_dev_poll_loop(cfg_loader, store))
|
|
1263
|
+
asyncio.ensure_future(_repo_task_poll_loop(cfg_loader, store))
|
|
998
1264
|
|
|
999
1265
|
while True:
|
|
1000
1266
|
try:
|