@geekbeer/minion 2.33.4 → 2.42.5

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/win/server.js CHANGED
@@ -14,12 +14,22 @@ const fastify = require('fastify')({ logger: true })
14
14
 
15
15
  // Compatible modules (reused from Linux)
16
16
  const { config, validate, isHqConfigured } = require('../core/config')
17
+ const { sendHeartbeat } = require('../core/api')
18
+ const { version } = require('../package.json')
17
19
  const workflowStore = require('../core/stores/workflow-store')
18
20
  const routineStore = require('../core/stores/routine-store')
19
21
 
22
+ // Heartbeat interval: fixed at 30s (not user-configurable)
23
+ const HEARTBEAT_INTERVAL_MS = 30_000
24
+ let heartbeatTimer = null
25
+
20
26
  // Windows-specific modules
21
27
  const workflowRunner = require('./workflow-runner')
22
28
  const routineRunner = require('./routine-runner')
29
+
30
+ // Pull-model daemons (from core/)
31
+ const stepPoller = require('../core/lib/step-poller')
32
+ const revisionWatcher = require('../core/lib/revision-watcher')
23
33
  const { commandRoutes, getProcessManager, getAllowedCommands } = require('./routes/commands')
24
34
  const { terminalRoutes, cleanupSessions } = require('./routes/terminal')
25
35
  const { startTerminalServer, stopTerminalServer } = require('./terminal-server')
@@ -30,6 +40,7 @@ const { configRoutes } = require('./routes/config')
30
40
 
31
41
  // Compatible route modules (reused from Linux)
32
42
  const { healthRoutes, setOffline } = require('../core/routes/health')
43
+ const { diagnoseRoutes } = require('../core/routes/diagnose')
33
44
  const { skillRoutes } = require('../core/routes/skills')
34
45
  const { workflowRoutes } = require('../core/routes/workflows')
35
46
  const { routineRoutes } = require('../core/routes/routines')
