@geekbeer/minion 2.6.0 → 2.10.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/README.md CHANGED
@@ -37,6 +37,19 @@ minion-cli health # Run health check
37
37
  minion-cli log -m "Task completed" -l info -s skill-name
38
38
  ```
39
39
 
40
+ ### Issue Reporting
41
+
42
+ ```javascript
43
+ const { reportIssue } = require('@geekbeer/minion/api')
44
+
45
+ await reportIssue({
46
+ title: 'HQ APIで502エラーが発生する',
47
+ body: '## 状況\n...\n\n## 再現手順\n...',
48
+ labels: ['bug', 'critical']
49
+ })
50
+ // → { success: true, issue_url: '...', issue_number: 42 }
51
+ ```
52
+
40
53
  ## Environment Variables
41
54
 
42
55
  | Variable | Description | Default |
package/api.js CHANGED
@@ -50,7 +50,33 @@ async function reportExecution(data) {
50
50
  })
51
51
  }
52
52
 
53
+ /**
54
+ * Report a single workflow step completion to HQ.
55
+ * Called by the post-execution hook after a dispatched skill finishes.
56
+ * @param {object} data - { workflow_execution_id, step_index, status }
57
+ */
58
+ async function reportStepComplete(data) {
59
+ return request('/step-complete', {
60
+ method: 'POST',
61
+ body: JSON.stringify(data),
62
+ })
63
+ }
64
+
65
+ /**
66
+ * Create a GitHub Issue via HQ for bug reports or enhancement suggestions
67
+ * @param {object} data - { title: string, body: string, labels?: string[] }
68
+ * @returns {Promise<{ success: boolean, issue_url: string, issue_number: number }>}
69
+ */
70
+ async function reportIssue(data) {
71
+ return request('/report', {
72
+ method: 'POST',
73
+ body: JSON.stringify(data),
74
+ })
75
+ }
76
+
53
77
  module.exports = {
54
78
  request,
55
79
  reportExecution,
80
+ reportStepComplete,
81
+ reportIssue,
56
82
  }
package/config.js CHANGED
@@ -11,8 +11,32 @@
11
11
  *
12
12
  * Other optional environment variables:
13
13
  * - AGENT_PORT: Port for the local agent server (default: 3001)
14
+ * - MINION_USER: System user running the agent (used to resolve home directory)
14
15
  */
15
16
 
17
+ const os = require('os')
18
+ const { execSync } = require('child_process')
19
+
20
+ /**
21
+ * Resolve the correct home directory for the minion user.
22
+ * In supervisord environments, os.homedir() may return /root because
23
+ * the HOME env var is not inherited. This function uses MINION_USER
24
+ * (set in .env during setup) to look up the correct home via getent passwd.
25
+ */
26
+ function resolveHomeDir() {
27
+ const minionUser = process.env.MINION_USER
28
+ if (minionUser) {
29
+ try {
30
+ const entry = execSync(`getent passwd ${minionUser}`, { encoding: 'utf-8' }).trim()
31
+ const home = entry.split(':')[5]
32
+ if (home) return home
33
+ } catch {
34
+ // getent failed — fall back to os.homedir()
35
+ }
36
+ }
37
+ return os.homedir()
38
+ }
39
+
16
40
  const config = {
17
41
  // HQ Server Configuration (optional - omit for standalone mode)
18
42
  HQ_URL: process.env.HQ_URL || '',
@@ -21,6 +45,9 @@ const config = {
21
45
 
22
46
  // Server settings
23
47
  AGENT_PORT: parseInt(process.env.AGENT_PORT, 10) || 3001,
48
+
49
+ // Resolved home directory (safe for supervisord environments)
50
+ HOME_DIR: resolveHomeDir(),
24
51
  }
25
52
 
26
53
  /**
@@ -6,7 +6,8 @@
6
6
 
7
7
  const fs = require('fs').promises
8
8
  const path = require('path')
9
- const os = require('os')
9
+
10
+ const { config } = require('./config')
10
11
 
11
12
  // Max executions to keep (older ones are pruned)
12
13
  const MAX_EXECUTIONS = 200
@@ -21,7 +22,7 @@ function getExecutionFilePath() {
21
22
  require('fs').accessSync(path.dirname(optPath))
22
23
  return optPath
23
24
  } catch {
24
- return path.join(os.homedir(), 'executions.json')
25
+ return path.join(config.HOME_DIR, 'executions.json')
25
26
  }
26
27
  }
27
28
 
@@ -0,0 +1,114 @@
1
+ /**
2
+ * LLM Service authentication checker
3
+ *
4
+ * Detects whether supported LLM CLIs are authenticated and ready to use.
5
+ * CLIs are pre-installed on all minions; this module only checks auth status.
6
+ * Results are cached in memory for 60 seconds to avoid excessive filesystem checks.
7
+ */
8
+
9
+ const fs = require('fs')
10
+ const path = require('path')
11
+ const { config } = require('../config')
12
+
13
+ const CACHE_TTL_MS = 60000
14
+
15
+ let cachedResult = null
16
+ let cachedAt = 0
17
+
18
+ /**
19
+ * Check Claude Code authentication.
20
+ * Claude stores OAuth credentials in ~/.claude/.credentials.json
21
+ */
22
+ function isClaudeAuthenticated() {
23
+ const candidates = [
24
+ path.join(config.HOME_DIR, '.claude', '.credentials.json'),
25
+ path.join(config.HOME_DIR, '.claude', 'credentials.json'),
26
+ ]
27
+ for (const p of candidates) {
28
+ try {
29
+ if (fs.existsSync(p)) {
30
+ const content = fs.readFileSync(p, 'utf-8')
31
+ const parsed = JSON.parse(content)
32
+ if (parsed && Object.keys(parsed).length > 0) return true
33
+ }
34
+ } catch {
35
+ // Invalid JSON or read error — not authenticated
36
+ }
37
+ }
38
+ return false
39
+ }
40
+
41
+ /**
42
+ * Check Gemini CLI authentication.
43
+ * Gemini uses Google OAuth tokens or API key env vars.
44
+ */
45
+ function isGeminiAuthenticated() {
46
+ if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) return true
47
+
48
+ const possiblePaths = [
49
+ path.join(config.HOME_DIR, '.config', 'gemini'),
50
+ path.join(config.HOME_DIR, '.config', 'gcloud', 'application_default_credentials.json'),
51
+ ]
52
+ for (const p of possiblePaths) {
53
+ try {
54
+ if (!fs.existsSync(p)) continue
55
+ const stat = fs.statSync(p)
56
+ if (stat.isDirectory()) {
57
+ if (fs.readdirSync(p).length > 0) return true
58
+ } else {
59
+ if (fs.readFileSync(p, 'utf-8').trim().length > 0) return true
60
+ }
61
+ } catch {
62
+ // Ignore
63
+ }
64
+ }
65
+ return false
66
+ }
67
+
68
+ /**
69
+ * Check Codex (OpenAI) authentication.
70
+ * Codex CLI uses OPENAI_API_KEY or config in ~/.codex/
71
+ */
72
+ function isCodexAuthenticated() {
73
+ if (process.env.OPENAI_API_KEY) return true
74
+
75
+ const codexConfig = path.join(config.HOME_DIR, '.codex')
76
+ try {
77
+ if (fs.existsSync(codexConfig) && fs.statSync(codexConfig).isDirectory()) {
78
+ if (fs.readdirSync(codexConfig).length > 0) return true
79
+ }
80
+ } catch {
81
+ // Ignore
82
+ }
83
+ return false
84
+ }
85
+
86
+ const SERVICE_DEFINITIONS = [
87
+ { name: 'claude', display_name: 'Claude Code', check: isClaudeAuthenticated },
88
+ { name: 'gemini', display_name: 'Gemini CLI', check: isGeminiAuthenticated },
89
+ { name: 'codex', display_name: 'Codex', check: isCodexAuthenticated },
90
+ ]
91
+
92
+ /**
93
+ * Get authenticated LLM services (cached for 60s).
94
+ * Returns all services with their authentication status.
95
+ * @returns {{ name: string, display_name: string, authenticated: boolean }[]}
96
+ */
97
+ function getLlmServices() {
98
+ const now = Date.now()
99
+ if (cachedResult && (now - cachedAt) < CACHE_TTL_MS) {
100
+ return cachedResult
101
+ }
102
+
103
+ const services = SERVICE_DEFINITIONS.map(({ name, display_name, check }) => ({
104
+ name,
105
+ display_name,
106
+ authenticated: check(),
107
+ }))
108
+
109
+ cachedResult = services
110
+ cachedAt = now
111
+ return services
112
+ }
113
+
114
+ module.exports = { getLlmServices }
package/minion-cli.sh CHANGED
@@ -88,6 +88,17 @@ svc_control() {
88
88
 
89
89
  AGENT_URL="${MINION_AGENT_URL:-http://localhost:3001}"
90
90
 
91
+ # Auto-load .env so that API_TOKEN etc. are available in interactive shells
92
+ ENV_FILE="/opt/minion-agent/.env"
93
+ if [ -f "$ENV_FILE" ] && [ -r "$ENV_FILE" ]; then
94
+ while IFS='=' read -r key value; do
95
+ [[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
96
+ if [ -z "${!key:-}" ]; then
97
+ export "$key=$value"
98
+ fi
99
+ done < "$ENV_FILE"
100
+ fi
101
+
91
102
  # ============================================================
92
103
  # setup subcommand
93
104
  # ============================================================
@@ -405,10 +416,10 @@ After=xvfb.service
405
416
  Requires=xvfb.service
406
417
 
407
418
  [Service]
408
- Type=forking
419
+ Type=simple
409
420
  User=${TARGET_USER}
410
421
  Environment=DISPLAY=:99
411
- ExecStart=/usr/bin/autocutsel -fork
422
+ ExecStart=/usr/bin/autocutsel
412
423
  Restart=always
413
424
  RestartSec=5
414
425
 
@@ -487,8 +498,9 @@ NVNCEOF
487
498
 
488
499
  supervisord)
489
500
  # Build environment line from .env values
501
+ # Include HOME and DISPLAY since supervisord does not set them when switching user
490
502
  local ENV_LINE="environment="
491
- local ENV_PAIRS=()
503
+ local ENV_PAIRS=("HOME=\"${TARGET_HOME}\"" "DISPLAY=\":99\"")
492
504
  while IFS='=' read -r key value; do
493
505
  [[ -z "$key" || "$key" == \#* ]] && continue
494
506
  ENV_PAIRS+=("${key}=\"${value}\"")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "2.6.0",
3
+ "version": "2.10.2",
4
4
  "description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Directive endpoints
3
+ *
4
+ * Receives temp skill directives from HQ and executes them.
5
+ * Used for one-shot workflow orchestration via system-embedded skills.
6
+ *
7
+ * Endpoints:
8
+ * - POST /api/directive - Receive and execute a temp skill directive
9
+ */
10
+
11
+ const fs = require('fs').promises
12
+ const path = require('path')
13
+ const crypto = require('crypto')
14
+
15
+ const { verifyToken } = require('../lib/auth')
16
+ const { config } = require('../config')
17
+ const { writeSkillToLocal } = require('./skills')
18
+ const workflowRunner = require('../workflow-runner')
19
+ const executionStore = require('../execution-store')
20
+ const logManager = require('../lib/log-manager')
21
+
22
+ /**
23
+ * Parse frontmatter from skill content to extract body
24
+ * @param {string} content - Full skill content with frontmatter
25
+ * @returns {{ metadata: object, body: string }}
26
+ */
27
+ function parseFrontmatter(content) {
28
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
29
+ if (!match) return { metadata: {}, body: content }
30
+
31
+ const metadata = {}
32
+ for (const line of match[1].split('\n')) {
33
+ const [key, ...rest] = line.split(':')
34
+ if (key && rest.length) {
35
+ metadata[key.trim()] = rest.join(':').trim()
36
+ }
37
+ }
38
+ return { metadata, body: match[2].trimStart() }
39
+ }
40
+
41
+ /**
42
+ * Register directive routes as Fastify plugin
43
+ * @param {import('fastify').FastifyInstance} fastify
44
+ */
45
+ async function directiveRoutes(fastify) {
46
+ // Receive and execute a temp skill directive from HQ
47
+ fastify.post('/api/directive', async (request, reply) => {
48
+ if (!verifyToken(request)) {
49
+ reply.code(401)
50
+ return { success: false, error: 'Unauthorized' }
51
+ }
52
+
53
+ const { skill_name, skill_content, execution_id, context } = request.body || {}
54
+
55
+ if (!skill_name || !skill_content) {
56
+ reply.code(400)
57
+ return { success: false, error: 'skill_name and skill_content are required' }
58
+ }
59
+
60
+ // Validate temp skill name (must start with __)
61
+ if (!skill_name.startsWith('__')) {
62
+ reply.code(400)
63
+ return { success: false, error: 'Directive skill names must start with __' }
64
+ }
65
+
66
+ const effectiveExecutionId = execution_id || crypto.randomUUID()
67
+ const sessionName = `dir-${effectiveExecutionId.substring(0, 8)}-${effectiveExecutionId.substring(8, 12)}`
68
+
69
+ console.log(`[Directive] Received directive: ${skill_name} (execution: ${effectiveExecutionId})`)
70
+ console.log(`[Directive] Session: ${sessionName}`)
71
+
72
+ try {
73
+ // 1. Write temp skill to local filesystem
74
+ // The skill_content is the full content with frontmatter — write as-is
75
+ const homeDir = config.HOME_DIR
76
+ const skillDir = path.join(homeDir, '.claude', 'skills', skill_name)
77
+ await fs.mkdir(skillDir, { recursive: true })
78
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), skill_content, 'utf-8')
79
+
80
+ console.log(`[Directive] Temp skill written: ${skillDir}`)
81
+ } catch (err) {
82
+ console.error(`[Directive] Failed to write temp skill: ${err.message}`)
83
+ reply.code(500)
84
+ return { success: false, error: `Failed to write temp skill: ${err.message}` }
85
+ }
86
+
87
+ const startedAt = new Date().toISOString()
88
+ const logFile = logManager.getLogPath(effectiveExecutionId)
89
+ const workflowName = context?.workflow_name || skill_name
90
+
91
+ // Save initial execution record
92
+ await executionStore.save({
93
+ id: effectiveExecutionId,
94
+ skill_name,
95
+ workflow_id: null,
96
+ workflow_name: workflowName,
97
+ status: 'running',
98
+ outcome: null,
99
+ started_at: startedAt,
100
+ completed_at: null,
101
+ parent_execution_id: null,
102
+ error_message: null,
103
+ log_file: logFile,
104
+ })
105
+
106
+ // 2. Run async — respond immediately with 202
107
+ const executionPromise = (async () => {
108
+ const homeDir = config.HOME_DIR
109
+ const skillDir = path.join(homeDir, '.claude', 'skills', skill_name)
110
+
111
+ try {
112
+ // Execute as a single-skill workflow
113
+ // skipExecutionReport: the orchestration template has its own
114
+ // completion report (section 5), so /execution-report is redundant
115
+ // and would use the wrong (local) execution ID.
116
+ const result = await workflowRunner.runWorkflow({
117
+ id: effectiveExecutionId,
118
+ name: workflowName,
119
+ pipeline_skill_names: [skill_name],
120
+ }, { skipExecutionReport: true })
121
+
122
+ console.log(`[Directive] Execution completed: ${skill_name} (success: ${result.execution_id ? 'yes' : 'no'})`)
123
+ } catch (err) {
124
+ console.error(`[Directive] Execution failed: ${err.message}`)
125
+ await executionStore.save({
126
+ id: effectiveExecutionId,
127
+ skill_name,
128
+ workflow_id: null,
129
+ workflow_name: workflowName,
130
+ status: 'failed',
131
+ outcome: 'failure',
132
+ started_at: startedAt,
133
+ completed_at: new Date().toISOString(),
134
+ parent_execution_id: null,
135
+ error_message: err.message,
136
+ log_file: logFile,
137
+ })
138
+ } finally {
139
+ // 3. Cleanup temp skill directory
140
+ try {
141
+ await fs.rm(skillDir, { recursive: true, force: true })
142
+ console.log(`[Directive] Temp skill cleaned up: ${skillDir}`)
143
+ } catch (cleanupErr) {
144
+ console.error(`[Directive] Failed to cleanup temp skill: ${cleanupErr.message}`)
145
+ }
146
+ }
147
+ })()
148
+
149
+ executionPromise.catch(err => {
150
+ console.error(`[Directive] Unhandled error: ${err.message}`)
151
+ })
152
+
153
+ reply.code(202)
154
+ return {
155
+ success: true,
156
+ session_name: sessionName,
157
+ execution_id: effectiveExecutionId,
158
+ message: 'Directive accepted',
159
+ }
160
+ })
161
+ }
162
+
163
+ module.exports = { directiveRoutes }
package/routes/files.js CHANGED
@@ -14,13 +14,13 @@
14
14
  const fs = require('fs').promises
15
15
  const fsSync = require('fs')
16
16
  const path = require('path')
17
- const os = require('os')
18
17
  const { spawn } = require('child_process')
19
18
 
20
19
  const { verifyToken } = require('../lib/auth')
20
+ const { config } = require('../config')
21
21
 
22
22
  /** Base directory for file storage */
23
- const FILES_DIR = path.join(os.homedir(), 'files')
23
+ const FILES_DIR = path.join(config.HOME_DIR, 'files')
24
24
 
25
25
  /** Max upload size: 50MB */
26
26
  const MAX_UPLOAD_SIZE = 50 * 1024 * 1024
package/routes/health.js CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  const { version } = require('../package.json')
11
+ const { getLlmServices } = require('../lib/llm-checker')
11
12
 
12
13
  // Shared status state
13
14
  let currentStatus = 'online'
@@ -48,6 +49,7 @@ async function healthRoutes(fastify) {
48
49
  uptime: process.uptime(),
49
50
  version,
50
51
  timestamp: new Date().toISOString(),
52
+ llm_services: getLlmServices(),
51
53
  }
52
54
  })
53
55
 
package/routes/index.js CHANGED
@@ -48,6 +48,9 @@
48
48
  * GET /api/files/* - Download a file (auth required)
49
49
  * POST /api/files/* - Upload a file (auth required)
50
50
  * DELETE /api/files/* - Delete a file (auth required)
51
+ *
52
+ * Directives (routes/directives.js)
53
+ * POST /api/directive - Receive and execute a temp skill directive (auth required)
51
54
  * ─────────────────────────────────────────────────────────────────────────────
52
55
  */
53
56
 
@@ -58,6 +61,7 @@ const { workflowRoutes } = require('./workflows')
58
61
  const { routineRoutes } = require('./routines')
59
62
  const { terminalRoutes } = require('./terminal')
60
63
  const { fileRoutes } = require('./files')
64
+ const { directiveRoutes } = require('./directives')
61
65
 
62
66
  /**
63
67
  * Register all routes with Fastify instance
@@ -71,6 +75,7 @@ async function registerRoutes(fastify) {
71
75
  await fastify.register(routineRoutes)
72
76
  await fastify.register(terminalRoutes)
73
77
  await fastify.register(fileRoutes)
78
+ await fastify.register(directiveRoutes)
74
79
  }
75
80
 
76
81
  module.exports = {
package/routes/skills.js CHANGED
@@ -7,15 +7,19 @@
7
7
  * - POST /api/skills/push/:name - Push local skill to HQ
8
8
  * - POST /api/skills/fetch/:name - Fetch skill from HQ and deploy locally
9
9
  * - GET /api/skills/remote - List skills on HQ
10
+ * - POST /api/skills/run - Run a single deployed skill in a tmux session
10
11
  */
11
12
 
12
13
  const fs = require('fs').promises
13
14
  const path = require('path')
14
- const os = require('os')
15
+ const crypto = require('crypto')
15
16
 
16
17
  const { verifyToken } = require('../lib/auth')
17
18
  const api = require('../api')
18
- const { isHqConfigured } = require('../config')
19
+ const { config, isHqConfigured } = require('../config')
20
+ const workflowRunner = require('../workflow-runner')
21
+ const executionStore = require('../execution-store')
22
+ const logManager = require('../lib/log-manager')
19
23
 
20
24
  /**
21
25
  * Parse YAML frontmatter from SKILL.md content
@@ -49,7 +53,7 @@ function parseFrontmatter(content) {
49
53
  * @returns {Promise<{path: string, references_count: number}>}
50
54
  */
51
55
  async function writeSkillToLocal(name, { content, description, display_name, references = [] }) {
52
- const skillDir = path.join(os.homedir(), '.claude', 'skills', name)
56
+ const skillDir = path.join(config.HOME_DIR, '.claude', 'skills', name)
53
57
  const referencesDir = path.join(skillDir, 'references')
54
58
 
55
59
  await fs.mkdir(skillDir, { recursive: true })
@@ -88,7 +92,7 @@ async function writeSkillToLocal(name, { content, description, display_name, ref
88
92
  * @throws {Error} If skill not found locally or HQ rejects
89
93
  */
90
94
  async function pushSkillToHQ(name) {
91
- const homeDir = os.homedir()
95
+ const homeDir = config.HOME_DIR
92
96
  const skillDir = path.join(homeDir, '.claude', 'skills', name)
93
97
  const skillMdPath = path.join(skillDir, 'SKILL.md')
94
98
 
@@ -135,7 +139,7 @@ async function skillRoutes(fastify) {
135
139
  console.log('[Skills] Listing deployed skills')
136
140
 
137
141
  try {
138
- const homeDir = os.homedir()
142
+ const homeDir = config.HOME_DIR
139
143
  const skillsDir = path.join(homeDir, '.claude', 'skills')
140
144
 
141
145
  // Check if skills directory exists
@@ -276,6 +280,121 @@ async function skillRoutes(fastify) {
276
280
  }
277
281
  })
278
282
 
283
+ // Run a single deployed skill in a tmux session
284
+ // Called by HQ dispatch-step to execute a skill on this minion
285
+ fastify.post('/api/skills/run', async (request, reply) => {
286
+ if (!verifyToken(request)) {
287
+ reply.code(401)
288
+ return { success: false, error: 'Unauthorized' }
289
+ }
290
+
291
+ const { skill_name, execution_id, step_index, workflow_name } = request.body || {}
292
+
293
+ if (!skill_name) {
294
+ reply.code(400)
295
+ return { success: false, error: 'skill_name is required' }
296
+ }
297
+
298
+ // Verify skill exists locally
299
+ const homeDir = config.HOME_DIR
300
+ const skillMdPath = path.join(homeDir, '.claude', 'skills', skill_name, 'SKILL.md')
301
+ try {
302
+ await fs.access(skillMdPath)
303
+ } catch {
304
+ reply.code(404)
305
+ return { success: false, error: `Skill not found locally: ${skill_name}` }
306
+ }
307
+
308
+ const effectiveExecutionId = execution_id || crypto.randomUUID()
309
+ const stepLabel = step_index != null ? `-${step_index}` : ''
310
+ const sessionName = `step-${effectiveExecutionId.substring(0, 8)}${stepLabel}`
311
+
312
+ console.log(`[Skills] Running skill: ${skill_name} (session: ${sessionName})`)
313
+
314
+ // Build a synthetic workflow object for executeWorkflowSession-like execution
315
+ const skillNames = [skill_name]
316
+ const startedAt = new Date().toISOString()
317
+ const logFile = logManager.getLogPath(effectiveExecutionId)
318
+
319
+ // Save initial execution record
320
+ await executionStore.save({
321
+ id: effectiveExecutionId,
322
+ skill_name,
323
+ workflow_id: null,
324
+ workflow_name: workflow_name || null,
325
+ status: 'running',
326
+ outcome: null,
327
+ started_at: startedAt,
328
+ completed_at: null,
329
+ parent_execution_id: null,
330
+ error_message: null,
331
+ log_file: logFile,
332
+ })
333
+
334
+ // When dispatched as a workflow step, skip /execution-report in the prompt
335
+ // because the post-execution hook below handles completion reporting.
336
+ const isDispatchedStep = execution_id && step_index != null
337
+ const runOptions = isDispatchedStep ? { skipExecutionReport: true } : {}
338
+
339
+ // Run asynchronously — respond immediately
340
+ const executionPromise = (async () => {
341
+ let success = false
342
+ try {
343
+ const syntheticWorkflow = {
344
+ id: effectiveExecutionId,
345
+ name: workflow_name || skill_name,
346
+ }
347
+ const result = await workflowRunner.runWorkflow({
348
+ ...syntheticWorkflow,
349
+ pipeline_skill_names: skillNames,
350
+ }, runOptions)
351
+ success = !!result?.execution_id
352
+ return result
353
+ } catch (err) {
354
+ console.error(`[Skills] Run failed for ${skill_name}: ${err.message}`)
355
+ await executionStore.save({
356
+ id: effectiveExecutionId,
357
+ skill_name,
358
+ workflow_id: null,
359
+ workflow_name: workflow_name || null,
360
+ status: 'failed',
361
+ outcome: 'failure',
362
+ started_at: startedAt,
363
+ completed_at: new Date().toISOString(),
364
+ parent_execution_id: null,
365
+ error_message: err.message,
366
+ log_file: logFile,
367
+ })
368
+ } finally {
369
+ // Post-execution hook: report step completion to HQ
370
+ if (isDispatchedStep) {
371
+ try {
372
+ await api.reportStepComplete({
373
+ workflow_execution_id: execution_id,
374
+ step_index,
375
+ status: success ? 'completed' : 'failed',
376
+ })
377
+ console.log(`[Skills] Step completion reported: step ${step_index} → ${success ? 'completed' : 'failed'}`)
378
+ } catch (hookErr) {
379
+ console.error(`[Skills] Failed to report step completion: ${hookErr.message}`)
380
+ }
381
+ }
382
+ }
383
+ })()
384
+
385
+ executionPromise.catch(err => {
386
+ console.error(`[Skills] Unhandled error running ${skill_name}: ${err.message}`)
387
+ })
388
+
389
+ reply.code(202)
390
+ return {
391
+ success: true,
392
+ session_name: sessionName,
393
+ execution_id: effectiveExecutionId,
394
+ message: `Skill "${skill_name}" execution started`,
395
+ }
396
+ })
397
+
279
398
  // Delete a skill from local .claude/skills directory
280
399
  fastify.delete('/api/skills/:name', async (request, reply) => {
281
400
  if (!verifyToken(request)) {
@@ -299,7 +418,7 @@ async function skillRoutes(fastify) {
299
418
  console.log(`[Skills] Deleting skill: ${name}`)
300
419
 
301
420
  try {
302
- const homeDir = os.homedir()
421
+ const homeDir = config.HOME_DIR
303
422
  const skillDir = path.join(homeDir, '.claude', 'skills', name)
304
423
 
305
424
  // Check if skill exists
@@ -18,13 +18,13 @@ const { exec, spawn } = require('child_process')
18
18
  const { promisify } = require('util')
19
19
  const path = require('path')
20
20
  const net = require('net')
21
- const os = require('os')
22
21
  const execAsync = promisify(exec)
23
22
 
24
23
  const { verifyToken } = require('../lib/auth')
24
+ const { config } = require('../config')
25
25
 
26
26
  // Ensure consistent HOME for tmux socket path
27
- const homeDir = os.homedir()
27
+ const homeDir = config.HOME_DIR
28
28
 
29
29
  // ============================================================================
30
30
  // ttyd Process Management
@@ -18,7 +18,6 @@
18
18
 
19
19
  const fs = require('fs').promises
20
20
  const path = require('path')
21
- const os = require('os')
22
21
 
23
22
  const { verifyToken } = require('../lib/auth')
24
23
  const workflowRunner = require('../workflow-runner')
@@ -26,7 +25,7 @@ const workflowStore = require('../workflow-store')
26
25
  const executionStore = require('../execution-store')
27
26
  const logManager = require('../lib/log-manager')
28
27
  const api = require('../api')
29
- const { isHqConfigured } = require('../config')
28
+ const { config, isHqConfigured } = require('../config')
30
29
  const { writeSkillToLocal, pushSkillToHQ } = require('./skills')
31
30
 
32
31
  /**
@@ -214,7 +213,7 @@ async function workflowRoutes(fastify) {
214
213
 
215
214
  // 2. Fetch pipeline skills that are not deployed locally
216
215
  const fetchedSkills = []
217
- const homeDir = os.homedir()
216
+ const homeDir = config.HOME_DIR
218
217
 
219
218
  for (const skillName of workflow.pipeline_skill_names || []) {
220
219
  const skillMdPath = path.join(homeDir, '.claude', 'skills', skillName, 'SKILL.md')
package/routine-runner.js CHANGED
@@ -14,11 +14,11 @@ 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 os = require('os')
18
17
  const path = require('path')
19
18
  const fs = require('fs').promises
20
19
  const execAsync = promisify(exec)
21
20
 
21
+ const { config } = require('./config')
22
22
  const executionStore = require('./execution-store')
23
23
  const routineStore = require('./routine-store')
24
24
  const logManager = require('./lib/log-manager')
@@ -92,7 +92,7 @@ async function cleanupMarkerFile(sessionName) {
92
92
  * @returns {Promise<{success: boolean, error?: string, sessionName?: string}>}
93
93
  */
94
94
  async function executeRoutineSession(routine, executionId, skillNames) {
95
- const homeDir = os.homedir()
95
+ const homeDir = config.HOME_DIR
96
96
  const sessionName = generateSessionName(routine.id, executionId)
97
97
 
98
98
  // Build prompt: run each skill in sequence, then execution-report
package/routine-store.js CHANGED
@@ -6,7 +6,8 @@
6
6
 
7
7
  const fs = require('fs').promises
8
8
  const path = require('path')
9
- const os = require('os')
9
+
10
+ const { config } = require('./config')
10
11
 
11
12
  // Routine file location: /opt/minion-agent/routines.json (systemd/supervisord)
12
13
  // or ~/routines.json (standalone)
@@ -16,7 +17,7 @@ function getRoutineFilePath() {
16
17
  require('fs').accessSync(path.dirname(optPath))
17
18
  return optPath
18
19
  } catch {
19
- return path.join(os.homedir(), 'routines.json')
20
+ return path.join(config.HOME_DIR, 'routines.json')
20
21
  }
21
22
  }
22
23
 
package/rules/minion.md CHANGED
@@ -8,17 +8,45 @@ A local Agent API server runs at `http://localhost:3001` and a CLI tool `minion-
8
8
  ```
9
9
  Project (組織・課金単位)
10
10
  ├── Context (markdown, PMが更新)
11
- ├── Members (minion + role)
12
- └── Workflows (何をするか, スケジュールなし)
11
+ ├── Members (minion + role: pm | engineer)
12
+ └── Workflows (何をするか + オプションのcronスケジュール)
13
+ ├── Versions (pipeline の不変スナップショット)
14
+ └── Executions (実行履歴, ステップごとの進捗)
13
15
 
14
16
  Minion
15
- └── Routines (いつ・何を, cron付き)
17
+ └── Routines (いつ・何を, cron付き, ミニオンローカルのタスク)
16
18
  ```
