@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/.env.example +0 -3
- package/README.md +0 -1
- package/core/api.js +13 -0
- package/core/config.js +46 -1
- package/core/lib/log-manager.js +4 -1
- package/core/lib/platform.js +8 -13
- package/core/lib/revision-watcher.js +252 -0
- package/core/lib/step-poller.js +222 -0
- package/core/lib/strip-ansi.js +18 -0
- package/core/lib/workflow-orchestrator.js +382 -0
- package/core/routes/diagnose.js +296 -0
- package/core/routes/health.js +27 -0
- package/core/routes/routines.js +15 -10
- package/core/routes/skills.js +4 -1
- package/core/routes/workflows.js +49 -2
- package/core/stores/chat-store.js +8 -1
- package/core/stores/routine-store.js +2 -2
- package/linux/lib/process-manager.js +14 -0
- package/linux/minion-cli.sh +57 -16
- package/linux/routes/chat.js +182 -20
- package/linux/routes/config.js +8 -12
- package/linux/routine-runner.js +5 -4
- package/linux/server.js +53 -1
- package/linux/workflow-runner.js +25 -61
- package/package.json +1 -1
- package/roles/pm.md +11 -12
- package/win/lib/process-manager.js +15 -0
- package/win/minion-cli.ps1 +122 -27
- package/win/routes/chat.js +178 -14
- package/win/routes/config.js +6 -2
- package/win/routine-runner.js +4 -2
- package/win/server.js +53 -0
- package/win/workflow-runner.js +31 -43
- package/skills/execution-report/SKILL.md +0 -106
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
|
}
|
package/win/workflow-runner.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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(' -> ')}
|
|
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
|
-
|
|
196
|
-
|
|
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(
|
|
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
|
|
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
|