@@ -47,6 +58,27 @@ console.log(`[Config] Available commands: ${Object.keys(ALLOWED_COMMANDS).join('
47
58
  async function shutdown(signal) {
48
59
  console.log(`[Server] Received ${signal}, shutting down...`)
49
60
  setOffline()
61
+
62
+ // Stop heartbeat timer
63
+ if (heartbeatTimer) {
64
+ clearInterval(heartbeatTimer)
65
+ heartbeatTimer = null
66
+ }
67
+
68
+ // Send offline heartbeat to HQ (best-effort, don't block shutdown)
69
+ if (isHqConfigured()) {
70
+ try {
71
+ await Promise.race([
72
+ sendHeartbeat({ status: 'offline', version }),
73
+ new Promise(resolve => setTimeout(resolve, 3000)),
74
+ ])
75
+ } catch {
76
+ // Best-effort — don't block shutdown
77
+ }
78
+ }
79
+
80
+ stepPoller.stop()
81
+ revisionWatcher.stop()
50
82
  workflowRunner.stopAll()
51
83
  routineRunner.stopAll()
52
84
  stopTerminalServer()
@@ -155,6 +187,7 @@ function syncBundledDocs() {
155
187
  async function registerRoutes(app) {
156
188
  // Shared routes (from core/) - inject runners via opts
157
189
  await app.register(healthRoutes)
190
+ await app.register(diagnoseRoutes)
158
191
  await app.register(skillRoutes, { workflowRunner })
159
192
  await app.register(workflowRoutes, { workflowRunner })
160
193
  await app.register(routineRoutes, { routineRunner })
@@ -218,6 +251,26 @@ async function start() {
218
251
 
219
252
  if (isHqConfigured()) {
220
253
  console.log(`[Server] HQ URL: ${config.HQ_URL}`)
254
+
255
+ // Send initial online heartbeat
256
+ const { getStatus } = require('../core/routes/health')
257
+ const { currentTask } = getStatus()
258
+ sendHeartbeat({ status: 'online', current_task: currentTask, version }).catch(err => {
259
+ console.error('[Heartbeat] Initial heartbeat failed:', err.message)
260
+ })
261
+
262
+ // Start periodic heartbeat
263
+ heartbeatTimer = setInterval(() => {
264
+ const { currentStatus, currentTask } = getStatus()
265
+ sendHeartbeat({ status: currentStatus, current_task: currentTask, version }).catch(err => {
266
+ console.error('[Heartbeat] Periodic heartbeat failed:', err.message)
267
+ })
268
+ }, HEARTBEAT_INTERVAL_MS)
269
+ console.log(`[Heartbeat] Sending every ${HEARTBEAT_INTERVAL_MS / 1000}s`)
270
+
271
+ // Start Pull-model daemons
272
+ stepPoller.start()
273
+ revisionWatcher.start()
221
274
  } else {
222
275
  console.log('[Server] Running in standalone mode (no HQ connection)')
223
276
  }
@@ -9,11 +9,13 @@
9
9
  * - Exit codes captured via onExit callback (no file polling)
10
10
  * - Output logged via onData handler (replaces tmux pipe-pane)
11
11
  * - Sessions tracked in-memory via activeSessions Map
12
+ * - Outcome (success/failure) is determined by CLI exit code and recorded automatically
12
13
  */
13
14
 
14
15
  const { Cron } = require('croner')
15
16
  const crypto = require('crypto')
16
17
  const path = require('path')
18
+ const { stripAnsi } = require('../core/lib/strip-ansi')
17
19
  const fs = require('fs').promises
18
20
  const fsSync = require('fs')
19
21
 
@@ -21,7 +23,7 @@ const { config } = require('../core/config')
21
23
  const executionStore = require('../core/stores/execution-store')
22
24
  const workflowStore = require('../core/stores/workflow-store')
23
25
  const logManager = require('../core/lib/log-manager')
24
- const { MARKER_DIR, buildExtendedPath } = require('../core/lib/platform')
26
+ const { buildExtendedPath } = require('../core/lib/platform')
25
27
 
26
28
  // Active cron jobs keyed by workflow ID
27
29
  const activeJobs = new Map()
@@ -46,25 +48,6 @@ function generateSessionName(workflowId, executionId) {
46
48
  return execShort ? `wf-${workflowShort}-${execShort}` : `wf-${workflowShort}`
47
49
  }
48
50
 
49
- async function writeMarkerFile(sessionName, data) {
50
- try {
51
- await fs.mkdir(MARKER_DIR, { recursive: true })
52
- const filePath = path.join(MARKER_DIR, `${sessionName}.json`)
53
- await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
54
- console.log(`[WorkflowRunner] Wrote marker file: ${filePath}`)
55
- } catch (err) {
56
- console.error(`[WorkflowRunner] Failed to write marker file: ${err.message}`)
57
- }
58
- }
59
-
60
- async function cleanupMarkerFile(sessionName) {
61
- try {
62
- const filePath = path.join(MARKER_DIR, `${sessionName}.json`)
63
- await fs.unlink(filePath)
64
- console.log(`[WorkflowRunner] Cleaned up marker file: ${filePath}`)
65
- } catch { /* ignore */ }
66
- }
67
-
68
51
  /**
69
52
  * Load node-pty dynamically (it's an optionalDependency).
70
53
  * @returns {object} The node-pty module
@@ -100,15 +83,13 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
100
83
  ? `## Revision Feedback\nThe reviewer requested changes to your previous output. Address the following feedback:\n${options.revisionFeedback}\n\n`
101
84
  : ''
102
85
 
103
- const prompt = options.skipExecutionReport
104
- ? `${rolePrefix}${revisionContext}Run the following skills in order: ${skillCommands}.`
105
- : `${rolePrefix}${revisionContext}Run the following skills in order: ${skillCommands}. After completing all skills, run /execution-report to report the results.`
86
+ const prompt = `${rolePrefix}${revisionContext}Run the following skills in order: ${skillCommands}.`
106
87
 
107
88
  const extendedPath = buildExtendedPath(homeDir)
108
89
  const logFile = logManager.getLogPath(executionId)
109
90
 
110
91
  console.log(`[WorkflowRunner] Executing workflow: ${workflow.name}`)
111
- console.log(`[WorkflowRunner] Skills: ${skillNames.join(' -> ')} -> execution-report`)
92
+ console.log(`[WorkflowRunner] Skills: ${skillNames.join(' -> ')}`)
112
93
  console.log(`[WorkflowRunner] Session: ${sessionName}`)
113
94
  console.log(`[WorkflowRunner] Log file: ${logFile}`)
114
95
  console.log(`[WorkflowRunner] HOME: ${homeDir}`)
@@ -124,15 +105,6 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
124
105
  activeSessions.delete(sessionName)
125
106
  }
