@geekbeer/minion 3.43.0 → 3.49.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/config.js +3 -1
- package/core/lib/board-task-context.js +87 -0
- package/core/lib/board-task-poller.js +210 -0
- package/core/lib/concurrency-manager.js +56 -0
- package/core/lib/dag-step-poller.js +16 -10
- package/core/lib/platform.js +39 -19
- package/core/routes/daemons.js +2 -0
- package/core/routes/diagnose.js +27 -2
- package/docs/api-reference.md +73 -1
- package/linux/board-task-runner.js +227 -0
- package/linux/routes/chat.js +26 -6
- package/linux/server.js +5 -0
- package/mac/bin/hq +4 -0
- package/mac/board-task-runner.js +4 -0
- package/mac/lib/process-manager.js +109 -0
- package/mac/minion-cli.sh +1353 -0
- package/mac/routes/chat.js +7 -0
- package/mac/routes/commands.js +119 -0
- package/mac/routes/config.js +8 -0
- package/mac/routes/directives.js +6 -0
- package/mac/routes/files.js +6 -0
- package/mac/routes/terminal.js +7 -0
- package/mac/routine-runner.js +4 -0
- package/mac/server.js +413 -0
- package/mac/terminal-proxy.js +6 -0
- package/mac/vnc-auth-proxy.js +402 -0
- package/mac/workflow-runner.js +7 -0
- package/package.json +6 -2
- package/postinstall.js +33 -12
- package/rules/core.md +30 -0
- package/win/board-task-runner.js +181 -0
- package/win/routes/chat.js +24 -6
- package/win/routes/terminal.js +8 -0
- package/win/server.js +5 -0
- package/win/wsl-session-server.js +136 -1
package/docs/api-reference.md
CHANGED
|
@@ -803,12 +803,29 @@ POST body:
|
|
|
803
803
|
"priority": "normal", // low|normal|high|urgent (default: normal)
|
|
804
804
|
"milestone_id": null,
|
|
805
805
|
"parent_task_id": null, // 指定時は子タスクとして作成。親が既に子の場合 400
|
|
806
|
+
"sprint_id": null, // スプリントに含める場合のみ指定
|
|
806
807
|
"assignee_minion_id": null,
|
|
807
808
|
"assignee_user_id": null, // assignee_minion_id と同時指定不可
|
|
808
|
-
"due_date": null
|
|
809
|
+
"due_date": null, // YYYY-MM-DD
|
|
810
|
+
"acceptance_criteria": null // [{id?, text, checked?}] または string[] (idは省略時サーバが付与)
|
|
809
811
|
}
|
|
810
812
|
```
|
|
811
813
|
|
|
814
|
+
**`acceptance_criteria` の形式:**
|
|
815
|
+
- `null` = 未設定
|
|
816
|
+
- `[{id: uuid, text: string, checked: boolean}, ...]` または `["text", "text", ...]`
|
|
817
|
+
- 既存項目の checked を更新する場合は **id を保持して** PATCH すること(idを省くと新規追加扱い)
|
|
818
|
+
- 配列を渡すとサーバ側で全置換される。既存項目を残したい場合は GET → 部分更新 → PATCH の手順を踏む
|
|
819
|
+
|
|
820
|
+
**スプリント自動着手 (Definition of Ready):**
|
|
821
|
+
- ボードタスクは `assignee_minion_id == self && status IN ('todo', 'doing') && sprint.status == 'active'` のとき、
|
|
822
|
+
ミニオンの `board-task-poller` が自動検知し `doing` へ遷移して **専用 tmux セッション (`bt-{taskId8}`) で自走** を起動する (v3.46.0〜)。
|
|
823
|
+
- `doing` 状態のタスクも対象に含むのは前スプリントからの持ち越しを救済するため。同名 tmux セッションが既に生きていれば再起動せずスキップ (二重実行防止)。
|
|
824
|
+
- スプリント開始(`active`遷移)には全タスクで以下が必要:
|
|
825
|
+
- `assignee_minion_id` が設定されている
|
|
826
|
+
- `acceptance_criteria` が1件以上ある
|
|
827
|
+
- 自動着手したミニオンは acceptance_criteria を満たしたら `status: 'review'` に遷移する。`done` は基本的に人間の承認後。
|
|
828
|
+
|
|
812
829
|
ミニオン経由で作成したタスクには `created_by_minion_id` に自身のIDが自動設定される。
|
|
813
830
|
|
|
814
831
|
PATCH body: 同じフィールドを任意で部分指定。**`status` を変更すると `status_changed_at` がサーバ側で自動更新される**(stalled 検出に使われる)。手動で `status_changed_at` を渡しても無視される。`assignee_minion_id` を set するともう一方の `assignee_user_id` は自動 null、その逆も同じ。
|
|
@@ -825,6 +842,61 @@ reorder body:
|
|
|
825
842
|
|
|
826
843
|
両 neighbor が null の場合は列の先頭/初期値で挿入される。
|
|
827
844
|
|
|
845
|
+
#### `[task:UUID]` チケットタグ (HQチャット内専用)
|
|
846
|
+
|
|
847
|
+
ユーザーがHQチャットでチケットを参照する際、`[task:UUID]` 形式のタグを
|
|
848
|
+
メッセージに埋め込むことができる。HQプロキシがタグを検出してタスク詳細を
|
|
849
|
+
解決し、チャットリクエストの `referenced_tasks` フィールドに同梱して
|
|
850
|
+
ミニオンに送信する。ミニオンは受信時にプロンプト先頭へ「参照チケット」
|
|
851
|
+
ブロックとして注入する(履歴には保存されない)。
|
|
852
|
+
|
|
853
|
+
ユーザーメッセージ例:
|
|
854
|
+
```
|
|
855
|
+
[task:550e8400-e29b-41d4-a716-446655440000] このチケットの進捗を教えて
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
Claudeへの注入例:
|
|
859
|
+
```
|
|
860
|
+
[参照チケット — ユーザーがメッセージ内で `[task:UUID]` 形式で参照しているHQボード上のタスク]
|
|
861
|
+
- [task:550e8400-...] チケット名 (status: doing, priority: high) / 説明...
|
|
862
|
+
詳細/更新: GET|PATCH $HQ_URL/api/minion/projects/<projectId>/tasks/<taskId>
|
|
863
|
+
|
|
864
|
+
(ユーザーメッセージ本文)
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
応答内でユーザーへのチケット言及にも同タグを使ってよい(HQ側でチップに描画される)。
|
|
868
|
+
|
|
869
|
+
### Project Sprints (Agile自動着手)
|
|
870
|
+
|
|
871
|
+
スプリントはプロジェクトスコープのエンティティで、`planned -> active -> completed` の状態遷移を持つ。
|
|
872
|
+
**1プロジェクトにつき active スプリントは1つだけ** (DB側の partial unique index で強制)。
|
|
873
|
+
|
|
874
|
+
スプリントが `active` のとき、含まれるタスクのうち以下を満たすものはミニオンの `board-task-poller` (30秒間隔) が
|
|
875
|
+
自動的に検知して着手する:
|
|
876
|
+
- `assignee_minion_id == 自分`
|
|
877
|
+
- `status IN ('todo', 'doing')`
|
|
878
|
+
- `sprint.status == 'active'`
|
|
879
|
+
|
|
880
|
+
着手時の流れ (v3.46.0〜):
|
|
881
|
+
1. ローカル tmux に `bt-{taskId8}` セッションが既にあればスキップ (二重実行防止 / 持ち越しの再起動防止)
|
|
882
|
+
2. `POST /api/minion/board-tasks/:taskId/claim` で冪等に `doing` に遷移 (`todo→doing` または `doing→doing` でタイムスタンプ更新)
|
|
883
|
+
3. **専用 tmux セッション (`bt-{taskId8}`) で `claude -p` を直接実行**。Linuxはホスト上のtmux、Windowsは WSL 内の tmux。
|
|
884
|
+
タスクのタイトル/受け入れ要件、project_contexts.content、メンバー一覧がプロンプトに自動注入される。
|
|
885
|
+
4. ミニオンは acceptance_criteria を満たしたら `status: 'review'` に遷移してユーザーレビュー待ちにする。
|
|
886
|
+
|
|
887
|
+
セッション可視性:
|
|
888
|
+
- Linuxミニオン: `tmux ls | grep '^bt-'` でセッション一覧。WSターミナルからアタッチ可能。
|
|
889
|
+
- Windowsミニオン: `wsl tmux ls | grep '^bt-'` で確認 (or HQ ダッシュボードのターミナル一覧で `wsl-tmux` タイプとして表示)。
|
|
890
|
+
|
|
891
|
+
ミニオンが自分で参照する用途のためのスプリント API:
|
|
892
|
+
|
|
893
|
+
| Method | Endpoint | Description |
|
|
894
|
+
|--------|----------|-------------|
|
|
895
|
+
| GET | `/api/projects/:projectId/sprints` | スプリント一覧 (HQ認証 — ユーザーUI用) |
|
|
896
|
+
|
|
897
|
+
ミニオンからスプリントを直接作成・遷移させる必要は通常ない (PMロールのユーザーが管理する)。
|
|
898
|
+
状況確認用には標準のタスクAPIで `?sprint_id=<uuid>` を使えば所属タスクが取得できる。
|
|
899
|
+
|
|
828
900
|
### Project Milestones
|
|
829
901
|
|
|
830
902
|
| Method | Endpoint | Description |
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Board Task Runner (Linux)
|
|
3
|
+
*
|
|
4
|
+
* Starts a board task in a dedicated detached tmux session so the work is
|
|
5
|
+
* visible from the WS terminal (`tmux ls` shows `bt-{taskId8}`) and survives
|
|
6
|
+
* minion restarts. Unlike workflow/routine runners this does not run on a
|
|
7
|
+
* cron — it is invoked by board-task-poller after a successful claim.
|
|
8
|
+
*
|
|
9
|
+
* Session naming: `bt-{taskId.slice(0,8)}`
|
|
10
|
+
*
|
|
11
|
+
* Lifecycle:
|
|
12
|
+
* 1. If a `bt-*` tmux session already exists for this task, return early
|
|
13
|
+
* (carry-over case: minion restarted while the task was running, or a
|
|
14
|
+
* previous sprint left it alive). The existing session is the authority.
|
|
15
|
+
* 2. Otherwise spawn `claude -p < kickoff.txt` inside a new tmux session
|
|
16
|
+
* with remain-on-exit so the post-completion buffer stays inspectable.
|
|
17
|
+
* 3. Wait for an exit-code file to appear, then release.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { exec } = require('child_process')
|
|
21
|
+
const { promisify } = require('util')
|
|
22
|
+
const fs = require('fs').promises
|
|
23
|
+
const os = require('os')
|
|
24
|
+
const path = require('path')
|
|
25
|
+
const execAsync = promisify(exec)
|
|
26
|
+
|
|
27
|
+
const { config } = require('../core/config')
|
|
28
|
+
const runningTasks = require('../core/lib/running-tasks')
|
|
29
|
+
const logManager = require('../core/lib/log-manager')
|
|
30
|
+
const { getActivePrimary } = require('../core/llm-plugins/lib/active')
|
|
31
|
+
const { buildBoardTaskInjection } = require('../core/lib/board-task-context')
|
|
32
|
+
|
|
33
|
+
// 4 hours — board tasks can be long-running but not infinite.
|
|
34
|
+
const TASK_TIMEOUT_MS = 4 * 60 * 60 * 1000
|
|
35
|
+
const POLL_INTERVAL_MS = 5_000
|
|
36
|
+
|
|
37
|
+
function generateSessionName(taskId) {
|
|
38
|
+
if (!taskId) throw new Error('taskId is required')
|
|
39
|
+
return `bt-${String(taskId).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 listBoardTaskSessions() {
|
|
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('bt-'))
|
|
58
|
+
} catch {
|
|
59
|
+
return []
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildKickoffPrompt(task, contextData) {
|
|
64
|
+
const lines = [
|
|
65
|
+
`[ボードタスク自動着手] [task:${task.id}] ${task.title}`,
|
|
66
|
+
'',
|
|
67
|
+
'このボードタスクが自動でアサインされたため、着手を開始してください。',
|
|
68
|
+
'アクティブスプリント内でステータスを `doing` にしました。',
|
|
69
|
+
'',
|
|
70
|
+
'完了したら以下で `review` に遷移してユーザーにレビュー依頼してください:',
|
|
71
|
+
` PATCH \$HQ_URL/api/minion/projects/${task.project_id}/tasks/${task.id}`,
|
|
72
|
+
' Body: {"status": "review"}',
|
|
73
|
+
'',
|
|
74
|
+
]
|
|
75
|
+
const injection = buildBoardTaskInjection({
|
|
76
|
+
type: 'board_task',
|
|
77
|
+
task,
|
|
78
|
+
project_id: task.project_id,
|
|
79
|
+
sprint: contextData.sprint || null,
|
|
80
|
+
project_context_content: contextData.projectContextContent || null,
|
|
81
|
+
members: contextData.members || null,
|
|
82
|
+
})
|
|
83
|
+
if (injection) {
|
|
84
|
+
lines.push(injection)
|
|
85
|
+
}
|
|
86
|
+
return lines.join('\n')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Start (or resume tracking of) a board task tmux session.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} params
|
|
93
|
+
* @param {object} params.task - Claimed task object from HQ.
|
|
94
|
+
* @param {object} [params.contextData] - { sprint, projectContextContent, members }
|
|
95
|
+
* @returns {Promise<{ sessionName: string, started: boolean, success: boolean, error?: string }>}
|
|
96
|
+
*/
|
|
97
|
+
async function runBoardTask({ task, contextData = {} }) {
|
|
98
|
+
if (!task || !task.id) {
|
|
99
|
+
return { sessionName: null, started: false, success: false, error: 'task is required' }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const sessionName = generateSessionName(task.id)
|
|
103
|
+
const homeDir = config.HOME_DIR
|
|
104
|
+
|
|
105
|
+
// Carry-over: a tmux session for this task is already alive. Don't relaunch.
|
|
106
|
+
if (await tmuxHasSession(sessionName)) {
|
|
107
|
+
console.log(`[BoardTaskRunner] tmux session ${sessionName} already exists, skipping start`)
|
|
108
|
+
runningTasks.add({
|
|
109
|
+
type: 'board_task',
|
|
110
|
+
session_name: sessionName,
|
|
111
|
+
board_task_id: task.id,
|
|
112
|
+
started_at: new Date().toISOString(),
|
|
113
|
+
})
|
|
114
|
+
return { sessionName, started: false, success: true }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const exitCodeFile = `/tmp/tmux-exit-${sessionName}`
|
|
118
|
+
const promptFile = path.join(os.tmpdir(), `minion-board-task-prompt-${sessionName}.txt`)
|
|
119
|
+
const execScript = path.join(os.tmpdir(), `minion-board-task-exec-${sessionName}.sh`)
|
|
120
|
+
|
|
121
|
+
console.log(`[BoardTaskRunner] Starting board task ${task.id} (${task.title})`)
|
|
122
|
+
console.log(`[BoardTaskRunner] tmux session: ${sessionName}`)
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await logManager.ensureLogDir()
|
|
126
|
+
await execAsync(`rm -f "${exitCodeFile}"`)
|
|
127
|
+
|
|
128
|
+
const prompt = buildKickoffPrompt(task, contextData)
|
|
129
|
+
await fs.writeFile(promptFile, prompt, 'utf-8')
|
|
130
|
+
|
|
131
|
+
const primary = getActivePrimary()
|
|
132
|
+
let llmCommand
|
|
133
|
+
if (primary && typeof primary.buildShellInvocation === 'function') {
|
|
134
|
+
llmCommand = primary.buildShellInvocation({ promptFile })
|
|
135
|
+
} else if (config.LLM_COMMAND) {
|
|
136
|
+
llmCommand = `${config.LLM_COMMAND} < ${promptFile}`
|
|
137
|
+
} else {
|
|
138
|
+
throw new Error('No LLM configured. Set a Primary plugin via /api/llm/config or LLM_COMMAND in minion.env')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await fs.writeFile(
|
|
142
|
+
execScript,
|
|
143
|
+
`#!/bin/bash\n${llmCommand}\necho $? > ${exitCodeFile}\n`,
|
|
144
|
+
'utf-8',
|
|
145
|
+
)
|
|
146
|
+
await execAsync(`chmod +x "${execScript}"`)
|
|
147
|
+
|
|
148
|
+
await execAsync(
|
|
149
|
+
`tmux new-session -d -s "${sessionName}" -x 200 -y 50`,
|
|
150
|
+
{ cwd: homeDir },
|
|
151
|
+
)
|
|
152
|
+
await execAsync(`tmux set-option -t "${sessionName}" remain-on-exit on`)
|
|
153
|
+
|
|
154
|
+
// Pipe-pane to per-task log file (reuse log-manager naming under board-task-)
|
|
155
|
+
const logFile = path.join(logManager.LOG_DIR || path.join(homeDir, '.minion', 'logs'), `board-task-${task.id}.log`)
|
|
156
|
+
try {
|
|
157
|
+
await execAsync(`tmux pipe-pane -o -t "${sessionName}" "cat >> ${logFile}"`)
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error(`[BoardTaskRunner] pipe-pane failed: ${err.message}`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await execAsync(
|
|
163
|
+
`tmux send-keys -t "${sessionName}" "bash ${execScript}" Enter`,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
runningTasks.add({
|
|
167
|
+
type: 'board_task',
|
|
168
|
+
session_name: sessionName,
|
|
169
|
+
board_task_id: task.id,
|
|
170
|
+
started_at: new Date().toISOString(),
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const startTime = Date.now()
|
|
174
|
+
while (Date.now() - startTime < TASK_TIMEOUT_MS) {
|
|
175
|
+
try {
|
|
176
|
+
await fs.access(exitCodeFile)
|
|
177
|
+
break
|
|
178
|
+
} catch {
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (Date.now() - startTime >= TASK_TIMEOUT_MS) {
|
|
184
|
+
console.error(`[BoardTaskRunner] Task ${task.id} timed out after ${TASK_TIMEOUT_MS / 1000}s`)
|
|
185
|
+
await execAsync(`tmux kill-session -t "${sessionName}" 2>/dev/null || true`)
|
|
186
|
+
return { sessionName, started: true, success: false, error: 'Execution timeout' }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let exitCode = 0
|
|
190
|
+
try {
|
|
191
|
+
const { stdout } = await execAsync(`cat "${exitCodeFile}"`)
|
|
192
|
+
exitCode = parseInt(stdout.trim(), 10) || 0
|
|
193
|
+
await execAsync(`rm -f "${exitCodeFile}"`)
|
|
194
|
+
} catch {
|
|
195
|
+
// Treat as success if exit code can't be read but file existed
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (exitCode === 0) {
|
|
199
|
+
console.log(`[BoardTaskRunner] Task ${task.id} CLI completed`)
|
|
200
|
+
return { sessionName, started: true, success: true }
|
|
201
|
+
}
|
|
202
|
+
console.error(`[BoardTaskRunner] Task ${task.id} CLI exited with code ${exitCode}`)
|
|
203
|
+
return { sessionName, started: true, success: false, error: `Exit code: ${exitCode}` }
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.error(`[BoardTaskRunner] Task ${task.id} failed: ${err.message}`)
|
|
206
|
+
return { sessionName, started: true, success: false, error: err.message }
|
|
207
|
+
} finally {
|
|
208
|
+
runningTasks.remove(sessionName)
|
|
209
|
+
try { await fs.unlink(promptFile) } catch {}
|
|
210
|
+
try { await fs.unlink(execScript) } catch {}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check if a board task is currently running (tmux session alive).
|
|
216
|
+
* Used by the poller to skip tasks already being worked on.
|
|
217
|
+
*/
|
|
218
|
+
async function isBoardTaskRunning(taskId) {
|
|
219
|
+
return tmuxHasSession(generateSessionName(taskId))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
runBoardTask,
|
|
224
|
+
isBoardTaskRunning,
|
|
225
|
+
generateSessionName,
|
|
226
|
+
listBoardTaskSessions,
|
|
227
|
+
}
|
package/linux/routes/chat.js
CHANGED
|
@@ -43,7 +43,7 @@ async function chatRoutes(fastify) {
|
|
|
43
43
|
return { success: false, error: 'Unauthorized' }
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const { message, session_id, context, workspace_id } = request.body || {}
|
|
46
|
+
const { message, session_id, context, workspace_id, referenced_tasks } = request.body || {}
|
|
47
47
|
|
|
48
48
|
if (!message || typeof message !== 'string') {
|
|
49
49
|
reply.code(400)
|
|
@@ -53,7 +53,9 @@ async function chatRoutes(fastify) {
|
|
|
53
53
|
const workspaceId = workspace_id || null
|
|
54
54
|
|
|
55
55
|
// Build prompt — add memory context on new sessions + page context + workspace
|
|
56
|
-
|
|
56
|
+
// referenced_tasks is injected into the prompt only (not stored in history)
|
|
57
|
+
// so the user's chat log keeps just the [task:UUID] tag, not a noisy dump.
|
|
58
|
+
const prompt = await buildContextPrefix(message, context, session_id, workspaceId, referenced_tasks)
|
|
57
59
|
|
|
58
60
|
// Store user message
|
|
59
61
|
const currentSessionId = session_id || null
|
|
@@ -258,9 +260,27 @@ ${indexed}`
|
|
|
258
260
|
* On new sessions (no session_id), injects minion memory + recent daily logs.
|
|
259
261
|
* No conversation history injection — Claude CLI handles that via --resume.
|
|
260
262
|
*/
|
|
261
|
-
async function buildContextPrefix(message, context, sessionId, workspaceId) {
|
|
263
|
+
async function buildContextPrefix(message, context, sessionId, workspaceId, referencedTasks) {
|
|
262
264
|
const parts = []
|
|
263
265
|
|
|
266
|
+
// Resolved [task:UUID] tags from HQ — surface ticket details so Claude can
|
|
267
|
+
// talk about them without first having to hit the API. Always include when
|
|
268
|
+
// present (regardless of session state), since tag references can appear in
|
|
269
|
+
// any turn.
|
|
270
|
+
if (Array.isArray(referencedTasks) && referencedTasks.length > 0) {
|
|
271
|
+
parts.push('[参照チケット — ユーザーがメッセージ内で `[task:UUID]` 形式で参照しているHQボード上のタスク]')
|
|
272
|
+
for (const t of referencedTasks) {
|
|
273
|
+
if (!t || !t.id) continue
|
|
274
|
+
const desc = t.description ? ` / ${String(t.description).slice(0, 200).replace(/\s+/g, ' ')}` : ''
|
|
275
|
+
const due = t.due_date ? ` / 期限: ${t.due_date}` : ''
|
|
276
|
+
parts.push(
|
|
277
|
+
`- [task:${t.id}] ${t.title || '(無題)'} (status: ${t.status || '?'}, priority: ${t.priority || '?'}${due})${desc}`,
|
|
278
|
+
` 詳細/更新: GET|PATCH $HQ_URL/api/minion/projects/${t.project_id}/tasks/${t.id}`,
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
parts.push('')
|
|
282
|
+
}
|
|
283
|
+
|
|
264
284
|
// Inject workspace context so Claude Code knows which workspace it's operating in
|
|
265
285
|
if (workspaceId) {
|
|
266
286
|
const workspaceStore = require('../../core/stores/workspace-store')
|
|
@@ -411,9 +431,9 @@ async function buildContextPrefix(message, context, sessionId, workspaceId) {
|
|
|
411
431
|
` hq fetch project ${context.projectId}`,
|
|
412
432
|
` hq fetch project-context ${context.projectId}`,
|
|
413
433
|
`タスク・マイルストーン・健康度を扱う場合は以下のAPIを使えます (Bearer 認証必須):`,
|
|
414
|
-
` GET \$HQ_URL/api/minion/projects/${context.projectId}/tasks # 一覧 (?milestone_id= ?status= ?priority=high,urgent ?assignee_minion_id= ?overdue=true ?q=<substring> 等で絞り込み可。q は日本語OKの部分一致)`,
|
|
415
|
-
` POST \$HQ_URL/api/minion/projects/${context.projectId}/tasks # 作成 (body: title, description?, status?, priority?, milestone_id?, parent_task_id?, assignee_minion_id?, due_date?)`,
|
|
416
|
-
` PATCH \$HQ_URL/api/minion/projects/${context.projectId}/tasks/<id> # 更新 (status変更で status_changed_at がサーバ自動更新、
|
|
434
|
+
` GET \$HQ_URL/api/minion/projects/${context.projectId}/tasks # 一覧 (?milestone_id= ?sprint_id= ?status= ?priority=high,urgent ?assignee_minion_id= ?overdue=true ?q=<substring> 等で絞り込み可。q は日本語OKの部分一致)`,
|
|
435
|
+
` POST \$HQ_URL/api/minion/projects/${context.projectId}/tasks # 作成 (body: title, description?, status?, priority?, milestone_id?, sprint_id?, parent_task_id?, assignee_minion_id?, due_date?, acceptance_criteria?)`,
|
|
436
|
+
` PATCH \$HQ_URL/api/minion/projects/${context.projectId}/tasks/<id> # 更新 (status変更で status_changed_at がサーバ自動更新、acceptance_criteria/sprint_id も更新可。AC更新時は既存idを保持)`,
|
|
417
437
|
` GET \$HQ_URL/api/minion/projects/${context.projectId}/milestones # マイルストーン一覧`,
|
|
418
438
|
` GET \$HQ_URL/api/minion/projects/${context.projectId}/health # 健康度サマリ (overdue/stalled/マイルストーン進捗。progress_pct は leaf タスク基準)`,
|
|
419
439
|
`タスクは5段階Kanban (backlog→todo→doing→review→done)、親子は2階層まで(孫不可)。priority は low|normal|high|urgent (可視化+フィルタ用)。親EPICに milestone_id を付ければ子タスクも進捗に自動反映される。詳細は ~/.minion/docs/api-reference.md の「Project Tasks」「Project Milestones」「Project Health」を参照。`,
|
package/linux/server.js
CHANGED
|
@@ -50,6 +50,7 @@ let lastBeatAt = null
|
|
|
50
50
|
// Linux-specific modules
|
|
51
51
|
const workflowRunner = require('./workflow-runner')
|
|
52
52
|
const routineRunner = require('./routine-runner')
|
|
53
|
+
const boardTaskRunner = require('./board-task-runner')
|
|
53
54
|
const { cleanupTtyd, killStaleTtydProcesses } = require('./routes/terminal')
|
|
54
55
|
const { startTerminalProxy, stopTerminalProxy } = require('./terminal-proxy')
|
|
55
56
|
|
|
@@ -59,6 +60,7 @@ const { getConfigWarnings } = require('../core/lib/config-warnings')
|
|
|
59
60
|
// Pull-model daemons (from core/)
|
|
60
61
|
const stepPoller = require('../core/lib/step-poller')
|
|
61
62
|
const dagStepPoller = require('../core/lib/dag-step-poller')
|
|
63
|
+
const boardTaskPoller = require('../core/lib/board-task-poller')
|
|
62
64
|
const revisionWatcher = require('../core/lib/revision-watcher')
|
|
63
65
|
const reflectionScheduler = require('../core/lib/reflection-scheduler')
|
|
64
66
|
const threadWatcher = require('../core/lib/thread-watcher')
|
|
@@ -125,6 +127,7 @@ async function shutdown(signal) {
|
|
|
125
127
|
// Stop pollers, runners, and scheduler
|
|
126
128
|
stepPoller.stop()
|
|
127
129
|
dagStepPoller.stop()
|
|
130
|
+
boardTaskPoller.stop()
|
|
128
131
|
revisionWatcher.stop()
|
|
129
132
|
reflectionScheduler.stop()
|
|
130
133
|
threadWatcher.stop()
|
|
@@ -404,6 +407,8 @@ async function start() {
|
|
|
404
407
|
// Start Pull-model daemons
|
|
405
408
|
stepPoller.start()
|
|
406
409
|
dagStepPoller.start()
|
|
410
|
+
boardTaskPoller.setRunner(boardTaskRunner)
|
|
411
|
+
boardTaskPoller.start()
|
|
407
412
|
revisionWatcher.start()
|
|
408
413
|
threadWatcher.start(runQuickLlmCall)
|
|
409
414
|
} else {
|
package/mac/bin/hq
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process manager detection and command building (macOS / launchd)
|
|
3
|
+
*
|
|
4
|
+
* macOS uses launchd for service management. The minion agent runs as a
|
|
5
|
+
* LaunchAgent under the dedicated `minion` user; restarts go through
|
|
6
|
+
* `launchctl kickstart -k gui/<uid>/<label>`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { execSync } = require('child_process')
|
|
10
|
+
|
|
11
|
+
// Use sudo only when not running as root (parity with linux/lib/process-manager.js).
|
|
12
|
+
// Most launchctl operations on `gui/<self-uid>/...` do NOT require sudo.
|
|
13
|
+
const SUDO = process.getuid && process.getuid() !== 0 ? 'sudo ' : ''
|
|
14
|
+
|
|
15
|
+
// Resolve once at module load. Used to build `gui/<uid>/<label>` targets.
|
|
16
|
+
function resolveUid() {
|
|
17
|
+
if (process.getuid) return process.getuid()
|
|
18
|
+
// Fallback (should never hit on macOS): query `id -u`
|
|
19
|
+
try {
|
|
20
|
+
return parseInt(execSync('id -u', { encoding: 'utf-8' }).trim(), 10)
|
|
21
|
+
} catch {
|
|
22
|
+
return 0
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const UID = resolveUid()
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Detect the process manager.
|
|
30
|
+
* On macOS, launchd is the only option (PID 1).
|
|
31
|
+
* Returns 'standalone' only if launchctl is somehow missing — which would
|
|
32
|
+
* indicate a broken environment, since launchctl is part of the OS.
|
|
33
|
+
* @returns {'launchd' | 'standalone'}
|
|
34
|
+
*/
|
|
35
|
+
function detectProcessManager() {
|
|
36
|
+
try {
|
|
37
|
+
execSync('launchctl version', { stdio: 'ignore' })
|
|
38
|
+
return 'launchd'
|
|
39
|
+
} catch {
|
|
40
|
+
return 'standalone'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build allowed commands based on detected process manager.
|
|
46
|
+
* @param {string} procMgr - Process manager type
|
|
47
|
+
* @returns {Record<string, { description: string; command: string; deferred?: boolean }>}
|
|
48
|
+
*/
|
|
49
|
+
function buildAllowedCommands(procMgr) {
|
|
50
|
+
const commands = {}
|
|
51
|
+
|
|
52
|
+
if (procMgr === 'launchd') {
|
|
53
|
+
const AGENT_TARGET = `gui/${UID}/com.geekbeer.minion`
|
|
54
|
+
const WEBSOCKIFY_TARGET = `gui/${UID}/com.geekbeer.websockify`
|
|
55
|
+
const TMUX_INIT_TARGET = `gui/${UID}/com.geekbeer.tmux-init`
|
|
56
|
+
|
|
57
|
+
commands['restart-agent'] = {
|
|
58
|
+
description: 'Restart the minion agent service',
|
|
59
|
+
command: `launchctl kickstart -k ${AGENT_TARGET}`,
|
|
60
|
+
deferred: true,
|
|
61
|
+
}
|
|
62
|
+
commands['update-agent'] = {
|
|
63
|
+
description: 'Update @geekbeer/minion to latest version and restart',
|
|
64
|
+
command: `${SUDO}npm install -g @geekbeer/minion@latest && launchctl kickstart -k ${AGENT_TARGET}`,
|
|
65
|
+
deferred: true,
|
|
66
|
+
}
|
|
67
|
+
commands['update-agent-dev'] = {
|
|
68
|
+
description: 'Update @geekbeer/minion from Verdaccio (dev) and restart',
|
|
69
|
+
command: `${SUDO}npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873 && launchctl kickstart -k ${AGENT_TARGET}`,
|
|
70
|
+
deferred: true,
|
|
71
|
+
}
|
|
72
|
+
commands['restart-display'] = {
|
|
73
|
+
description: 'Restart websockify (noVNC bridge); native Screen Sharing is system-managed',
|
|
74
|
+
command: `launchctl kickstart -k ${WEBSOCKIFY_TARGET}`,
|
|
75
|
+
}
|
|
76
|
+
commands['restart-all'] = {
|
|
77
|
+
description: 'Restart websockify and the agent',
|
|
78
|
+
command: `launchctl kickstart -k ${WEBSOCKIFY_TARGET} && launchctl kickstart -k ${AGENT_TARGET}`,
|
|
79
|
+
deferred: true,
|
|
80
|
+
}
|
|
81
|
+
commands['status-services'] = {
|
|
82
|
+
description: 'Print status of all minion LaunchAgents',
|
|
83
|
+
command: `launchctl print ${AGENT_TARGET} 2>/dev/null | head -40 ; echo "---" ; launchctl print ${WEBSOCKIFY_TARGET} 2>/dev/null | head -20 ; echo "---" ; launchctl print ${TMUX_INIT_TARGET} 2>/dev/null | head -20`,
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
// Standalone mode: limited commands (launchctl unavailable — unlikely on macOS)
|
|
87
|
+
commands['update-agent'] = {
|
|
88
|
+
description: 'Update @geekbeer/minion to latest version',
|
|
89
|
+
command: `${SUDO}npm install -g @geekbeer/minion@latest`,
|
|
90
|
+
}
|
|
91
|
+
commands['update-agent-dev'] = {
|
|
92
|
+
description: 'Update @geekbeer/minion from Verdaccio (dev)',
|
|
93
|
+
command: `${SUDO}npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873`,
|
|
94
|
+
}
|
|
95
|
+
commands['status-services'] = {
|
|
96
|
+
description: 'Show agent process info',
|
|
97
|
+
command: 'echo "Process Manager: standalone (launchctl unavailable)" && echo "Agent PID: $$" && echo "Node version: $(node -v)" && echo "Uptime: $(ps -o etime= -p $$)"',
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return commands
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
SUDO,
|
|
106
|
+
UID,
|
|
107
|
+
detectProcessManager,
|
|
108
|
+
buildAllowedCommands,
|
|
109
|
+
}
|