17
19
 
18
- - **Workflow** はプロジェクトスコープ。「何をするか」を定義。スケジュールは持たない。
19
- - **Routine** はミニオンスコープ。「いつ・何を」を定義。cron_expression を持ち、自律トリガーする。
20
+ - **Workflow** はプロジェクトスコープ。バージョン管理されたパイプライン(スキル列)を持つ。オプションで `cron_expression` によるスケジュール実行が可能。HQ UIからワンショット実行もできる。
21
+ - **Routine** はミニオンスコープ。ミニオンローカルの定期タスク。cron_expression を持ち自律トリガーする。
20
22
  - ミニオンは複数プロジェクトに `pm` または `engineer` として参加できる。
21
- - ワークフローは直接トリガーされず、ルーティン経由で実行される(`project-workflow-check` スキル)。
23
+ - ワークフローの各ステップには `assigned_role`(pm/engineer)と `requires_review` フラグがある。
24
+
25
+ ## Workflow Execution Model (DB ステートマシン)
26
+
27
+ ワークフロー実行はHQのDBがステートマシンとして管理する。ミニオン間通信は不要。
28
+
29
+ ```
30
+ 1. Execution 作成 (HQ UIのRunボタン or cronスケジュール)
31
+ → workflow_execution (status='pending')
32
+ → workflow_step_executions (全ステップ status='pending')
33
+
34
+ 2. ミニオンエージェントが GET /api/minion/pending-steps をポーリング
35
+ → 自分のロールに該当 & 前ステップ完了済みのステップを取得
36
+
37
+ 3. ミニオンがステップを実行し完了を報告
38
+ → POST /api/minion/execution で status/outcome を更新
39
+
40
+ 4. 次ステップが eligible に → 別のミニオンが検知して実行
41
+ → requires_review=true の場合はHQでレビュー承認後に次へ
42
+
43
+ 5. 全ステップ完了 → execution 全体を completed に
44
+ ```
45
+
46
+ **ポイント:**
47
+ - HQの責務は「executionレコードを作る(指示書発行)」だけ
48
+ - 各ミニオンは自分の担当ステップだけをポーリングして実行
49
+ - ミニオンエージェント(Port 3001)が軽量HTTPポーリングを行い、pending検知時のみClaude Codeを起動(トークン節約)
22
50
 
