@yemi33/minions 0.1.1957 → 0.1.1958
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/engine/routing.js +22 -4
- package/engine.js +134 -11
- package/package.json +1 -1
- package/routing.md +1 -1
package/engine/routing.js
CHANGED
|
@@ -190,7 +190,7 @@ function normalizeAgentHints(agentHints, authorAgent = null, agents = null) {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
function resolveAgent(workType, config, opts = {}) {
|
|
193
|
-
const { authorAgent = null, agentHints = null, dryRun = false } = opts || {};
|
|
193
|
+
const { authorAgent = null, agentHints = null, dryRun = false, excludeAgent = null } = opts || {};
|
|
194
194
|
const route = routeForWorkType(workType);
|
|
195
195
|
const agents = config.agents || {};
|
|
196
196
|
|
|
@@ -198,7 +198,18 @@ function resolveAgent(workType, config, opts = {}) {
|
|
|
198
198
|
let preferred = route.preferred === '_author_' ? authorAgent : route.preferred;
|
|
199
199
|
let fallback = route.fallback === '_author_' ? authorAgent : route.fallback;
|
|
200
200
|
|
|
201
|
+
// Self-review ban (W-mp7jl5w3001e8c7e): when caller passes excludeAgent, that
|
|
202
|
+
// agent ID is filtered out of every selection path (preferred, fallback,
|
|
203
|
+
// hinted, anyIdle, temp). The auto-routing PR-review path passes
|
|
204
|
+
// excludeAgent=pr.agent so we never auto-dispatch a review to the author.
|
|
205
|
+
// Explicit-override callers (work-item with agent hint, dashboard force) MUST
|
|
206
|
+
// NOT pass excludeAgent — operator intent wins. Comparison is lowercased so
|
|
207
|
+
// case mismatches between pr.agent and config keys can't slip through.
|
|
208
|
+
const excludeKey = excludeAgent ? String(excludeAgent).toLowerCase() : null;
|
|
209
|
+
const isExcluded = (id) => excludeKey != null && String(id || '').toLowerCase() === excludeKey;
|
|
210
|
+
|
|
201
211
|
const isAvailable = (id) => {
|
|
212
|
+
if (isExcluded(id)) return false;
|
|
202
213
|
if (!agents[id] || !isAgentIdle(id) || (!dryRun && _claimedAgents.has(id))) return false;
|
|
203
214
|
// Budget check — no budget means infinite (no limit)
|
|
204
215
|
const budget = agents[id].monthlyBudgetUsd;
|
|
@@ -212,7 +223,7 @@ function resolveAgent(workType, config, opts = {}) {
|
|
|
212
223
|
const pickAnyIdle = (exclude = []) => {
|
|
213
224
|
const excludeSet = new Set(exclude.filter(Boolean));
|
|
214
225
|
const idle = Object.keys(agents)
|
|
215
|
-
.filter(id => !excludeSet.has(id) && isAvailable(id))
|
|
226
|
+
.filter(id => !excludeSet.has(id) && !isExcluded(id) && isAvailable(id))
|
|
216
227
|
.sort((a, b) => getAgentErrorRate(a) - getAgentErrorRate(b));
|
|
217
228
|
if (idle[0]) { if (!dryRun) _claimedAgents.add(idle[0]); return idle[0]; }
|
|
218
229
|
return null;
|
|
@@ -221,6 +232,7 @@ function resolveAgent(workType, config, opts = {}) {
|
|
|
221
232
|
const hintedAgents = normalizeAgentHints(agentHints, authorAgent, agents);
|
|
222
233
|
if (hintedAgents.length > 0) {
|
|
223
234
|
for (const id of hintedAgents) {
|
|
235
|
+
if (isExcluded(id)) continue;
|
|
224
236
|
if (isAvailable(id)) { if (!dryRun) _claimedAgents.add(id); return id; }
|
|
225
237
|
}
|
|
226
238
|
}
|
|
@@ -236,7 +248,9 @@ function resolveAgent(workType, config, opts = {}) {
|
|
|
236
248
|
const anyIdle = pickAnyIdle([preferred, fallback]);
|
|
237
249
|
if (anyIdle) return anyIdle;
|
|
238
250
|
|
|
239
|
-
// No idle configured agent — try temp agent if enabled
|
|
251
|
+
// No idle configured agent — try temp agent if enabled. Temp agent IDs
|
|
252
|
+
// (`temp-<uid>`) are by construction never equal to a named author agent, so
|
|
253
|
+
// they're always valid non-author reviewers under excludeAgent.
|
|
240
254
|
if (config.engine?.allowTempAgents) {
|
|
241
255
|
// Enforce per-tick temp-agent budget so temps count against maxConcurrent.
|
|
242
256
|
// Without this guard, a mass-discovery pass (e.g. 20 PR build failures) would
|
|
@@ -261,13 +275,17 @@ function resolveAgent(workType, config, opts = {}) {
|
|
|
261
275
|
}
|
|
262
276
|
|
|
263
277
|
function resolveAgentReservation(workType, config, opts = {}) {
|
|
264
|
-
const { authorAgent = null, agentHints = null } = opts || {};
|
|
278
|
+
const { authorAgent = null, agentHints = null, excludeAgent = null } = opts || {};
|
|
265
279
|
const route = routeForWorkType(workType);
|
|
266
280
|
const agents = config.agents || {};
|
|
267
281
|
const hintedAgents = normalizeAgentHints(agentHints, authorAgent, agents);
|
|
268
282
|
|
|
283
|
+
const excludeKey = excludeAgent ? String(excludeAgent).toLowerCase() : null;
|
|
284
|
+
const isExcluded = (id) => excludeKey != null && String(id || '').toLowerCase() === excludeKey;
|
|
285
|
+
|
|
269
286
|
const hasBudget = (id) => {
|
|
270
287
|
if (!agents[id]) return false;
|
|
288
|
+
if (isExcluded(id)) return false;
|
|
271
289
|
const budget = agents[id].monthlyBudgetUsd;
|
|
272
290
|
return !(budget && budget > 0 && getMonthlySpend(id) >= budget);
|
|
273
291
|
};
|
package/engine.js
CHANGED
|
@@ -3599,11 +3599,32 @@ async function discoverFromPrs(config, project) {
|
|
|
3599
3599
|
}
|
|
3600
3600
|
} catch (e) { log('warn', `Pre-dispatch vote check for ${pr.id}: ${e.message} — skipping dispatch`); continue; }
|
|
3601
3601
|
|
|
3602
|
-
|
|
3603
|
-
|
|
3602
|
+
// Self-review ban (W-mp7jl5w3001e8c7e): exclude the PR author from the
|
|
3603
|
+
// candidate pool. If no non-author reviewer is available right now, defer
|
|
3604
|
+
// by queueing a pending entry with _pendingReason: 'no_non_author_reviewer'
|
|
3605
|
+
// so the per-tick dispatcher re-evaluates and promotes once a non-author
|
|
3606
|
+
// becomes idle (modeled on the work-item _pendingReason=no_agent gate).
|
|
3607
|
+
const reviewAuthor = pr.agent || null;
|
|
3608
|
+
const agentId = resolveAgent('review', config, reviewAuthor ? { excludeAgent: reviewAuthor } : {});
|
|
3604
3609
|
const prBranch = ensurePrBranchForDispatch(project, pr, 'review');
|
|
3605
3610
|
if (!prBranch) continue;
|
|
3606
3611
|
|
|
3612
|
+
if (!agentId) {
|
|
3613
|
+
const deferred = buildPrDispatch(routing.ANY_AGENT, config, project, pr, 'review', {
|
|
3614
|
+
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: prBranch,
|
|
3615
|
+
pr_author: pr.agent || '', pr_url: pr.url || '',
|
|
3616
|
+
}, `Review ${pr.id}: ${pr.title}`, {
|
|
3617
|
+
dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta,
|
|
3618
|
+
deferReviewerResolution: true,
|
|
3619
|
+
});
|
|
3620
|
+
if (deferred) {
|
|
3621
|
+
deferred._pendingReason = 'no_non_author_reviewer';
|
|
3622
|
+
log('info', `Review for ${pr.id} deferred: no non-author reviewer available (author=${reviewAuthor || '<unknown>'}) — queued pending`);
|
|
3623
|
+
newWork.push(deferred);
|
|
3624
|
+
}
|
|
3625
|
+
continue;
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3607
3628
|
const item = buildPrDispatch(agentId, config, project, pr, 'review', {
|
|
3608
3629
|
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: prBranch,
|
|
3609
3630
|
pr_author: pr.agent || '', pr_url: pr.url || '',
|
|
@@ -3729,11 +3750,30 @@ async function discoverFromPrs(config, project) {
|
|
|
3729
3750
|
}
|
|
3730
3751
|
} catch (e) { log('warn', `Pre-dispatch vote check for ${pr.id}: ${e.message} — skipping dispatch`); continue; }
|
|
3731
3752
|
|
|
3732
|
-
|
|
3733
|
-
if
|
|
3753
|
+
// Self-review ban (W-mp7jl5w3001e8c7e): exclude PR author from re-review
|
|
3754
|
+
// candidate pool; defer with no_non_author_reviewer if no eligible
|
|
3755
|
+
// reviewer is available right now (re-evaluated each tick).
|
|
3756
|
+
const reviewAuthor = pr.agent || null;
|
|
3757
|
+
const agentId = resolveAgent('review', config, reviewAuthor ? { excludeAgent: reviewAuthor } : {});
|
|
3734
3758
|
const prBranch = ensurePrBranchForDispatch(project, pr, 're-review');
|
|
3735
3759
|
if (!prBranch) continue;
|
|
3736
3760
|
|
|
3761
|
+
if (!agentId) {
|
|
3762
|
+
const deferred = buildPrDispatch(routing.ANY_AGENT, config, project, pr, 'review', {
|
|
3763
|
+
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: prBranch,
|
|
3764
|
+
pr_author: pr.agent || '', pr_url: pr.url || '',
|
|
3765
|
+
}, `Review ${pr.id}: ${pr.title}`, {
|
|
3766
|
+
dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta,
|
|
3767
|
+
deferReviewerResolution: true,
|
|
3768
|
+
});
|
|
3769
|
+
if (deferred) {
|
|
3770
|
+
deferred._pendingReason = 'no_non_author_reviewer';
|
|
3771
|
+
log('info', `Re-review for ${pr.id} deferred: no non-author reviewer available (author=${reviewAuthor || '<unknown>'}) — queued pending`);
|
|
3772
|
+
newWork.push(deferred);
|
|
3773
|
+
}
|
|
3774
|
+
continue;
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3737
3777
|
const item = buildPrDispatch(agentId, config, project, pr, 'review', {
|
|
3738
3778
|
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: prBranch,
|
|
3739
3779
|
pr_author: pr.agent || '', pr_url: pr.url || '',
|
|
@@ -4061,6 +4101,42 @@ function refreshDeferredWorkItemPrompt(item, config) {
|
|
|
4061
4101
|
item.meta.deferAgentResolution = false;
|
|
4062
4102
|
}
|
|
4063
4103
|
|
|
4104
|
+
// Self-review ban (W-mp7jl5w3001e8c7e): re-render the deferred PR review
|
|
4105
|
+
// prompt once a real reviewer is assigned. PR review dispatches that hit the
|
|
4106
|
+
// no_non_author_reviewer gate are queued with agent=ANY_AGENT and a placeholder
|
|
4107
|
+
// prompt; this rebuilds the prompt with the actual agent's identity so the
|
|
4108
|
+
// playbook's {{agent_id}}/{{agent_name}}/{{agent_role}} vars resolve correctly.
|
|
4109
|
+
function refreshDeferredReviewPrompt(item, config) {
|
|
4110
|
+
if (!item?.meta?.deferReviewerResolution) return;
|
|
4111
|
+
if (!item.agent || item.agent === routing.ANY_AGENT) return;
|
|
4112
|
+
if (item.type !== WORK_TYPE.REVIEW) return;
|
|
4113
|
+
const pr = item.meta?.pr;
|
|
4114
|
+
if (!pr) return;
|
|
4115
|
+
const project = projectFromDispatchMeta(item.meta?.project, config);
|
|
4116
|
+
if (!project) return;
|
|
4117
|
+
const prBranch = item.meta.branch || pr.branch || '';
|
|
4118
|
+
const prNumber = shared.getPrNumber(pr);
|
|
4119
|
+
const extraVars = {
|
|
4120
|
+
pr_id: pr.id,
|
|
4121
|
+
pr_number: prNumber,
|
|
4122
|
+
pr_title: pr.title || '',
|
|
4123
|
+
pr_branch: prBranch,
|
|
4124
|
+
pr_author: pr.agent || '',
|
|
4125
|
+
pr_url: pr.url || '',
|
|
4126
|
+
};
|
|
4127
|
+
const rebuilt = buildPrDispatch(
|
|
4128
|
+
item.agent, config, project, pr, WORK_TYPE.REVIEW,
|
|
4129
|
+
extraVars, `Review ${pr.id}: ${pr.title || ''}`, item.meta
|
|
4130
|
+
);
|
|
4131
|
+
if (rebuilt && rebuilt.prompt) {
|
|
4132
|
+
item.prompt = rebuilt.prompt;
|
|
4133
|
+
item.task = rebuilt.task;
|
|
4134
|
+
if (rebuilt.agentName) item.agentName = rebuilt.agentName;
|
|
4135
|
+
if (rebuilt.agentRole) item.agentRole = rebuilt.agentRole;
|
|
4136
|
+
}
|
|
4137
|
+
item.meta.deferReviewerResolution = false;
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4064
4140
|
function discoverFromWorkItems(config, project) {
|
|
4065
4141
|
const src = project?.workSources?.workItems || config.workSources?.workItems;
|
|
4066
4142
|
if (!src?.enabled) {
|
|
@@ -5139,6 +5215,16 @@ function getPendingDispatchRoutingOpts(item) {
|
|
|
5139
5215
|
const opts = { agentHints: routing.extractAgentHints(item?.meta?.item) };
|
|
5140
5216
|
const authorAgent = item?.meta?.pr?.agent;
|
|
5141
5217
|
if (authorAgent) opts.authorAgent = authorAgent;
|
|
5218
|
+
// Self-review ban (W-mp7jl5w3001e8c7e): when re-routing a deferred review
|
|
5219
|
+
// dispatch, exclude the PR author from the candidate pool. Operator-explicit
|
|
5220
|
+
// dispatches (meta.explicitAgent === true) bypass this gate.
|
|
5221
|
+
if (
|
|
5222
|
+
routing.normalizeWorkType(item?.type, WORK_TYPE.IMPLEMENT) === WORK_TYPE.REVIEW
|
|
5223
|
+
&& authorAgent
|
|
5224
|
+
&& !item?.meta?.explicitAgent
|
|
5225
|
+
) {
|
|
5226
|
+
opts.excludeAgent = authorAgent;
|
|
5227
|
+
}
|
|
5142
5228
|
return opts;
|
|
5143
5229
|
}
|
|
5144
5230
|
|
|
@@ -5157,6 +5243,16 @@ function assignPendingDispatchAgent(item, agentId, config) {
|
|
|
5157
5243
|
item.agentRole = agents[agentId]?.role || tempAgents.get(agentId)?.role || 'Agent';
|
|
5158
5244
|
delete item._agentBusySince;
|
|
5159
5245
|
delete item.skipReason;
|
|
5246
|
+
// Self-review ban (W-mp7jl5w3001e8c7e): re-render the deferred review prompt
|
|
5247
|
+
// for the actual reviewer. Centralized here so EVERY reassignment path
|
|
5248
|
+
// (initial ANY_AGENT routing, busy-agent reroute, unspawned-temp swap)
|
|
5249
|
+
// refreshes the prompt before spawn — otherwise the placeholder ANY_AGENT
|
|
5250
|
+
// prompt could leak into the agent context.
|
|
5251
|
+
refreshDeferredReviewPrompt(item, config);
|
|
5252
|
+
// Clear the pending-reason marker once the gate is satisfied so the
|
|
5253
|
+
// dashboard / log surfaces no longer show 'no_non_author_reviewer' for an
|
|
5254
|
+
// item that just got assigned a real reviewer.
|
|
5255
|
+
if (item._pendingReason === 'no_non_author_reviewer') delete item._pendingReason;
|
|
5160
5256
|
}
|
|
5161
5257
|
|
|
5162
5258
|
function clearPendingDispatchAgent(item) {
|
|
@@ -5175,11 +5271,15 @@ function persistPendingDispatchAgent(item) {
|
|
|
5175
5271
|
p.agent = item.agent;
|
|
5176
5272
|
p.agentName = item.agentName;
|
|
5177
5273
|
p.agentRole = item.agentRole;
|
|
5274
|
+
if (item.prompt) p.prompt = item.prompt;
|
|
5275
|
+
if (item.task) p.task = item.task;
|
|
5178
5276
|
} else {
|
|
5179
5277
|
delete p.agent;
|
|
5180
5278
|
delete p.agentName;
|
|
5181
5279
|
delete p.agentRole;
|
|
5182
5280
|
}
|
|
5281
|
+
if (item._pendingReason) p._pendingReason = item._pendingReason;
|
|
5282
|
+
else delete p._pendingReason;
|
|
5183
5283
|
delete p._agentBusySince;
|
|
5184
5284
|
delete p.skipReason;
|
|
5185
5285
|
}
|
|
@@ -5664,18 +5764,38 @@ async function tickInner() {
|
|
|
5664
5764
|
log('warn', `Duplicate dispatch ID ${item.id} in pending queue — skipping`);
|
|
5665
5765
|
continue;
|
|
5666
5766
|
}
|
|
5767
|
+
// Self-review ban (W-mp7jl5w3001e8c7e): defensive pre-spawn guard. A pending
|
|
5768
|
+
// review entry assigned to the PR author (legacy queue, manual edit, race)
|
|
5769
|
+
// must NOT spawn under the author. Clear the agent so it falls into the
|
|
5770
|
+
// ANY_AGENT re-route below (which excludes the author via
|
|
5771
|
+
// resolvePendingDispatchAgent). Operator overrides (meta.explicitAgent) are
|
|
5772
|
+
// honored — no clear, just a warning.
|
|
5773
|
+
if (
|
|
5774
|
+
routing.normalizeWorkType(item.type, WORK_TYPE.IMPLEMENT) === WORK_TYPE.REVIEW
|
|
5775
|
+
&& typeof item.agent === 'string'
|
|
5776
|
+
&& item.agent
|
|
5777
|
+
&& item.agent !== routing.ANY_AGENT
|
|
5778
|
+
&& item.meta?.pr?.agent
|
|
5779
|
+
&& String(item.agent).toLowerCase() === String(item.meta.pr.agent).toLowerCase()
|
|
5780
|
+
) {
|
|
5781
|
+
if (item.meta.explicitAgent) {
|
|
5782
|
+
log('warn', `Operator override: dispatching self-review for ${item.meta.pr.id || item.id} to author ${item.agent}`);
|
|
5783
|
+
} else {
|
|
5784
|
+
log('info', `Self-review guard: clearing author ${item.agent} from pending review ${item.id} (${item.meta.pr.id || ''}) — will re-route to a non-author reviewer`);
|
|
5785
|
+
clearPendingDispatchAgent(item);
|
|
5786
|
+
item.agent = routing.ANY_AGENT;
|
|
5787
|
+
item.meta.deferReviewerResolution = true;
|
|
5788
|
+
item._pendingReason = 'no_non_author_reviewer';
|
|
5789
|
+
try { persistPendingDispatchAgent(item); } catch (e) { log('warn', `Persist self-review clear for ${item.id} failed: ${e.message}`); }
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5667
5792
|
if (item.agent === routing.ANY_AGENT) {
|
|
5668
|
-
const routedAgent =
|
|
5793
|
+
const routedAgent = resolvePendingDispatchAgent(item, config);
|
|
5669
5794
|
if (!routedAgent) {
|
|
5670
5795
|
log('debug', `Pending dispatch ${item.id} is waiting for any available agent`);
|
|
5671
5796
|
continue;
|
|
5672
5797
|
}
|
|
5673
|
-
item
|
|
5674
|
-
item.agentName = config.agents[routedAgent]?.name || tempAgents.get(routedAgent)?.name || routedAgent;
|
|
5675
|
-
item.agentRole = config.agents[routedAgent]?.role || tempAgents.get(routedAgent)?.role || 'Agent';
|
|
5676
|
-
delete item._agentBusySince;
|
|
5677
|
-
delete item.skipReason;
|
|
5678
|
-
refreshDeferredWorkItemPrompt(item, config);
|
|
5798
|
+
assignPendingDispatchAgent(item, routedAgent, config);
|
|
5679
5799
|
try {
|
|
5680
5800
|
if (_isTickStale(myGeneration)) return;
|
|
5681
5801
|
mutateDispatch((dp) => {
|
|
@@ -5685,7 +5805,10 @@ async function tickInner() {
|
|
|
5685
5805
|
p.agentName = item.agentName;
|
|
5686
5806
|
p.agentRole = item.agentRole;
|
|
5687
5807
|
p.prompt = item.prompt;
|
|
5808
|
+
p.task = item.task;
|
|
5688
5809
|
if (item.meta) p.meta = item.meta;
|
|
5810
|
+
if (item._pendingReason) p._pendingReason = item._pendingReason;
|
|
5811
|
+
else delete p._pendingReason;
|
|
5689
5812
|
delete p._agentBusySince;
|
|
5690
5813
|
delete p.skipReason;
|
|
5691
5814
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1958",
|
|
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"
|
package/routing.md
CHANGED
|
@@ -32,7 +32,7 @@ Notes:
|
|
|
32
32
|
## Rules
|
|
33
33
|
|
|
34
34
|
1. **Eager by default** — spawn all agents who can start work, not one at a time
|
|
35
|
-
2. **Self-review is
|
|
35
|
+
2. **Self-review is forbidden** — auto review dispatch must always pick an agent other than the PR author. When no non-author reviewer is available, the dispatch waits in `dispatch.pending` with `_pendingReason: 'no_non_author_reviewer'` and is re-evaluated each tick (it promotes to active automatically once a non-author becomes idle). Explicit operator dispatch (e.g. a work item with `agent: <author>`) is honored as an override but is logged as a warning.
|
|
36
36
|
3. **Exploration gates implementation** — when exploring, finish before implementing
|
|
37
37
|
4. **Implementation informs PRD** — Lambert reads build summaries before writing PRD
|
|
38
38
|
5. **All rules in `notes.md` apply** — engine injects them into every playbook
|