@misterhuydo/sentinel 1.0.98 → 1.1.0
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/bin/sentinel.js +87 -86
- package/lib/test.js +208 -0
- package/package.json +1 -1
- package/python/sentinel/sentinel_boss.py +7 -2
- package/python/sentinel/slack_bot.py +24 -6
- package/.cairn/views/a348d8_sentinel.js +0 -58
package/.cairn/.hint-lock
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-
|
|
1
|
+
2026-03-23T08:08:15.205Z
|
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-23T08:15:23.322Z",
|
|
3
|
+
"checkpoint_at": "2026-03-23T08:15:23.323Z",
|
|
4
4
|
"active_files": [],
|
|
5
5
|
"notes": [],
|
|
6
6
|
"mtime_snapshot": {}
|
package/bin/sentinel.js
CHANGED
|
@@ -1,86 +1,87 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const chalk = require('chalk');
|
|
5
|
-
const [,, command = 'help', ...args] = process.argv;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
${chalk.cyan('
|
|
20
|
-
${chalk.cyan('
|
|
21
|
-
${chalk.cyan('
|
|
22
|
-
${chalk.cyan('
|
|
23
|
-
${chalk.cyan('
|
|
24
|
-
${chalk.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.log(chalk.yellow(' ⚠'), 'upgrade module failed to load (' + loadErr.message + ')');
|
|
45
|
-
console.log(chalk.cyan(' →'), 'Running bare npm install to self-heal...');
|
|
46
|
-
const { spawnSync } = require('child_process');
|
|
47
|
-
const r = spawnSync('npm', ['install', '-g', '@misterhuydo/sentinel@latest'],
|
|
48
|
-
{ stdio: 'inherit' });
|
|
49
|
-
if (r.status === 0) {
|
|
50
|
-
console.log(chalk.green(' ✔'), 'Self-healed — run `sentinel upgrade` again to finish');
|
|
51
|
-
} else {
|
|
52
|
-
console.error(chalk.red(' ✖'), 'npm install failed — try: npm install -g @misterhuydo/sentinel');
|
|
53
|
-
}
|
|
54
|
-
process.exit(r.status || 0);
|
|
55
|
-
}
|
|
56
|
-
await upgradeCmd();
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
case 'help':
|
|
60
|
-
default:
|
|
61
|
-
printUsage();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function printUsage() {
|
|
66
|
-
const { version } = require('../package.json');
|
|
67
|
-
console.log(`${chalk.bold('sentinel')} v${version} — Autonomous DevOps Agent
|
|
68
|
-
|
|
69
|
-
${chalk.bold('Usage:')}
|
|
70
|
-
sentinel init Interactive setup — install everything and create workspace
|
|
71
|
-
sentinel add <name> Add a blank project (fill config manually)
|
|
72
|
-
sentinel add <git-url> Add a project pre-configured for a GitHub repo
|
|
73
|
-
sentinel add <project.json> Add a project from a local JSON config file
|
|
74
|
-
sentinel add <https://host/cfg.json> Add a project from a remote JSON config URL
|
|
75
|
-
sentinel
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
--
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const [,, command = 'help', ...args] = process.argv;
|
|
6
|
+
|
|
7
|
+
if (command === '--version' || command === '-v') {
|
|
8
|
+
const { version } = require('../package.json');
|
|
9
|
+
console.log(version);
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
if (command === '--help' || command === '-h') {
|
|
13
|
+
printUsage();
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const BANNER = `
|
|
18
|
+
${chalk.cyan('███████╗███████╗███╗ ██╗████████╗██╗███╗ ██╗███████╗██╗')}
|
|
19
|
+
${chalk.cyan('██╔════╝██╔════╝████╗ ██║╚══██╔══╝██║████╗ ██║██╔════╝██║')}
|
|
20
|
+
${chalk.cyan('███████╗█████╗ ██╔██╗ ██║ ██║ ██║██╔██╗ ██║█████╗ ██║')}
|
|
21
|
+
${chalk.cyan('╚════██║██╔══╝ ██║╚██╗██║ ██║ ██║██║╚██╗██║██╔══╝ ██║')}
|
|
22
|
+
${chalk.cyan('███████║███████╗██║ ╚████║ ██║ ██║██║ ╚████║███████╗███████╗')}
|
|
23
|
+
${chalk.cyan('╚══════╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝')}
|
|
24
|
+
${chalk.gray(' Autonomous DevOps Agent')}
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
async function main() {
|
|
28
|
+
console.log(BANNER);
|
|
29
|
+
switch (command) {
|
|
30
|
+
case 'init':
|
|
31
|
+
await require('../lib/init')();
|
|
32
|
+
break;
|
|
33
|
+
case 'add':
|
|
34
|
+
await require('../lib/add')(args[0]);
|
|
35
|
+
break;
|
|
36
|
+
case 'test':
|
|
37
|
+
await require('../lib/test')(args[0]);
|
|
38
|
+
break;
|
|
39
|
+
case 'upgrade': {
|
|
40
|
+
let upgradeCmd;
|
|
41
|
+
try {
|
|
42
|
+
upgradeCmd = require('../lib/upgrade');
|
|
43
|
+
} catch (loadErr) {
|
|
44
|
+
console.log(chalk.yellow(' ⚠'), 'upgrade module failed to load (' + loadErr.message + ')');
|
|
45
|
+
console.log(chalk.cyan(' →'), 'Running bare npm install to self-heal...');
|
|
46
|
+
const { spawnSync } = require('child_process');
|
|
47
|
+
const r = spawnSync('npm', ['install', '-g', '@misterhuydo/sentinel@latest'],
|
|
48
|
+
{ stdio: 'inherit' });
|
|
49
|
+
if (r.status === 0) {
|
|
50
|
+
console.log(chalk.green(' ✔'), 'Self-healed — run `sentinel upgrade` again to finish');
|
|
51
|
+
} else {
|
|
52
|
+
console.error(chalk.red(' ✖'), 'npm install failed — try: npm install -g @misterhuydo/sentinel');
|
|
53
|
+
}
|
|
54
|
+
process.exit(r.status || 0);
|
|
55
|
+
}
|
|
56
|
+
await upgradeCmd();
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case 'help':
|
|
60
|
+
default:
|
|
61
|
+
printUsage();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function printUsage() {
|
|
66
|
+
const { version } = require('../package.json');
|
|
67
|
+
console.log(`${chalk.bold('sentinel')} v${version} — Autonomous DevOps Agent
|
|
68
|
+
|
|
69
|
+
${chalk.bold('Usage:')}
|
|
70
|
+
sentinel init Interactive setup — install everything and create workspace
|
|
71
|
+
sentinel add <name> Add a blank project (fill config manually)
|
|
72
|
+
sentinel add <git-url> Add a project pre-configured for a GitHub repo
|
|
73
|
+
sentinel add <project.json> Add a project from a local JSON config file
|
|
74
|
+
sentinel add <https://host/cfg.json> Add a project from a remote JSON config URL
|
|
75
|
+
sentinel test [project] Validate installation and config before going live
|
|
76
|
+
sentinel upgrade Pull latest version and hot-deploy Python source
|
|
77
|
+
|
|
78
|
+
${chalk.bold('Options:')}
|
|
79
|
+
--version, -v Print version
|
|
80
|
+
--help, -h Print this help
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
main().catch(err => {
|
|
85
|
+
console.error(chalk.red('Error:'), err.message);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
});
|
package/lib/test.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { execSync, spawnSync } = require('child_process');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
const ok = msg => console.log(chalk.green(' ✔'), msg);
|
|
9
|
+
const fail = msg => console.log(chalk.red(' ✖'), msg);
|
|
10
|
+
const warn = msg => console.log(chalk.yellow(' ⚠'), msg);
|
|
11
|
+
const info = msg => console.log(chalk.cyan(' →'), msg);
|
|
12
|
+
|
|
13
|
+
module.exports = async function testInstall(projectName) {
|
|
14
|
+
const defaultWorkspace = path.join(os.homedir(), 'sentinel');
|
|
15
|
+
const projectDir = projectName
|
|
16
|
+
? path.join(defaultWorkspace, projectName)
|
|
17
|
+
: _findActiveProject(defaultWorkspace);
|
|
18
|
+
|
|
19
|
+
console.log(chalk.bold('\nSentinel — Installation Check\n'));
|
|
20
|
+
|
|
21
|
+
let passed = 0;
|
|
22
|
+
let failed = 0;
|
|
23
|
+
|
|
24
|
+
// ── 1. Tools ──────────────────────────────────────────────────────────────
|
|
25
|
+
info('Checking required tools...');
|
|
26
|
+
|
|
27
|
+
const tools = [
|
|
28
|
+
{ cmd: 'python3 --version', label: 'Python 3' },
|
|
29
|
+
{ cmd: 'node --version', label: 'Node.js' },
|
|
30
|
+
{ cmd: 'git --version', label: 'git' },
|
|
31
|
+
];
|
|
32
|
+
for (const { cmd, label } of tools) {
|
|
33
|
+
try {
|
|
34
|
+
const out = execSync(cmd, { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
|
|
35
|
+
ok(`${label}: ${out}`);
|
|
36
|
+
passed++;
|
|
37
|
+
} catch {
|
|
38
|
+
fail(`${label} not found`);
|
|
39
|
+
failed++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── 2. npm globals ────────────────────────────────────────────────────────
|
|
44
|
+
info('Checking npm globals...');
|
|
45
|
+
|
|
46
|
+
const npms = [
|
|
47
|
+
{ cmd: 'sentinel --version', label: '@misterhuydo/sentinel' },
|
|
48
|
+
{ cmd: 'cairn --version', label: '@misterhuydo/cairn-mcp' },
|
|
49
|
+
{ cmd: 'claude --version', label: '@anthropic-ai/claude-code'},
|
|
50
|
+
];
|
|
51
|
+
for (const { cmd, label } of npms) {
|
|
52
|
+
try {
|
|
53
|
+
const out = execSync(cmd, { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
|
|
54
|
+
ok(`${label}: ${out}`);
|
|
55
|
+
passed++;
|
|
56
|
+
} catch {
|
|
57
|
+
fail(`${label} not installed — run: npm install -g ${label}`);
|
|
58
|
+
failed++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── 3. Claude auth ────────────────────────────────────────────────────────
|
|
63
|
+
info('Checking Claude auth...');
|
|
64
|
+
const apiKey = _readSentinelProp(projectDir, 'ANTHROPIC_API_KEY')
|
|
65
|
+
|| process.env.ANTHROPIC_API_KEY || '';
|
|
66
|
+
const claudeProTasks = (_readSentinelProp(projectDir, 'CLAUDE_PRO_FOR_TASKS') || 'true').toLowerCase() !== 'false';
|
|
67
|
+
|
|
68
|
+
if (apiKey) {
|
|
69
|
+
ok(`ANTHROPIC_API_KEY configured (${apiKey.slice(0, 12)}...)`);
|
|
70
|
+
passed++;
|
|
71
|
+
} else {
|
|
72
|
+
warn('ANTHROPIC_API_KEY not set — Sentinel Boss will use CLI fallback only');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const r = spawnSync('claude', ['--version'], { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
|
|
77
|
+
if (r.status === 0) {
|
|
78
|
+
ok(`claude CLI available: ${(r.stdout || '').trim()}`);
|
|
79
|
+
if (claudeProTasks) {
|
|
80
|
+
ok('CLAUDE_PRO_FOR_TASKS=true — fix_engine will use Claude Pro subscription');
|
|
81
|
+
}
|
|
82
|
+
passed++;
|
|
83
|
+
} else {
|
|
84
|
+
warn('claude CLI found but --version failed');
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
fail('claude CLI not found');
|
|
88
|
+
failed++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!apiKey) {
|
|
92
|
+
// Try a quick login check
|
|
93
|
+
try {
|
|
94
|
+
const r = spawnSync('claude', ['--print', 'ping', '--no-interactive'],
|
|
95
|
+
{ encoding: 'utf8', timeout: 10000, stdio: ['pipe','pipe','pipe'] });
|
|
96
|
+
if (r.status === 0) {
|
|
97
|
+
ok('claude OAuth session active');
|
|
98
|
+
passed++;
|
|
99
|
+
} else {
|
|
100
|
+
warn('claude OAuth session may be expired — run: claude login');
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
warn('Could not verify claude OAuth session');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── 4. Project config ─────────────────────────────────────────────────────
|
|
108
|
+
if (projectDir && fs.existsSync(projectDir)) {
|
|
109
|
+
info(`Checking project config at ${projectDir}...`);
|
|
110
|
+
|
|
111
|
+
const sentinelProps = path.join(projectDir, 'config', 'sentinel.properties');
|
|
112
|
+
if (fs.existsSync(sentinelProps)) {
|
|
113
|
+
ok('sentinel.properties found');
|
|
114
|
+
passed++;
|
|
115
|
+
} else {
|
|
116
|
+
fail(`sentinel.properties missing at ${sentinelProps}`);
|
|
117
|
+
failed++;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const logConfigs = path.join(projectDir, 'config', 'log-configs');
|
|
121
|
+
const repoConfigs = path.join(projectDir, 'config', 'repo-configs');
|
|
122
|
+
|
|
123
|
+
const logCount = fs.existsSync(logConfigs) ? fs.readdirSync(logConfigs).filter(f => f.endsWith('.properties') && !f.startsWith('_')).length : 0;
|
|
124
|
+
const repoCount = fs.existsSync(repoConfigs) ? fs.readdirSync(repoConfigs).filter(f => f.endsWith('.properties') && !f.startsWith('_')).length : 0;
|
|
125
|
+
|
|
126
|
+
if (logCount > 0) { ok(`${logCount} log-config(s) found`); passed++; }
|
|
127
|
+
else { warn('No log-configs found — add at least one in config/log-configs/'); }
|
|
128
|
+
|
|
129
|
+
if (repoCount > 0) { ok(`${repoCount} repo-config(s) found`); passed++; }
|
|
130
|
+
else { warn('No repo-configs found — add at least one in config/repo-configs/'); }
|
|
131
|
+
|
|
132
|
+
// GitHub token
|
|
133
|
+
const ghToken = _readSentinelProp(projectDir, 'GITHUB_TOKEN') || '';
|
|
134
|
+
if (ghToken) { ok('GITHUB_TOKEN configured'); passed++; }
|
|
135
|
+
else { warn('GITHUB_TOKEN not set — cannot open PRs'); }
|
|
136
|
+
|
|
137
|
+
// Slack
|
|
138
|
+
const slackBot = _readSentinelProp(projectDir, 'SLACK_BOT_TOKEN') || '';
|
|
139
|
+
const slackApp = _readSentinelProp(projectDir, 'SLACK_APP_TOKEN') || '';
|
|
140
|
+
if (slackBot && slackApp) { ok('Slack tokens configured (Boss enabled)'); passed++; }
|
|
141
|
+
else { warn('Slack tokens not set — Boss disabled'); }
|
|
142
|
+
|
|
143
|
+
} else if (projectName) {
|
|
144
|
+
fail(`Project '${projectName}' not found at ${projectDir}`);
|
|
145
|
+
failed++;
|
|
146
|
+
} else {
|
|
147
|
+
warn('No project specified — skipping project config checks');
|
|
148
|
+
warn('Run: sentinel test <project-name>');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── 5. Python deps ────────────────────────────────────────────────────────
|
|
152
|
+
info('Checking Python dependencies...');
|
|
153
|
+
const codeDir = path.join(defaultWorkspace, 'code');
|
|
154
|
+
const reqFile = path.join(codeDir, 'requirements.txt');
|
|
155
|
+
if (fs.existsSync(reqFile)) {
|
|
156
|
+
try {
|
|
157
|
+
execSync('python3 -c "import paramiko, schedule, requests, jinja2"',
|
|
158
|
+
{ encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
|
|
159
|
+
ok('Python dependencies installed');
|
|
160
|
+
passed++;
|
|
161
|
+
} catch {
|
|
162
|
+
fail('Python dependencies missing — run: pip install -r requirements.txt');
|
|
163
|
+
failed++;
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
warn('requirements.txt not found — run: sentinel init');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Summary ───────────────────────────────────────────────────────────────
|
|
170
|
+
console.log('');
|
|
171
|
+
if (failed === 0) {
|
|
172
|
+
console.log(chalk.green.bold(` ✔ All checks passed (${passed} ok)`));
|
|
173
|
+
} else {
|
|
174
|
+
console.log(chalk.yellow.bold(` ${passed} passed, ${failed} failed`));
|
|
175
|
+
console.log(chalk.gray(' Fix the issues above before starting Sentinel.'));
|
|
176
|
+
}
|
|
177
|
+
console.log('');
|
|
178
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
function _findActiveProject(workspace) {
|
|
183
|
+
if (!fs.existsSync(workspace)) return null;
|
|
184
|
+
const dirs = fs.readdirSync(workspace)
|
|
185
|
+
.map(d => path.join(workspace, d))
|
|
186
|
+
.filter(d => fs.statSync(d).isDirectory()
|
|
187
|
+
&& fs.existsSync(path.join(d, 'config', 'sentinel.properties')));
|
|
188
|
+
return dirs.length === 1 ? dirs[0] : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function _readSentinelProp(projectDir, key) {
|
|
192
|
+
if (!projectDir) return '';
|
|
193
|
+
// Try project-level first, then workspace-level
|
|
194
|
+
const candidates = [
|
|
195
|
+
path.join(projectDir, 'config', 'sentinel.properties'),
|
|
196
|
+
path.join(os.homedir(), 'sentinel', 'sentinel.properties'),
|
|
197
|
+
];
|
|
198
|
+
for (const f of candidates) {
|
|
199
|
+
if (!fs.existsSync(f)) continue;
|
|
200
|
+
for (const line of fs.readFileSync(f, 'utf8').split('\n')) {
|
|
201
|
+
const stripped = line.trim();
|
|
202
|
+
if (stripped.startsWith('#') || !stripped.includes('=')) continue;
|
|
203
|
+
const [k, ...rest] = stripped.split('=');
|
|
204
|
+
if (k.trim() === key) return rest.join('=').split('#')[0].trim();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return '';
|
|
208
|
+
}
|
package/package.json
CHANGED
|
@@ -191,6 +191,8 @@ Issue identification — before calling create_issue:
|
|
|
191
191
|
3. Before calling the tool, confirm with the user in natural language:
|
|
192
192
|
e.g. "I'll create an issue for project *1881* — here's what I have: [summary]. Look right?"
|
|
193
193
|
Wait for their confirmation before proceeding.
|
|
194
|
+
EXCEPTION: if the user's message already contains a clear project + unambiguous description,
|
|
195
|
+
skip the confirmation and create immediately — don't ask when nothing is unclear.
|
|
194
196
|
4. After creating, tell them the issue was queued and Sentinel will pick it up on the next poll.
|
|
195
197
|
|
|
196
198
|
When the engineer's request is fully handled, end your LAST message with the token: [DONE]
|
|
@@ -826,7 +828,6 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
826
828
|
issues_dir.mkdir(exist_ok=True)
|
|
827
829
|
fname = f"slack-{uuid.uuid4().hex[:8]}.txt"
|
|
828
830
|
|
|
829
|
-
from datetime import datetime, timezone as _tz
|
|
830
831
|
submitter_name = store.get_user_name(user_id) if user_id else ""
|
|
831
832
|
submitter_line = f"SUBMITTED_BY: {submitter_name} ({user_id})" if user_id else ""
|
|
832
833
|
lines = []
|
|
@@ -836,7 +837,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
836
837
|
lines.append(f"TARGET_REPO: {target_repo}")
|
|
837
838
|
if support_url:
|
|
838
839
|
lines.append(f"SUPPORT_URL: {support_url}")
|
|
839
|
-
lines.append(f"SUBMITTED_AT: {datetime.now(
|
|
840
|
+
lines.append(f"SUBMITTED_AT: {datetime.now(timezone.utc).isoformat()}")
|
|
840
841
|
lines.append("")
|
|
841
842
|
lines.append(description)
|
|
842
843
|
if attachments_summary:
|
|
@@ -1652,6 +1653,7 @@ async def _handle_with_cli(
|
|
|
1652
1653
|
+ f"\nManaged repos: {', '.join(repos) if repos else '(none configured)'}"
|
|
1653
1654
|
+ (f"\nLog sources: {', '.join(log_sources)}" if log_sources else "")
|
|
1654
1655
|
+ f"\nAdmin access for this user: {'YES — admin tools are available' if is_admin else 'NO — admin tools will be refused'}"
|
|
1656
|
+
+ "\nNOTE: Running in CLI fallback mode — admin tools and some features are unavailable. Ask user to configure ANTHROPIC_API_KEY for full features."
|
|
1655
1657
|
+ f"\n\nCurrent status (last 24 h):\n{status_json}"
|
|
1656
1658
|
+ f"\n\nOpen PRs:\n{prs_json}"
|
|
1657
1659
|
+ (f"\n\nLog search results:\n{search_json}" if search_json else "")
|
|
@@ -1793,6 +1795,9 @@ async def _handle_with_api(
|
|
|
1793
1795
|
if not reply:
|
|
1794
1796
|
greeting = f"Hi {user_name}! " if user_name else "Hi! "
|
|
1795
1797
|
reply = f"{greeting}I'm Sentinel, your autonomous DevOps agent. How can I help you?"
|
|
1798
|
+
# Heuristic override: if reply ends with a question, Claude is waiting for input
|
|
1799
|
+
if is_done and re.search(r'\?\s*$', reply):
|
|
1800
|
+
is_done = False
|
|
1796
1801
|
history.append({"role": "assistant", "content": response.content})
|
|
1797
1802
|
return reply, is_done
|
|
1798
1803
|
|
|
@@ -379,9 +379,9 @@ async def _run_turn(session: _Session, message: str, client, cfg_loader, store,
|
|
|
379
379
|
if len(session.history) > _MAX_HISTORY_TURNS * 2:
|
|
380
380
|
session.history = session.history[-(_MAX_HISTORY_TURNS * 2):]
|
|
381
381
|
|
|
382
|
-
#
|
|
382
|
+
# Post "thinking..." and keep its ts so we can replace it with the real reply
|
|
383
383
|
session.busy = True
|
|
384
|
-
await _post(client, channel, "_thinking..._")
|
|
384
|
+
thinking_ts = await _post(client, channel, "_thinking..._")
|
|
385
385
|
|
|
386
386
|
reply = ""
|
|
387
387
|
is_done = True
|
|
@@ -401,20 +401,38 @@ async def _run_turn(session: _Session, message: str, client, cfg_loader, store,
|
|
|
401
401
|
finally:
|
|
402
402
|
session.busy = False
|
|
403
403
|
|
|
404
|
-
|
|
404
|
+
if thinking_ts:
|
|
405
|
+
await _update(client, channel, thinking_ts, reply)
|
|
406
|
+
else:
|
|
407
|
+
await _post(client, channel, reply)
|
|
405
408
|
# If session ended, save current history; if history was just cleared it will already be [] in DB
|
|
406
409
|
store.save_conversation(session.user_id, session.history)
|
|
407
410
|
|
|
408
411
|
|
|
409
412
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
410
413
|
|
|
411
|
-
async def _post(client, channel: str, text: str) ->
|
|
414
|
+
async def _post(client, channel: str, text: str) -> str:
|
|
415
|
+
"""Post a message and return its ts (for later update), or '' on failure."""
|
|
412
416
|
if not text:
|
|
413
|
-
return
|
|
417
|
+
return ""
|
|
414
418
|
try:
|
|
415
|
-
await client.chat_postMessage(channel=channel, text=text)
|
|
419
|
+
resp = await client.chat_postMessage(channel=channel, text=text)
|
|
420
|
+
return resp.get("ts", "")
|
|
416
421
|
except Exception as e:
|
|
417
422
|
logger.warning("Slack post failed: %s", e)
|
|
423
|
+
return ""
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
async def _update(client, channel: str, ts: str, text: str) -> None:
|
|
427
|
+
"""Replace an existing message in place (used to swap 'thinking...' with the reply)."""
|
|
428
|
+
if not text or not ts:
|
|
429
|
+
return
|
|
430
|
+
try:
|
|
431
|
+
await client.chat_update(channel=channel, ts=ts, text=text)
|
|
432
|
+
except Exception as e:
|
|
433
|
+
# Fall back to posting a new message if update fails (e.g. missing chat:write scope)
|
|
434
|
+
logger.warning("Slack update failed (%s), posting new message", e)
|
|
435
|
+
await _post(client, channel, text)
|
|
418
436
|
|
|
419
437
|
|
|
420
438
|
async def _resolve_name(client, user_id: str) -> str:
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
const chalk = require('chalk');
|
|
4
|
-
const [,, command = 'help', ...args] = process.argv;
|
|
5
|
-
if (command === '--version' || command === '-v') {
|
|
6
|
-
const { version } = require('../package.json');
|
|
7
|
-
console.log(version);
|
|
8
|
-
process.exit(0);
|
|
9
|
-
}
|
|
10
|
-
if (command === '--help' || command === '-h') {
|
|
11
|
-
printUsage();
|
|
12
|
-
process.exit(0);
|
|
13
|
-
}
|
|
14
|
-
const BANNER = `
|
|
15
|
-
${chalk.cyan('███████╗███████╗███╗ ██╗████████╗██╗███╗ ██╗███████╗██╗')}
|
|
16
|
-
${chalk.cyan('██╔════╝██╔════╝████╗ ██║╚══██╔══╝██║████╗ ██║██╔════╝██║')}
|
|
17
|
-
${chalk.cyan('███████╗█████╗ ██╔██╗ ██║ ██║ ██║██╔██╗ ██║█████╗ ██║')}
|
|
18
|
-
${chalk.cyan('╚════██║██╔══╝ ██║╚██╗██║ ██║ ██║██║╚██╗██║██╔══╝ ██║')}
|
|
19
|
-
${chalk.cyan('███████║███████╗██║ ╚████║ ██║ ██║██║ ╚████║███████╗███████╗')}
|
|
20
|
-
${chalk.cyan('╚══════╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝')}
|
|
21
|
-
${chalk.gray(' Autonomous DevOps Agent')}
|
|
22
|
-
`;
|
|
23
|
-
async function main() {
|
|
24
|
-
console.log(BANNER);
|
|
25
|
-
switch (command) {
|
|
26
|
-
case 'init':
|
|
27
|
-
await require('../lib/init')();
|
|
28
|
-
break;
|
|
29
|
-
case 'add':
|
|
30
|
-
await require('../lib/add')(args[0]);
|
|
31
|
-
break;
|
|
32
|
-
case 'upgrade':
|
|
33
|
-
await require('../lib/upgrade')();
|
|
34
|
-
break;
|
|
35
|
-
case 'help':
|
|
36
|
-
default:
|
|
37
|
-
printUsage();
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function printUsage() {
|
|
41
|
-
const { version } = require('../package.json');
|
|
42
|
-
console.log(`${chalk.bold('sentinel')} v${version} — Autonomous DevOps Agent
|
|
43
|
-
${chalk.bold('Usage:')}
|
|
44
|
-
sentinel init Interactive setup — install everything and create workspace
|
|
45
|
-
sentinel add <name> Add a blank project (fill config manually)
|
|
46
|
-
sentinel add <git-url> Add a project pre-configured for a GitHub repo
|
|
47
|
-
sentinel add <project.json> Add a project from a local JSON config file
|
|
48
|
-
sentinel add <https://host/cfg.json> Add a project from a remote JSON config URL
|
|
49
|
-
sentinel upgrade Pull latest version and hot-deploy Python source
|
|
50
|
-
${chalk.bold('Options:')}
|
|
51
|
-
--version, -v Print version
|
|
52
|
-
--help, -h Print this help
|
|
53
|
-
`);
|
|
54
|
-
}
|
|
55
|
-
main().catch(err => {
|
|
56
|
-
console.error(chalk.red('Error:'), err.message);
|
|
57
|
-
process.exit(1);
|
|
58
|
-
});
|