126
107
 
127
- // Write marker file BEFORE starting session
128
- await writeMarkerFile(sessionName, {
129
- execution_id: executionId,
130
- workflow_id: workflow.id,
131
- workflow_name: workflow.name,
132
- skill_names: skillNames,
133
- started_at: new Date().toISOString(),
134
- })
135
-
136
108
  // Build the LLM command
137
109
  if (!config.LLM_COMMAND) {
138
110
  throw new Error('LLM_COMMAND is not configured. Set LLM_COMMAND in minion.env')
@@ -146,9 +118,6 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
146
118
  HOME: homeDir,
147
119
  USERPROFILE: homeDir,
148
120
  PATH: extendedPath,
149
- MINION_EXECUTION_ID: executionId,
150
- MINION_WORKFLOW_ID: workflow.id,
151
- MINION_WORKFLOW_NAME: workflow.name,
152
121
  }
153
122
 
154
123
  // Open log file for streaming writes
@@ -192,14 +161,15 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
192
161
  }, timeout)
193
162
 
194
163
  ptyProcess.onData((data) => {
195
- outputBuffer += data
196
- session.buffer += data
164
+ const cleaned = stripAnsi(data)
165
+ outputBuffer += cleaned
166
+ session.buffer += cleaned
197
167
  // Cap buffer at 1MB to prevent memory issues
198
168
  if (session.buffer.length > 1024 * 1024) {
199
169
  session.buffer = session.buffer.slice(-512 * 1024)
200
170
  }
201
171
  // Write to log file
202
- try { logStream.write(data) } catch { /* ignore */ }
172
+ try { logStream.write(cleaned) } catch { /* ignore */ }
203
173
  })
204
174
 
205
175
  ptyProcess.onExit(({ exitCode }) => {
@@ -222,8 +192,6 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
222
192
  } catch (error) {
223
193
  console.error(`[WorkflowRunner] Workflow ${workflow.name} failed: ${error.message}`)
224
194
  return { success: false, error: error.message, sessionName }
225
- } finally {
226
- await cleanupMarkerFile(sessionName)
227
195
  }
228
196
  }
229
197
 
