@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 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 + session clear) at a configured time.
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
- * TIMEZONE - Timezone for the schedule (default: "Asia/Tokyo")
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 timezone = config.TIMEZONE || 'Asia/Tokyo'
80
+ const systemTz = getSystemTimezone()
67
81
 
68
82
  try {
69
- cronJob = new Cron(cronExpr, { timezone }, async () => {
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} (${timezone}), next run: ${nextRun ? nextRun.toISOString() : 'unknown'}`)
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 or TIMEZONE updated via API).
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: true,
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
  }
@@ -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(),
@@ -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<{filename: string, content: string}>} [opts.references] - Reference files
52
- * @returns {Promise<{path: string, references_count: number}>}
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, references = [] }) {
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 referencesDir = path.join(skillDir, 'references')
56
+ const filesDir = path.join(skillDir, 'files')
57
57
 
58
58
  await fs.mkdir(skillDir, { recursive: true })
59
- await fs.mkdir(referencesDir, { recursive: true })
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 reference files
76
- for (const ref of references) {
77
- if (ref.filename && ref.content) {
78
- const safeFilename = path.basename(ref.filename)
79
- await fs.writeFile(path.join(referencesDir, safeFilename), ref.content, 'utf-8')
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, references_count: references.length }
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 references
103
- const referencesDir = path.join(skillDir, 'references')
104
- const references = []
102
+ // Read skill files
103
+ const filesDir = path.join(skillDir, 'files')
104
+ const files = []
105
105
  try {
106
- const refEntries = await fs.readdir(referencesDir)
107
- for (const filename of refEntries) {
108
- const refContent = await fs.readFile(path.join(referencesDir, filename), 'utf-8')
109
- references.push({ filename, content: refContent })
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 directory
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
- references,
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
- references: skill.references || [],
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 and file size.
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({ date, size: stat.size })
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
  }
@@ -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` | (disabled) | Daily reflection time (e.g., `"03:00"`) |
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
- Example set reflection at 3:00 AM JST:
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`, `TIMEZONE`
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 |
@@ -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
@@ -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"
@@ -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', 'TIMEZONE']
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', 'TIMEZONE']
24
+ const REFLECTION_KEYS = ['REFLECTION_TIME']
25
25
 
26
26
  const BACKUP_PATHS = [
27
27
  '~/.claude',
@@ -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: result.success
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "2.48.3",
3
+ "version": "2.50.2",
4
4
  "description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
5
5
  "main": "linux/server.js",
6
6
  "bin": {
@@ -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} - Shell command that writes and launches the update script
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 and launch it detached
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
- return `powershell -ExecutionPolicy Bypass -Command "Start-Process powershell -ArgumentList '-ExecutionPolicy Bypass -WindowStyle Hidden -File \\"${scriptPath}\\"' -WindowStyle Hidden"`
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
- command: buildUpdateScript(
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
- command: buildUpdateScript(
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
- command: buildUpdateScript(
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
- command: buildUpdateScript(
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
- command: buildUpdateScript(
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
- command: buildUpdateScript(
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'] = {
@@ -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 }
@@ -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
- exec(allowedCommand.command, { timeout: 60000, shell: true }, (err) => {
59
- if (err) console.error(`[Command] Deferred command failed: ${command} - ${err.message}`)
60
- else console.log(`[Command] Deferred command completed: ${command}`)
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 {
@@ -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', 'TIMEZONE']
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', 'TIMEZONE']
21
+ const REFLECTION_KEYS = ['REFLECTION_TIME']
22
22
 
23
23
  const BACKUP_PATHS = [
24
24
  '~/.claude',
@@ -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: result.success
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) {