@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 +13 -0
- package/api.js +26 -0
- package/config.js +27 -0
- package/execution-store.js +3 -2
- package/lib/llm-checker.js +114 -0
- package/minion-cli.sh +15 -3
- package/package.json +1 -1
- package/routes/directives.js +163 -0
- package/routes/files.js +2 -2
- package/routes/health.js +2 -0
- package/routes/index.js +5 -0
- package/routes/skills.js +125 -6
- package/routes/terminal.js +2 -2
- package/routes/workflows.js +2 -3
- package/routine-runner.js +2 -2
- package/routine-store.js +3 -2
- package/rules/minion.md +222 -28
- package/server.js +3 -4
- package/settings/permissions.json +2 -0
- package/workflow-runner.js +14 -11
- package/workflow-store.js +3 -2
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
|
/**
|
package/execution-store.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs').promises
|
|
8
8
|
const path = require('path')
|
|
9
|
-
|
|
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(
|
|
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=
|
|
419
|
+
Type=simple
|
|
409
420
|
User=${TARGET_USER}
|
|
410
421
|
Environment=DISPLAY=:99
|
|
411
|
-
ExecStart=/usr/bin/autocutsel
|
|
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
|
@@ -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(
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
421
|
+
const homeDir = config.HOME_DIR
|
|
303
422
|
const skillDir = path.join(homeDir, '.claude', 'skills', name)
|
|
304
423
|
|
|
305
424
|
// Check if skill exists
|
package/routes/terminal.js
CHANGED
|
@@ -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 =
|
|
27
|
+
const homeDir = config.HOME_DIR
|
|
28
28
|
|
|
29
29
|
// ============================================================================
|
|
30
30
|
// ttyd Process Management
|
package/routes/workflows.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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**
|
|
20
|
+
- **Workflow** はプロジェクトスコープ。バージョン管理されたパイプライン(スキル列)を持つ。オプションで `cron_expression` によるスケジュール実行が可能。HQ UIからワンショット実行もできる。
|
|
21
|
+
- **Routine** はミニオンスコープ。ミニオンローカルの定期タスク。cron_expression を持ち自律トリガーする。
|
|
20
22
|
- ミニオンは複数プロジェクトに `pm` または `engineer` として参加できる。
|
|
21
|
-
-
|
|
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": "
|
|
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,
|
|
210
|
-
| POST | `/api/minion/skills` |
|
|
363
|
+
| GET | `/api/minion/skills/[name]` | スキル詳細取得(content, files 含む) |
|
|
364
|
+
| POST | `/api/minion/skills` | スキル登録/更新(新バージョン自動作成) |
|
|
211
365
|
|
|
212
|
-
|
|
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
|
|
258
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
110
|
+
const targetRulesDir = path.join(config.HOME_DIR, '.claude', 'rules')
|
|
112
111
|
|
|
113
112
|
try {
|
|
114
113
|
if (!fs.existsSync(bundledRulesDir)) return
|
package/workflow-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 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 =
|
|
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
|
|
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 =
|
|
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
|
|
191
|
-
|
|
194
|
+
await fs.access(exitCodeFile)
|
|
195
|
+
break // Exit code file exists — claude -p has finished
|
|
192
196
|
} catch {
|
|
193
|
-
|
|
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
|
-
|
|
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(
|
|
21
|
+
return path.join(config.HOME_DIR, 'workflows.json')
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|