@yemi33/minions 0.1.565 → 0.1.567

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.567 (2026-04-08)
4
+
5
+ ### Features
6
+ - add ccModel/ccEffort settings and agent runtime tracking
7
+
8
+ ### Other
9
+ - refactor: use safeJson/safeWrite for spawn-agent cache, remove unused checkedAt
10
+
3
11
  ## 0.1.565 (2026-04-08)
4
12
 
5
13
  ### Fixes
@@ -59,11 +59,20 @@ function renderMetrics(metrics) {
59
59
  merged.prsApproved += tm.prsApproved || 0;
60
60
  merged.prsRejected += tm.prsRejected || 0;
61
61
  merged.reviewsDone += tm.reviewsDone || 0;
62
+ merged.totalRuntimeMs = (merged.totalRuntimeMs || 0) + (tm.totalRuntimeMs || 0);
62
63
  }
63
64
  rows.push(['Temp Agents (' + temps.length + ')', merged]);
64
65
  }
65
66
 
66
- let html = '<table class="pr-table"><thead><tr><th>Agent</th><th>Done</th><th>Errors</th><th>PRs</th><th>Approved</th><th>Rejected</th><th>Rate</th><th>Reviews</th></tr></thead><tbody>';
67
+ function fmtAvgRuntime(m) {
68
+ const total = m.totalRuntimeMs || 0;
69
+ const count = (m.tasksCompleted || 0) + (m.tasksErrored || 0);
70
+ if (!count || !total) return '-';
71
+ const avgMin = total / count / 60000;
72
+ return avgMin < 1 ? '<1m' : Math.round(avgMin) + 'm';
73
+ }
74
+
75
+ let html = '<table class="pr-table"><thead><tr><th>Agent</th><th>Done</th><th>Errors</th><th>PRs</th><th>Approved</th><th>Rejected</th><th>Rate</th><th>Reviews</th><th>Avg Runtime</th></tr></thead><tbody>';
67
76
  for (const [id, m] of rows) {
68
77
  const rate = m.prsCreated > 0 ? Math.round((m.prsApproved / m.prsCreated) * 100) + '%' : '-';
69
78
  const rateColor = m.prsCreated > 0 ? (m.prsApproved / m.prsCreated >= 0.7 ? 'var(--green)' : 'var(--red)') : 'var(--muted)';
@@ -76,6 +85,7 @@ function renderMetrics(metrics) {
76
85
  '<td style="color:' + (m.prsRejected > 0 ? 'var(--red)' : 'var(--muted)') + '">' + (m.prsRejected || 0) + '</td>' +
77
86
  '<td style="color:' + rateColor + ';font-weight:600">' + rate + '</td>' +
78
87
  '<td>' + (m.reviewsDone || 0) + '</td>' +
88
+ '<td style="color:var(--muted)">' + fmtAvgRuntime(m) + '</td>' +
79
89
  '</tr>';
80
90
  }
81
91
  html += '</tbody></table>';
package/dashboard.js CHANGED
@@ -368,6 +368,8 @@ function getStatus() {
368
368
  decompose: CONFIG.engine?.autoDecompose !== false,
369
369
  tempAgents: !!CONFIG.engine?.allowTempAgents,
370
370
  inboxThreshold: CONFIG.engine?.inboxConsolidateThreshold || shared.ENGINE_DEFAULTS.inboxConsolidateThreshold,
371
+ ccModel: CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel,
372
+ ccEffort: CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort,
371
373
  },
372
374
  initialized: !!(CONFIG.agents && Object.keys(CONFIG.agents).length > 0),
373
375
  installId: safeRead(path.join(MINIONS_DIR, '.install-id')).trim() || null,
@@ -568,7 +570,7 @@ Available action types:
568
570
  - **schedule**: Create or update a scheduled task. Fields: id (unique slug), title, cron (3-field: minute hour dayOfWeek), workType (implement/test/explore/ask/review/fix), project (optional), agent (optional), description (optional), priority (optional), enabled (default true). Example cron: "0 9 2" = every Tuesday at 9am.
569
571
  - **delete-schedule**: Delete a scheduled task. Fields: id.
570
572
  - **create-meeting**: Start a team meeting. Fields: title (short meeting name), agenda (detailed text — what agents should investigate/debate, numbered items work best), agents (array of agent IDs), rounds (optional, default 3), project (optional).
571
- - **set-config**: Update engine settings. Fields: setting (setting name), value (new value). Valid settings: autoApprovePlans (bool), autoDecompose (bool), allowTempAgents (bool), maxConcurrent (number), maxTurns (number). Example: { "type": "set-config", "setting": "autoApprovePlans", "value": true }
573
+ - **set-config**: Update engine settings. Fields: setting (setting name), value (new value). Valid settings: autoApprovePlans (bool), autoDecompose (bool), allowTempAgents (bool), maxConcurrent (number), maxTurns (number), ccModel (sonnet/haiku/opus), ccEffort (null/low/medium/high). Example: { "type": "set-config", "setting": "autoApprovePlans", "value": true }
572
574
  - **edit-pipeline**: Update an existing pipeline. Fields: id (pipeline ID), title (optional), stages (optional, JSON array — omit "agent" on stages unless the user specifically requests one; the engine routes to any available agent by default), trigger (optional, { cron: "minute hour dow" } or null for manual).
573
575
  - **unpin**: Remove a pinned note. Fields: title (exact title of the pinned note to remove)
574
576
  - **archive-plan**: Archive a completed/paused plan. Fields: file (PRD .json or plan .md filename)
@@ -801,7 +803,9 @@ function updateSession(store, key, sessionId, existing) {
801
803
  * @param {number} opts.maxTurns - Max tool-use turns
802
804
  * @param {string} opts.allowedTools - Comma-separated tool list
803
805
  */
804
- async function ccCall(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = 900000, maxTurns = 50, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model = 'sonnet' } = {}) {
806
+ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = 900000, maxTurns = 50, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model } = {}) {
807
+ if (!model) model = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
808
+ const ccEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
805
809
  const existing = resolveSession(store, sessionKey);
806
810
  let sessionId = existing ? existing.sessionId : null;
807
811
 
@@ -817,7 +821,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
817
821
  // Attempt 1: resume existing session — skip preamble (session already has context)
818
822
  if (sessionId && maxTurns > 1) {
819
823
  result = await llm.callLLM(buildPrompt({ includePreamble: false }), '', {
820
- timeout, label, model, maxTurns, allowedTools, sessionId,
824
+ timeout, label, model, maxTurns, allowedTools, sessionId, effort: ccEffort,
821
825
  });
822
826
  llm.trackEngineUsage(label, result.usage);
823
827
 
@@ -852,7 +856,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
852
856
  // Attempt 2: fresh session (include preamble for full context)
853
857
  const freshPrompt = buildPrompt();
854
858
  result = await llm.callLLM(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
855
- timeout, label, model, maxTurns, allowedTools,
859
+ timeout, label, model, maxTurns, allowedTools, effort: ccEffort,
856
860
  });
857
861
  llm.trackEngineUsage(label, result.usage);
858
862
 
@@ -866,7 +870,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
866
870
  console.log(`[${label}] Fresh call also failed (code=${result.code}, empty=${!result.text}), retrying once more...`);
867
871
  await new Promise(r => setTimeout(r, 2000));
868
872
  result = await llm.callLLM(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
869
- timeout, label, model, maxTurns, allowedTools,
873
+ timeout, label, model, maxTurns, allowedTools, effort: ccEffort,
870
874
  });
