@geekbeer/minion 2.53.3 → 2.56.1
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/core/lib/capability-checker.js +44 -74
- package/core/lib/package-scanner.js +370 -0
- package/core/lib/platform.js +0 -2
- package/core/lib/running-tasks.js +125 -0
- package/core/lib/step-poller.js +6 -5
- package/core/routes/health.js +18 -12
- package/core/routes/skills.js +7 -5
- package/core/stores/variable-store.js +4 -0
- package/docs/api-reference.md +1 -1
- package/docs/environment-setup.md +15 -9
- package/docs/task-guides.md +25 -2
- package/linux/routes/chat.js +2 -32
- package/linux/routine-runner.js +16 -75
- package/linux/server.js +22 -5
- package/linux/workflow-runner.js +15 -29
- package/package.json +1 -1
- package/roles/engineer.md +1 -4
- package/rules/core.md +7 -3
- package/win/lib/process-manager.js +9 -1
- package/win/routes/chat.js +1 -17
- package/win/routine-runner.js +2 -39
- package/win/server.js +19 -4
- package/win/workflow-runner.js +3 -13
package/core/routes/health.js
CHANGED
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
* - GET /api/health - Health check
|
|
6
6
|
* - GET /api/status - Get current status
|
|
7
7
|
* - POST /api/status - Update status
|
|
8
|
-
* -
|
|
8
|
+
* - GET /api/capabilities - Get current capabilities (MCP servers, packages, env vars)
|
|
9
|
+
* - POST /api/capabilities/check - Check requirements (MCP servers, packages, env vars)
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const { version } = require('../../package.json')
|
|
12
13
|
const { config, isHqConfigured } = require('../config')
|
|
13
14
|
const { sendHeartbeat } = require('../api')
|
|
14
15
|
const { getLlmServices, isLlmCommandConfigured } = require('../lib/llm-checker')
|
|
15
|
-
const { getCapabilities,
|
|
16
|
+
const { getCapabilities, checkRequirements } = require('../lib/capability-checker')
|
|
17
|
+
const runningTasks = require('../lib/running-tasks')
|
|
16
18
|
|
|
17
19
|
function maskToken(token) {
|
|
18
20
|
if (!token || token.length < 8) return token ? '***' : ''
|
|
@@ -64,7 +66,6 @@ async function healthRoutes(fastify) {
|
|
|
64
66
|
timestamp: new Date().toISOString(),
|
|
65
67
|
llm_services: getLlmServices(),
|
|
66
68
|
llm_command_configured: isLlmCommandConfigured(),
|
|
67
|
-
capabilities: getCapabilities(),
|
|
68
69
|
env: {
|
|
69
70
|
HQ_URL: config.HQ_URL || '',
|
|
70
71
|
MINION_ID: config.MINION_ID || '',
|
|
@@ -99,7 +100,7 @@ async function healthRoutes(fastify) {
|
|
|
99
100
|
|
|
100
101
|
// Push status change to HQ immediately via heartbeat
|
|
101
102
|
if (changed && isHqConfigured()) {
|
|
102
|
-
sendHeartbeat({ status: currentStatus, current_task: currentTask, version }).catch(err => {
|
|
103
|
+
sendHeartbeat({ status: currentStatus, current_task: currentTask, running_tasks: runningTasks.getAll(), version }).catch(err => {
|
|
103
104
|
console.error('[Heartbeat] Status-change heartbeat failed:', err.message)
|
|
104
105
|
})
|
|
105
106
|
}
|
|
@@ -107,16 +108,21 @@ async function healthRoutes(fastify) {
|
|
|
107
108
|
return { success: true }
|
|
108
109
|
})
|
|
109
110
|
|
|
110
|
-
//
|
|
111
|
-
fastify.
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
// Get current capabilities (MCP servers, installed packages, env var keys)
|
|
112
|
+
fastify.get('/api/capabilities', async () => {
|
|
113
|
+
return getCapabilities()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Check requirements against current capabilities
|
|
117
|
+
fastify.post('/api/capabilities/check', async (request, reply) => {
|
|
118
|
+
const { mcp_servers, packages, env_vars } = request.body || {}
|
|
119
|
+
|
|
120
|
+
if (!mcp_servers && !packages && !env_vars) {
|
|
114
121
|
reply.code(400)
|
|
115
|
-
return { error: '
|
|
122
|
+
return { error: 'At least one of mcp_servers, packages, or env_vars must be provided' }
|
|
116
123
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return { cli_tools: checkTools(limited) }
|
|
124
|
+
|
|
125
|
+
return checkRequirements({ mcp_servers, packages, env_vars })
|
|
120
126
|
})
|
|
121
127
|
}
|
|
122
128
|
|
package/core/routes/skills.js
CHANGED
|
@@ -248,11 +248,14 @@ async function skillRoutes(fastify, opts) {
|
|
|
248
248
|
return { success: false, error: 'Invalid skill name' }
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
|
|
251
|
+
// Forward template vars query param to HQ for {{VAR_NAME}} expansion
|
|
252
|
+
const vars = request.query.vars || ''
|
|
253
|
+
const queryString = vars ? `?vars=${encodeURIComponent(vars)}` : ''
|
|
254
|
+
console.log(`[Skills] Fetching skill from HQ: ${name}${vars ? ' (with template vars)' : ''}`)
|
|
252
255
|
|
|
253
256
|
try {
|
|
254
|
-
// Fetch from HQ
|
|
255
|
-
const skill = await api.request(`/skills/${encodeURIComponent(name)}`)
|
|
257
|
+
// Fetch from HQ (with optional template variable expansion)
|
|
258
|
+
const skill = await api.request(`/skills/${encodeURIComponent(name)}${queryString}`)
|
|
256
259
|
|
|
257
260
|
// Write to local filesystem using shared helper
|
|
258
261
|
const result = await writeSkillToLocal(name, {
|
|
@@ -308,7 +311,7 @@ async function skillRoutes(fastify, opts) {
|
|
|
308
311
|
return { success: false, error: 'Unauthorized' }
|
|
309
312
|
}
|
|
310
313
|
|
|
311
|
-
const { skill_name, execution_id, step_index, workflow_name, role, revision_feedback
|
|
314
|
+
const { skill_name, execution_id, step_index, workflow_name, role, revision_feedback } = request.body || {}
|
|
312
315
|
|
|
313
316
|
if (!skill_name) {
|
|
314
317
|
reply.code(400)
|
|
@@ -357,7 +360,6 @@ async function skillRoutes(fastify, opts) {
|
|
|
357
360
|
const runOptions = {}
|
|
358
361
|
if (role) runOptions.role = role
|
|
359
362
|
if (revision_feedback) runOptions.revisionFeedback = revision_feedback
|
|
360
|
-
if (extra_env && typeof extra_env === 'object') runOptions.extraEnv = extra_env
|
|
361
363
|
|
|
362
364
|
// Run asynchronously — respond immediately
|
|
363
365
|
const executionPromise = (async () => {
|
|
@@ -98,6 +98,8 @@ function set(type, key, value) {
|
|
|
98
98
|
const data = parseEnvFile(filePath)
|
|
99
99
|
data[key] = value
|
|
100
100
|
writeEnvFile(filePath, data)
|
|
101
|
+
// Sync to process.env so running child processes inherit the updated value
|
|
102
|
+
process.env[key] = value
|
|
101
103
|
console.log(`[VariableStore] Set ${type} key: ${key}`)
|
|
102
104
|
}
|
|
103
105
|
|
|
@@ -113,6 +115,8 @@ function remove(type, key) {
|
|
|
113
115
|
if (!(key in data)) return false
|
|
114
116
|
delete data[key]
|
|
115
117
|
writeEnvFile(filePath, data)
|
|
118
|
+
// Sync to process.env so running child processes no longer inherit the removed value
|
|
119
|
+
delete process.env[key]
|
|
116
120
|
console.log(`[VariableStore] Removed ${type} key: ${key}`)
|
|
117
121
|
return true
|
|
118
122
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -11,13 +11,16 @@
|
|
|
11
11
|
```yaml
|
|
12
12
|
requires:
|
|
13
13
|
mcp_servers: [playwright]
|
|
14
|
-
|
|
14
|
+
packages:
|
|
15
|
+
apt: [jq, imagemagick]
|
|
16
|
+
env_vars: [API_KEY]
|
|
15
17
|
```
|
|
16
18
|
|
|
17
19
|
| 種別 | 検出方法 | チェック対象 |
|
|
18
20
|
|------|---------|-------------|
|
|
19
21
|
| `mcp_servers` | `~/.mcp.json` を読み取り、`mcpServers` キーにサーバー名が存在するか確認 | 設定ファイルの有無のみ(実際の起動テストはしない) |
|
|
20
|
-
| `
|
|
22
|
+
| `packages` | 各パッケージマネージャーの list コマンドでインストール済みパッケージを取得し、突合 | パッケージの存在 |
|
|
23
|
+
| `env_vars` | ミニオンローカル変数/シークレット + HQ注入変数 | キーの存在 |
|
|
21
24
|
|
|
22
25
|
**重要**: MCP サーバーは `~/.mcp.json` に設定しないと検出されない。npm パッケージをインストールしただけでは不十分。
|
|
23
26
|
|
|
@@ -91,9 +94,9 @@ URL ベースの MCP サーバーは `url` フィールドで指定する(`com
|
|
|
91
94
|
|
|
92
95
|
---
|
|
93
96
|
|
|
94
|
-
##
|
|
97
|
+
## パッケージのインストール
|
|
95
98
|
|
|
96
|
-
スキルが `requires.
|
|
99
|
+
スキルが `requires.packages` で宣言したパッケージは、対応するパッケージマネージャーの list コマンドで検出される。
|
|
97
100
|
|
|
98
101
|
### パッケージマネージャーの使い分け
|
|
99
102
|
|
|
@@ -162,7 +165,7 @@ pip3 install --user yq
|
|
|
162
165
|
|
|
163
166
|
### npx(一時実行)
|
|
164
167
|
|
|
165
|
-
`npx` はインストールせずに npm パッケージを一時実行する。`
|
|
168
|
+
`npx` はインストールせずに npm パッケージを一時実行する。`packages` の事前チェックでは検出されないため、恒久的に使うツールには `npm install -g` を使うこと。
|
|
166
169
|
|
|
167
170
|
```bash
|
|
168
171
|
# 一時的な利用(事前チェックでは検出されない)
|
|
@@ -189,8 +192,11 @@ cat ~/.mcp.json | node -e "
|
|
|
189
192
|
console.log('Configured MCP servers:', Object.keys(servers).join(', ') || '(none)');
|
|
190
193
|
"
|
|
191
194
|
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
195
|
+
# ケイパビリティが更新されているか確認(キャッシュなし、常に最新を返す)
|
|
196
|
+
curl -s "http://localhost:8080/api/capabilities" \
|
|
197
|
+
-H "Authorization: Bearer $API_TOKEN" | node -e "
|
|
198
|
+
const data = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
|
|
199
|
+
console.log('MCP servers:', Object.keys(data.mcp_servers || {}).join(', ') || '(none)');
|
|
200
|
+
console.log('Packages:', Object.keys(data.packages || {}).map(m => m + ':' + data.packages[m].items.length).join(', ') || '(none)');
|
|
201
|
+
"
|
|
196
202
|
```
|
package/docs/task-guides.md
CHANGED
|
@@ -23,10 +23,14 @@ name: my-skill
|
|
|
23
23
|
description: What this skill does
|
|
24
24
|
requires:
|
|
25
25
|
mcp_servers: [playwright, supabase]
|
|
26
|
-
|
|
26
|
+
packages:
|
|
27
|
+
apt: [ffmpeg]
|
|
28
|
+
npm: ["@anthropic-ai/claude-code"]
|
|
29
|
+
env_vars: [API_KEY]
|
|
27
30
|
---
|
|
28
31
|
|
|
29
32
|
Skill instructions here...
|
|
33
|
+
Use {{PROJECT_VAR}} to reference project/workflow variables.
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
フロントマターのフィールド:
|
|
@@ -37,11 +41,30 @@ Skill instructions here...
|
|
|
37
41
|
| `description` | Yes | スキルの説明 |
|
|
38
42
|
| `requires` | No | 実行に必要な依存関係 |
|
|
39
43
|
| `requires.mcp_servers` | No | 必要な MCP サーバー名のリスト |
|
|
40
|
-
| `requires.
|
|
44
|
+
| `requires.packages` | No | 必要なパッケージ(パッケージマネージャー別に指定) |
|
|
45
|
+
| `requires.env_vars` | No | 必要な環境変数キーのリスト |
|
|
41
46
|
|
|
42
47
|
`requires` を宣言すると、ワークフロー実行前にミニオンの環境と照合する事前チェック(readiness check)が行われる。
|
|
43
48
|
未宣言の場合は常に実行可能と見なされる。
|
|
44
49
|
|
|
50
|
+
### テンプレート変数
|
|
51
|
+
|
|
52
|
+
スキル本文中で `{{VAR_NAME}}` と記述すると、ワークフロー実行時にHQ上のプロジェクト変数・ワークフロー変数の値で自動的に置換される。スキルを再利用しつつ、プロジェクトごとに異なるパラメータを渡したい場合に使う。
|
|
53
|
+
|
|
54
|
+
```markdown
|
|
55
|
+
---
|
|
56
|
+
name: deploy-site
|
|
57
|
+
description: サイトをデプロイする
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
{{DEPLOY_TARGET}} に {{SITE_URL}} をデプロイしてください。
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- 変数名は英数字とアンダースコアのみ(`\w+`)
|
|
64
|
+
- 未定義の変数は `{{VAR_NAME}}` のまま残る(エラーにはならない)
|
|
65
|
+
- ワークフロー変数はプロジェクト変数を上書きする(同名キーの場合)
|
|
66
|
+
- ミニオン変数・シークレットは `process.env` 経由で利用可能(テンプレートではなく環境変数として)
|
|
67
|
+
|
|
45
68
|
### 2. HQ に反映する
|
|
46
69
|
|
|
47
70
|
```bash
|
package/linux/routes/chat.js
CHANGED
|
@@ -301,16 +301,6 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
301
301
|
const binaryPath = path.join(config.HOME_DIR, '.local', 'bin', binaryName)
|
|
302
302
|
const binary = fs.existsSync(binaryPath) ? binaryPath : binaryName
|
|
303
303
|
|
|
304
|
-
const extendedPath = [
|
|
305
|
-
`${config.HOME_DIR}/bin`,
|
|
306
|
-
`${config.HOME_DIR}/.npm-global/bin`,
|
|
307
|
-
`${config.HOME_DIR}/.local/bin`,
|
|
308
|
-
`${config.HOME_DIR}/.claude/bin`,
|
|
309
|
-
'/usr/local/bin',
|
|
310
|
-
'/usr/bin',
|
|
311
|
-
'/bin',
|
|
312
|
-
].join(':')
|
|
313
|
-
|
|
314
304
|
// Build CLI args (no --max-turns: allow unlimited turns for task completion)
|
|
315
305
|
const args = [
|
|
316
306
|
'-p',
|
|
@@ -329,16 +319,12 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
329
319
|
|
|
330
320
|
console.log(`[Chat] spawning: ${binary} ${sessionId ? `--resume ${sessionId}` : '(new session)'} (cwd: ${config.HOME_DIR})`)
|
|
331
321
|
|
|
322
|
+
// PATH, HOME, DISPLAY, and minion variables/secrets are already set in
|
|
323
|
+
// process.env at server startup, so child processes inherit them automatically.
|
|
332
324
|
const child = spawn(binary, args, {
|
|
333
325
|
cwd: config.HOME_DIR,
|
|
334
326
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
335
327
|
timeout: 600000, // 10 min
|
|
336
|
-
env: {
|
|
337
|
-
...process.env,
|
|
338
|
-
HOME: config.HOME_DIR,
|
|
339
|
-
PATH: extendedPath,
|
|
340
|
-
DISPLAY: ':99',
|
|
341
|
-
},
|
|
342
328
|
})
|
|
343
329
|
|
|
344
330
|
// Track active child process for abort
|
|
@@ -540,28 +526,12 @@ function runQuickLlmCall(prompt) {
|
|
|
540
526
|
const binaryPath = path.join(config.HOME_DIR, '.local', 'bin', binaryName)
|
|
541
527
|
const binary = fs.existsSync(binaryPath) ? binaryPath : binaryName
|
|
542
528
|
|
|
543
|
-
const extendedPath = [
|
|
544
|
-
`${config.HOME_DIR}/bin`,
|
|
545
|
-
`${config.HOME_DIR}/.npm-global/bin`,
|
|
546
|
-
`${config.HOME_DIR}/.local/bin`,
|
|
547
|
-
`${config.HOME_DIR}/.claude/bin`,
|
|
548
|
-
'/usr/local/bin',
|
|
549
|
-
'/usr/bin',
|
|
550
|
-
'/bin',
|
|
551
|
-
].join(':')
|
|
552
|
-
|
|
553
529
|
const args = ['-p', '--model', 'haiku', '--max-turns', '1', '--output-format', 'json']
|
|
554
530
|
|
|
555
531
|
const child = spawn(binary, args, {
|
|
556
532
|
cwd: config.HOME_DIR,
|
|
557
533
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
558
534
|
timeout: 30000,
|
|
559
|
-
env: {
|
|
560
|
-
...process.env,
|
|
561
|
-
HOME: config.HOME_DIR,
|
|
562
|
-
PATH: extendedPath,
|
|
563
|
-
DISPLAY: ':99',
|
|
564
|
-
},
|
|
565
535
|
})
|
|
566
536
|
|
|
567
537
|
child.stdin.write(prompt)
|
package/linux/routine-runner.js
CHANGED
|
@@ -14,7 +14,6 @@ const { Cron } = require('croner')
|
|
|
14
14
|
const { exec } = require('child_process')
|
|
15
15
|
const { promisify } = require('util')
|
|
16
16
|
const crypto = require('crypto')
|
|
17
|
-
const path = require('path')
|
|
18
17
|
const fs = require('fs').promises
|
|
19
18
|
const execAsync = promisify(exec)
|
|
20
19
|
|
|
@@ -22,7 +21,7 @@ const { config } = require('../core/config')
|
|
|
22
21
|
const executionStore = require('../core/stores/execution-store')
|
|
23
22
|
const routineStore = require('../core/stores/routine-store')
|
|
24
23
|
const logManager = require('../core/lib/log-manager')
|
|
25
|
-
const
|
|
24
|
+
const runningTasks = require('../core/lib/running-tasks')
|
|
26
25
|
|
|
27
26
|
// Active cron jobs keyed by routine ID
|
|
28
27
|
const activeJobs = new Map()
|
|
@@ -30,9 +29,6 @@ const activeJobs = new Map()
|
|
|
30
29
|
// Currently running executions
|
|
31
30
|
const runningExecutions = new Map()
|
|
32
31
|
|
|
33
|
-
// Marker file directory (shared with workflow-runner)
|
|
34
|
-
const MARKER_DIR = '/tmp/minion-executions'
|
|
35
|
-
|
|
36
32
|
/**
|
|
37
33
|
* Sleep for specified milliseconds
|
|
38
34
|
* @param {number} ms - Milliseconds to sleep
|
|
@@ -54,36 +50,6 @@ function generateSessionName(routineId, executionId) {
|
|
|
54
50
|
return execShort ? `rt-${routineShort}-${execShort}` : `rt-${routineShort}`
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
/**
|
|
58
|
-
* Write execution marker file for skill to read
|
|
59
|
-
* @param {string} sessionName - tmux session name
|
|
60
|
-
* @param {object} data - Execution metadata
|
|
61
|
-
*/
|
|
62
|
-
async function writeMarkerFile(sessionName, data) {
|
|
63
|
-
try {
|
|
64
|
-
await fs.mkdir(MARKER_DIR, { recursive: true })
|
|
65
|
-
const filePath = path.join(MARKER_DIR, `${sessionName}.json`)
|
|
66
|
-
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
67
|
-
console.log(`[RoutineRunner] Wrote marker file: ${filePath}`)
|
|
68
|
-
} catch (err) {
|
|
69
|
-
console.error(`[RoutineRunner] Failed to write marker file: ${err.message}`)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Clean up marker file after execution
|
|
75
|
-
* @param {string} sessionName - tmux session name
|
|
76
|
-
*/
|
|
77
|
-
async function cleanupMarkerFile(sessionName) {
|
|
78
|
-
try {
|
|
79
|
-
const filePath = path.join(MARKER_DIR, `${sessionName}.json`)
|
|
80
|
-
await fs.unlink(filePath)
|
|
81
|
-
console.log(`[RoutineRunner] Cleaned up marker file: ${filePath}`)
|
|
82
|
-
} catch {
|
|
83
|
-
// Ignore if file doesn't exist
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
53
|
/**
|
|
88
54
|
* Execute a routine in a single CLI session
|
|
89
55
|
* All skills run sequentially with context preserved.
|
|
@@ -106,17 +72,6 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
106
72
|
|
|
107
73
|
const prompt = `${contextPrefix}Run the following skills in order: ${skillCommands}. After completing all skills, run /execution-report to report the results.`
|
|
108
74
|
|
|
109
|
-
// Extend PATH to include common CLI installation locations
|
|
110
|
-
const additionalPaths = [
|
|
111
|
-
path.join(homeDir, 'bin'),
|
|
112
|
-
path.join(homeDir, '.npm-global', 'bin'),
|
|
113
|
-
path.join(homeDir, '.local', 'bin'),
|
|
114
|
-
path.join(homeDir, '.claude', 'bin'),
|
|
115
|
-
'/usr/local/bin',
|
|
116
|
-
]
|
|
117
|
-
const currentPath = process.env.PATH || ''
|
|
118
|
-
const extendedPath = [...additionalPaths, currentPath].join(':')
|
|
119
|
-
|
|
120
75
|
// Exit code file to capture CLI result
|
|
121
76
|
const exitCodeFile = `/tmp/tmux-exit-${sessionName}`
|
|
122
77
|
|
|
@@ -139,48 +94,28 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
139
94
|
// Remove old exit code file
|
|
140
95
|
await execAsync(`rm -f "${exitCodeFile}"`)
|
|
141
96
|
|
|
142
|
-
// Write marker file BEFORE starting session
|
|
143
|
-
await writeMarkerFile(sessionName, {
|
|
144
|
-
execution_id: executionId,
|
|
145
|
-
routine_id: routine.id,
|
|
146
|
-
routine_name: routine.name,
|
|
147
|
-
skill_names: skillNames,
|
|
148
|
-
started_at: new Date().toISOString(),
|
|
149
|
-
})
|
|
150
|
-
|
|
151
97
|
// Build the command to run in tmux
|
|
152
98
|
if (!config.LLM_COMMAND) {
|
|
153
99
|
throw new Error('LLM_COMMAND is not configured. Set LLM_COMMAND in minion.env (e.g., LLM_COMMAND="claude -p \'{prompt}\'")')
|
|
154
100
|
}
|
|
155
101
|
const escapedPrompt = prompt.replace(/'/g, "'\\''")
|
|
156
102
|
const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
|
|
157
|
-
const execCommand = `${llmCommand}; echo $? > ${exitCodeFile}`
|
|
158
|
-
|
|
159
|
-
// Build injected environment: minion variables/secrets (routines don't receive HQ vars)
|
|
160
|
-
const injectedEnv = variableStore.buildEnv()
|
|
161
103
|
|
|
162
|
-
// Create tmux session with
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
104
|
+
// Create tmux session with the LLM command.
|
|
105
|
+
// PATH, HOME, DISPLAY, and minion variables/secrets are already set in
|
|
106
|
+
// process.env at server startup, so child processes inherit them automatically.
|
|
107
|
+
// Per-execution identifiers are passed via -e flags for the session environment.
|
|
166
108
|
const tmuxCommand = [
|
|
167
109
|
'tmux new-session -d',
|
|
168
110
|
`-s "${sessionName}"`,
|
|
169
111
|
'-x 200 -y 50',
|
|
170
|
-
`-e "DISPLAY=:99"`,
|
|
171
|
-
`-e "PATH=${extendedPath}"`,
|
|
172
|
-
`-e "HOME=${homeDir}"`,
|
|
173
112
|
`-e "MINION_EXECUTION_ID=${executionId}"`,
|
|
174
113
|
`-e "MINION_ROUTINE_ID=${routine.id}"`,
|
|
175
114
|
`-e "MINION_ROUTINE_NAME=${routine.name.replace(/"/g, '\\"')}"`,
|
|
176
|
-
|
|
177
|
-
`"${execCommand}"`,
|
|
115
|
+
`"${llmCommand}; echo $? > ${exitCodeFile}"`,
|
|
178
116
|
].join(' ')
|
|
179
117
|
|
|
180
|
-
await execAsync(tmuxCommand, {
|
|
181
|
-
cwd: homeDir,
|
|
182
|
-
env: { ...process.env, HOME: homeDir },
|
|
183
|
-
})
|
|
118
|
+
await execAsync(tmuxCommand, { cwd: homeDir })
|
|
184
119
|
|
|
185
120
|
// Keep session alive after command completes (for debugging via terminal mirror)
|
|
186
121
|
await execAsync(`tmux set-option -t "${sessionName}" remain-on-exit on`)
|
|
@@ -238,8 +173,6 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
238
173
|
} catch (error) {
|
|
239
174
|
console.error(`[RoutineRunner] Routine ${routine.name} failed: ${error.message}`)
|
|
240
175
|
return { success: false, error: error.message, sessionName }
|
|
241
|
-
} finally {
|
|
242
|
-
await cleanupMarkerFile(sessionName)
|
|
243
176
|
}
|
|
244
177
|
}
|
|
245
178
|
|
|
@@ -269,6 +202,14 @@ async function runRoutine(routine) {
|
|
|
269
202
|
session_name: sessionName,
|
|
270
203
|
})
|
|
271
204
|
|
|
205
|
+
// Track in running_tasks for dashboard visibility
|
|
206
|
+
runningTasks.add({
|
|
207
|
+
type: 'routine',
|
|
208
|
+
routine_id: routine.id,
|
|
209
|
+
session_name: sessionName,
|
|
210
|
+
started_at: startedAt,
|
|
211
|
+
})
|
|
212
|
+
|
|
272
213
|
const logFile = logManager.getLogPath(executionId)
|
|
273
214
|
|
|
274
215
|
// Save: routine running
|
|
@@ -310,6 +251,7 @@ async function runRoutine(routine) {
|
|
|
310
251
|
await routineStore.updateLastRun(routine.id)
|
|
311
252
|
|
|
312
253
|
runningExecutions.delete(executionId)
|
|
254
|
+
runningTasks.remove(sessionName)
|
|
313
255
|
console.log(`[RoutineRunner] Completed routine: ${routine.name}`)
|
|
314
256
|
|
|
315
257
|
return { execution_id: executionId, session_name: sessionName }
|
|
@@ -418,5 +360,4 @@ module.exports = {
|
|
|
418
360
|
runRoutine,
|
|
419
361
|
getRoutineById,
|
|
420
362
|
generateSessionName,
|
|
421
|
-
MARKER_DIR,
|
|
422
363
|
}
|
package/linux/server.js
CHANGED
|
@@ -35,7 +35,7 @@ const PACKAGE_ROOT = path.join(__dirname, '..')
|
|
|
35
35
|
const { config, validate, isHqConfigured } = require('../core/config')
|
|
36
36
|
const { sendHeartbeat } = require('../core/api')
|
|
37
37
|
const { version } = require('../package.json')
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
const workflowStore = require('../core/stores/workflow-store')
|
|
40
40
|
const routineStore = require('../core/stores/routine-store')
|
|
41
41
|
|
|
@@ -53,6 +53,7 @@ const { startTerminalProxy, stopTerminalProxy } = require('./terminal-proxy')
|
|
|
53
53
|
const stepPoller = require('../core/lib/step-poller')
|
|
54
54
|
const revisionWatcher = require('../core/lib/revision-watcher')
|
|
55
55
|
const reflectionScheduler = require('../core/lib/reflection-scheduler')
|
|
56
|
+
const runningTasks = require('../core/lib/running-tasks')
|
|
56
57
|
|
|
57
58
|
// Shared routes (from core/)
|
|
58
59
|
const { healthRoutes, setOffline } = require('../core/routes/health')
|
|
@@ -92,11 +93,12 @@ async function shutdown(signal) {
|
|
|
92
93
|
heartbeatTimer = null
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
//
|
|
96
|
+
// Clear running tasks and send offline heartbeat to HQ (best-effort)
|
|
97
|
+
runningTasks.clear()
|
|
96
98
|
if (isHqConfigured()) {
|
|
97
99
|
try {
|
|
98
100
|
await Promise.race([
|
|
99
|
-
sendHeartbeat({ status: 'offline',
|
|
101
|
+
sendHeartbeat({ status: 'offline', running_tasks: [], version }),
|
|
100
102
|
new Promise(resolve => setTimeout(resolve, 3000)),
|
|
101
103
|
])
|
|
102
104
|
} catch {
|
|
@@ -276,6 +278,21 @@ async function registerAllRoutes(app) {
|
|
|
276
278
|
// Start server
|
|
277
279
|
async function start() {
|
|
278
280
|
try {
|
|
281
|
+
// Extend process.env so all child processes (tmux, spawn) inherit correct environment.
|
|
282
|
+
// This eliminates the need for per-invocation PATH building in runners/chat.
|
|
283
|
+
const { buildExtendedPath } = require('../core/lib/platform')
|
|
284
|
+
process.env.PATH = buildExtendedPath(config.HOME_DIR)
|
|
285
|
+
process.env.HOME = config.HOME_DIR
|
|
286
|
+
process.env.DISPLAY = process.env.DISPLAY || ':99'
|
|
287
|
+
|
|
288
|
+
// Load minion variables/secrets into process.env for child process inheritance
|
|
289
|
+
const variableStore = require('../core/stores/variable-store')
|
|
290
|
+
const minionEnv = variableStore.buildEnv()
|
|
291
|
+
for (const [key, value] of Object.entries(minionEnv)) {
|
|
292
|
+
if (!(key in process.env)) process.env[key] = value
|
|
293
|
+
}
|
|
294
|
+
console.log(`[Server] Loaded ${Object.keys(minionEnv).length} minion variables/secrets into process.env`)
|
|
295
|
+
|
|
279
296
|
// Sync bundled assets
|
|
280
297
|
syncBundledRules()
|
|
281
298
|
syncBundledRoles()
|
|
@@ -331,14 +348,14 @@ async function start() {
|
|
|
331
348
|
// Send initial online heartbeat
|
|
332
349
|
const { getStatus } = require('../core/routes/health')
|
|
333
350
|
const { currentTask } = getStatus()
|
|
334
|
-
sendHeartbeat({ status: 'online', current_task: currentTask,
|
|
351
|
+
sendHeartbeat({ status: 'online', current_task: currentTask, running_tasks: runningTasks.getAll(), version }).catch(err => {
|
|
335
352
|
console.error('[Heartbeat] Initial heartbeat failed:', err.message)
|
|
336
353
|
})
|
|
337
354
|
|
|
338
355
|
// Start periodic heartbeat
|
|
339
356
|
heartbeatTimer = setInterval(() => {
|
|
340
357
|
const { currentStatus, currentTask } = getStatus()
|
|
341
|
-
sendHeartbeat({ status: currentStatus, current_task: currentTask,
|
|
358
|
+
sendHeartbeat({ status: currentStatus, current_task: currentTask, running_tasks: runningTasks.getAll(), version }).catch(err => {
|
|
342
359
|
console.error('[Heartbeat] Periodic heartbeat failed:', err.message)
|
|
343
360
|
})
|
|
344
361
|
}, HEARTBEAT_INTERVAL_MS)
|
package/linux/workflow-runner.js
CHANGED
|
@@ -13,15 +13,14 @@ const { Cron } = require('croner')
|
|
|
13
13
|
const { exec } = require('child_process')
|
|
14
14
|
const { promisify } = require('util')
|
|
15
15
|
const crypto = require('crypto')
|
|
16
|
-
const path = require('path')
|
|
17
16
|
const fs = require('fs').promises
|
|
18
17
|
const execAsync = promisify(exec)
|
|
19
18
|
|
|
20
19
|
const { config } = require('../core/config')
|
|
21
20
|
const executionStore = require('../core/stores/execution-store')
|
|
22
21
|
const workflowStore = require('../core/stores/workflow-store')
|
|
23
|
-
const variableStore = require('../core/stores/variable-store')
|
|
24
22
|
const logManager = require('../core/lib/log-manager')
|
|
23
|
+
const runningTasks = require('../core/lib/running-tasks')
|
|
25
24
|
|
|
26
25
|
// Active cron jobs keyed by workflow ID
|
|
27
26
|
const activeJobs = new Map()
|
|
@@ -76,17 +75,6 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
76
75
|
|
|
77
76
|
const prompt = `${rolePrefix}${revisionContext}Run the following skills in order: ${skillCommands}.`
|
|
78
77
|
|
|
79
|
-
// Extend PATH to include common CLI installation locations
|
|
80
|
-
const additionalPaths = [
|
|
81
|
-
path.join(homeDir, 'bin'),
|
|
82
|
-
path.join(homeDir, '.npm-global', 'bin'),
|
|
83
|
-
path.join(homeDir, '.local', 'bin'),
|
|
84
|
-
path.join(homeDir, '.claude', 'bin'),
|
|
85
|
-
'/usr/local/bin',
|
|
86
|
-
]
|
|
87
|
-
const currentPath = process.env.PATH || ''
|
|
88
|
-
const extendedPath = [...additionalPaths, currentPath].join(':')
|
|
89
|
-
|
|
90
78
|
// Exit code file to capture CLI result
|
|
91
79
|
const exitCodeFile = `/tmp/tmux-exit-${sessionName}`
|
|
92
80
|
|
|
@@ -116,29 +104,18 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
116
104
|
}
|
|
117
105
|
const escapedPrompt = prompt.replace(/'/g, "'\\''")
|
|
118
106
|
const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
|
|
119
|
-
const execCommand = `${llmCommand}; echo $? > ${exitCodeFile}`
|
|
120
|
-
|
|
121
|
-
// Build injected environment: minion variables/secrets + extra vars from HQ
|
|
122
|
-
const injectedEnv = variableStore.buildEnv(options.extraEnv || {})
|
|
123
107
|
|
|
124
|
-
// Create tmux session with
|
|
125
|
-
|
|
126
|
-
|
|
108
|
+
// Create tmux session with the LLM command.
|
|
109
|
+
// PATH, HOME, DISPLAY, and minion variables/secrets are already set in
|
|
110
|
+
// process.env at server startup, so child processes inherit them automatically.
|
|
127
111
|
const tmuxCommand = [
|
|
128
112
|
'tmux new-session -d',
|
|
129
113
|
`-s "${sessionName}"`,
|
|
130
114
|
'-x 200 -y 50',
|
|
131
|
-
|
|
132
|
-
`-e "PATH=${extendedPath}"`,
|
|
133
|
-
`-e "HOME=${homeDir}"`,
|
|
134
|
-
...tmuxEnvFlags,
|
|
135
|
-
`"${execCommand}"`,
|
|
115
|
+
`"${llmCommand}; echo $? > ${exitCodeFile}"`,
|
|
136
116
|
].join(' ')
|
|
137
117
|
|
|
138
|
-
await execAsync(tmuxCommand, {
|
|
139
|
-
cwd: homeDir,
|
|
140
|
-
env: { ...process.env, HOME: homeDir },
|
|
141
|
-
})
|
|
118
|
+
await execAsync(tmuxCommand, { cwd: homeDir })
|
|
142
119
|
|
|
143
120
|
// Keep session alive after command completes (for debugging via terminal mirror)
|
|
144
121
|
await execAsync(`tmux set-option -t "${sessionName}" remain-on-exit on`)
|
|
@@ -237,6 +214,14 @@ async function runWorkflow(workflow, options = {}) {
|
|
|
237
214
|
session_name: sessionName,
|
|
238
215
|
})
|
|
239
216
|
|
|
217
|
+
// Track in running_tasks for dashboard visibility
|
|
218
|
+
runningTasks.add({
|
|
219
|
+
type: 'workflow',
|
|
220
|
+
workflow_execution_id: executionId,
|
|
221
|
+
session_name: sessionName,
|
|
222
|
+
started_at: startedAt,
|
|
223
|
+
})
|
|
224
|
+
|
|
240
225
|
// Log file path for this execution
|
|
241
226
|
const logFile = logManager.getLogPath(executionId)
|
|
242
227
|
|
|
@@ -315,6 +300,7 @@ async function runWorkflow(workflow, options = {}) {
|
|
|
315
300
|
await workflowStore.updateLastRun(workflow.id)
|
|
316
301
|
|
|
317
302
|
runningExecutions.delete(executionId)
|
|
303
|
+
runningTasks.remove(sessionName)
|
|
318
304
|
console.log(`[WorkflowRunner] Completed workflow: ${workflow.name}`)
|
|
319
305
|
|
|
320
306
|
return { execution_id: executionId, session_name: sessionName }
|
package/package.json
CHANGED
package/roles/engineer.md
CHANGED