@geekbeer/minion 3.57.0 → 3.59.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/meetings/meeting-mcp-server.js +244 -0
- package/core/meetings/meeting-prompt.js +49 -0
- package/core/routes/meetings.js +111 -0
- package/docs/api-reference.md +71 -2
- package/docs/task-guides.md +28 -0
- package/linux/meeting-runner.js +215 -0
- package/linux/server.js +6 -0
- package/package.json +1 -1
- package/roles/engineer.md +3 -1
- package/roles/pm.md +6 -2
- package/rules/core.md +50 -10
- package/win/meeting-runner.js +165 -0
- package/win/server.js +5 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Meeting MCP server (stdio).
|
|
4
|
+
*
|
|
5
|
+
* Spawned by meeting-runner per meeting. Bridges the in-tmux claude process
|
|
6
|
+
* to the HQ meeting endpoints. Exposes four tools:
|
|
7
|
+
*
|
|
8
|
+
* meeting_wait_for_next_message(timeout_ms?)
|
|
9
|
+
* Long-polls HQ for new messages since the last call. Returns either
|
|
10
|
+
* { messages: [...], history_count } or { meeting_ended: true }.
|
|
11
|
+
*
|
|
12
|
+
* meeting_speak(text)
|
|
13
|
+
* Posts a message to the meeting from this minion.
|
|
14
|
+
*
|
|
15
|
+
* meeting_get_state()
|
|
16
|
+
* Returns meeting metadata + participant list.
|
|
17
|
+
*
|
|
18
|
+
* meeting_leave()
|
|
19
|
+
* Records left_at for this minion. Returns { ok: true }; claude should
|
|
20
|
+
* exit its participation loop after this call.
|
|
21
|
+
*
|
|
22
|
+
* Environment (passed by meeting-runner):
|
|
23
|
+
* MINION_MEETING_ID - meeting UUID
|
|
24
|
+
* HQ_URL - HQ server URL
|
|
25
|
+
* API_TOKEN - minion's API token (Bearer)
|
|
26
|
+
*
|
|
27
|
+
* Protocol: JSON-RPC 2.0 over stdio.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const readline = require('readline')
|
|
31
|
+
|
|
32
|
+
const HQ_URL = process.env.HQ_URL
|
|
33
|
+
const API_TOKEN = process.env.API_TOKEN
|
|
34
|
+
const MEETING_ID = process.env.MINION_MEETING_ID
|
|
35
|
+
|
|
36
|
+
if (!HQ_URL || !API_TOKEN || !MEETING_ID) {
|
|
37
|
+
process.stderr.write('[meeting-mcp] Missing required env: HQ_URL / API_TOKEN / MINION_MEETING_ID\n')
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let lastSeenAt = null
|
|
42
|
+
|
|
43
|
+
async function hqFetch(path, options = {}) {
|
|
44
|
+
const url = `${HQ_URL}${path}`
|
|
45
|
+
const resp = await fetch(url, {
|
|
46
|
+
...options,
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
Authorization: `Bearer ${API_TOKEN}`,
|
|
50
|
+
...(options.headers || {}),
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
let data = null
|
|
54
|
+
try {
|
|
55
|
+
data = await resp.json()
|
|
56
|
+
} catch {
|
|
57
|
+
data = {}
|
|
58
|
+
}
|
|
59
|
+
return { resp, data }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function waitForNextMessage() {
|
|
63
|
+
const params = new URLSearchParams({ wait: 'true' })
|
|
64
|
+
if (lastSeenAt) params.set('after', lastSeenAt)
|
|
65
|
+
const { resp, data } = await hqFetch(
|
|
66
|
+
`/api/minion/meetings/${MEETING_ID}/messages?${params}`,
|
|
67
|
+
)
|
|
68
|
+
if (resp.status === 404 || data.meeting_ended) {
|
|
69
|
+
return { meeting_ended: true }
|
|
70
|
+
}
|
|
71
|
+
const messages = data.messages || []
|
|
72
|
+
if (messages.length > 0) {
|
|
73
|
+
lastSeenAt = messages[messages.length - 1].created_at
|
|
74
|
+
}
|
|
75
|
+
return { messages, history_count: messages.length }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function speak(text) {
|
|
79
|
+
const { resp, data } = await hqFetch(
|
|
80
|
+
`/api/minion/meetings/${MEETING_ID}/messages`,
|
|
81
|
+
{ method: 'POST', body: JSON.stringify({ text }) },
|
|
82
|
+
)
|
|
83
|
+
if (!resp.ok) {
|
|
84
|
+
throw new Error(data.error || `speak failed: ${resp.status}`)
|
|
85
|
+
}
|
|
86
|
+
return { message_id: data.message?.id }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function getState() {
|
|
90
|
+
const { resp, data } = await hqFetch(`/api/minion/meetings/${MEETING_ID}`)
|
|
91
|
+
if (resp.status === 404) {
|
|
92
|
+
return { meeting_ended: true }
|
|
93
|
+
}
|
|
94
|
+
if (!resp.ok) {
|
|
95
|
+
throw new Error(data.error || `state failed: ${resp.status}`)
|
|
96
|
+
}
|
|
97
|
+
return data
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function leave() {
|
|
101
|
+
await hqFetch(`/api/minion/meetings/${MEETING_ID}`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
body: JSON.stringify({ action: 'leave' }),
|
|
104
|
+
})
|
|
105
|
+
return { ok: true }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Tool definitions --------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
function toolsList() {
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
name: 'meeting_wait_for_next_message',
|
|
114
|
+
description:
|
|
115
|
+
'Long-poll for the next message in the meeting (up to ~25s). ' +
|
|
116
|
+
'Returns { messages: [...], history_count } when new messages arrive, ' +
|
|
117
|
+
'or { meeting_ended: true } if the meeting has been ended by the host. ' +
|
|
118
|
+
'Use this in a loop to listen to the meeting.',
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'meeting_speak',
|
|
126
|
+
description:
|
|
127
|
+
'Say something in the meeting. Your message will be visible to all participants. ' +
|
|
128
|
+
'Keep it conversational and short (1-3 sentences) unless the discussion requires depth.',
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {
|
|
132
|
+
text: { type: 'string', description: 'What you want to say.' },
|
|
133
|
+
},
|
|
134
|
+
required: ['text'],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'meeting_get_state',
|
|
139
|
+
description:
|
|
140
|
+
'Get meeting metadata (title, purpose, participants). Useful at session start.',
|
|
141
|
+
inputSchema: { type: 'object', properties: {} },
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'meeting_leave',
|
|
145
|
+
description:
|
|
146
|
+
'Leave the meeting. Call this when the meeting is over or you have been ' +
|
|
147
|
+
'explicitly excused. After calling this, exit your participation loop.',
|
|
148
|
+
inputSchema: { type: 'object', properties: {} },
|
|
149
|
+
},
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function handleToolCall(name, args) {
|
|
154
|
+
switch (name) {
|
|
155
|
+
case 'meeting_wait_for_next_message':
|
|
156
|
+
return waitForNextMessage()
|
|
157
|
+
case 'meeting_speak':
|
|
158
|
+
return speak(String(args.text || ''))
|
|
159
|
+
case 'meeting_get_state':
|
|
160
|
+
return getState()
|
|
161
|
+
case 'meeting_leave':
|
|
162
|
+
return leave()
|
|
163
|
+
default:
|
|
164
|
+
throw new Error(`Unknown tool: ${name}`)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --- JSON-RPC transport ------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
function send(msg) {
|
|
171
|
+
process.stdout.write(JSON.stringify(msg) + '\n')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function sendResult(id, result) {
|
|
175
|
+
send({ jsonrpc: '2.0', id, result })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sendError(id, code, message) {
|
|
179
|
+
send({ jsonrpc: '2.0', id, error: { code, message } })
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function dispatch(msg) {
|
|
183
|
+
const { id, method, params } = msg
|
|
184
|
+
try {
|
|
185
|
+
switch (method) {
|
|
186
|
+
case 'initialize':
|
|
187
|
+
sendResult(id, {
|
|
188
|
+
protocolVersion: '2024-11-05',
|
|
189
|
+
serverInfo: { name: 'minion-meeting', version: '1.0.0' },
|
|
190
|
+
capabilities: { tools: {} },
|
|
191
|
+
})
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
case 'notifications/initialized':
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
case 'tools/list':
|
|
198
|
+
sendResult(id, { tools: toolsList() })
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
case 'tools/call': {
|
|
202
|
+
const { name, arguments: args } = params || {}
|
|
203
|
+
const result = await handleToolCall(name, args || {})
|
|
204
|
+
sendResult(id, {
|
|
205
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
206
|
+
isError: false,
|
|
207
|
+
})
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
case 'ping':
|
|
212
|
+
sendResult(id, {})
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
default:
|
|
216
|
+
if (id !== undefined) sendError(id, -32601, `Method not found: ${method}`)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if (id !== undefined) sendError(id, -32000, err.message || String(err))
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function start() {
|
|
225
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false })
|
|
226
|
+
rl.on('line', (line) => {
|
|
227
|
+
const trimmed = line.trim()
|
|
228
|
+
if (!trimmed) return
|
|
229
|
+
let msg
|
|
230
|
+
try {
|
|
231
|
+
msg = JSON.parse(trimmed)
|
|
232
|
+
} catch {
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
dispatch(msg)
|
|
236
|
+
})
|
|
237
|
+
rl.on('close', () => process.exit(0))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (require.main === module) {
|
|
241
|
+
start()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = { start, toolsList, handleToolCall }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the kickoff prompt injected into the claude process when joining
|
|
3
|
+
* a meeting. The prompt:
|
|
4
|
+
* 1. Explains the meeting context (title, purpose, participants).
|
|
5
|
+
* 2. Instructs claude to use the meeting MCP tools to participate.
|
|
6
|
+
* 3. Hands control to claude to run its own participation loop.
|
|
7
|
+
*
|
|
8
|
+
* Output behavior (sprint plan, retro notes, etc.) is intentionally NOT
|
|
9
|
+
* hardcoded here — that lives in role-specific guidance (PM core.md etc.)
|
|
10
|
+
* so different meeting purposes can produce different artifacts.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function buildMeetingKickoffPrompt({ meetingId, title, purpose, host, selfName, role }) {
|
|
14
|
+
const lines = [
|
|
15
|
+
`[ミーティング参加] [meeting:${meetingId}]`,
|
|
16
|
+
`あなたは「${selfName}」として、進行中のミーティングに招待されました。`,
|
|
17
|
+
'',
|
|
18
|
+
'## ミーティング情報',
|
|
19
|
+
`- タイトル: ${title}`,
|
|
20
|
+
purpose ? `- 目的: ${purpose}` : null,
|
|
21
|
+
host ? `- ホスト: ${host}` : null,
|
|
22
|
+
role ? `- あなたの役割: ${role}` : null,
|
|
23
|
+
'',
|
|
24
|
+
'## 参加方法',
|
|
25
|
+
'meeting MCP ツールを使って参加してください。基本ループ:',
|
|
26
|
+
'',
|
|
27
|
+
'1. `meeting_get_state` でミーティングの状態と参加者を確認',
|
|
28
|
+
'2. `meeting_wait_for_next_message` で次の発言を待つ (long-poll, 最大25秒)',
|
|
29
|
+
'3. 受け取ったメッセージに対して、応答すべきか判断する',
|
|
30
|
+
' - 自分の役割・コンテキストに関連する話題なら応答する',
|
|
31
|
+
' - 直前の発言が他の参加者宛 (mention) なら基本的にスルー',
|
|
32
|
+
' - 重複・冗長な発言は避ける (他のミニオンが既に同趣旨を発言していたら追従しない)',
|
|
33
|
+
'4. 応答する場合は `meeting_speak` で 1-3 文の短い発言を送る',
|
|
34
|
+
'5. `meeting_wait_for_next_message` の戻り値が `{ meeting_ended: true }` になったら',
|
|
35
|
+
' `meeting_leave` を呼んでループを終了する',
|
|
36
|
+
'',
|
|
37
|
+
'## 発言ガイドライン',
|
|
38
|
+
'- 1回の発言は **2-3 文以内** を基本とする (会議でだらだら話さない)',
|
|
39
|
+
'- 議論を前進させる発言を優先する (賛否・確認・代替案・質問)',
|
|
40
|
+
'- 既存のあなたのタスク・プロジェクト状況を知っていれば、それを踏まえて発言する',
|
|
41
|
+
'- 役割に応じた成果物がある場合 (PM=議事録/sprint計画 等) は、',
|
|
42
|
+
' あなたの core ルールに従って終了前に作成する',
|
|
43
|
+
'',
|
|
44
|
+
'では `meeting_get_state` から開始してください。',
|
|
45
|
+
].filter(Boolean)
|
|
46
|
+
return lines.join('\n')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { buildMeetingKickoffPrompt }
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meetings routes (experimental)
|
|
3
|
+
*
|
|
4
|
+
* Receives meeting lifecycle pushes from HQ:
|
|
5
|
+
* POST /api/meetings/invitations — HQ invites this minion to a meeting
|
|
6
|
+
* DELETE /api/meetings/:id — HQ notifies the meeting has ended
|
|
7
|
+
* GET /api/meetings/active — Debug: list currently-active sessions
|
|
8
|
+
*
|
|
9
|
+
* The actual participation (waiting for messages, speaking) happens inside
|
|
10
|
+
* the spawned tmux session via the meeting MCP server — HQ never controls
|
|
11
|
+
* the minion's thoughts, it just opens and closes the meeting room.
|
|
12
|
+
*
|
|
13
|
+
* Platform-specific runner is dependency-injected via fastify opts so the
|
|
14
|
+
* same route module works on Linux (tmux) and Windows (WSL).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { verifyToken } = require('../lib/auth')
|
|
18
|
+
|
|
19
|
+
async function meetingRoutes(fastify, opts) {
|
|
20
|
+
const meetingRunner = opts.meetingRunner
|
|
21
|
+
if (!meetingRunner) {
|
|
22
|
+
throw new Error('meetingRoutes requires { meetingRunner } in opts')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* POST /api/meetings/invitations
|
|
27
|
+
* Body: { meeting_id, title, purpose, hq_url, host }
|
|
28
|
+
* Spawns a tmux session that runs claude with the meeting MCP server.
|
|
29
|
+
*/
|
|
30
|
+
fastify.post('/api/meetings/invitations', async (request, reply) => {
|
|
31
|
+
if (!verifyToken(request)) {
|
|
32
|
+
reply.code(401)
|
|
33
|
+
return { success: false, error: 'Unauthorized' }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const body = request.body || {}
|
|
37
|
+
const { meeting_id, title, purpose, hq_url, host, self_name, role } = body
|
|
38
|
+
if (!meeting_id || !title || !hq_url) {
|
|
39
|
+
reply.code(400)
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
error: 'meeting_id, title, and hq_url are required',
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const result = await meetingRunner.runMeeting({
|
|
48
|
+
meetingId: meeting_id,
|
|
49
|
+
title,
|
|
50
|
+
purpose: purpose || null,
|
|
51
|
+
host: host || 'host',
|
|
52
|
+
hqUrl: hq_url,
|
|
53
|
+
selfName: self_name || null,
|
|
54
|
+
role: role || null,
|
|
55
|
+
})
|
|
56
|
+
if (!result.success) {
|
|
57
|
+
reply.code(500)
|
|
58
|
+
return { success: false, error: result.error || 'Failed to start meeting session' }
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
success: true,
|
|
62
|
+
session_name: result.sessionName,
|
|
63
|
+
started: result.started,
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(`[Meetings] Failed to start meeting: ${err.message}`)
|
|
67
|
+
reply.code(500)
|
|
68
|
+
return { success: false, error: err.message }
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* DELETE /api/meetings/:id
|
|
74
|
+
* HQ notifies that the meeting has been ended/deleted. Kill the tmux session.
|
|
75
|
+
*/
|
|
76
|
+
fastify.delete('/api/meetings/:id', async (request, reply) => {
|
|
77
|
+
if (!verifyToken(request)) {
|
|
78
|
+
reply.code(401)
|
|
79
|
+
return { success: false, error: 'Unauthorized' }
|
|
80
|
+
}
|
|
81
|
+
const meetingId = request.params.id
|
|
82
|
+
try {
|
|
83
|
+
const killed = await meetingRunner.killMeetingSession(meetingId)
|
|
84
|
+
return { success: true, killed }
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error(`[Meetings] Failed to kill meeting ${meetingId}: ${err.message}`)
|
|
87
|
+
reply.code(500)
|
|
88
|
+
return { success: false, error: err.message }
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* GET /api/meetings/active
|
|
94
|
+
* Debug: list active meeting tmux sessions.
|
|
95
|
+
*/
|
|
96
|
+
fastify.get('/api/meetings/active', async (request, reply) => {
|
|
97
|
+
if (!verifyToken(request)) {
|
|
98
|
+
reply.code(401)
|
|
99
|
+
return { success: false, error: 'Unauthorized' }
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const sessions = await meetingRunner.listMeetingSessions()
|
|
103
|
+
return { success: true, sessions }
|
|
104
|
+
} catch (err) {
|
|
105
|
+
reply.code(500)
|
|
106
|
+
return { success: false, error: err.message }
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { meetingRoutes }
|
package/docs/api-reference.md
CHANGED
|
@@ -504,7 +504,7 @@ POST `/api/threads` body (プロジェクト紐づきディスカッション):
|
|
|
504
504
|
| `thread_type` | string | No | `help`(デフォルト)or `discussion` |
|
|
505
505
|
| `title` | string | Yes | スレッドの要約 |
|
|
506
506
|
| `content` | string | Yes | スレッド本文(thread_messagesの最初のメッセージとして保存) |
|
|
507
|
-
| `mentions` | string[] | No | メンション対象。形式: `role:engineer`, `role:pm`, `minion:<minion_id>`, `user` |
|
|
507
|
+
| `mentions` | string[] | No | メンション対象。形式: `user:<auth_user_id>` (個別指名・推奨), `role:engineer`, `role:pm`, `role:accountant`, `minion:<minion_id>`, `user` (誰でも良い場合のフォールバック) |
|
|
508
508
|
| `context` | object | No | 任意のメタデータ(category, urgency, dag_execution_id等) |
|
|
509
509
|
|
|
510
510
|
**プロジェクト紐づけの使い分け:**
|
|
@@ -520,7 +520,11 @@ POST `/api/threads` body (プロジェクト紐づきディスカッション):
|
|
|
520
520
|
- メンションされたミニオンは優先的にスレッドを評価する
|
|
521
521
|
- メンションがない場合、全チームメンバーがLLMで関連性を判定してから参加
|
|
522
522
|
- トークン消費を抑えるため、当事者が明確な場合はメンションを推奨
|
|
523
|
-
|
|
523
|
+
|
|
524
|
+
**人間へのメンション (重要):**
|
|
525
|
+
1. **個別指名 `user:<auth_user_id>`** を最優先で使う。`/api/minion/me/project/:id/members` または `/api/minion/workspaces/:id/members` で人間メンバーの `user_id` を引いてから指定する。同名 `display_name` が複数いるワークスペースでも誤通知しない
|
|
526
|
+
2. **`role:pm` / `role:engineer` / `role:accountant`** はプロジェクトの該当ロール全員に届く(プロジェクト紐づきスレッドのみ)
|
|
527
|
+
3. **`user` (generic)** はプロジェクトメンバーの人間全員に届く。プロジェクト紐づきでないワークスペーススレッドでは workspace_members 全員。「誰でも良い」場合のフォールバック扱いで、原則 1 か 2 を先に検討する
|
|
524
528
|
|
|
525
529
|
POST `/api/threads/:id/messages` body:
|
|
526
530
|
```json
|
|
@@ -763,6 +767,18 @@ Web ページの読み取り・要約・情報抽出をミニオン内のサブ
|
|
|
763
767
|
|
|
764
768
|
Available commands: `restart-agent`, `update-agent`, `restart-display`, `restart-all`, `status-services`
|
|
765
769
|
|
|
770
|
+
### Meeting Rooms 🧪 (experimental, v3.59.0〜)
|
|
771
|
+
|
|
772
|
+
HQ がミーティング招待を直接ミニオンへプッシュするエンドポイント。受信すると `mt-{meetingId8}` 名の tmux セッションを起動し、claude + meeting MCP サーバーで会議に参加する。
|
|
773
|
+
|
|
774
|
+
| Method | Endpoint | Description |
|
|
775
|
+
|--------|----------|-------------|
|
|
776
|
+
| POST | `/api/meetings/invitations` | HQからの招待受信。Body: `{meeting_id, title, purpose, hq_url, host, self_name?, role?}` |
|
|
777
|
+
| DELETE | `/api/meetings/:id` | HQからの終了通知 (tmuxセッションkill) |
|
|
778
|
+
| GET | `/api/meetings/active` | デバッグ: アクティブな会議セッション一覧 |
|
|
779
|
+
|
|
780
|
+
参加後の動作 (`meeting` MCPツール) は core.md の「Meeting Participation」を参照。
|
|
781
|
+
|
|
766
782
|
### Admin (HQ → Minion only)
|
|
767
783
|
|
|
768
784
|
HQ が課金状態の変化に応じてミニオンに直接プッシュするエンドポイント。
|
|
@@ -811,6 +827,46 @@ Response:
|
|
|
811
827
|
|
|
812
828
|
`role` is one of `"pm"` (project manager), `"engineer"`, or `"accountant"`.
|
|
813
829
|
|
|
830
|
+
### Project Members
|
|
831
|
+
|
|
832
|
+
| Method | Endpoint | Description |
|
|
833
|
+
|--------|----------|-------------|
|
|
834
|
+
| GET | `/api/minion/me/project/[id]/members` | プロジェクトのメンバー一覧(ミニオン+人間) |
|
|
835
|
+
|
|
836
|
+
Response:
|
|
837
|
+
```json
|
|
838
|
+
{
|
|
839
|
+
"minions": [
|
|
840
|
+
{ "minion_id": "uuid", "name": "Mary", "status": "online", "role": "pm", "joined_at": "..." }
|
|
841
|
+
],
|
|
842
|
+
"humans": [
|
|
843
|
+
{ "user_id": "uuid", "display_name": "yunoda", "email": "yunoda@example.com", "role": "engineer", "joined_at": "..." }
|
|
844
|
+
]
|
|
845
|
+
}
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
スレッドに `user:<auth_user_id>` で個別指名する前に、このエンドポイントで `user_id` を引いてくる。`display_name` が重複するワークスペースで誤通知を避けるため、ミニオンは必ず `email` または既知の `user_id` で当人を特定すること。`role` はプロジェクトロール (`pm` / `engineer` / `accountant`)。
|
|
849
|
+
|
|
850
|
+
### Workspace Members
|
|
851
|
+
|
|
852
|
+
| Method | Endpoint | Description |
|
|
853
|
+
|--------|----------|-------------|
|
|
854
|
+
| GET | `/api/minion/workspaces/[id]/members` | ワークスペースのメンバー一覧(人間+ミニオン) |
|
|
855
|
+
|
|
856
|
+
Response:
|
|
857
|
+
```json
|
|
858
|
+
{
|
|
859
|
+
"humans": [
|
|
860
|
+
{ "user_id": "uuid", "display_name": "yunoda", "email": "yunoda@example.com", "role": "owner", "joined_at": "..." }
|
|
861
|
+
],
|
|
862
|
+
"minions": [
|
|
863
|
+
{ "minion_id": "uuid", "name": "Mary", "status": "online", "joined_at": "..." }
|
|
864
|
+
]
|
|
865
|
+
}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
プロジェクト紐づきがないワークスペーススレッド(朝作業ルーティンのブロッカー報告など)でメンション先を引きたいときに使う。`role` はワークスペースロール (`owner` / `admin` / `member`)。
|
|
869
|
+
|
|
814
870
|
### Project Context
|
|
815
871
|
|
|
816
872
|
| Method | Endpoint | Description |
|
|
@@ -1930,3 +1986,16 @@ hq note list <project_id>
|
|
|
1930
1986
|
hq note get <project_id> <note_id>
|
|
1931
1987
|
hq note search <project_id> "キーワード"
|
|
1932
1988
|
```
|
|
1989
|
+
|
|
1990
|
+
### Meeting Rooms 🧪 (HQ, experimental, v3.59.0〜)
|
|
1991
|
+
|
|
1992
|
+
ミーティング機能はミニオン上の `meeting` MCP サーバーが叩く HQ エンドポイント。通常 AI 側から直接コールすることはなく、招待 (`POST /api/meetings/invitations` をミニオンが受信) で起動された claude セッションが MCP ツール経由で利用する。
|
|
1993
|
+
|
|
1994
|
+
| Method | Endpoint | Description |
|
|
1995
|
+
|--------|----------|-------------|
|
|
1996
|
+
| GET | `/api/minion/meetings/:id` | ミーティングのメタデータ + 参加者一覧。`meeting_get_state` ツールから呼ばれる |
|
|
1997
|
+
| GET | `/api/minion/meetings/:id/messages?after=<iso>&wait=true` | long-poll で次の発言を待機 (最大 ~25秒)。404 = 会議終了 |
|
|
1998
|
+
| POST | `/api/minion/meetings/:id/messages` | 発言。Body: `{text}` |
|
|
1999
|
+
| POST | `/api/minion/meetings/:id` | アクション。Body: `{action: 'leave' \| 'heartbeat'}` |
|
|
2000
|
+
|
|
2001
|
+
これらは MCP ツール (`meeting_wait_for_next_message`, `meeting_speak`, `meeting_leave`) として claude に公開される。直接叩く必要はない。
|
package/docs/task-guides.md
CHANGED
|
@@ -793,6 +793,34 @@ await reportIssue({ title: '...', body: '...', labels: ['bug'] })
|
|
|
793
793
|
|
|
794
794
|
---
|
|
795
795
|
|
|
796
|
+
## ミーティングへの参加 🧪 (experimental, v3.59.0〜)
|
|
797
|
+
|
|
798
|
+
HQから招待されると、`mt-{meetingId8}` 名の tmux セッションで claude が自動起動して `meeting` MCP サーバー経由で会議に参加する。あなた自身がコマンドを叩いて参加・退出する必要は無い。
|
|
799
|
+
|
|
800
|
+
**セッション中のあなたの責任:**
|
|
801
|
+
|
|
802
|
+
1. `meeting_get_state` で目的・参加者を確認 (起動直後に呼ぶ)
|
|
803
|
+
2. `meeting_wait_for_next_message` をループで呼んで発言を聞く
|
|
804
|
+
3. 自分が応答すべき内容なら `meeting_speak` で発言 (1-3文)
|
|
805
|
+
4. 戻り値が `meeting_ended: true` になったら `meeting_leave` を呼んでループ脱出
|
|
806
|
+
|
|
807
|
+
**振る舞い指針:** core.md の「Meeting Participation」を参照。特に:
|
|
808
|
+
- 発言は短く (会議でだらだら話さない)
|
|
809
|
+
- 重複発言を避ける
|
|
810
|
+
- 議論を前進させる発言を優先 (賛否・代替案・質問)
|
|
811
|
+
- PMロールなら議事進行と成果物作成も担う
|
|
812
|
+
|
|
813
|
+
**確認方法:**
|
|
814
|
+
```bash
|
|
815
|
+
# アクティブな会議セッションを確認
|
|
816
|
+
tmux ls | grep '^mt-'
|
|
817
|
+
|
|
818
|
+
# 自分が参加中の会議の状態
|
|
819
|
+
curl -H "Authorization: Bearer $API_TOKEN" http://localhost:8080/api/meetings/active
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
セッション復帰機構は未実装。tmux/claude が落ちた場合は再起動されず、新しい招待を待つ。
|
|
823
|
+
|
|
796
824
|
## ツール・MCPサーバーのインストール
|
|
797
825
|
|
|
798
826
|
スキルが `requires` で宣言している MCP サーバーや CLI ツールが不足している場合は、`~/.minion/docs/environment-setup.md` の手順に従ってインストールする。
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meeting Runner (Linux)
|
|
3
|
+
*
|
|
4
|
+
* Spawns a dedicated tmux session per meeting invitation so the claude
|
|
5
|
+
* participation loop is isolated from chat / board-task sessions and can
|
|
6
|
+
* be inspected via `tmux attach -t mt-{id8}`.
|
|
7
|
+
*
|
|
8
|
+
* Session naming: `mt-{meetingId.slice(0,8)}`
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle:
|
|
11
|
+
* 1. Receive invitation from HQ (via routes/meetings POST /api/meetings/invitations).
|
|
12
|
+
* 2. If a `mt-*` session for this meeting already exists, return early
|
|
13
|
+
* (idempotent: HQ may retry pushes).
|
|
14
|
+
* 3. Write a temporary MCP config + kickoff prompt, then spawn:
|
|
15
|
+
* tmux new-session -d -s mt-xxx claude --mcp-config <tmp> <kickoff>
|
|
16
|
+
* The meeting MCP server starts as a stdio subprocess of claude.
|
|
17
|
+
* 4. tmux session exits when claude exits (after meeting_leave is called
|
|
18
|
+
* or the meeting was deleted on HQ side).
|
|
19
|
+
*
|
|
20
|
+
* No exit-code polling here (unlike board-task-runner) — meetings are
|
|
21
|
+
* fire-and-forget; observability is via `tmux ls` + meeting logs.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const { exec } = require('child_process')
|
|
25
|
+
const { promisify } = require('util')
|
|
26
|
+
const fs = require('fs').promises
|
|
27
|
+
const os = require('os')
|
|
28
|
+
const path = require('path')
|
|
29
|
+
const execAsync = promisify(exec)
|
|
30
|
+
|
|
31
|
+
const { config } = require('../core/config')
|
|
32
|
+
const runningTasks = require('../core/lib/running-tasks')
|
|
33
|
+
const logManager = require('../core/lib/log-manager')
|
|
34
|
+
const { getActivePrimary } = require('../core/llm-plugins/lib/active')
|
|
35
|
+
const { buildMeetingKickoffPrompt } = require('../core/meetings/meeting-prompt')
|
|
36
|
+
|
|
37
|
+
function generateSessionName(meetingId) {
|
|
38
|
+
if (!meetingId) throw new Error('meetingId is required')
|
|
39
|
+
return `mt-${String(meetingId).substring(0, 8)}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function tmuxHasSession(sessionName) {
|
|
43
|
+
try {
|
|
44
|
+
await execAsync(`tmux has-session -t "${sessionName}" 2>/dev/null`)
|
|
45
|
+
return true
|
|
46
|
+
} catch {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function listMeetingSessions() {
|
|
52
|
+
try {
|
|
53
|
+
const { stdout } = await execAsync(`tmux ls -F '#S' 2>/dev/null`)
|
|
54
|
+
return stdout
|
|
55
|
+
.split('\n')
|
|
56
|
+
.map((s) => s.trim())
|
|
57
|
+
.filter((s) => s.startsWith('mt-'))
|
|
58
|
+
} catch {
|
|
59
|
+
return []
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function killMeetingSession(meetingId) {
|
|
64
|
+
const sessionName = generateSessionName(meetingId)
|
|
65
|
+
try {
|
|
66
|
+
await execAsync(`tmux kill-session -t "${sessionName}" 2>/dev/null`)
|
|
67
|
+
runningTasks.remove(sessionName)
|
|
68
|
+
return true
|
|
69
|
+
} catch {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Start a meeting participation tmux session.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} params
|
|
78
|
+
* @param {string} params.meetingId
|
|
79
|
+
* @param {string} params.title
|
|
80
|
+
* @param {string|null} params.purpose
|
|
81
|
+
* @param {string} params.host
|
|
82
|
+
* @param {string} params.hqUrl
|
|
83
|
+
* @param {string} [params.selfName] - This minion's display name
|
|
84
|
+
* @param {string} [params.role] - This minion's role in the project (e.g. 'pm')
|
|
85
|
+
*/
|
|
86
|
+
async function runMeeting({ meetingId, title, purpose, host, hqUrl, selfName, role }) {
|
|
87
|
+
if (!meetingId) {
|
|
88
|
+
return { sessionName: null, started: false, success: false, error: 'meetingId is required' }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const sessionName = generateSessionName(meetingId)
|
|
92
|
+
|
|
93
|
+
if (await tmuxHasSession(sessionName)) {
|
|
94
|
+
console.log(`[MeetingRunner] tmux session ${sessionName} already exists, skipping start`)
|
|
95
|
+
return { sessionName, started: false, success: true }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const promptFile = path.join(os.tmpdir(), `minion-meeting-prompt-${sessionName}.txt`)
|
|
99
|
+
const mcpConfigFile = path.join(os.tmpdir(), `minion-meeting-mcp-${sessionName}.json`)
|
|
100
|
+
const execScript = path.join(os.tmpdir(), `minion-meeting-exec-${sessionName}.sh`)
|
|
101
|
+
const mcpServerPath = path.join(__dirname, '..', 'core', 'meetings', 'meeting-mcp-server.js')
|
|
102
|
+
|
|
103
|
+
console.log(`[MeetingRunner] Starting meeting ${meetingId} (${title})`)
|
|
104
|
+
console.log(`[MeetingRunner] tmux session: ${sessionName}`)
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await logManager.ensureLogDir()
|
|
108
|
+
|
|
109
|
+
const prompt = buildMeetingKickoffPrompt({
|
|
110
|
+
meetingId,
|
|
111
|
+
title,
|
|
112
|
+
purpose,
|
|
113
|
+
host,
|
|
114
|
+
selfName: selfName || 'minion',
|
|
115
|
+
role: role || null,
|
|
116
|
+
})
|
|
117
|
+
await fs.writeFile(promptFile, prompt, 'utf-8')
|
|
118
|
+
|
|
119
|
+
// Use the minion's own config.HQ_URL — set correctly at provisioning time.
|
|
120
|
+
// For docker minions this is the docker-network internal URL
|
|
121
|
+
// (HQ_INTERNAL_URL=http://frontend-server:3000), which differs from
|
|
122
|
+
// browser-facing NEXT_PUBLIC_SITE_URL=http://localhost:3000. Falling back
|
|
123
|
+
// to the URL pushed by HQ only if local config is missing.
|
|
124
|
+
const effectiveHqUrl = config.HQ_URL || hqUrl
|
|
125
|
+
if (!effectiveHqUrl) {
|
|
126
|
+
throw new Error('No HQ_URL configured (neither minion config nor HQ push)')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// MCP config injecting our env into the meeting server child process
|
|
130
|
+
const mcpConfig = {
|
|
131
|
+
mcpServers: {
|
|
132
|
+
meeting: {
|
|
133
|
+
command: 'node',
|
|
134
|
+
args: [mcpServerPath],
|
|
135
|
+
env: {
|
|
136
|
+
HQ_URL: effectiveHqUrl,
|
|
137
|
+
API_TOKEN: config.API_TOKEN,
|
|
138
|
+
MINION_MEETING_ID: meetingId,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
await fs.writeFile(mcpConfigFile, JSON.stringify(mcpConfig, null, 2), 'utf-8')
|
|
144
|
+
|
|
145
|
+
const primary = getActivePrimary()
|
|
146
|
+
if (!primary || primary.name !== 'claude') {
|
|
147
|
+
throw new Error(
|
|
148
|
+
'Meeting participation currently requires the claude primary LLM (MCP support). ' +
|
|
149
|
+
`Active primary: ${primary ? primary.name : 'none'}`,
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Build invocation directly to inject --mcp-config (not supported by
|
|
154
|
+
// buildShellInvocation, which is shared across plugins).
|
|
155
|
+
//
|
|
156
|
+
// --allowedTools: Claude Code defaults to prompting the user for every MCP
|
|
157
|
+
// tool call, which hangs in -p mode (no interactive UI). Pre-authorize the
|
|
158
|
+
// meeting MCP tools so claude can run the participation loop without
|
|
159
|
+
// approval prompts. We do NOT bypass permissions globally — built-in tools
|
|
160
|
+
// (Bash, Read, Write etc.) still go through the user's normal allow list.
|
|
161
|
+
const { resolveBinary } = require('../core/llm-plugins/lib/spawn-helper')
|
|
162
|
+
const claudeBin = resolveBinary('claude', config.HOME_DIR)
|
|
163
|
+
const allowedTools = [
|
|
164
|
+
'mcp__meeting__meeting_wait_for_next_message',
|
|
165
|
+
'mcp__meeting__meeting_speak',
|
|
166
|
+
'mcp__meeting__meeting_get_state',
|
|
167
|
+
'mcp__meeting__meeting_leave',
|
|
168
|
+
].join(',')
|
|
169
|
+
const llmCommand = `${claudeBin} -p --mcp-config "${mcpConfigFile}" --allowedTools "${allowedTools}" < "${promptFile}"`
|
|
170
|
+
|
|
171
|
+
await fs.writeFile(
|
|
172
|
+
execScript,
|
|
173
|
+
`#!/bin/bash\nexport HQ_URL="${effectiveHqUrl}"\nexport API_TOKEN="${config.API_TOKEN}"\nexport MINION_MEETING_ID="${meetingId}"\n${llmCommand}\n`,
|
|
174
|
+
'utf-8',
|
|
175
|
+
)
|
|
176
|
+
await execAsync(`chmod +x "${execScript}"`)
|
|
177
|
+
|
|
178
|
+
await execAsync(`tmux new-session -d -s "${sessionName}" -x 200 -y 50`, {
|
|
179
|
+
cwd: config.HOME_DIR,
|
|
180
|
+
})
|
|
181
|
+
await execAsync(`tmux set-option -t "${sessionName}" remain-on-exit on`)
|
|
182
|
+
|
|
183
|
+
const logFile = path.join(
|
|
184
|
+
logManager.LOG_DIR || path.join(config.HOME_DIR, '.minion', 'logs'),
|
|
185
|
+
`meeting-${meetingId}.log`,
|
|
186
|
+
)
|
|
187
|
+
try {
|
|
188
|
+
await execAsync(`tmux pipe-pane -o -t "${sessionName}" "cat >> ${logFile}"`)
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.error(`[MeetingRunner] pipe-pane failed: ${err.message}`)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
await execAsync(`tmux send-keys -t "${sessionName}" "bash ${execScript}" Enter`)
|
|
194
|
+
|
|
195
|
+
runningTasks.add({
|
|
196
|
+
type: 'meeting',
|
|
197
|
+
session_name: sessionName,
|
|
198
|
+
meeting_id: meetingId,
|
|
199
|
+
started_at: new Date().toISOString(),
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
console.log(`[MeetingRunner] Meeting ${meetingId} session started`)
|
|
203
|
+
return { sessionName, started: true, success: true }
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.error(`[MeetingRunner] Meeting ${meetingId} failed to start: ${err.message}`)
|
|
206
|
+
return { sessionName, started: false, success: false, error: err.message }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
runMeeting,
|
|
212
|
+
killMeetingSession,
|
|
213
|
+
listMeetingSessions,
|
|
214
|
+
generateSessionName,
|
|
215
|
+
}
|
package/linux/server.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* TODOs: GET/POST /api/todos, GET/PUT/DELETE /api/todos/:id
|
|
24
24
|
* Emails: POST /api/email/inbox, GET /api/email/inbox, GET/PUT/DELETE /api/email/inbox/:id
|
|
25
25
|
* Sudoers: GET /api/sudoers
|
|
26
|
+
* Meetings (exp): POST /api/meetings/invitations, DELETE /api/meetings/:id, GET /api/meetings/active
|
|
26
27
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
27
28
|
*/
|
|
28
29
|
|
|
@@ -94,6 +95,8 @@ const { fileRoutes } = require('./routes/files')
|
|
|
94
95
|
const { directiveRoutes } = require('./routes/directives')
|
|
95
96
|
const { chatRoutes, runQuickLlmCall } = require('./routes/chat')
|
|
96
97
|
const { configRoutes } = require('./routes/config')
|
|
98
|
+
const { meetingRoutes } = require('../core/routes/meetings')
|
|
99
|
+
const meetingRunner = require('./meeting-runner')
|
|
97
100
|
|
|
98
101
|
// Validate configuration before starting
|
|
99
102
|
validate()
|
|
@@ -308,6 +311,9 @@ async function registerAllRoutes(app) {
|
|
|
308
311
|
await app.register(directiveRoutes)
|
|
309
312
|
await app.register(chatRoutes)
|
|
310
313
|
await app.register(configRoutes)
|
|
314
|
+
|
|
315
|
+
// Experimental: meetings (shared route + linux runner)
|
|
316
|
+
await app.register(meetingRoutes, { meetingRunner })
|
|
311
317
|
}
|
|
312
318
|
|
|
313
319
|
// Start server
|
package/package.json
CHANGED
package/roles/engineer.md
CHANGED
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
|
|
29
29
|
1. **まずプロジェクトメモリーを検索** — `GET /api/project-memories?project_id=...&search=キーワード` で過去の知見を確認
|
|
30
30
|
2. **自己解決不可なら即ヘルプスレッドを起票** — `POST /api/threads` で `thread_type: "help"` を作成。`attempted_resolution` に試したことを記載
|
|
31
|
-
3.
|
|
31
|
+
3. **人間にしか解決できない問題は即エスカレーション** — 認証コード、パスワード、外部承認など
|
|
32
|
+
- 当事者が特定できる場合は **`GET /api/minion/me/project/<project_id>/members` で `user_id` を引いて `mentions: ["user:<auth_user_id>"]` で個別指名** (display_name 重複時の誤通知を避けるため)
|
|
33
|
+
- 誰でも対応可能な問題なら `mentions: ["user"]` (generic)
|
|
32
34
|
4. **解決後はスレッドを resolve し、知見をプロジェクトメモリーに保存**
|
|
33
35
|
|
|
34
36
|
**スキルを失敗終了させる前に、必ずヘルプスレッドでエスカレーションすること。**
|
package/roles/pm.md
CHANGED
|
@@ -48,14 +48,18 @@ PMとして、ブロッカーの解決を主導する責務がある:
|
|
|
48
48
|
|
|
49
49
|
### 自分がブロッカーに遭遇した場合
|
|
50
50
|
1. **プロジェクトメモリーを検索** — 過去の知見で解決できないか確認
|
|
51
|
-
2.
|
|
51
|
+
2. **自己解決不可ならヘルプスレッドを起票** — PMが解決できない問題は人間にエスカレーション
|
|
52
|
+
- 特定の人にしか分からない問題なら **`GET /api/minion/me/project/<project_id>/members` で `user_id` を引いて `mentions: ["user:<auth_user_id>"]` で個別指名**
|
|
53
|
+
- 誰でも良い性質の問題なら `mentions: ["user"]` (generic)
|
|
52
54
|
3. **解決後はスレッドを resolve し、知見をプロジェクトメモリーに保存**
|
|
53
55
|
|
|
54
56
|
### エンジニアからのヘルプスレッドに対応する場合
|
|
55
57
|
1. **thread_watcher が自動で通知するので、`role:pm` 宛のスレッドに回答する**
|
|
56
|
-
2.
|
|
58
|
+
2. **自分で解決できない場合は人間にエスカレーション** — 当事者が特定できるなら個別指名 (`user:<auth_user_id>`)、不明なら `user` (generic)
|
|
57
59
|
3. **解決策が汎用的なら、プロジェクトメモリーへの保存を促す**
|
|
58
60
|
|
|
61
|
+
display_name はワークスペース内で一意とは限らない。同名問題を避けるため、**generic `user` より個別指名 (`user:<id>`) を優先する**こと。`user_id` の取得方法は `~/.minion/docs/api-reference.md` の「Project Members / Workspace Members」セクションを参照。
|
|
62
|
+
|
|
59
63
|
詳細は `~/.minion/rules/core.md` の「Blocker Handling」セクションを参照。
|
|
60
64
|
|
|
61
65
|
## Routine (従来方式)
|
package/rules/core.md
CHANGED
|
@@ -277,6 +277,38 @@ Routine 実行中は以下もtmuxセッション環境で利用可能:
|
|
|
277
277
|
|
|
278
278
|
詳細な API 仕様は `~/.minion/docs/api-reference.md` の「Project Sprints」「Project Tasks」セクションを参照。
|
|
279
279
|
|
|
280
|
+
## Meeting Participation (ミーティング参加, experimental, v3.59.0〜)
|
|
281
|
+
|
|
282
|
+
HQ からミーティング招待 (`POST /api/meetings/invitations`) を受け取ると、専用 tmux セッション `mt-{meetingId8}` で claude が起動し、`meeting` MCP サーバー経由で会議に参加する。
|
|
283
|
+
|
|
284
|
+
**基本ループ:**
|
|
285
|
+
1. `meeting_get_state` で参加者・目的・タイトルを把握
|
|
286
|
+
2. `meeting_wait_for_next_message` で次の発言を long-poll (最大 25秒)
|
|
287
|
+
3. 受信メッセージを評価:
|
|
288
|
+
- 自分の役割・コンテキストに関連するか?
|
|
289
|
+
- 直前の発言で他者が同趣旨を発言していないか?
|
|
290
|
+
- 自分宛 (mention) か、自分が応答すべき話題か?
|
|
291
|
+
4. 応答すべきなら `meeting_speak` で **1-3 文程度** の発言を送信
|
|
292
|
+
5. 戻り値が `{ meeting_ended: true }` になったら `meeting_leave` を呼んで終了
|
|
293
|
+
|
|
294
|
+
**振る舞いのルール:**
|
|
295
|
+
- 発言は短く保つ (会議でだらだら話さない)
|
|
296
|
+
- 他のミニオンの発言と重複しない (既に同じ趣旨を誰かが言っていたら追従しない)
|
|
297
|
+
- 議論を前進させる発言を優先 (賛否・代替案・質問・確認)
|
|
298
|
+
- 既存タスク・スプリント状態を踏まえて発言する
|
|
299
|
+
- **PMロールで参加する場合は議事進行と成果物作成も担う**:
|
|
300
|
+
- 議論が発散したら論点を整理して提示
|
|
301
|
+
- ミーティング終了前に趣旨に応じた成果物を作成:
|
|
302
|
+
- スプリント計画 → `hq sprint create` + `hq task create` (連結)
|
|
303
|
+
- 振り返り → `hq note create`
|
|
304
|
+
- ブロッカー解決 → 関連スレッドを resolve + note作成
|
|
305
|
+
- 趣旨が曖昧な場合は終了前に「何を成果物として残すか」を参加者に確認
|
|
306
|
+
- 結論が出なかった話題は thread や project task に転記してフォロー
|
|
307
|
+
|
|
308
|
+
セッション復帰機構はまだ未実装 (claude/tmux が落ちた場合は再起動しない)。
|
|
309
|
+
|
|
310
|
+
API詳細は `~/.minion/docs/api-reference.md` の「Meeting Rooms」セクションを参照。
|
|
311
|
+
|
|
280
312
|
## Todo運用ルール
|
|
281
313
|
|
|
282
314
|
チャット中のタスクは `/api/todos` に登録して進捗管理すること。圧縮(context compaction)を跨いでも作業を完遂するための仕組みがある。
|
|
@@ -322,7 +354,8 @@ API詳細は `~/.minion/docs/api-reference.md` の「Todos」セクションを
|
|
|
322
354
|
"thread_type": "help",
|
|
323
355
|
"title": "問題の要約(1行)",
|
|
324
356
|
"content": "状況の詳細説明",
|
|
325
|
-
"mentions": ["
|
|
357
|
+
"mentions": ["user:<auth_user_id>"], // 特定の人に聞くなら個別指名 (推奨)
|
|
358
|
+
// 誰でも良いなら ["user"] / ["role:pm"] 等
|
|
326
359
|
"context": {
|
|
327
360
|
"category": "auth|environment|external-service|information|approval",
|
|
328
361
|
"attempted_resolution": "試行した内容"
|
|
@@ -336,7 +369,7 @@ API詳細は `~/.minion/docs/api-reference.md` の「Todos」セクションを
|
|
|
336
369
|
"thread_type": "help",
|
|
337
370
|
"title": "問題の要約(1行)",
|
|
338
371
|
"content": "状況の詳細説明",
|
|
339
|
-
"mentions": ["user"],
|
|
372
|
+
"mentions": ["user:<auth_user_id>"], // 個別指名 (推奨)。誰でも良いなら ["user"]
|
|
340
373
|
"context": {
|
|
341
374
|
"category": "auth|environment|external-service|information|approval",
|
|
342
375
|
"attempted_resolution": "試行した内容"
|
|
@@ -366,18 +399,25 @@ API詳細は `~/.minion/docs/api-reference.md` の「Todos」セクションを
|
|
|
366
399
|
|
|
367
400
|
### メンションの使い分け
|
|
368
401
|
|
|
369
|
-
| 状況 | メンション |
|
|
370
|
-
|
|
371
|
-
|
|
|
372
|
-
|
|
|
373
|
-
|
|
|
374
|
-
|
|
|
375
|
-
|
|
|
402
|
+
| 状況 | メンション | 備考 |
|
|
403
|
+
|------|-----------|------|
|
|
404
|
+
| 特定の人間を指名(推奨) | `["user:<auth_user_id>"]` | 事前に Members API で `user_id` を解決すること |
|
|
405
|
+
| PMに判断を仰ぐ | `["role:pm"]` | プロジェクト紐づきスレッドのみ有効 |
|
|
406
|
+
| エンジニアの知見が必要 | `["role:engineer"]` | プロジェクト紐づきスレッドのみ有効 |
|
|
407
|
+
| 特定ミニオンに聞く | `["minion:<minion_id>"]` | |
|
|
408
|
+
| 誰でも良いから人間に聞く | `["user"]` | プロジェクト紐づきならプロジェクト人間メンバー、紐づきがなければワークスペース人間メンバー全員 |
|
|
409
|
+
| チーム全体に共有 | `[]`(メンションなし) | |
|
|
410
|
+
|
|
411
|
+
**人間メンバーの user_id 解決:**
|
|
412
|
+
- プロジェクト紐づきスレッド: `GET /api/minion/me/project/<project_id>/members` で `humans[].user_id` を取得
|
|
413
|
+
- ワークスペーススレッド: `GET /api/minion/workspaces/<workspace_id>/members` で `humans[].user_id` を取得
|
|
414
|
+
|
|
415
|
+
display_name が同じユーザーが同一ワークスペースに複数いる可能性があるため、原則として個別指名 (`user:<id>`) を優先する。「誰でも良い」場合のみ generic `user` を使う。
|
|
376
416
|
|
|
377
417
|
### 重要なルール
|
|
378
418
|
|
|
379
419
|
- **スレッド起票時は `attempted_resolution` を必ず含める。** 何を試したか不明だと、回答者が同じことを提案してしまう。
|
|
380
|
-
-
|
|
420
|
+
- **人間にしか解決できない問題(認証コード入力、外部サービスのパスワード等)は即メンション付き** で起票する。自己解決を試みて時間を浪費しないこと。特定の人にしか分からないことなら個別指名 (`user:<id>`)、誰でも良い性質の問題なら `user`(generic) を使う。
|
|
381
421
|
- **ブロッカーが解決したらスレッドを `resolve` する。** 放置しない。
|
|
382
422
|
- **解決策をプロジェクトメモリーに保存する。** 同じブロッカーに再度遭遇する他のミニオンの助けになる。
|
|
383
423
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meeting Runner (Windows / WSL)
|
|
3
|
+
*
|
|
4
|
+
* Spawns a dedicated tmux session inside WSL per meeting invitation.
|
|
5
|
+
* Mirrors linux/meeting-runner.js but routes commands through WSL since
|
|
6
|
+
* Windows native tmux is not available.
|
|
7
|
+
*
|
|
8
|
+
* Session naming: `mt-{meetingId.slice(0,8)}`
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { exec } = require('child_process')
|
|
12
|
+
const { promisify } = require('util')
|
|
13
|
+
const fs = require('fs').promises
|
|
14
|
+
const os = require('os')
|
|
15
|
+
const path = require('path')
|
|
16
|
+
const execAsync = promisify(exec)
|
|
17
|
+
|
|
18
|
+
const { config } = require('../core/config')
|
|
19
|
+
const runningTasks = require('../core/lib/running-tasks')
|
|
20
|
+
const logManager = require('../core/lib/log-manager')
|
|
21
|
+
const { getActivePrimary } = require('../core/llm-plugins/lib/active')
|
|
22
|
+
const { buildMeetingKickoffPrompt } = require('../core/meetings/meeting-prompt')
|
|
23
|
+
|
|
24
|
+
function generateSessionName(meetingId) {
|
|
25
|
+
if (!meetingId) throw new Error('meetingId is required')
|
|
26
|
+
return `mt-${String(meetingId).substring(0, 8)}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function wslExec(cmd) {
|
|
30
|
+
return execAsync(`wsl bash -lc ${JSON.stringify(cmd)}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function tmuxHasSession(sessionName) {
|
|
34
|
+
try {
|
|
35
|
+
await wslExec(`tmux has-session -t "${sessionName}" 2>/dev/null`)
|
|
36
|
+
return true
|
|
37
|
+
} catch {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function listMeetingSessions() {
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await wslExec(`tmux ls -F '#S' 2>/dev/null`)
|
|
45
|
+
return stdout
|
|
46
|
+
.split('\n')
|
|
47
|
+
.map((s) => s.trim())
|
|
48
|
+
.filter((s) => s.startsWith('mt-'))
|
|
49
|
+
} catch {
|
|
50
|
+
return []
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function killMeetingSession(meetingId) {
|
|
55
|
+
const sessionName = generateSessionName(meetingId)
|
|
56
|
+
try {
|
|
57
|
+
await wslExec(`tmux kill-session -t "${sessionName}" 2>/dev/null`)
|
|
58
|
+
runningTasks.remove(sessionName)
|
|
59
|
+
return true
|
|
60
|
+
} catch {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function runMeeting({ meetingId, title, purpose, host, hqUrl, selfName, role }) {
|
|
66
|
+
if (!meetingId) {
|
|
67
|
+
return { sessionName: null, started: false, success: false, error: 'meetingId is required' }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const sessionName = generateSessionName(meetingId)
|
|
71
|
+
|
|
72
|
+
if (await tmuxHasSession(sessionName)) {
|
|
73
|
+
console.log(`[MeetingRunner-win] tmux session ${sessionName} already exists, skipping start`)
|
|
74
|
+
return { sessionName, started: false, success: true }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const tmpdir = os.tmpdir()
|
|
78
|
+
const promptFile = path.join(tmpdir, `minion-meeting-prompt-${sessionName}.txt`)
|
|
79
|
+
const mcpConfigFile = path.join(tmpdir, `minion-meeting-mcp-${sessionName}.json`)
|
|
80
|
+
const execScript = path.join(tmpdir, `minion-meeting-exec-${sessionName}.sh`)
|
|
81
|
+
const mcpServerPath = path.join(__dirname, '..', 'core', 'meetings', 'meeting-mcp-server.js')
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await logManager.ensureLogDir()
|
|
85
|
+
|
|
86
|
+
const prompt = buildMeetingKickoffPrompt({
|
|
87
|
+
meetingId,
|
|
88
|
+
title,
|
|
89
|
+
purpose,
|
|
90
|
+
host,
|
|
91
|
+
selfName: selfName || 'minion',
|
|
92
|
+
role: role || null,
|
|
93
|
+
})
|
|
94
|
+
await fs.writeFile(promptFile, prompt, 'utf-8')
|
|
95
|
+
|
|
96
|
+
// Use the minion's own config.HQ_URL when available. HQ-pushed hq_url is
|
|
97
|
+
// only a fallback because the minion already has the correct URL set at
|
|
98
|
+
// provisioning time.
|
|
99
|
+
const effectiveHqUrl = config.HQ_URL || hqUrl
|
|
100
|
+
if (!effectiveHqUrl) {
|
|
101
|
+
throw new Error('No HQ_URL configured (neither minion config nor HQ push)')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const mcpConfig = {
|
|
105
|
+
mcpServers: {
|
|
106
|
+
meeting: {
|
|
107
|
+
command: 'node',
|
|
108
|
+
args: [mcpServerPath],
|
|
109
|
+
env: {
|
|
110
|
+
HQ_URL: effectiveHqUrl,
|
|
111
|
+
API_TOKEN: config.API_TOKEN,
|
|
112
|
+
MINION_MEETING_ID: meetingId,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
await fs.writeFile(mcpConfigFile, JSON.stringify(mcpConfig, null, 2), 'utf-8')
|
|
118
|
+
|
|
119
|
+
const primary = getActivePrimary()
|
|
120
|
+
if (!primary || primary.name !== 'claude') {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'Meeting participation currently requires the claude primary LLM (MCP support).',
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// See linux/meeting-runner.js for why --allowedTools is needed (-p mode
|
|
127
|
+
// hangs on MCP permission prompts without this).
|
|
128
|
+
const allowedTools = [
|
|
129
|
+
'mcp__meeting__meeting_wait_for_next_message',
|
|
130
|
+
'mcp__meeting__meeting_speak',
|
|
131
|
+
'mcp__meeting__meeting_get_state',
|
|
132
|
+
'mcp__meeting__meeting_leave',
|
|
133
|
+
].join(',')
|
|
134
|
+
|
|
135
|
+
await fs.writeFile(
|
|
136
|
+
execScript,
|
|
137
|
+
`#!/bin/bash\nexport HQ_URL="${effectiveHqUrl}"\nexport API_TOKEN="${config.API_TOKEN}"\nexport MINION_MEETING_ID="${meetingId}"\nclaude -p --mcp-config "${mcpConfigFile}" --allowedTools "${allowedTools}" < "${promptFile}"\n`,
|
|
138
|
+
'utf-8',
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
await wslExec(`chmod +x "${execScript}"`)
|
|
142
|
+
await wslExec(`tmux new-session -d -s "${sessionName}" -x 200 -y 50`)
|
|
143
|
+
await wslExec(`tmux set-option -t "${sessionName}" remain-on-exit on`)
|
|
144
|
+
await wslExec(`tmux send-keys -t "${sessionName}" "bash ${execScript}" Enter`)
|
|
145
|
+
|
|
146
|
+
runningTasks.add({
|
|
147
|
+
type: 'meeting',
|
|
148
|
+
session_name: sessionName,
|
|
149
|
+
meeting_id: meetingId,
|
|
150
|
+
started_at: new Date().toISOString(),
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
return { sessionName, started: true, success: true }
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error(`[MeetingRunner-win] Failed to start meeting ${meetingId}: ${err.message}`)
|
|
156
|
+
return { sessionName, started: false, success: false, error: err.message }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
runMeeting,
|
|
162
|
+
killMeetingSession,
|
|
163
|
+
listMeetingSessions,
|
|
164
|
+
generateSessionName,
|
|
165
|
+
}
|
package/win/server.js
CHANGED
|
@@ -49,6 +49,8 @@ const { fileRoutes } = require('./routes/files')
|
|
|
49
49
|
const { directiveRoutes } = require('./routes/directives')
|
|
50
50
|
const { chatRoutes, runQuickLlmCall } = require('./routes/chat')
|
|
51
51
|
const { configRoutes } = require('./routes/config')
|
|
52
|
+
const { meetingRoutes } = require('../core/routes/meetings')
|
|
53
|
+
const meetingRunner = require('./meeting-runner')
|
|
52
54
|
|
|
53
55
|
// Compatible route modules (reused from Linux)
|
|
54
56
|
const { healthRoutes, setOffline } = require('../core/routes/health')
|
|
@@ -255,6 +257,9 @@ async function registerRoutes(app) {
|
|
|
255
257
|
await app.register(directiveRoutes)
|
|
256
258
|
await app.register(chatRoutes)
|
|
257
259
|
await app.register(configRoutes)
|
|
260
|
+
|
|
261
|
+
// Experimental: meetings (shared route + windows runner)
|
|
262
|
+
await app.register(meetingRoutes, { meetingRunner })
|
|
258
263
|
}
|
|
259
264
|
|
|
260
265
|
// Start server
|