@geekbeer/minion 3.24.0 → 3.25.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/db.js CHANGED
@@ -192,6 +192,14 @@ function initSchema(db) {
192
192
  VALUES (new.rowid, new.content);
193
193
  END;
194
194
 
195
+ -- ==================== workspaces (cache from HQ) ====================
196
+ CREATE TABLE IF NOT EXISTS workspaces (
197
+ id TEXT PRIMARY KEY,
198
+ name TEXT NOT NULL,
199
+ slug TEXT NOT NULL,
200
+ updated_at INTEGER NOT NULL
201
+ );
202
+
195
203
  -- ==================== chat_sessions ====================
196
204
  CREATE TABLE IF NOT EXISTS chat_sessions (
197
205
  session_id TEXT PRIMARY KEY,
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Workspace Store (SQLite)
3
+ * Caches workspace memberships synced from HQ via heartbeat.
4
+ */
5
+
6
+ const { getDb } = require('../db')
7
+
8
+ /**
9
+ * Upsert all workspaces (replace entire cache with latest from HQ).
10
+ * @param {Array<{ id: string, name: string, slug: string }>} workspaces
11
+ */
12
+ function upsertAll(workspaces) {
13
+ const db = getDb()
14
+ const upsert = db.prepare(
15
+ 'INSERT INTO workspaces (id, name, slug, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name=excluded.name, slug=excluded.slug, updated_at=excluded.updated_at'
16
+ )
17
+ const deleteAll = db.prepare('DELETE FROM workspaces')
18
+ const tx = db.transaction((items) => {
19
+ deleteAll.run()
20
+ const now = Date.now()
21
+ for (const ws of items) {
22
+ upsert.run(ws.id, ws.name, ws.slug, now)
23
+ }
24
+ })
25
+ tx(workspaces || [])
26
+ }
27
+
28
+ /**
29
+ * List all cached workspaces.
30
+ * @returns {Array<{ id: string, name: string, slug: string, updated_at: number }>}
31
+ */
32
+ function list() {
33
+ const db = getDb()
34
+ return db.prepare('SELECT * FROM workspaces ORDER BY name').all()
35
+ }
36
+
37
+ /**
38
+ * Get a workspace by ID.
39
+ * @param {string} id
40
+ * @returns {{ id: string, name: string, slug: string, updated_at: number } | undefined}
41
+ */
42
+ function getById(id) {
43
+ if (!id) return undefined
44
+ const db = getDb()
45
+ return db.prepare('SELECT * FROM workspaces WHERE id = ?').get(id)
46
+ }
47
+
48
+ module.exports = { upsertAll, list, getById }
@@ -17,6 +17,21 @@ Authentication: `Authorization: Bearer $API_TOKEN` header (except where noted).
17
17
  | GET | `/api/status` | Agent status, uptime, version, HQ connection |
18
18
  | POST | `/api/status` | Update status. Body: `{status, current_task}` |
19
19
 
20
+ ### Workspaces
21
+
22
+ ミニオンは複数のワークスペースに所属できる。所属ワークスペースはハートビートレスポンスで自動同期される。
23
+
24
+ | Method | HQ Endpoint | Description |
25
+ |--------|-------------|-------------|
26
+ | GET | `/api/minion/workspaces` | 所属ワークスペース一覧を取得 |
27
+
28
+ ハートビートレスポンスにも `workspaces` 配列が含まれ、30秒ごとに自動同期される。
29
+
30
+ ```bash
31
+ # hq CLIで所属ワークスペースを確認
32
+ hq list workspaces
33
+ ```
34
+
20
35
  ### Skills
21
36
 
22
37
  | Method | Endpoint | Description |
package/linux/bin/hq CHANGED
@@ -16,6 +16,7 @@
16
16
  # hq fetch project-context <id> - Get project context (shared Markdown document)
17
17
  # hq fetch dag-workflow <id> - Get DAG workflow details (graph, version)
18
18
  # hq fetch dag-execution <id> - Get DAG execution details (nodes, status)
19
+ # hq list workspaces - List workspaces this minion belongs to
19
20
  # hq create dag-workflow <body.json> - Create a DAG workflow (PM only). Body: {project_id, name, graph?, ...}
20
21
  # hq put dag-workflow <id> <body.json> - Update DAG workflow draft (PM only). Body: {graph?, content?, ...}
21
22
  # hq publish dag-workflow <id> - Publish the draft as a new version (PM only, validated)
@@ -144,6 +145,7 @@ print_usage() {
144
145
  echo " hq fetch project-context <id> - Get project context" >&2
145
146
  echo " hq fetch dag-workflow <id> - Get DAG workflow details" >&2
146
147
  echo " hq fetch dag-execution <id> - Get DAG execution details" >&2
148
+ echo " hq list workspaces - List workspaces this minion belongs to" >&2
147
149
  echo " hq create dag-workflow <body.json> - Create a DAG workflow (PM only)" >&2
148
150
  echo " hq put dag-workflow <id> <body.json> - Update DAG workflow draft (PM only)" >&2
149
151
  echo " hq publish dag-workflow <id> - Publish the draft as a new version (PM only)" >&2
@@ -192,6 +194,20 @@ case "${1:-}" in
192
194
  esac
193
195
  ;;
194
196
 
197
+ list)
198
+ resource="${2:-}"
199
+ case "$resource" in
200
+ workspaces)
201
+ fetch_resource "$BASE_URL/workspaces"
202
+ ;;
203
+ *)
204
+ echo "Unknown list resource: $resource" >&2
205
+ echo "Usage: hq list workspaces" >&2
206
+ exit 1
207
+ ;;
208
+ esac
209
+ ;;
210
+
195
211
  create)
