@geekbeer/minion 3.30.0 → 3.34.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.
@@ -385,7 +385,8 @@ function buildTransformSkillContent(instruction, inputData, outputContracts) {
385
385
  lines.push('| Field | Type | Required | Description |')
386
386
  lines.push('|-------|------|----------|-------------|')
387
387
  for (const f of oc.contract.fields || []) {
388
- lines.push(`| ${f.key} | ${f.type} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |`)
388
+ const typeDisplay = f.type === 'array' && f.items ? `array<${f.items}>` : f.type
389
+ lines.push(`| ${f.key} | ${typeDisplay} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |`)
389
390
  }
390
391
  }
391
392
  }
@@ -35,7 +35,7 @@ function streamClaude(input, onEvent, ctx = {}) {
35
35
  const child = spawn(bin, args, {
36
36
  cwd: config.HOME_DIR,
37
37
  stdio: ['pipe', 'pipe', 'pipe'],
38
- timeout: input.timeoutMs || 600000,
38
+ timeout: input.timeoutMs || 3600000, // 60 min default
39
39
  env: {
40
40
  ...process.env,
41
41
  HOME: config.HOME_DIR,
@@ -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` でフル検証
@@ -1206,15 +1211,48 @@ POST `/api/minion/dag-workflows/:id/publish` (body なし):
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
 
1213
1219
  **注意:**
1214
1220
  - エッジに設定する `contract` は `graph.contracts` に存在する名前でなければならない(存在しない名前を指定すると 400 エラー)
1221
+ - **エッジが参照できる Contract は 1 つのみ**。`edge.contract` に配列を渡すことは不可(400エラー)。複数の型構造を束ねたい場合は、それらを束ねた複合Contractを1つ定義してから参照すること
1215
1222
  - contract を削除すると、参照しているエッジの `contract` フィールドが自動でクリアされる(DELETE / PUT 共通)
1216
1223
  - `validate` エンドポイントはダングリング参照(存在しない contract への参照)をエラーとして報告する
1217
1224
 
1225
+ **Contract内で List<別Contract> を表現する方法 (items):**
1226
+
1227
+ `type: 'array'` のフィールドに `items` プロパティを設定すると、配列要素の型を別Contractで記述できる。`items` の値は `graph.contracts` 内のContract名。
1228
+
1229
+ ```json
1230
+ {
1231
+ "contracts": {
1232
+ "Article": {
1233
+ "description": "個別の記事",
1234
+ "fields": [
1235
+ { "key": "title", "type": "string", "required": true, "description": "タイトル" },
1236
+ { "key": "url", "type": "url", "description": "記事URL" }
1237
+ ]
1238
+ },
1239
+ "NewsCollection": {
1240
+ "description": "収集されたニュース全体",
1241
+ "fields": [
1242
+ { "key": "articles", "type": "array", "items": "Article", "required": true, "description": "記事リスト" },
1243
+ { "key": "collected_at", "type": "string", "required": true, "description": "収集日時" },
1244
+ { "key": "count", "type": "number", "required": true, "description": "件数" }
1245
+ ]
1246
+ }
1247
+ },
1248
+ "edges": [
1249
+ { "id": "edge_1", "source": "a", "target": "b", "contract": "NewsCollection" }
1250
+ ]
1251
+ }
1252
+ ```
1253
+
1254
+ この構造で「エッジは単一Contract (`NewsCollection`) を参照し、その内部で `articles` フィールドが `Article[]` 型」という意味になる。
1255
+
1218
1256
  ##### GET `/api/minion/dag-workflows/:id/contracts` — 全contracts取得
1219
1257
 
1220
1258
  レスポンス: `{ "contracts": { "name": { "description": "...", "fields": [...] }, ... } }`
@@ -1096,13 +1096,28 @@ SUPEOF
1096
1096
  HOSTNAME_VAL=$(hostname 2>/dev/null || echo "")
1097
1097
  LAN_IP=$(detect_lan_ip)
1098
1098
 
1099
+ # Collect machine specs
1100
+ local CPU_MODEL CPU_CORES MEMORY_MB DISK_GB OS_INFO ARCH_INFO
1101
+ CPU_MODEL=$(grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2 | sed 's/^ //' || echo "unknown")
1102
+ CPU_CORES=$(nproc 2>/dev/null || grep -c '^processor' /proc/cpuinfo 2>/dev/null || echo "0")
1103
+ MEMORY_MB=$(awk '/MemTotal/ {printf "%.0f", $2/1024}' /proc/meminfo 2>/dev/null || echo "0")
1104
+ DISK_GB=$(df -BG / 2>/dev/null | awk 'NR==2 {gsub("G",""); print $2}' || echo "0")
1105
+ OS_INFO=$(. /etc/os-release 2>/dev/null && echo "${PRETTY_NAME}" || uname -s 2>/dev/null || echo "unknown")
1106
+ ARCH_INFO=$(uname -m 2>/dev/null || echo "unknown")
1107
+
1108
+ local MACHINE_SPECS
1109
+ MACHINE_SPECS=$(cat <<SPECEOF
1110
+ {"cpu_model":"${CPU_MODEL}","cpu_cores":${CPU_CORES},"memory_mb":${MEMORY_MB},"disk_gb":${DISK_GB},"os":"${OS_INFO}","arch":"${ARCH_INFO}"}
1111
+ SPECEOF
1112
+ )
1113
+
1099
1114
  local BODY
1100
1115
  if [ -f /.dockerenv ]; then
1101
- BODY="{\"internal_ip_address\":\"${HOSTNAME_VAL}\"}"
1116
+ BODY="{\"internal_ip_address\":\"${HOSTNAME_VAL}\",\"machine_specs\":${MACHINE_SPECS}}"
1102
1117
  elif [ -n "$LAN_IP" ]; then
1103
- BODY="{\"internal_ip_address\":\"${LAN_IP}\",\"ip_address\":\"${LAN_IP}\"}"
1118
+ BODY="{\"internal_ip_address\":\"${LAN_IP}\",\"ip_address\":\"${LAN_IP}\",\"machine_specs\":${MACHINE_SPECS}}"
1104
1119
  else
1105
- BODY="{\"internal_ip_address\":\"${HOSTNAME_VAL}\"}"
1120
+ BODY="{\"internal_ip_address\":\"${HOSTNAME_VAL}\",\"machine_specs\":${MACHINE_SPECS}}"
1106
1121
  fi
1107
1122
 
1108
1123
  NOTIFY_RESPONSE=$(curl -sfL -X POST "${HQ_URL}/api/minion/setup-complete" \
@@ -353,6 +353,36 @@ async function buildContextPrefix(message, context, sessionId, workspaceId) {
353
353
  )
354
354
  }
355
355
 
356
+ // Task planning guidance — instruct Claude to create TODOs before starting work
357
+ if (!sessionId) {
358
+ const port = require('../../core/config').config.AGENT_PORT
359
+ parts.push(
360
+ '[作業手順 — 必ず守ること]',
361
+ '依頼された作業に着手する**前に**、以下の手順を踏むこと:',
362
+ '',
363
+ '1. 依頼内容を分析し、必要なステップをToDoとして登録する',
364
+ '2. 各ステップを完了するたびに、ToDoのstatusを `done` に更新する',
365
+ '3. すべてのToDoが完了したら、ユーザーに完了報告する',
366
+ '',
367
+ 'ToDo APIの使い方:',
368
+ '```bash',
369
+ '# ToDo作成(session_idは後でセッションIDが判明してから設定)',
370
+ `curl -X POST http://localhost:${port}/api/todos \\`,
371
+ ' -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \\',
372
+ ' -d \'{"title": "ステップの説明", "session_id": "SESSION_ID", "priority": "normal"}\'',
373
+ '',
374
+ '# ToDo完了',
375
+ `curl -X PUT http://localhost:${port}/api/todos/{id} \\`,
376
+ ' -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \\',
377
+ ' -d \'{"status": "done"}\'',
378
+ '```',
379
+ '',
380
+ 'ToDoを先に作成する理由: チャットのコンテキストが圧縮されても、未完了のToDoは次のターンで自動的に再表示されます。',
381
+ 'これにより、長時間の作業でも途中で何をすべきか見失うことがありません。',
382
+ ''
383
+ )
384
+ }
385
+
356
386
  if (context) {
357
387
  switch (context.type) {
358
388
  case 'skill':
@@ -403,7 +433,7 @@ async function buildContextPrefix(message, context, sessionId, workspaceId) {
403
433
  ` hq fetch dag-workflow ${context.dagWorkflowId}`,
404
434
  `プロジェクトコンテキスト:`,
405
435
  ` hq fetch project-context ${context.projectId}`,
406
- `PMロールの場合、ノード/エッジ操作APIでインクリメンタルに編集できます(推奨):`,
436
+ `PMロールの場合、ノード/エッジ操作APIでインクリメンタルに編集してください(**強く推奨**、全文PUTは型取り違えが起きやすいためエラーになりがち):`,
407
437
  ` hq dag add-node ${context.dagWorkflowId} <body.json> # ノード追加`,
408
438
  ` hq dag update-node ${context.dagWorkflowId} <node-id> <body.json> # ノード更新`,
409
439
  ` hq dag remove-node ${context.dagWorkflowId} <node-id> # ノード削除`,
@@ -411,7 +441,8 @@ async function buildContextPrefix(message, context, sessionId, workspaceId) {
411
441
  ` hq dag remove-edge ${context.dagWorkflowId} <edge-id> # エッジ削除`,
412
442
  ` hq dag validate ${context.dagWorkflowId} # ドラフト検証(公開せず)`,
413
443
  ` hq publish dag-workflow ${context.dagWorkflowId} # 公開`,
414
- `graph JSON 全文操作も可能: hq put dag-workflow ${context.dagWorkflowId} <body.json>`,
444
+ `Contract編集時の重要な規則: edge.contract は単一Contract名(string)のみ、配列不可。List<X> type:"array" + items:"X" で表現。詳細は ~/.minion/docs/api-reference.md の「Contracts API」参照。`,
445
+ `graph JSON 全文PUTは非推奨: hq put dag-workflow ${context.dagWorkflowId} <body.json>`,
415
446
  `新規作成は: hq create dag-workflow <body.json>`,
416
447
  `プロジェクト内の DAG ワークフロー一覧: hq list dag-workflows ${context.projectId}`,
417
448
  `DAG の構造(nodes/edges/node types/scope_path 等)や実行フローの詳細は ~/.minion/docs/api-reference.md の「DAG Workflows」セクション、および ~/.minion/docs/task-guides.md の「DAG ワークフロー」セクションを参照してください。`,
@@ -568,7 +599,7 @@ function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, original
568
599
  const child = spawn(binary, args, {
569
600
  cwd: config.HOME_DIR,
570
601
  stdio: ['pipe', 'pipe', 'pipe'],
571
- timeout: 600000, // 10 min
602
+ timeout: 3600000, // 60 min — allow long-running tasks to complete
572
603
  })
573
604
 
574
605
  // Track active child process for abort
@@ -106,7 +106,8 @@ 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
- contractContext += `| ${f.key} | ${f.type} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
109
+ const typeDisplay = f.type === 'array' && f.items ? `array<${f.items}>` : f.type
110
+ contractContext += `| ${f.key} | ${typeDisplay} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
110
111
  }
111
112
  }
112
113
  contractContext += '\n'
@@ -117,7 +118,8 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
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
- contractContext += `| ${f.key} | ${f.type} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
121
+ const typeDisplay = f.type === 'array' && f.items ? `array<${f.items}>` : f.type
122
+ contractContext += `| ${f.key} | ${typeDisplay} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
121
123
  }
122
124
  }
123
125
  contractContext += '\n'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "3.30.0",
3
+ "version": "3.34.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
@@ -28,6 +28,10 @@ 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」を参照)
31
35
  - **Routine**: ミニオンスコープ。ミニオンローカルの定期タスク。
32
36
  - **Workspace**: ミニオンは複数のワークスペースに所属でき、スキルやプロジェクトはワークスペース単位でスコープされる。チャットセッションもワークスペース別に分離される。所属ワークスペースはハートビートで自動同期され、`hq list workspaces` で確認できる。
33
37
  - ミニオンは複数プロジェクトに `pm`、`engineer`、`accountant` として参加できる。
@@ -1522,7 +1522,21 @@ function Invoke-Configure {
1522
1522
  $bodyHash = @{}
1523
1523
  if ($lanIp) { $bodyHash['ip_address'] = $lanIp; $bodyHash['internal_ip_address'] = $lanIp }
1524
1524
  else { $bodyHash['internal_ip_address'] = [System.Net.Dns]::GetHostName() }
1525
- $body = $bodyHash | ConvertTo-Json
1525
+
1526
+ # Collect machine specs
1527
+ $cpuInfo = Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1
1528
+ $osInfo = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
1529
+ $diskInfo = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'" -ErrorAction SilentlyContinue
1530
+ $bodyHash['machine_specs'] = @{
1531
+ cpu_model = if ($cpuInfo) { $cpuInfo.Name } else { 'unknown' }
1532
+ cpu_cores = if ($cpuInfo) { $cpuInfo.NumberOfLogicalProcessors } else { 0 }
1533
+ memory_mb = if ($osInfo) { [math]::Round($osInfo.TotalVisibleMemorySize / 1024) } else { 0 }
1534
+ disk_gb = if ($diskInfo) { [math]::Round($diskInfo.Size / 1GB) } else { 0 }
1535
+ os = if ($osInfo) { $osInfo.Caption } else { 'Windows' }
1536
+ arch = $env:PROCESSOR_ARCHITECTURE
1537
+ }
1538
+
1539
+ $body = $bodyHash | ConvertTo-Json -Depth 3
1526
1540
  Invoke-RestMethod -Uri "$HqUrl/api/minion/setup-complete" -Method Post -Headers $headers -Body $body -ErrorAction Stop | Out-Null
1527
1541
  Write-Detail "HQ notified"
1528
1542
  }
@@ -416,6 +416,36 @@ async function buildContextPrefix(message, context, sessionId, workspaceId) {
416
416
  )
417
417
  }
