@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.
@@ -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
+ }
@@ -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`):
@@ -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`, `maturity`, `my_role` 等が含まれる。個別の graph 詳細は `hq fetch dag-workflow <id>` で取得する。
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, maturity }" >&2
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "3.49.1",
3
+ "version": "3.51.1",
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
@@ -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, maturity }"
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()