@geekbeer/minion 2.54.1 → 2.57.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/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 +2 -1
- 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 +7 -4
- package/docs/task-guides.md +19 -0
- package/linux/minion-cli.sh +5 -0
- package/linux/routes/chat.js +2 -32
- package/linux/routine-runner.js +16 -75
- package/linux/server.js +21 -4
- 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/routes/chat.js +1 -17
- package/win/routine-runner.js +2 -39
- package/win/server.js +15 -0
- package/win/workflow-runner.js +3 -13
package/core/lib/platform.js
CHANGED
|
@@ -31,7 +31,6 @@ function resolveDataDir() {
|
|
|
31
31
|
|
|
32
32
|
const DATA_DIR = resolveDataDir()
|
|
33
33
|
const LOG_DIR = path.join(DATA_DIR, 'logs')
|
|
34
|
-
const MARKER_DIR = path.join(TEMP_DIR, 'minion-executions')
|
|
35
34
|
|
|
36
35
|
/**
|
|
37
36
|
* Build extended PATH including common CLI installation locations.
|
|
@@ -104,7 +103,6 @@ module.exports = {
|
|
|
104
103
|
TEMP_DIR,
|
|
105
104
|
DATA_DIR,
|
|
106
105
|
LOG_DIR,
|
|
107
|
-
MARKER_DIR,
|
|
108
106
|
buildExtendedPath,
|
|
109
107
|
getExitCodePath,
|
|
110
108
|
getDefaultShell,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running Tasks Store
|
|
3
|
+
*
|
|
4
|
+
* In-memory store that tracks what tmux sessions are currently running
|
|
5
|
+
* on this minion. Reported to HQ via heartbeat so the dashboard can
|
|
6
|
+
* show the minion's actual state (the "body" of the minion).
|
|
7
|
+
*
|
|
8
|
+
* Mutations trigger an immediate heartbeat (debounced to 2s) so the
|
|
9
|
+
* dashboard receives near-real-time updates.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { isHqConfigured } = require('../config')
|
|
13
|
+
|
|
14
|
+
/** @type {Array<import('../../src/lib/supabase/types').RunningTask>} */
|
|
15
|
+
let tasks = []
|
|
16
|
+
|
|
17
|
+
// Debounce timer for immediate heartbeat on mutation
|
|
18
|
+
let debounceTimer = null
|
|
19
|
+
const DEBOUNCE_MS = 2000
|
|
20
|
+
|
|
21
|
+
// Lazy reference — resolved on first use to avoid circular require
|
|
22
|
+
let _sendHeartbeat = null
|
|
23
|
+
let _getStatus = null
|
|
24
|
+
let _version = null
|
|
25
|
+
|
|
26
|
+
function getSendHeartbeat() {
|
|
27
|
+
if (!_sendHeartbeat) {
|
|
28
|
+
_sendHeartbeat = require('../api').sendHeartbeat
|
|
29
|
+
}
|
|
30
|
+
return _sendHeartbeat
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getStatusFn() {
|
|
34
|
+
if (!_getStatus) {
|
|
35
|
+
_getStatus = require('../routes/health').getStatus
|
|
36
|
+
}
|
|
37
|
+
return _getStatus
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getVersion() {
|
|
41
|
+
if (!_version) {
|
|
42
|
+
_version = require('../../package.json').version
|
|
43
|
+
}
|
|
44
|
+
return _version
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Push a status change to HQ immediately (debounced).
|
|
49
|
+
*/
|
|
50
|
+
function pushToHQ() {
|
|
51
|
+
if (!isHqConfigured()) return
|
|
52
|
+
|
|
53
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
54
|
+
debounceTimer = setTimeout(() => {
|
|
55
|
+
debounceTimer = null
|
|
56
|
+
try {
|
|
57
|
+
const { currentStatus, currentTask } = getStatusFn()
|
|
58
|
+
getSendHeartbeat()({
|
|
59
|
+
status: currentStatus,
|
|
60
|
+
current_task: currentTask,
|
|
61
|
+
running_tasks: tasks,
|
|
62
|
+
version: getVersion(),
|
|
63
|
+
}).catch(err => {
|
|
64
|
+
console.error('[RunningTasks] Heartbeat push failed:', err.message)
|
|
65
|
+
})
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error('[RunningTasks] Failed to push heartbeat:', err.message)
|
|
68
|
+
}
|
|
69
|
+
}, DEBOUNCE_MS)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Add a running task.
|
|
74
|
+
* @param {object} entry
|
|
75
|
+
* @param {'workflow'|'routine'|'directive'} entry.type
|
|
76
|
+
* @param {string} [entry.workflow_execution_id]
|
|
77
|
+
* @param {number} [entry.step_index]
|
|
78
|
+
* @param {string} [entry.routine_id]
|
|
79
|
+
* @param {string} entry.session_name
|
|
80
|
+
* @param {string} [entry.started_at] - ISO timestamp, defaults to now
|
|
81
|
+
*/
|
|
82
|
+
function add(entry) {
|
|
83
|
+
const task = {
|
|
84
|
+
type: entry.type,
|
|
85
|
+
session_name: entry.session_name,
|
|
86
|
+
started_at: entry.started_at || new Date().toISOString(),
|
|
87
|
+
}
|
|
88
|
+
if (entry.workflow_execution_id) task.workflow_execution_id = entry.workflow_execution_id
|
|
89
|
+
if (entry.step_index != null) task.step_index = entry.step_index
|
|
90
|
+
if (entry.routine_id) task.routine_id = entry.routine_id
|
|
91
|
+
|
|
92
|
+
tasks.push(task)
|
|
93
|
+
console.log(`[RunningTasks] Added: ${task.type} session=${task.session_name}`)
|
|
94
|
+
pushToHQ()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Remove a running task by session name.
|
|
99
|
+
* @param {string} sessionName
|
|
100
|
+
*/
|
|
101
|
+
function remove(sessionName) {
|
|
102
|
+
const before = tasks.length
|
|
103
|
+
tasks = tasks.filter(t => t.session_name !== sessionName)
|
|
104
|
+
if (tasks.length < before) {
|
|
105
|
+
console.log(`[RunningTasks] Removed: session=${sessionName}`)
|
|
106
|
+
pushToHQ()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get all currently running tasks.
|
|
112
|
+
* @returns {Array}
|
|
113
|
+
*/
|
|
114
|
+
function getAll() {
|
|
115
|
+
return [...tasks]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Clear all tasks (used on shutdown).
|
|
120
|
+
*/
|
|
121
|
+
function clear() {
|
|
122
|
+
tasks = []
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { add, remove, getAll, clear }
|
package/core/lib/step-poller.js
CHANGED
|
@@ -94,7 +94,7 @@ async function executeStep(step) {
|
|
|
94
94
|
assigned_role,
|
|
95
95
|
skill_name,
|
|
96
96
|
revision_feedback,
|
|
97
|
-
|
|
97
|
+
template_vars,
|
|
98
98
|
} = step
|
|
99
99
|
|
|
100
100
|
console.log(
|
|
@@ -125,9 +125,13 @@ async function executeStep(step) {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// 2. Fetch the skill from HQ to ensure it's deployed locally
|
|
128
|
+
// Pass template_vars as ?vars= so HQ expands {{VAR_NAME}} in SKILL.md
|
|
128
129
|
if (skill_name) {
|
|
129
130
|
try {
|
|
130
|
-
const
|
|
131
|
+
const varsParam = template_vars
|
|
132
|
+
? `?vars=${Buffer.from(JSON.stringify(template_vars)).toString('base64')}`
|
|
133
|
+
: ''
|
|
134
|
+
const fetchUrl = `http://localhost:${config.AGENT_PORT || 8080}/api/skills/fetch/${encodeURIComponent(skill_name)}${varsParam}`
|
|
131
135
|
const fetchResp = await fetch(fetchUrl, {
|
|
132
136
|
method: 'POST',
|
|
133
137
|
headers: { 'Authorization': `Bearer ${config.API_TOKEN}` },
|
|
@@ -152,9 +156,6 @@ async function executeStep(step) {
|
|
|
152
156
|
if (revision_feedback) {
|
|
153
157
|
runPayload.revision_feedback = revision_feedback
|
|
154
158
|
}
|
|
155
|
-
if (extra_env && typeof extra_env === 'object') {
|
|
156
|
-
runPayload.extra_env = extra_env
|
|
157
|
-
}
|
|
158
159
|
|
|
159
160
|
const runUrl = `http://localhost:${config.AGENT_PORT || 8080}/api/skills/run`
|
|
160
161
|
const runResp = await fetch(runUrl, {
|
package/core/routes/health.js
CHANGED
|
@@ -14,6 +14,7 @@ const { config, isHqConfigured } = require('../config')
|
|
|
14
14
|
const { sendHeartbeat } = require('../api')
|
|
15
15
|
const { getLlmServices, isLlmCommandConfigured } = require('../lib/llm-checker')
|
|
16
16
|
const { getCapabilities, checkRequirements } = require('../lib/capability-checker')
|
|
17
|
+
const runningTasks = require('../lib/running-tasks')
|
|
17
18
|
|
|
18
19
|
function maskToken(token) {
|
|
19
20
|
if (!token || token.length < 8) return token ? '***' : ''
|
|
@@ -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
|
}
|
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
|
@@ -192,8 +192,11 @@ cat ~/.mcp.json | node -e "
|
|
|
192
192
|
console.log('Configured MCP servers:', Object.keys(servers).join(', ') || '(none)');
|
|
193
193
|
"
|
|
194
194
|
|
|
195
|
-
#
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
"
|
|
199
202
|
```
|
package/docs/task-guides.md
CHANGED
|
@@ -30,6 +30,7 @@ requires:
|
|
|
30
30
|
---
|
|
31
31
|
|
|
32
32
|
Skill instructions here...
|
|
33
|
+
Use {{PROJECT_VAR}} to reference project/workflow variables.
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
フロントマターのフィールド:
|
|
@@ -46,6 +47,24 @@ Skill instructions here...
|
|
|
46
47
|
`requires` を宣言すると、ワークフロー実行前にミニオンの環境と照合する事前チェック(readiness check)が行われる。
|
|
47
48
|
未宣言の場合は常に実行可能と見なされる。
|
|
48
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
|
+
|
|
49
68
|
### 2. HQ に反映する
|
|
50
69
|
|
|
51
70
|
```bash
|
package/linux/minion-cli.sh
CHANGED
|
@@ -323,7 +323,12 @@ do_setup() {
|
|
|
323
323
|
echo "${TARGET_USER} ALL=(root) NOPASSWD: ${NPM_BIN} install -g @geekbeer/minion@latest"
|
|
324
324
|
echo "${TARGET_USER} ALL=(root) NOPASSWD: ${NPM_BIN} install -g @geekbeer/minion@latest --registry *"
|
|
325
325
|
echo "${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/apt-get install *"
|
|
326
|
+
echo "${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/apt-get update"
|
|
326
327
|
echo "${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/apt-get remove *"
|
|
328
|
+
# Allow sh -c wrappers (used by playwright install-deps, etc.)
|
|
329
|
+
echo "${TARGET_USER} ALL=(root) NOPASSWD: /bin/sh -c *apt-get*"
|
|
330
|
+
echo "${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/sh -c *apt-get*"
|
|
331
|
+
echo "${TARGET_USER} ALL=(root) NOPASSWD: /bin/sh -c *dpkg*"
|
|
327
332
|
|
|
328
333
|
case "$PROC_MGR" in
|
|
329
334
|
systemd)
|
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
|
@@ -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', version }),
|
|
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 }).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 }).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)
|
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
package/rules/core.md
CHANGED
|
@@ -62,10 +62,14 @@ minion-cli --version # バージョン確認
|
|
|
62
62
|
| `AGENT_PORT` | Agent HTTP port (default: 8080) |
|
|
63
63
|
| `MINION_USER` | System user running the agent |
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
Routine 実行中は以下もtmuxセッション環境で利用可能:
|
|
66
66
|
- `MINION_EXECUTION_ID` — 実行UUID
|
|
67
|
-
- `
|
|
68
|
-
- `
|
|
67
|
+
- `MINION_ROUTINE_ID` — ルーティンUUID
|
|
68
|
+
- `MINION_ROUTINE_NAME` — ルーティン名
|
|
69
|
+
|
|
70
|
+
ミニオン変数・シークレット(HQ UIまたはAPI経由で設定)はサーバー起動時に `process.env` にロードされ、全子プロセスで利用可能。
|
|
71
|
+
|
|
72
|
+
プロジェクト変数・ワークフロー変数はスキル本文の `{{VAR_NAME}}` テンプレートとして展開される。スキル作成時にパラメータ化したい値は `{{変数名}}` で記述すること。
|
|
69
73
|
|
|
70
74
|
## Skills Directory
|
|
71
75
|
|
package/win/routes/chat.js
CHANGED
|
@@ -18,7 +18,7 @@ const path = require('path')
|
|
|
18
18
|
const { verifyToken } = require('../../core/lib/auth')
|
|
19
19
|
const { config } = require('../../core/config')
|
|
20
20
|
const chatStore = require('../../core/stores/chat-store')
|
|
21
|
-
const {
|
|
21
|
+
const { DATA_DIR } = require('../../core/lib/platform')
|
|
22
22
|
const { runEndOfDay } = require('../../core/lib/end-of-day')
|
|
23
23
|
|
|
24
24
|
let activeChatChild = null
|
|
@@ -253,9 +253,6 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
253
253
|
const binaryPath = path.join(config.HOME_DIR, '.local', 'bin', binaryName)
|
|
254
254
|
const binary = fs.existsSync(binaryPath) ? binaryPath : binaryName
|
|
255
255
|
|
|
256
|
-
// Windows-compatible PATH construction
|
|
257
|
-
const extendedPath = buildExtendedPath(config.HOME_DIR)
|
|
258
|
-
|
|
259
256
|
const args = ['-p', '--verbose', '--model', 'sonnet', '--output-format', 'stream-json']
|
|
260
257
|
if (sessionId) args.push('--resume', sessionId)
|
|
261
258
|
// Prompt is passed via stdin (not as CLI argument) to avoid
|
|
@@ -268,12 +265,6 @@ function streamLlmResponse(res, prompt, sessionId) {
|
|
|
268
265
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
269
266
|
timeout: 600000,
|
|
270
267
|
shell: true, // Required on Windows to resolve .cmd shims (e.g. claude.cmd)
|
|
271
|
-
env: {
|
|
272
|
-
...process.env,
|
|
273
|
-
HOME: config.HOME_DIR,
|
|
274
|
-
USERPROFILE: config.HOME_DIR,
|
|
275
|
-
PATH: extendedPath,
|
|
276
|
-
},
|
|
277
268
|
})
|
|
278
269
|
|
|
279
270
|
activeChatChild = child
|
|
@@ -442,7 +433,6 @@ function runQuickLlmCall(prompt) {
|
|
|
442
433
|
}
|
|
443
434
|
const binaryPath = path.join(config.HOME_DIR, '.local', 'bin', binaryName)
|
|
444
435
|
const binary = fs.existsSync(binaryPath) ? binaryPath : binaryName
|
|
445
|
-
const extendedPath = buildExtendedPath(config.HOME_DIR)
|
|
446
436
|
|
|
447
437
|
const args = ['-p', '--model', 'haiku', '--max-turns', '1', '--output-format', 'json']
|
|
448
438
|
|
|
@@ -451,12 +441,6 @@ function runQuickLlmCall(prompt) {
|
|
|
451
441
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
452
442
|
timeout: 30000,
|
|
453
443
|
shell: true,
|
|
454
|
-
env: {
|
|
455
|
-
...process.env,
|
|
456
|
-
HOME: config.HOME_DIR,
|
|
457
|
-
USERPROFILE: config.HOME_DIR,
|
|
458
|
-
PATH: extendedPath,
|
|
459
|
-
},
|
|
460
444
|
})
|
|
461
445
|
|
|
462
446
|
child.stdin.write(prompt)
|
package/win/routine-runner.js
CHANGED
|
@@ -16,8 +16,6 @@ const { config } = require('../core/config')
|
|
|
16
16
|
const executionStore = require('../core/stores/execution-store')
|
|
17
17
|
const routineStore = require('../core/stores/routine-store')
|
|
18
18
|
const logManager = require('../core/lib/log-manager')
|
|
19
|
-
const { MARKER_DIR, buildExtendedPath } = require('../core/lib/platform')
|
|
20
|
-
const variableStore = require('../core/stores/variable-store')
|
|
21
19
|
const { activeSessions } = require('./workflow-runner')
|
|
22
20
|
|
|
23
21
|
const activeJobs = new Map()
|
|
@@ -33,25 +31,6 @@ function generateSessionName(routineId, executionId) {
|
|
|
33
31
|
return execShort ? `rt-${routineShort}-${execShort}` : `rt-${routineShort}`
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
async function writeMarkerFile(sessionName, data) {
|
|
37
|
-
try {
|
|
38
|
-
await fs.mkdir(MARKER_DIR, { recursive: true })
|
|
39
|
-
const filePath = path.join(MARKER_DIR, `${sessionName}.json`)
|
|
40
|
-
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
41
|
-
console.log(`[RoutineRunner] Wrote marker file: ${filePath}`)
|
|
42
|
-
} catch (err) {
|
|
43
|
-
console.error(`[RoutineRunner] Failed to write marker file: ${err.message}`)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function cleanupMarkerFile(sessionName) {
|
|
48
|
-
try {
|
|
49
|
-
const filePath = path.join(MARKER_DIR, `${sessionName}.json`)
|
|
50
|
-
await fs.unlink(filePath)
|
|
51
|
-
console.log(`[RoutineRunner] Cleaned up marker file: ${filePath}`)
|
|
52
|
-
} catch { /* ignore */ }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
34
|
function loadNodePty() {
|
|
56
35
|
// Prefer prebuilt binaries (no Build Tools required)
|
|
57
36
|
try { return require('node-pty-prebuilt-multiarch') } catch {}
|
|
@@ -74,7 +53,6 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
74
53
|
: ''
|
|
75
54
|
const prompt = `${contextPrefix}Run the following skills in order: ${skillCommands}. After completing all skills, run /execution-report to report the results.`
|
|
76
55
|
|
|
77
|
-
const extendedPath = buildExtendedPath(homeDir)
|
|
78
56
|
const logFile = logManager.getLogPath(executionId)
|
|
79
57
|
|
|
80
58
|
console.log(`[RoutineRunner] Executing routine: ${routine.name}`)
|
|
@@ -92,28 +70,16 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
92
70
|
activeSessions.delete(sessionName)
|
|
93
71
|
}
|
|
94
72
|
|
|
95
|
-
await writeMarkerFile(sessionName, {
|
|
96
|
-
execution_id: executionId,
|
|
97
|
-
routine_id: routine.id,
|
|
98
|
-
routine_name: routine.name,
|
|
99
|
-
skill_names: skillNames,
|
|
100
|
-
started_at: new Date().toISOString(),
|
|
101
|
-
})
|
|
102
|
-
|
|
103
73
|
if (!config.LLM_COMMAND) {
|
|
104
74
|
throw new Error('LLM_COMMAND is not configured. Set LLM_COMMAND in minion.env')
|
|
105
75
|
}
|
|
106
76
|
const escapedPrompt = prompt.replace(/'/g, "''")
|
|
107
77
|
const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
|
|
108
78
|
|
|
109
|
-
//
|
|
110
|
-
|
|
79
|
+
// PATH, HOME, USERPROFILE, and minion variables/secrets are already set in
|
|
80
|
+
// process.env at server startup. Per-execution identifiers are added here.
|
|
111
81
|
const env = {
|
|
112
82
|
...process.env,
|
|
113
|
-
...injectedEnv,
|
|
114
|
-
HOME: homeDir,
|
|
115
|
-
USERPROFILE: homeDir,
|
|
116
|
-
PATH: extendedPath,
|
|
117
83
|
MINION_EXECUTION_ID: executionId,
|
|
118
84
|
MINION_ROUTINE_ID: routine.id,
|
|
119
85
|
MINION_ROUTINE_NAME: routine.name,
|
|
@@ -181,8 +147,6 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
181
147
|
} catch (error) {
|
|
182
148
|
console.error(`[RoutineRunner] Routine ${routine.name} failed: ${error.message}`)
|
|
183
149
|
return { success: false, error: error.message, sessionName }
|
|
184
|
-
} finally {
|
|
185
|
-
await cleanupMarkerFile(sessionName)
|
|
186
150
|
}
|
|
187
151
|
}
|
|
188
152
|
|
|
@@ -326,5 +290,4 @@ module.exports = {
|
|
|
326
290
|
runRoutine,
|
|
327
291
|
getRoutineById,
|
|
328
292
|
generateSessionName,
|
|
329
|
-
MARKER_DIR,
|
|
330
293
|
}
|
package/win/server.js
CHANGED
|
@@ -214,6 +214,21 @@ async function registerRoutes(app) {
|
|
|
214
214
|
// Start server
|
|
215
215
|
async function start() {
|
|
216
216
|
try {
|
|
217
|
+
// Extend process.env so all child processes (spawn) inherit correct environment.
|
|
218
|
+
// This eliminates the need for per-invocation PATH building in runners/chat.
|
|
219
|
+
const { buildExtendedPath } = require('../core/lib/platform')
|
|
220
|
+
process.env.PATH = buildExtendedPath(config.HOME_DIR)
|
|
221
|
+
process.env.HOME = config.HOME_DIR
|
|
222
|
+
process.env.USERPROFILE = config.HOME_DIR
|
|
223
|
+
|
|
224
|
+
// Load minion variables/secrets into process.env for child process inheritance
|
|
225
|
+
const variableStore = require('../core/stores/variable-store')
|
|
226
|
+
const minionEnv = variableStore.buildEnv()
|
|
227
|
+
for (const [key, value] of Object.entries(minionEnv)) {
|
|
228
|
+
if (!(key in process.env)) process.env[key] = value
|
|
229
|
+
}
|
|
230
|
+
console.log(`[Server] Loaded ${Object.keys(minionEnv).length} minion variables/secrets into process.env`)
|
|
231
|
+
|
|
217
232
|
// Sync bundled assets
|
|
218
233
|
syncBundledRules()
|
|
219
234
|
syncBundledRoles()
|
package/win/workflow-runner.js
CHANGED
|
@@ -23,8 +23,6 @@ const { config } = require('../core/config')
|
|
|
23
23
|
const executionStore = require('../core/stores/execution-store')
|
|
24
24
|
const workflowStore = require('../core/stores/workflow-store')
|
|
25
25
|
const logManager = require('../core/lib/log-manager')
|
|
26
|
-
const { buildExtendedPath } = require('../core/lib/platform')
|
|
27
|
-
const variableStore = require('../core/stores/variable-store')
|
|
28
26
|
|
|
29
27
|
// Active cron jobs keyed by workflow ID
|
|
30
28
|
const activeJobs = new Map()
|
|
@@ -86,7 +84,6 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
86
84
|
|
|
87
85
|
const prompt = `${rolePrefix}${revisionContext}Run the following skills in order: ${skillCommands}.`
|
|
88
86
|
|
|
89
|
-
const extendedPath = buildExtendedPath(homeDir)
|
|
90
87
|
const logFile = logManager.getLogPath(executionId)
|
|
91
88
|
|
|
92
89
|
console.log(`[WorkflowRunner] Executing workflow: ${workflow.name}`)
|
|
@@ -113,15 +110,8 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
113
110
|
const escapedPrompt = prompt.replace(/'/g, "''")
|
|
114
111
|
const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
|
|
115
112
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
const env = {
|
|
119
|
-
...process.env,
|
|
120
|
-
...injectedEnv,
|
|
121
|
-
HOME: homeDir,
|
|
122
|
-
USERPROFILE: homeDir,
|
|
123
|
-
PATH: extendedPath,
|
|
124
|
-
}
|
|
113
|
+
// PATH, HOME, USERPROFILE, and minion variables/secrets are already set in
|
|
114
|
+
// process.env at server startup, so child processes inherit them automatically.
|
|
125
115
|
|
|
126
116
|
// Open log file for streaming writes
|
|
127
117
|
const logDir = path.dirname(logFile)
|
|
@@ -137,7 +127,7 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
137
127
|
cols: 200,
|
|
138
128
|
rows: 50,
|
|
139
129
|
cwd: homeDir,
|
|
140
|
-
env,
|
|
130
|
+
env: process.env,
|
|
141
131
|
})
|
|
142
132
|
|
|
143
133
|
// Track session
|