@geekbeer/minion 3.24.0 → 3.26.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 +8 -0
- package/core/stores/workspace-store.js +48 -0
- package/docs/api-reference.md +68 -0
- package/linux/bin/hq +190 -0
- package/linux/routes/chat.js +32 -3
- package/linux/server.js +10 -2
- package/package.json +1 -1
- package/rules/core.md +57 -0
- package/win/bin/hq.ps1 +13 -0
- package/win/routes/chat.js +31 -2
- package/win/server.js +10 -2
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 }
|
package/docs/api-reference.md
CHANGED
|
@@ -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 |
|
|
@@ -102,6 +117,7 @@ curl http://localhost:8080/api/terminal/wsl/status \
|
|
|
102
117
|
### Files
|
|
103
118
|
|
|
104
119
|
Files are stored in `~/files/`. Max upload size: 50MB.
|
|
120
|
+
**バイナリ成果物(PDF、画像、ZIP等)はここに配置する。** ユーザーがHQダッシュボードからダウンロードできる。
|
|
105
121
|
|
|
106
122
|
| Method | Endpoint | Description |
|
|
107
123
|
|--------|----------|-------------|
|
|
@@ -1433,3 +1449,55 @@ Response:
|
|
|
1433
1449
|
|
|
1434
1450
|
ローカルエージェントの `/api/project-memories` は上記 HQ API へのプロキシ。
|
|
1435
1451
|
詳細なリクエスト/レスポンス仕様はローカル API セクションの「Project Memories」を参照。
|
|
1452
|
+
|
|
1453
|
+
### Notes (HQ, プロジェクトノート)
|
|
1454
|
+
|
|
1455
|
+
プロジェクトに紐づくノート。**テキストベースの成果物(レポート、調査結果、要約等)はノートに保存すること。**
|
|
1456
|
+
ノートはHQダッシュボードでユーザーが即座に閲覧・編集・検索でき、バージョン管理もされる。
|
|
1457
|
+
|
|
1458
|
+
| Method | Endpoint | Description |
|
|
1459
|
+
|--------|----------|-------------|
|
|
1460
|
+
| GET | `/api/minion/projects/:projectId/notes` | プロジェクトのノート一覧。Query: `?status=active&include_content=true` |
|
|
1461
|
+
| POST | `/api/minion/projects/:projectId/notes` | ノートを作成しプロジェクトにリンク |
|
|
1462
|
+
| GET | `/api/minion/projects/:projectId/notes/:noteId` | ノート詳細(全文含む) |
|
|
1463
|
+
| PATCH | `/api/minion/projects/:projectId/notes/:noteId` | ノートを更新(内容変更時にバージョン自動作成) |
|
|
1464
|
+
| GET | `/api/minion/projects/:projectId/notes/search?q=keyword` | ノートを全文検索 |
|
|
1465
|
+
|
|
1466
|
+
POST `/api/minion/projects/:projectId/notes` body:
|
|
1467
|
+
```json
|
|
1468
|
+
{
|
|
1469
|
+
"title": "調査レポート: ユーザー行動分析",
|
|
1470
|
+
"content": "## 概要\n..."
|
|
1471
|
+
}
|
|
1472
|
+
```
|
|
1473
|
+
|
|
1474
|
+
| Field | Type | Required | Description |
|
|
1475
|
+
|-------|------|----------|-------------|
|
|
1476
|
+
| `title` | string | Yes | ノートのタイトル |
|
|
1477
|
+
| `content` | string | No | ノートの本文(Markdown) |
|
|
1478
|
+
|
|
1479
|
+
PATCH `/api/minion/projects/:projectId/notes/:noteId` body:
|
|
1480
|
+
```json
|
|
1481
|
+
{
|
|
1482
|
+
"title": "更新されたタイトル",
|
|
1483
|
+
"content": "更新された本文",
|
|
1484
|
+
"change_summary": "データを追加"
|
|
1485
|
+
}
|
|
1486
|
+
```
|
|
1487
|
+
|
|
1488
|
+
| Field | Type | Required | Description |
|
|
1489
|
+
|-------|------|----------|-------------|
|
|
1490
|
+
| `title` | string | No | 新しいタイトル |
|
|
1491
|
+
| `content` | string | No | 新しい本文 |
|
|
1492
|
+
| `change_summary` | string | No | 変更の要約(バージョン履歴に記録) |
|
|
1493
|
+
|
|
1494
|
+
#### hq CLI ラッパー
|
|
1495
|
+
|
|
1496
|
+
```bash
|
|
1497
|
+
hq note create <project_id> --title "タイトル" --content "本文"
|
|
1498
|
+
hq note create <project_id> --title "タイトル" --file /tmp/report.md
|
|
1499
|
+
hq note update <project_id> <note_id> --content "更新内容" --change-summary "修正理由"
|
|
1500
|
+
hq note list <project_id>
|
|
1501
|
+
hq note get <project_id> <note_id>
|
|
1502
|
+
hq note search <project_id> "キーワード"
|
|
1503
|
+
```
|
package/linux/bin/hq
CHANGED
|
@@ -16,6 +16,14 @@
|
|
|
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
|
|
20
|
+
# hq note create <project_id> --title "Title" --content "Body" [--file path]
|
|
21
|
+
# - Create a note linked to the project
|
|
22
|
+
# hq note update <project_id> <note_id> --content "Body" [--title "Title"] [--file path] [--change-summary "summary"]
|
|
23
|
+
# - Update an existing note
|
|
24
|
+
# hq note list <project_id> - List notes in the project
|
|
25
|
+
# hq note get <project_id> <note_id> - Get a single note with full content
|
|
26
|
+
# hq note search <project_id> <query> - Search notes in the project
|
|
19
27
|
# hq create dag-workflow <body.json> - Create a DAG workflow (PM only). Body: {project_id, name, graph?, ...}
|
|
20
28
|
# hq put dag-workflow <id> <body.json> - Update DAG workflow draft (PM only). Body: {graph?, content?, ...}
|
|
21
29
|
# hq publish dag-workflow <id> - Publish the draft as a new version (PM only, validated)
|
|
@@ -144,6 +152,12 @@ print_usage() {
|
|
|
144
152
|
echo " hq fetch project-context <id> - Get project context" >&2
|
|
145
153
|
echo " hq fetch dag-workflow <id> - Get DAG workflow details" >&2
|
|
146
154
|
echo " hq fetch dag-execution <id> - Get DAG execution details" >&2
|
|
155
|
+
echo " hq list workspaces - List workspaces this minion belongs to" >&2
|
|
156
|
+
echo " hq note create <project_id> --title \"Title\" --content \"Body\" [--file path]" >&2
|
|
157
|
+
echo " hq note update <project_id> <note_id> --content \"Body\" [--title \"Title\"] [--file path]" >&2
|
|
158
|
+
echo " hq note list <project_id> - List notes in the project" >&2
|
|
159
|
+
echo " hq note get <project_id> <note_id> - Get a single note" >&2
|
|
160
|
+
echo " hq note search <project_id> <query> - Search notes in the project" >&2
|
|
147
161
|
echo " hq create dag-workflow <body.json> - Create a DAG workflow (PM only)" >&2
|
|
148
162
|
echo " hq put dag-workflow <id> <body.json> - Update DAG workflow draft (PM only)" >&2
|
|
149
163
|
echo " hq publish dag-workflow <id> - Publish the draft as a new version (PM only)" >&2
|
|
@@ -192,6 +206,20 @@ case "${1:-}" in
|
|
|
192
206
|
esac
|
|
193
207
|
;;
|
|
194
208
|
|
|
209
|
+
list)
|
|
210
|
+
resource="${2:-}"
|
|
211
|
+
case "$resource" in
|
|
212
|
+
workspaces)
|
|
213
|
+
fetch_resource "$BASE_URL/workspaces"
|
|
214
|
+
;;
|
|
215
|
+
*)
|
|
216
|
+
echo "Unknown list resource: $resource" >&2
|
|
217
|
+
echo "Usage: hq list workspaces" >&2
|
|
218
|
+
exit 1
|
|
219
|
+
;;
|
|
220
|
+
esac
|
|
221
|
+
;;
|
|
222
|
+
|
|
195
223
|
create)
|
|
196
224
|
resource="${2:-}"
|
|
197
225
|
case "$resource" in
|
|
@@ -254,6 +282,168 @@ case "${1:-}" in
|
|
|
254
282
|
esac
|
|
255
283
|
;;
|
|
256
284
|
|
|
285
|
+
note)
|
|
286
|
+
action="${2:-}"
|
|
287
|
+
case "$action" in
|
|
288
|
+
create)
|
|
289
|
+
project_id="${3:-}"
|
|
290
|
+
if [ -z "$project_id" ]; then
|
|
291
|
+
echo "Usage: hq note create <project_id> --title \"Title\" --content \"Body\" [--file path]" >&2
|
|
292
|
+
exit 1
|
|
293
|
+
fi
|
|
294
|
+
shift 3
|
|
295
|
+
note_title=""
|
|
296
|
+
note_content=""
|
|
297
|
+
note_file=""
|
|
298
|
+
while [ $# -gt 0 ]; do
|
|
299
|
+
case "$1" in
|
|
300
|
+
--title) note_title="$2"; shift 2 ;;
|
|
301
|
+
--content) note_content="$2"; shift 2 ;;
|
|
302
|
+
--file) note_file="$2"; shift 2 ;;
|
|
303
|
+
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
304
|
+
esac
|
|
305
|
+
done
|
|
306
|
+
if [ -z "$note_title" ]; then
|
|
307
|
+
echo "Error: --title is required" >&2
|
|
308
|
+
exit 1
|
|
309
|
+
fi
|
|
310
|
+
if [ -n "$note_file" ]; then
|
|
311
|
+
if [ ! -f "$note_file" ]; then
|
|
312
|
+
echo "Error: file not found: $note_file" >&2
|
|
313
|
+
exit 1
|
|
314
|
+
fi
|
|
315
|
+
note_content=$(cat "$note_file")
|
|
316
|
+
fi
|
|
317
|
+
if [ -z "$note_content" ]; then
|
|
318
|
+
echo "Error: --content or --file is required" >&2
|
|
319
|
+
exit 1
|
|
320
|
+
fi
|
|
321
|
+
# Build JSON body using jq or python3
|
|
322
|
+
if command -v jq &>/dev/null; then
|
|
323
|
+
body=$(jq -n --arg t "$note_title" --arg c "$note_content" '{title: $t, content: $c}')
|
|
324
|
+
elif command -v python3 &>/dev/null; then
|
|
325
|
+
body=$(python3 -c "import json,sys; print(json.dumps({'title': sys.argv[1], 'content': sys.argv[2]}))" "$note_title" "$note_content")
|
|
326
|
+
else
|
|
327
|
+
echo "Error: jq or python3 is required to build JSON" >&2
|
|
328
|
+
exit 1
|
|
329
|
+
fi
|
|
330
|
+
tmpfile=$(mktemp)
|
|
331
|
+
echo "$body" > "$tmpfile"
|
|
332
|
+
send_json_request POST "$BASE_URL/projects/$project_id/notes" "$tmpfile"
|
|
333
|
+
rm -f "$tmpfile"
|
|
334
|
+
;;
|
|
335
|
+
update)
|
|
336
|
+
project_id="${3:-}"
|
|
337
|
+
note_id="${4:-}"
|
|
338
|
+
if [ -z "$project_id" ] || [ -z "$note_id" ]; then
|
|
339
|
+
echo "Usage: hq note update <project_id> <note_id> --content \"Body\" [--title \"Title\"] [--file path] [--change-summary \"summary\"]" >&2
|
|
340
|
+
exit 1
|
|
341
|
+
fi
|
|
342
|
+
shift 4
|
|
343
|
+
note_title=""
|
|
344
|
+
note_content=""
|
|
345
|
+
note_file=""
|
|
346
|
+
change_summary=""
|
|
347
|
+
while [ $# -gt 0 ]; do
|
|
348
|
+
case "$1" in
|
|
349
|
+
--title) note_title="$2"; shift 2 ;;
|
|
350
|
+
--content) note_content="$2"; shift 2 ;;
|
|
351
|
+
--file) note_file="$2"; shift 2 ;;
|
|
352
|
+
--change-summary) change_summary="$2"; shift 2 ;;
|
|
353
|
+
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
354
|
+
esac
|
|
355
|
+
done
|
|
356
|
+
if [ -n "$note_file" ]; then
|
|
357
|
+
if [ ! -f "$note_file" ]; then
|
|
358
|
+
echo "Error: file not found: $note_file" >&2
|
|
359
|
+
exit 1
|
|
360
|
+
fi
|
|
361
|
+
note_content=$(cat "$note_file")
|
|
362
|
+
fi
|
|
363
|
+
if [ -z "$note_content" ] && [ -z "$note_title" ]; then
|
|
364
|
+
echo "Error: at least --content, --file, or --title is required" >&2
|
|
365
|
+
exit 1
|
|
366
|
+
fi
|
|
367
|
+
# Build JSON body
|
|
368
|
+
if command -v jq &>/dev/null; then
|
|
369
|
+
body="{}"
|
|
370
|
+
[ -n "$note_title" ] && body=$(echo "$body" | jq --arg t "$note_title" '. + {title: $t}')
|
|
371
|
+
[ -n "$note_content" ] && body=$(echo "$body" | jq --arg c "$note_content" '. + {content: $c}')
|
|
372
|
+
[ -n "$change_summary" ] && body=$(echo "$body" | jq --arg s "$change_summary" '. + {change_summary: $s}')
|
|
373
|
+
elif command -v python3 &>/dev/null; then
|
|
374
|
+
body=$(python3 -c "
|
|
375
|
+
import json, sys
|
|
376
|
+
d = {}
|
|
377
|
+
if sys.argv[1]: d['title'] = sys.argv[1]
|
|
378
|
+
if sys.argv[2]: d['content'] = sys.argv[2]
|
|
379
|
+
if sys.argv[3]: d['change_summary'] = sys.argv[3]
|
|
380
|
+
print(json.dumps(d))
|
|
381
|
+
" "$note_title" "$note_content" "$change_summary")
|
|
382
|
+
else
|
|
383
|
+
echo "Error: jq or python3 is required to build JSON" >&2
|
|
384
|
+
exit 1
|
|
385
|
+
fi
|
|
386
|
+
tmpfile=$(mktemp)
|
|
387
|
+
echo "$body" > "$tmpfile"
|
|
388
|
+
# Use PATCH for update
|
|
389
|
+
response=$(curl -s -w "\n%{http_code}" -X PATCH \
|
|
390
|
+
-H "Authorization: Bearer $API_TOKEN" \
|
|
391
|
+
-H "Content-Type: application/json" \
|
|
392
|
+
--data-binary "@$tmpfile" \
|
|
393
|
+
"$BASE_URL/projects/$project_id/notes/$note_id")
|
|
394
|
+
rm -f "$tmpfile"
|
|
395
|
+
http_code=$(echo "$response" | tail -1)
|
|
396
|
+
body_out=$(echo "$response" | sed '$d')
|
|
397
|
+
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
|
|
398
|
+
echo "$body_out" | format_json
|
|
399
|
+
else
|
|
400
|
+
echo "Error: HQ API returned HTTP $http_code" >&2
|
|
401
|
+
echo "$body_out" >&2
|
|
402
|
+
exit 1
|
|
403
|
+
fi
|
|
404
|
+
;;
|
|
405
|
+
list)
|
|
406
|
+
project_id="${3:-}"
|
|
407
|
+
if [ -z "$project_id" ]; then
|
|
408
|
+
echo "Usage: hq note list <project_id>" >&2
|
|
409
|
+
exit 1
|
|
410
|
+
fi
|
|
411
|
+
fetch_resource "$BASE_URL/projects/$project_id/notes?include_content=false"
|
|
412
|
+
;;
|
|
413
|
+
get)
|
|
414
|
+
project_id="${3:-}"
|
|
415
|
+
note_id="${4:-}"
|
|
416
|
+
if [ -z "$project_id" ] || [ -z "$note_id" ]; then
|
|
417
|
+
echo "Usage: hq note get <project_id> <note_id>" >&2
|
|
418
|
+
exit 1
|
|
419
|
+
fi
|
|
420
|
+
fetch_resource "$BASE_URL/projects/$project_id/notes/$note_id"
|
|
421
|
+
;;
|
|
422
|
+
search)
|
|
423
|
+
project_id="${3:-}"
|
|
424
|
+
query="${4:-}"
|
|
425
|
+
if [ -z "$project_id" ] || [ -z "$query" ]; then
|
|
426
|
+
echo "Usage: hq note search <project_id> <query>" >&2
|
|
427
|
+
exit 1
|
|
428
|
+
fi
|
|
429
|
+
# URL-encode the query
|
|
430
|
+
if command -v jq &>/dev/null; then
|
|
431
|
+
encoded=$(printf '%s' "$query" | jq -sRr @uri)
|
|
432
|
+
elif command -v python3 &>/dev/null; then
|
|
433
|
+
encoded=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$query")
|
|
434
|
+
else
|
|
435
|
+
encoded="$query"
|
|
436
|
+
fi
|
|
437
|
+
fetch_resource "$BASE_URL/projects/$project_id/notes/search?q=$encoded"
|
|
438
|
+
;;
|
|
439
|
+
*)
|
|
440
|
+
echo "Unknown note action: $action" >&2
|
|
441
|
+
echo "Usage: hq note {create|update|list|get|search} ..." >&2
|
|
442
|
+
exit 1
|
|
443
|
+
;;
|
|
444
|
+
esac
|
|
445
|
+
;;
|
|
446
|
+
|
|
257
447
|
*)
|
|
258
448
|
print_usage
|
|
259
449
|
exit 1
|
package/linux/routes/chat.js
CHANGED
|
@@ -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
|
|
@@ -324,6 +337,22 @@ async function buildContextPrefix(message, context, sessionId) {
|
|
|
324
337
|
)
|
|
325
338
|
}
|
|
326
339
|
|
|
340
|
+
// File output guidance — always inject on new sessions
|
|
341
|
+
if (!sessionId) {
|
|
342
|
+
parts.push(
|
|
343
|
+
'[ファイル出力ルール]',
|
|
344
|
+
'成果物の保存先は以下のルールに従うこと。`/home/minion/` 直下にファイルを保存しないこと。',
|
|
345
|
+
'',
|
|
346
|
+
'- **テキスト成果物**(レポート、調査結果、要約等)→ ノートに保存: `hq note create <project_id> --title "タイトル" --content "本文"`',
|
|
347
|
+
'- **バイナリファイル**(PDF、画像、ZIP等)→ `~/files/` に配置(ユーザーがHQからダウンロード可能)',
|
|
348
|
+
'- **一時ファイル** → `/tmp/` に配置(作業後に削除)',
|
|
349
|
+
'',
|
|
350
|
+
'プロジェクトコンテキストがない場合、テキスト成果物はローカルメモリ(`POST /api/memory`)に保存する。',
|
|
351
|
+
'詳細は `~/.minion/rules/core.md` の「File Output Rules」セクションを参照。',
|
|
352
|
+
''
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
327
356
|
if (context) {
|
|
328
357
|
switch (context.type) {
|
|
329
358
|
case 'skill':
|
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
|
-
|
|
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
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
|
|
@@ -306,6 +307,62 @@ API詳細は `~/.minion/docs/api-reference.md` の「Todos」セクションを
|
|
|
306
307
|
- **ブロッカーが解決したらスレッドを `resolve` する。** 放置しない。
|
|
307
308
|
- **解決策をプロジェクトメモリーに保存する。** 同じブロッカーに再度遭遇する他のミニオンの助けになる。
|
|
308
309
|
|
|
310
|
+
## File Output Rules (ファイル出力ルール)
|
|
311
|
+
|
|
312
|
+
成果物の保存先は以下のルールに従うこと。**`/home/minion/` 直下にファイルを保存してはならない。** ユーザーがHQダッシュボードからアクセスできない場所にファイルを置いても意味がない。
|
|
313
|
+
|
|
314
|
+
### 保存先の判断基準
|
|
315
|
+
|
|
316
|
+
| 出力タイプ | 保存先 | 理由 |
|
|
317
|
+
|-----------|--------|------|
|
|
318
|
+
| テキスト(レポート、調査結果、要約、分析結果等) | **ノート** (`hq note create`) | HQダッシュボードで即閲覧・編集・検索可能。バージョン管理もされる |
|
|
319
|
+
| バイナリ(PDF、画像、ZIP、Excel等) | **`~/files/`** | Files API経由でユーザーがダウンロード可能 |
|
|
320
|
+
| コード・設定ファイル | **ワークスペース内の適切なディレクトリ** | プロジェクトのリポジトリやワークスペースに配置 |
|
|
321
|
+
| 一時ファイル(中間データ、一時スクリプト等) | **`/tmp/`** | ユーザーに見せる必要なし。作業後に削除 |
|
|
322
|
+
|
|
323
|
+
### ノートへの保存 (`hq note` コマンド)
|
|
324
|
+
|
|
325
|
+
テキストベースの成果物は原則ノートに保存する。ノートはプロジェクトに紐づき、HQダッシュボードで閲覧できる。
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
# ノートを作成(プロジェクトに紐づけ)
|
|
329
|
+
hq note create <project_id> --title "レポートタイトル" --content "本文..."
|
|
330
|
+
|
|
331
|
+
# 長い内容はファイルから読み込む
|
|
332
|
+
hq note create <project_id> --title "調査結果" --file /tmp/report.md
|
|
333
|
+
|
|
334
|
+
# 既存ノートを更新
|
|
335
|
+
hq note update <project_id> <note_id> --content "更新された内容"
|
|
336
|
+
|
|
337
|
+
# ノート一覧を確認
|
|
338
|
+
hq note list <project_id>
|
|
339
|
+
|
|
340
|
+
# ノート内容を取得
|
|
341
|
+
hq note get <project_id> <note_id>
|
|
342
|
+
|
|
343
|
+
# ノートを検索
|
|
344
|
+
hq note search <project_id> "キーワード"
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### `~/files/` への保存
|
|
348
|
+
|
|
349
|
+
バイナリファイルやユーザーがダウンロードする必要があるファイルは `~/files/` に配置する。
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
# ファイルを保存(~/files/ 配下に自動配置)
|
|
353
|
+
cp /tmp/output.pdf ~/files/output.pdf
|
|
354
|
+
|
|
355
|
+
# サブディレクトリで整理も可能
|
|
356
|
+
mkdir -p ~/files/reports/2026-04
|
|
357
|
+
cp /tmp/report.pdf ~/files/reports/2026-04/report.pdf
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### プロジェクトコンテキストがない場合
|
|
361
|
+
|
|
362
|
+
チャットやルーティンなど、プロジェクトに紐づかない作業の場合:
|
|
363
|
+
- テキスト成果物 → ローカルメモリ (`POST /api/memory`) に保存
|
|
364
|
+
- バイナリファイル → `~/files/` に保存
|
|
365
|
+
|
|
309
366
|
## Documentation
|
|
310
367
|
|
|
311
368
|
API仕様やタスク手順の詳細は以下を参照:
|
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) {
|
package/win/routes/chat.js
CHANGED
|
@@ -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.
|
|
@@ -387,6 +400,22 @@ async function buildContextPrefix(message, context, sessionId) {
|
|
|
387
400
|
)
|
|
388
401
|
}
|
|
389
402
|
|
|
403
|
+
// File output guidance — always inject on new sessions
|
|
404
|
+
if (!sessionId) {
|
|
405
|
+
parts.push(
|
|
406
|
+
'[ファイル出力ルール]',
|
|
407
|
+
'成果物の保存先は以下のルールに従うこと。`/home/minion/` 直下にファイルを保存しないこと。',
|
|
408
|
+
'',
|
|
409
|
+
'- **テキスト成果物**(レポート、調査結果、要約等)→ ノートに保存: `hq note create <project_id> --title "タイトル" --content "本文"`',
|
|
410
|
+
'- **バイナリファイル**(PDF、画像、ZIP等)→ `~/files/` に配置(ユーザーがHQからダウンロード可能)',
|
|
411
|
+
'- **一時ファイル** → `/tmp/` に配置(作業後に削除)',
|
|
412
|
+
'',
|
|
413
|
+
'プロジェクトコンテキストがない場合、テキスト成果物はローカルメモリ(`POST /api/memory`)に保存する。',
|
|
414
|
+
'詳細は `~/.minion/rules/core.md` の「File Output Rules」セクションを参照。',
|
|
415
|
+
''
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
390
419
|
if (context) {
|
|
391
420
|
switch (context.type) {
|
|
392
421
|
case 'skill':
|
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
|
-
|
|
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
|
})
|