@geekbeer/minion 2.48.3 → 2.50.2
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/config.js +1 -2
- package/core/lib/reflection-scheduler.js +22 -9
- package/core/routes/health.js +4 -0
- package/core/routes/skills.js +31 -21
- package/core/stores/daily-log-store.js +8 -3
- package/docs/api-reference.md +63 -4
- package/docs/task-guides.md +16 -1
- package/linux/minion-cli.sh +0 -13
- package/linux/routes/config.js +2 -2
- package/linux/workflow-runner.js +18 -3
- package/package.json +1 -1
- package/win/lib/process-manager.js +17 -15
- package/win/minion-cli.ps1 +0 -7
- package/win/routes/commands.js +23 -5
- package/win/routes/config.js +2 -2
- package/win/workflow-runner.js +17 -5
package/core/config.js
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
* - AGENT_PORT: Port for the local agent server (default: 8080)
|
|
14
14
|
* - MINION_USER: System user running the agent (used to resolve home directory)
|
|
15
15
|
* - REFLECTION_TIME: Daily self-reflection time in HH:MM format (e.g., "03:00")
|
|
16
|
-
* - TIMEZONE: Timezone for reflection schedule (default: "Asia/Tokyo")
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
18
|
const os = require('os')
|
|
@@ -92,8 +91,8 @@ const config = {
|
|
|
92
91
|
|
|
93
92
|
// Self-reflection schedule (自己反省時間)
|
|
94
93
|
// Daily time to automatically run end-of-day processing (log + memory extraction + session clear)
|
|
94
|
+
// Time is interpreted in the system's local timezone (set via TZ env var or OS settings)
|
|
95
95
|
REFLECTION_TIME: process.env.REFLECTION_TIME ?? '03:00', // "HH:MM" format, empty = disabled
|
|
96
|
-
TIMEZONE: process.env.TIMEZONE || 'Asia/Tokyo',
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
/**
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
* Reflection Scheduler (自己反省スケジューラ)
|
|
3
3
|
*
|
|
4
4
|
* Built-in server-level scheduler that triggers end-of-day processing
|
|
5
|
-
* (daily log generation + memory extraction
|
|
5
|
+
* (daily log generation + memory extraction) at a configured time.
|
|
6
|
+
* Chat session is preserved to maintain conversation continuity.
|
|
6
7
|
*
|
|
7
8
|
* Unlike routines (user-deletable), this is a core architectural feature
|
|
8
9
|
* that ensures the minion regularly consolidates its conversation history.
|
|
9
10
|
*
|
|
10
11
|
* Configuration:
|
|
11
12
|
* REFLECTION_TIME - Time to run daily reflection (format: "HH:MM", e.g., "03:00")
|
|
12
|
-
*
|
|
13
|
+
*
|
|
14
|
+
* Time is interpreted in the system's local timezone (set via TZ env var or OS settings).
|
|
13
15
|
*
|
|
14
16
|
* @module core/lib/reflection-scheduler
|
|
15
17
|
*/
|
|
@@ -42,6 +44,18 @@ function parseToCron(timeStr) {
|
|
|
42
44
|
return `0 ${minute} ${hour} * * *`
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Get the system's local timezone name.
|
|
49
|
+
* @returns {string} IANA timezone string (e.g., "Asia/Tokyo", "UTC")
|
|
50
|
+
*/
|
|
51
|
+
function getSystemTimezone() {
|
|
52
|
+
try {
|
|
53
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
54
|
+
} catch {
|
|
55
|
+
return 'UTC'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
/**
|
|
46
60
|
* Start the reflection scheduler.
|
|
47
61
|
* If REFLECTION_TIME is not configured, the scheduler remains inactive.
|
|
@@ -63,15 +77,15 @@ function start(runQuickLlmCall) {
|
|
|
63
77
|
return
|
|
64
78
|
}
|
|
65
79
|
|
|
66
|
-
const
|
|
80
|
+
const systemTz = getSystemTimezone()
|
|
67
81
|
|
|
68
82
|
try {
|
|
69
|
-
cronJob = new Cron(cronExpr,
|
|
83
|
+
cronJob = new Cron(cronExpr, async () => {
|
|
70
84
|
await runReflection()
|
|
71
85
|
})
|
|
72
86
|
|
|
73
87
|
const nextRun = cronJob.nextRun()
|
|
74
|
-
console.log(`[ReflectionScheduler] Scheduled at ${reflectionTime} (${
|
|
88
|
+
console.log(`[ReflectionScheduler] Scheduled at ${reflectionTime} (system timezone: ${systemTz}), next run: ${nextRun ? nextRun.toISOString() : 'unknown'}`)
|
|
75
89
|
} catch (err) {
|
|
76
90
|
console.error(`[ReflectionScheduler] Failed to start: ${err.message}`)
|
|
77
91
|
}
|
|
@@ -89,7 +103,7 @@ function stop() {
|
|
|
89
103
|
}
|
|
90
104
|
|
|
91
105
|
/**
|
|
92
|
-
* Reschedule after config change (e.g., REFLECTION_TIME
|
|
106
|
+
* Reschedule after config change (e.g., REFLECTION_TIME updated via API).
|
|
93
107
|
* Requires that start() was called previously with a runQuickLlmCall function.
|
|
94
108
|
*/
|
|
95
109
|
function reschedule() {
|
|
@@ -122,7 +136,7 @@ async function runReflection() {
|
|
|
122
136
|
try {
|
|
123
137
|
const result = await runEndOfDay({
|
|
124
138
|
runQuickLlmCall: llmCallFn,
|
|
125
|
-
clearSession:
|
|
139
|
+
clearSession: false,
|
|
126
140
|
})
|
|
127
141
|
|
|
128
142
|
if (result.daily_log) {
|
|
@@ -143,13 +157,12 @@ async function runReflection() {
|
|
|
143
157
|
*/
|
|
144
158
|
function getStatus() {
|
|
145
159
|
const reflectionTime = config.REFLECTION_TIME || ''
|
|
146
|
-
const timezone = config.TIMEZONE || 'Asia/Tokyo'
|
|
147
160
|
const nextRun = cronJob ? cronJob.nextRun() : null
|
|
148
161
|
|
|
149
162
|
return {
|
|
150
163
|
enabled: !!cronJob,
|
|
151
164
|
reflection_time: reflectionTime,
|
|
152
|
-
timezone,
|
|
165
|
+
timezone: getSystemTimezone(),
|
|
153
166
|
next_run: nextRun ? nextRun.toISOString() : null,
|
|
154
167
|
}
|
|
155
168
|
}
|
package/core/routes/health.js
CHANGED
|
@@ -51,11 +51,15 @@ async function healthRoutes(fastify) {
|
|
|
51
51
|
|
|
52
52
|
// Get current status
|
|
53
53
|
fastify.get('/api/status', async () => {
|
|
54
|
+
let systemTimezone = 'UTC'
|
|
55
|
+
try { systemTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone } catch {}
|
|
56
|
+
|
|
54
57
|
return {
|
|
55
58
|
status: currentStatus,
|
|
56
59
|
current_task: currentTask,
|
|
57
60
|
uptime: process.uptime(),
|
|
58
61
|
version,
|
|
62
|
+
timezone: systemTimezone,
|
|
59
63
|
timestamp: new Date().toISOString(),
|
|
60
64
|
llm_services: getLlmServices(),
|
|
61
65
|
llm_command_configured: isLlmCommandConfigured(),
|
package/core/routes/skills.js
CHANGED
|
@@ -48,15 +48,15 @@ function parseFrontmatter(content) {
|
|
|
48
48
|
* @param {string} opts.content - Skill body (markdown without frontmatter)
|
|
49
49
|
* @param {string} [opts.description] - Skill description for frontmatter
|
|
50
50
|
* @param {string} [opts.display_name] - Display name for frontmatter
|
|
51
|
-
* @param {Array<{
|
|
52
|
-
* @returns {Promise<{path: string,
|
|
51
|
+
* @param {Array<{path: string, content?: string}>} [opts.files] - Skill files from HQ storage
|
|
52
|
+
* @returns {Promise<{path: string, files_count: number}>}
|
|
53
53
|
*/
|
|
54
|
-
async function writeSkillToLocal(name, { content, description, display_name, type,
|
|
54
|
+
async function writeSkillToLocal(name, { content, description, display_name, type, files = [] }) {
|
|
55
55
|
const skillDir = path.join(config.HOME_DIR, '.claude', 'skills', name)
|
|
56
|
-
const
|
|
56
|
+
const filesDir = path.join(skillDir, 'files')
|
|
57
57
|
|
|
58
58
|
await fs.mkdir(skillDir, { recursive: true })
|
|
59
|
-
await fs.mkdir(
|
|
59
|
+
await fs.mkdir(filesDir, { recursive: true })
|
|
60
60
|
|
|
61
61
|
// Build frontmatter with all available metadata
|
|
62
62
|
const frontmatterLines = [
|
|
@@ -72,15 +72,15 @@ async function writeSkillToLocal(name, { content, description, display_name, typ
|
|
|
72
72
|
'utf-8'
|
|
73
73
|
)
|
|
74
74
|
|
|
75
|
-
// Write
|
|
76
|
-
for (const
|
|
77
|
-
if (
|
|
78
|
-
const safeFilename = path.basename(
|
|
79
|
-
await fs.writeFile(path.join(
|
|
75
|
+
// Write skill files
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
if (file.path && file.content) {
|
|
78
|
+
const safeFilename = path.basename(file.path)
|
|
79
|
+
await fs.writeFile(path.join(filesDir, safeFilename), file.content, 'utf-8')
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
return { path: skillDir,
|
|
83
|
+
return { path: skillDir, files_count: files.length }
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
@@ -99,17 +99,27 @@ async function pushSkillToHQ(name) {
|
|
|
99
99
|
const rawContent = await fs.readFile(skillMdPath, 'utf-8')
|
|
100
100
|
const { metadata, body } = parseFrontmatter(rawContent)
|
|
101
101
|
|
|
102
|
-
// Read
|
|
103
|
-
const
|
|
104
|
-
const
|
|
102
|
+
// Read skill files
|
|
103
|
+
const filesDir = path.join(skillDir, 'files')
|
|
104
|
+
const files = []
|
|
105
105
|
try {
|
|
106
|
-
const
|
|
107
|
-
for (const filename of
|
|
108
|
-
const
|
|
109
|
-
|
|
106
|
+
const fileEntries = await fs.readdir(filesDir)
|
|
107
|
+
for (const filename of fileEntries) {
|
|
108
|
+
const fileContent = await fs.readFile(path.join(filesDir, filename), 'utf-8')
|
|
109
|
+
files.push({ path: filename, content: fileContent })
|
|
110
110
|
}
|
|
111
111
|
} catch {
|
|
112
|
-
// No references
|
|
112
|
+
// No files directory — try legacy references/ for backward compatibility
|
|
113
|
+
try {
|
|
114
|
+
const legacyDir = path.join(skillDir, 'references')
|
|
115
|
+
const legacyEntries = await fs.readdir(legacyDir)
|
|
116
|
+
for (const filename of legacyEntries) {
|
|
117
|
+
const fileContent = await fs.readFile(path.join(legacyDir, filename), 'utf-8')
|
|
118
|
+
files.push({ path: filename, content: fileContent })
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// No files at all
|
|
122
|
+
}
|
|
113
123
|
}
|
|
114
124
|
|
|
115
125
|
return api.request('/skills', {
|
|
@@ -120,7 +130,7 @@ async function pushSkillToHQ(name) {
|
|
|
120
130
|
description: metadata.description || '',
|
|
121
131
|
content: body,
|
|
122
132
|
type: metadata.type || 'workflow',
|
|
123
|
-
|
|
133
|
+
files,
|
|
124
134
|
}),
|
|
125
135
|
})
|
|
126
136
|
}
|
|
@@ -243,7 +253,7 @@ async function skillRoutes(fastify, opts) {
|
|
|
243
253
|
description: skill.description,
|
|
244
254
|
display_name: skill.display_name,
|
|
245
255
|
type: skill.type,
|
|
246
|
-
|
|
256
|
+
files: skill.files || [],
|
|
247
257
|
})
|
|
248
258
|
|
|
249
259
|
console.log(`[Skills] Skill fetched and deployed: ${name}`)
|
|
@@ -42,8 +42,8 @@ function isValidDate(date) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* List all daily logs with date
|
|
46
|
-
* @returns {Promise<Array<{ date: string, size: number }>>} Sorted descending by date
|
|
45
|
+
* List all daily logs with date, file size, and timestamps.
|
|
46
|
+
* @returns {Promise<Array<{ date: string, size: number, created_at: string, updated_at: string }>>} Sorted descending by date
|
|
47
47
|
*/
|
|
48
48
|
async function listLogs() {
|
|
49
49
|
await ensureDir()
|
|
@@ -61,7 +61,12 @@ async function listLogs() {
|
|
|
61
61
|
if (!isValidDate(date)) continue
|
|
62
62
|
try {
|
|
63
63
|
const stat = await fs.stat(path.join(LOGS_DIR, file))
|
|
64
|
-
logs.push({
|
|
64
|
+
logs.push({
|
|
65
|
+
date,
|
|
66
|
+
size: stat.size,
|
|
67
|
+
created_at: stat.birthtime.toISOString(),
|
|
68
|
+
updated_at: stat.mtime.toISOString(),
|
|
69
|
+
})
|
|
65
70
|
} catch {
|
|
66
71
|
// Skip unreadable
|
|
67
72
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -179,10 +179,11 @@ Configuration via `PUT /api/config/env`:
|
|
|
179
179
|
|
|
180
180
|
| Key | Format | Default | Description |
|
|
181
181
|
|-----|--------|---------|-------------|
|
|
182
|
-
| `REFLECTION_TIME` | `HH:MM` |
|
|
183
|
-
| `TIMEZONE` | IANA tz | `Asia/Tokyo` | Timezone for the schedule |
|
|
182
|
+
| `REFLECTION_TIME` | `HH:MM` | `03:00` | Daily reflection time (interpreted in system timezone) |
|
|
184
183
|
|
|
185
|
-
|
|
184
|
+
Timezone follows the system setting (Docker: `TZ` env var, VPS: `timedatectl`).
|
|
185
|
+
|
|
186
|
+
Example — set reflection at 3:00 AM:
|
|
186
187
|
```bash
|
|
187
188
|
curl -X PUT /api/config/env \
|
|
188
189
|
-H "Authorization: Bearer $TOKEN" \
|
|
@@ -201,7 +202,7 @@ Changes via the config API take effect immediately (no restart required).
|
|
|
201
202
|
| GET | `/api/config/backup` | Download config files as tar.gz |
|
|
202
203
|
| POST | `/api/config/restore` | Restore config from tar.gz |
|
|
203
204
|
|
|
204
|
-
Allowed keys: `LLM_COMMAND`, `REFLECTION_TIME
|
|
205
|
+
Allowed keys: `LLM_COMMAND`, `REFLECTION_TIME`
|
|
205
206
|
|
|
206
207
|
### Commands
|
|
207
208
|
|
|
@@ -252,6 +253,64 @@ Response:
|
|
|
252
253
|
|
|
253
254
|
PUT body: `{ "content": "markdown string" }`
|
|
254
255
|
|
|
256
|
+
### Project Variables (PM only)
|
|
257
|
+
|
|
258
|
+
| Method | Endpoint | Description |
|
|
259
|
+
|--------|----------|-------------|
|
|
260
|
+
| POST | `/api/minion/me/project/[id]/variables/[key]` | プロジェクト変数を設定(upsert) |
|
|
261
|
+
| DELETE | `/api/minion/me/project/[id]/variables/[key]` | プロジェクト変数を削除 |
|
|
262
|
+
|
|
263
|
+
POST body:
|
|
264
|
+
```json
|
|
265
|
+
{
|
|
266
|
+
"value": "variable value (max 2000 chars)"
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Key format: `/^[A-Za-z_][A-Za-z0-9_]{0,99}$/`(環境変数命名規約)
|
|
271
|
+
|
|
272
|
+
Response:
|
|
273
|
+
```json
|
|
274
|
+
{
|
|
275
|
+
"success": true,
|
|
276
|
+
"key": "MY_VAR",
|
|
277
|
+
"value": "variable value"
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
プロジェクト変数はワークフロー実行時に `extra_env` としてミニオンに渡される。
|
|
282
|
+
|
|
283
|
+
### Workflow Variables (PM only)
|
|
284
|
+
|
|
285
|
+
| Method | Endpoint | Description |
|
|
286
|
+
|--------|----------|-------------|
|
|
287
|
+
| POST | `/api/minion/me/project/[id]/workflows/[wfId]/variables/[key]` | ワークフロー変数を設定(upsert) |
|
|
288
|
+
| DELETE | `/api/minion/me/project/[id]/workflows/[wfId]/variables/[key]` | ワークフロー変数を削除 |
|
|
289
|
+
|
|
290
|
+
POST body:
|
|
291
|
+
```json
|
|
292
|
+
{
|
|
293
|
+
"value": "variable value (max 2000 chars)"
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Key format: `/^[A-Za-z_][A-Za-z0-9_]{0,99}$/`
|
|
298
|
+
|
|
299
|
+
Response:
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"success": true,
|
|
303
|
+
"key": "MY_VAR",
|
|
304
|
+
"value": "variable value"
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
ワークフロー変数はプロジェクト変数を上書きする(同名キーの場合)。実行時のマージ順序:
|
|
309
|
+
1. ミニオンローカル変数
|
|
310
|
+
2. ミニオンシークレット
|
|
311
|
+
3. プロジェクト変数
|
|
312
|
+
4. ワークフロー変数(最優先)
|
|
313
|
+
|
|
255
314
|
### Workflows (project-scoped, versioned)
|
|
256
315
|
|
|
257
316
|
| Method | Endpoint | Description |
|
package/docs/task-guides.md
CHANGED
|
@@ -20,13 +20,28 @@ SKILL.md フォーマット:
|
|
|
20
20
|
```markdown
|
|
21
21
|
---
|
|
22
22
|
name: my-skill
|
|
23
|
-
display_name: My Skill
|
|
24
23
|
description: What this skill does
|
|
24
|
+
requires:
|
|
25
|
+
mcp_servers: [playwright, supabase]
|
|
26
|
+
cli_tools: [git, node]
|
|
25
27
|
---
|
|
26
28
|
|
|
27
29
|
Skill instructions here...
|
|
28
30
|
```
|
|
29
31
|
|
|
32
|
+
フロントマターのフィールド:
|
|
33
|
+
|
|
34
|
+
| Field | Required | Description |
|
|
35
|
+
|-------|----------|-------------|
|
|
36
|
+
| `name` | Yes | スキル識別子(小文字・ハイフン・数字) |
|
|
37
|
+
| `description` | Yes | スキルの説明 |
|
|
38
|
+
| `requires` | No | 実行に必要な依存関係 |
|
|
39
|
+
| `requires.mcp_servers` | No | 必要な MCP サーバー名のリスト |
|
|
40
|
+
| `requires.cli_tools` | No | 必要な CLI ツール名のリスト |
|
|
41
|
+
|
|
42
|
+
`requires` を宣言すると、ワークフロー実行前にミニオンの環境と照合する事前チェック(readiness check)が行われる。
|
|
43
|
+
未宣言の場合は常に実行可能と見なされる。
|
|
44
|
+
|
|
30
45
|
### 2. HQ に反映する
|
|
31
46
|
|
|
32
47
|
```bash
|
package/linux/minion-cli.sh
CHANGED
|
@@ -308,19 +308,6 @@ do_setup() {
|
|
|
308
308
|
ENV_CONTENT+="MINION_USER=${TARGET_USER}\n"
|
|
309
309
|
ENV_CONTENT+="REFLECTION_TIME=03:00\n"
|
|
310
310
|
|
|
311
|
-
# Detect system timezone (IANA format)
|
|
312
|
-
local SYS_TZ=""
|
|
313
|
-
if command -v timedatectl &>/dev/null; then
|
|
314
|
-
SYS_TZ=$(timedatectl show --property=Timezone --value 2>/dev/null || true)
|
|
315
|
-
fi
|
|
316
|
-
if [ -z "$SYS_TZ" ] && [ -f /etc/timezone ]; then
|
|
317
|
-
SYS_TZ=$(cat /etc/timezone 2>/dev/null | tr -d '[:space:]')
|
|
318
|
-
fi
|
|
319
|
-
if [ -z "$SYS_TZ" ] && [ -L /etc/localtime ]; then
|
|
320
|
-
SYS_TZ=$(readlink /etc/localtime | sed 's|.*/zoneinfo/||')
|
|
321
|
-
fi
|
|
322
|
-
ENV_CONTENT+="TIMEZONE=${SYS_TZ:-Asia/Tokyo}\n"
|
|
323
|
-
|
|
324
311
|
echo -e "$ENV_CONTENT" | $SUDO tee /opt/minion-agent/.env > /dev/null
|
|
325
312
|
$SUDO chown "${TARGET_USER}:${TARGET_USER}" /opt/minion-agent/.env
|
|
326
313
|
echo " -> /opt/minion-agent/.env generated"
|
package/linux/routes/config.js
CHANGED
|
@@ -18,10 +18,10 @@ const { resolveEnvFilePath: resolveEnvFilePathFromPlatform } = require('../../co
|
|
|
18
18
|
const reflectionScheduler = require('../../core/lib/reflection-scheduler')
|
|
19
19
|
|
|
20
20
|
/** Keys that can be read/written via the config API */
|
|
21
|
-
const ALLOWED_ENV_KEYS = ['LLM_COMMAND', 'REFLECTION_TIME'
|
|
21
|
+
const ALLOWED_ENV_KEYS = ['LLM_COMMAND', 'REFLECTION_TIME']
|
|
22
22
|
|
|
23
23
|
/** Keys that trigger a reflection scheduler reschedule when changed */
|
|
24
|
-
const REFLECTION_KEYS = ['REFLECTION_TIME'
|
|
24
|
+
const REFLECTION_KEYS = ['REFLECTION_TIME']
|
|
25
25
|
|
|
26
26
|
const BACKUP_PATHS = [
|
|
27
27
|
'~/.claude',
|
package/linux/workflow-runner.js
CHANGED
|
@@ -277,6 +277,23 @@ async function runWorkflow(workflow, options = {}) {
|
|
|
277
277
|
log_file: logFile,
|
|
278
278
|
})
|
|
279
279
|
|
|
280
|
+
// Extract summary from execution log file (captured via tmux pipe-pane)
|
|
281
|
+
let summary = result.success
|
|
282
|
+
? `All skills completed successfully: ${pipelineSkillNames.join(', ')}`
|
|
283
|
+
: `Workflow failed: ${result.error || 'unknown error'}`
|
|
284
|
+
try {
|
|
285
|
+
const logData = await logManager.readLog(executionId, { tail: 200 })
|
|
286
|
+
if (logData && logData.content && logData.content.trim().length > 0) {
|
|
287
|
+
const MAX_SUMMARY_LENGTH = 10000
|
|
288
|
+
const content = logData.content.trim()
|
|
289
|
+
summary = content.length > MAX_SUMMARY_LENGTH
|
|
290
|
+
? content.slice(-MAX_SUMMARY_LENGTH)
|
|
291
|
+
: content
|
|
292
|
+
}
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.error(`[WorkflowRunner] Failed to read log for summary: ${err.message}`)
|
|
295
|
+
}
|
|
296
|
+
|
|
280
297
|
// Report outcome via local API (same data the execution-report skill used to send)
|
|
281
298
|
try {
|
|
282
299
|
const resp = await fetch(`http://localhost:${config.AGENT_PORT || 8080}/api/executions/${executionId}/outcome`, {
|
|
@@ -284,9 +301,7 @@ async function runWorkflow(workflow, options = {}) {
|
|
|
284
301
|
headers: { 'Content-Type': 'application/json' },
|
|
285
302
|
body: JSON.stringify({
|
|
286
303
|
outcome,
|
|
287
|
-
summary
|
|
288
|
-
? `All skills completed successfully: ${pipelineSkillNames.join(', ')}`
|
|
289
|
-
: `Workflow failed: ${result.error || 'unknown error'}`,
|
|
304
|
+
summary,
|
|
290
305
|
}),
|
|
291
306
|
})
|
|
292
307
|
if (!resp.ok) {
|
package/package.json
CHANGED
|
@@ -52,7 +52,7 @@ function detectProcessManager() {
|
|
|
52
52
|
* @param {string} npmInstallCmd - The npm install command to run
|
|
53
53
|
* @param {string} stopCmd - Command/script block to stop the agent
|
|
54
54
|
* @param {string} startCmd - Command/script block to start the agent
|
|
55
|
-
* @returns {string} -
|
|
55
|
+
* @returns {string} - Path to the generated update script (.ps1)
|
|
56
56
|
*/
|
|
57
57
|
function buildUpdateScript(npmInstallCmd, stopCmd, startCmd) {
|
|
58
58
|
const dataDir = path.join(os.homedir(), '.minion')
|
|
@@ -83,11 +83,13 @@ function buildUpdateScript(npmInstallCmd, stopCmd, startCmd) {
|
|
|
83
83
|
`}`,
|
|
84
84
|
].join('\n')
|
|
85
85
|
|
|
86
|
-
// Write the script to disk
|
|
86
|
+
// Write the script to disk
|
|
87
87
|
try { fs.mkdirSync(dataDir, { recursive: true }) } catch { /* exists */ }
|
|
88
88
|
fs.writeFileSync(scriptPath, ps1, 'utf-8')
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
// Return script path — caller uses spawn() with detached:true to launch it,
|
|
91
|
+
// avoiding cmd.exe quoting issues and -WindowStyle Hidden hangs in non-interactive sessions.
|
|
92
|
+
return scriptPath
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
/**
|
|
@@ -113,20 +115,20 @@ function buildAllowedCommands(procMgr) {
|
|
|
113
115
|
}
|
|
114
116
|
commands['update-agent'] = {
|
|
115
117
|
description: 'Update @geekbeer/minion to latest version and restart',
|
|
116
|
-
|
|
118
|
+
spawnArgs: ['powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', buildUpdateScript(
|
|
117
119
|
'npm install -g @geekbeer/minion@latest',
|
|
118
120
|
stopBlock,
|
|
119
121
|
startBlock,
|
|
120
|
-
),
|
|
122
|
+
)]],
|
|
121
123
|
deferred: true,
|
|
122
124
|
}
|
|
123
125
|
commands['update-agent-dev'] = {
|
|
124
126
|
description: 'Update @geekbeer/minion from Verdaccio (dev) and restart',
|
|
125
|
-
|
|
127
|
+
spawnArgs: ['powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', buildUpdateScript(
|
|
126
128
|
'npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873',
|
|
127
129
|
stopBlock,
|
|
128
130
|
startBlock,
|
|
129
|
-
),
|
|
131
|
+
)]],
|
|
130
132
|
deferred: true,
|
|
131
133
|
}
|
|
132
134
|
commands['status-services'] = {
|
|
@@ -141,20 +143,20 @@ function buildAllowedCommands(procMgr) {
|
|
|
141
143
|
}
|
|
142
144
|
commands['update-agent'] = {
|
|
143
145
|
description: 'Update @geekbeer/minion to latest version and restart',
|
|
144
|
-
|
|
146
|
+
spawnArgs: ['powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', buildUpdateScript(
|
|
145
147
|
'npm install -g @geekbeer/minion@latest',
|
|
146
148
|
'nssm stop minion-agent',
|
|
147
149
|
'nssm start minion-agent',
|
|
148
|
-
),
|
|
150
|
+
)]],
|
|
149
151
|
deferred: true,
|
|
150
152
|
}
|
|
151
153
|
commands['update-agent-dev'] = {
|
|
152
154
|
description: 'Update @geekbeer/minion from Verdaccio (dev) and restart',
|
|
153
|
-
|
|
155
|
+
spawnArgs: ['powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', buildUpdateScript(
|
|
154
156
|
'npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873',
|
|
155
157
|
'nssm stop minion-agent',
|
|
156
158
|
'nssm start minion-agent',
|
|
157
|
-
),
|
|
159
|
+
)]],
|
|
158
160
|
deferred: true,
|
|
159
161
|
}
|
|
160
162
|
commands['restart-display'] = {
|
|
@@ -173,20 +175,20 @@ function buildAllowedCommands(procMgr) {
|
|
|
173
175
|
}
|
|
174
176
|
commands['update-agent'] = {
|
|
175
177
|
description: 'Update @geekbeer/minion to latest version and restart',
|
|
176
|
-
|
|
178
|
+
spawnArgs: ['powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', buildUpdateScript(
|
|
177
179
|
'npm install -g @geekbeer/minion@latest',
|
|
178
180
|
'net stop minion-agent',
|
|
179
181
|
'net start minion-agent',
|
|
180
|
-
),
|
|
182
|
+
)]],
|
|
181
183
|
deferred: true,
|
|
182
184
|
}
|
|
183
185
|
commands['update-agent-dev'] = {
|
|
184
186
|
description: 'Update @geekbeer/minion from Verdaccio (dev) and restart',
|
|
185
|
-
|
|
187
|
+
spawnArgs: ['powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', buildUpdateScript(
|
|
186
188
|
'npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873',
|
|
187
189
|
'net stop minion-agent',
|
|
188
190
|
'net start minion-agent',
|
|
189
|
-
),
|
|
191
|
+
)]],
|
|
190
192
|
deferred: true,
|
|
191
193
|
}
|
|
192
194
|
commands['status-services'] = {
|
package/win/minion-cli.ps1
CHANGED
|
@@ -390,17 +390,10 @@ function Invoke-Setup {
|
|
|
390
390
|
Write-Step 4 $totalSteps "Creating config directory and .env..."
|
|
391
391
|
New-Item -Path $DataDir -ItemType Directory -Force | Out-Null
|
|
392
392
|
New-Item -Path $LogDir -ItemType Directory -Force | Out-Null
|
|
393
|
-
# Detect system timezone (IANA format)
|
|
394
|
-
$sysTz = 'Asia/Tokyo'
|
|
395
|
-
try {
|
|
396
|
-
$tzInfo = Get-TimeZone
|
|
397
|
-
if ($tzInfo.Id) { $sysTz = $tzInfo.Id }
|
|
398
|
-
} catch {}
|
|
399
393
|
$envValues = @{
|
|
400
394
|
'AGENT_PORT' = '8080'
|
|
401
395
|
'MINION_USER' = $env:USERNAME
|
|
402
396
|
'REFLECTION_TIME' = '03:00'
|
|
403
|
-
'TIMEZONE' = $sysTz
|
|
404
397
|
}
|
|
405
398
|
if ($HqUrl) { $envValues['HQ_URL'] = $HqUrl }
|
|
406
399
|
if ($ApiToken) { $envValues['API_TOKEN'] = $ApiToken }
|
package/win/routes/commands.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Same API as routes/commands.js but uses win/process-manager.js
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const { exec } = require('child_process')
|
|
7
|
+
const { exec, spawn } = require('child_process')
|
|
8
8
|
const { promisify } = require('util')
|
|
9
9
|
const execAsync = promisify(exec)
|
|
10
10
|
|
|
@@ -55,10 +55,28 @@ async function commandRoutes(fastify) {
|
|
|
55
55
|
if (allowedCommand.deferred) {
|
|
56
56
|
console.log(`[Command] Scheduling deferred command: ${command}`)
|
|
57
57
|
setTimeout(() => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
if (allowedCommand.spawnArgs) {
|
|
59
|
+
// Use spawn with detached:true for update scripts.
|
|
60
|
+
// Avoids cmd.exe quoting issues and -WindowStyle Hidden hangs
|
|
61
|
+
// in non-interactive sessions that caused HQ update timeouts.
|
|
62
|
+
const [cmd, args] = allowedCommand.spawnArgs
|
|
63
|
+
try {
|
|
64
|
+
const child = spawn(cmd, args, {
|
|
65
|
+
detached: true,
|
|
66
|
+
stdio: 'ignore',
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
})
|
|
69
|
+
child.unref()
|
|
70
|
+
console.log(`[Command] Deferred command spawned: ${command} (pid: ${child.pid})`)
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error(`[Command] Deferred spawn failed: ${command} - ${err.message}`)
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
exec(allowedCommand.command, { timeout: 60000, shell: true }, (err) => {
|
|
76
|
+
if (err) console.error(`[Command] Deferred command failed: ${command} - ${err.message}`)
|
|
77
|
+
else console.log(`[Command] Deferred command completed: ${command}`)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
62
80
|
}, 1000)
|
|
63
81
|
|
|
64
82
|
return {
|
package/win/routes/config.js
CHANGED
|
@@ -15,10 +15,10 @@ const { resolveEnvFilePath } = require('../../core/lib/platform')
|
|
|
15
15
|
|
|
16
16
|
const reflectionScheduler = require('../../core/lib/reflection-scheduler')
|
|
17
17
|
|
|
18
|
-
const ALLOWED_ENV_KEYS = ['LLM_COMMAND', 'REFLECTION_TIME'
|
|
18
|
+
const ALLOWED_ENV_KEYS = ['LLM_COMMAND', 'REFLECTION_TIME']
|
|
19
19
|
|
|
20
20
|
/** Keys that trigger a reflection scheduler reschedule when changed */
|
|
21
|
-
const REFLECTION_KEYS = ['REFLECTION_TIME'
|
|
21
|
+
const REFLECTION_KEYS = ['REFLECTION_TIME']
|
|
22
22
|
|
|
23
23
|
const BACKUP_PATHS = [
|
|
24
24
|
'~/.claude',
|
package/win/workflow-runner.js
CHANGED
|
@@ -183,12 +183,19 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
183
183
|
// Close log stream
|
|
184
184
|
try { logStream.end() } catch { /* ignore */ }
|
|
185
185
|
|
|
186
|
+
// Capture output for summary
|
|
187
|
+
const MAX_SUMMARY_LENGTH = 10000
|
|
188
|
+
const output = outputBuffer.trim()
|
|
189
|
+
const capturedOutput = output.length > MAX_SUMMARY_LENGTH
|
|
190
|
+
? output.slice(-MAX_SUMMARY_LENGTH)
|
|
191
|
+
: output
|
|
192
|
+
|
|
186
193
|
if (exitCode === 0) {
|
|
187
194
|
console.log(`[WorkflowRunner] Workflow ${workflow.name} completed successfully`)
|
|
188
|
-
resolve({ success: true, sessionName })
|
|
195
|
+
resolve({ success: true, sessionName, output: capturedOutput })
|
|
189
196
|
} else {
|
|
190
197
|
console.error(`[WorkflowRunner] Workflow ${workflow.name} failed with exit code: ${exitCode}`)
|
|
191
|
-
resolve({ success: false, error: `Exit code: ${exitCode}`, sessionName })
|
|
198
|
+
resolve({ success: false, error: `Exit code: ${exitCode}`, sessionName, output: capturedOutput })
|
|
192
199
|
}
|
|
193
200
|
})
|
|
194
201
|
})
|
|
@@ -261,6 +268,13 @@ async function runWorkflow(workflow, options = {}) {
|
|
|
261
268
|
log_file: logFile,
|
|
262
269
|
})
|
|
263
270
|
|
|
271
|
+
// Use captured output as summary, falling back to generic message
|
|
272
|
+
const summary = (result.output && result.output.length > 0)
|
|
273
|
+
? result.output
|
|
274
|
+
: result.success
|
|
275
|
+
? `All skills completed successfully: ${pipelineSkillNames.join(', ')}`
|
|
276
|
+
: `Workflow failed: ${result.error || 'unknown error'}`
|
|
277
|
+
|
|
264
278
|
// Report outcome via local API
|
|
265
279
|
try {
|
|
266
280
|
const resp = await fetch(`http://localhost:${config.AGENT_PORT || 8080}/api/executions/${executionId}/outcome`, {
|
|
@@ -268,9 +282,7 @@ async function runWorkflow(workflow, options = {}) {
|
|
|
268
282
|
headers: { 'Content-Type': 'application/json' },
|
|
269
283
|
body: JSON.stringify({
|
|
270
284
|
outcome,
|
|
271
|
-
summary
|
|
272
|
-
? `All skills completed successfully: ${pipelineSkillNames.join(', ')}`
|
|
273
|
-
: `Workflow failed: ${result.error || 'unknown error'}`,
|
|
285
|
+
summary,
|
|
274
286
|
}),
|
|
275
287
|
})
|
|
276
288
|
if (!resp.ok) {
|