418
418
 
419
+ // Task planning guidance — instruct Claude to create TODOs before starting work
420
+ if (!sessionId) {
421
+ const port = require('../../core/config').config.AGENT_PORT
422
+ parts.push(
423
+ '[作業手順 — 必ず守ること]',
424
+ '依頼された作業に着手する**前に**、以下の手順を踏むこと:',
425
+ '',
426
+ '1. 依頼内容を分析し、必要なステップをToDoとして登録する',
427
+ '2. 各ステップを完了するたびに、ToDoのstatusを `done` に更新する',
428
+ '3. すべてのToDoが完了したら、ユーザーに完了報告する',
429
+ '',
430
+ 'ToDo APIの使い方:',
431
+ '```bash',
432
+ '# ToDo作成(session_idは後でセッションIDが判明してから設定)',
433
+ `curl -X POST http://localhost:${port}/api/todos \\`,
434
+ ' -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \\',
435
+ ' -d \'{"title": "ステップの説明", "session_id": "SESSION_ID", "priority": "normal"}\'',
436
+ '',
437
+ '# ToDo完了',
438
+ `curl -X PUT http://localhost:${port}/api/todos/{id} \\`,
439
+ ' -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \\',
440
+ ' -d \'{"status": "done"}\'',
441
+ '```',
442
+ '',
443
+ 'ToDoを先に作成する理由: チャットのコンテキストが圧縮されても、未完了のToDoは次のターンで自動的に再表示されます。',
444
+ 'これにより、長時間の作業でも途中で何をすべきか見失うことがありません。',
445
+ ''
446
+ )
447
+ }
448
+
419
449
  if (context) {
420
450
  switch (context.type) {
421
451
  case 'skill':
@@ -464,7 +494,7 @@ async function buildContextPrefix(message, context, sessionId, workspaceId) {
464
494
  ` hq fetch dag-workflow ${context.dagWorkflowId}`,
465
495
  `プロジェクトコンテキスト:`,
466
496
  ` hq fetch project-context ${context.projectId}`,
467
- `PMロールの場合、ノード/エッジ操作APIでインクリメンタルに編集できます(推奨):`,
497
+ `PMロールの場合、ノード/エッジ操作APIでインクリメンタルに編集してください(**強く推奨**、全文PUTは型取り違えが起きやすいためエラーになりがち):`,
468
498
  ` hq dag add-node ${context.dagWorkflowId} <body.json> # ノード追加`,
469
499
  ` hq dag update-node ${context.dagWorkflowId} <node-id> <body.json> # ノード更新`,
470
500
  ` hq dag remove-node ${context.dagWorkflowId} <node-id> # ノード削除`,
@@ -472,7 +502,8 @@ async function buildContextPrefix(message, context, sessionId, workspaceId) {
472
502
  ` hq dag remove-edge ${context.dagWorkflowId} <edge-id> # エッジ削除`,
473
503
  ` hq dag validate ${context.dagWorkflowId} # ドラフト検証(公開せず)`,
474
504
  ` hq publish dag-workflow ${context.dagWorkflowId} # 公開`,
475
- `graph JSON 全文操作も可能: hq put dag-workflow ${context.dagWorkflowId} <body.json>`,
505
+ `Contract編集時の重要な規則: edge.contract は単一Contract名(string)のみ、配列不可。List<X> type:"array" + items:"X" で表現。詳細は ~/.minion/docs/api-reference.md の「Contracts API」参照。`,
506
+ `graph JSON 全文PUTは非推奨: hq put dag-workflow ${context.dagWorkflowId} <body.json>`,
476
507
  `新規作成は: hq create dag-workflow <body.json>`,
477
508
  `プロジェクト内の DAG ワークフロー一覧: hq list dag-workflows ${context.projectId}`,
478
509
  `DAG の構造(nodes/edges/node types/scope_path 等)や実行フローの詳細は ~/.minion/docs/api-reference.md の「DAG Workflows」セクション、および ~/.minion/docs/task-guides.md の「DAG ワークフロー」セクションを参照してください。`,
@@ -602,7 +633,7 @@ function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, original
602
633
  const child = spawn(binary, args, {
603
634
  cwd: config.HOME_DIR,
604
635
  stdio: ['pipe', 'pipe', 'pipe'],
605
- timeout: 600000,
636
+ timeout: 3600000, // 60 min — allow long-running tasks to complete
606
637
  shell: true, // Required on Windows to resolve .cmd shims (e.g. claude.cmd)
607
638
  })
608
639
 
@@ -113,7 +113,8 @@ 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
- contractContext += `| ${f.key} | ${f.type} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
116
+ const typeDisplay = f.type === 'array' && f.items ? `array<${f.items}>` : f.type
117
+ contractContext += `| ${f.key} | ${typeDisplay} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
117
118
  }
118
119
  }
119
120
  contractContext += '\n'
@@ -124,7 +125,8 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
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
- contractContext += `| ${f.key} | ${f.type} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
128
+ const typeDisplay = f.type === 'array' && f.items ? `array<${f.items}>` : f.type
129
+ contractContext += `| ${f.key} | ${typeDisplay} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
128
130
  }
129
131
  }
130
132
  contractContext += '\n'
@@ -187,7 +187,7 @@ function streamLlmResponse(res, prompt, sessionId) {
187
187
  const child = spawn(binary, args, {
188
188
  cwd: HOME_DIR,
189
189
  stdio: ['pipe', 'pipe', 'pipe'],
190
- timeout: 600000,
190
+ timeout: 3600000, // 60 min — allow long-running tasks to complete
191
191
  shell: true,
192
192
  })
193
193