@geekbeer/minion 3.17.0 → 3.23.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.
@@ -23,6 +23,9 @@ const routineStore = require('../core/stores/routine-store')
23
23
  const logManager = require('../core/lib/log-manager')
24
24
  const runningTasks = require('../core/lib/running-tasks')
25
25
  const { expandSkillTemplates, restoreSkillTemplates } = require('../core/lib/template-expander')
26
+ const { getActivePrimary } = require('../core/llm-plugins/lib/active')
27
+ const os = require('os')
28
+ const path = require('path')
26
29
 
27
30
  // Active cron jobs keyed by routine ID
28
31
  const activeJobs = new Map()
@@ -64,14 +67,19 @@ async function executeRoutineSession(routine, executionId, skillNames) {
64
67
  const sessionName = generateSessionName(routine.id, executionId)
65
68
 
66
69
  // Build prompt: run each skill in sequence, then execution-report
67
- const skillCommands = skillNames.map(name => `/${name}`).join(', then ')
70
+ const primary = getActivePrimary()
71
+ const formatSkill = primary && typeof primary.formatSkillInvocation === 'function'
72
+ ? primary.formatSkillInvocation.bind(primary)
73
+ : (name) => `/${name}`
74
+ const skillCommands = skillNames.map(name => formatSkill(name)).join(', then ')
68
75
 
69
76
  // Inject routine context as prompt prefix (mirrors workflow-runner's role injection pattern)
70
77
  const contextPrefix = routine.context
71
78
  ? `## Context\n\n${routine.context}\n\n---\n\n`
72
79
  : ''
73
80
 
74
- const prompt = `${contextPrefix}Run the following skills in order: ${skillCommands}. After completing all skills, run /execution-report to report the results.`
81
+ const reportSkill = formatSkill('execution-report')
82
+ const prompt = `${contextPrefix}Run the following skills in order: ${skillCommands}. After completing all skills, run ${reportSkill} to report the results.`
75
83
 
76
84
  // Exit code file to capture CLI result
77
85
  const exitCodeFile = `/tmp/tmux-exit-${sessionName}`
@@ -106,12 +114,19 @@ async function executeRoutineSession(routine, executionId, skillNames) {
106
114
  // Remove old exit code file
107
115
  await execAsync(`rm -f "${exitCodeFile}"`)
108
116
 
109
- // Build the command to run in tmux
110
- if (!config.LLM_COMMAND) {
111
- throw new Error('LLM_COMMAND is not configured. Set LLM_COMMAND in minion.env (e.g., LLM_COMMAND="claude -p \'{prompt}\'")')
117
+ // Build the LLM invocation (prefer plugin system, fall back to LLM_COMMAND).
118
+ let llmCommand
119
+ const primary = getActivePrimary()
120
+ if (primary && typeof primary.buildShellInvocation === 'function') {
121
+ const promptFile = path.join(os.tmpdir(), `minion-routine-prompt-${sessionName}.txt`)
122
+ await fs.writeFile(promptFile, prompt, 'utf-8')
123
+ llmCommand = primary.buildShellInvocation({ promptFile })
124
+ } else if (config.LLM_COMMAND) {
125
+ const escapedPrompt = prompt.replace(/'/g, "'\\''")
126
+ llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
127
+ } else {
128
+ throw new Error('No LLM configured. Set a Primary plugin via /api/llm/config or LLM_COMMAND in minion.env')
112
129
  }
113
- const escapedPrompt = prompt.replace(/'/g, "'\\''")
114
- const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
115
130
 
116
131
  // Create tmux session with the LLM command.
117
132
  // PATH, HOME, DISPLAY, and minion secrets are already set in
package/linux/server.js CHANGED
@@ -80,6 +80,7 @@ const { threadRoutes } = require('../core/routes/threads')
80
80
  const { todoRoutes } = require('../core/routes/todos')
81
81
  const { emailRoutes } = require('../core/routes/emails')
82
82
  const { daemonRoutes } = require('../core/routes/daemons')
83
+ const { llmRoutes } = require('../core/routes/llm')
83
84
 
84
85
  // Linux-specific routes
85
86
  const { commandRoutes, getProcessManager, getAllowedCommands } = require('./routes/commands')
@@ -290,6 +291,7 @@ async function registerAllRoutes(app) {
290
291
  await app.register(todoRoutes)
291
292
  await app.register(emailRoutes)
292
293
  await app.register(daemonRoutes, { heartbeatStatus: () => ({ running: !!heartbeatTimer, last_beat_at: lastBeatAt }) })
294
+ await app.register(llmRoutes)
293
295
 
294
296
  // Linux-specific routes
295
297
  await app.register(commandRoutes)
@@ -22,6 +22,9 @@ const workflowStore = require('../core/stores/workflow-store')
22
22
  const logManager = require('../core/lib/log-manager')
23
23
  const runningTasks = require('../core/lib/running-tasks')
24
24
  const { expandSkillTemplates, restoreSkillTemplates } = require('../core/lib/template-expander')
25
+ const { getActivePrimary } = require('../core/llm-plugins/lib/active')
26
+ const os = require('os')
27
+ const path = require('path')
25
28
 
26
29
  // Active cron jobs keyed by workflow ID
27
30
  const activeJobs = new Map()
@@ -61,12 +64,17 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
61
64
  const homeDir = config.HOME_DIR
62
65
  const sessionName = generateSessionName(workflow.id, executionId)
63
66
 
64
- // Build prompt: run each skill in sequence
65
- const skillCommands = skillNames.map(name => `/${name}`).join(', then ')
67
+ // Build prompt: run each skill in sequence.
68
+ // Use the Primary plugin's invocation syntax (Claude/Codex use /skill, Gemini uses natural language).
69
+ const primary = getActivePrimary()
70
+ const formatSkill = primary && typeof primary.formatSkillInvocation === 'function'
71
+ ? primary.formatSkillInvocation.bind(primary)
72
+ : (name) => `/${name}`
73
+ const skillCommands = skillNames.map(name => formatSkill(name)).join(', then ')
66
74
 
67
75
  // Inject role context if provided (e.g. "pm" or "engineer")
68
76
  const rolePrefix = options.role
69
- ? `You are acting as the "${options.role}" role in this session. Read ~/.minion/roles/${options.role}.md for your role guidelines before proceeding.\n\n`
77
+ ? `You are acting as the ${options.role} role in this session. Read ~/.minion/roles/${options.role}.md for your role guidelines before proceeding.\n\n`
70
78
  : ''
71
79
 
72
80
  // Inject revision feedback if this is a re-execution after reviewer requested changes
@@ -110,12 +118,22 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
110
118
  // Remove old exit code file
111
119
  await execAsync(`rm -f "${exitCodeFile}"`)
112
120
 
113
- // Build the command to run in tmux
114
- if (!config.LLM_COMMAND) {
115
- throw new Error('LLM_COMMAND is not configured. Set LLM_COMMAND in minion.env (e.g., LLM_COMMAND="claude -p \'{prompt}\'")')
121
+ // Build the LLM invocation. Prefer the plugin system; fall back to LLM_COMMAND.
122
+ let llmCommand
123
+ const primary = getActivePrimary()
124
+ if (primary && typeof primary.buildShellInvocation === 'function') {
125
+ // Write prompt to a temp file and have the plugin build `<bin> ... < file`.
126
+ // This bypasses shell quoting entirely (root cause of the historical
127
+ // LLM_COMMAND quote-corruption class of bugs).
128
+ const promptFile = path.join(os.tmpdir(), `minion-workflow-prompt-${sessionName}.txt`)
129
+ await fs.writeFile(promptFile, prompt, 'utf-8')
130
+ llmCommand = primary.buildShellInvocation({ promptFile })
131
+ } else if (config.LLM_COMMAND) {
132
+ const escapedPrompt = prompt.replace(/'/g, "'\\''")
133
+ llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
134
+ } else {
135
+ throw new Error('No LLM configured. Set a Primary plugin via /api/llm/config or LLM_COMMAND in minion.env')
116
136
  }
117
- const escapedPrompt = prompt.replace(/'/g, "'\\''")
118
- const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
119
137
 
120
138
  // Create tmux session with the LLM command.
121
139
  // PATH, HOME, DISPLAY, and minion secrets are already set in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "3.17.0",
3
+ "version": "3.23.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
@@ -14,18 +14,22 @@ You are an AI agent running on a Minion VPS, managed by @geekbeer/minion.
14
14
  ```
15
15
  Project (組織・課金単位)
16
16
  ├── Context (markdown, PMが更新)
17
- ├── Members (minion + role: pm | engineer)
18
- └── Workflows (スキルのパイプライン + オプションcronスケジュール)
19
- ├── Versions (pipeline の不変スナップショット)
20
- └── Executions (実行履歴, ステップごとの進捗)
17
+ ├── Members (minion + role: pm | engineer | accountant)
18
+ ├── Workflows (スキルの線形パイプライン + オプションcronスケジュール)
19
+ ├── Versions (pipeline の不変スナップショット)
20
+ └── Executions (実行履歴, ステップごとの進捗)
21
+ └── DAG Workflows (ノード/エッジ形式のワークフロー, beta)
22
+ ├── Versions (graph の不変スナップショット)
23
+ └── Executions (node_executions の集合, scope-aware)
21
24
 
22
25
  Minion
23
26
  └── Routines (ミニオンローカルの定期タスク, cron付き)
24
27
  ```
25
28
 
26
- - **Workflow**: プロジェクトスコープ。バージョン管理されたスキルパイプライン。
29
+ - **Workflow**: プロジェクトスコープ。線形パイプライン形式のバージョン管理ワークフロー。ミニオンAPIで push/fetch 可能。
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 ワークフロー」を参照。
27
31
  - **Routine**: ミニオンスコープ。ミニオンローカルの定期タスク。
28
- - ミニオンは複数プロジェクトに `pm` または `engineer` として参加できる。
32
+ - ミニオンは複数プロジェクトに `pm`、`engineer`、`accountant` として参加できる。
29
33
 
30
34
  ## Email
31
35
 
@@ -153,7 +157,9 @@ Note: Codex CLI の `.codex/` ディレクトリはLLMからの直接編集が
153
157
 
154
158
  `$HQ_URL/api/minion/*` — 認証: `Authorization: Bearer $API_TOKEN`
155
159
 
156
- 主なカテゴリ: Projects, Context, Workflows, Skills, Executions, Routines, Reports
160
+ 主なカテゴリ: Projects, Context, Workflows, DAG Workflows, Skills, Executions, Routines, Reports
161
+
162
+ DAG ワークフローのランタイム API は `$HQ_URL/api/dag/minion/*`(pending-nodes / claim-node / node-complete)。`dag-step-poller` デーモンが自動でポーリングするため、通常ミニオンのAI側から直接叩くことは無い。
157
163
 
158
164
  ## Environment Variables
159
165
 
@@ -197,6 +203,18 @@ Routine 実行中は以下もtmuxセッション環境で利用可能:
197
203
  - `pm.md` — PM (Project Manager) のガイドライン
198
204
  - `engineer.md` — Engineer のガイドライン
199
205
 
206
+ ## Todo運用ルール
207
+
208
+ チャット中のタスクは `/api/todos` に登録して進捗管理すること。圧縮(context compaction)を跨いでも作業を完遂するための仕組みがある。
209
+
210
+ - **Todoは「1往復で完了する粒度」に分解して登録する。** 大きいタスクは複数のTodoに分ける。粒度を小さく保てば、完了マークを圧縮に奪われにくい。
211
+ - **完了したら即座に done にマークする。** まとめて更新しない。`PUT /api/todos/:id` で `status=done`。
212
+ - **チャットセッション内で作成するTodoには必ず `session_id` を含める。** プロンプト冒頭の `[現在のチャットセッションID]` の値を使う。紐づいた未完了Todoは次ターン以降に自動で再掲される(圧縮を跨いでも失われない)。
213
+ - **再掲されたTodoを見たら、着手前に「既に完了していないか」を確認する。** 完了済みなら done に更新、未完なら続行。
214
+ - 同一Todoが規定回数以上再掲されても未完了のままの場合、再掲は自動停止する。進展しないTodoはブロッカーとして起票するか手動で `cancelled` にすること。
215
+
216
+ API詳細は `~/.minion/docs/api-reference.md` の「Todos」セクションを参照。
217
+
200
218
  ## Blocker Handling (ブロッカー対処)
201
219
 
202
220
  タスク実行中にブロッカー(自力で解決できない問題)に遭遇した場合、以下のフローに従うこと。
package/win/bin/hq.ps1 CHANGED
@@ -4,9 +4,9 @@
4
4
  HQ API helper for minion chat context (Windows version)
5
5
 
6
6
  .DESCRIPTION
7
- Fetches resource details from the HQ server API.
8
- Used by Claude CLI during chat to retrieve information about
9
- skills, workflows, and projects that the user is viewing on the dashboard.
7
+ Fetches and manipulates resources on the HQ server API.
8
+ Used by Claude CLI during chat to retrieve / edit information about
9
+ skills, workflows, projects, and DAG workflows.
10
10
 
11
11
  Environment variables (inherited from minion server):
12
12
  HQ_URL - HQ server URL (e.g., https://minion-agent.com)
@@ -17,6 +17,11 @@
17
17
  .\hq.ps1 fetch workflow <name>
18
18
  .\hq.ps1 fetch project <id>
19
19
  .\hq.ps1 fetch project-context <id>
20
+ .\hq.ps1 fetch dag-workflow <id>
21
+ .\hq.ps1 fetch dag-execution <id>
22
+ .\hq.ps1 create dag-workflow <body.json>
23
+ .\hq.ps1 put dag-workflow <id> <body.json>
24
+ .\hq.ps1 publish dag-workflow <id>
20
25
  #>
21
26
 
22
27
  param(
@@ -24,15 +29,17 @@ param(
24
29
  [string]$Command,
25
30
 
26
31
  [Parameter(Position = 1)]
27
- [string]$Resource,
32
+ [string]$Arg1,
28
33
 
29
34
  [Parameter(Position = 2)]
30
- [string]$Identifier
35
+ [string]$Arg2,
36
+
37
+ [Parameter(Position = 3)]
38
+ [string]$Arg3
31
39
  )
32
40
 
33
41
  $ErrorActionPreference = 'Stop'
34
42
 
35
- # Validate required environment variables
36
43
  if (-not $env:HQ_URL) {
37
44
  Write-Error "Error: HQ_URL is not set"
38
45
  exit 1
@@ -47,10 +54,46 @@ $Headers = @{ 'Authorization' = "Bearer $($env:API_TOKEN)" }
47
54
 
48
55
  function Invoke-HqApi {
49
56
  param([string]$Url)
50
-
51
57
  try {
52
58
  $response = Invoke-RestMethod -Uri $Url -Headers $Headers -Method Get -ErrorAction Stop
53
- $response | ConvertTo-Json -Depth 10
59
+ $response | ConvertTo-Json -Depth 20
60
+ }
61
+ catch {
62
+ $statusCode = $_.Exception.Response.StatusCode.value__
63
+ Write-Error "Error: HQ API returned HTTP $statusCode"
64
+ Write-Error $_.ErrorDetails.Message
65
+ exit 1
66
+ }
67
+ }
68
+
69
+ function Assert-ValidJsonFile {
70
+ param([string]$Path)
71
+ if (-not (Test-Path -Path $Path -PathType Leaf)) {
72
+ Write-Error "Error: file not found: $Path"
73
+ exit 1
74
+ }
75
+ try {
76
+ $raw = Get-Content -Raw -Path $Path
77
+ $null = $raw | ConvertFrom-Json -ErrorAction Stop
78
+ }
79
+ catch {
80
+ Write-Error "Error: invalid JSON syntax in $Path"
81
+ Write-Error $_.Exception.Message
82
+ exit 1
83
+ }
84
+ }
85
+
86
+ function Send-JsonRequest {
87
+ param(
88
+ [string]$Method,
89
+ [string]$Url,
90
+ [string]$Path
91
+ )
92
+ $raw = Get-Content -Raw -Path $Path
93
+ try {
94
+ $response = Invoke-RestMethod -Uri $Url -Headers $Headers -Method $Method `
95
+ -ContentType 'application/json' -Body $raw -ErrorAction Stop
96
+ $response | ConvertTo-Json -Depth 20
54
97
  }
55
98
  catch {
56
99
  $statusCode = $_.Exception.Response.StatusCode.value__
@@ -60,49 +103,134 @@ function Invoke-HqApi {
60
103
  }
61
104
  }
62
105
 
106
+ function Send-EmptyPost {
107
+ param([string]$Url)
108
+ try {
109
+ $response = Invoke-RestMethod -Uri $Url -Headers $Headers -Method Post `
110
+ -ContentType 'application/json' -Body '' -ErrorAction Stop
111
+ $response | ConvertTo-Json -Depth 20
112
+ }
113
+ catch {
114
+ $statusCode = $_.Exception.Response.StatusCode.value__
115
+ Write-Error "Error: HQ API returned HTTP $statusCode"
116
+ Write-Error $_.ErrorDetails.Message
117
+ exit 1
118
+ }
119
+ }
120
+
121
+ function Show-Usage {
122
+ Write-Host "HQ API helper for minion chat" -ForegroundColor Cyan
123
+ Write-Host ""
124
+ Write-Host "Usage:"
125
+ Write-Host " hq fetch skill <name> - Get skill details"
126
+ Write-Host " hq fetch workflow <name> - Get workflow details"
127
+ Write-Host " hq fetch project <id> - Get project info"
128
+ Write-Host " hq fetch project-context <id> - Get project context"
129
+ Write-Host " hq fetch dag-workflow <id> - Get DAG workflow details"
130
+ Write-Host " hq fetch dag-execution <id> - Get DAG execution details"
131
+ Write-Host " hq create dag-workflow <body.json> - Create a DAG workflow (PM only)"
132
+ Write-Host " hq put dag-workflow <id> <body.json> - Update DAG workflow draft (PM only)"
133
+ Write-Host " hq publish dag-workflow <id> - Publish the draft as a new version (PM only)"
134
+ }
135
+
63
136
  switch ($Command) {
64
137
  'fetch' {
138
+ $Resource = $Arg1
139
+ $Identifier = $Arg2
65
140
  if (-not $Resource -or -not $Identifier) {
66
- Write-Error "Usage: hq fetch {skill|workflow|project|project-context} <identifier>"
141
+ Write-Error "Usage: hq fetch {skill|workflow|project|project-context|dag-workflow|dag-execution} <identifier>"
67
142
  exit 1
68
143
  }
69
144
 
70
145
  switch ($Resource) {
71
- 'skill' {
72
- Invoke-HqApi "$BaseUrl/skills/$Identifier"
73
- }
74
- 'workflow' {
75
- Invoke-HqApi "$BaseUrl/workflows/$Identifier"
76
- }
146
+ 'skill' { Invoke-HqApi "$BaseUrl/skills/$Identifier" }
147
+ 'workflow' { Invoke-HqApi "$BaseUrl/workflows/$Identifier" }
77
148
  'project' {
78
149
  $response = Invoke-RestMethod -Uri "$BaseUrl/me/projects" -Headers $Headers -Method Get
79
150
  $project = $response.projects | Where-Object { $_.id -eq $Identifier }
80
151
  if ($project) {
81
- $project | ConvertTo-Json -Depth 10
152
+ $project | ConvertTo-Json -Depth 20
82
153
  }
83
154
  else {
84
155
  Write-Error "Project not found: $Identifier"
85
156
  exit 1
86
157
  }
87
158
  }
88
- 'project-context' {
89
- Invoke-HqApi "$BaseUrl/me/project/$Identifier/context"
90
- }
159
+ 'project-context' { Invoke-HqApi "$BaseUrl/me/project/$Identifier/context" }
160
+ 'dag-workflow' { Invoke-HqApi "$BaseUrl/dag-workflows/$Identifier" }
161
+ 'dag-execution' { Invoke-HqApi "$BaseUrl/dag-executions/$Identifier" }
91
162
  default {
92
163
  Write-Error "Unknown resource: $Resource"
93
- Write-Error "Usage: hq fetch {skill|workflow|project|project-context} <identifier>"
164
+ Write-Error "Usage: hq fetch {skill|workflow|project|project-context|dag-workflow|dag-execution} <identifier>"
165
+ exit 1
166
+ }
167
+ }
168
+ }
169
+
170
+ 'create' {
171
+ $Resource = $Arg1
172
+ switch ($Resource) {
173
+ 'dag-workflow' {
174
+ $BodyFile = $Arg2
175
+ if (-not $BodyFile) {
176
+ Write-Error "Usage: hq create dag-workflow <body.json>"
177
+ Write-Error " body.json must contain at least { project_id, name } and optionally { graph, content, change_summary }"
178
+ exit 1
179
+ }
180
+ Assert-ValidJsonFile -Path $BodyFile
181
+ Send-JsonRequest -Method 'Post' -Url "$BaseUrl/dag-workflows" -Path $BodyFile
182
+ }
183
+ default {
184
+ Write-Error "Unknown create resource: $Resource"
185
+ Write-Error "Usage: hq create dag-workflow <body.json>"
186
+ exit 1
187
+ }
188
+ }
189
+ }
190
+
191
+ 'put' {
192
+ $Resource = $Arg1
193
+ switch ($Resource) {
194
+ 'dag-workflow' {
195
+ $Id = $Arg2
196
+ $BodyFile = $Arg3
197
+ if (-not $Id -or -not $BodyFile) {
198
+ Write-Error "Usage: hq put dag-workflow <id> <body.json>"
199
+ Write-Error " body.json may contain { graph, content, change_summary, name, is_active, maturity }"
200
+ exit 1
201
+ }
202
+ Assert-ValidJsonFile -Path $BodyFile
203
+ Send-JsonRequest -Method 'Put' -Url "$BaseUrl/dag-workflows/$Id" -Path $BodyFile
204
+ }
205
+ default {
206
+ Write-Error "Unknown put resource: $Resource"
207
+ Write-Error "Usage: hq put dag-workflow <id> <body.json>"
94
208
  exit 1
95
209
  }
96
210
  }
97
211
  }
212
+
213
+ 'publish' {
214
+ $Resource = $Arg1
215
+ switch ($Resource) {
216
+ 'dag-workflow' {
217
+ $Id = $Arg2
218
+ if (-not $Id) {
219
+ Write-Error "Usage: hq publish dag-workflow <id>"
220
+ exit 1
221
+ }
222
+ Send-EmptyPost -Url "$BaseUrl/dag-workflows/$Id/publish"
223
+ }
224
+ default {
225
+ Write-Error "Unknown publish resource: $Resource"
226
+ Write-Error "Usage: hq publish dag-workflow <id>"
227
+ exit 1
228
+ }
229
+ }
230
+ }
231
+
98
232
  default {
99
- Write-Host "HQ API helper for minion chat" -ForegroundColor Cyan
100
- Write-Host ""
101
- Write-Host "Usage:"
102
- Write-Host " hq fetch skill <name> - Get skill details"
103
- Write-Host " hq fetch workflow <name> - Get workflow details"
104
- Write-Host " hq fetch project <id> - Get project info"
105
- Write-Host " hq fetch project-context <id> - Get project context"
233
+ Show-Usage
106
234
  exit 1
107
235
  }
108
236
  }
@@ -19,8 +19,10 @@ const http = require('http')
19
19
  const { verifyToken } = require('../../core/lib/auth')
20
20
  const { config } = require('../../core/config')
21
21
  const chatStore = require('../../core/stores/chat-store')
22
+ const todoStore = require('../../core/stores/todo-store')
22
23
  const { DATA_DIR } = require('../../core/lib/platform')
23
24
  const { runEndOfDay } = require('../../core/lib/end-of-day')
25
+ const { getActivePrimary } = require('../../core/llm-plugins/lib/active')
24
26
 
25
27
  let activeChatChild = null
26
28
  let wslModeActive = false
@@ -318,6 +320,32 @@ ${indexed}`
318
320
  async function buildContextPrefix(message, context, sessionId) {
319
321
  const parts = []
320
322
 
323
+ // Re-inject unfinished todos tied to this session. Survives context
324
+ // compaction — Claude sees outstanding todos again on the next turn.
325
+ // Skipped past MAX_INJECTION_COUNT to prevent infinite loops.
326
+ if (sessionId) {
327
+ parts.push(
328
+ `[現在のチャットセッションID] ${sessionId}`,
329
+ '新規Todoを作成する際は `session_id` にこの値を含めてください(圧縮を跨いだ自動再掲の対象になります)。',
330
+ ''
331
+ )
332
+ const activeTodos = todoStore.listActiveForSession(sessionId)
333
+ if (activeTodos.length > 0) {
334
+ parts.push(
335
+ '[未完了のToDo(このセッション起点)]',
336
+ '以下のToDoが未完了のまま残っています。着手前に「既に完了していないか」を確認し、',
337
+ '完了済みなら `PUT /api/todos/:id` で status=done に更新、未完なら続行してください。',
338
+ ''
339
+ )
340
+ for (const t of activeTodos) {
341
+ const desc = t.description ? ` — ${t.description}` : ''
342
+ parts.push(`- [${t.id}] (${t.status}/${t.priority}) ${t.title}${desc}`)
343
+ }
344
+ parts.push('')
345
+ todoStore.markInjected(activeTodos.map(t => t.id))
346
+ }
347
+ }
348
+
321
349
  // Tell the LLM how to access memory and daily logs via API
322
350
  if (!sessionId) {
323
351
  const port = require('../../core/config').config.AGENT_PORT
@@ -398,6 +426,45 @@ async function buildContextPrefix(message, context, sessionId) {
398
426
  )
399
427
  }
400
428
  break
429
+ case 'dag-workflow':
430
+ if (context.projectId && context.dagWorkflowId) {
431
+ parts.push(
432
+ `ユーザーはHQダッシュボードで DAG ワークフロー (ID: ${context.dagWorkflowId}) のエディタ/詳細を閲覧しています。`,
433
+ `DAG ワークフローはノード/エッジ形式でスキル間の依存関係を表現し、fan-out / join / conditional / transform / review をサポートします。`,
434
+ `DAG ワークフロー情報を取得するには以下を実行してください:`,
435
+ ` hq fetch dag-workflow ${context.dagWorkflowId}`,
436
+ `プロジェクトコンテキスト:`,
437
+ ` hq fetch project-context ${context.projectId}`,
438
+ `PMロールの場合、graph JSON を直接編集できます:`,
439
+ ` hq put dag-workflow ${context.dagWorkflowId} <body.json> # ドラフト更新 (構造チェックのみ)`,
440
+ ` hq publish dag-workflow ${context.dagWorkflowId} # ドラフトを新バージョンとして公開 (フル検証)`,
441
+ `新規作成は: hq create dag-workflow <body.json>`,
442
+ `DAG の構造(nodes/edges/node types/scope_path 等)や実行フローの詳細は ~/.minion/docs/api-reference.md の「DAG Workflows」セクション、および ~/.minion/docs/task-guides.md の「DAG ワークフロー」セクションを参照してください。`,
443
+ `取得した内容をもとに回答してください。`
444
+ )
445
+ }
446
+ break
447
+ case 'dag-execution':
448
+ if (context.projectId && context.dagExecutionId) {
449
+ parts.push(
450
+ `ユーザーはHQダッシュボードで DAG 実行 (ID: ${context.dagExecutionId}) の詳細を閲覧しています。`,
451
+ `DAG 実行の詳細(graph_snapshot + 各 node_executions の状態)を取得するには以下を実行してください:`,
452
+ ` hq fetch dag-execution ${context.dagExecutionId}`
453
+ )
454
+ if (context.dagWorkflowId) {
455
+ parts.push(
456
+ `対応するDAGワークフロー定義:`,
457
+ ` hq fetch dag-workflow ${context.dagWorkflowId}`
458
+ )
459
+ }
460
+ parts.push(
461
+ `プロジェクトコンテキスト:`,
462
+ ` hq fetch project-context ${context.projectId}`,
463
+ `ノード状態の意味(pending/waiting/running/completed/failed/skipped, review_status, scope_path 等)は ~/.minion/docs/api-reference.md の「DAG Workflows」セクションを参照してください。`,
464
+ `取得した内容をもとに回答してください。`
465
+ )
466
+ }
467
+ break
401
468
  }
402
469
  }
403
470
  if (parts.length > 0) return `${parts.join('\n')}\n\n${message}`
@@ -416,7 +483,70 @@ function getLlmBinary() {
416
483
  * Tracks block types to correctly forward tool_use vs text events
417
484
  * and counts turns for session management.
418
485
  */
419
- function streamLlmResponse(res, prompt, sessionId, workspaceId, originalMessage) {
486
+ async function streamLlmResponse(res, prompt, sessionId, workspaceId, originalMessage) {
487
+ const primary = getActivePrimary()
488
+ if (primary) {
489
+ return streamViaPlugin(primary, res, prompt, sessionId, workspaceId, originalMessage)
490
+ }
491
+ return streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, originalMessage)
492
+ }
493
+
494
+ async function streamViaPlugin(plugin, res, prompt, sessionId, workspaceId, originalMessage) {
495
+ const input = { prompt }
496
+ const activeRef = { current: null }
497
+
498
+ let fullResponse = ''
499
+ let resolvedSessionId = sessionId || null
500
+ let turnCount = 0
501
+
502
+ const emit = event => {
503
+ if (event.type === 'session') {
504
+ resolvedSessionId = event.sessionId
505
+ } else if (event.type === 'delta') {
506
+ fullResponse += event.content
507
+ res.write(`data: ${JSON.stringify({ type: 'delta', content: event.content })}\n\n`)
508
+ } else if (event.type === 'text') {
509
+ fullResponse += event.content
510
+ res.write(`data: ${JSON.stringify({ type: 'text', content: event.content })}\n\n`)
511
+ turnCount++
512
+ } else {
513
+ res.write(`data: ${JSON.stringify(event)}\n\n`)
514
+ }
515
+ }
516
+
517
+ res.on('close', () => { activeRef.current?.kill?.('SIGTERM') })
518
+
519
+ let output
520
+ if (plugin.capabilities.streaming && typeof plugin.stream === 'function') {
521
+ output = await plugin.stream(input, emit, { resumeSessionId: sessionId, activeChildRef: activeRef })
522
+ } else {
523
+ output = await plugin.invoke(input)
524
+ if (output.text) {
525
+ fullResponse = output.text
526
+ res.write(`data: ${JSON.stringify({ type: 'text', content: output.text })}\n\n`)
527
+ turnCount = 1
528
+ }
529
+ if (output.error) {
530
+ res.write(`data: ${JSON.stringify({ type: 'error', error: output.error.message })}\n\n`)
531
+ }
532
+ }
533
+ resolvedSessionId = output?.metadata?.sessionId || resolvedSessionId
534
+
535
+ if (resolvedSessionId) {
536
+ if (!sessionId) {
537
+ await chatStore.addMessage(resolvedSessionId, { role: 'user', content: originalMessage || prompt }, undefined, workspaceId)
538
+ }
539
+ if (fullResponse) {
540
+ await chatStore.addMessage(resolvedSessionId, { role: 'assistant', content: fullResponse }, turnCount, workspaceId)
541
+ }
542
+ }
543
+
544
+ const session = await chatStore.load(workspaceId)
545
+ const totalTurnCount = session?.turn_count || turnCount
546
+ res.write(`data: ${JSON.stringify({ type: 'done', session_id: resolvedSessionId, turn_count: totalTurnCount })}\n\n`)
547
+ }
548
+
549
+ function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, originalMessage) {
420
550
  return new Promise((resolve, reject) => {
421
551
  const binaryName = getLlmBinary()
422
552
  if (!binaryName) {
@@ -597,7 +727,17 @@ function streamLlmResponse(res, prompt, sessionId, workspaceId, originalMessage)
597
727
  /**
598
728
  * Run a quick non-streaming LLM call (for summarization etc.)
599
729
  */
600
- function runQuickLlmCall(prompt) {
730
+ async function runQuickLlmCall(prompt) {
731
+ const primary = getActivePrimary()
732
+ if (primary) {
733
+ const output = await primary.invoke({ prompt, model: primary.name === 'claude' ? 'haiku' : undefined, timeoutMs: 30000 })
734
+ if (output.error) throw new Error(output.error.message)
735
+ return output.text || ''
736
+ }
737
+ return runQuickLlmCallLegacy(prompt)
738
+ }
739
+
740
+ function runQuickLlmCallLegacy(prompt) {
601
741
  return new Promise((resolve, reject) => {
602
742
  const binaryName = getLlmBinary()
603
743
  if (!binaryName) {