23
51
  ## CLI Commands
24
52
 
@@ -66,13 +94,9 @@ Authentication: `Authorization: Bearer $API_TOKEN` header (except where noted).
66
94
  | POST | `/api/workflows/push/:name` | Push local workflow to HQ |
67
95
  | POST | `/api/workflows/fetch/:name` | Fetch workflow from HQ and deploy locally (+ pipeline skills) |
68
96
  | GET | `/api/workflows/remote` | List workflows on HQ |
69
- | PUT | `/api/workflows/:id/schedule` | **Legacy.** Update cron_expression/is_active. 新アーキではRoutine側で管理 |
70
97
  | DELETE | `/api/workflows/:id` | Remove a local workflow |
71
98
  | POST | `/api/workflows/trigger` | Manual trigger. Body: `{workflow_id}` |
72
99
 
73
- > **Note:** 新アーキテクチャでは Workflow にスケジュールがなく、Routine の cron で自律トリガーする設計。
74
- > `PUT /api/workflows/:id/schedule` はレガシー互換で残っているが、新規利用は非推奨。
75
-
76
100
  ### Executions
77
101
 
78
102
  | Method | Endpoint | Description |
@@ -156,20 +180,34 @@ Response:
156
180
 
157
181
  PUT body: `{ "content": "markdown string" }`
