@geekbeer/minion 3.8.1 → 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 +6 -5
- package/core/db.js +17 -0
- package/core/lib/end-of-day.js +3 -0
- package/core/lib/thread-watcher.js +46 -22
- package/core/routes/memory.js +17 -10
- package/core/stores/memory-store.js +70 -21
- package/docs/api-reference.md +31 -26
- package/linux/routes/chat.js +3 -0
- package/package.json +1 -1
- package/rules/core.md +9 -10
- package/settings/permissions.json +2 -1
- package/win/routes/chat.js +3 -0
package/core/api.js
CHANGED
|
@@ -87,8 +87,8 @@ async function sendHeartbeat(data) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* Create a thread
|
|
91
|
-
* @param {object} data - { project_id?,
|
|
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 {
|
|
103
|
+
* @param {string} [projectId] - Optional project ID to filter by
|
|
104
104
|
* @returns {Promise<{ threads: object[] }>}
|
|
105
105
|
*/
|
|
106
|
-
async function getOpenThreads(
|
|
107
|
-
|
|
106
|
+
async function getOpenThreads(projectId) {
|
|
107
|
+
const params = projectId ? `?project_id=${projectId}` : ''
|
|
108
|
+
return request(`/threads/open${params}`)
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
/**
|
package/core/db.js
CHANGED
|
@@ -425,6 +425,23 @@ function migrateSchema(db) {
|
|
|
425
425
|
|
|
426
426
|
console.log('[DB] Migration 2 complete: emails FTS5 added')
|
|
427
427
|
}
|
|
428
|
+
|
|
429
|
+
if (currentVersion < 3) {
|
|
430
|
+
console.log('[DB] Migration 3: Adding project_id to memories...')
|
|
431
|
+
|
|
432
|
+
db.exec(`
|
|
433
|
+
-- Add nullable project_id column to memories table.
|
|
434
|
+
-- NULL = universal memory (cross-project knowledge).
|
|
435
|
+
-- Set = project-scoped memory.
|
|
436
|
+
ALTER TABLE memories ADD COLUMN project_id TEXT DEFAULT NULL;
|
|
437
|
+
|
|
438
|
+
CREATE INDEX idx_memories_project_id ON memories(project_id);
|
|
439
|
+
|
|
440
|
+
INSERT INTO schema_version (version) VALUES (3);
|
|
441
|
+
`)
|
|
442
|
+
|
|
443
|
+
console.log('[DB] Migration 3 complete: memories.project_id added')
|
|
444
|
+
}
|
|
428
445
|
}
|
|
429
446
|
|
|
430
447
|
/**
|
package/core/lib/end-of-day.js
CHANGED
|
@@ -42,8 +42,10 @@ async function runEndOfDay({ runQuickLlmCall, clearSession = false }) {
|
|
|
42
42
|
- title: 短いタイトル
|
|
43
43
|
- category: user(ユーザーの好み)/ feedback(フィードバック)/ project(プロジェクト情報)/ reference(参照情報)のいずれか
|
|
44
44
|
- content: 詳細な内容
|
|
45
|
+
- project_id: この学びが特定プロジェクトに固有の場合はそのプロジェクトID(UUID)。汎用的なノウハウやユーザーの好みなど、プロジェクト横断で活用すべき学びの場合はnull
|
|
45
46
|
|
|
46
47
|
学びがない場合は空配列にしてください。すでに常識的なことは含めないでください。
|
|
48
|
+
特定のプロジェクトの技術スタック・ルール・設計判断などはproject_idを設定し、汎用的なスキルや知見はnullにしてください。
|
|
47
49
|
|
|
48
50
|
会話:
|
|
49
51
|
${conversationText}
|
|
@@ -83,6 +85,7 @@ JSON形式のみで回答してください(\`\`\`jsonブロックは不要)
|
|
|
83
85
|
title: learning.title,
|
|
84
86
|
category: learning.category || 'reference',
|
|
85
87
|
content: learning.content,
|
|
88
|
+
project_id: learning.project_id || null,
|
|
86
89
|
})
|
|
87
90
|
memoriesAdded++
|
|
88
91
|
}
|
|
@@ -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
|
-
*
|
|
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
|
-
//
|
|
134
|
-
|
|
135
|
-
let
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
${
|
|
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:
|
|
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
|
-
|
|
316
|
-
if (
|
|
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=${
|
|
346
|
+
`/project-memories?project_id=${projectId}${searchParam}`
|
|
323
347
|
)
|
|
324
348
|
|
|
325
349
|
if (!memData.memories || memData.memories.length === 0) return
|
package/core/routes/memory.js
CHANGED
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
* Provides CRUD + search for the minion's long-term memory entries.
|
|
5
5
|
* Memory is stored in SQLite with FTS5 full-text search.
|
|
6
6
|
*
|
|
7
|
+
* project_id scoping:
|
|
8
|
+
* - Entries with project_id are scoped to a specific project.
|
|
9
|
+
* - Entries without project_id are universal (cross-project knowledge).
|
|
10
|
+
* - Queries with ?project_id=xxx return scoped + universal entries.
|
|
11
|
+
*
|
|
7
12
|
* Endpoints:
|
|
8
13
|
* GET /api/memory - List all entries (or search with ?search=keyword)
|
|
14
|
+
* Optional: ?project_id=xxx to scope results
|
|
9
15
|
* GET /api/memory/:id - Get a single entry
|
|
10
|
-
* POST /api/memory - Create a new entry
|
|
11
|
-
* PUT /api/memory/:id - Update an entry
|
|
16
|
+
* POST /api/memory - Create a new entry (optional project_id in body)
|
|
17
|
+
* PUT /api/memory/:id - Update an entry (optional project_id in body)
|
|
12
18
|
* DELETE /api/memory/:id - Delete an entry
|
|
13
19
|
*/
|
|
14
20
|
|
|
@@ -28,13 +34,13 @@ async function memoryRoutes(fastify) {
|
|
|
28
34
|
return { success: false, error: 'Unauthorized' }
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
const { search } = request.query || {}
|
|
37
|
+
const { search, project_id } = request.query || {}
|
|
32
38
|
if (search) {
|
|
33
|
-
const entries = memoryStore.searchEntries(search)
|
|
39
|
+
const entries = memoryStore.searchEntries(search, { projectId: project_id })
|
|
34
40
|
return { success: true, entries }
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
const entries = memoryStore.listEntries()
|
|
43
|
+
const entries = memoryStore.listEntries({ projectId: project_id })
|
|
38
44
|
return { success: true, entries }
|
|
39
45
|
})
|
|
40
46
|
|
|
@@ -61,14 +67,14 @@ async function memoryRoutes(fastify) {
|
|
|
61
67
|
return { success: false, error: 'Unauthorized' }
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
const { title, category, content } = request.body || {}
|
|
70
|
+
const { title, category, content, project_id } = request.body || {}
|
|
65
71
|
if (!title || !content) {
|
|
66
72
|
reply.code(400)
|
|
67
73
|
return { success: false, error: 'title and content are required' }
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
const entry = memoryStore.saveEntry({ title, category, content })
|
|
71
|
-
console.log(`[Memory] Created entry: ${entry.id} (${entry.title})`)
|
|
76
|
+
const entry = memoryStore.saveEntry({ title, category, content, project_id })
|
|
77
|
+
console.log(`[Memory] Created entry: ${entry.id} (${entry.title})${entry.project_id ? ` [project:${entry.project_id}]` : ' [universal]'}`)
|
|
72
78
|
return { success: true, entry }
|
|
73
79
|
})
|
|
74
80
|
|
|
@@ -85,15 +91,16 @@ async function memoryRoutes(fastify) {
|
|
|
85
91
|
return { success: false, error: 'Entry not found' }
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
const { title, category, content } = request.body || {}
|
|
94
|
+
const { title, category, content, project_id } = request.body || {}
|
|
89
95
|
const entry = memoryStore.saveEntry({
|
|
90
96
|
id: request.params.id,
|
|
91
97
|
title: title || existing.title,
|
|
92
98
|
category: category || existing.category,
|
|
93
99
|
content: content !== undefined ? content : existing.content,
|
|
100
|
+
project_id: project_id !== undefined ? project_id : existing.project_id,
|
|
94
101
|
})
|
|
95
102
|
|
|
96
|
-
console.log(`[Memory] Updated entry: ${entry.id} (${entry.title})`)
|
|
103
|
+
console.log(`[Memory] Updated entry: ${entry.id} (${entry.title})${entry.project_id ? ` [project:${entry.project_id}]` : ' [universal]'}`)
|
|
97
104
|
return { success: true, entry }
|
|
98
105
|
})
|
|
99
106
|
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* Supports full-text search via FTS5.
|
|
5
5
|
*
|
|
6
6
|
* Categories: user, feedback, project, reference
|
|
7
|
+
*
|
|
8
|
+
* project_id scoping:
|
|
9
|
+
* - NULL project_id = universal memory (cross-project knowledge/know-how)
|
|
10
|
+
* - Set project_id = project-scoped memory
|
|
11
|
+
* - When querying with a project_id, returns both scoped + universal entries
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
const crypto = require('crypto')
|
|
@@ -13,17 +18,31 @@ const VALID_CATEGORIES = ['user', 'feedback', 'project', 'reference']
|
|
|
13
18
|
|
|
14
19
|
/**
|
|
15
20
|
* List all memory entries (metadata + excerpt, no full body).
|
|
16
|
-
* @
|
|
21
|
+
* @param {{ projectId?: string }} [opts] - Optional filter options
|
|
22
|
+
* - projectId: If set, returns entries for that project + universal (NULL) entries.
|
|
23
|
+
* If omitted, returns all entries.
|
|
24
|
+
* @returns {Array<{ id, title, category, project_id, created_at, updated_at, excerpt }>}
|
|
17
25
|
*/
|
|
18
|
-
function listEntries() {
|
|
26
|
+
function listEntries(opts = {}) {
|
|
19
27
|
const db = getDb()
|
|
20
|
-
const
|
|
21
|
-
|
|
28
|
+
const { projectId } = opts
|
|
29
|
+
|
|
30
|
+
if (projectId) {
|
|
31
|
+
return db.prepare(`
|
|
32
|
+
SELECT id, title, category, project_id, created_at, updated_at,
|
|
33
|
+
substr(content, 1, 200) as excerpt
|
|
34
|
+
FROM memories
|
|
35
|
+
WHERE project_id = ? OR project_id IS NULL
|
|
36
|
+
ORDER BY updated_at DESC
|
|
37
|
+
`).all(projectId)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return db.prepare(`
|
|
41
|
+
SELECT id, title, category, project_id, created_at, updated_at,
|
|
22
42
|
substr(content, 1, 200) as excerpt
|
|
23
43
|
FROM memories
|
|
24
44
|
ORDER BY updated_at DESC
|
|
25
45
|
`).all()
|
|
26
|
-
return rows
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
/**
|
|
@@ -39,7 +58,8 @@ function loadEntry(id) {
|
|
|
39
58
|
|
|
40
59
|
/**
|
|
41
60
|
* Save (create or update) a memory entry.
|
|
42
|
-
* @param {{ id?, title, category, content }} entry
|
|
61
|
+
* @param {{ id?, title, category, content, project_id? }} entry
|
|
62
|
+
* - project_id: UUID of the project this memory belongs to, or null/undefined for universal
|
|
43
63
|
* @returns {object} The saved entry
|
|
44
64
|
*/
|
|
45
65
|
function saveEntry(entry) {
|
|
@@ -47,6 +67,7 @@ function saveEntry(entry) {
|
|
|
47
67
|
const now = new Date().toISOString()
|
|
48
68
|
const id = entry.id || crypto.randomBytes(6).toString('hex')
|
|
49
69
|
const category = VALID_CATEGORIES.includes(entry.category) ? entry.category : 'reference'
|
|
70
|
+
const projectId = entry.project_id || null
|
|
50
71
|
|
|
51
72
|
const existing = db.prepare('SELECT created_at FROM memories WHERE id = ?').get(id)
|
|
52
73
|
|
|
@@ -55,20 +76,21 @@ function saveEntry(entry) {
|
|
|
55
76
|
title: entry.title || 'Untitled',
|
|
56
77
|
category,
|
|
57
78
|
content: entry.content || '',
|
|
79
|
+
project_id: projectId,
|
|
58
80
|
created_at: existing ? existing.created_at : now,
|
|
59
81
|
updated_at: now,
|
|
60
82
|
}
|
|
61
83
|
|
|
62
84
|
if (existing) {
|
|
63
85
|
db.prepare(`
|
|
64
|
-
UPDATE memories SET title = ?, category = ?, content = ?, updated_at = ?
|
|
86
|
+
UPDATE memories SET title = ?, category = ?, content = ?, project_id = ?, updated_at = ?
|
|
65
87
|
WHERE id = ?
|
|
66
|
-
`).run(full.title, full.category, full.content, full.updated_at, full.id)
|
|
88
|
+
`).run(full.title, full.category, full.content, full.project_id, full.updated_at, full.id)
|
|
67
89
|
} else {
|
|
68
90
|
db.prepare(`
|
|
69
|
-
INSERT INTO memories (id, title, category, content, created_at, updated_at)
|
|
70
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
71
|
-
`).run(full.id, full.title, full.category, full.content, full.created_at, full.updated_at)
|
|
91
|
+
INSERT INTO memories (id, title, category, content, project_id, created_at, updated_at)
|
|
92
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
93
|
+
`).run(full.id, full.title, full.category, full.content, full.project_id, full.created_at, full.updated_at)
|
|
72
94
|
}
|
|
73
95
|
|
|
74
96
|
return full
|
|
@@ -89,11 +111,14 @@ function deleteEntry(id) {
|
|
|
89
111
|
* Search memory entries using FTS5 full-text search (trigram tokenizer).
|
|
90
112
|
* Falls back to LIKE for queries shorter than 3 characters (trigram minimum).
|
|
91
113
|
* @param {string} query - Search query
|
|
92
|
-
* @param {number}
|
|
93
|
-
*
|
|
114
|
+
* @param {{ limit?: number, projectId?: string }} [opts] - Search options
|
|
115
|
+
* - limit: Max results (default 20)
|
|
116
|
+
* - projectId: If set, returns matches for that project + universal (NULL) entries
|
|
117
|
+
* @returns {Array<{ id, title, category, project_id, content, created_at, updated_at }>}
|
|
94
118
|
*/
|
|
95
|
-
function searchEntries(query,
|
|
119
|
+
function searchEntries(query, opts = {}) {
|
|
96
120
|
const db = getDb()
|
|
121
|
+
const { limit = 20, projectId } = typeof opts === 'number' ? { limit: opts } : opts
|
|
97
122
|
const terms = query.split(/\s+/).filter(Boolean)
|
|
98
123
|
if (terms.length === 0) return []
|
|
99
124
|
|
|
@@ -108,11 +133,18 @@ function searchEntries(query, limit = 20) {
|
|
|
108
133
|
const like = `%${t}%`
|
|
109
134
|
return [like, like]
|
|
110
135
|
})
|
|
136
|
+
|
|
137
|
+
let projectFilter = ''
|
|
138
|
+
if (projectId) {
|
|
139
|
+
projectFilter = ' AND (project_id = ? OR project_id IS NULL)'
|
|
140
|
+
params.push(projectId)
|
|
141
|
+
}
|
|
142
|
+
|
|
111
143
|
params.push(limit)
|
|
112
144
|
return db.prepare(`
|
|
113
|
-
SELECT id, title, category, content, created_at, updated_at
|
|
145
|
+
SELECT id, title, category, project_id, content, created_at, updated_at
|
|
114
146
|
FROM memories
|
|
115
|
-
WHERE ${conditions}
|
|
147
|
+
WHERE ${conditions}${projectFilter}
|
|
116
148
|
ORDER BY updated_at DESC
|
|
117
149
|
LIMIT ?
|
|
118
150
|
`).all(...params)
|
|
@@ -120,8 +152,21 @@ function searchEntries(query, limit = 20) {
|
|
|
120
152
|
|
|
121
153
|
// FTS5 trigram search: wrap each term in double quotes for exact substring match
|
|
122
154
|
const sanitized = terms.map(t => `"${t.replace(/"/g, '')}"`).join(' ')
|
|
155
|
+
|
|
156
|
+
if (projectId) {
|
|
157
|
+
return db.prepare(`
|
|
158
|
+
SELECT m.id, m.title, m.category, m.project_id, m.content, m.created_at, m.updated_at
|
|
159
|
+
FROM memories m
|
|
160
|
+
JOIN memories_fts f ON m.rowid = f.rowid
|
|
161
|
+
WHERE memories_fts MATCH ?
|
|
162
|
+
AND (m.project_id = ? OR m.project_id IS NULL)
|
|
163
|
+
ORDER BY rank
|
|
164
|
+
LIMIT ?
|
|
165
|
+
`).all(sanitized, projectId, limit)
|
|
166
|
+
}
|
|
167
|
+
|
|
123
168
|
return db.prepare(`
|
|
124
|
-
SELECT m.id, m.title, m.category, m.content, m.created_at, m.updated_at
|
|
169
|
+
SELECT m.id, m.title, m.category, m.project_id, m.content, m.created_at, m.updated_at
|
|
125
170
|
FROM memories m
|
|
126
171
|
JOIN memories_fts f ON m.rowid = f.rowid
|
|
127
172
|
WHERE memories_fts MATCH ?
|
|
@@ -133,18 +178,22 @@ function searchEntries(query, limit = 20) {
|
|
|
133
178
|
/**
|
|
134
179
|
* Get a context snippet suitable for chat injection.
|
|
135
180
|
* Returns the most important memory entries as a formatted string.
|
|
136
|
-
* @param {
|
|
181
|
+
* @param {{ maxChars?: number, projectId?: string }} [opts] - Options
|
|
182
|
+
* - maxChars: Maximum characters to return (default 2000)
|
|
183
|
+
* - projectId: If set, includes project-scoped + universal entries
|
|
137
184
|
* @returns {string}
|
|
138
185
|
*/
|
|
139
|
-
function getContextSnippet(
|
|
140
|
-
const
|
|
186
|
+
function getContextSnippet(opts = {}) {
|
|
187
|
+
const { maxChars = 2000, projectId } = typeof opts === 'number' ? { maxChars: opts } : opts
|
|
188
|
+
const entries = listEntries({ projectId })
|
|
141
189
|
if (entries.length === 0) return ''
|
|
142
190
|
|
|
143
191
|
const parts = []
|
|
144
192
|
let totalLen = 0
|
|
145
193
|
|
|
146
194
|
for (const e of entries) {
|
|
147
|
-
const
|
|
195
|
+
const scope = e.project_id ? '[project-scoped]' : '[universal]'
|
|
196
|
+
const line = `${scope} [${e.category}] ${e.title}: ${e.excerpt}`
|
|
148
197
|
if (totalLen + line.length > maxChars) break
|
|
149
198
|
parts.push(line)
|
|
150
199
|
totalLen += line.length + 1
|
package/docs/api-reference.md
CHANGED
|
@@ -83,19 +83,26 @@ Full-text search supported via FTS5.
|
|
|
83
83
|
|
|
84
84
|
| Method | Endpoint | Description |
|
|
85
85
|
|--------|----------|-------------|
|
|
86
|
-
| GET | `/api/memory` | List all memory entries (id, title, category, excerpt, timestamps) |
|
|
86
|
+
| GET | `/api/memory` | List all memory entries (id, title, category, project_id, excerpt, timestamps) |
|
|
87
87
|
| GET | `/api/memory?search=keyword` | Full-text search on title and content (FTS5) |
|
|
88
|
+
| GET | `/api/memory?project_id=xxx` | List entries scoped to project + universal entries |
|
|
89
|
+
| GET | `/api/memory?search=keyword&project_id=xxx` | Search within project scope + universal |
|
|
88
90
|
| GET | `/api/memory/:id` | Get full memory entry |
|
|
89
|
-
| POST | `/api/memory` | Create memory entry. Body: `{title, category, content}` |
|
|
90
|
-
| PUT | `/api/memory/:id` | Update memory entry. Body: `{title?, category?, content?}` |
|
|
91
|
+
| POST | `/api/memory` | Create memory entry. Body: `{title, category, content, project_id?}` |
|
|
92
|
+
| PUT | `/api/memory/:id` | Update memory entry. Body: `{title?, category?, content?, project_id?}` |
|
|
91
93
|
| DELETE | `/api/memory/:id` | Delete a memory entry |
|
|
92
94
|
|
|
95
|
+
**Project scoping**: `project_id` is optional. When set, the entry is scoped to that project.
|
|
96
|
+
When `project_id` is omitted (NULL), the entry is universal (cross-project knowledge).
|
|
97
|
+
Queries with `?project_id=xxx` return both project-scoped AND universal entries.
|
|
98
|
+
|
|
93
99
|
POST/PUT body:
|
|
94
100
|
```json
|
|
95
101
|
{
|
|
96
102
|
"title": "ユーザーはレビューで日本語を好む",
|
|
97
103
|
"category": "user",
|
|
98
|
-
"content": "コードレビューのコメントは日本語で記述すること。"
|
|
104
|
+
"content": "コードレビューのコメントは日本語で記述すること。",
|
|
105
|
+
"project_id": null
|
|
99
106
|
}
|
|
100
107
|
```
|
|
101
108
|
|
|
@@ -280,27 +287,26 @@ GET `/api/daemons/status` response:
|
|
|
280
287
|
|
|
281
288
|
### Threads (スレッド)
|
|
282
289
|
|
|
283
|
-
|
|
290
|
+
ワークスペースレベルのコミュニケーションチャネル。ブロッカー共有やチーム議論に使う。
|
|
284
291
|
リクエストは HQ にプロキシされる。
|
|
285
292
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
- **Workspace** (`scope: "workspace"`): プロジェクトに属さないスレッド。ルーティンのブロッカー、全体連絡等。オーナーの全ミニオンが参加。
|
|
293
|
+
すべてのスレッドはワークスペースに所属し、プロジェクトへの紐づけは多対多(`thread_project_links`)で管理される。
|
|
294
|
+
1つのスレッドを複数プロジェクトにリンクしたり、プロジェクトに紐づけずにワークスペース全体のスレッドとして使うこともできる。
|
|
289
295
|
|
|
290
296
|
| Method | Endpoint | Description |
|
|
291
297
|
|--------|----------|-------------|
|
|
292
|
-
| GET | `/api/threads` |
|
|
298
|
+
| GET | `/api/threads` | オープンスレッド一覧 |
|
|
293
299
|
| POST | `/api/threads` | スレッドを起票(初回メッセージを同時作成) |
|
|
294
300
|
| GET | `/api/threads/:id` | スレッド詳細 + メッセージ一覧 |
|
|
295
301
|
| POST | `/api/threads/:id/messages` | スレッドにメッセージを投稿 |
|
|
296
302
|
| POST | `/api/threads/:id/resolve` | スレッドを解決済みにする |
|
|
297
303
|
| POST | `/api/threads/:id/cancel` | スレッドをキャンセル |
|
|
298
|
-
| DELETE | `/api/threads/:id` |
|
|
304
|
+
| DELETE | `/api/threads/:id` | スレッドを完全削除 |
|
|
299
305
|
|
|
300
|
-
POST `/api/threads` body (
|
|
306
|
+
POST `/api/threads` body (プロジェクト紐づきヘルプスレッド):
|
|
301
307
|
```json
|
|
302
308
|
{
|
|
303
|
-
"
|
|
309
|
+
"project_ids": ["uuid"],
|
|
304
310
|
"thread_type": "help",
|
|
305
311
|
"title": "ランサーズの2FA認証コードが必要",
|
|
306
312
|
"content": "ログイン時に2段階認証を要求された。メールで届く6桁コードの入力が必要。",
|
|
@@ -315,7 +321,6 @@ POST `/api/threads` body (プロジェクトスレッド — ヘルプ):
|
|
|
315
321
|
POST `/api/threads` body (ワークスペーススレッド — ルーティンのブロッカー):
|
|
316
322
|
```json
|
|
317
323
|
{
|
|
318
|
-
"scope": "workspace",
|
|
319
324
|
"category": "general",
|
|
320
325
|
"thread_type": "help",
|
|
321
326
|
"title": "朝作業ルーティン: APIキーの期限切れ",
|
|
@@ -328,10 +333,10 @@ POST `/api/threads` body (ワークスペーススレッド — ルーティン
|
|
|
328
333
|
}
|
|
329
334
|
```
|
|
330
335
|
|
|
331
|
-
POST `/api/threads` body (
|
|
336
|
+
POST `/api/threads` body (プロジェクト紐づきディスカッション):
|
|
332
337
|
```json
|
|
333
338
|
{
|
|
334
|
-
"
|
|
339
|
+
"project_ids": ["uuid"],
|
|
335
340
|
"thread_type": "discussion",
|
|
336
341
|
"title": "デプロイ手順の確認",
|
|
337
342
|
"content": "本番デプロイ前にステージングで確認するフローに変えたい。意見ある?",
|
|
@@ -341,19 +346,19 @@ POST `/api/threads` body (プロジェクトスレッド — ディスカッシ
|
|
|
341
346
|
|
|
342
347
|
| Field | Type | Required | Description |
|
|
343
348
|
|-------|------|----------|-------------|
|
|
344
|
-
| `
|
|
345
|
-
| `
|
|
346
|
-
| `category` | string | No |
|
|
349
|
+
| `project_id` | string | No | リンクするプロジェクトUUID(単一、後方互換) |
|
|
350
|
+
| `project_ids` | string[] | No | リンクするプロジェクトUUID(複数対応) |
|
|
351
|
+
| `category` | string | No | カテゴリ: `general`(デフォルト), `ops`, `standup` |
|
|
347
352
|
| `thread_type` | string | No | `help`(デフォルト)or `discussion` |
|
|
348
353
|
| `title` | string | Yes | スレッドの要約 |
|
|
349
354
|
| `content` | string | Yes | スレッド本文(thread_messagesの最初のメッセージとして保存) |
|
|
350
355
|
| `mentions` | string[] | No | メンション対象。形式: `role:engineer`, `role:pm`, `minion:<minion_id>`, `user` |
|
|
351
356
|
| `context` | object | No | 任意のメタデータ(category, urgency, workflow_execution_id等) |
|
|
352
357
|
|
|
353
|
-
|
|
354
|
-
- ワークフロー実行中(プロジェクトあり)→ `
|
|
355
|
-
- ルーティン実行中(プロジェクトなし)→ `
|
|
356
|
-
- プロジェクト外の一般的な質問・報告 → `
|
|
358
|
+
**プロジェクト紐づけの使い分け:**
|
|
359
|
+
- ワークフロー実行中(プロジェクトあり)→ `project_ids: ["uuid"]`
|
|
360
|
+
- ルーティン実行中(プロジェクトなし)→ `project_ids` を省略
|
|
361
|
+
- プロジェクト外の一般的な質問・報告 → `project_ids` を省略 + `category`
|
|
357
362
|
|
|
358
363
|
**thread_type の違い:**
|
|
359
364
|
- `help`: ブロッカー解決。`resolve` で解決
|
|
@@ -440,8 +445,8 @@ POST `/api/project-memories` body:
|
|
|
440
445
|
**推奨ワークフロー:**
|
|
441
446
|
1. ブロッカー発生 → プロジェクトコンテキストの場合は `GET /api/project-memories?project_id=...&category=auth&search=2fa` で既知の知見を検索
|
|
442
447
|
2. 該当あり → 知見に基づいて自己解決 or 即エスカレーション
|
|
443
|
-
3. 該当なし → `POST /api/threads`
|
|
444
|
-
4. 解決後 →
|
|
448
|
+
3. 該当なし → `POST /api/threads` でブロッカー起票(プロジェクトありなら `project_ids: ["uuid"]`、なしなら省略)
|
|
449
|
+
4. 解決後 → プロジェクト紐づきスレッドの場合は `POST /api/project-memories` で知見を蓄積
|
|
445
450
|
|
|
446
451
|
### Email
|
|
447
452
|
|
|
@@ -902,8 +907,8 @@ Response:
|
|
|
902
907
|
|
|
903
908
|
| Method | Endpoint | Description |
|
|
904
909
|
|--------|----------|-------------|
|
|
905
|
-
| POST | `/api/minion/threads` | スレッドを起票(
|
|
906
|
-
| GET | `/api/minion/threads/open` | 未解決スレッド一覧(`?
|
|
910
|
+
| POST | `/api/minion/threads` | スレッドを起票(project_ids, category対応。初回メッセージを同時作成) |
|
|
911
|
+
| GET | `/api/minion/threads/open` | 未解決スレッド一覧(`?project_id=uuid` で絞り込み可) |
|
|
907
912
|
| GET | `/api/minion/threads/:id` | スレッド詳細 + メッセージ一覧 |
|
|
908
913
|
| POST | `/api/minion/threads/:id/messages` | スレッドにメッセージを投稿 |
|
|
909
914
|
| PATCH | `/api/minion/threads/:id/resolve` | スレッドを解決済みにする。Body: `{resolution}` |
|
package/linux/routes/chat.js
CHANGED
|
@@ -246,6 +246,9 @@ async function buildContextPrefix(message, context, sessionId) {
|
|
|
246
246
|
'# メモリ検索(キーワードで全文検索)',
|
|
247
247
|
`curl -H "Authorization: Bearer $API_TOKEN" "${baseUrl}/api/memory?search=キーワード"`,
|
|
248
248
|
'',
|
|
249
|
+
'# プロジェクトスコープでメモリ検索(プロジェクト固有 + 汎用メモリを返す)',
|
|
250
|
+
`curl -H "Authorization: Bearer $API_TOKEN" "${baseUrl}/api/memory?search=キーワード&project_id=プロジェクトUUID"`,
|
|
251
|
+
'',
|
|
249
252
|
'# メモリ一覧',
|
|
250
253
|
`curl -H "Authorization: Bearer $API_TOKEN" ${baseUrl}/api/memory`,
|
|
251
254
|
'',
|
package/package.json
CHANGED
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
|
-
"
|
|
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
|
-
| ワークフロー実行中のブロッカー | `
|
|
248
|
-
| ルーティン実行中のブロッカー | `
|
|
249
|
-
| プロジェクト外の一般的な質問・報告 | `
|
|
244
|
+
| 状況 | 設定 | 説明 |
|
|
245
|
+
|------|------|------|
|
|
246
|
+
| ワークフロー実行中のブロッカー | `project_ids: ["uuid"]` | プロジェクトに紐づけ。プロジェクトビューに表示される |
|
|
247
|
+
| ルーティン実行中のブロッカー | `project_ids` を省略 | プロジェクトに紐づけない。ワークスペース全体に表示 |
|
|
248
|
+
| プロジェクト外の一般的な質問・報告 | `project_ids` を省略 + `category` | カテゴリ: `general`, `ops`, `standup` |
|
|
250
249
|
|
|
251
250
|
### メンションの使い分け
|
|
252
251
|
|
package/win/routes/chat.js
CHANGED
|
@@ -208,6 +208,9 @@ async function buildContextPrefix(message, context, sessionId) {
|
|
|
208
208
|
'# メモリ検索(キーワードで全文検索)',
|
|
209
209
|
`curl -H "Authorization: Bearer $API_TOKEN" "${baseUrl}/api/memory?search=キーワード"`,
|
|
210
210
|
'',
|
|
211
|
+
'# プロジェクトスコープでメモリ検索(プロジェクト固有 + 汎用メモリを返す)',
|
|
212
|
+
`curl -H "Authorization: Bearer $API_TOKEN" "${baseUrl}/api/memory?search=キーワード&project_id=プロジェクトUUID"`,
|
|
213
|
+
'',
|
|
211
214
|
'# メモリ一覧',
|
|
212
215
|
`curl -H "Authorization: Bearer $API_TOKEN" ${baseUrl}/api/memory`,
|
|
213
216
|
'',
|