@geekbeer/minion 3.32.0 → 3.36.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/lib/dag-step-poller.js +59 -15
- package/docs/api-reference.md +48 -7
- package/docs/task-guides.md +51 -4
- package/linux/routes/chat.js +4 -2
- package/linux/routine-runner.js +22 -5
- package/linux/workflow-runner.js +38 -12
- package/package.json +1 -1
- package/rules/core.md +7 -0
- package/win/routes/chat.js +4 -2
- package/win/workflow-runner.js +5 -3
|
@@ -255,6 +255,7 @@ async function executeTransformNode(node) {
|
|
|
255
255
|
assigned_role,
|
|
256
256
|
input_data,
|
|
257
257
|
transform_instruction,
|
|
258
|
+
input_contracts,
|
|
258
259
|
output_contracts,
|
|
259
260
|
} = node
|
|
260
261
|
|
|
@@ -290,7 +291,12 @@ async function executeTransformNode(node) {
|
|
|
290
291
|
|
|
291
292
|
// 2. Create ephemeral skill from transform_instruction.
|
|
292
293
|
// Write to every active plugin's skill dir so any Primary can find it.
|
|
293
|
-
const skillContent = buildTransformSkillContent(
|
|
294
|
+
const skillContent = buildTransformSkillContent(
|
|
295
|
+
transform_instruction,
|
|
296
|
+
input_data,
|
|
297
|
+
input_contracts,
|
|
298
|
+
output_contracts,
|
|
299
|
+
)
|
|
294
300
|
for (const dir of ephemeralSkillDirs) {
|
|
295
301
|
await fs.mkdir(dir, { recursive: true })
|
|
296
302
|
await fs.writeFile(path.join(dir, 'SKILL.md'), skillContent, 'utf-8')
|
|
@@ -357,50 +363,88 @@ async function executeTransformNode(node) {
|
|
|
357
363
|
|
|
358
364
|
/**
|
|
359
365
|
* Build SKILL.md content for a transform node's ephemeral skill.
|
|
366
|
+
*
|
|
367
|
+
* Transform is contract-driven: the Output Contract is the authoritative
|
|
368
|
+
* target shape, enforced by HQ's runtime validator on node-complete.
|
|
369
|
+
* `instruction` is an optional hint layered on top for cases where the
|
|
370
|
+
* contract alone doesn't convey the intent (unit conversion, filter
|
|
371
|
+
* criteria, renaming conventions, etc.).
|
|
360
372
|
*/
|
|
361
|
-
function buildTransformSkillContent(instruction, inputData, outputContracts) {
|
|
373
|
+
function buildTransformSkillContent(instruction, inputData, inputContracts, outputContracts) {
|
|
362
374
|
const lines = [
|
|
363
375
|
'---',
|
|
364
376
|
'name: dag-transform',
|
|
365
377
|
'description: DAG Transform Node',
|
|
366
378
|
'---',
|
|
367
379
|
'',
|
|
368
|
-
'You are a data transformation step in a DAG workflow.',
|
|
380
|
+
'You are a data transformation step in a DAG workflow. Your job is to',
|
|
381
|
+
'reshape the Input Data into an object that conforms **exactly** to the',
|
|
382
|
+
'Output Contract. HQ will reject the result if any required field is',
|
|
383
|
+
'missing or has the wrong type.',
|
|
369
384
|
'',
|
|
370
385
|
'## Input Data',
|
|
371
386
|
'```json',
|
|
372
387
|
JSON.stringify(inputData, null, 2),
|
|
373
388
|
'```',
|
|
374
|
-
'',
|
|
375
|
-
'## Transform Instruction',
|
|
376
|
-
instruction,
|
|
377
389
|
]
|
|
378
390
|
|
|
391
|
+
if (inputContracts && inputContracts.length > 0) {
|
|
392
|
+
lines.push('', '## Input Contract')
|
|
393
|
+
lines.push('The Input Data above conforms to:')
|
|
394
|
+
for (const ic of inputContracts) {
|
|
395
|
+
lines.push(`### ${ic.contract_name}`)
|
|
396
|
+
lines.push(ic.contract.description || '')
|
|
397
|
+
appendContractTable(lines, ic.contract.fields || [])
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
379
401
|
if (outputContracts && outputContracts.length > 0) {
|
|
380
|
-
lines.push('', '## Output Contract')
|
|
381
|
-
lines.push(
|
|
402
|
+
lines.push('', '## Output Contract (REQUIRED)')
|
|
403
|
+
lines.push(
|
|
404
|
+
'Produce a JSON object matching this contract. Every required field',
|
|
405
|
+
'must be present with the declared type. Extra fields are discarded.',
|
|
406
|
+
)
|
|
382
407
|
for (const oc of outputContracts) {
|
|
383
408
|
lines.push(`### ${oc.contract_name}`)
|
|
384
409
|
lines.push(oc.contract.description || '')
|
|
385
|
-
lines.
|
|
386
|
-
lines.push('|-------|------|----------|-------------|')
|
|
387
|
-
for (const f of oc.contract.fields || []) {
|
|
388
|
-
lines.push(`| ${f.key} | ${f.type} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |`)
|
|
389
|
-
}
|
|
410
|
+
appendContractTable(lines, oc.contract.fields || [])
|
|
390
411
|
}
|
|
412
|
+
} else {
|
|
413
|
+
lines.push(
|
|
414
|
+
'',
|
|
415
|
+
'## Output Contract',
|
|
416
|
+
'⚠️ No output contract was declared on the outgoing edge. This transform',
|
|
417
|
+
'node is misconfigured and should not have reached execution. Return',
|
|
418
|
+
'the input data unchanged and fail loudly.',
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (instruction && instruction.trim()) {
|
|
423
|
+
lines.push('', '## Hint (optional)')
|
|
424
|
+
lines.push(instruction.trim())
|
|
391
425
|
}
|
|
392
426
|
|
|
393
427
|
lines.push(
|
|
394
428
|
'',
|
|
395
429
|
'## Task',
|
|
396
|
-
'
|
|
397
|
-
'
|
|
430
|
+
'1. Read Input Data and the Output Contract carefully.',
|
|
431
|
+
'2. Construct a JSON object satisfying the Output Contract.',
|
|
432
|
+
'3. Output it under a "## Output Data" section with a json code block.',
|
|
398
433
|
'Do NOT output anything other than the Output Data section.',
|
|
399
434
|
)
|
|
400
435
|
|
|
401
436
|
return lines.join('\n')
|
|
402
437
|
}
|
|
403
438
|
|
|
439
|
+
function appendContractTable(lines, fields) {
|
|
440
|
+
lines.push('| Field | Type | Required | Description |')
|
|
441
|
+
lines.push('|-------|------|----------|-------------|')
|
|
442
|
+
for (const f of fields) {
|
|
443
|
+
const typeDisplay = f.type === 'array' && f.items ? `array<${f.items}>` : f.type
|
|
444
|
+
lines.push(`| ${f.name} | ${typeDisplay} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |`)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
404
448
|
/**
|
|
405
449
|
* Resolve skill_version_id to skill name via HQ API.
|
|
406
450
|
*/
|
package/docs/api-reference.md
CHANGED
|
@@ -1048,8 +1048,13 @@ PUT `/api/minion/dag-workflows/:id` body(全フィールド optional、省略
|
|
|
1048
1048
|
}
|
|
1049
1049
|
```
|
|
1050
1050
|
|
|
1051
|
-
- `graph` を渡すと draft_graph
|
|
1051
|
+
- `graph` を渡すと draft_graph が上書きされる(構造チェック + structural validation: contract参照の型整合性チェックは実行、ノード完全性チェックは `publish` 時のみ)
|
|
1052
1052
|
- `content` / `change_summary` は `graph` と併せて draft スロットに保存される
|
|
1053
|
+
- **推奨**: graph 全文PUTは**原則使用しない**。代わりに個別APIの `/nodes` `/edges` `/contracts` を使ってインクリメンタルに編集すること。全文PUTは型の取り違え(例: `edge.contract` に配列を渡す)が発生しやすく、バリデーションエラーで400が返る
|
|
1054
|
+
- 具体的な制約:
|
|
1055
|
+
- `edge.contract` は**単一の**Contract名(string)。配列は不可
|
|
1056
|
+
- `contract.fields[].items` は Contract名(string)で、`graph.contracts` 内に存在するものを参照
|
|
1057
|
+
- 不整合があると `{ "error": "...", "details": [...] }` の形式で400エラー
|
|
1053
1058
|
|
|
1054
1059
|
POST `/api/minion/dag-workflows/:id/publish` (body なし):
|
|
1055
1060
|
- 現在の draft_graph を `validateDagGraph` でフル検証
|
|
@@ -1105,7 +1110,7 @@ POST `/api/minion/dag-workflows/:id/publish` (body なし):
|
|
|
1105
1110
|
| `fan_out` | `fan_out_source`, `template` | `template` は sub-graph `{ nodes, edges }` |
|
|
1106
1111
|
| `join` | `join_mode`, `aggregation` | |
|
|
1107
1112
|
| `conditional` | `condition_type`, `branches` or `default_branch` | |
|
|
1108
|
-
| `transform` | `
|
|
1113
|
+
| `transform` | `assigned_role`, incoming edge 1 本 + outgoing edge 1 本 (両方 `contract` 必須) | `transform_instruction` は optional hint。I/O 型は edge の contract から自動導出される |
|
|
1109
1114
|
|
|
1110
1115
|
**review ノード追加の例:**
|
|
1111
1116
|
```json
|
|
@@ -1202,19 +1207,54 @@ POST `/api/minion/dag-workflows/:id/publish` (body なし):
|
|
|
1202
1207
|
{
|
|
1203
1208
|
description: string // contract の説明
|
|
1204
1209
|
fields: [{
|
|
1205
|
-
|
|
1210
|
+
name: string // フィールド名(※ "key" ではない)
|
|
1206
1211
|
type: "string" | "number" | "boolean" | "url" | "array" | "object" // フィールド型
|
|
1207
1212
|
description: string // フィールドの説明
|
|
1208
1213
|
required?: boolean // 必須フラグ (省略時 false)
|
|
1214
|
+
items?: string // type='array' 時のみ。要素の型を表す別Contract名
|
|
1209
1215
|
}]
|
|
1210
1216
|
}
|
|
1211
1217
|
```
|
|
1212
1218
|
|
|
1219
|
+
**⚠️ 重要: 各 field には必ず `name` プロパティを使用すること(`key` ではない)**。他のスキーマ言語(JSON Schema、OpenAPI 等)の慣習に引きずられて `key` と書かないように注意。構造バリデーションで弾かれる(400エラー)。
|
|
1220
|
+
|
|
1213
1221
|
**注意:**
|
|
1214
1222
|
- エッジに設定する `contract` は `graph.contracts` に存在する名前でなければならない(存在しない名前を指定すると 400 エラー)
|
|
1223
|
+
- **エッジが参照できる Contract は 1 つのみ**。`edge.contract` に配列を渡すことは不可(400エラー)。複数の型構造を束ねたい場合は、それらを束ねた複合Contractを1つ定義してから参照すること
|
|
1215
1224
|
- contract を削除すると、参照しているエッジの `contract` フィールドが自動でクリアされる(DELETE / PUT 共通)
|
|
1216
1225
|
- `validate` エンドポイントはダングリング参照(存在しない contract への参照)をエラーとして報告する
|
|
1217
1226
|
|
|
1227
|
+
**Contract内で List<別Contract> を表現する方法 (items):**
|
|
1228
|
+
|
|
1229
|
+
`type: 'array'` のフィールドに `items` プロパティを設定すると、配列要素の型を別Contractで記述できる。`items` の値は `graph.contracts` 内のContract名。
|
|
1230
|
+
|
|
1231
|
+
```json
|
|
1232
|
+
{
|
|
1233
|
+
"contracts": {
|
|
1234
|
+
"Article": {
|
|
1235
|
+
"description": "個別の記事",
|
|
1236
|
+
"fields": [
|
|
1237
|
+
{ "name": "title", "type": "string", "required": true, "description": "タイトル" },
|
|
1238
|
+
{ "name": "url", "type": "url", "description": "記事URL" }
|
|
1239
|
+
]
|
|
1240
|
+
},
|
|
1241
|
+
"NewsCollection": {
|
|
1242
|
+
"description": "収集されたニュース全体",
|
|
1243
|
+
"fields": [
|
|
1244
|
+
{ "name": "articles", "type": "array", "items": "Article", "required": true, "description": "記事リスト" },
|
|
1245
|
+
{ "name": "collected_at", "type": "string", "required": true, "description": "収集日時" },
|
|
1246
|
+
{ "name": "count", "type": "number", "required": true, "description": "件数" }
|
|
1247
|
+
]
|
|
1248
|
+
}
|
|
1249
|
+
},
|
|
1250
|
+
"edges": [
|
|
1251
|
+
{ "id": "edge_1", "source": "a", "target": "b", "contract": "NewsCollection" }
|
|
1252
|
+
]
|
|
1253
|
+
}
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
この構造で「エッジは単一Contract (`NewsCollection`) を参照し、その内部で `articles` フィールドが `Article[]` 型」という意味になる。
|
|
1257
|
+
|
|
1218
1258
|
##### GET `/api/minion/dag-workflows/:id/contracts` — 全contracts取得
|
|
1219
1259
|
|
|
1220
1260
|
レスポンス: `{ "contracts": { "name": { "description": "...", "fields": [...] }, ... } }`
|
|
@@ -1227,8 +1267,8 @@ POST `/api/minion/dag-workflows/:id/publish` (body なし):
|
|
|
1227
1267
|
"prototype": {
|
|
1228
1268
|
"description": "プロトタイプ成果物",
|
|
1229
1269
|
"fields": [
|
|
1230
|
-
{ "
|
|
1231
|
-
{ "
|
|
1270
|
+
{ "name": "git_url", "type": "url", "description": "リポジトリURL", "required": true },
|
|
1271
|
+
{ "name": "preview_url", "type": "url", "description": "プレビューURL" }
|
|
1232
1272
|
]
|
|
1233
1273
|
}
|
|
1234
1274
|
}
|
|
@@ -1243,7 +1283,7 @@ POST `/api/minion/dag-workflows/:id/publish` (body なし):
|
|
|
1243
1283
|
"contract": {
|
|
1244
1284
|
"description": "プロトタイプ成果物",
|
|
1245
1285
|
"fields": [
|
|
1246
|
-
{ "
|
|
1286
|
+
{ "name": "git_url", "type": "url", "description": "リポジトリURL", "required": true }
|
|
1247
1287
|
]
|
|
1248
1288
|
}
|
|
1249
1289
|
}
|
|
@@ -1490,6 +1530,7 @@ Body:
|
|
|
1490
1530
|
- `status: completed` で `requires_review` なノードはサーバ側で `review_status=review_pending` になりカスケードは停止(レビュー承認まで下流は生成されない)
|
|
1491
1531
|
- `status: failed` でもカスケードは走る(fan-out join が `on_failure=ignore|collect` で集約できるため)
|
|
1492
1532
|
- `output_data` は下流ノードの `input_data` に伝播する。**スキル実行時はスキル本文の「## Output Data」セクションの JSON コードブロックを抽出して `output_data` に載せる規約**(ミニオンの `dag-node-executor` がこの抽出を行う)
|
|
1533
|
+
- **Contract runtime validation**: 報告時に outgoing edge の `contract` で `output_data` が検証される。違反があれば HQ はノードを `failed` に書き換え、`contract_violations` カラムに構造化済み違反を保存し、`output_summary` に詳細を追記する。スキル・transform 共通。contract が貼られていない edge の先については検証スキップ
|
|
1493
1534
|
|
|
1494
1535
|
Response:
|
|
1495
1536
|
```json
|
|
@@ -1551,7 +1592,7 @@ DAG ワークフローの graph は以下の構造で保存される(`dag_work
|
|
|
1551
1592
|
| `start` | エントリポイント | ❌ (内部) |
|
|
1552
1593
|
| `end` | 終端 | ❌ (内部) |
|
|
1553
1594
|
| `skill` | スキル実行。`skill_version_id` と `assigned_role` が必須 | ✅ |
|
|
1554
|
-
| `transform` |
|
|
1595
|
+
| `transform` | contract 同士のブリッジ。I/O 型は incoming/outgoing edge の `contract` から自動導出、出力は HQ が contract validate。`transform_instruction` は optional hint | ✅ |
|
|
1555
1596
|
| `review` | レビューゲート。`approved` / `revision_requested` で分岐 | ❌ (内部) |
|
|
1556
1597
|
| `fan_out` | 配列入力をテンプレートsub-graphに展開して並列実行。子が全て settle すると自ノードが completed に遷移 | ❌ (内部) |
|
|
1557
1598
|
| `join` | N本の上流エッジを待ち合わせる汎用バリア。fan_out とは独立 | ❌ (内部) |
|
package/docs/task-guides.md
CHANGED
|
@@ -455,17 +455,64 @@ fan_out_source = ".items"
|
|
|
455
455
|
|
|
456
456
|
ミニオンから見ると、fan-out 内の skill/transform ノードも通常どおり `pending-nodes` に返ってくる(`scope_path` が空でない点だけが違い)。
|
|
457
457
|
|
|
458
|
-
### Transform
|
|
458
|
+
### Transform ノード(contract 駆動)
|
|
459
459
|
|
|
460
|
-
Transform ノードは LLM
|
|
460
|
+
Transform ノードは **contract 同士のブリッジ** となる軽量ノード。入出力の shape は **incoming/outgoing edge に貼った contract から自動導出** され、LLM はその Output Contract に適合する JSON を生成する。HQ は `node-complete` 報告時に output を contract で検証し、違反ならノードを failed にする。
|
|
461
461
|
|
|
462
|
+
**静的バリデーション要件:**
|
|
463
|
+
|
|
464
|
+
- incoming edge がちょうど 1 本で、**必ず contract を持つ**
|
|
465
|
+
- outgoing edge がちょうど 1 本で、**必ず contract を持つ**
|
|
466
|
+
- `transform_instruction` は **optional**(contract だけで意図が明確なら空欄で OK)
|
|
467
|
+
- `assigned_role` は必須
|
|
468
|
+
|
|
469
|
+
**典型用途: 「スキルの Markdown レポート出力 → 下流 contract の shape に整形」**
|
|
470
|
+
|
|
471
|
+
```
|
|
472
|
+
incoming edge contract: compe-search-result (items: array<compe-item>, total_count, search_criteria)
|
|
473
|
+
outgoing edge contract: selected-items (items: array<compe-item>)
|
|
474
|
+
|
|
475
|
+
transform_instruction (optional): "上位5件のみを items に残してください"
|
|
476
|
+
input_data (upstream skill output): { "_raw": "# 検索レポート\n..." }
|
|
477
|
+
↓ (LLM は Output Contract を見てプロンプト通りに整形)
|
|
478
|
+
output_data: { "items": [ {...}, {...}, {...}, {...}, {...} ] }
|
|
479
|
+
↓ HQ が contract で検証、OK なら cascade 続行
|
|
462
480
|
```
|
|
463
|
-
|
|
481
|
+
|
|
482
|
+
**典型用途: 配列フィルタ**
|
|
483
|
+
|
|
484
|
+
```
|
|
485
|
+
transform_instruction: "items 配列から title が 'Item B' のエントリを除外"
|
|
464
486
|
input_data: { "items": [{ "title": "Item A" }, { "title": "Item B" }, { "title": "Item C" }] }
|
|
465
|
-
↓
|
|
487
|
+
↓
|
|
466
488
|
output_data: { "items": [{ "title": "Item A" }, { "title": "Item C" }] }
|
|
467
489
|
```
|
|
468
490
|
|
|
491
|
+
### DAG 構築時のノード選定フロー
|
|
492
|
+
|
|
493
|
+
スキルは汎用的で再利用可能な資産であり、ワークフロー固有の contract に合わせて SKILL.md を改修することは原則行わない。代わりに以下のフローで判断する:
|
|
494
|
+
|
|
495
|
+
1. スキルノードの outgoing edge に contract を貼る
|
|
496
|
+
2. そのスキルが contract に沿った `## Output Data` JSON ブロックを出力できるか確認
|
|
497
|
+
- **Yes**: そのまま接続。runtime validation が成功すれば cascade 続行
|
|
498
|
+
- **No**: スキルと下流ノードの間に **transform ノードを挟む**。 transform の outgoing edge に下流向け contract を貼り、incoming edge はスキルからの「実質 `_raw` だけ」の契約にする(あるいは contract を定義しない中間エッジとして置く構成)
|
|
499
|
+
3. fan_out ノードの前では特に注意: incoming edge contract の `fan_out_source` が指すフィールドが `type='array'` で宣言されている必要がある(そうでなければ validator がエラーを出す)
|
|
500
|
+
|
|
501
|
+
### 契約違反時の挙動
|
|
502
|
+
|
|
503
|
+
`node-complete` で outgoing edge の contract に `output_data` が適合しない場合:
|
|
504
|
+
|
|
505
|
+
- ノードは `status='failed'`, `outcome='failure'` として記録される
|
|
506
|
+
- `contract_violations` カラムに違反詳細(path / expected / actual / message)が JSON で保存される
|
|
507
|
+
- `output_summary` の末尾にも人間可読な違反メッセージが追記される
|
|
508
|
+
- 下流の join / fan-out は `on_failure` policy に従って進む(`ignore` / `collect` / `fail_all`)
|
|
509
|
+
|
|
510
|
+
**よくある違反:**
|
|
511
|
+
|
|
512
|
+
- skill が `## Output Data` セクションを出力しなかった → `{ _raw: "..." }` フォールバックとなり、required field がすべて未定義で failed
|
|
513
|
+
- skill 出力の型が contract と違う(`string` が required だが `number` が返った等)
|
|
514
|
+
- transform の出力 JSON が broken(JSON.parse 失敗→`_raw` → 同じく required 欠落)
|
|
515
|
+
|
|
469
516
|
### Review ノードとリビジョン
|
|
470
517
|
|
|
471
518
|
Review ノードはレビューゲート。`review_status=review_pending` で下流カスケードが停止する。レビュアーが:
|
package/linux/routes/chat.js
CHANGED
|
@@ -433,7 +433,7 @@ async function buildContextPrefix(message, context, sessionId, workspaceId) {
|
|
|
433
433
|
` hq fetch dag-workflow ${context.dagWorkflowId}`,
|
|
434
434
|
`プロジェクトコンテキスト:`,
|
|
435
435
|
` hq fetch project-context ${context.projectId}`,
|
|
436
|
-
`PMロールの場合、ノード/エッジ操作API
|
|
436
|
+
`PMロールの場合、ノード/エッジ操作APIでインクリメンタルに編集してください(**強く推奨**、全文PUTは型取り違えが起きやすいためエラーになりがち):`,
|
|
437
437
|
` hq dag add-node ${context.dagWorkflowId} <body.json> # ノード追加`,
|
|
438
438
|
` hq dag update-node ${context.dagWorkflowId} <node-id> <body.json> # ノード更新`,
|
|
439
439
|
` hq dag remove-node ${context.dagWorkflowId} <node-id> # ノード削除`,
|
|
@@ -441,7 +441,9 @@ async function buildContextPrefix(message, context, sessionId, workspaceId) {
|
|
|
441
441
|
` hq dag remove-edge ${context.dagWorkflowId} <edge-id> # エッジ削除`,
|
|
442
442
|
` hq dag validate ${context.dagWorkflowId} # ドラフト検証(公開せず)`,
|
|
443
443
|
` hq publish dag-workflow ${context.dagWorkflowId} # 公開`,
|
|
444
|
-
`
|
|
444
|
+
`Contract編集時の重要な規則: edge.contract は単一Contract名(string)のみ、配列不可。List<X> は type:"array" + items:"X" で表現。**contract.fields[] の各要素は { "name": "...", "type": "...", ... } の形式で、"key" ではなく "name" を使うこと**(JSON Schema等の慣習に引きずられないように)。詳細は ~/.minion/docs/api-reference.md の「Contracts API」参照。`,
|
|
445
|
+
`Contract はランタイムで強制される型: node-complete 時に outgoing edge の contract で output_data が検証され、違反はノード failed 扱い。transform は contract 同士のブリッジ(I/O 型は edge の contract から自動導出、transform_instruction は optional hint)。スキルが contract に沿った ## Output Data を出せない場合はスキルと下流の間に transform を挟んで整形すること。`,
|
|
446
|
+
`graph JSON 全文PUTは非推奨: hq put dag-workflow ${context.dagWorkflowId} <body.json>`,
|
|
445
447
|
`新規作成は: hq create dag-workflow <body.json>`,
|
|
446
448
|
`プロジェクト内の DAG ワークフロー一覧: hq list dag-workflows ${context.projectId}`,
|
|
447
449
|
`DAG の構造(nodes/edges/node types/scope_path 等)や実行フローの詳細は ~/.minion/docs/api-reference.md の「DAG Workflows」セクション、および ~/.minion/docs/task-guides.md の「DAG ワークフロー」セクションを参照してください。`,
|
package/linux/routine-runner.js
CHANGED
|
@@ -128,10 +128,22 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
128
128
|
throw new Error('No LLM configured. Set a Primary plugin via /api/llm/config or LLM_COMMAND in minion.env')
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
// Create tmux session
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
131
|
+
// Create the tmux session as an empty shell first, then configure
|
|
132
|
+
// remain-on-exit, then inject the command via send-keys. This avoids
|
|
133
|
+
// the race where a fast-failing LLM command tears down the session
|
|
134
|
+
// before set-option can run (see workflow-runner.js for the full
|
|
135
|
+
// explanation — the same fix applies here).
|
|
136
|
+
//
|
|
137
|
+
// Per-execution identifiers are passed via -e flags so the session
|
|
138
|
+
// shell inherits them; send-keys runs inside that shell.
|
|
139
|
+
const execScript = path.join(os.tmpdir(), `minion-routine-exec-${sessionName}.sh`)
|
|
140
|
+
await fs.writeFile(
|
|
141
|
+
execScript,
|
|
142
|
+
`#!/bin/bash\n${llmCommand}\necho $? > ${exitCodeFile}\n`,
|
|
143
|
+
'utf-8',
|
|
144
|
+
)
|
|
145
|
+
await execAsync(`chmod +x "${execScript}"`)
|
|
146
|
+
|
|
135
147
|
const tmuxCommand = [
|
|
136
148
|
'tmux new-session -d',
|
|
137
149
|
`-s "${sessionName}"`,
|
|
@@ -139,7 +151,6 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
139
151
|
`-e "MINION_EXECUTION_ID=${executionId}"`,
|
|
140
152
|
`-e "MINION_ROUTINE_ID=${routine.id}"`,
|
|
141
153
|
`-e "MINION_ROUTINE_NAME=${routine.name.replace(/"/g, '\\"')}"`,
|
|
142
|
-
`"${llmCommand}; echo $? > ${exitCodeFile}"`,
|
|
143
154
|
].join(' ')
|
|
144
155
|
|
|
145
156
|
await execAsync(tmuxCommand, { cwd: homeDir })
|
|
@@ -155,6 +166,12 @@ async function executeRoutineSession(routine, executionId, skillNames) {
|
|
|
155
166
|
console.error(`[RoutineRunner] Failed to set up pipe-pane: ${err.message}`)
|
|
156
167
|
}
|
|
157
168
|
|
|
169
|
+
// Now that remain-on-exit and pipe-pane are in place, inject the
|
|
170
|
+
// actual command.
|
|
171
|
+
await execAsync(
|
|
172
|
+
`tmux send-keys -t "${sessionName}" "bash ${execScript}" Enter`,
|
|
173
|
+
)
|
|
174
|
+
|
|
158
175
|
console.log(`[RoutineRunner] Started tmux session: ${sessionName}`)
|
|
159
176
|
|
|
160
177
|
// Wait for session to complete (poll for exit code file)
|
package/linux/workflow-runner.js
CHANGED
|
@@ -106,18 +106,20 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
106
106
|
contractContext += `### ${ic.contract_name}\n${ic.contract.description || ''}\n`
|
|
107
107
|
contractContext += '| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n'
|
|
108
108
|
for (const f of ic.contract.fields || []) {
|
|
109
|
-
|
|
109
|
+
const typeDisplay = f.type === 'array' && f.items ? `array<${f.items}>` : f.type
|
|
110
|
+
contractContext += `| ${f.name} | ${typeDisplay} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
contractContext += '\n'
|
|
113
114
|
}
|
|
114
115
|
if (options.dagOutputContracts && options.dagOutputContracts.length > 0) {
|
|
115
|
-
contractContext += '## Output Contract\
|
|
116
|
+
contractContext += '## Output Contract (REQUIRED)\nEnd your execution report with a "## Output Data" section containing a single `json` code block whose content conforms **exactly** to the contract(s) below. HQ validates this JSON on completion; any missing required field or type mismatch fails the node.\n\nIf the skill\'s natural output does not match this shape, add a transform node upstream instead of reshaping inside the skill.\n\n'
|
|
116
117
|
for (const oc of options.dagOutputContracts) {
|
|
117
118
|
contractContext += `### ${oc.contract_name}\n${oc.contract.description || ''}\n`
|
|
118
119
|
contractContext += '| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n'
|
|
119
120
|
for (const f of oc.contract.fields || []) {
|
|
120
|
-
|
|
121
|
+
const typeDisplay = f.type === 'array' && f.items ? `array<${f.items}>` : f.type
|
|
122
|
+
contractContext += `| ${f.name} | ${typeDisplay} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
contractContext += '\n'
|
|
@@ -176,17 +178,34 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
176
178
|
throw new Error('No LLM configured. Set a Primary plugin via /api/llm/config or LLM_COMMAND in minion.env')
|
|
177
179
|
}
|
|
178
180
|
|
|
179
|
-
// Create tmux session
|
|
181
|
+
// Create the tmux session as an empty shell first, then configure it,
|
|
182
|
+
// then inject the command via send-keys.
|
|
183
|
+
//
|
|
184
|
+
// Why two steps instead of `tmux new-session -d <cmd>`: if the LLM
|
|
185
|
+
// command exits very quickly (auth failure, missing binary, etc.) the
|
|
186
|
+
// session dies before we can call `set-option remain-on-exit on`, and
|
|
187
|
+
// the subsequent set-option/pipe-pane calls fail with "no such
|
|
188
|
+
// window". DAG transform nodes exposed this race because ephemeral
|
|
189
|
+
// skills start immediately and some failure modes (invalid prompt,
|
|
190
|
+
// missing LLM config) return instantly.
|
|
191
|
+
//
|
|
192
|
+
// Writing the invocation to a script file also insulates the command
|
|
193
|
+
// from shell-escaping edge cases when send-keys types it into the
|
|
194
|
+
// session.
|
|
195
|
+
const execScript = path.join(os.tmpdir(), `minion-workflow-exec-${sessionName}.sh`)
|
|
196
|
+
await fs.writeFile(
|
|
197
|
+
execScript,
|
|
198
|
+
`#!/bin/bash\n${llmCommand}\necho $? > ${exitCodeFile}\n`,
|
|
199
|
+
'utf-8',
|
|
200
|
+
)
|
|
201
|
+
await execAsync(`chmod +x "${execScript}"`)
|
|
202
|
+
|
|
180
203
|
// PATH, HOME, DISPLAY, and minion secrets are already set in
|
|
181
204
|
// process.env at server startup, so child processes inherit them automatically.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
`"${llmCommand}; echo $? > ${exitCodeFile}"`,
|
|
187
|
-
].join(' ')
|
|
188
|
-
|
|
189
|
-
await execAsync(tmuxCommand, { cwd: homeDir })
|
|
205
|
+
await execAsync(
|
|
206
|
+
`tmux new-session -d -s "${sessionName}" -x 200 -y 50`,
|
|
207
|
+
{ cwd: homeDir },
|
|
208
|
+
)
|
|
190
209
|
|
|
191
210
|
// Keep session alive after command completes (for debugging via terminal mirror)
|
|
192
211
|
await execAsync(`tmux set-option -t "${sessionName}" remain-on-exit on`)
|
|
@@ -201,6 +220,13 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
201
220
|
// Continue execution even if pipe-pane fails
|
|
202
221
|
}
|
|
203
222
|
|
|
223
|
+
// Now that remain-on-exit and pipe-pane are in place, inject the
|
|
224
|
+
// actual command. A fast-failing command will still be captured in the
|
|
225
|
+
// log and the exit code file will be written.
|
|
226
|
+
await execAsync(
|
|
227
|
+
`tmux send-keys -t "${sessionName}" "bash ${execScript}" Enter`,
|
|
228
|
+
)
|
|
229
|
+
|
|
204
230
|
console.log(`[WorkflowRunner] Started tmux session: ${sessionName}`)
|
|
205
231
|
|
|
206
232
|
// Wait for session to complete (poll for exit code file)
|
package/package.json
CHANGED
package/rules/core.md
CHANGED
|
@@ -28,6 +28,13 @@ Minion
|
|
|
28
28
|
|
|
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
|
+
- **PMロールで編集する場合の重要な規則**:
|
|
32
|
+
- ノード/エッジ/contract の追加・更新は**個別API** (`/nodes` `/edges` `/contracts`) を使うこと。`PUT /dag-workflows/:id` による graph 全文PUTは型の取り違えが起きやすく、バリデーションエラーで 400 が返る
|
|
33
|
+
- `edge.contract` は**単一のContract名(string)**のみ。配列は不可。複数の型構造を束ねたい場合は、それらを内包する複合Contractを1つ定義する
|
|
34
|
+
- Contract内で `List<別Contract>` を表現するには `type: 'array'` + `items: "別Contract名"` を使う(詳細は `~/.minion/docs/api-reference.md` の「Contracts API」を参照)
|
|
35
|
+
- **Contract はランタイムで強制される型定義**。`node-complete` 報告時に outgoing edge の contract で `output_data` が検証され、違反はノード `failed` 扱い。スキルが contract に沿った `## Output Data` を出せない場合は **transform ノードをスキルと下流の間に挟んで整形**すること。スキル側の SKILL.md を各ワークフロー専用に改修するのは原則 NG(スキルは汎用資産)
|
|
36
|
+
- **transform ノードの I/O 型は edge の contract から自動導出**。incoming edge と outgoing edge にそれぞれ contract を必ず貼ること。`transform_instruction` は contract だけで意図が伝わらない場合の補足ヒント(任意)
|
|
37
|
+
- **fan_out の incoming edge に contract を貼る場合**、`fan_out_source` が指すフィールドが contract 内に `type='array'` として宣言されている必要がある(静的検証で弾かれる)
|
|
31
38
|
- **Routine**: ミニオンスコープ。ミニオンローカルの定期タスク。
|
|
32
39
|
- **Workspace**: ミニオンは複数のワークスペースに所属でき、スキルやプロジェクトはワークスペース単位でスコープされる。チャットセッションもワークスペース別に分離される。所属ワークスペースはハートビートで自動同期され、`hq list workspaces` で確認できる。
|
|
33
40
|
- ミニオンは複数プロジェクトに `pm`、`engineer`、`accountant` として参加できる。
|
package/win/routes/chat.js
CHANGED
|
@@ -494,7 +494,7 @@ async function buildContextPrefix(message, context, sessionId, workspaceId) {
|
|
|
494
494
|
` hq fetch dag-workflow ${context.dagWorkflowId}`,
|
|
495
495
|
`プロジェクトコンテキスト:`,
|
|
496
496
|
` hq fetch project-context ${context.projectId}`,
|
|
497
|
-
`PMロールの場合、ノード/エッジ操作API
|
|
497
|
+
`PMロールの場合、ノード/エッジ操作APIでインクリメンタルに編集してください(**強く推奨**、全文PUTは型取り違えが起きやすいためエラーになりがち):`,
|
|
498
498
|
` hq dag add-node ${context.dagWorkflowId} <body.json> # ノード追加`,
|
|
499
499
|
` hq dag update-node ${context.dagWorkflowId} <node-id> <body.json> # ノード更新`,
|
|
500
500
|
` hq dag remove-node ${context.dagWorkflowId} <node-id> # ノード削除`,
|
|
@@ -502,7 +502,9 @@ async function buildContextPrefix(message, context, sessionId, workspaceId) {
|
|
|
502
502
|
` hq dag remove-edge ${context.dagWorkflowId} <edge-id> # エッジ削除`,
|
|
503
503
|
` hq dag validate ${context.dagWorkflowId} # ドラフト検証(公開せず)`,
|
|
504
504
|
` hq publish dag-workflow ${context.dagWorkflowId} # 公開`,
|
|
505
|
-
`
|
|
505
|
+
`Contract編集時の重要な規則: edge.contract は単一Contract名(string)のみ、配列不可。List<X> は type:"array" + items:"X" で表現。**contract.fields[] の各要素は { "name": "...", "type": "...", ... } の形式で、"key" ではなく "name" を使うこと**(JSON Schema等の慣習に引きずられないように)。詳細は ~/.minion/docs/api-reference.md の「Contracts API」参照。`,
|
|
506
|
+
`Contract はランタイムで強制される型: node-complete 時に outgoing edge の contract で output_data が検証され、違反はノード failed 扱い。transform は contract 同士のブリッジ(I/O 型は edge の contract から自動導出、transform_instruction は optional hint)。スキルが contract に沿った ## Output Data を出せない場合はスキルと下流の間に transform を挟んで整形すること。`,
|
|
507
|
+
`graph JSON 全文PUTは非推奨: hq put dag-workflow ${context.dagWorkflowId} <body.json>`,
|
|
506
508
|
`新規作成は: hq create dag-workflow <body.json>`,
|
|
507
509
|
`プロジェクト内の DAG ワークフロー一覧: hq list dag-workflows ${context.projectId}`,
|
|
508
510
|
`DAG の構造(nodes/edges/node types/scope_path 等)や実行フローの詳細は ~/.minion/docs/api-reference.md の「DAG Workflows」セクション、および ~/.minion/docs/task-guides.md の「DAG ワークフロー」セクションを参照してください。`,
|
package/win/workflow-runner.js
CHANGED
|
@@ -113,18 +113,20 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
|
|
|
113
113
|
contractContext += `### ${ic.contract_name}\n${ic.contract.description || ''}\n`
|
|
114
114
|
contractContext += '| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n'
|
|
115
115
|
for (const f of ic.contract.fields || []) {
|
|
116
|
-
|
|
116
|
+
const typeDisplay = f.type === 'array' && f.items ? `array<${f.items}>` : f.type
|
|
117
|
+
contractContext += `| ${f.name} | ${typeDisplay} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
contractContext += '\n'
|
|
120
121
|
}
|
|
121
122
|
if (options.dagOutputContracts && options.dagOutputContracts.length > 0) {
|
|
122
|
-
contractContext += '## Output Contract\
|
|
123
|
+
contractContext += '## Output Contract (REQUIRED)\nEnd your execution report with a "## Output Data" section containing a single `json` code block whose content conforms **exactly** to the contract(s) below. HQ validates this JSON on completion; any missing required field or type mismatch fails the node.\n\nIf the skill\'s natural output does not match this shape, add a transform node upstream instead of reshaping inside the skill.\n\n'
|
|
123
124
|
for (const oc of options.dagOutputContracts) {
|
|
124
125
|
contractContext += `### ${oc.contract_name}\n${oc.contract.description || ''}\n`
|
|
125
126
|
contractContext += '| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n'
|
|
126
127
|
for (const f of oc.contract.fields || []) {
|
|
127
|
-
|
|
128
|
+
const typeDisplay = f.type === 'array' && f.items ? `array<${f.items}>` : f.type
|
|
129
|
+
contractContext += `| ${f.name} | ${typeDisplay} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
contractContext += '\n'
|