871
875
  llm.trackEngineUsage(label, result.usage);
872
876
 
@@ -3374,10 +3378,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3374
3378
  const prompt = preamble + '\n\n---\n\n' + body.message;
3375
3379
 
3376
3380
  const { callLLMStreaming, trackEngineUsage: trackUsage } = require('./engine/llm');
3381
+ const streamModel = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
3382
+ const streamEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
3377
3383
  const llmPromise = callLLMStreaming(prompt, CC_STATIC_SYSTEM_PROMPT, {
3378
- timeout: 900000, label: 'command-center', model: 'sonnet', maxTurns: 50,
3384
+ timeout: 900000, label: 'command-center', model: streamModel, maxTurns: 50,
3379
3385
  allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
3380
- sessionId,
3386
+ sessionId, effort: streamEffort,
3381
3387
  onChunk: (text) => {
3382
3388
  try { res.write('data: ' + JSON.stringify({ type: 'chunk', text }) + '\n\n'); } catch {}
3383
3389
  },
@@ -3582,6 +3588,15 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3582
3588
  }
3583
3589
  // String fields
3584
3590
  if (e.worktreeRoot !== undefined) config.engine.worktreeRoot = String(e.worktreeRoot || D.worktreeRoot);
3591
+ // CC model/effort
3592
+ if (e.ccModel !== undefined) {
3593
+ const valid = ['sonnet', 'haiku', 'opus'];
3594
+ config.engine.ccModel = valid.includes(e.ccModel) ? e.ccModel : D.ccModel;
3595
+ }
3596
+ if (e.ccEffort !== undefined) {
3597
+ const valid = [null, 'low', 'medium', 'high'];
3598
+ config.engine.ccEffort = valid.includes(e.ccEffort) ? e.ccEffort : null;
3599
+ }
3585
3600
  // Boolean fields
3586
3601
  for (const key of ['autoApprovePlans', 'evalLoop', 'autoDecompose', 'allowTempAgents']) {
3587
3602
  if (e[key] !== undefined) config.engine[key] = !!e[key];
@@ -1019,6 +1019,12 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
1019
1019
  m.lastTask = dispatchItem.task;
1020
1020
  m.lastCompleted = ts();
1021
1021
  if (model) m.model = model;
1022
+ // Track runtime (wall-clock duration from dispatch start to completion)
1023
+ const runtimeMs = (dispatchItem.started_at && dispatchItem.completed_at)
1024
+ ? new Date(dispatchItem.completed_at).getTime() - new Date(dispatchItem.started_at).getTime()
1025
+ : 0;
1026
+ if (runtimeMs > 0) m.totalRuntimeMs = (m.totalRuntimeMs || 0) + runtimeMs;
1027
+
1022
1028
  if (result === DISPATCH_RESULT.SUCCESS) {
1023
1029
  m.tasksCompleted++;
1024
1030
  if (prsCreatedCount > 0) m.prsCreated = (m.prsCreated || 0) + prsCreatedCount;
@@ -1036,9 +1042,10 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
1036
1042
  }
1037
1043
  const today = dateStamp();
1038
1044
  if (!metrics._daily) metrics._daily = {};
1039
- if (!metrics._daily[today]) metrics._daily[today] = { costUsd: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0, tasks: 0 };
1045
+ if (!metrics._daily[today]) metrics._daily[today] = { costUsd: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0, tasks: 0, runtimeMs: 0 };
1040
1046
  const daily = metrics._daily[today];
1041
1047
  daily.tasks++;
1048
+ if (runtimeMs > 0) daily.runtimeMs = (daily.runtimeMs || 0) + runtimeMs;
1042
1049
  if (taskUsage) {
1043
1050
  daily.costUsd += taskUsage.costUsd || 0;
1044
1051
  daily.inputTokens += taskUsage.inputTokens || 0;
package/engine/llm.js CHANGED
@@ -44,7 +44,7 @@ function trackEngineUsage(category, usage) {
44
44
 
45
45
  // ── Core LLM Call ───────────────────────────────────────────────────────────
46
46
 
47
- function callLLM(promptText, sysPromptText, { timeout = 120000, label = 'llm', model = 'sonnet', maxTurns = 1, allowedTools = '', sessionId = null } = {}) {
47
+ function callLLM(promptText, sysPromptText, { timeout = 120000, label = 'llm', model = 'sonnet', maxTurns = 1, allowedTools = '', sessionId = null, effort = null } = {}) {
48
48
  return new Promise((resolve) => {
49
49
  const id = uid();
50
50
  const tmpDir = path.join(ENGINE_DIR, 'tmp');
@@ -61,6 +61,7 @@ function callLLM(promptText, sysPromptText, { timeout = 120000, label = 'llm', m
61
61
  '--verbose',
62
62
  ];
63
63
  if (allowedTools) args.push('--allowedTools', allowedTools);
64
+ if (effort) args.push('--effort', effort);
64
65
  args.push('--permission-mode', 'bypassPermissions');
65
66
 
66
67
  if (sessionId) args.push('--resume', sessionId);
@@ -113,7 +114,7 @@ function isResumeSessionStillValid(result) {
113
114
  * Returns the same result object as callLLM when the process completes.
114
115
  * onChunk(text) is called for each assistant text block as it arrives.
115
116
  */
116
- function callLLMStreaming(promptText, sysPromptText, { timeout = 120000, label = 'llm', model = 'sonnet', maxTurns = 1, allowedTools = '', sessionId = null, onChunk = () => {}, onToolUse = null } = {}) {
117
+ function callLLMStreaming(promptText, sysPromptText, { timeout = 120000, label = 'llm', model = 'sonnet', maxTurns = 1, allowedTools = '', sessionId = null, onChunk = () => {}, onToolUse = null, effort = null } = {}) {
117
118
  let _abort = null;
118
119
  const promise = new Promise((resolve) => {
119
120
  const id = uid();
@@ -131,6 +132,7 @@ function callLLMStreaming(promptText, sysPromptText, { timeout = 120000, label =
131
132
  '--verbose',
132
133
  ];
133
134
  if (allowedTools) args.push('--allowedTools', allowedTools);
135
+ if (effort) args.push('--effort', effort);
134
136
  args.push('--permission-mode', 'bypassPermissions');
135
137
  if (sessionId) args.push('--resume', sessionId);
136
138
 
package/engine/shared.js CHANGED
@@ -529,6 +529,8 @@ const ENGINE_DEFAULTS = {
529
529
  lockRetries: 2, // retry lock acquisition this many times after initial timeout (total attempts = 1 + lockRetries)
530
530
  lockRetryBackoffMs: 500, // base backoff between lock retries (doubles each attempt: 500ms, 1s, 2s, ...)
531
531
  maxBuildFixAttempts: 3, // max consecutive auto-fix dispatch cycles per PR before escalation to human
532
+ ccModel: 'sonnet', // model for Command Center and doc-chat (sonnet, haiku, opus)
533
+ ccEffort: null, // effort level for CC/doc-chat (null, 'low', 'medium', 'high')
532
534
  };
533
535
 
534
536
  // ─── Status & Type Constants ─────────────────────────────────────────────────
@@ -572,6 +574,7 @@ const DEFAULT_AGENT_METRICS = {
572
574
  reviewsDone: 0,
573
575
  lastTask: null, lastCompleted: null,
574
576
  totalCostUsd: 0, totalInputTokens: 0, totalOutputTokens: 0, totalCacheRead: 0,
577
+ totalRuntimeMs: 0, // cumulative agent runtime across all tasks
575
578
  };
576
579
 
577
580
  const DEFAULT_AGENTS = {
@@ -8,7 +8,7 @@
8
8
 
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
- const { exec, runFile, cleanChildEnv, killGracefully, killImmediate, ts } = require('./shared');
11
+ const { exec, runFile, cleanChildEnv, killGracefully, killImmediate, ts, safeJson, safeWrite } = require('./shared');
12
12
 
13
13
  const [,, promptFile, sysPromptFile, ...extraArgs] = process.argv;
14
14
 
@@ -29,14 +29,12 @@ const capsCachePath = path.join(__dirname, 'claude-caps.json');
29
29
  let _sysPromptFileSupported = null;
30
30
 
31
31
  // Fast path: use cached binary path if it still exists on disk
32
- try {
33
- const caps = JSON.parse(fs.readFileSync(capsCachePath, 'utf8'));
34
- if (caps.claudeBin && fs.existsSync(caps.claudeBin)) {
35
- claudeBin = caps.claudeBin;
36
- claudeIsNative = !!caps.claudeIsNative;
37
- _sysPromptFileSupported = caps.sysPromptFile ?? null;
38
- }
39
- } catch {}
32
+ const caps = safeJson(capsCachePath);
33
+ if (caps?.claudeBin && fs.existsSync(caps.claudeBin)) {
34
+ claudeBin = caps.claudeBin;
35
+ claudeIsNative = !!caps.claudeIsNative;
36
+ _sysPromptFileSupported = caps.sysPromptFile ?? null;
37
+ }
40
38
 
41
39
  // Strategy 1: Check if `claude` is on PATH (native installer or npm global bin)
42
40
  if (!claudeBin) try {
@@ -127,7 +125,7 @@ if (_sysPromptFileSupported === null) {
127
125
  _sysPromptFileSupported = (testResult.stdout || '').includes('system-prompt-file');
128
126
  } catch { _sysPromptFileSupported = true; /* assume supported */ }
129
127
  // Save binary path + capability flag together
130
- try { fs.writeFileSync(capsCachePath, JSON.stringify({ claudeBin, claudeIsNative, sysPromptFile: _sysPromptFileSupported, checkedAt: ts() })); } catch {}
128
+ try { safeWrite(capsCachePath, { claudeBin, claudeIsNative, sysPromptFile: _sysPromptFileSupported }); } catch {}
131
129
  }
132
130
  if (!isResume) try {
133
131
  if (!_sysPromptFileSupported) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.565",
3
+ "version": "0.1.567",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"