@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.
- package/lib/.cairn/minify-map.json +12 -0
- package/lib/.cairn/views/244a09_generate.js +274 -0
- package/lib/.cairn/views/fb78ac_upgrade.js +16 -1
- package/lib/generate.js +6 -1
- package/package.json +1 -1
- package/python/scripts/fix_ask_codebase_context.py +249 -0
- package/python/scripts/fix_ask_codebase_stdin.py +49 -0
- package/python/scripts/fix_chain_slack.py +67 -0
- package/python/scripts/fix_fstring.py +51 -0
- package/python/scripts/fix_knowledge_cache.py +323 -0
- package/python/scripts/fix_knowledge_cache_staleness.py +294 -0
- package/python/scripts/fix_merge_confirm.py +295 -0
- package/python/scripts/fix_permission_messages.py +78 -0
- package/python/scripts/fix_pr_check_head_detect.py +84 -0
- package/python/scripts/fix_pr_msg_newlines.py +57 -0
- package/python/scripts/fix_pr_tracking_boss.py +265 -0
- package/python/scripts/fix_pr_tracking_db.py +212 -0
- package/python/scripts/fix_pr_tracking_main.py +174 -0
- package/python/scripts/fix_project_isolation.py +197 -0
- package/python/scripts/fix_system_prompt.py +444 -0
- package/python/scripts/fix_two_bugs.py +220 -0
- package/python/scripts/patch_chain_release.py +236 -0
- package/python/sentinel/cicd_trigger.py +125 -16
- package/python/sentinel/dependency_manager.py +129 -18
- package/python/sentinel/git_manager.py +46 -12
- package/python/sentinel/notify.py +34 -0
- package/python/sentinel/sentinel_boss.py +4139 -3326
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Add _pr_check_loop to main.py:
|
|
4
|
+
- Polls GitHub API every 30 min for open PRs across all managed repos
|
|
5
|
+
- Upserts into pull_requests table
|
|
6
|
+
- Posts one-time Slack notification per new PR to admin channel
|
|
7
|
+
- Closes PRs in DB that are no longer open on GitHub
|
|
8
|
+
"""
|
|
9
|
+
import ast, sys
|
|
10
|
+
|
|
11
|
+
MAIN = '/home/sentinel/sentinel/code/sentinel/main.py'
|
|
12
|
+
|
|
13
|
+
with open(MAIN, 'r', encoding='utf-8') as f:
|
|
14
|
+
src = f.read()
|
|
15
|
+
|
|
16
|
+
# ── 1. Add the _pr_check_loop function before run_loop ────────────────────────
|
|
17
|
+
|
|
18
|
+
ANCHOR = '''async def run_loop(cfg_loader: ConfigLoader, store: StateStore):'''
|
|
19
|
+
|
|
20
|
+
if ANCHOR not in src:
|
|
21
|
+
print("ERROR: run_loop anchor not found")
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
PR_LOOP = '''async def _pr_check_loop(cfg_loader: ConfigLoader, store: StateStore) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Poll GitHub for open PRs across all managed repos every 30 min.
|
|
27
|
+
New PRs are saved to pull_requests table and admins are notified once.
|
|
28
|
+
"""
|
|
29
|
+
import re as _re
|
|
30
|
+
import requests as _req
|
|
31
|
+
|
|
32
|
+
CHECK_INTERVAL = 1800 # 30 minutes
|
|
33
|
+
|
|
34
|
+
def _classify_source(pr: dict) -> str:
|
|
35
|
+
author = (pr.get("user") or {}).get("login", "")
|
|
36
|
+
head = (pr.get("head") or {}).get("ref", "")
|
|
37
|
+
if author == "renovate[bot]" or author.startswith("renovate"):
|
|
38
|
+
return "renovate"
|
|
39
|
+
if head.startswith("sentinel/fix-"):
|
|
40
|
+
return "sentinel-fix"
|
|
41
|
+
return "external"
|
|
42
|
+
|
|
43
|
+
def _owner_repo(repo_cfg) -> str:
|
|
44
|
+
url = getattr(repo_cfg, "repo_url", "") or ""
|
|
45
|
+
if url.startswith("git@"):
|
|
46
|
+
return url.split(":")[-1].removesuffix(".git")
|
|
47
|
+
parts = url.rstrip("/").split("/")
|
|
48
|
+
return "/".join(parts[-2:]).removesuffix(".git")
|
|
49
|
+
|
|
50
|
+
await asyncio.sleep(60) # short delay at startup so everything else is ready
|
|
51
|
+
|
|
52
|
+
while True:
|
|
53
|
+
cfg = cfg_loader.sentinel
|
|
54
|
+
gh_token = cfg.github_token
|
|
55
|
+
if not gh_token:
|
|
56
|
+
await asyncio.sleep(CHECK_INTERVAL)
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
headers = {
|
|
60
|
+
"Authorization": f"Bearer {gh_token}",
|
|
61
|
+
"Accept": "application/vnd.github+json",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for repo_name, repo_cfg in cfg_loader.repos.items():
|
|
65
|
+
owner_repo = _owner_repo(repo_cfg)
|
|
66
|
+
if not owner_repo or "/" not in owner_repo:
|
|
67
|
+
continue
|
|
68
|
+
try:
|
|
69
|
+
resp = _req.get(
|
|
70
|
+
f"https://api.github.com/repos/{owner_repo}/pulls?state=open&per_page=50",
|
|
71
|
+
headers=headers, timeout=15,
|
|
72
|
+
)
|
|
73
|
+
if resp.status_code != 200:
|
|
74
|
+
logger.debug("PR check %s: HTTP %s", repo_name, resp.status_code)
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
open_prs = resp.json()
|
|
78
|
+
seen_nums = set()
|
|
79
|
+
for pr in open_prs:
|
|
80
|
+
num = pr["number"]
|
|
81
|
+
url = pr["html_url"]
|
|
82
|
+
title = pr.get("title", "")
|
|
83
|
+
author = (pr.get("user") or {}).get("login", "unknown")
|
|
84
|
+
head = (pr.get("head") or {}).get("ref", "")
|
|
85
|
+
base = (pr.get("base") or {}).get("ref", "main")
|
|
86
|
+
source = _classify_source(pr)
|
|
87
|
+
seen_nums.add(num)
|
|
88
|
+
|
|
89
|
+
is_new = store.upsert_pr(
|
|
90
|
+
repo_name=repo_name, pr_number=num, pr_url=url,
|
|
91
|
+
title=title, author=author, head_branch=head,
|
|
92
|
+
base_branch=base, source=source, pr_state="open",
|
|
93
|
+
)
|
|
94
|
+
if is_new:
|
|
95
|
+
logger.info("PR check: new PR %s #%d (%s) by %s", repo_name, num, source, author)
|
|
96
|
+
|
|
97
|
+
# Mark PRs in DB that are no longer open on GitHub
|
|
98
|
+
all_tracked = store.get_prs(repo_name=repo_name, state="open")
|
|
99
|
+
for tracked in all_tracked:
|
|
100
|
+
if tracked["pr_number"] not in seen_nums:
|
|
101
|
+
store.close_pr(repo_name, tracked["pr_number"], state="closed")
|
|
102
|
+
logger.info("PR check: closed PR %s #%d (no longer open on GitHub)",
|
|
103
|
+
repo_name, tracked["pr_number"])
|
|
104
|
+
|
|
105
|
+
except Exception as _e:
|
|
106
|
+
logger.debug("PR check error for %s: %s", repo_name, _e)
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
# Notify admins of any unnotified new PRs (one message per PR)
|
|
110
|
+
to_notify = store.get_prs_for_notification()
|
|
111
|
+
if to_notify and cfg.slack_bot_token and cfg.slack_channel:
|
|
112
|
+
from .notify import slack_alert
|
|
113
|
+
for pr in to_notify:
|
|
114
|
+
source_tag = {
|
|
115
|
+
"renovate": ":dependabot: Renovate",
|
|
116
|
+
"sentinel-fix": ":robot_face: Sentinel fix",
|
|
117
|
+
"external": ":octocat: External",
|
|
118
|
+
}.get(pr.get("source", "external"), ":octocat: PR")
|
|
119
|
+
_nl = "\\n"
|
|
120
|
+
msg = (
|
|
121
|
+
f":bell: *New PR* \\u2014 `{pr['repo_name']}` #{pr['pr_number']} [{source_tag}]{_nl}"
|
|
122
|
+
f"*{pr['title']}*{_nl}"
|
|
123
|
+
f"By `{pr['author']}` on branch `{pr['head_branch']}`{_nl}"
|
|
124
|
+
f"{pr['pr_url']}{_nl}"
|
|
125
|
+
f"_Reply: `list prs` to review, then `merge PR #{pr['pr_number']} in {pr['repo_name']}` "
|
|
126
|
+
f"or `drop PR #{pr['pr_number']} in {pr['repo_name']}`_"
|
|
127
|
+
)
|
|
128
|
+
try:
|
|
129
|
+
slack_alert(cfg.slack_bot_token, cfg.slack_channel, msg)
|
|
130
|
+
store.mark_pr_notified(pr['repo_name'], pr['pr_number'])
|
|
131
|
+
logger.info("PR notified: %s #%d", pr['repo_name'], pr['pr_number'])
|
|
132
|
+
except Exception as _ne:
|
|
133
|
+
logger.warning("Failed to notify PR %s #%d: %s", pr['repo_name'], pr['pr_number'], _ne)
|
|
134
|
+
|
|
135
|
+
await asyncio.sleep(CHECK_INTERVAL)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
'''
|
|
139
|
+
|
|
140
|
+
src = src.replace(ANCHOR, PR_LOOP + ANCHOR, 1)
|
|
141
|
+
print("Step 1 OK: _pr_check_loop added")
|
|
142
|
+
|
|
143
|
+
# ── 2. Start the loop in run_loop ─────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
OLD_START = ''' if cfg_loader.sentinel.slack_bot_token:
|
|
146
|
+
from .slack_bot import run_slack_bot
|
|
147
|
+
asyncio.ensure_future(run_slack_bot(cfg_loader, store))'''
|
|
148
|
+
|
|
149
|
+
NEW_START = ''' if cfg_loader.sentinel.slack_bot_token:
|
|
150
|
+
from .slack_bot import run_slack_bot
|
|
151
|
+
asyncio.ensure_future(run_slack_bot(cfg_loader, store))
|
|
152
|
+
|
|
153
|
+
if cfg_loader.sentinel.github_token:
|
|
154
|
+
asyncio.ensure_future(_pr_check_loop(cfg_loader, store))'''
|
|
155
|
+
|
|
156
|
+
if OLD_START not in src:
|
|
157
|
+
print("ERROR: slack_bot start anchor not found")
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
src = src.replace(OLD_START, NEW_START, 1)
|
|
160
|
+
print("Step 2 OK: _pr_check_loop started in run_loop")
|
|
161
|
+
|
|
162
|
+
with open(MAIN, 'w', encoding='utf-8') as f:
|
|
163
|
+
f.write(src)
|
|
164
|
+
print("Written OK")
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
ast.parse(src)
|
|
168
|
+
print("Syntax OK")
|
|
169
|
+
except SyntaxError as e:
|
|
170
|
+
lines = src.splitlines()
|
|
171
|
+
print(f"SyntaxError at line {e.lineno}: {e.msg}")
|
|
172
|
+
for i in range(max(0, e.lineno-5), min(len(lines), e.lineno+3)):
|
|
173
|
+
print(f" {i+1}: {lines[i]}")
|
|
174
|
+
sys.exit(1)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Project isolation + identity:
|
|
4
|
+
1. Add SLACK_WORKSPACE_ID + PROJECT_DESCRIPTION to config_loader
|
|
5
|
+
2. Verify workspace_id on every incoming Slack event in slack_bot.py
|
|
6
|
+
3. Inject project identity + scope isolation into the runtime system prompt
|
|
7
|
+
4. Boss refuses cross-project requests by design
|
|
8
|
+
"""
|
|
9
|
+
import ast, sys
|
|
10
|
+
|
|
11
|
+
CODE = '/home/sentinel/sentinel/code/sentinel'
|
|
12
|
+
|
|
13
|
+
# ── 1. Add fields to SentinelConfig in config_loader.py ──────────────────────
|
|
14
|
+
|
|
15
|
+
with open(f'{CODE}/config_loader.py', 'r', encoding='utf-8') as f:
|
|
16
|
+
cfg_src = f.read()
|
|
17
|
+
|
|
18
|
+
OLD_CFG = ''' project_name: str = "" # optional: friendly name used by Sentinel Boss (e.g. "1881")'''
|
|
19
|
+
|
|
20
|
+
NEW_CFG = ''' project_name: str = "" # optional: friendly name used by Sentinel Boss (e.g. "1881")
|
|
21
|
+
project_description: str = "" # short description of what this project is/does
|
|
22
|
+
slack_workspace_id: str = "" # Slack team_id (T...) — if set, reject events from other workspaces'''
|
|
23
|
+
|
|
24
|
+
if OLD_CFG not in cfg_src:
|
|
25
|
+
print("ERROR: project_name field not found in config_loader")
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
cfg_src = cfg_src.replace(OLD_CFG, NEW_CFG, 1)
|
|
28
|
+
|
|
29
|
+
OLD_LOAD = ''' c.project_name = d.get("PROJECT_NAME", "") or Path(self.config_dir).resolve().parent.name'''
|
|
30
|
+
NEW_LOAD = ''' c.project_name = d.get("PROJECT_NAME", "") or Path(self.config_dir).resolve().parent.name
|
|
31
|
+
c.project_description = d.get("PROJECT_DESCRIPTION", "")
|
|
32
|
+
c.slack_workspace_id = d.get("SLACK_WORKSPACE_ID", "").strip()'''
|
|
33
|
+
|
|
34
|
+
if OLD_LOAD not in cfg_src:
|
|
35
|
+
print("ERROR: project_name load line not found in config_loader")
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
cfg_src = cfg_src.replace(OLD_LOAD, NEW_LOAD, 1)
|
|
38
|
+
|
|
39
|
+
with open(f'{CODE}/config_loader.py', 'w', encoding='utf-8') as f:
|
|
40
|
+
f.write(cfg_src)
|
|
41
|
+
try:
|
|
42
|
+
ast.parse(cfg_src)
|
|
43
|
+
print("Step 1 OK: SLACK_WORKSPACE_ID + PROJECT_DESCRIPTION added to config")
|
|
44
|
+
except SyntaxError as e:
|
|
45
|
+
print(f"SyntaxError config_loader line {e.lineno}: {e.msg}"); sys.exit(1)
|
|
46
|
+
|
|
47
|
+
# ── 2. Workspace verification in slack_bot.py ─────────────────────────────────
|
|
48
|
+
|
|
49
|
+
with open(f'{CODE}/slack_bot.py', 'r', encoding='utf-8') as f:
|
|
50
|
+
bot_src = f.read()
|
|
51
|
+
|
|
52
|
+
# Inject workspace check right after the user_id / allowlist check in _dispatch
|
|
53
|
+
OLD_DISPATCH_CHECK = ''' # Allowlist check — if SLACK_ALLOWED_USERS is configured, silently ignore everyone else
|
|
54
|
+
allowed = cfg_loader.sentinel.slack_allowed_users
|
|
55
|
+
if allowed and user_id not in allowed:
|
|
56
|
+
logger.warning("Boss: ignoring message from unauthorised user %s", user_id)
|
|
57
|
+
return'''
|
|
58
|
+
|
|
59
|
+
NEW_DISPATCH_CHECK = ''' # Workspace isolation — if SLACK_WORKSPACE_ID is set, reject events from other workspaces
|
|
60
|
+
expected_workspace = cfg_loader.sentinel.slack_workspace_id
|
|
61
|
+
if expected_workspace:
|
|
62
|
+
event_team = event.get("team") or event.get("team_id", "")
|
|
63
|
+
if event_team and event_team != expected_workspace:
|
|
64
|
+
logger.warning(
|
|
65
|
+
"Boss: ignoring event from workspace %s (expected %s) — user %s",
|
|
66
|
+
event_team, expected_workspace, user_id,
|
|
67
|
+
)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
# Allowlist check — if SLACK_ALLOWED_USERS is configured, silently ignore everyone else
|
|
71
|
+
allowed = cfg_loader.sentinel.slack_allowed_users
|
|
72
|
+
if allowed and user_id not in allowed:
|
|
73
|
+
logger.warning("Boss: ignoring message from unauthorised user %s", user_id)
|
|
74
|
+
return'''
|
|
75
|
+
|
|
76
|
+
if OLD_DISPATCH_CHECK not in bot_src:
|
|
77
|
+
print("ERROR: allowlist check anchor not found in slack_bot")
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
bot_src = bot_src.replace(OLD_DISPATCH_CHECK, NEW_DISPATCH_CHECK, 1)
|
|
80
|
+
|
|
81
|
+
with open(f'{CODE}/slack_bot.py', 'w', encoding='utf-8') as f:
|
|
82
|
+
f.write(bot_src)
|
|
83
|
+
try:
|
|
84
|
+
ast.parse(bot_src)
|
|
85
|
+
print("Step 2 OK: workspace isolation check added to slack_bot._dispatch")
|
|
86
|
+
except SyntaxError as e:
|
|
87
|
+
print(f"SyntaxError slack_bot line {e.lineno}: {e.msg}"); sys.exit(1)
|
|
88
|
+
|
|
89
|
+
# ── 3. Inject project identity into the runtime system prompt ─────────────────
|
|
90
|
+
|
|
91
|
+
with open(f'{CODE}/sentinel_boss.py', 'r', encoding='utf-8') as f:
|
|
92
|
+
boss = f.read()
|
|
93
|
+
|
|
94
|
+
# Update _resolve_system to accept project context and prepend it
|
|
95
|
+
OLD_RESOLVE = '''def _resolve_system(boss_mode: str = "standard") -> str:
|
|
96
|
+
hint = _BOSS_MODE_HINTS.get(boss_mode, _BOSS_MODE_HINTS["standard"])
|
|
97
|
+
return _SYSTEM.replace("{BOSS_MODE_HINT}", hint)'''
|
|
98
|
+
|
|
99
|
+
NEW_RESOLVE = '''def _resolve_system(boss_mode: str = "standard",
|
|
100
|
+
project_name: str = "",
|
|
101
|
+
project_description: str = "",
|
|
102
|
+
other_project_names: list | None = None) -> str:
|
|
103
|
+
"""Build the system prompt, prepending a project-identity block."""
|
|
104
|
+
hint = _BOSS_MODE_HINTS.get(boss_mode, _BOSS_MODE_HINTS["standard"])
|
|
105
|
+
base = _SYSTEM.replace("{BOSS_MODE_HINT}", hint)
|
|
106
|
+
|
|
107
|
+
if not project_name:
|
|
108
|
+
return base
|
|
109
|
+
|
|
110
|
+
# Project identity header — injected at the very top
|
|
111
|
+
desc_line = f"\\nProject description: {project_description}" if project_description else ""
|
|
112
|
+
others = [n for n in (other_project_names or []) if n.lower() != project_name.lower()]
|
|
113
|
+
if others:
|
|
114
|
+
scope_line = (
|
|
115
|
+
f"\\n\\nSCOPE ISOLATION (important): You serve ONLY the {project_name} project. "
|
|
116
|
+
f"This Sentinel host also runs instances for: {', '.join(others)}. "
|
|
117
|
+
f"If a user asks about {', '.join(others)} or any other project not in your repos, "
|
|
118
|
+
f"decline and explain you are scoped to {project_name} only. "
|
|
119
|
+
f"Never expose config, logs, errors, or code from other projects."
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
scope_line = (
|
|
123
|
+
f"\\n\\nSCOPE: You serve ONLY the {project_name} project. "
|
|
124
|
+
f"Decline requests about projects or repos you do not manage."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
identity = (
|
|
128
|
+
f"PROJECT IDENTITY\\n"
|
|
129
|
+
f"You are Sentinel Boss for: {project_name}{desc_line}"
|
|
130
|
+
f"{scope_line}\\n"
|
|
131
|
+
f"{'=' * 60}\\n\\n"
|
|
132
|
+
)
|
|
133
|
+
return identity + base'''
|
|
134
|
+
|
|
135
|
+
if OLD_RESOLVE not in boss:
|
|
136
|
+
print("ERROR: _resolve_system not found in boss")
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
boss = boss.replace(OLD_RESOLVE, NEW_RESOLVE, 1)
|
|
139
|
+
print("Step 3a OK: _resolve_system updated to accept project context")
|
|
140
|
+
|
|
141
|
+
# Update both call sites of _resolve_system to pass project + other-projects context
|
|
142
|
+
# There are two call sites (CLI mode ~3711 and API mode ~3910)
|
|
143
|
+
# We'll update the API mode one first — it has access to cfg_loader
|
|
144
|
+
|
|
145
|
+
OLD_SYSTEM_CALL_API = ''' system = (
|
|
146
|
+
_resolve_system(getattr(cfg_loader.sentinel, "boss_mode", "standard"))'''
|
|
147
|
+
|
|
148
|
+
NEW_SYSTEM_CALL_API = ''' _known_projects = [_read_project_name(d) for d in _find_project_dirs()]
|
|
149
|
+
system = (
|
|
150
|
+
_resolve_system(
|
|
151
|
+
boss_mode=getattr(cfg_loader.sentinel, "boss_mode", "standard"),
|
|
152
|
+
project_name=cfg_loader.sentinel.project_name or _read_project_name(Path(".")),
|
|
153
|
+
project_description=getattr(cfg_loader.sentinel, "project_description", ""),
|
|
154
|
+
other_project_names=_known_projects,
|
|
155
|
+
)'''
|
|
156
|
+
|
|
157
|
+
if OLD_SYSTEM_CALL_API not in boss:
|
|
158
|
+
print("ERROR: API system prompt call not found")
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
boss = boss.replace(OLD_SYSTEM_CALL_API, NEW_SYSTEM_CALL_API, 1)
|
|
161
|
+
print("Step 3b OK: API-mode system prompt passes project identity")
|
|
162
|
+
|
|
163
|
+
# CLI fallback mode
|
|
164
|
+
OLD_SYSTEM_CALL_CLI = ''' _resolve_system(getattr(cfg_loader.sentinel, "boss_mode", "standard"))
|
|
165
|
+
+ (f"\\nYou are speaking with: {user_name}'''
|
|
166
|
+
|
|
167
|
+
NEW_SYSTEM_CALL_CLI = ''' _resolve_system(
|
|
168
|
+
boss_mode=getattr(cfg_loader.sentinel, "boss_mode", "standard"),
|
|
169
|
+
project_name=cfg_loader.sentinel.project_name or _read_project_name(Path(".")),
|
|
170
|
+
project_description=getattr(cfg_loader.sentinel, "project_description", ""),
|
|
171
|
+
other_project_names=[_read_project_name(d) for d in _find_project_dirs()],
|
|
172
|
+
)
|
|
173
|
+
+ (f"\\nYou are speaking with: {user_name}'''
|
|
174
|
+
|
|
175
|
+
if OLD_SYSTEM_CALL_CLI not in boss:
|
|
176
|
+
print("ERROR: CLI system prompt call not found")
|
|
177
|
+
sys.exit(1)
|
|
178
|
+
boss = boss.replace(OLD_SYSTEM_CALL_CLI, NEW_SYSTEM_CALL_CLI, 1)
|
|
179
|
+
print("Step 3c OK: CLI-mode system prompt passes project identity")
|
|
180
|
+
|
|
181
|
+
with open(f'{CODE}/sentinel_boss.py', 'w', encoding='utf-8') as f:
|
|
182
|
+
f.write(boss)
|
|
183
|
+
try:
|
|
184
|
+
ast.parse(boss)
|
|
185
|
+
print("Step 3 OK: sentinel_boss.py Syntax OK")
|
|
186
|
+
except SyntaxError as e:
|
|
187
|
+
lines = boss.splitlines()
|
|
188
|
+
print(f"SyntaxError line {e.lineno}: {e.msg}")
|
|
189
|
+
for i in range(max(0, e.lineno-4), min(len(lines), e.lineno+3)):
|
|
190
|
+
print(f" {i+1}: {lines[i]}")
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
|
|
193
|
+
print("\nAll steps complete.")
|
|
194
|
+
print("Add to each project's sentinel.properties:")
|
|
195
|
+
print(" PROJECT_NAME=1881")
|
|
196
|
+
print(" PROJECT_DESCRIPTION=Norwegian directory services and telecom platform")
|
|
197
|
+
print(" SLACK_WORKSPACE_ID=T01234ABCD # Slack team_id for workspace verification")
|