158
182
 
159
- ### Workflows (project-scoped)
183
+ ### Workflows (project-scoped, versioned)
160
184
 
161
185
  | Method | Endpoint | Description |
162
186
  |--------|----------|-------------|
163
- | GET | `/api/minion/workflows` | 参加プロジェクトのワークフロー一覧 |
187
+ | GET | `/api/minion/workflows` | 参加プロジェクトのアクティブなワークフロー一覧 |
188
+ | POST | `/api/minion/workflows` | ワークフローを push(新規作成 or 新バージョン) |
164
189
 
165
- Response:
190
+ GET Response:
166
191
  ```json
167
192
  {
168
193
  "workflows": [
169
194
  {
170
- "name": "workflow-name",
195
+ "name": "daily-check",
171
196
  "pipeline_skill_names": ["skill-1", "skill-2"],
197
+ "pipeline": [
198
+ {
199
+ "skill_version_id": "uuid",
200
+ "skill_name": "skill-1",
201
+ "skill_display_name": "Skill One",
202
+ "skill_version": 3,
203
+ "assigned_role": "engineer",
204
+ "requires_review": false,
205
+ "is_my_step": true
206
+ }
207
+ ],
172
208
  "content": "...",
209
+ "version": 3,
210
+ "cron_expression": "0 9 * * *",
173
211
  "project_id": "uuid",
174
212
  "created_at": "..."
175
213
  }
@@ -177,6 +215,122 @@ Response:
177
215
  }
178
216
  ```