@@ -273,14 +241,16 @@ async function runWorkflow(workflow, options = {}) {
273
241
 
274
242
  const result = await executeWorkflowSession(workflow, executionId, pipelineSkillNames, options)
275
243
  const completedAt = new Date().toISOString()
244
+ const outcome = result.success ? 'success' : 'failure'
276
245
 
246
+ // Save execution with outcome determined by CLI exit code
277
247
  await saveExecution({
278
248
  id: executionId,
279
249
  skill_name: pipelineSkillNames.join(' -> '),
280
250
  workflow_id: workflow.id,
281
251
  workflow_name: workflow.name,
282
252
  status: result.success ? 'completed' : 'failed',
283
- outcome: result.success ? null : 'failure',
253
+ outcome,
284
254
  started_at: startedAt,
285
255
  completed_at: completedAt,
286
256
  parent_execution_id: null,
@@ -288,6 +258,25 @@ async function runWorkflow(workflow, options = {}) {
288
258
  log_file: logFile,
289
259
  })
290
260
 
261
+ // Report outcome via local API
262
+ try {
263
+ const resp = await fetch(`http://localhost:${config.AGENT_PORT || 8080}/api/executions/${executionId}/outcome`, {
264
+ method: 'POST',
265
+ headers: { 'Content-Type': 'application/json' },
266
+ body: JSON.stringify({
267
+ outcome,
268
+ summary: result.success
269
+ ? `All skills completed successfully: ${pipelineSkillNames.join(', ')}`
270
+ : `Workflow failed: ${result.error || 'unknown error'}`,
271
+ }),
272
+ })
273
+ if (!resp.ok) {
274
+ console.error(`[WorkflowRunner] Failed to report outcome: ${resp.status}`)
275
+ }
276
+ } catch (err) {
277
+ console.error(`[WorkflowRunner] Failed to report outcome: ${err.message}`)
278
+ }
279
+
291
280
  await workflowStore.updateLastRun(workflow.id)
292
281
 
293
282
  runningExecutions.delete(executionId)
@@ -376,5 +365,4 @@ module.exports = {
376
365
  getWorkflowById,
377
366
  generateSessionName,
378
367
  activeSessions,
379
- MARKER_DIR,
380
368
  }
@@ -1,106 +0,0 @@
1
- ---
2
- name: execution-report
3
- description: Report workflow execution outcome to minion agent
4
- ---
5
-
6
- # Execution Report Skill
7
-
8
- ## Overview
9
-
10
- This skill reports the outcome of a workflow execution to the minion agent. It is **automatically called at the end of every workflow** and should not be manually added to workflow pipelines.
11
-
12
- ## How It Works
13
-
14
- 1. Read the marker file to get execution metadata
15
- 2. Analyze the results of all skills executed in this session
16
- 3. Report the outcome to the minion API
17
-
18
- ## Instructions
19
-
20
- ### Step 1: Get Execution Context
21
-
22
- The execution context is provided via environment variables:
23
-
24
- ```bash
25
- # Get execution context from environment
26
- EXECUTION_ID="${MINION_EXECUTION_ID}"
27
- WORKFLOW_ID="${MINION_WORKFLOW_ID}"
28
- WORKFLOW_NAME="${MINION_WORKFLOW_NAME}"
29
- ```
30
-
31
- If the environment variables are not set, check the marker file as fallback:
32
-
33
- ```bash
34
- # Fallback: Read from marker file
35
- if [ -z "$EXECUTION_ID" ]; then
36
- SESSION_NAME=$(tmux display-message -p '#S' 2>/dev/null)
37
- MARKER_FILE="/tmp/minion-executions/${SESSION_NAME}.json"
38
- if [ -f "$MARKER_FILE" ]; then
39
- EXECUTION_ID=$(cat "$MARKER_FILE" | grep -o '"execution_id": *"[^"]*"' | cut -d'"' -f4)
40
- fi
41
- fi
42
- ```
43
-
44
- **Expected values:**
45
- - `EXECUTION_ID`: UUID of the current execution
46
- - `WORKFLOW_ID`: UUID of the workflow
47
- - `WORKFLOW_NAME`: Human-readable workflow name
48
-
49
- ### Step 2: Analyze Execution Results
50
-
51
- Review what happened during this session:
52
-
53
- - **outcome**: Choose one of:
54
- - `success` - All skills completed successfully
55
- - `failure` - Critical error occurred, objectives not met
56
- - `partial` - Some skills succeeded, some failed or were skipped
57
-
58
- - **summary**: One-line summary of results (max 50 chars)
59
-
60
- - **details**: Detailed report in markdown format
61
-
62
- ### Step 3: Report Outcome
63
-
64
- Use the EXECUTION_ID to report results:
65
-
66
- ```bash
67
- curl -s -X POST "http://localhost:8080/api/executions/${EXECUTION_ID}/outcome" \
68
- -H "Content-Type: application/json" \
69
- -d '{
70
- "outcome": "success|failure|partial",
71
- "summary": "Brief summary here",
72
- "details": "## Detailed Report\n\n- Action 1\n- Action 2\n..."
73
- }'
74
- ```
75
-
76
- ## Report Format (details field)
77
-
78
- ```markdown
79
- ## Execution Summary
80
- - Workflow: {workflow_name}
81
- - Skills: {skill_names}
82
- - Outcome: {outcome}
83
-
84
- ## Actions Taken
85
- - [List of actions performed]
86
-
87
- ## Artifacts
88
- - [Files created, messages sent, etc.]
89
-
90
- ## Issues & Notes
91
- - [Errors or unexpected events]
92
- - [Notes for future runs]
93
- ```
94
-
95
- ## No Execution Context?
96
-
97
- If `MINION_EXECUTION_ID` is not set and the marker file doesn't exist, this is likely a manual execution. In this case:
98
- 1. Log a warning: "No execution context found. Treating as manual execution."
99
- 2. Skip the outcome API call (no execution record to update)
100
-
101
- ## Important Notes
102
-
103
- - This skill is automatically appended to every workflow
104
- - Do NOT add it to workflow pipelines manually
105
- - Execution context is provided via `MINION_EXECUTION_ID`, `MINION_WORKFLOW_ID`, and `MINION_WORKFLOW_NAME` environment variables
106
- - The marker file is kept as a fallback and is cleaned up automatically after workflow completion