@geekbeer/minion 3.49.1 → 3.51.1
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/lib/dag-cron-poller.js +101 -0
- package/core/lib/note-mentions.js +54 -0
- package/docs/api-reference.md +30 -3
- package/docs/task-guides.md +3 -1
- package/linux/bin/hq +1 -1
- package/linux/server.js +2 -0
- package/package.json +1 -1
- package/rules/core.md +27 -1
- package/win/bin/hq.ps1 +1 -1
- package/win/server.js +2 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DAG Cron Poller
|
|
3
|
+
*
|
|
4
|
+
* Polling daemon that asks HQ to fire any due cron-scheduled DAG workflows
|
|
5
|
+
* for projects where this minion is the PM. The actual claim, trigger, and
|
|
6
|
+
* next_run_at advancement are all done in HQ — the poller is just a clock
|
|
7
|
+
* source. Multiple PMs in the same project are race-safe via a conditional
|
|
8
|
+
* UPDATE on dag_workflows.next_run_at inside the HQ handler.
|
|
9
|
+
*
|
|
10
|
+
* The minimum cron interval (5 min) makes a 60s polling cadence more than
|
|
11
|
+
* precise enough; cron precision is bounded by max(poll_interval, 60s).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { config, isHqConfigured } = require('../config')
|
|
15
|
+
|
|
16
|
+
const POLL_INTERVAL_MS = 60_000
|
|
17
|
+
|
|
18
|
+
let polling = false
|
|
19
|
+
let pollTimer = null
|
|
20
|
+
let lastPollAt = null
|
|
21
|
+
let lastFiredCount = 0
|
|
22
|
+
|
|
23
|
+
async function pollOnce() {
|
|
24
|
+
if (!isHqConfigured()) return
|
|
25
|
+
if (polling) return
|
|
26
|
+
|
|
27
|
+
polling = true
|
|
28
|
+
try {
|
|
29
|
+
const url = `${config.HQ_URL}/api/dag/minion/dag-cron-tick`
|
|
30
|
+
const resp = await fetch(url, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
'Authorization': `Bearer ${config.API_TOKEN}`,
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
if (!resp.ok) {
|
|
39
|
+
throw new Error(`dag-cron-tick failed: ${resp.status}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const data = await resp.json()
|
|
43
|
+
const fired = Array.isArray(data.fired) ? data.fired : []
|
|
44
|
+
const skipped = Array.isArray(data.skipped) ? data.skipped : []
|
|
45
|
+
const errors = Array.isArray(data.errors) ? data.errors : []
|
|
46
|
+
|
|
47
|
+
lastFiredCount = fired.length
|
|
48
|
+
|
|
49
|
+
if (fired.length > 0 || errors.length > 0) {
|
|
50
|
+
console.log(
|
|
51
|
+
`[DagCronPoller] Fired ${fired.length}, skipped ${skipped.length}, errors ${errors.length}`
|
|
52
|
+
)
|
|
53
|
+
for (const f of fired) {
|
|
54
|
+
console.log(`[DagCronPoller] fired: ${f.name} (workflow ${f.workflow_id} → execution ${f.execution_id})`)
|
|
55
|
+
}
|
|
56
|
+
for (const e of errors) {
|
|
57
|
+
console.error(`[DagCronPoller] error on ${e.workflow_id}: ${e.error}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err.message?.includes('fetch failed') || err.message?.includes('ECONNREFUSED')) {
|
|
62
|
+
console.log('[DagCronPoller] HQ unreachable, will retry next cycle')
|
|
63
|
+
} else {
|
|
64
|
+
console.error(`[DagCronPoller] Poll error: ${err.message}`)
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
polling = false
|
|
68
|
+
lastPollAt = new Date().toISOString()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function start() {
|
|
73
|
+
if (!isHqConfigured()) {
|
|
74
|
+
console.log('[DagCronPoller] HQ not configured, cron poller disabled')
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Stagger initial poll so the three pollers (step / dag-step / cron) do
|
|
79
|
+
// not all hit HQ in the same second on minion startup.
|
|
80
|
+
setTimeout(() => pollOnce(), 12_000)
|
|
81
|
+
pollTimer = setInterval(() => pollOnce(), POLL_INTERVAL_MS)
|
|
82
|
+
console.log(`[DagCronPoller] Started (polling every ${POLL_INTERVAL_MS / 1000}s)`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function stop() {
|
|
86
|
+
if (pollTimer) {
|
|
87
|
+
clearInterval(pollTimer)
|
|
88
|
+
pollTimer = null
|
|
89
|
+
console.log('[DagCronPoller] Stopped')
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getStatus() {
|
|
94
|
+
return {
|
|
95
|
+
running: pollTimer !== null,
|
|
96
|
+
last_poll_at: lastPollAt,
|
|
97
|
+
last_fired_count: lastFiredCount,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { start, stop, pollOnce, getStatus }
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for the note user-mention syntax: `@[Display Name](user:UUID)`.
|
|
3
|
+
*
|
|
4
|
+
* HQ stores notes as Markdown. When humans write `@[Yuno](user:abc-...)` in
|
|
5
|
+
* the note editor (via Ctrl+I), the minion needs to read it as a normal
|
|
6
|
+
* `@Yuno`-style attribution rather than the raw bracket form. These helpers
|
|
7
|
+
* normalize and parse that syntax so skills/runners do not have to reinvent it.
|
|
8
|
+
*
|
|
9
|
+
* The UUID is the stable identity; the display name is a snapshot at insertion
|
|
10
|
+
* time and is the human-friendly handle.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Allow short test ids too (>= 8 chars) so the helper is usable in fixtures
|
|
14
|
+
// even when the canonical 36-char UUID is not present.
|
|
15
|
+
const USER_MENTION_REGEX = /@\[([^\]\n]+?)\]\(user:([0-9a-fA-F-]{8,})\)/g
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Replace `@[Display Name](user:UUID)` with `@Display Name`.
|
|
19
|
+
* Pass minion-bound markdown through this before injecting into a prompt or
|
|
20
|
+
* task payload so Claude does not see the raw bracket syntax.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} markdown
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
function normalizeNoteMentions(markdown) {
|
|
26
|
+
if (typeof markdown !== 'string' || markdown.length === 0) return markdown
|
|
27
|
+
return markdown.replace(USER_MENTION_REGEX, (_match, name) => `@${name}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extract every user mention as { userId, displayName } in document order.
|
|
32
|
+
* Useful when a workflow step needs to know "who wrote this note" without
|
|
33
|
+
* round-tripping through HQ.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} markdown
|
|
36
|
+
* @returns {Array<{ userId: string, displayName: string }>}
|
|
37
|
+
*/
|
|
38
|
+
function extractNoteMentions(markdown) {
|
|
39
|
+
if (typeof markdown !== 'string' || markdown.length === 0) return []
|
|
40
|
+
const out = []
|
|
41
|
+
// RegExp.exec with /g maintains lastIndex on the regex itself, so use a fresh copy.
|
|
42
|
+
const re = new RegExp(USER_MENTION_REGEX.source, 'g')
|
|
43
|
+
let match
|
|
44
|
+
while ((match = re.exec(markdown)) !== null) {
|
|
45
|
+
out.push({ displayName: match[1], userId: match[2] })
|
|
46
|
+
}
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
USER_MENTION_REGEX,
|
|
52
|
+
normalizeNoteMentions,
|
|
53
|
+
extractNoteMentions,
|
|
54
|
+
}
|
package/docs/api-reference.md
CHANGED
|
@@ -1275,8 +1275,7 @@ PUT `/api/minion/dag-workflows/:id` body(全フィールド optional、省略
|
|
|
1275
1275
|
"content": "...",
|
|
1276
1276
|
"change_summary": "Updated fan-out branch",
|
|
1277
1277
|
"name": "renamed-dag",
|
|
1278
|
-
"is_active": true
|
|
1279
|
-
"maturity": "preview|stable|..."
|
|
1278
|
+
"is_active": true
|
|
1280
1279
|
}
|
|
1281
1280
|
```
|
|
1282
1281
|
|
|
@@ -1634,7 +1633,6 @@ GET `/api/minion/dag-workflows/:id` Response:
|
|
|
1634
1633
|
"project_id": "uuid",
|
|
1635
1634
|
"name": "my-dag",
|
|
1636
1635
|
"is_active": true,
|
|
1637
|
-
"maturity": "draft|stable|...",
|
|
1638
1636
|
"current_version_id": "uuid",
|
|
1639
1637
|
"draft_graph": { "nodes": [...], "edges": [...] } ,
|
|
1640
1638
|
"draft_content": "markdown|null",
|
|
@@ -1693,9 +1691,12 @@ GET `/api/minion/dag-executions/:id` Response:
|
|
|
1693
1691
|
| GET | `/api/dag/minion/pending-nodes` | 自分が実行すべき pending ノード一覧(ロール一致・依存解決済み) |
|
|
1694
1692
|
| POST | `/api/dag/minion/claim-node` | ノードを楽観ロックで取得(排他実行) |
|
|
1695
1693
|
| POST | `/api/dag/minion/node-complete` | ノード完了を報告し、下流ノードへカスケード |
|
|
1694
|
+
| POST | `/api/dag/minion/dag-cron-tick` | PMロールのミニオン専用。所属プロジェクトのcron-enabled DAGワークフローを発火 |
|
|
1696
1695
|
|
|
1697
1696
|
ミニオンの `dag-step-poller` デーモンが30秒ごとに `pending-nodes` を叩き、最大2並列で claim → skill/transform 実行 → node-complete の流れを回す。`skill`・`transform` 以外のノード(start/end/review/fan_out/join/conditional)はHQ内部のカスケードエンジンが処理するため、ミニオンには返されない。
|
|
1698
1697
|
|
|
1698
|
+
`dag-cron-poller` デーモン(v3.51.0〜)は60秒ごとに `dag-cron-tick` を叩く。HQ側で「呼び出しミニオンがPMロールであるプロジェクトのDAGワークフロー」のうち `cron_enabled AND next_run_at <= now()` を atomic claim → triggerDagExecution。Push型ではないので、PMミニオンが offline ならcron発火が遅延する。
|
|
1699
|
+
|
|
1699
1700
|
#### GET /api/dag/minion/pending-nodes
|
|
1700
1701
|
|
|
1701
1702
|
Response:
|
|
@@ -1774,6 +1775,32 @@ Response:
|
|
|
1774
1775
|
{ "success": true, "review_pending": true }
|
|
1775
1776
|
```
|
|
1776
1777
|
|
|
1778
|
+
#### POST /api/dag/minion/dag-cron-tick
|
|
1779
|
+
|
|
1780
|
+
PMロールのミニオン専用。所属プロジェクトのcron-enabled DAGワークフローを発火させる。`dag-cron-poller` が60秒間隔で叩くため、AI側から直接呼ぶ必要はない。
|
|
1781
|
+
|
|
1782
|
+
Body: なし
|
|
1783
|
+
|
|
1784
|
+
Response:
|
|
1785
|
+
```json
|
|
1786
|
+
{
|
|
1787
|
+
"fired": [
|
|
1788
|
+
{ "workflow_id": "uuid", "execution_id": "uuid", "name": "Daily Report" }
|
|
1789
|
+
],
|
|
1790
|
+
"skipped": [
|
|
1791
|
+
{ "workflow_id": "uuid", "reason": "claimed_by_other|already_running" }
|
|
1792
|
+
],
|
|
1793
|
+
"errors": [
|
|
1794
|
+
{ "workflow_id": "uuid", "error": "..." }
|
|
1795
|
+
]
|
|
1796
|
+
}
|
|
1797
|
+
```
|
|
1798
|
+
|
|
1799
|
+
- 呼び出しミニオンがPMロールでないプロジェクトのワークフローは対象外(HQ側で `project_members.role='pm'` でフィルタ)
|
|
1800
|
+
- 同一プロジェクトに複数のPMミニオンが居る場合、条件付き UPDATE で先勝ちclaim。負けた側は `skipped: claimed_by_other` で返る
|
|
1801
|
+
- `cron_skip_if_running=true` のワークフローで前回実行が `pending`/`running` のままなら `skipped: already_running` で返る
|
|
1802
|
+
- 発火後の経路は手動トリガーと同じ。`dag_executions` 行作成 → start auto-complete → cascade → 各ミニオンの `dag-step-poller` が pending node を pull
|
|
1803
|
+
|
|
1777
1804
|
### DAG Graph Structure
|
|
1778
1805
|
|
|
1779
1806
|
DAG ワークフローの graph は以下の構造で保存される(`dag_workflow_versions.graph` / `dag_executions.graph_snapshot`):
|
package/docs/task-guides.md
CHANGED
|
@@ -154,6 +154,8 @@ DAG ワークフローは有向非巡回グラフでスキル間の依存関係
|
|
|
154
154
|
|
|
155
155
|
**HQダッシュボードの DAG エディタ(プロジェクト画面 → 「DAG (beta)」タブ)で GUI 編集**できるほか、**ミニオンからは JSON ベースで編集可能**(PMロールのみ)。ミニオンは実行ランタイムも担当し、`dag-step-poller` デーモンが pending ノードを自動で処理します。
|
|
156
156
|
|
|
157
|
+
**cronスケジュール (v3.51.0〜):** DAGワークフローは cron 式による定期実行をサポート。設定はHQダッシュボードのDAGビューにある SchedulePanel から行う(最小実行間隔 5分)。発火主体はそのプロジェクトのPMロールのミニオンで、`dag-cron-poller` デーモンが60秒間隔で `/api/dag/minion/dag-cron-tick` を叩いて発火させる。PM不在のプロジェクトでは cron は発火しない。
|
|
158
|
+
|
|
157
159
|
### DAG ワークフロー一覧の取得
|
|
158
160
|
|
|
159
161
|
```bash
|
|
@@ -164,7 +166,7 @@ hq list dag-workflows
|
|
|
164
166
|
hq list dag-workflows <project-uuid>
|
|
165
167
|
```
|
|
166
168
|
|
|
167
|
-
レスポンスには各ワークフローの `id`, `name`, `is_active`, `
|
|
169
|
+
レスポンスには各ワークフローの `id`, `name`, `is_active`, `my_role` 等が含まれる。個別の graph 詳細は `hq fetch dag-workflow <id>` で取得する。
|
|
168
170
|
|
|
169
171
|
### ミニオンによる DAG ワークフロー編集 (PM のみ)
|
|
170
172
|
|
package/linux/bin/hq
CHANGED
|
@@ -275,7 +275,7 @@ case "${1:-}" in
|
|
|
275
275
|
body_file="${4:-}"
|
|
276
276
|
if [ -z "$id" ] || [ -z "$body_file" ]; then
|
|
277
277
|
echo "Usage: hq put dag-workflow <id> <body.json>" >&2
|
|
278
|
-
echo " body.json may contain { graph, content, change_summary, name, is_active
|
|
278
|
+
echo " body.json may contain { graph, content, change_summary, name, is_active }" >&2
|
|
279
279
|
exit 1
|
|
280
280
|
fi
|
|
281
281
|
validate_json_file "$body_file"
|
package/linux/server.js
CHANGED
|
@@ -60,6 +60,7 @@ const { getConfigWarnings } = require('../core/lib/config-warnings')
|
|
|
60
60
|
// Pull-model daemons (from core/)
|
|
61
61
|
const stepPoller = require('../core/lib/step-poller')
|
|
62
62
|
const dagStepPoller = require('../core/lib/dag-step-poller')
|
|
63
|
+
const dagCronPoller = require('../core/lib/dag-cron-poller')
|
|
63
64
|
const boardTaskPoller = require('../core/lib/board-task-poller')
|
|
64
65
|
const revisionWatcher = require('../core/lib/revision-watcher')
|
|
65
66
|
const reflectionScheduler = require('../core/lib/reflection-scheduler')
|
|
@@ -407,6 +408,7 @@ async function start() {
|
|
|
407
408
|
// Start Pull-model daemons
|
|
408
409
|
stepPoller.start()
|
|
409
410
|
dagStepPoller.start()
|
|
411
|
+
dagCronPoller.start()
|
|
410
412
|
boardTaskPoller.setRunner(boardTaskRunner)
|
|
411
413
|
boardTaskPoller.start()
|
|
412
414
|
revisionWatcher.start()
|
package/package.json
CHANGED
package/rules/core.md
CHANGED
|
@@ -176,7 +176,9 @@ Note: Codex CLI の `.codex/` ディレクトリはLLMからの直接編集が
|
|
|
176
176
|
|
|
177
177
|
主なカテゴリ: Projects, Context, Workflows, DAG Workflows, Skills, Executions, Routines, Reports
|
|
178
178
|
|
|
179
|
-
DAG ワークフローのランタイム API は `$HQ_URL/api/dag/minion/*`(pending-nodes / claim-node / node-complete)。`dag-step-poller` デーモンが自動でポーリングするため、通常ミニオンのAI側から直接叩くことは無い。
|
|
179
|
+
DAG ワークフローのランタイム API は `$HQ_URL/api/dag/minion/*`(pending-nodes / claim-node / node-complete / dag-cron-tick)。`dag-step-poller` と `dag-cron-poller` デーモンが自動でポーリングするため、通常ミニオンのAI側から直接叩くことは無い。
|
|
180
|
+
|
|
181
|
+
**DAG Cron Trigger (v3.51.0〜):** `dag_workflows.cron_enabled = true` が設定されたDAGワークフローは、所属プロジェクトのPMミニオンの `dag-cron-poller` が60秒間隔で `POST /api/dag/minion/dag-cron-tick` を叩いて発火する。発火主体はPMロールのミニオンに限定(HQ側で `project_members.role='pm'` でフィルタ)。最小実行間隔は5分(HQ側 `validateCronExpression` で enforce)。発火後の経路は手動トリガーと同じで、`dag_executions` 行が作成され、通常通り各ミニオンの `dag-step-poller` がpendingノードをclaimする。
|
|
180
182
|
|
|
181
183
|
## Environment Variables
|
|
182
184
|
|
|
@@ -390,6 +392,30 @@ hq note get <project_id> <note_id>
|
|
|
390
392
|
hq note search <project_id> "キーワード"
|
|
391
393
|
```
|
|
392
394
|
|
|
395
|
+
### ノート内のユーザーメンション
|
|
396
|
+
|
|
397
|
+
人間ユーザーがノート編集UIで `Ctrl+I` を押すと、自分の発言を示すアバター付きチップが行頭に挿入される。マークダウン上は次の形式で永続化される:
|
|
398
|
+
|
|
399
|
+
```
|
|
400
|
+
@[Display Name](user:UUID)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
ミニオンがノートを読む場合、この記法はそのままだと冗長なので、`@/Display Name` 表記に正規化してから処理する。`packages/minion/core/lib/note-mentions.js` のヘルパを使う:
|
|
404
|
+
|
|
405
|
+
```js
|
|
406
|
+
const { normalizeNoteMentions, extractNoteMentions } = require('./core/lib/note-mentions')
|
|
407
|
+
|
|
408
|
+
// 例: ノート本文を読み取って正規化
|
|
409
|
+
const normalized = normalizeNoteMentions(noteMarkdown)
|
|
410
|
+
// "@Yuno が書きました。"
|
|
411
|
+
|
|
412
|
+
// 例: 誰の発言かを抽出
|
|
413
|
+
const mentions = extractNoteMentions(noteMarkdown)
|
|
414
|
+
// [{ displayName: 'Yuno', userId: 'abc-...' }, ...]
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
UUIDは安定IDなので、表示名が変わっても発言者の同一性を判定できる。書き戻す側のミニオンは原則この記法を生成しないこと(人間専用の発言マーカー)。
|
|
418
|
+
|
|
393
419
|
### `~/files/` への保存
|
|
394
420
|
|
|
395
421
|
バイナリファイルやユーザーがダウンロードする必要があるファイルは `~/files/` に配置する。
|
package/win/bin/hq.ps1
CHANGED
|
@@ -237,7 +237,7 @@ switch ($Command) {
|
|
|
237
237
|
$BodyFile = $Arg3
|
|
238
238
|
if (-not $Id -or -not $BodyFile) {
|
|
239
239
|
Write-Error "Usage: hq put dag-workflow <id> <body.json>"
|
|
240
|
-
Write-Error " body.json may contain { graph, content, change_summary, name, is_active
|
|
240
|
+
Write-Error " body.json may contain { graph, content, change_summary, name, is_active }"
|
|
241
241
|
exit 1
|
|
242
242
|
}
|
|
243
243
|
Assert-ValidJsonFile -Path $BodyFile
|
package/win/server.js
CHANGED
|
@@ -36,6 +36,7 @@ const { getConfigWarnings } = require('../core/lib/config-warnings')
|
|
|
36
36
|
// Pull-model daemons (from core/)
|
|
37
37
|
const stepPoller = require('../core/lib/step-poller')
|
|
38
38
|
const dagStepPoller = require('../core/lib/dag-step-poller')
|
|
39
|
+
const dagCronPoller = require('../core/lib/dag-cron-poller')
|
|
39
40
|
const boardTaskPoller = require('../core/lib/board-task-poller')
|
|
40
41
|
const revisionWatcher = require('../core/lib/revision-watcher')
|
|
41
42
|
const reflectionScheduler = require('../core/lib/reflection-scheduler')
|
|
@@ -374,6 +375,7 @@ async function start() {
|
|
|
374
375
|
// Start Pull-model daemons
|
|
375
376
|
stepPoller.start()
|
|
376
377
|
dagStepPoller.start()
|
|
378
|
+
dagCronPoller.start()
|
|
377
379
|
boardTaskPoller.setRunner(boardTaskRunner)
|
|
378
380
|
boardTaskPoller.start()
|
|
379
381
|
revisionWatcher.start()
|