179
217
 
218
+ 各ステップの `is_my_step` はミニオン自身のプロジェクトロールと `assigned_role` の一致を示す。
219
+
220
+ POST body (push):
221
+ ```json
222
+ {
223
+ "name": "my-workflow",
224
+ "pipeline_skill_names": ["skill-1", "skill-2"],
225
+ "content": "Workflow description",
226
+ "project_id": "uuid",
227
+ "change_summary": "Added skill-2 to pipeline"
228
+ }
229
+ ```
230
+
231
+ push するとパイプライン内のスキル名が `skill_version_id` に解決され、新バージョンが自動作成される。
232
+
233
+ ### Pending Steps (軽量ポーリング)
234
+
235
+ | Method | Endpoint | Description |
236
+ |--------|----------|-------------|
237
+ | GET | `/api/minion/pending-steps` | 自分が実行すべき pending ステップ一覧 |
238
+
239
+ **このエンドポイントはミニオンエージェント(非AI)による高頻度ポーリング用。**
240
+ Claude Code(AIスキル実行)はステップ検知時のみ起動する。
241
+
242
+ Response:
243
+ ```json
244
+ {
245
+ "steps": [
246
+ {
247
+ "step_execution_id": "uuid",
248
+ "execution_id": "uuid",
249
+ "workflow_name": "daily-check",
250
+ "step_index": 0,
251
+ "skill_version_id": "uuid",
252
+ "assigned_role": "engineer"
253
+ }
254
+ ]
255
+ }
256
+ ```
257
+
258
+ 返却条件:
259
+ - `assigned_role` がミニオンのプロジェクトロールと一致
260
+ - ステップの `status` が `pending`
261
+ - 前ステップが全て `completed`(`requires_review` の場合は `approved` も必要)
262
+
263
+ ### Workflow Execution Recording
264
+
265
+ | Method | Endpoint | Description |
266
+ |--------|----------|-------------|
267
+ | POST | `/api/minion/workflow-execution` | 新規 execution 開始を記録 |
268
+ | POST | `/api/minion/execution` | ステップ実行状況を HQ に報告 |
269
+
270
+ POST `/api/minion/workflow-execution` body:
271
+ ```json
272
+ {
273
+ "workflow_version_id": "uuid"
274
+ }
275
+ ```
276
+
277
+ Response:
278
+ ```json
279
+ {
280
+ "execution_id": "uuid",
281
+ "status": "running",
282
+ "steps_count": 3
283
+ }
284
+ ```
285
+
286
+ POST `/api/minion/execution` body:
287
+ ```json
288
+ {
289
+ "workflow_execution_id": "uuid",
290
+ "steps": [
291
+ {
292
+ "step_index": 0,
293
+ "skill_version_id": "uuid",
294
+ "assigned_role": "engineer",
295
+ "status": "completed",
296
+ "outcome": "success",
297
+ "started_at": "...",
298
+ "completed_at": "..."
299
+ }
300
+ ],
301
+ "status": "running",
302
+ "started_at": "..."
303
+ }
304
+ ```
305
+
306
+ ### Issue Reporting (GitHub Issue 起票)
307
+
308
+ | Method | Endpoint | Description |
309
+ |--------|----------|-------------|
310
+ | POST | `/api/minion/report` | バグや改善提案を GitHub Issue として起票 |
311
+
312
+ POST `/api/minion/report` body:
313
+ ```json
314
+ {
315
+ "title": "HQ APIで502エラーが発生する",
316
+ "body": "## 状況\n...\n\n## 再現手順\n...\n\n## エラー情報\n...\n\n## 環境\n...",
317
+ "labels": ["bug", "critical"]
318
+ }
319
+ ```
320
+
321
+ Response:
322
+ ```json
323
+ {
324
+ "success": true,
325
+ "issue_url": "https://github.com/owner/repo/issues/42",
326
+ "issue_number": 42
327
+ }
328
+ ```
329
+
330
+ 使用可能なラベル: `bug`, `enhancement`, `critical`, `minor`
331
+
332
+ **使い方:** サービスのバグや改善点を発見したら、このエンドポイントで GitHub Issue を起票する。報告者のミニオン名と ID が自動で本文に付記される。
333
+
180
334
  ### Routines (minion-scoped)