196
212
  resource="${2:-}"
197
213
  case "$resource" in
@@ -52,8 +52,8 @@ async function chatRoutes(fastify) {
52
52
 
53
53
  const workspaceId = workspace_id || null
54
54
 
55
- // Build prompt — add memory context on new sessions + page context
56
- const prompt = await buildContextPrefix(message, context, session_id)
55
+ // Build prompt — add memory context on new sessions + page context + workspace
56
+ const prompt = await buildContextPrefix(message, context, session_id, workspaceId)
57
57
 
58
58
  // Store user message
59
59
  const currentSessionId = session_id || null
@@ -253,9 +253,22 @@ ${indexed}`
253
253
  * On new sessions (no session_id), injects minion memory + recent daily logs.
254
254
  * No conversation history injection — Claude CLI handles that via --resume.
255
255
  */
256
- async function buildContextPrefix(message, context, sessionId) {
256
+ async function buildContextPrefix(message, context, sessionId, workspaceId) {
257
257
  const parts = []
258
258
 
259
+ // Inject workspace context so Claude Code knows which workspace it's operating in
260
+ if (workspaceId) {
261
+ const workspaceStore = require('../../core/stores/workspace-store')
262
+ const ws = workspaceStore.getById(workspaceId)
263
+ if (ws) {
264
+ parts.push(
265
+ `[現在のワークスペース] ${ws.name} (ID: ${ws.id})`,
266
+ 'スキル操作やプロジェクト参照はこのワークスペースのスコープで行われます。',
267
+ ''
268
+ )
269
+ }
270
+ }
271
+
259
272
  // Re-inject unfinished todos tied to this session. This is how we survive
260
273
  // context compaction: even if Claude forgot the plan, the outstanding
261
274
  // todos are re-shown on the next turn. Todos past MAX_INJECTION_COUNT are
package/linux/server.js CHANGED
@@ -378,15 +378,23 @@ async function start() {
378
378
  const { currentTask } = getStatus()
379
379
  const todoStore = require('../core/stores/todo-store')
380
380
  const getTodoSummary = () => { try { return todoStore.getSummary() } catch { return null } }
381
- sendHeartbeat({ status: 'online', current_task: currentTask, running_tasks: runningTasks.getAll(), config_warnings: getConfigWarnings(), todo_summary: getTodoSummary(), version }).catch(err => {
381
+ const workspaceStore = require('../core/stores/workspace-store')
382
+ const syncWorkspaces = (response) => {
383
+ if (response && Array.isArray(response.workspaces)) {
384
+ workspaceStore.upsertAll(response.workspaces)
385
+ }
386
+ }
387
+
388
+ sendHeartbeat({ status: 'online', current_task: currentTask, running_tasks: runningTasks.getAll(), config_warnings: getConfigWarnings(), todo_summary: getTodoSummary(), version }).then(syncWorkspaces).catch(err => {
382
389
  console.error('[Heartbeat] Initial heartbeat failed:', err.message)
383
390
  })
384
391
 
385
392
  // Start periodic heartbeat
386
393
  heartbeatTimer = setInterval(() => {
387
394
  const { currentStatus, currentTask } = getStatus()
388
- sendHeartbeat({ status: currentStatus, current_task: currentTask, running_tasks: runningTasks.getAll(), config_warnings: getConfigWarnings(), todo_summary: getTodoSummary(), version }).then(() => {
395
+ sendHeartbeat({ status: currentStatus, current_task: currentTask, running_tasks: runningTasks.getAll(), config_warnings: getConfigWarnings(), todo_summary: getTodoSummary(), version }).then(response => {
389
396
  lastBeatAt = new Date().toISOString()
397
+ syncWorkspaces(response)
390
398
  }).catch(err => {
391
399
  console.error('[Heartbeat] Periodic heartbeat failed:', err.message)
392
400
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "3.24.0",
3
+ "version": "3.25.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
@@ -29,6 +29,7 @@ Minion
29
29
  - **Workflow**: プロジェクトスコープ。線形パイプライン形式のバージョン管理ワークフロー。ミニオンAPIで push/fetch 可能。
30
30
  - **DAG Workflow**: プロジェクトスコープ。ノード/エッジで依存関係を表現する新方式。fan-out / join / conditional / transform / review をサポート。作成・編集はHQダッシュボードのみ、ミニオンは `dag-step-poller` で自動実行。詳細は `~/.minion/docs/api-reference.md` の「DAG Workflows」と `~/.minion/docs/task-guides.md` の「DAG ワークフロー」を参照。
31
31
  - **Routine**: ミニオンスコープ。ミニオンローカルの定期タスク。
32
+ - **Workspace**: ミニオンは複数のワークスペースに所属でき、スキルやプロジェクトはワークスペース単位でスコープされる。チャットセッションもワークスペース別に分離される。所属ワークスペースはハートビートで自動同期され、`hq list workspaces` で確認できる。
32
33
  - ミニオンは複数プロジェクトに `pm`、`engineer`、`accountant` として参加できる。
33
34
 
34
35
  ## Email
package/win/bin/hq.ps1 CHANGED
@@ -128,6 +128,7 @@ function Show-Usage {
128
128
  Write-Host " hq fetch project-context <id> - Get project context"
129
129
  Write-Host " hq fetch dag-workflow <id> - Get DAG workflow details"
130
130
  Write-Host " hq fetch dag-execution <id> - Get DAG execution details"
131
+ Write-Host " hq list workspaces - List workspaces this minion belongs to"
131
132
  Write-Host " hq create dag-workflow <body.json> - Create a DAG workflow (PM only)"
132
133
  Write-Host " hq put dag-workflow <id> <body.json> - Update DAG workflow draft (PM only)"
133
134
  Write-Host " hq publish dag-workflow <id> - Publish the draft as a new version (PM only)"
@@ -167,6 +168,18 @@ switch ($Command) {
167
168
  }
168
169
  }
169
170
 
171
+ 'list' {
172
+ $Resource = $Arg1
173
+ switch ($Resource) {
174
+ 'workspaces' { Invoke-HqApi "$BaseUrl/workspaces" }
175
+ default {
176
+ Write-Error "Unknown list resource: $Resource"
177
+ Write-Error "Usage: hq list workspaces"
178
+ exit 1
179
+ }
180
+ }
181
+ }
182
+
170
183
  'create' {
171
184
  $Resource = $Arg1
172
185
  switch ($Resource) {
@@ -125,7 +125,7 @@ async function chatRoutes(fastify) {
125
125
  }
126
126
 
127
127
  const workspaceId = workspace_id || null
128
- const prompt = await buildContextPrefix(message, context, session_id)
128
+ const prompt = await buildContextPrefix(message, context, session_id, workspaceId)
129
129
  const currentSessionId = session_id || null
130
130
 
131
131
  if (currentSessionId) {
@@ -317,9 +317,22 @@ ${indexed}`
317
317
  })
