@geekbeer/minion 3.29.4 → 3.32.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.
@@ -126,6 +126,8 @@ async function executeSkillNode(node) {
126
126
  input_data,
127
127
  revision_feedback,
128
128
  review_history,
129
+ input_contracts,
130
+ output_contracts,
129
131
  } = node
130
132
 
131
133
  console.log(
@@ -174,16 +176,18 @@ async function executeSkillNode(node) {
174
176
  }
175
177
  }
176
178
 
177
- // 4. Run the skill with input_data context
179
+ // 4. Run the skill with input_data and contract context
178
180
  const runPayload = {
179
181
  skill_name: skillName,
180
182
  execution_id,
181
183
  workflow_name: dag_workflow_name,
182
184
  role: assigned_role,
183
- // DAG-specific: inject input_data as context
185
+ // DAG-specific: inject input_data and contracts as context
184
186
  dag_node_id: node_id,
185
187
  dag_input_data: input_data,
186
188
  dag_node_execution_id: node_execution_id,
189
+ dag_input_contracts: input_contracts || null,
190
+ dag_output_contracts: output_contracts || null,
187
191
  }
188
192
  if (revision_feedback) {
189
193
  runPayload.revision_feedback = revision_feedback
@@ -251,6 +255,7 @@ async function executeTransformNode(node) {
251
255
  assigned_role,
252
256
  input_data,
253
257
  transform_instruction,
258
+ output_contracts,
254
259
  } = node
255
260
 
256
261
  console.log(
@@ -285,7 +290,7 @@ async function executeTransformNode(node) {
285
290
 
286
291
  // 2. Create ephemeral skill from transform_instruction.
287
292
  // Write to every active plugin's skill dir so any Primary can find it.
288
- const skillContent = buildTransformSkillContent(transform_instruction, input_data)
293
+ const skillContent = buildTransformSkillContent(transform_instruction, input_data, output_contracts)
289
294
  for (const dir of ephemeralSkillDirs) {
290
295
  await fs.mkdir(dir, { recursive: true })
291
296
  await fs.writeFile(path.join(dir, 'SKILL.md'), skillContent, 'utf-8')
@@ -353,8 +358,8 @@ async function executeTransformNode(node) {
353
358
  /**
354
359
  * Build SKILL.md content for a transform node's ephemeral skill.
355
360
  */
356
- function buildTransformSkillContent(instruction, inputData) {
357
- return [
361
+ function buildTransformSkillContent(instruction, inputData, outputContracts) {
362
+ const lines = [
358
363
  '---',
359
364
  'name: dag-transform',
360
365
  'description: DAG Transform Node',
@@ -369,12 +374,31 @@ function buildTransformSkillContent(instruction, inputData) {
369
374
  '',
370
375
  '## Transform Instruction',
371
376
  instruction,
377
+ ]
378
+
379
+ if (outputContracts && outputContracts.length > 0) {
380
+ lines.push('', '## Output Contract')
381
+ lines.push('Your output MUST conform to the following contract(s):')
382
+ for (const oc of outputContracts) {
383
+ lines.push(`### ${oc.contract_name}`)
384
+ lines.push(oc.contract.description || '')
385
+ lines.push('| Field | Type | Required | Description |')
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
+ }
390
+ }
391
+ }
392
+
393
+ lines.push(
372
394
  '',
373
395
  '## Task',
374
396
  'Apply the transform instruction to the input data above.',
375
397
  'Output the result as a JSON object in an "## Output Data" section with a json code block.',
376
398
  'Do NOT output anything other than the Output Data section.',
377
- ].join('\n')
399
+ )
400
+
401
+ return lines.join('\n')
378
402
  }
379
403
 
380
404
  /**
@@ -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,
@@ -344,6 +344,9 @@ async function skillRoutes(fastify, opts) {
344
344
  revision_feedback,
345
345
  review_history,
346
346
  dag_node_execution_id,
347
+ dag_input_data,
348
+ dag_input_contracts,
349
+ dag_output_contracts,
347
350
  } = request.body || {}
348
351
 
349
352
  if (!skill_name) {
@@ -393,6 +396,9 @@ async function skillRoutes(fastify, opts) {
393
396
  if (role) runOptions.role = role
394
397
  if (revision_feedback) runOptions.revisionFeedback = revision_feedback
395
398
  if (review_history && review_history.length > 0) runOptions.reviewHistory = review_history
399
+ if (dag_input_data) runOptions.dagInputData = dag_input_data
400
+ if (dag_input_contracts) runOptions.dagInputContracts = dag_input_contracts
401
+ if (dag_output_contracts) runOptions.dagOutputContracts = dag_output_contracts
396
402
 
397
403
  // Run asynchronously — respond immediately
398
404
  const executionPromise = (async () => {
@@ -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':
@@ -568,7 +598,7 @@ function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, original
568
598
  const child = spawn(binary, args, {
569
599
  cwd: config.HOME_DIR,
570
600
  stdio: ['pipe', 'pipe', 'pipe'],
571
- timeout: 600000, // 10 min
601
+ timeout: 3600000, // 60 min — allow long-running tasks to complete
572
602
  })
573
603
 
574
604
  // Track active child process for abort
@@ -92,7 +92,38 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
92
92
  revisionContext = `## Revision Feedback\nThe reviewer requested changes to your previous output. Address the following feedback:\n${options.revisionFeedback}\n\n`
93
93
  }
94
94
 
95
- const prompt = `${rolePrefix}${revisionContext}Run the following skills in order: ${skillCommands}.`
95
+ // Inject DAG input data context (data from upstream nodes)
96
+ let dagDataContext = ''
97
+ if (options.dagInputData && Object.keys(options.dagInputData).length > 0) {
98
+ dagDataContext = `## Input Data (from upstream nodes)\nThe following data was produced by upstream steps in this workflow. Use it as context for your task.\n\`\`\`json\n${JSON.stringify(options.dagInputData, null, 2)}\n\`\`\`\n\n`
99
+ }
100
+
101
+ // Inject DAG contract context (input/output contracts from edges)
102
+ let contractContext = ''
103
+ if (options.dagInputContracts && options.dagInputContracts.length > 0) {
104
+ contractContext += '## Input Contracts\nThe input data above conforms to the following contract(s):\n'
105
+ for (const ic of options.dagInputContracts) {
106
+ contractContext += `### ${ic.contract_name}\n${ic.contract.description || ''}\n`
107
+ contractContext += '| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n'
108
+ for (const f of ic.contract.fields || []) {
109
+ contractContext += `| ${f.key} | ${f.type} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
110
+ }
111
+ }
112
+ contractContext += '\n'
113
+ }
114
+ if (options.dagOutputContracts && options.dagOutputContracts.length > 0) {
115
+ contractContext += '## Output Contract\nYour output MUST conform to the following contract(s). Include all required fields in your execution report.\n'
116
+ for (const oc of options.dagOutputContracts) {
117
+ contractContext += `### ${oc.contract_name}\n${oc.contract.description || ''}\n`
118
+ contractContext += '| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n'
119
+ for (const f of oc.contract.fields || []) {
120
+ contractContext += `| ${f.key} | ${f.type} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
121
+ }
122
+ }
123
+ contractContext += '\n'
124
+ }
125
+
126
+ const prompt = `${rolePrefix}${revisionContext}${dagDataContext}${contractContext}Run the following skills in order: ${skillCommands}.`
96
127
 
97
128
  // Exit code file to capture CLI result
98
129
  const exitCodeFile = `/tmp/tmux-exit-${sessionName}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "3.29.4",
3
+ "version": "3.32.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": {
@@ -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':
@@ -602,7 +632,7 @@ function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, original
602
632
  const child = spawn(binary, args, {
603
633
  cwd: config.HOME_DIR,
604
634
  stdio: ['pipe', 'pipe', 'pipe'],
605
- timeout: 600000,
635
+ timeout: 3600000, // 60 min — allow long-running tasks to complete
606
636
  shell: true, // Required on Windows to resolve .cmd shims (e.g. claude.cmd)
607
637
  })
608
638
 
@@ -99,7 +99,38 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
99
99
  revisionContext = `## Revision Feedback\nThe reviewer requested changes to your previous output. Address the following feedback:\n${options.revisionFeedback}\n\n`
100
100
  }
101
101
 
102
- const prompt = `${rolePrefix}${revisionContext}Run the following skills in order: ${skillCommands}.`
102
+ // Inject DAG input data context (data from upstream nodes)
103
+ let dagDataContext = ''
104
+ if (options.dagInputData && Object.keys(options.dagInputData).length > 0) {
105
+ dagDataContext = `## Input Data (from upstream nodes)\nThe following data was produced by upstream steps in this workflow. Use it as context for your task.\n\`\`\`json\n${JSON.stringify(options.dagInputData, null, 2)}\n\`\`\`\n\n`
106
+ }
107
+
108
+ // Inject DAG contract context (input/output contracts from edges)
109
+ let contractContext = ''
110
+ if (options.dagInputContracts && options.dagInputContracts.length > 0) {
111
+ contractContext += '## Input Contracts\nThe input data above conforms to the following contract(s):\n'
112
+ for (const ic of options.dagInputContracts) {
113
+ contractContext += `### ${ic.contract_name}\n${ic.contract.description || ''}\n`
114
+ contractContext += '| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n'
115
+ for (const f of ic.contract.fields || []) {
116
+ contractContext += `| ${f.key} | ${f.type} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
117
+ }
118
+ }
119
+ contractContext += '\n'
120
+ }
121
+ if (options.dagOutputContracts && options.dagOutputContracts.length > 0) {
122
+ contractContext += '## Output Contract\nYour output MUST conform to the following contract(s). Include all required fields in your execution report.\n'
123
+ for (const oc of options.dagOutputContracts) {
124
+ contractContext += `### ${oc.contract_name}\n${oc.contract.description || ''}\n`
125
+ contractContext += '| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n'
126
+ for (const f of oc.contract.fields || []) {
127
+ contractContext += `| ${f.key} | ${f.type} | ${f.required ? 'Yes' : 'No'} | ${f.description || ''} |\n`
128
+ }
129
+ }
130
+ contractContext += '\n'
131
+ }
132
+
133
+ const prompt = `${rolePrefix}${revisionContext}${dagDataContext}${contractContext}Run the following skills in order: ${skillCommands}.`
103
134
 
104
135
  const logFile = logManager.getLogPath(executionId)
105
136
 
@@ -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