181
335
 
182
336
  | Method | Endpoint | Description |
@@ -206,14 +360,10 @@ Response:
206
360
  | Method | Endpoint | Description |
207
361
  |--------|----------|-------------|
208
362
  | GET | `/api/minion/skills` | HQ に登録されたスキル一覧 |
209
- | GET | `/api/minion/skills/[name]` | スキル詳細取得(content, references 含む) |
210
- | POST | `/api/minion/skills` | スキル登録/更新 |
363
+ | GET | `/api/minion/skills/[name]` | スキル詳細取得(content, files 含む) |
364
+ | POST | `/api/minion/skills` | スキル登録/更新(新バージョン自動作成) |
211
365
 
212
- ### Execution Reporting
213
-
214
- | Method | Endpoint | Description |
215
- |--------|----------|-------------|
216
- | POST | `/api/minion/execution` | 実行状況を HQ に報告 |
366
+ スキルはバージョン管理される。push ごとに新バージョンが作成され、ファイルは Supabase Storage に保存される。
217
367
 
218
368
  ## Environment Variables
219
369
 
@@ -254,27 +404,53 @@ When not configured, the agent runs in standalone mode and HQ-dependent features
254
404
 
255
405
  ## Workflow Structure
256
406
 
257
- Workflows are project-scoped and fetched from HQ via `GET /api/minion/workflows`.
258
- They are also stored locally in `workflows.json`. Each workflow object:
407
+ Workflows are project-scoped and versioned. Each version is an immutable snapshot of the pipeline.
408
+ Fetched from HQ via `GET /api/minion/workflows`. Also stored locally in `workflows.json`.
259
409
 
