@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
|
@@ -4,5 +4,17 @@
|
|
|
4
4
|
"state": "edit-ready",
|
|
5
5
|
"minifiedAt": 1774403831187.837,
|
|
6
6
|
"readCount": 1
|
|
7
|
+
},
|
|
8
|
+
"J:\\Projects\\Sentinel\\cli\\lib\\upgrade.js": {
|
|
9
|
+
"tempPath": "J:\\Projects\\Sentinel\\cli\\lib\\.cairn\\views\\fb78ac_upgrade.js",
|
|
10
|
+
"state": "edit-ready",
|
|
11
|
+
"minifiedAt": 1774409075884.3267,
|
|
12
|
+
"readCount": 1
|
|
13
|
+
},
|
|
14
|
+
"J:\\Projects\\Sentinel\\cli\\lib\\generate.js": {
|
|
15
|
+
"tempPath": "J:\\Projects\\Sentinel\\cli\\lib\\.cairn\\views\\244a09_generate.js",
|
|
16
|
+
"state": "compressed",
|
|
17
|
+
"minifiedAt": 1774454098784.183,
|
|
18
|
+
"readCount": 1
|
|
7
19
|
}
|
|
8
20
|
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
function writeExampleProject(projectDir, codeDir, pythonBin, anthropicKey = '', slackTokens = {}) {
|
|
5
|
+
const configDir = path.join(projectDir, 'config', 'log-configs');
|
|
6
|
+
const repoDir = path.join(projectDir, 'config', 'repo-configs');
|
|
7
|
+
fs.ensureDirSync(configDir);
|
|
8
|
+
fs.ensureDirSync(repoDir);
|
|
9
|
+
const tplDir = path.join(__dirname, '..', 'templates');
|
|
10
|
+
let sentinelProps = fs.readFileSync(path.join(tplDir, 'sentinel.properties'), 'utf8');
|
|
11
|
+
if (anthropicKey) {
|
|
12
|
+
sentinelProps = sentinelProps.replace(/^# ANTHROPIC_API_KEY=.*/m, `ANTHROPIC_API_KEY=${anthropicKey}`);
|
|
13
|
+
}
|
|
14
|
+
if (slackTokens.botToken) {
|
|
15
|
+
sentinelProps = sentinelProps.replace(/^# SLACK_BOT_TOKEN=.*/m, `SLACK_BOT_TOKEN=${slackTokens.botToken}`);
|
|
16
|
+
}
|
|
17
|
+
if (slackTokens.appToken) {
|
|
18
|
+
sentinelProps = sentinelProps.replace(/^# SLACK_APP_TOKEN=.*/m, `SLACK_APP_TOKEN=${slackTokens.appToken}`);
|
|
19
|
+
}
|
|
20
|
+
fs.writeFileSync(path.join(projectDir, 'config', 'sentinel.properties'), sentinelProps);
|
|
21
|
+
fs.copySync(path.join(tplDir, 'log-configs', '_example.properties'), path.join(configDir, '_example.properties'));
|
|
22
|
+
fs.copySync(path.join(tplDir, 'repo-configs', '_example.properties'), path.join(repoDir, '_example.properties'));
|
|
23
|
+
generateProjectScripts(projectDir, codeDir, pythonBin);
|
|
24
|
+
}
|
|
25
|
+
function generateProjectScripts(projectDir, codeDir, pythonBin) {
|
|
26
|
+
const name = path.basename(projectDir);
|
|
27
|
+
fs.writeFileSync(path.join(projectDir, 'start.sh'), `#!/usr/bin/env bash
|
|
28
|
+
# Start this Sentinel instance
|
|
29
|
+
set -euo pipefail
|
|
30
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
31
|
+
PID_FILE="$DIR/sentinel.pid"
|
|
32
|
+
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
|
33
|
+
echo "[sentinel] ${name} already running (PID $(cat "$PID_FILE"))"
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
# Kill any orphaned sentinel processes for this project (stale PIDs not in PID file)
|
|
37
|
+
pkill -f "sentinel.main --config $DIR/config" 2>/dev/null || true
|
|
38
|
+
rm -f "$PID_FILE"
|
|
39
|
+
WORKSPACE="$(dirname "$DIR")"
|
|
40
|
+
# Check Claude Code authentication — skip if CLAUDE_PRO_FOR_TASKS=false in either config
|
|
41
|
+
_claude_pro=true
|
|
42
|
+
for _conf in "$WORKSPACE/sentinel.properties" "$DIR/config/sentinel.properties"; do
|
|
43
|
+
if [[ -f "$_conf" ]]; then
|
|
44
|
+
_val=$(grep -iE "^CLAUDE_PRO_FOR_TASKS[[:space:]]*=" "$_conf" 2>/dev/null | tail -1 | cut -d= -f2- | tr -d ' ' | tr '[:upper:]' '[:lower:]' || true)
|
|
45
|
+
[[ -n "$_val" ]] && _claude_pro="$_val"
|
|
46
|
+
fi
|
|
47
|
+
done
|
|
48
|
+
if [[ "$_claude_pro" != "false" ]]; then
|
|
49
|
+
AUTH_OUT=$(claude --print \"hi\" 2>&1 || true)
|
|
50
|
+
if echo "$AUTH_OUT" | grep -Eqi "not logged in|/login"; then
|
|
51
|
+
echo ""
|
|
52
|
+
echo "[sentinel] Claude Code is not authenticated."
|
|
53
|
+
echo " 1. Open a new terminal and run: claude"
|
|
54
|
+
echo " 2. Type /login at the prompt"
|
|
55
|
+
echo " 3. Open the URL in any browser and log in"
|
|
56
|
+
echo " 4. Type /exit when done"
|
|
57
|
+
echo " 5. Re-run this script"
|
|
58
|
+
echo ""
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
mkdir -p "$DIR/logs" "$WORKSPACE/logs" "$DIR/workspace/fetched" "$DIR/workspace/patches" "$DIR/issues"
|
|
63
|
+
cd "$DIR"
|
|
64
|
+
# Ensure npm-global bin (cairn-mcp, claude) and ~/.local/bin (auto-installed tools) are on PATH
|
|
65
|
+
export PATH="$HOME/.npm-global/bin:$HOME/.local/bin:$PATH"
|
|
66
|
+
PYTHONPATH="${codeDir}" "${codeDir}/.venv/bin/python3" -m sentinel.main --config ./config \\
|
|
67
|
+
>> "$DIR/logs/sentinel.log" 2>&1 &
|
|
68
|
+
echo $! > "$PID_FILE"
|
|
69
|
+
echo "[sentinel] ${name} started (PID $!)"
|
|
70
|
+
echo " project log : $DIR/logs/sentinel.log"
|
|
71
|
+
echo " workspace log: $WORKSPACE/logs/sentinel.log"
|
|
72
|
+
`, { mode: 0o755 });
|
|
73
|
+
fs.writeFileSync(path.join(projectDir, 'stop.sh'), `#!/usr/bin/env bash
|
|
74
|
+
# Stop this Sentinel instance
|
|
75
|
+
set -euo pipefail
|
|
76
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
77
|
+
PID_FILE="$DIR/sentinel.pid"
|
|
78
|
+
if [[ ! -f "$PID_FILE" ]]; then
|
|
79
|
+
echo "[sentinel] ${name} — no PID file, not running"
|
|
80
|
+
exit 0
|
|
81
|
+
fi
|
|
82
|
+
PID=$(cat "$PID_FILE")
|
|
83
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
84
|
+
kill "$PID"
|
|
85
|
+
echo "[sentinel] ${name} stopped (PID $PID)"
|
|
86
|
+
else
|
|
87
|
+
echo "[sentinel] ${name} — PID $PID not running"
|
|
88
|
+
fi
|
|
89
|
+
rm -f "$PID_FILE"
|
|
90
|
+
`, { mode: 0o755 });
|
|
91
|
+
}
|
|
92
|
+
function generateWorkspaceScripts(workspace, smtpConfig = {}, slackConfig = {}, authConfig = {}, githubToken = '') {
|
|
93
|
+
const workspaceProps = path.join(workspace, 'sentinel.properties');
|
|
94
|
+
if (!fs.existsSync(workspaceProps)) {
|
|
95
|
+
const tplDir = path.join(__dirname, '..', 'templates');
|
|
96
|
+
let tpl = fs.readFileSync(path.join(tplDir, 'workspace-sentinel.properties'), 'utf8');
|
|
97
|
+
if (smtpConfig.host) tpl = tpl.replace('SMTP_HOST=smtp.gmail.com', 'SMTP_HOST=' + smtpConfig.host);
|
|
98
|
+
if (smtpConfig.port) tpl = tpl.replace('SMTP_PORT=587', 'SMTP_PORT=' + smtpConfig.port);
|
|
99
|
+
if (smtpConfig.user) tpl = tpl.replace('SMTP_USER=sentinel@yourdomain.com', 'SMTP_USER=' + smtpConfig.user);
|
|
100
|
+
if (smtpConfig.password) tpl = tpl.replace('SMTP_PASSWORD=<app-password>', 'SMTP_PASSWORD=' + smtpConfig.password);
|
|
101
|
+
fs.writeFileSync(workspaceProps, tpl);
|
|
102
|
+
}
|
|
103
|
+
if (authConfig.apiKey || authConfig.claudeProForTasks !== undefined) {
|
|
104
|
+
let props = fs.readFileSync(workspaceProps, 'utf8');
|
|
105
|
+
if (authConfig.apiKey) {
|
|
106
|
+
if (/^#?\s*ANTHROPIC_API_KEY=/m.test(props))
|
|
107
|
+
props = props.replace(/^#?\s*ANTHROPIC_API_KEY=.*/mg, 'ANTHROPIC_API_KEY=' + authConfig.apiKey);
|
|
108
|
+
else
|
|
109
|
+
props = props.trimEnd() + '\nANTHROPIC_API_KEY=' + authConfig.apiKey + '\n';
|
|
110
|
+
}
|
|
111
|
+
if (authConfig.claudeProForTasks !== undefined) {
|
|
112
|
+
const val = authConfig.claudeProForTasks ? 'true' : 'false';
|
|
113
|
+
if (/^#?\s*CLAUDE_PRO_FOR_TASKS=/m.test(props))
|
|
114
|
+
props = props.replace(/^#?\s*CLAUDE_PRO_FOR_TASKS=.*/mg, 'CLAUDE_PRO_FOR_TASKS=' + val);
|
|
115
|
+
else
|
|
116
|
+
props = props.trimEnd() + '\nCLAUDE_PRO_FOR_TASKS=' + val + '\n';
|
|
117
|
+
}
|
|
118
|
+
fs.writeFileSync(workspaceProps, props);
|
|
119
|
+
}
|
|
120
|
+
if (githubToken) {
|
|
121
|
+
let props = fs.readFileSync(workspaceProps, 'utf8');
|
|
122
|
+
if (/^#?\s*GITHUB_TOKEN=/m.test(props))
|
|
123
|
+
props = props.replace(/^#?\s*GITHUB_TOKEN=.*/mg, 'GITHUB_TOKEN=' + githubToken);
|
|
124
|
+
else
|
|
125
|
+
props = props.trimEnd() + '\nGITHUB_TOKEN=' + githubToken + '\n';
|
|
126
|
+
fs.writeFileSync(workspaceProps, props);
|
|
127
|
+
}
|
|
128
|
+
if (slackConfig.botToken || slackConfig.appToken) {
|
|
129
|
+
let props = fs.readFileSync(workspaceProps, 'utf8');
|
|
130
|
+
if (slackConfig.botToken) {
|
|
131
|
+
if (/^#?\s*SLACK_BOT_TOKEN=/m.test(props))
|
|
132
|
+
props = props.replace(/^#?\s*SLACK_BOT_TOKEN=.*/mg, 'SLACK_BOT_TOKEN=' + slackConfig.botToken);
|
|
133
|
+
else
|
|
134
|
+
props = props.trimEnd() + '\nSLACK_BOT_TOKEN=' + slackConfig.botToken + '\n';
|
|
135
|
+
}
|
|
136
|
+
if (slackConfig.appToken) {
|
|
137
|
+
if (/^#?\s*SLACK_APP_TOKEN=/m.test(props))
|
|
138
|
+
props = props.replace(/^#?\s*SLACK_APP_TOKEN=.*/mg, 'SLACK_APP_TOKEN=' + slackConfig.appToken);
|
|
139
|
+
else
|
|
140
|
+
props = props.trimEnd() + '\nSLACK_APP_TOKEN=' + slackConfig.appToken + '\n';
|
|
141
|
+
}
|
|
142
|
+
fs.writeFileSync(workspaceProps, props);
|
|
143
|
+
}
|
|
144
|
+
fs.writeFileSync(path.join(workspace, 'startAll.sh'), `#!/usr/bin/env bash
|
|
145
|
+
# Start all valid Sentinel project instances.
|
|
146
|
+
# A valid project must have config/repo-configs; do
|
|
147
|
+
[[ -d "$project_dir" ]] || continue
|
|
148
|
+
name=$(basename "$project_dir")
|
|
149
|
+
echo " $NON_PROJECT " | grep -qw "$name" && continue
|
|
150
|
+
# Auto-generate start.sh / stop.sh if missing (codeDir = $WORKSPACE/code)
|
|
151
|
+
if [[ ! -f "$project_dir/start.sh" ]]; then
|
|
152
|
+
code_dir="$WORKSPACE/code"
|
|
153
|
+
python_bin="$code_dir/.venv/bin/python3"
|
|
154
|
+
sed -e "s|__NAME__|$name|g" -e "s|__CODE_DIR__|$code_dir|g" -e "s|__PYTHON_BIN__|$python_bin|g" << 'STARTSH' > "$project_dir/start.sh"
|
|
155
|
+
#!/usr/bin/env bash
|
|
156
|
+
set -euo pipefail
|
|
157
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
158
|
+
PID_FILE="$DIR/sentinel.pid"
|
|
159
|
+
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
|
160
|
+
echo "[sentinel] __NAME__ already running (PID $(cat "$PID_FILE"))"
|
|
161
|
+
exit 0
|
|
162
|
+
fi
|
|
163
|
+
pkill -f "sentinel.main --config $DIR/config" 2>/dev/null || true
|
|
164
|
+
rm -f "$PID_FILE"
|
|
165
|
+
WORKSPACE="$(dirname "$DIR")"
|
|
166
|
+
_claude_pro=true
|
|
167
|
+
for _conf in "$WORKSPACE/sentinel.properties" "$DIR/config/sentinel.properties"; do
|
|
168
|
+
if [[ -f "$_conf" ]]; then
|
|
169
|
+
_val=$(grep -iE "^CLAUDE_PRO_FOR_TASKS[[:space:]]*=" "$_conf" 2>/dev/null | tail -1 | cut -d= -f2- | tr -d ' ' | tr '[:upper:]' '[:lower:]' || true)
|
|
170
|
+
[[ -n "$_val" ]] && _claude_pro="$_val"
|
|
171
|
+
fi
|
|
172
|
+
done
|
|
173
|
+
if [[ "$_claude_pro" != "false" ]]; then
|
|
174
|
+
AUTH_OUT=$(claude --print \"hi\" 2>&1 || true)
|
|
175
|
+
if echo "$AUTH_OUT" | grep -Eqi "not logged in|/login"; then
|
|
176
|
+
echo "[sentinel] Claude Code is not authenticated. Run: claude then /login"
|
|
177
|
+
exit 1
|
|
178
|
+
fi
|
|
179
|
+
fi
|
|
180
|
+
mkdir -p "$DIR/logs" "$WORKSPACE/logs" "$DIR/workspace/fetched" "$DIR/workspace/patches" "$DIR/issues"
|
|
181
|
+
cd "$DIR"
|
|
182
|
+
PYTHONPATH="__CODE_DIR__" "__PYTHON_BIN__" -m sentinel.main --config ./config \
|
|
183
|
+
> "$DIR/logs/sentinel.log" 2>&1 &
|
|
184
|
+
echo $! > "$PID_FILE"
|
|
185
|
+
echo "[sentinel] __NAME__ started (PID $!)"
|
|
186
|
+
echo " project log : $DIR/logs/sentinel.log"
|
|
187
|
+
echo " workspace log: $WORKSPACE/logs/sentinel.log"
|
|
188
|
+
STARTSH
|
|
189
|
+
chmod +x "$project_dir/start.sh"
|
|
190
|
+
echo "[sentinel] Auto-generated start.sh for $name"
|
|
191
|
+
fi
|
|
192
|
+
if [[ ! -f "$project_dir/stop.sh" ]]; then
|
|
193
|
+
sed -e "s|__NAME__|$name|g" << 'STOPSH' > "$project_dir/stop.sh"
|
|
194
|
+
#!/usr/bin/env bash
|
|
195
|
+
set -euo pipefail
|
|
196
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
197
|
+
PID_FILE="$DIR/sentinel.pid"
|
|
198
|
+
if [[ ! -f "$PID_FILE" ]]; then
|
|
199
|
+
echo "[sentinel] __NAME__ — no PID file, not running"
|
|
200
|
+
exit 0
|
|
201
|
+
fi
|
|
202
|
+
PID=$(cat "$PID_FILE")
|
|
203
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
204
|
+
kill "$PID"
|
|
205
|
+
echo "[sentinel] __NAME__ stopped (PID $PID)"
|
|
206
|
+
else
|
|
207
|
+
echo "[sentinel] __NAME__ — PID $PID not running"
|
|
208
|
+
fi
|
|
209
|
+
rm -f "$PID_FILE"
|
|
210
|
+
STOPSH
|
|
211
|
+
chmod +x "$project_dir/stop.sh"
|
|
212
|
+
echo "[sentinel] Auto-generated stop.sh for $name"
|
|
213
|
+
fi
|
|
214
|
+
# Must have at least one repo-config with a valid GitHub REPO_URL
|
|
215
|
+
repo_configs_dir="$project_dir/config/repo-configs"
|
|
216
|
+
if [[ ! -d "$repo_configs_dir" ]]; then
|
|
217
|
+
echo "[sentinel] Skipping $name — config/repo-configs/ directory not found"
|
|
218
|
+
skipped=$((skipped + 1))
|
|
219
|
+
continue
|
|
220
|
+
fi
|
|
221
|
+
has_config=false
|
|
222
|
+
valid_repo=false
|
|
223
|
+
for props in "$repo_configs_dir/"*.properties; do
|
|
224
|
+
[[ -f "$props" ]] || continue
|
|
225
|
+
[[ "$(basename "$props")" == _* ]] && continue
|
|
226
|
+
has_config=true
|
|
227
|
+
if grep -qE "^REPO_URL[[:space:]]*=[[:space:]]*(git@github\.com:|https://github\.com/)" "$props"; then
|
|
228
|
+
valid_repo=true
|
|
229
|
+
break
|
|
230
|
+
else
|
|
231
|
+
repo_url=$(grep -E "^REPO_URL[[:space:]]*=" "$props" | head -1 | cut -d= -f2- | xargs 2>/dev/null || true)
|
|
232
|
+
if [[ -z "$repo_url" ]]; then
|
|
233
|
+
echo "[sentinel] Skipping $name — REPO_URL not set in $(basename \"$props\")"
|
|
234
|
+
else
|
|
235
|
+
echo "[sentinel] Skipping $name — REPO_URL in $(basename \"$props\") is not a GitHub URL: $repo_url"
|
|
236
|
+
fi
|
|
237
|
+
fi
|
|
238
|
+
done
|
|
239
|
+
if [[ "$has_config" == "false" ]]; then
|
|
240
|
+
echo "[sentinel] Skipping $name — no .properties files in config/repo-configs/ (only _example?)"
|
|
241
|
+
skipped=$((skipped + 1))
|
|
242
|
+
continue
|
|
243
|
+
fi
|
|
244
|
+
if [[ "$valid_repo" == "false" ]]; then
|
|
245
|
+
skipped=$((skipped + 1))
|
|
246
|
+
continue
|
|
247
|
+
fi
|
|
248
|
+
if bash "$project_dir/start.sh"; then
|
|
249
|
+
started=$((started + 1))
|
|
250
|
+
else
|
|
251
|
+
echo "[sentinel] Failed to start $name"
|
|
252
|
+
skipped=$((skipped + 1))
|
|
253
|
+
fi
|
|
254
|
+
done
|
|
255
|
+
echo "[sentinel] $started project(s) started, $skipped skipped"
|
|
256
|
+
`, { mode: 0o755 });
|
|
257
|
+
// stopAll.sh
|
|
258
|
+
fs.writeFileSync(path.join(workspace, 'stopAll.sh'), `#!/usr/bin/env bash
|
|
259
|
+
# Stop all Sentinel project instances
|
|
260
|
+
WORKSPACE="$(cd "$(dirname "$0")" && pwd)"
|
|
261
|
+
stopped=0
|
|
262
|
+
NON_PROJECT="code repos logs issues workspace"
|
|
263
|
+
for project_dir in "$WORKSPACE"/*/; do
|
|
264
|
+
[[ -d "$project_dir" ]] || continue
|
|
265
|
+
name=$(basename "$project_dir")
|
|
266
|
+
echo " $NON_PROJECT " | grep -qw "$name" && continue
|
|
267
|
+
[[ -f "$project_dir/stop.sh" ]] || continue
|
|
268
|
+
bash "$project_dir/stop.sh"
|
|
269
|
+
stopped=$((stopped + 1))
|
|
270
|
+
done
|
|
271
|
+
echo "[sentinel] $stopped project(s) stopped"
|
|
272
|
+
`, { mode: 0o755 });
|
|
273
|
+
}
|
|
274
|
+
module.exports = { writeExampleProject, generateProjectScripts, generateWorkspaceScripts };
|
|
@@ -91,6 +91,21 @@ module.exports = async function upgrade() {
|
|
|
91
91
|
if (shFiles.length) spawnSync('chmod', ['+x', ...shFiles], { stdio: 'inherit' });
|
|
92
92
|
}
|
|
93
93
|
ok('Python source updated');
|
|
94
|
+
info('Updating Python packages...');
|
|
95
|
+
const venv = path.join(codeDir, '.venv');
|
|
96
|
+
const pip = fs.existsSync(path.join(venv, 'bin', 'pip')) ? path.join(venv, 'bin', 'pip')
|
|
97
|
+
: fs.existsSync(path.join(venv, 'Scripts', 'pip.exe')) ? path.join(venv, 'Scripts', 'pip.exe')
|
|
98
|
+
: null;
|
|
99
|
+
if (pip) {
|
|
100
|
+
const pipResult = spawnSync(pip, ['install', '--upgrade', '-r', path.join(codeDir, 'requirements.txt')], { stdio: 'inherit' });
|
|
101
|
+
if (pipResult.status !== 0) {
|
|
102
|
+
warn('pip install failed — some Python packages may be missing');
|
|
103
|
+
} else {
|
|
104
|
+
ok('Python packages up to date');
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
warn('venv not found — skipping pip install (run: sentinel init)');
|
|
108
|
+
}
|
|
94
109
|
info('Patching Claude Code permissions…');
|
|
95
110
|
ensureClaudePermissions();
|
|
96
111
|
const { version: latest } = require(path.join(pkgDir, 'package.json'));
|
|
@@ -98,7 +113,7 @@ module.exports = async function upgrade() {
|
|
|
98
113
|
info('Regenerating scripts...');
|
|
99
114
|
const { generateProjectScripts, generateWorkspaceScripts } = require('./generate');
|
|
100
115
|
generateWorkspaceScripts(defaultWorkspace);
|
|
101
|
-
const pythonBin =
|
|
116
|
+
const pythonBin = path.join(venv, 'bin', 'python3');
|
|
102
117
|
const NON_PROJECT_DIRS = new Set(['logs', 'code', 'repos', 'workspace', 'issues']);
|
|
103
118
|
let regenerated = 0;
|
|
104
119
|
try {
|
package/lib/generate.js
CHANGED
|
@@ -75,8 +75,13 @@ if [[ "$_claude_pro" != "false" ]]; then
|
|
|
75
75
|
fi
|
|
76
76
|
mkdir -p "$DIR/logs" "$WORKSPACE/logs" "$DIR/workspace/fetched" "$DIR/workspace/patches" "$DIR/issues"
|
|
77
77
|
cd "$DIR"
|
|
78
|
-
#
|
|
78
|
+
# Auto-detect JAVA_HOME (needed for Maven)
|
|
79
|
+
for _jdk in "$HOME"/jdk-* "$HOME"/.jdk /usr/lib/jvm/java-21-openjdk /usr/lib/jvm/java-11-openjdk; do
|
|
80
|
+
if [[ -d "$_jdk" ]] && [[ -x "$_jdk/bin/java" ]]; then export JAVA_HOME="$_jdk"; break; fi
|
|
81
|
+
done
|
|
82
|
+
# Ensure npm-global bin (cairn-mcp, claude), ~/.local/bin (auto-installed tools), and JAVA_HOME on PATH
|
|
79
83
|
export PATH="$HOME/.npm-global/bin:$HOME/.local/bin:$PATH"
|
|
84
|
+
[[ -n "$JAVA_HOME" ]] && export PATH="$JAVA_HOME/bin:$PATH"
|
|
80
85
|
PYTHONPATH="${codeDir}" "${codeDir}/.venv/bin/python3" -m sentinel.main --config ./config \\
|
|
81
86
|
>> "$DIR/logs/sentinel.log" 2>&1 &
|
|
82
87
|
echo $! > "$PID_FILE"
|
package/package.json
CHANGED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Improve ask_codebase:
|
|
4
|
+
1. Enrich prompt with project context (all repos Sentinel manages, their paths/URLs)
|
|
5
|
+
2. Add optional 'mode' param: explore (default) | issues
|
|
6
|
+
3. 'issues' mode: Claude outputs structured GitHub issue suggestions
|
|
7
|
+
4. Tool description updated to mention project-level discussions and issue raising
|
|
8
|
+
"""
|
|
9
|
+
import ast, sys
|
|
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
|
+
# ── 1. Update tool definition ─────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
OLD_DEF = ''' "name": "ask_codebase",
|
|
19
|
+
"description": (
|
|
20
|
+
"Ask any natural-language question about a managed codebase. "
|
|
21
|
+
"Accepts a repo name (e.g. 'STS', 'elprint-sales') OR a project name (e.g. '1881', 'elprint') "
|
|
22
|
+
"— if a project name is given and it has multiple repos, all are queried. "
|
|
23
|
+
"Claude Code answers using its full codebase knowledge — no need to specify how. "
|
|
24
|
+
"Use for: 'what does 1881 do?', 'TODOs in 1881', 'find PIN validation in STS', "
|
|
25
|
+
"'security issues in elprint-sales?', 'summarize the cairn repo'."
|
|
26
|
+
),
|
|
27
|
+
"input_schema": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"repo": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Repo name (e.g. 'STS', 'elprint-sales') OR project name (e.g. '1881', 'elprint') — project name queries all its repos",
|
|
33
|
+
},
|
|
34
|
+
"question": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Natural language question about the codebase",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
"required": ["repo", "question"],
|
|
40
|
+
},
|
|
41
|
+
},'''
|
|
42
|
+
|
|
43
|
+
NEW_DEF = ''' "name": "ask_codebase",
|
|
44
|
+
"description": (
|
|
45
|
+
"Ask any natural-language question about a managed codebase — or discuss architecture, "
|
|
46
|
+
"extensions, and new features. Claude Code explores the repo(s) with full file access. "
|
|
47
|
+
"Accepts a repo name (e.g. 'STS', 'TypeLib') OR project name (e.g. '1881', 'Whydah') — "
|
|
48
|
+
"project name queries all its repos. "
|
|
49
|
+
"Use for: 'what does 1881 do?', 'describe the project structure', "
|
|
50
|
+
"'what should we implement next in STS?', 'find security issues in elprint-sales', "
|
|
51
|
+
"'what features could we add to TypeLib?', 'summarise the cairn architecture'. "
|
|
52
|
+
"Use mode=issues to make Claude output structured GitHub issue suggestions "
|
|
53
|
+
"(e.g. 'raise issues for improvements in STS', 'what bugs should we track in 1881?')."
|
|
54
|
+
),
|
|
55
|
+
"input_schema": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"repo": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Repo name (e.g. 'STS', 'TypeLib') OR project name (e.g. '1881', 'Whydah')",
|
|
61
|
+
},
|
|
62
|
+
"question": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "Natural language question, task, or discussion prompt about the codebase",
|
|
65
|
+
},
|
|
66
|
+
"mode": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"enum": ["explore", "issues"],
|
|
69
|
+
"description": (
|
|
70
|
+
"explore (default): answer freely, describe structure, discuss architecture. "
|
|
71
|
+
"issues: Claude outputs structured GitHub issue suggestions "
|
|
72
|
+
"(title + description + labels) that can be raised on GitHub."
|
|
73
|
+
),
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
"required": ["repo", "question"],
|
|
77
|
+
},
|
|
78
|
+
},'''
|
|
79
|
+
|
|
80
|
+
if OLD_DEF not in boss:
|
|
81
|
+
print("ERROR: ask_codebase tool definition not found")
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
boss = boss.replace(OLD_DEF, NEW_DEF, 1)
|
|
84
|
+
print("Step 1 OK: tool definition updated")
|
|
85
|
+
|
|
86
|
+
# ── 2. Update handler — richer prompt + mode support ─────────────────────────
|
|
87
|
+
|
|
88
|
+
OLD_HANDLER = ''' cfg = cfg_loader.sentinel
|
|
89
|
+
env = os.environ.copy()
|
|
90
|
+
# Only inject API key when Claude Pro is NOT preferred for heavy tasks
|
|
91
|
+
if cfg.anthropic_api_key and not cfg.claude_pro_for_tasks:
|
|
92
|
+
env["ANTHROPIC_API_KEY"] = cfg.anthropic_api_key
|
|
93
|
+
|
|
94
|
+
def _ask_one(repo_name, repo_cfg) -> dict:
|
|
95
|
+
local_path = Path(repo_cfg.local_path)
|
|
96
|
+
if not local_path.exists():
|
|
97
|
+
return {"repo": repo_name, "error": f"not cloned yet at {local_path}"}
|
|
98
|
+
prompt = (
|
|
99
|
+
f"You are a code analyst. Answer the following question about the codebase at: {local_path}\\n\\n"
|
|
100
|
+
f"Question: {question}\\n\\n"
|
|
101
|
+
f"Use whatever tools you need to answer accurately. Be concise and direct. Plain text only."
|
|
102
|
+
)
|
|
103
|
+
try:
|
|
104
|
+
r = subprocess.run(
|
|
105
|
+
([cfg.claude_code_bin, "--dangerously-skip-permissions", "--print", prompt]
|
|
106
|
+
if os.getuid() != 0 else
|
|
107
|
+
[cfg.claude_code_bin, "--print", prompt]),
|
|
108
|
+
capture_output=True, text=True, timeout=180, env=env,
|
|
109
|
+
cwd=str(local_path), stdin=subprocess.DEVNULL,
|
|
110
|
+
)
|
|
111
|
+
output = (r.stdout or "").strip()
|
|
112
|
+
logger.info("Boss ask_codebase %s rc=%d len=%d", repo_name, r.returncode, len(output))
|
|
113
|
+
if r.returncode != 0 and not output:
|
|
114
|
+
raw_err = (r.stderr or "")
|
|
115
|
+
alert_if_rate_limited(
|
|
116
|
+
cfg.slack_bot_token, cfg.slack_channel,
|
|
117
|
+
f"ask_codebase/{repo_name}", raw_err,
|
|
118
|
+
)
|
|
119
|
+
return {"repo": repo_name, "error": f"claude --print failed (rc={r.returncode}): {raw_err[:200]}"}
|
|
120
|
+
return {"repo": repo_name, "answer": output[:3000]}
|
|
121
|
+
except subprocess.TimeoutExpired:
|
|
122
|
+
return {"repo": repo_name, "error": "timed out after 180s"}
|
|
123
|
+
except Exception as e:
|
|
124
|
+
return {"repo": repo_name, "error": str(e)}
|
|
125
|
+
|
|
126
|
+
if len(matched) == 1:
|
|
127
|
+
result = _ask_one(*matched[0])
|
|
128
|
+
# Unwrap single-repo result for cleaner response
|
|
129
|
+
return json.dumps(result)
|
|
130
|
+
|
|
131
|
+
# Multiple repos — query each and combine
|
|
132
|
+
results = [_ask_one(rn, r) for rn, r in matched]
|
|
133
|
+
return json.dumps({"project": target, "repos_queried": len(results), "results": results})'''
|
|
134
|
+
|
|
135
|
+
NEW_HANDLER = ''' mode = inputs.get("mode", "explore")
|
|
136
|
+
|
|
137
|
+
cfg = cfg_loader.sentinel
|
|
138
|
+
env = os.environ.copy()
|
|
139
|
+
# Only inject API key when Claude Pro is NOT preferred for heavy tasks
|
|
140
|
+
if cfg.anthropic_api_key and not cfg.claude_pro_for_tasks:
|
|
141
|
+
env["ANTHROPIC_API_KEY"] = cfg.anthropic_api_key
|
|
142
|
+
|
|
143
|
+
# Build project context block (all repos this Sentinel instance manages)
|
|
144
|
+
_project_name = _read_project_name(Path("."))
|
|
145
|
+
_all_repos_lines = []
|
|
146
|
+
for _rn, _rc in cfg_loader.repos.items():
|
|
147
|
+
_url = getattr(_rc, "repo_url", "") or ""
|
|
148
|
+
_path = getattr(_rc, "local_path", "") or ""
|
|
149
|
+
_prefixes = getattr(_rc, "package_prefixes", "") or ""
|
|
150
|
+
_all_repos_lines.append(
|
|
151
|
+
f" - {_rn}: path={_path}"
|
|
152
|
+
+ (f", url={_url}" if _url else "")
|
|
153
|
+
+ (f", packages={_prefixes}" if _prefixes else "")
|
|
154
|
+
)
|
|
155
|
+
_project_ctx = (
|
|
156
|
+
f"Project: {_project_name}\\n"
|
|
157
|
+
f"Repos managed by this Sentinel instance:\\n"
|
|
158
|
+
+ "\\n".join(_all_repos_lines)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _ask_one(repo_name, repo_cfg) -> dict:
|
|
162
|
+
local_path = Path(repo_cfg.local_path)
|
|
163
|
+
if not local_path.exists():
|
|
164
|
+
return {"repo": repo_name, "error": f"not cloned yet at {local_path}"}
|
|
165
|
+
|
|
166
|
+
if mode == "issues":
|
|
167
|
+
_mode_instruction = (
|
|
168
|
+
"Output a structured list of GitHub issues to raise for this codebase.\\n"
|
|
169
|
+
"For each issue include:\\n"
|
|
170
|
+
" TITLE: <concise issue title>\\n"
|
|
171
|
+
" LABELS: <bug|enhancement|tech-debt|security|performance>\\n"
|
|
172
|
+
" DESCRIPTION: <2-4 sentences: what, why, suggested approach>\\n"
|
|
173
|
+
"---\\n"
|
|
174
|
+
"Focus on: bugs, missing error handling, performance bottlenecks, "
|
|
175
|
+
"security gaps, missing tests, tech-debt, and useful new features.\\n"
|
|
176
|
+
"Output plain text only — no markdown headers."
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
_mode_instruction = (
|
|
180
|
+
"Explore the codebase freely — read files, search for patterns, examine structure.\\n"
|
|
181
|
+
"Answer thoroughly. You may discuss architecture, suggest extensions, "
|
|
182
|
+
"describe design patterns, or analyse any aspect of the code.\\n"
|
|
183
|
+
"Plain text only. Be concise but complete."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
prompt = (
|
|
187
|
+
f"{_project_ctx}\\n\\n"
|
|
188
|
+
f"You are now analysing: {repo_name} at {local_path}\\n\\n"
|
|
189
|
+
f"{_mode_instruction}\\n\\n"
|
|
190
|
+
f"Question / Task: {question}"
|
|
191
|
+
)
|
|
192
|
+
try:
|
|
193
|
+
r = subprocess.run(
|
|
194
|
+
([cfg.claude_code_bin, "--dangerously-skip-permissions", "--print", prompt]
|
|
195
|
+
if os.getuid() != 0 else
|
|
196
|
+
[cfg.claude_code_bin, "--print", prompt]),
|
|
197
|
+
capture_output=True, text=True, timeout=300, env=env,
|
|
198
|
+
cwd=str(local_path), stdin=subprocess.DEVNULL,
|
|
199
|
+
)
|
|
200
|
+
output = (r.stdout or "").strip()
|
|
201
|
+
logger.info("Boss ask_codebase %s mode=%s rc=%d len=%d", repo_name, mode, r.returncode, len(output))
|
|
202
|
+
if r.returncode != 0 and not output:
|
|
203
|
+
raw_err = (r.stderr or "")
|
|
204
|
+
alert_if_rate_limited(
|
|
205
|
+
cfg.slack_bot_token, cfg.slack_channel,
|
|
206
|
+
f"ask_codebase/{repo_name}", raw_err,
|
|
207
|
+
)
|
|
208
|
+
return {"repo": repo_name, "error": f"claude --print failed (rc={r.returncode}): {raw_err[:200]}"}
|
|
209
|
+
return {"repo": repo_name, "answer": output[:4000]}
|
|
210
|
+
except subprocess.TimeoutExpired:
|
|
211
|
+
return {"repo": repo_name, "error": "timed out after 300s"}
|
|
212
|
+
except Exception as e:
|
|
213
|
+
return {"repo": repo_name, "error": str(e)}
|
|
214
|
+
|
|
215
|
+
if len(matched) == 1:
|
|
216
|
+
result = _ask_one(*matched[0])
|
|
217
|
+
# Unwrap single-repo result for cleaner response
|
|
218
|
+
return json.dumps(result)
|
|
219
|
+
|
|
220
|
+
# Multiple repos — query each and combine
|
|
221
|
+
results = [_ask_one(rn, r) for rn, r in matched]
|
|
222
|
+
return json.dumps({"project": target, "repos_queried": len(results), "results": results})'''
|
|
223
|
+
|
|
224
|
+
if OLD_HANDLER not in boss:
|
|
225
|
+
print("ERROR: ask_codebase handler not found")
|
|
226
|
+
# Show context to help debug
|
|
227
|
+
idx = boss.find("def _ask_one")
|
|
228
|
+
if idx >= 0:
|
|
229
|
+
print(repr(boss[idx-200:idx+100]))
|
|
230
|
+
sys.exit(1)
|
|
231
|
+
boss = boss.replace(OLD_HANDLER, NEW_HANDLER, 1)
|
|
232
|
+
print("Step 2 OK: handler updated with richer prompt + mode support")
|
|
233
|
+
|
|
234
|
+
with open(BOSS, 'w', encoding='utf-8') as f:
|
|
235
|
+
f.write(boss)
|
|
236
|
+
print("Written OK")
|
|
237
|
+
|
|
238
|
+
# ── Syntax check ──────────────────────────────────────────────────────────────
|
|
239
|
+
with open(BOSS, 'r', encoding='utf-8') as f:
|
|
240
|
+
src = f.read()
|
|
241
|
+
try:
|
|
242
|
+
ast.parse(src)
|
|
243
|
+
print("Syntax OK")
|
|
244
|
+
except SyntaxError as e:
|
|
245
|
+
lines = src.splitlines()
|
|
246
|
+
print(f"SyntaxError at line {e.lineno}: {e.msg}")
|
|
247
|
+
for i in range(max(0, e.lineno-5), min(len(lines), e.lineno+3)):
|
|
248
|
+
print(f" {i+1}: {lines[i]}")
|
|
249
|
+
sys.exit(1)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fix ask_codebase: add stdin=DEVNULL so claude --print doesn't hang waiting for stdin."""
|
|
3
|
+
import ast, sys
|
|
4
|
+
|
|
5
|
+
BOSS = '/home/sentinel/sentinel/code/sentinel/sentinel_boss.py'
|
|
6
|
+
|
|
7
|
+
with open(BOSS, 'r', encoding='utf-8') as f:
|
|
8
|
+
content = f.read()
|
|
9
|
+
|
|
10
|
+
OLD = (
|
|
11
|
+
' r = subprocess.run(\n'
|
|
12
|
+
' ([cfg.claude_code_bin, "--dangerously-skip-permissions", "--print", prompt]\n'
|
|
13
|
+
' if os.getuid() != 0 else\n'
|
|
14
|
+
' [cfg.claude_code_bin, "--print", prompt]),\n'
|
|
15
|
+
' capture_output=True, text=True, timeout=180, env=env,\n'
|
|
16
|
+
' cwd=str(local_path),\n'
|
|
17
|
+
' )'
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
NEW = (
|
|
21
|
+
' r = subprocess.run(\n'
|
|
22
|
+
' ([cfg.claude_code_bin, "--dangerously-skip-permissions", "--print", prompt]\n'
|
|
23
|
+
' if os.getuid() != 0 else\n'
|
|
24
|
+
' [cfg.claude_code_bin, "--print", prompt]),\n'
|
|
25
|
+
' capture_output=True, text=True, timeout=180, env=env,\n'
|
|
26
|
+
' cwd=str(local_path), stdin=subprocess.DEVNULL,\n'
|
|
27
|
+
' )'
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if OLD not in content:
|
|
31
|
+
print("ERROR: target block not found")
|
|
32
|
+
# Show context
|
|
33
|
+
idx = content.find('capture_output=True, text=True, timeout=180')
|
|
34
|
+
if idx >= 0:
|
|
35
|
+
print(repr(content[idx-200:idx+100]))
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
content = content.replace(OLD, NEW, 1)
|
|
39
|
+
|
|
40
|
+
with open(BOSS, 'w', encoding='utf-8') as f:
|
|
41
|
+
f.write(content)
|
|
42
|
+
print("Fixed: stdin=DEVNULL added to ask_codebase subprocess call")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
ast.parse(content)
|
|
46
|
+
print("Syntax OK")
|
|
47
|
+
except SyntaxError as e:
|
|
48
|
+
print(f"SyntaxError at line {e.lineno}: {e.msg}")
|
|
49
|
+
sys.exit(1)
|