@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.
@@ -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
- * - POST /api/capabilities/check-tools - Check arbitrary CLI tools on-demand
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, checkTools } = require('../lib/capability-checker')
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
- // Check arbitrary CLI tools on-demand
111
- fastify.post('/api/capabilities/check-tools', async (request, reply) => {
112
- const { tools } = request.body || {}
113
- if (!Array.isArray(tools) || tools.length === 0) {
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: 'tools must be a non-empty array of tool names' }
122
+ return { error: 'At least one of mcp_servers, packages, or env_vars must be provided' }
116
123
  }
117
- // Limit to 20 tools per request to avoid abuse
118
- const limited = tools.slice(0, 20).filter(t => typeof t === 'string' && t.length > 0)
119
- return { cli_tools: checkTools(limited) }
124
+
125
+ return checkRequirements({ mcp_servers, packages, env_vars })
120
126
  })
121
127
  }
122
128
 
@@ -248,11 +248,14 @@ async function skillRoutes(fastify, opts) {
248
248
  return { success: false, error: 'Invalid skill name' }
249
249
  }
250
250
 
251
- console.log(`[Skills] Fetching skill from HQ: ${name}`)
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, extra_env } = request.body || {}
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
  }
@@ -278,7 +278,7 @@ Response:
278
278
  }
279
279
  ```
280
280
 
281
- プロジェクト変数はワークフロー実行時に `extra_env` としてミニオンに渡される。
281
+ プロジェクト変数はワークフロー実行時に `template_vars` としてミニオンに渡され、スキルの `{{VAR_NAME}}` テンプレートを展開する。
282
282
 
283
283
  ### Workflow Variables (PM only)
284
284
 
@@ -11,13 +11,16 @@
11
11
  ```yaml
12
12
  requires:
13
13
  mcp_servers: [playwright]
14
- cli_tools: [jq, imagemagick]
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
- | `cli_tools` | `which <tool>` でパスが通っているか確認 | コマンドの存在(バージョンも取得を試みる) |
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
- ## CLI ツールのインストール
97
+ ## パッケージのインストール
95
98
 
96
- スキルが `requires.cli_tools` で宣言したツールは、`which` コマンドでパスが通っていれば検出される。
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 パッケージを一時実行する。`cli_tools` の事前チェックでは検出されないため、恒久的に使うツールには `npm install -g` を使うこと。
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
- # (キャパビリティは 5 分間キャッシュされるため、即座に反映したい場合)
194
- curl -s -X POST "http://localhost:8080/api/capabilities/clear-cache" \
195
- -H "Authorization: Bearer $API_TOKEN"
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
  ```
@@ -23,10 +23,14 @@ name: my-skill
23
23
  description: What this skill does
24
24
  requires:
25
25
  mcp_servers: [playwright, supabase]
26
- cli_tools: [git, node]
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.cli_tools` | No | 必要な CLI ツール名のリスト |
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
@@ -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)
@@ -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 variableStore = require('../core/stores/variable-store')
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 extended environment
163
- // Pass execution context as environment variables for /execution-report skill
164
- const tmuxEnvFlags = Object.entries(injectedEnv)
165
- .map(([k, v]) => `-e "${k}=${v.replace(/"/g, '\\"')}"`)
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
- ...tmuxEnvFlags,
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
- const { getCapabilities } = require('../core/lib/capability-checker')
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
- // Send offline heartbeat to HQ (best-effort, don't block shutdown)
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', version, capabilities: getCapabilities() }),
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, version, capabilities: getCapabilities() }).catch(err => {
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, version, capabilities: getCapabilities() }).catch(err => {
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)
@@ -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 extended environment
125
- const tmuxEnvFlags = Object.entries(injectedEnv)
126
- .map(([k, v]) => `-e "${k}=${v.replace(/"/g, '\\"')}"`)
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
- `-e "DISPLAY=:99"`,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "2.53.3",
3
+ "version": "2.56.1",
4
4
  "description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
5
5
  "main": "linux/server.js",
6
6
  "bin": {
package/roles/engineer.md CHANGED
@@ -25,7 +25,4 @@
25
25
 
26
26
  ## 実行コンテキスト
27
27
 
28
- ワークフロー実行中は以下の環境変数が利用可能:
29
- - `MINION_EXECUTION_ID` — 現在の実行UUID
30
- - `MINION_WORKFLOW_ID` — ワークフローUUID
31
- - `MINION_WORKFLOW_NAME` — ワークフロー名
28
+ ミニオン変数・シークレット(HQ UIまたはAPI経由で設定)はサーバー起動時に `process.env` にロードされ、ワークフロー実行中も利用可能。