260
410
  ```json
261
411
  {
262
- "id": "uuid",
263
412
  "name": "my-workflow",
264
413
  "pipeline_skill_names": ["skill-1", "skill-2", "execution-report"],
414
+ "pipeline": [
415
+ {
416
+ "skill_version_id": "uuid",
417
+ "skill_name": "skill-1",
418
+ "skill_display_name": "Skill One",
419
+ "skill_version": 2,
420
+ "assigned_role": "engineer",
421
+ "requires_review": false,
422
+ "is_my_step": true
423
+ }
424
+ ],
265
425
  "content": "Markdown description of the workflow",
426
+ "version": 2,
427
+ "cron_expression": "0 9 * * *",
266
428
  "project_id": "uuid"
267
429
  }
268
430
  ```
269
431
 
270
432
  | Field | Type | Description |
271
433
  |-------|------|-------------|
272
- | `id` | string | UUID (auto-generated) |
273
434
  | `name` | string | Slug identifier (`/^[a-z0-9-]+$/`) |
274
- | `pipeline_skill_names` | string[] | Ordered skill names to execute |
435
+ | `pipeline_skill_names` | string[] | Ordered skill names (for display/push) |
436
+ | `pipeline` | PipelineStep[] | Resolved pipeline with version IDs and roles |
275
437
  | `content` | string | Markdown body describing the workflow |
438
+ | `version` | number | Current version number (auto-incremented on push) |
439
+ | `cron_expression` | string\|null | Cron schedule (null = manual/one-shot only) |
276
440
  | `project_id` | string | UUID of the parent project |
277
441
 
442
+ ### Pipeline Step Fields
443
+
444
+ | Field | Type | Description |
445
+ |-------|------|-------------|
446
+ | `skill_version_id` | string | UUID of the specific skill version |
447
+ | `skill_name` | string | Skill slug name |
448
+ | `skill_display_name` | string | Human-readable skill name |
449
+ | `skill_version` | number | Skill version number |
450
+ | `assigned_role` | string | `"pm"` or `"engineer"` — who executes this step |
451
+ | `requires_review` | boolean | If true, human review required after completion |
452
+ | `is_my_step` | boolean | Whether this minion's role matches assigned_role |
453
+
278
454
  ### Syncing workflows with HQ
279
455
 
280
456
  - `POST /api/workflows/push/:name` — Push local workflow to HQ. Pipeline skills are auto-pushed first.
@@ -320,9 +496,27 @@ Routines run on cron schedules. Each execution:
320
496
  3. Appends `execution-report` skill to report outcome
321
497
  4. Environment variables `MINION_EXECUTION_ID`, `MINION_ROUTINE_ID`, `MINION_ROUTINE_NAME` are available during execution
322
498
 
323
- ### Workflow Execution via Routine
499
+ ## Workflow Step Execution via Pending Steps
500
+
501
+ ミニオンエージェントは `GET /api/minion/pending-steps` を定期ポーリングし、
502
+ 自分の担当ステップを検知したら Claude Code を起動してスキルを実行する。
503
+
504
+ ```
505
+ ミニオンエージェント (Port 3001, 常駐, トークン不要)
506
+
507
+ ├── 軽量ポーリング: N秒ごとに GET /api/minion/pending-steps
508
+ │ → HTTPのみ、AIなし、コストゼロ
509
+ │ → pendingステップがなければ何もしない
510
+
511
+ └── ステップ検知時のみ:
512
+ 1. Claude Code 起動 → 該当スキルを実行
513
+ 2. POST /api/minion/execution → ステップ完了を報告
514
+ 3. Claude Code 終了 → トークン消費はここだけ
515
+ ```
516
+
517
+ ### Workflow Execution via Routine (従来方式)
324
518
 