318
318
  }
319
319
 
320
- async function buildContextPrefix(message, context, sessionId) {
320
+ async function buildContextPrefix(message, context, sessionId, workspaceId) {
321
321
  const parts = []
322
322
 
323
+ // Inject workspace context so Claude Code knows which workspace it's operating in
324
+ if (workspaceId) {
325
+ const workspaceStore = require('../../core/stores/workspace-store')
326
+ const ws = workspaceStore.getById(workspaceId)
327
+ if (ws) {
328
+ parts.push(
329
+ `[現在のワークスペース] ${ws.name} (ID: ${ws.id})`,
330
+ 'スキル操作やプロジェクト参照はこのワークスペースのスコープで行われます。',
331
+ ''
332
+ )
333
+ }
334
+ }
335
+
323
336
  // Re-inject unfinished todos tied to this session. Survives context
324
337
  // compaction — Claude sees outstanding todos again on the next turn.
325
338
  // Skipped past MAX_INJECTION_COUNT to prevent infinite loops.
package/win/server.js CHANGED
@@ -345,15 +345,23 @@ async function start() {
345
345
  const { currentTask } = getStatus()
346
346
  const todoStore = require('../core/stores/todo-store')
347
347
  const getTodoSummary = () => { try { return todoStore.getSummary() } catch { return null } }
348
- sendHeartbeat({ status: 'online', current_task: currentTask, config_warnings: getConfigWarnings(), todo_summary: getTodoSummary(), version }).catch(err => {
348
+ const workspaceStore = require('../core/stores/workspace-store')
349
+ const syncWorkspaces = (response) => {
350
+ if (response && Array.isArray(response.workspaces)) {
351
+ workspaceStore.upsertAll(response.workspaces)
352
+ }
353
+ }
354
+
355
+ sendHeartbeat({ status: 'online', current_task: currentTask, config_warnings: getConfigWarnings(), todo_summary: getTodoSummary(), version }).then(syncWorkspaces).catch(err => {
349
356
  console.error('[Heartbeat] Initial heartbeat failed:', err.message)
350
357
  })
351
358
 
352
359
  // Start periodic heartbeat
353
360
  heartbeatTimer = setInterval(() => {
354
361
  const { currentStatus, currentTask } = getStatus()
355
- sendHeartbeat({ status: currentStatus, current_task: currentTask, config_warnings: getConfigWarnings(), todo_summary: getTodoSummary(), version }).then(() => {
362
+ sendHeartbeat({ status: currentStatus, current_task: currentTask, config_warnings: getConfigWarnings(), todo_summary: getTodoSummary(), version }).then(response => {
356
363
  lastBeatAt = new Date().toISOString()
364
+ syncWorkspaces(response)
357
365
  }).catch(err => {
358
366
  console.error('[Heartbeat] Periodic heartbeat failed:', err.message)
359
367
  })