@geekbeer/minion 3.9.0 → 3.10.0

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/api.js CHANGED
@@ -87,8 +87,8 @@ async function sendHeartbeat(data) {
87
87
  }
88
88
 
89
89
  /**
90
- * Create a thread (project-scoped or workspace-scoped) on HQ.
91
- * @param {object} data - { project_id?, scope?, category?, title, content, thread_type?, mentions?, context? }
90
+ * Create a thread on HQ (workspace-scoped, optionally linked to projects).
91
+ * @param {object} data - { project_id?, project_ids?, category?, title, content, thread_type?, mentions?, context? }
92
92
  * @returns {Promise<{ thread: object }>}
93
93
  */
94
94
  async function createThread(data) {
@@ -100,11 +100,12 @@ async function createThread(data) {
100
100
 
101
101
  /**
102
102
  * Get open threads accessible to this minion.
103
- * @param {'project' | 'workspace' | 'all'} [scope='all'] - Filter by scope
103
+ * @param {string} [projectId] - Optional project ID to filter by
104
104
  * @returns {Promise<{ threads: object[] }>}
105
105
  */
106
- async function getOpenThreads(scope = 'all') {
107
- return request(`/threads/open?scope=${scope}`)
106
+ async function getOpenThreads(projectId) {
107
+ const params = projectId ? `?project_id=${projectId}` : ''
108
+ return request(`/threads/open${params}`)
108
109
  }
109
110
 
110
111
  /**
@@ -2,7 +2,7 @@
2
2
  * Project Thread Watcher
3
3
  *
4
4
  * Polling daemon that monitors open help/discussion threads in the minion's
5
- * projects. Uses LLM to decide whether to participate in conversations.
5
+ * workspace. Uses LLM to decide whether to participate in conversations.
6
6
  *
7
7
  * Token-saving strategies:
8
8
  * 1. Read tracking: only evaluate threads with new messages since last check
@@ -68,7 +68,7 @@ async function getMyProjects() {
68
68
  *
69
69
  * @param {object} thread
70
70
  * @param {object[]} messages - Only new messages to check
71
- * @param {string} myRole - This minion's role in the project
71
+ * @param {string} myRole - This minion's role in the relevant project (or 'engineer' default)
72
72
  * @returns {boolean}
73
73
  */
74
74
  function isMentioned(thread, messages, myRole) {
@@ -130,14 +130,20 @@ async function pollOnce() {
130
130
  * Process a single thread: check for new activity and evaluate if needed.
131
131
  */
132
132
  async function processThread(thread, myProjects, now) {
133
- // For workspace threads, create a synthetic project context
134
- // For project threads, find role via membership
135
- let myProject
136
- if (thread.scope === 'workspace') {
137
- myProject = { id: null, name: 'Workspace', role: 'engineer' }
138
- } else {
139
- myProject = myProjects.find(p => p.id === thread.project_id)
140
- if (!myProject) return // Not a member
133
+ // Determine role from linked projects (use first matching project)
134
+ let myRole = 'engineer'
135
+ let projectContext = null
136
+ const linkedProjects = thread.linked_projects || []
137
+
138
+ if (linkedProjects.length > 0) {
139
+ for (const lp of linkedProjects) {
140
+ const myProject = myProjects.find(p => p.id === lp.id)
141
+ if (myProject) {
142
+ myRole = myProject.role || 'engineer'
143
+ projectContext = myProject
144
+ break
145
+ }
146
+ }
141
147
  }
142
148
 
143
149
  const state = readState.get(thread.id) || { lastMessageCount: 0, lastEvalAt: 0 }
@@ -172,7 +178,7 @@ async function processThread(thread, myProjects, now) {
172
178
  readState.set(thread.id, { ...state, lastMessageCount: messageCount })
173
179
 
174
180
  // Check mentions first (no LLM needed to decide relevance)
175
- const mentioned = isMentioned(thread, newMessages, myProject.role)
181
+ const mentioned = isMentioned(thread, newMessages, myRole)
176
182
 
177
183
  // If not mentioned and cooldown not expired, skip LLM evaluation
178
184
  if (!mentioned && now - state.lastEvalAt < EVAL_COOLDOWN_MS) {
@@ -187,14 +193,14 @@ async function processThread(thread, myProjects, now) {
187
193
  }
188
194
 
189
195
  // LLM evaluation
190
- await evaluateWithLlm(thread, detail.thread, messages, newMessages, myProject, mentioned)
196
+ await evaluateWithLlm(thread, detail.thread, messages, newMessages, myRole, projectContext, mentioned)
191
197
  readState.set(thread.id, { lastMessageCount: messageCount, lastEvalAt: now })
192
198
  }
193
199
 
194
200
  /**
195
201
  * Use LLM to evaluate thread and potentially respond.
196
202
  */
197
- async function evaluateWithLlm(threadSummary, threadDetail, allMessages, newMessages, myProject, mentioned) {
203
+ async function evaluateWithLlm(threadSummary, threadDetail, allMessages, newMessages, myRole, projectContext, mentioned) {
198
204
  // Build conversation context (limit to last 20 messages to control tokens)
199
205
  const recentMessages = allMessages.slice(-20)
200
206
  const messageHistory = recentMessages
@@ -218,10 +224,21 @@ async function evaluateWithLlm(threadSummary, threadDetail, allMessages, newMess
218
224
  ctx.attempted_resolution ? `試行済み: ${ctx.attempted_resolution}` : '',
219
225
  ].filter(Boolean).join('\n')
220
226
 
221
- const isWorkspace = threadDetail.scope === 'workspace'
222
- const scopeContext = isWorkspace
223
- ? `あなたはワークスペースのメンバー(ID: ${config.MINION_ID})です。\nこれはプロジェクトに属さないワークスペーススレッドです(カテゴリ: ${threadDetail.category || 'general'})。`
224
- : `あなたはプロジェクト「${myProject.name}」のチームメンバー(ロール: ${myProject.role}、ID: ${config.MINION_ID})です。`
227
+ // Build scope context from linked projects
228
+ const linkedProjects = threadDetail.linked_projects || []
229
+ let scopeContext
230
+ if (projectContext) {
231
+ scopeContext = `あなたはプロジェクト「${projectContext.name}」のチームメンバー(ロール: ${myRole}、ID: ${config.MINION_ID})です。`
232
+ } else if (linkedProjects.length > 0) {
233
+ const projectNames = linkedProjects.map(p => p.name).join(', ')
234
+ scopeContext = `あなたはワークスペースのメンバー(ID: ${config.MINION_ID})です。\nこのスレッドは以下のプロジェクトに紐づいています: ${projectNames}`
235
+ } else {
236
+ scopeContext = `あなたはワークスペースのメンバー(ID: ${config.MINION_ID})です。\nこれはプロジェクトに紐づかないワークスペーススレッドです(カテゴリ: ${threadDetail.category || 'general'})。`
237
+ }
238
+
239
+ const roleGuidance = projectContext
240
+ ? `- 自分のロール(${myRole})に関連する話題か`
241
+ : '- ワークスペーススレッドではすべてのミニオンが参加可能。自分が貢献できる場合は積極的に参加する'
225
242
 
226
243
  const prompt = `${scopeContext}
227
244
  以下のスレッドに対して、あなたが返信すべきかどうかを判断し、返信する場合はその内容を生成してください。
@@ -247,7 +264,7 @@ ${messageHistory || '(メッセージなし)'}
247
264
  判断基準:
248
265
  - 自分が起票したスレッドの場合、他のメンバーの回答を待つべき(追加情報がある場合を除く)
249
266
  - メンション対象が特定のロールやミニオンに限定されている場合、自分が対象でなければ静観する
250
- ${isWorkspace ? '- ワークスペーススレッドではすべてのミニオンが参加可能。自分が貢献できる場合は積極的に参加する' : `- 自分のロール(${myProject.role})に関連する話題か`}
267
+ ${roleGuidance}
251
268
  - 自分が貢献できる知見や意見があるか
252
269
  - 既に十分な回答がある場合は重複を避ける
253
270
  - 人間に聞くべき場合は @user メンションを含めて返信する
@@ -289,13 +306,17 @@ ${isWorkspace ? '- ワークスペーススレッドではすべてのミニオ
289
306
  if (parsed.todo && parsed.todo.title) {
290
307
  try {
291
308
  const todoStore = require('../stores/todo-store')
309
+ // Use first linked project ID if available
310
+ const linkedProjectId = (threadSummary.linked_projects && threadSummary.linked_projects.length > 0)
311
+ ? threadSummary.linked_projects[0].id
312
+ : null
292
313
  todoStore.add({
293
314
  title: parsed.todo.title,
294
315
  priority: parsed.todo.priority || 'normal',
295
316
  due_at: parsed.todo.due_at || null,
296
317
  source_type: 'thread',
297
318
  source_id: threadSummary.id,
298
- project_id: threadSummary.project_id || null,
319
+ project_id: linkedProjectId,
299
320
  })
300
321
  console.log(`[ThreadWatcher] Created TODO from thread: "${parsed.todo.title}"`)
301
322
  } catch (err) {
@@ -309,17 +330,20 @@ ${isWorkspace ? '- ワークスペーススレッドではすべてのミニオ
309
330
 
310
331
  /**
311
332
  * Fallback: memory-only matching when LLM is not available.
333
+ * Uses linked projects to search for relevant memories.
312
334
  */
313
335
  async function fallbackMemoryMatch(thread) {
314
336
  try {
315
- // Workspace threads have no project_id — skip memory matching
316
- if (!thread.project_id) return
337
+ const linkedProjects = thread.linked_projects || []
338
+ if (linkedProjects.length === 0) return
317
339
 
340
+ // Search memories from the first linked project
341
+ const projectId = linkedProjects[0].id
318
342
  const ctx = thread.context || {}
319
343
  const category = ctx.category || ''
320
344
  const searchParam = category ? `&category=${category}` : ''
321
345
  const memData = await api.request(
322
- `/project-memories?project_id=${thread.project_id}${searchParam}`
346
+ `/project-memories?project_id=${projectId}${searchParam}`
323
347
  )
324
348
 
325
349
  if (!memData.memories || memData.memories.length === 0) return
@@ -287,27 +287,26 @@ GET `/api/daemons/status` response:
287
287
 
288
288
  ### Threads (スレッド)
289
289
 
290
- プロジェクト内またはワークスペースレベルのコミュニケーションチャネル。ブロッカー共有やチーム議論に使う。
290
+ ワークスペースレベルのコミュニケーションチャネル。ブロッカー共有やチーム議論に使う。
291
291
  リクエストは HQ にプロキシされる。
292
292
 
293
- **2つのスコープ:**
294
- - **Project** (`scope: "project"`): プロジェクトに紐づくスレッド。`project_id` 必須。プロジェクトメンバーが参加。
295
- - **Workspace** (`scope: "workspace"`): プロジェクトに属さないスレッド。ルーティンのブロッカー、全体連絡等。オーナーの全ミニオンが参加。
293
+ すべてのスレッドはワークスペースに所属し、プロジェクトへの紐づけは多対多(`thread_project_links`)で管理される。
294
+ 1つのスレッドを複数プロジェクトにリンクしたり、プロジェクトに紐づけずにワークスペース全体のスレッドとして使うこともできる。
296
295
 
297
296
  | Method | Endpoint | Description |
298
297
  |--------|----------|-------------|
299
- | GET | `/api/threads` | オープンスレッド一覧(プロジェクト+ワークスペース) |
298
+ | GET | `/api/threads` | オープンスレッド一覧 |
300
299
  | POST | `/api/threads` | スレッドを起票(初回メッセージを同時作成) |
301
300
  | GET | `/api/threads/:id` | スレッド詳細 + メッセージ一覧 |
302
301
  | POST | `/api/threads/:id/messages` | スレッドにメッセージを投稿 |
303
302
  | POST | `/api/threads/:id/resolve` | スレッドを解決済みにする |
304
303
  | POST | `/api/threads/:id/cancel` | スレッドをキャンセル |
305
- | DELETE | `/api/threads/:id` | スレッドを完全削除(project: PMのみ、workspace: オーナーの全ミニオン) |
304
+ | DELETE | `/api/threads/:id` | スレッドを完全削除 |
306
305
 
307
- POST `/api/threads` body (プロジェクトスレッド — ヘルプ):
306
+ POST `/api/threads` body (プロジェクト紐づきヘルプスレッド):
308
307
  ```json
309
308
  {
310
- "project_id": "uuid",
309
+ "project_ids": ["uuid"],
311
310
  "thread_type": "help",
312
311
  "title": "ランサーズの2FA認証コードが必要",
313
312
  "content": "ログイン時に2段階認証を要求された。メールで届く6桁コードの入力が必要。",
@@ -322,7 +321,6 @@ POST `/api/threads` body (プロジェクトスレッド — ヘルプ):
322
321
  POST `/api/threads` body (ワークスペーススレッド — ルーティンのブロッカー):
323
322
  ```json
324
323
  {
325
- "scope": "workspace",
326
324
  "category": "general",
327
325
  "thread_type": "help",
328
326
  "title": "朝作業ルーティン: APIキーの期限切れ",
@@ -335,10 +333,10 @@ POST `/api/threads` body (ワークスペーススレッド — ルーティン
335
333
  }
336
334
  ```
337
335
 
338
- POST `/api/threads` body (プロジェクトスレッド — ディスカッション):
336
+ POST `/api/threads` body (プロジェクト紐づきディスカッション):
339
337
  ```json
340
338
  {
341
- "project_id": "uuid",
339
+ "project_ids": ["uuid"],
342
340
  "thread_type": "discussion",
343
341
  "title": "デプロイ手順の確認",
344
342
  "content": "本番デプロイ前にステージングで確認するフローに変えたい。意見ある?",
@@ -348,19 +346,19 @@ POST `/api/threads` body (プロジェクトスレッド — ディスカッシ
348
346
 
349
347
  | Field | Type | Required | Description |
350
348
  |-------|------|----------|-------------|
351
- | `scope` | string | No | `project`(デフォルト)or `workspace` |
352
- | `project_id` | string | scope=project時 Yes | プロジェクト UUID |
353
- | `category` | string | No | ワークスペーススレッドのカテゴリ: `general`(デフォルト), `ops`, `standup` |
349
+ | `project_id` | string | No | リンクするプロジェクトUUID(単一、後方互換) |
350
+ | `project_ids` | string[] | No | リンクするプロジェクトUUID(複数対応) |
351
+ | `category` | string | No | カテゴリ: `general`(デフォルト), `ops`, `standup` |
354
352
  | `thread_type` | string | No | `help`(デフォルト)or `discussion` |
355
353
  | `title` | string | Yes | スレッドの要約 |
356
354
  | `content` | string | Yes | スレッド本文(thread_messagesの最初のメッセージとして保存) |
357
355
  | `mentions` | string[] | No | メンション対象。形式: `role:engineer`, `role:pm`, `minion:<minion_id>`, `user` |
358
356
  | `context` | object | No | 任意のメタデータ(category, urgency, workflow_execution_id等) |
359
357
 
360
- **scope の使い分け:**
361
- - ワークフロー実行中(プロジェクトあり)→ `scope: "project"` + `project_id`
362
- - ルーティン実行中(プロジェクトなし)→ `scope: "workspace"`
363
- - プロジェクト外の一般的な質問・報告 → `scope: "workspace"` + `category`
358
+ **プロジェクト紐づけの使い分け:**
359
+ - ワークフロー実行中(プロジェクトあり)→ `project_ids: ["uuid"]`
360
+ - ルーティン実行中(プロジェクトなし)→ `project_ids` を省略
361
+ - プロジェクト外の一般的な質問・報告 → `project_ids` を省略 + `category`
364
362
 
365
363
  **thread_type の違い:**
366
364
  - `help`: ブロッカー解決。`resolve` で解決
@@ -447,8 +445,8 @@ POST `/api/project-memories` body:
447
445
  **推奨ワークフロー:**
448
446
  1. ブロッカー発生 → プロジェクトコンテキストの場合は `GET /api/project-memories?project_id=...&category=auth&search=2fa` で既知の知見を検索
449
447
  2. 該当あり → 知見に基づいて自己解決 or 即エスカレーション
450
- 3. 該当なし → `POST /api/threads` でブロッカー起票(プロジェクトなら `scope: "project"` + `project_id`、ルーティンなら `scope: "workspace"`)
451
- 4. 解決後 → プロジェクトスレッドの場合は `POST /api/project-memories` で知見を蓄積
448
+ 3. 該当なし → `POST /api/threads` でブロッカー起票(プロジェクトありなら `project_ids: ["uuid"]`、なしなら省略)
449
+ 4. 解決後 → プロジェクト紐づきスレッドの場合は `POST /api/project-memories` で知見を蓄積
452
450
 
453
451
  ### Email
454
452
 
@@ -909,8 +907,8 @@ Response:
909
907
 
910
908
  | Method | Endpoint | Description |
911
909
  |--------|----------|-------------|
912
- | POST | `/api/minion/threads` | スレッドを起票(scope, category対応。初回メッセージを同時作成) |
913
- | GET | `/api/minion/threads/open` | 未解決スレッド一覧(`?scope=all\|project\|workspace`) |
910
+ | POST | `/api/minion/threads` | スレッドを起票(project_ids, category対応。初回メッセージを同時作成) |
911
+ | GET | `/api/minion/threads/open` | 未解決スレッド一覧(`?project_id=uuid` で絞り込み可) |
914
912
  | GET | `/api/minion/threads/:id` | スレッド詳細 + メッセージ一覧 |
915
913
  | POST | `/api/minion/threads/:id/messages` | スレッドにメッセージを投稿 |
916
914
  | PATCH | `/api/minion/threads/:id/resolve` | スレッドを解決済みにする。Body: `{resolution}` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "3.9.0",
3
+ "version": "3.10.0",
4
4
  "description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
5
5
  "main": "linux/server.js",
6
6
  "bin": {
package/rules/core.md CHANGED
@@ -198,10 +198,10 @@ Routine 実行中は以下もtmuxセッション環境で利用可能:
198
198
 
199
199
  2-b. 該当なし or 自己解決不可 → ヘルプスレッドを起票
200
200
 
201
- ■ ワークフロー実行中(プロジェクトあり)→ プロジェクトスレッド:
201
+ ■ ワークフロー実行中(プロジェクトあり)→ プロジェクト紐づきスレッド:
202
202
  POST /api/threads
203
203
  {
204
- "project_id": "...",
204
+ "project_ids": ["..."],
205
205
  "thread_type": "help",
206
206
  "title": "問題の要約(1行)",
207
207
  "content": "状況の詳細説明",
@@ -215,7 +215,6 @@ Routine 実行中は以下もtmuxセッション環境で利用可能:
215
215
  ■ ルーティン実行中(プロジェクトなし)→ ワークスペーススレッド:
216
216
  POST /api/threads
217
217
  {
218
- "scope": "workspace",
219
218
  "category": "general",
220
219
  "thread_type": "help",
221
220
  "title": "問題の要約(1行)",
@@ -229,7 +228,7 @@ Routine 実行中は以下もtmuxセッション環境で利用可能:
229
228
 
230
229
  3. スレッドの返信を待つ(thread_watcher が自動監視)
231
230
 
232
- 4. 解決後 → プロジェクトスレッドの場合はプロジェクトメモリーに知見を保存
231
+ 4. 解決後 → プロジェクト紐づきスレッドの場合はプロジェクトメモリーに知見を保存
233
232
  POST /api/project-memories
234
233
  {
235
234
  "project_id": "...",
@@ -240,13 +239,13 @@ Routine 実行中は以下もtmuxセッション環境で利用可能:
240
239
  }
241
240
  ```
242
241
 
243
- ### スレッドスコープの使い分け
242
+ ### スレッドのプロジェクト紐づけ
244
243
 
245
- | 状況 | スコープ | 説明 |
246
- |------|---------|------|
247
- | ワークフロー実行中のブロッカー | `scope: "project"` (デフォルト) | `project_id` 必須。プロジェクトメンバーが参加 |
248
- | ルーティン実行中のブロッカー | `scope: "workspace"` | `project_id` 不要。オーナーの全ミニオンが参加 |
249
- | プロジェクト外の一般的な質問・報告 | `scope: "workspace"` | カテゴリ: `general`, `ops`, `standup` |
244
+ | 状況 | 設定 | 説明 |
245
+ |------|------|------|
246
+ | ワークフロー実行中のブロッカー | `project_ids: ["uuid"]` | プロジェクトに紐づけ。プロジェクトビューに表示される |
247
+ | ルーティン実行中のブロッカー | `project_ids` を省略 | プロジェクトに紐づけない。ワークスペース全体に表示 |
248
+ | プロジェクト外の一般的な質問・報告 | `project_ids` を省略 + `category` | カテゴリ: `general`, `ops`, `standup` |
250
249
 
251
250
  ### メンションの使い分け
252
251