@yemi33/minions 0.1.1667 → 0.1.1668

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1668 (2026-05-01)
4
+
5
+ ### Fixes
6
+ - route stranded work items to available agents
7
+
3
8
  ## 0.1.1667 (2026-05-01)
4
9
 
5
10
  ### Other
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-01T21:02:29.699Z"
4
+ "cachedAt": "2026-05-01T23:20:43.938Z"
5
5
  }
package/engine.js CHANGED
@@ -32,7 +32,7 @@ const queries = require('./engine/queries');
32
32
 
33
33
  // ─── Paths ──────────────────────────────────────────────────────────────────
34
34
 
35
- const MINIONS_DIR = __dirname;
35
+ const MINIONS_DIR = shared.MINIONS_DIR;
36
36
  const ROUTING_PATH = path.join(MINIONS_DIR, 'routing.md');
37
37
  const PLAYBOOKS_DIR = path.join(MINIONS_DIR, 'playbooks');
38
38
  const ARCHIVE_DIR = path.join(MINIONS_DIR, 'notes', 'archive');
@@ -2745,9 +2745,9 @@ function discoverFromWorkItems(config, project) {
2745
2745
  }
2746
2746
  const agentHints = routing.extractAgentHints(item);
2747
2747
  const hasAgentHints = Array.isArray(agentHints) && agentHints.length > 0;
2748
- const hardPinRequested = routing.isAgentHardPinned(item);
2749
- let agentId = routing.getHardPinnedAgent(item, config.agents || {})
2750
- || (!hardPinRequested ? resolveAgent(workType, config, { agentHints }) : null);
2748
+ const hardPinnedAgent = routing.getHardPinnedAgent(item, config.agents || {});
2749
+ const hardPinRequested = !!hardPinnedAgent;
2750
+ let agentId = hardPinnedAgent || resolveAgent(workType, config, { agentHints });
2751
2751
  let reservedAgentId = agentId;
2752
2752
  const cfgAgents = config.agents || {};