325
- ワークフローは `project-workflow-check` システムスキルを通じてルーティンから実行される:
519
+ ワークフローは `project-workflow-check` システムスキルを通じてルーティンからも実行可能:
326
520
 
327
521
  ```
328
522
  ルーティン: "morning-work" (cron: 0 9 * * 1-5)
package/server.js CHANGED
@@ -7,7 +7,6 @@
7
7
 
8
8
  const fs = require('fs')
9
9
  const path = require('path')
10
- const os = require('os')
11
10
 
12
11
  const fastify = require('fastify')({ logger: true })
13
12
  const { config, validate, isHqConfigured } = require('./config')
@@ -56,7 +55,7 @@ process.on('SIGINT', () => shutdown('SIGINT'))
56
55
  */
57
56
  function syncPermissions() {
58
57
  const bundledPath = path.join(__dirname, 'settings', 'permissions.json')
59
- const settingsDir = path.join(os.homedir(), '.claude')
58
+ const settingsDir = path.join(config.HOME_DIR, '.claude')
60
59
  const settingsPath = path.join(settingsDir, 'settings.json')
61
60
 
62
61
  try {
@@ -90,7 +89,7 @@ function syncPermissions() {
90
89
  */
91
90
  function syncTmuxConfig() {
92
91
  const bundledPath = path.join(__dirname, 'settings', 'tmux.conf')
93
- const destPath = path.join(os.homedir(), '.tmux.conf')
92
+ const destPath = path.join(config.HOME_DIR, '.tmux.conf')
94
93
 
95
94
  try {
96
95
  if (!fs.existsSync(bundledPath)) return
@@ -108,7 +107,7 @@ function syncTmuxConfig() {
108
107
  */
109
108
  function syncBundledRules() {
110
109
  const bundledRulesDir = path.join(__dirname, 'rules')
111
- const targetRulesDir = path.join(os.homedir(), '.claude', 'rules')
110
+ const targetRulesDir = path.join(config.HOME_DIR, '.claude', 'rules')
112
111
 
113
112
  try {
114
113
  if (!fs.existsSync(bundledRulesDir)) return
@@ -4,6 +4,8 @@
4
4
  "Read",
5
5
  "Write",
6
6
  "Edit",
7
+ "WebSearch",
8
+ "WebFetch",
7
9
  "mcp__playwright__*"
8
10
  ],
9
11
  "deny": [
@@ -14,11 +14,11 @@ 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 os = require('os')
18
17
  const path = require('path')
19
18
  const fs = require('fs').promises
20
19
  const execAsync = promisify(exec)
21
20
 
21
+ const { config } = require('./config')
22
22
  const executionStore = require('./execution-store')
23
23
  const workflowStore = require('./workflow-store')
24
24
  const logManager = require('./lib/log-manager')
@@ -90,13 +90,17 @@ async function cleanupMarkerFile(sessionName) {
90
90
  * @param {object} workflow - Workflow configuration
91
91
  * @returns {Promise<{success: boolean, error?: string, sessionName?: string}>}
92
92
  */
93
- async function executeWorkflowSession(workflow, executionId, skillNames) {
94
- const homeDir = os.homedir()
93
+ async function executeWorkflowSession(workflow, executionId, skillNames, options = {}) {
94
+ const homeDir = config.HOME_DIR
95
95
  const sessionName = generateSessionName(workflow.id, executionId)
96
96
 
97
- // Build prompt: run each skill in sequence, then execution-report
97
+ // Build prompt: run each skill in sequence
98
+ // When skipExecutionReport is true (dispatched step), the minion server's
99
+ // post-execution hook handles completion reporting instead of /execution-report.
98
100
  const skillCommands = skillNames.map(name => `/${name}`).join(', then ')
99
- const prompt = `Run the following skills in order: ${skillCommands}. After completing all skills, run /execution-report to report the results.`
101
+ const prompt = options.skipExecutionReport
102
+ ? `Run the following skills in order: ${skillCommands}.`
103
+ : `Run the following skills in order: ${skillCommands}. After completing all skills, run /execution-report to report the results.`
100
104
 
101
105
  // Extend PATH to include common CLI installation locations
102
106
  const additionalPaths = [
@@ -187,11 +191,10 @@ async function executeWorkflowSession(workflow, executionId, skillNames) {
187
191
 
188
192
  while (Date.now() - startTime < timeout) {
189
193
  try {
190
- await execAsync(`tmux has-session -t "${sessionName}" 2>/dev/null`)
191
- await sleep(pollInterval)
194
+ await fs.access(exitCodeFile)
195
+ break // Exit code file exists — claude -p has finished
192
196
  } catch {
193
- // Session ended
194
- break
197
+ await sleep(pollInterval)
195
198
  }
196
199
  }
197
200
 
@@ -245,7 +248,7 @@ async function saveExecution(executionData) {
245
248
  * @param {object} workflow - Workflow configuration
246
249
  * @returns {Promise<{execution_id: string, session_name: string}>}
247
250
  */
248
- async function runWorkflow(workflow) {
251
+ async function runWorkflow(workflow, options = {}) {
249
252
  const pipelineSkillNames = workflow.pipeline_skill_names || []
250
253
 
251
254
  if (pipelineSkillNames.length === 0) {
@@ -285,7 +288,7 @@ async function runWorkflow(workflow) {
285
288
  })
286
289
 
287
290
  // Execute all skills in one session
288
- const result = await executeWorkflowSession(workflow, executionId, pipelineSkillNames)
291
+ const result = await executeWorkflowSession(workflow, executionId, pipelineSkillNames, options)
289
292
 
290
293
  const completedAt = new Date().toISOString()
291
294
  console.log(`[WorkflowRunner] executeWorkflowSession returned: success=${result.success}, error=${result.error || 'none'}`)
package/workflow-store.js CHANGED
@@ -6,7 +6,8 @@
6
6
 
7
7
  const fs = require('fs').promises
8
8
  const path = require('path')
9
- const os = require('os')
9
+
10
+ const { config } = require('./config')
10
11
 
11
12
  // Workflow file location: /opt/minion-agent/workflows.json (systemd/supervisord)
12
13
  // or ~/workflows.json (standalone)
@@ -17,7 +18,7 @@ function getWorkflowFilePath() {
17
18
  require('fs').accessSync(path.dirname(optPath))
18
19
  return optPath
19
20
  } catch {
20
- return path.join(os.homedir(), 'workflows.json')
21
+ return path.join(config.HOME_DIR, 'workflows.json')
21
22
  }
22
23
  }
23
24