@yemi33/minions 0.1.956 → 0.1.957

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,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.956 (2026-04-14)
3
+ ## 0.1.957 (2026-04-14)
4
4
 
5
5
  ### Features
6
6
  - wire agentBusyReassignMs into settings UI and persist
@@ -25,6 +25,7 @@
25
25
  - fix dispatch.js mutator fallback to use nullish coalescing (#966)
26
26
 
27
27
  ### Fixes
28
+ - suppress warn for optional template variables (#1061)
28
29
  - fix ENGINE_DEFAULTS ref and derive boolean settings from defaults
29
30
  - add autoFixConflicts to boolean settings persist list
30
31
  - tighten build query guard to require mergeCommitId
@@ -44,7 +45,6 @@
44
45
  - qaAbort race + skip kill on completed doc-chat
45
46
  - doc-chat Stop button actually works — release server guard on disconnect
46
47
  - CC streaming auto-retries fresh session when resume fails
47
- - 429 retry on abort race applies to all sends, not just queued
48
48
 
49
49
  ### Other
50
50
  - test(engine): add regression tests for autoFixConflicts flag and DEFAULTS alias
@@ -214,6 +214,22 @@ function resolveTaskContext(item, config) {
214
214
  // are optional by design. Only variables that make the playbook non-functional
215
215
  // when absent are listed here.
216
216
 
217
+ // ─── Optional Template Variables ────────────────────────────────────────────
218
+ // Variables that legitimately resolve to empty string for ad-hoc work items.
219
+ // These are silently filtered out of the empty-string warning to avoid
220
+ // masking real warnings. Add new optional vars here when they are contextual
221
+ // (plan-linked, checkpoint-dependent, etc.) and not required for the playbook
222
+ // to function.
223
+
224
+ const PLAYBOOK_OPTIONAL_VARS = new Set([
225
+ 'source_plan', // only set when work item is linked to a plan
226
+ 'plan_slug', // derived from source_plan
227
+ 'additional_context', // only set when item has a prompt
228
+ 'references', // only set when item.references has entries
229
+ 'acceptance_criteria', // only set when item.acceptanceCriteria has entries
230
+ 'checkpoint_context', // only set when resuming from a prior timeout
231
+ ]);
232
+
217
233
  const PLAYBOOK_REQUIRED_VARS = {
218
234
  'implement': ['item_id', 'item_name', 'branch_name', 'project_path'],
219
235
  'implement-shared': ['item_id', 'item_name', 'branch_name', 'worktree_path'],
@@ -364,9 +380,9 @@ function renderPlaybook(type, vars) {
364
380
  log('warn', `Playbook "${type}": substituted values contain unresolved {{...}} patterns (potential self-reference): ${selfRefVars.join(', ')}`);
365
381
  }
366
382
 
367
- // Warn on variables that resolved to empty string
383
+ // Warn on variables that resolved to empty string (skip known-optional vars)
368
384
  const emptyVars = Object.entries(allVars)
369
- .filter(([, val]) => String(val) === '')
385
+ .filter(([key, val]) => String(val) === '' && !PLAYBOOK_OPTIONAL_VARS.has(key))
370
386
  .map(([key]) => key);
371
387
  if (emptyVars.length > 0) {
372
388
  log('warn', `Playbook "${type}": template variables resolved to empty string: ${emptyVars.join(', ')}`);
@@ -551,6 +567,7 @@ module.exports = {
551
567
  renderPlaybook,
552
568
  validatePlaybookVars,
553
569
  PLAYBOOK_REQUIRED_VARS,
570
+ PLAYBOOK_OPTIONAL_VARS,
554
571
  buildSystemPrompt,
555
572
  buildAgentContext,
556
573
  selectPlaybook,
package/engine/shared.js CHANGED
@@ -555,6 +555,7 @@ const ENGINE_DEFAULTS = {
555
555
  autoCompletePrs: false, // auto-merge PRs when builds green + review approved (opt-in)
556
556
  prMergeMethod: 'squash', // merge method: squash, merge, rebase
557
557
  ignoredCommentAuthors: [], // comments from these authors are auto-closed and never trigger fixes
558
+ agentBusyReassignMs: 600000, // 10min — reassign work item to another agent if preferred agent is busy beyond this threshold
558
559
  ccModel: 'sonnet', // model for Command Center and doc-chat (sonnet, haiku, opus)
559
560
  ccEffort: null, // effort level for CC/doc-chat (null, 'low', 'medium', 'high')
560
561
  heartbeatTimeouts: {}, // populated after WORK_TYPE is defined (below)
package/engine.js CHANGED
@@ -1300,6 +1300,7 @@ async function spawnAgent(dispatchItem, config) {
1300
1300
  const item = dispatch.pending.splice(idx, 1)[0];
1301
1301
  item.started_at = startedAt;
1302
1302
  delete item.skipReason;
1303
+ delete item._agentBusySince;
1303
1304
  if (!dispatch.active.some(d => d.id === id)) {
1304
1305
  dispatch.active.push(item);
1305
1306
  }
@@ -3324,7 +3325,46 @@ async function tickInner() {
3324
3325
  log('warn', `Duplicate dispatch ID ${item.id} in pending queue — skipping`);
3325
3326
  continue;
3326
3327
  }
3327
- if (busyAgents.has(item.agent)) continue;
3328
+ if (busyAgents.has(item.agent)) {
3329
+ // Agent busy reassignment: if item has been waiting on a busy agent past the threshold,
3330
+ // try to find an alternative agent via routing. Skip explicitly assigned items.
3331
+ const reassignMs = config.engine?.agentBusyReassignMs ?? ENGINE_DEFAULTS.agentBusyReassignMs;
3332
+ const isExplicitReassign = !!item.meta?.item?.agent;
3333
+ if (!isExplicitReassign && reassignMs > 0 && item._agentBusySince) {
3334
+ const busySinceMs = new Date(item._agentBusySince).getTime();
3335
+ if (Date.now() - busySinceMs > reassignMs) {
3336
+ const originalAgent = item.agent;
3337
+ const altAgent = resolveAgent(item.type, config);
3338
+ if (altAgent && altAgent !== originalAgent && !busyAgents.has(altAgent)) {
3339
+ log('info', `Reassigning ${item.id} from ${originalAgent} to ${altAgent} — agent busy > ${reassignMs}ms`);
3340
+ item.agent = altAgent;
3341
+ item.agentName = config.agents[altAgent]?.name || tempAgents.get(altAgent)?.name || altAgent;
3342
+ item.agentRole = config.agents[altAgent]?.role || tempAgents.get(altAgent)?.role || 'Agent';
3343
+ delete item._agentBusySince;
3344
+ delete item.skipReason;
3345
+ // Persist reassignment to dispatch.json
3346
+ mutateDispatch((dp) => {
3347
+ const p = (dp.pending || []).find(d => d.id === item.id);
3348
+ if (p) {
3349
+ p.agent = altAgent;
3350
+ p.agentName = item.agentName;
3351
+ p.agentRole = item.agentRole;
3352
+ delete p._agentBusySince;
3353
+ delete p.skipReason;
3354
+ }
3355
+ return dp;
3356
+ });
3357
+ // Fall through to branch mutex / concurrency checks below
3358
+ } else {
3359
+ continue; // No alternative agent available — keep waiting
3360
+ }
3361
+ } else {
3362
+ continue; // Below threshold — keep waiting
3363
+ }
3364
+ } else {
3365
+ continue; // No _agentBusySince set yet or explicitly assigned — skip
3366
+ }
3367
+ }
3328
3368
  // Branch mutex: skip items targeting a branch already locked by an active or newly-dispatched task
3329
3369
  const itemBranch = item.meta?.branch ? sanitizeBranch(item.meta.branch) : null;
3330
3370
  if (itemBranch && lockedBranches.has(itemBranch)) continue;
@@ -3412,6 +3452,18 @@ async function tickInner() {
3412
3452
  reason = 'branch_locked';
3413
3453
  }
3414
3454
  }
3455
+ // Track when item first became blocked on a busy agent for reassignment threshold
3456
+ if (reason === 'agent_busy') {
3457
+ if (!item._agentBusySince) {
3458
+ item._agentBusySince = ts();
3459
+ skipReasonChanged = true;
3460
+ }
3461
+ } else {
3462
+ if (item._agentBusySince) {
3463
+ delete item._agentBusySince;
3464
+ skipReasonChanged = true;
3465
+ }
3466
+ }
3415
3467
  if (item.skipReason !== reason) {
3416
3468
  item.skipReason = reason;
3417
3469
  skipReasonChanged = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.956",
3
+ "version": "0.1.957",
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"