2753
2753
  const budgetBlocked = Object.keys(cfgAgents).some(id => {
@@ -2865,6 +2865,17 @@ function discoverFromWorkItems(config, project) {
2865
2865
  return newWork;
2866
2866
  }
2867
2867
 
2868
+
2869
+ function getSyntheticCentralProject() {
2870
+ const base = path.basename(MINIONS_DIR);
2871
+ const root = base === '.minions' ? path.dirname(MINIONS_DIR) : MINIONS_DIR;
2872
+ return { name: 'root', localPath: root, repoHost: 'github', repoName: path.basename(root) || 'root', mainBranch: 'main', _synthetic: true };
2873
+ }
2874
+
2875
+ function getCentralDispatchProjects(projects) {
2876
+ return projects.length > 0 ? projects : [getSyntheticCentralProject()];
2877
+ }
2878
+
2868
2879
  /**
2869
2880
  * Build the multi-project context section for central work items.
2870
2881
  * Inserted into the playbook via {{scope_section}}.
@@ -3131,6 +3142,7 @@ function discoverCentralWorkItems(config) {
3131
3142
  const centralPath = path.join(MINIONS_DIR, 'work-items.json');
3132
3143
  const items = safeJson(centralPath) || [];
3133
3144
  const projects = getProjects(config);
3145
+ const dispatchProjects = getCentralDispatchProjects(projects);
3134
3146
  const newWork = [];
3135
3147
  // Collect mutations to apply atomically inside lock callback (avoids TOCTOU)
3136
3148
  const mutations = new Map(); // item.id → { field: value, ... }
@@ -3190,15 +3202,14 @@ function discoverCentralWorkItems(config) {
3190
3202
 
3191
3203
  const assignments = idleAgents.map((agent, i) => ({
3192
3204
  agent,
3193
- assignedProject: projects.length > 0 ? projects[i % projects.length] : null
3205
+ assignedProject: dispatchProjects[i % dispatchProjects.length]
3194
3206
  }));
3195
3207
 
3196
3208
  for (const { agent, assignedProject } of assignments) {
3197
3209
  const fanKey = `${key}-${agent.id}`;
3198
3210
  if (isAlreadyDispatched(fanKey)) continue;
3199
3211
 
3200
- const ap = assignedProject || (projects.length > 0 ? projects[0] : null);
3201
- if (!ap) { log('warn', `Fan-out: skipping ${fanKey} — no projects configured`); continue; }
3212
+ const ap = assignedProject || dispatchProjects[0];
3202
3213
  const fanBranch = `fan/${item.id}/${agent.id}`;
3203
3214
  const vars = {
3204
3215
  ...buildBaseVars(agent.id, config, ap),
@@ -3208,7 +3219,7 @@ function discoverCentralWorkItems(config) {
3208
3219
  item_description: item.description || '',
3209
3220
  work_type: workType,
3210
3221
  additional_context: item.prompt ? `## Additional Context\n\n${item.prompt}` : '',
3211
- scope_section: buildProjectContext(projects, assignedProject, true, agent.name, agent.role),
3222
+ scope_section: buildProjectContext(dispatchProjects, assignedProject, true, agent.name, agent.role),
3212
3223
  project_path: ap?.localPath || '',
3213
3224
  branch_name: fanBranch,
3214
3225
  };
@@ -3252,16 +3263,16 @@ function discoverCentralWorkItems(config) {
3252
3263
  } else {
3253
3264
  // ─── Normal: single agent dispatch ──────────────────────────────
3254
3265
  const agentHints = routing.extractAgentHints(item);
3255
- const hardPinRequested = routing.isAgentHardPinned(item);
3256
- const agentId = routing.getHardPinnedAgent(item, config.agents || {})
3257
- || (!hardPinRequested ? resolveAgent(workType, config, { agentHints }) : null)
3266
+ const hardPinnedAgent = routing.getHardPinnedAgent(item, config.agents || {});
3267
+ const hardPinRequested = !!hardPinnedAgent;
3268
+ const agentId = hardPinnedAgent
3269
+ || resolveAgent(workType, config, { agentHints })
3258
3270
  || (!hardPinRequested && workType !== WORK_TYPE.FIX ? resolveAgentReservation(workType, config, { agentHints }) : null);
3259
3271
  if (!agentId) continue;
3260
3272
 
3261
3273
  const agentName = config.agents[agentId]?.name || agentId;
3262
3274
  const agentRole = config.agents[agentId]?.role || 'Agent';
3263
- const firstProject = projects.length > 0 ? projects[0] : null;
3264
- if (!firstProject) { log('warn', `Dispatch: skipping ${item.id} — no projects configured`); continue; }
3275
+ const firstProject = dispatchProjects[0];
3265
3276
 
3266
3277
  // Branch mutex: skip if target branch is locked by an active dispatch
3267
3278
  const centralBranch = item.branch || item.featureBranch || `work/${item.id}`;
@@ -3282,7 +3293,7 @@ function discoverCentralWorkItems(config) {
3282
3293
  task_id: item.id,
3283
3294
  work_type: workType,
3284
3295
  additional_context: item.prompt ? `## Additional Context\n\n${item.prompt}` : '',
3285
- scope_section: buildProjectContext(projects, null, false, agentName, agentRole),
3296
+ scope_section: buildProjectContext(dispatchProjects, null, false, agentName, agentRole),
3286
3297
  project_path: firstProject?.localPath || '',
3287
3298
  branch_name: centralBranch,
3288
3299
  };
@@ -3384,7 +3395,7 @@ function discoverCentralWorkItems(config) {
3384
3395
  agentRole,
3385
3396
  task: item.title || item.description?.slice(0, 80) || item.id,
3386
3397
  prompt,
3387
- meta: { dispatchKey: key, source: 'central-work-item', item: { ...item, ...mutations.get(item.id) }, planFileName: item.planFile || mutations.get(item.id)?._planFileName || null, branch: item.branch || item.featureBranch || `work/${item.id}` }
3398
+ meta: { dispatchKey: key, source: 'central-work-item', item: { ...item, ...mutations.get(item.id) }, planFileName: item.planFile || mutations.get(item.id)?._planFileName || null, branch: item.branch || item.featureBranch || `work/${item.id}`, project: { name: firstProject.name, localPath: firstProject.localPath } }
3388
3399
  });
3389
3400
 
3390
3401
  setCooldown(key);
@@ -4026,7 +4037,19 @@ async function tickInner() {
4026
4037
  persistPendingDispatchAgent(item);
4027
4038
  } catch (e) { log('warn', `Persist agent resolution for ${item.id} failed: ${e.message}`); }
4028
4039
  }
4029
- // #1204: Pre-assigned unspawned temp agents never unblock naturally.
4040
+ // Unknown configured agent: string ID that is neither configured nor a known temp agent
4041
+ const isUnknownAssignedAgent = typeof item.agent === 'string' && !item.agent.startsWith('temp-') && !config.agents?.[item.agent] && !tempAgents.has(item.agent);
4042
+ if (isUnknownAssignedAgent) {
4043
+ const fallback = resolvePendingDispatchAgent(item, config);
4044
+ if (!fallback) {
4045
+ log('warn', `Pending dispatch ${item.id} has unknown agent ${item.agent} and no fallback available — skipping`);
4046
+ continue;
4047
+ }
4048
+ log('info', `Pending dispatch ${item.id} unknown agent ${item.agent}; routed → ${fallback}`);
4049
+ assignPendingDispatchAgent(item, fallback, config);
4050
+ persistPendingDispatchAgent(item);
4051
+ }
4052
+ // #1204: Pre-assigned unspawned temp agents never unblock naturally.
4030
4053
  // When a batch discovery saturates maxConcurrent, resolveAgent hands out temp
4031
4054
  // IDs that get stamped onto pending items. Because those temp IDs are never
4032
4055
  // in busyAgents (they were never spawned), the agent-busy reassignment path
@@ -4044,43 +4067,28 @@ async function tickInner() {
4044
4067
  }
4045
4068
  }
4046
4069
  if (busyAgents.has(item.agent)) {
4047
- // Agent busy reassignment: if item has been waiting on a busy agent past the threshold,
4048
- // try to find an alternative agent via routing. Skip explicitly assigned items.
4049
- const reassignMs = config.engine?.agentBusyReassignMs ?? ENGINE_DEFAULTS.agentBusyReassignMs;
4050
- const isHardPinned = routing.isAgentHardPinned(item.meta?.item);
4051
- if (isSoftFixDispatch(item)) {
4052
- const originalAgent = item.agent;
4053
- const altAgent = resolvePendingDispatchAgent(item, config);
4054
- if (altAgent && altAgent !== originalAgent && !busyAgents.has(altAgent)) {
4055
- log('info', `Reassigning ${item.id} from ${originalAgent} to ${altAgent}soft fix suggestion unavailable`);
4056
- assignPendingDispatchAgent(item, altAgent, config);
4057
- persistPendingDispatchAgent(item);
4058
- // Fall through to branch mutex / concurrency checks below.
4059
- } else {
4060
- log('info', `Clearing busy soft fix agent on ${item.id} (${originalAgent}) — waiting for any available agent`);
4061
- clearPendingDispatchAgent(item);
4062
- persistPendingDispatchAgent(item);
4063
- continue;
4064
- }
4065
- } else if (!isHardPinned && reassignMs > 0 && item._agentBusySince) {
4066
- const busySinceMs = new Date(item._agentBusySince).getTime();
4067
- if (Date.now() - busySinceMs > reassignMs) {
4068
- const originalAgent = item.agent;
4069
- const altAgent = resolvePendingDispatchAgent(item, config);
4070
- if (altAgent && altAgent !== originalAgent && !busyAgents.has(altAgent)) {
4071
- log('info', `Reassigning ${item.id} from ${originalAgent} to ${altAgent} — agent busy > ${reassignMs}ms`);
4072
- assignPendingDispatchAgent(item, altAgent, config);
4073
- // Persist reassignment to dispatch.json
4074
- persistPendingDispatchAgent(item);
4075
- // Fall through to branch mutex / concurrency checks below
4076
- } else {
4077
- continue; // No alternative agent available — keep waiting
4078
- }
4079
- } else {
4080
- continue; // Below threshold — keep waiting
4081
- }
4070
+ // Agent busy reassignment: compute hard-pin status; if hard-pinned keep waiting.
4071
+ // For all non-hard-pinned items, reroute immediately (no threshold wait).
4072
+ const originalAgent = item.agent;
4073
+ const hardPinnedAgent = routing.getHardPinnedAgent(item.meta?.item, config.agents || {});
4074
+ const isHardPinned = !!hardPinnedAgent && hardPinnedAgent === originalAgent;
4075
+ if (isHardPinned) {
4076
+ continue; // Valid hard pin — keep waiting for pinned agent
4077
+ }
4078
+ // agent busy and idle alternative availablereroute immediately (no threshold)
4079
+ const altAgent = resolvePendingDispatchAgent(item, config);
4080
+ if (altAgent && altAgent !== originalAgent && !busyAgents.has(altAgent)) {
4081
+ log('info', `Reassigning ${item.id} from ${originalAgent} to ${altAgent} agent busy and idle alternative available`);
4082
+ assignPendingDispatchAgent(item, altAgent, config);
4083
+ persistPendingDispatchAgent(item);
4084
+ // Fall through to branch mutex / concurrency checks below
4085
+ } else if (isSoftFixDispatch(item)) {
4086
+ log('info', `Clearing busy soft fix agent on ${item.id} (${originalAgent}) — waiting for any available agent`);
4087
+ clearPendingDispatchAgent(item);
4088
+ persistPendingDispatchAgent(item);
4089
+ continue;
4082
4090
  } else {
4083
- continue; // No _agentBusySince set yet or explicitly assigned — skip
4091
+ continue; // No alternative agent available keep waiting
4084
4092
  }
4085
4093
  }
4086
4094
  // Branch mutex: skip items targeting a branch already locked by an active or newly-dispatched task
@@ -4217,7 +4225,7 @@ module.exports = {
4217
4225
  spawnAgent, resolveAgent,
4218
4226
 
4219
4227
  // Discovery
4220
- discoverWork, discoverFromPrs, discoverFromWorkItems,
4228
+ discoverWork, discoverFromPrs, discoverFromWorkItems, discoverCentralWorkItems,
4221
4229
  materializePlansAsWorkItems,
4222
4230
 
4223
4231
  // Shared helpers (used by lifecycle.js and tests)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1667",
3
+ "version": "0.1.1668",
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"