@misterhuydo/sentinel 1.4.68 → 1.4.70
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 +3 -3
- package/lib/.cairn/views/fb78ac_upgrade.js +16 -1
- package/lib/add.js +30 -16
- 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
package/.cairn/.hint-lock
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-
|
|
1
|
+
2026-03-26T18:21:14.192Z
|
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-26T18:22:50.768Z",
|
|
3
|
+
"checkpoint_at": "2026-03-26T18:22:50.770Z",
|
|
4
4
|
"active_files": [],
|
|
5
5
|
"notes": [],
|
|
6
6
|
"mtime_snapshot": {}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"J:\\Projects\\Sentinel\\cli\\lib\\
|
|
3
|
-
"tempPath": "J:\\Projects\\Sentinel\\cli\\lib\\.cairn\\views\\
|
|
2
|
+
"J:\\Projects\\Sentinel\\cli\\lib\\upgrade.js": {
|
|
3
|
+
"tempPath": "J:\\Projects\\Sentinel\\cli\\lib\\.cairn\\views\\fb78ac_upgrade.js",
|
|
4
4
|
"state": "edit-ready",
|
|
5
|
-
"minifiedAt":
|
|
5
|
+
"minifiedAt": 1774409075884.3267,
|
|
6
6
|
"readCount": 1
|
|
7
7
|
}
|
|
8
8
|
}
|
|
@@ -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/add.js
CHANGED
|
@@ -277,35 +277,49 @@ async function addFromGit(gitUrl, workspace) {
|
|
|
277
277
|
validate: v => VALID_NAME.test(v) || 'Use letters, numbers, hyphens only',
|
|
278
278
|
}], { onCancel: () => process.exit(0) });
|
|
279
279
|
|
|
280
|
-
// ── 1.
|
|
280
|
+
// ── 1. Set up SSH access to the primary (config) repo ─────────────────────
|
|
281
281
|
step(`[1/3] Setting up SSH access to ${repoSlug}`);
|
|
282
282
|
ensureKnownHosts();
|
|
283
|
-
const { keyFile } = generateDeployKey(repoSlug);
|
|
284
|
-
printDeployKeyInstructions(orgRepo, keyFile);
|
|
285
283
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
284
|
+
// Check if the repo is already accessible via the existing SSH key/agent.
|
|
285
|
+
// This is the case when GIT_ACCESS=ssh_user_key is configured — no deploy key needed.
|
|
286
|
+
const existingAccess = validateAccess(gitUrl, null);
|
|
287
|
+
let keyFile = null;
|
|
290
288
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
289
|
+
if (existingAccess.ok) {
|
|
290
|
+
ok(`${repoSlug}: reachable via existing SSH key — skipping deploy key generation`);
|
|
291
|
+
} else {
|
|
292
|
+
const { keyFile: generatedKey } = generateDeployKey(repoSlug);
|
|
293
|
+
keyFile = generatedKey;
|
|
294
|
+
printDeployKeyInstructions(orgRepo, keyFile);
|
|
295
|
+
|
|
296
|
+
await prompts({
|
|
297
|
+
type: 'text', name: '_', format: () => '',
|
|
298
|
+
message: chalk.bold(`Press Enter once you've added the deploy key to GitHub…`),
|
|
299
|
+
}, { onCancel: () => process.exit(0) });
|
|
300
|
+
|
|
301
|
+
// Validate primary repo with deploy key
|
|
302
|
+
const primary = validateAccess(gitUrl, keyFile);
|
|
303
|
+
if (!primary.ok) {
|
|
304
|
+
console.error(chalk.red(' ✖ Cannot reach ' + gitUrl));
|
|
305
|
+
if (primary.stderr) console.error(chalk.red(' ' + primary.stderr));
|
|
306
|
+
console.error(chalk.yellow(' Check the deploy key has write access, then re-run.'));
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
ok(`${repoSlug}: reachable`);
|
|
298
310
|
}
|
|
299
|
-
ok(`${repoSlug}: reachable`);
|
|
300
311
|
|
|
301
312
|
// ── 2. Clone primary repo and discover additional repos ────────────────────
|
|
302
313
|
const projectDir = path.join(workspace, name);
|
|
303
314
|
step(`[2/3] Scanning repo-configs in ${repoSlug}…`);
|
|
304
315
|
|
|
305
316
|
if (!fs.existsSync(projectDir)) {
|
|
317
|
+
const cloneEnv = keyFile
|
|
318
|
+
? gitEnv({ GIT_SSH_COMMAND: `ssh -i ${keyFile} -o StrictHostKeyChecking=no -o BatchMode=yes` })
|
|
319
|
+
: gitEnv({});
|
|
306
320
|
spawnSync(gitBin(), ['clone', '--depth', '1', gitUrl, projectDir], {
|
|
307
321
|
stdio: 'inherit',
|
|
308
|
-
env:
|
|
322
|
+
env: cloneEnv,
|
|
309
323
|
});
|
|
310
324
|
}
|
|
311
325
|
|
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)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fix the broken slack_alert block in chain_release handler."""
|
|
3
|
+
import ast, py_compile, tempfile, os, sys
|
|
4
|
+
|
|
5
|
+
TARGET = '/home/sentinel/sentinel/code/sentinel/sentinel_boss.py'
|
|
6
|
+
|
|
7
|
+
with open(TARGET, 'r', encoding='utf-8') as f:
|
|
8
|
+
lines = f.readlines()
|
|
9
|
+
|
|
10
|
+
# Find the broken block: look for doubled slack_alert lines near "Chain release"
|
|
11
|
+
# Strategy: find line with "all_ok = all(r.get" and replace everything up to
|
|
12
|
+
# "logger.info("Boss chain_release"
|
|
13
|
+
|
|
14
|
+
start_idx = None
|
|
15
|
+
end_idx = None
|
|
16
|
+
for i, line in enumerate(lines):
|
|
17
|
+
if 'all_ok = all(r.get("status") in ("released", "no_cicd")' in line:
|
|
18
|
+
start_idx = i
|
|
19
|
+
if start_idx and 'logger.info("Boss chain_release' in line:
|
|
20
|
+
end_idx = i
|
|
21
|
+
break
|
|
22
|
+
|
|
23
|
+
if start_idx is None or end_idx is None:
|
|
24
|
+
print(f"Could not find block: start={start_idx}, end={end_idx}")
|
|
25
|
+
# Print context
|
|
26
|
+
for i, line in enumerate(lines):
|
|
27
|
+
if 'chain_release' in line or 'all_ok' in line:
|
|
28
|
+
print(f"{i+1}: {line}", end='')
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
print(f"Replacing lines {start_idx+1}..{end_idx} (inclusive of logger line)")
|
|
32
|
+
|
|
33
|
+
# Build the replacement lines (8 spaces indent = inside if name == "chain_release" block)
|
|
34
|
+
replacement = '''\
|
|
35
|
+
all_ok = all(r.get("status") in ("released", "no_cicd") for r in results)
|
|
36
|
+
final = "completed" if all_ok else "partial" if results else "failed"
|
|
37
|
+
_icon = ":white_check_mark:" if all_ok else ":warning:"
|
|
38
|
+
_steps = ", ".join(
|
|
39
|
+
"`" + r["repo"] + "` " + ("\u2713" if r.get("status") in ("released", "no_cicd") else "\u2717")
|
|
40
|
+
for r in results
|
|
41
|
+
)
|
|
42
|
+
slack_alert(
|
|
43
|
+
cfg.slack_bot_token, cfg.slack_channel,
|
|
44
|
+
f"{_icon}: *Chain release {final}* \u2014 {_steps}",
|
|
45
|
+
)
|
|
46
|
+
'''
|
|
47
|
+
|
|
48
|
+
# Replace lines start_idx..end_idx-1 (keep the logger line)
|
|
49
|
+
new_lines = lines[:start_idx] + [replacement] + lines[end_idx:]
|
|
50
|
+
|
|
51
|
+
with open(TARGET, 'w', encoding='utf-8') as f:
|
|
52
|
+
f.writelines(new_lines)
|
|
53
|
+
|
|
54
|
+
print("Replacement done.")
|
|
55
|
+
|
|
56
|
+
# Syntax check
|
|
57
|
+
with open(TARGET, 'r') as f:
|
|
58
|
+
src = f.read()
|
|
59
|
+
try:
|
|
60
|
+
ast.parse(src)
|
|
61
|
+
print("Syntax OK")
|
|
62
|
+
except SyntaxError as e:
|
|
63
|
+
print(f"SyntaxError line {e.lineno}: {e.msg}")
|
|
64
|
+
ls = src.splitlines()
|
|
65
|
+
for i in range(max(0, e.lineno-5), min(len(ls), e.lineno+3)):
|
|
66
|
+
print(f"{i+1}: {ls[i]}")
|
|
67
|
+
sys.exit(1)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fix Python 3.9 f-string backslash issue in chain_release handler."""
|
|
3
|
+
|
|
4
|
+
TARGET = '/home/sentinel/sentinel/code/sentinel/sentinel_boss.py'
|
|
5
|
+
|
|
6
|
+
with open(TARGET, 'r', encoding='utf-8') as f:
|
|
7
|
+
content = f.read()
|
|
8
|
+
|
|
9
|
+
# Replace the problematic f-string block
|
|
10
|
+
old = (
|
|
11
|
+
" f\":{'white_check_mark' if all_ok else 'warning'}: *Chain release {final}* \\u2014 \"\n"
|
|
12
|
+
" + \", \".join(\n"
|
|
13
|
+
" f\"`{r['repo']}` {'\\u2713' if r.get('status') in ('released', 'no_cicd') else '\\u2717'}\"\n"
|
|
14
|
+
" for r in results\n"
|
|
15
|
+
" ),"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
new = (
|
|
19
|
+
" (\":white_check_mark:\" if all_ok else \":warning:\") + f\": *Chain release {final}* \\u2014 \"\n"
|
|
20
|
+
" + \", \".join(\n"
|
|
21
|
+
" \"`\" + r[\"repo\"] + \"` \" + (\"\\u2713\" if r.get(\"status\") in (\"released\", \"no_cicd\") else \"\\u2717\")\n"
|
|
22
|
+
" for r in results\n"
|
|
23
|
+
" ),"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if old not in content:
|
|
27
|
+
# Try finding it
|
|
28
|
+
idx = content.find("white_check_mark' if all_ok")
|
|
29
|
+
if idx >= 0:
|
|
30
|
+
print("Context around match point:")
|
|
31
|
+
print(repr(content[idx-10:idx+300]))
|
|
32
|
+
else:
|
|
33
|
+
print("Pattern not found at all")
|
|
34
|
+
import sys; sys.exit(1)
|
|
35
|
+
|
|
36
|
+
content = content.replace(old, new, 1)
|
|
37
|
+
|
|
38
|
+
with open(TARGET, 'w', encoding='utf-8') as f:
|
|
39
|
+
f.write(content)
|
|
40
|
+
|
|
41
|
+
print("Fixed f-string.")
|
|
42
|
+
|
|
43
|
+
import ast, py_compile, tempfile, os
|
|
44
|
+
with tempfile.NamedTemporaryFile(suffix='.py', delete=False, mode='w') as tmp:
|
|
45
|
+
tmp.write(content)
|
|
46
|
+
tmp_name = tmp.name
|
|
47
|
+
try:
|
|
48
|
+
py_compile.compile(tmp_name, doraise=True)
|
|
49
|
+
print("Syntax OK")
|
|
50
|
+
finally:
|
|
51
|
+
os.unlink(tmp_name)
|