@yemi33/minions 0.1.1657 → 0.1.1658
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 +8 -0
- package/dashboard.js +7 -10
- package/engine/copilot-models.json +1 -1
- package/engine/dispatch.js +1 -1
- package/engine/lifecycle.js +30 -61
- package/engine/pipeline.js +5 -3
- package/engine/routing.js +41 -10
- package/engine/scheduler.js +4 -2
- package/engine/timeout.js +2 -2
- package/engine.js +18 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dashboard.js
CHANGED
|
@@ -31,7 +31,7 @@ const dispatchMod = require('./engine/dispatch');
|
|
|
31
31
|
const steering = require('./engine/steering');
|
|
32
32
|
const os = require('os');
|
|
33
33
|
|
|
34
|
-
const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS, reopenWorkItem } = shared;
|
|
34
|
+
const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS, WORK_TYPE, reopenWorkItem } = shared;
|
|
35
35
|
const { getAgents, getAgentDetail, getPrdInfo, getWorkItems, getDispatchQueue,
|
|
36
36
|
getSkills, getInbox, getNotesWithMeta, getPullRequests,
|
|
37
37
|
getEngineLog, getMetrics, getKnowledgeBaseEntries, timeSince,
|
|
@@ -1392,7 +1392,7 @@ async function executeCCActions(actions) {
|
|
|
1392
1392
|
try {
|
|
1393
1393
|
switch (action.type) {
|
|
1394
1394
|
case 'dispatch': case 'fix': case 'implement': case 'explore': case 'review': case 'test': {
|
|
1395
|
-
const workType = action.workType || (action.type !== 'dispatch' ? action.type :
|
|
1395
|
+
const workType = routing.normalizeWorkType(action.workType || (action.type !== 'dispatch' ? action.type : WORK_TYPE.IMPLEMENT), WORK_TYPE.IMPLEMENT);
|
|
1396
1396
|
const id = 'W-' + shared.uid();
|
|
1397
1397
|
const project = action.project || '';
|
|
1398
1398
|
|
|
@@ -2733,22 +2733,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
2733
2733
|
}
|
|
2734
2734
|
const id = 'W-' + shared.uid();
|
|
2735
2735
|
const item = {
|
|
2736
|
-
id, title: body.title.trim(), type: body.type
|
|
2736
|
+
id, title: body.title.trim(), type: routing.normalizeWorkType(body.type, WORK_TYPE.IMPLEMENT),
|
|
2737
2737
|
priority: body.priority || 'medium', description: body.description || '',
|
|
2738
2738
|
status: WI_STATUS.PENDING, created: new Date().toISOString(), createdBy: 'dashboard',
|
|
2739
2739
|
};
|
|
2740
2740
|
if (body.scope) item.scope = body.scope;
|
|
2741
|
-
// Agent assignment normalization:
|
|
2742
|
-
//
|
|
2743
|
-
//
|
|
2744
|
-
// by setting `item.agent`. The engine reads `item.agent || resolveAgent(…)`,
|
|
2745
|
-
// so a hard-pinned item bypasses routing entirely and queues until that
|
|
2746
|
-
// exact agent is free. Multi-agent arrays remain `item.agents` (hints
|
|
2747
|
-
// for resolveAgent or fan-out scope).
|
|
2741
|
+
// Agent assignment normalization: `agent` and `agents` are routing hints.
|
|
2742
|
+
// Use agentLock/hardAgent only for the rare case where an item must wait
|
|
2743
|
+
// for one exact agent instead of falling through to another idle agent.
|
|
2748
2744
|
const _agentsArr = Array.isArray(body.agents) ? body.agents.filter(Boolean) : (typeof body.agents === 'string' && body.agents ? [body.agents] : []);
|
|
2749
2745
|
if (body.agent) item.agent = String(body.agent);
|
|
2750
2746
|
else if (_agentsArr.length === 1 && body.scope !== 'fan-out') item.agent = String(_agentsArr[0]);
|
|
2751
2747
|
if (_agentsArr.length > 0) item.agents = _agentsArr;
|
|
2748
|
+
if (body.agentLock === true || body.hardAgent === true) item.agentLock = true;
|
|
2752
2749
|
if (body.references) item.references = body.references;
|
|
2753
2750
|
if (body.acceptanceCriteria) item.acceptanceCriteria = body.acceptanceCriteria;
|
|
2754
2751
|
if (body.skipPr) item.skipPr = true;
|
package/engine/dispatch.js
CHANGED
|
@@ -81,7 +81,7 @@ function addToDispatch(item) {
|
|
|
81
81
|
let added = false;
|
|
82
82
|
mutateDispatch((dispatch) => {
|
|
83
83
|
// Dedup: skip if same work item ID is already pending or active
|
|
84
|
-
const wiId = item.meta?.item?.id;
|
|
84
|
+
const wiId = item.meta?.source === 'central-work-item-fanout' ? null : item.meta?.item?.id;
|
|
85
85
|
if (wiId) {
|
|
86
86
|
const existing = [...dispatch.pending, ...(dispatch.active || [])].find(d => d.meta?.item?.id === wiId);
|
|
87
87
|
if (existing) {
|
package/engine/lifecycle.js
CHANGED
|
@@ -1820,70 +1820,39 @@ function normalizeCompletionStatus(status) {
|
|
|
1820
1820
|
return String(status || '').trim().toLowerCase().replace(/[\s_]+/g, '-');
|
|
1821
1821
|
}
|
|
1822
1822
|
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1823
|
+
// Trust the agent's explicit structured `status` field as the only signal that
|
|
1824
|
+
// a completion is non-terminal. Earlier versions also scanned the agent's
|
|
1825
|
+
// resultSummary prose with regex (looking for "pending", "in progress",
|
|
1826
|
+
// "partial", "wake up", etc.), but that produced false positives on benign
|
|
1827
|
+
// phrases like "I checked the pending PRs" or "build is in progress on CI"
|
|
1828
|
+
// and burned 3-9 minutes of agent time per false-positive retry.
|
|
1829
|
+
//
|
|
1830
|
+
// Both structured signals (the JSON completion report at MINIONS_COMPLETION_REPORT
|
|
1831
|
+
// and the fenced ```completion block in stdout) carry a `status` field. If the
|
|
1832
|
+
// agent explicitly says they're not done, honor it; otherwise accept the
|
|
1833
|
+
// dispatch. The PR attachment contract still catches silent-failure cases
|
|
1834
|
+
// for PR-producing work.
|
|
1835
|
+
const NON_TERMINAL_COMPLETION_STATUSES = new Set([
|
|
1836
|
+
'partial', 'partially-complete', 'in-progress', 'pending', 'deferred',
|
|
1837
|
+
'blocked', 'incomplete', 'to-be-continued',
|
|
1838
|
+
'failed', 'failure', 'error',
|
|
1839
|
+
]);
|
|
1840
|
+
|
|
1841
|
+
function detectNonTerminalResultSummary(_resultSummary, structuredCompletion, completionReport) {
|
|
1842
|
+
const candidates = [completionReport?.status, structuredCompletion?.status];
|
|
1843
|
+
for (const status of candidates) {
|
|
1844
|
+
const norm = normalizeCompletionStatus(status);
|
|
1845
|
+
if (!norm) continue;
|
|
1846
|
+
if (NON_TERMINAL_COMPLETION_STATUSES.has(norm)) {
|
|
1847
|
+
const isFailure = norm === 'failed' || norm === 'failure' || norm === 'error';
|
|
1842
1848
|
return {
|
|
1843
|
-
phrase: `status:${
|
|
1844
|
-
reason:
|
|
1849
|
+
phrase: `status:${status}`,
|
|
1850
|
+
reason: isFailure
|
|
1851
|
+
? `Nonterminal completion summary: structured status is '${status}', not a successful terminal state`
|
|
1852
|
+
: `Nonterminal completion summary: structured status is '${status}'`,
|
|
1845
1853
|
};
|
|
1846
1854
|
}
|
|
1847
|
-
if (/^(?:fail|failed|failure|error)/.test(completionStatus)) {
|
|
1848
|
-
return {
|
|
1849
|
-
phrase: `status:${structuredCompletion.status}`,
|
|
1850
|
-
reason: `Nonterminal completion summary: structured status is '${structuredCompletion.status}', not a successful terminal state`,
|
|
1851
|
-
};
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
if (structuredCompletion?.pending && !isTerminalPendingValue(structuredCompletion.pending)) {
|
|
1856
|
-
return {
|
|
1857
|
-
phrase: 'pending',
|
|
1858
|
-
reason: `Nonterminal completion summary: pending work remains (${String(structuredCompletion.pending).slice(0, 160)})`,
|
|
1859
|
-
};
|
|
1860
1855
|
}
|
|
1861
|
-
|
|
1862
|
-
const text = String(resultSummary || '').replace(/\r/g, '').trim();
|
|
1863
|
-
if (!text) return null;
|
|
1864
|
-
|
|
1865
|
-
const patterns = [
|
|
1866
|
-
{ phrase: 'still running', re: /\b(?:still|currently|continues?\s+to\s+be)\s+(?:running|ongoing|in\s+progress)\b/i },
|
|
1867
|
-
{ phrase: 'will check later', re: /\b(?:i(?:'|’)ll|i\s+will|we(?:'|’)ll|we\s+will|will)\s+(?:check|verify|review|follow\s+up|revisit)\s+(?:again\s+)?(?:later|soon|in\b|after\b|when\b)/i },
|
|
1868
|
-
{ phrase: 'wake up', re: /\bwake(?:\s|-)?up\b|\bwake\b.*\b(?:check|verify|review)\b/i },
|
|
1869
|
-
{ phrase: 'not yet complete', re: /\b(?:not\s+yet|isn(?:'|’)t|not|incomplete|not\s+fully|not\s+completely)\s+(?:complete|completed|done|finished|validated|verified)\b/i },
|
|
1870
|
-
{ phrase: 'partial', re: /\bpartial(?:ly)?\b/i },
|
|
1871
|
-
{ phrase: 'to be continued', re: /\bto\s+be\s+continued\b|\btbc\b/i },
|
|
1872
|
-
{ phrase: 'in progress', re: /\bin\s+progress\b|\bongoing\b|\bincomplete\b/i },
|
|
1873
|
-
];
|
|
1874
|
-
for (const { phrase, re } of patterns) {
|
|
1875
|
-
if (re.test(text)) {
|
|
1876
|
-
return { phrase, reason: `Nonterminal completion summary: matched '${phrase}'` };
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
const pendingLines = text.split('\n').filter(line => /\bpending\b/i.test(line));
|
|
1881
|
-
for (const line of pendingLines) {
|
|
1882
|
-
if (!isTerminalPendingLine(line)) {
|
|
1883
|
-
return { phrase: 'pending', reason: `Nonterminal completion summary: matched 'pending'` };
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
1856
|
return null;
|
|
1888
1857
|
}
|
|
1889
1858
|
|
|
@@ -2256,7 +2225,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
2256
2225
|
|
|
2257
2226
|
let completionContractFailure = null;
|
|
2258
2227
|
if (effectiveSuccess && meta?.item?.id && !skipDoneStatus) {
|
|
2259
|
-
const nonTerminalCompletion = detectNonTerminalResultSummary(completionGateSummary, structuredCompletion);
|
|
2228
|
+
const nonTerminalCompletion = detectNonTerminalResultSummary(completionGateSummary, structuredCompletion, reportCompletion);
|
|
2260
2229
|
if (nonTerminalCompletion) {
|
|
2261
2230
|
skipDoneStatus = true;
|
|
2262
2231
|
const reason = deferNonTerminalCompletion(meta, nonTerminalCompletion);
|
package/engine/pipeline.js
CHANGED
|
@@ -9,6 +9,7 @@ const path = require('path');
|
|
|
9
9
|
const shared = require('./shared');
|
|
10
10
|
const queries = require('./queries');
|
|
11
11
|
const { safeJson, safeWrite, safeRead, safeReadDir, uid, log, ts, dateStamp, mutateJsonFileLocked, mutateWorkItems, slugify, formatTranscriptEntry, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, ENGINE_DEFAULTS, MINIONS_DIR } = shared;
|
|
12
|
+
const routing = require('./routing');
|
|
12
13
|
const http = require('http');
|
|
13
14
|
const { parseCronExpr, shouldRunNow } = require('./scheduler');
|
|
14
15
|
|
|
@@ -306,10 +307,11 @@ function executeTaskStage(stage, stageState, run, config) {
|
|
|
306
307
|
id,
|
|
307
308
|
title: item.title || stage.title,
|
|
308
309
|
description: item.description || stage.description || '',
|
|
309
|
-
type: item.type || stage.taskType
|
|
310
|
+
type: routing.normalizeWorkType(item.type || stage.taskType, WORK_TYPE.EXPLORE),
|
|
310
311
|
priority: item.priority || stage.priority || 'medium',
|
|
311
|
-
//
|
|
312
|
+
// Agent is a soft routing hint unless agentLock/hardAgent is set.
|
|
312
313
|
...(item.agent || stage.agent ? { agent: item.agent || stage.agent } : {}),
|
|
314
|
+
...(item.agentLock === true || stage.agentLock === true || item.hardAgent === true || stage.hardAgent === true ? { agentLock: true } : {}),
|
|
313
315
|
status: WI_STATUS.PENDING,
|
|
314
316
|
created: ts(),
|
|
315
317
|
createdBy: 'pipeline:' + run.pipelineId,
|
|
@@ -587,7 +589,7 @@ function executeMergePrsStage(stage, stageState, run, config) {
|
|
|
587
589
|
|
|
588
590
|
function executeScheduleStage(stage, stageState, config) {
|
|
589
591
|
// Create/update schedules in config
|
|
590
|
-
const schedules = stage.schedules || [{ id: stage.id + '-sched', cron: stage.cron, title: stage.title, type: stage.taskType
|
|
592
|
+
const schedules = stage.schedules || [{ id: stage.id + '-sched', cron: stage.cron, title: stage.title, type: routing.normalizeWorkType(stage.taskType, WORK_TYPE.IMPLEMENT) }];
|
|
591
593
|
// Write to config via shared
|
|
592
594
|
for (const sched of schedules) {
|
|
593
595
|
const existing = (config.schedules || []).find(s => s.id === sched.id);
|
package/engine/routing.js
CHANGED
|
@@ -8,7 +8,7 @@ const path = require('path');
|
|
|
8
8
|
const shared = require('./shared');
|
|
9
9
|
const queries = require('./queries');
|
|
10
10
|
|
|
11
|
-
const { safeJson, safeRead, log, ts } = shared;
|
|
11
|
+
const { safeJson, safeRead, log, ts, WORK_TYPE } = shared;
|
|
12
12
|
const { ENGINE_DIR, DISPATCH_PATH } = queries;
|
|
13
13
|
|
|
14
14
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
@@ -116,12 +116,43 @@ function setTempBudget(n) {
|
|
|
116
116
|
}
|
|
117
117
|
function getTempBudget() { return _tempBudget; }
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
function normalizeWorkType(workType, fallback = WORK_TYPE.IMPLEMENT) {
|
|
120
|
+
const type = String(workType || fallback || '').trim();
|
|
121
|
+
if (!type) return fallback;
|
|
122
|
+
return type;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function routeForWorkType(workType) {
|
|
126
|
+
const routes = getRoutingTableCached();
|
|
127
|
+
return routes[normalizeWorkType(workType)] || routes[WORK_TYPE.IMPLEMENT] || { preferred: '_any_', fallback: '_any_' };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isAgentHardPinned(item) {
|
|
131
|
+
return !!(item && (
|
|
132
|
+
item.agentLock === true ||
|
|
133
|
+
item.agent_locked === true ||
|
|
134
|
+
item.hardAgent === true ||
|
|
135
|
+
item.hard_agent === true
|
|
136
|
+
));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getHardPinnedAgent(item, agents = null) {
|
|
140
|
+
if (!isAgentHardPinned(item) || !item?.agent) return null;
|
|
141
|
+
const normalized = normalizeAgentHints(item.agent, null, agents);
|
|
142
|
+
return normalized[0] || null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Centralizes the work-item shape used to derive routing hints. `agent`,
|
|
146
|
+
// `preferred_agent`, and `agents` are soft preferences unless an explicit
|
|
147
|
+
// hard-pin flag is present (agentLock / hardAgent variants).
|
|
122
148
|
function extractAgentHints(item) {
|
|
123
149
|
if (!item || typeof item !== 'object') return null;
|
|
124
|
-
|
|
150
|
+
const hints = [];
|
|
151
|
+
if (!isAgentHardPinned(item) && item.agent) hints.push(item.agent);
|
|
152
|
+
if (item.preferred_agent) hints.push(item.preferred_agent);
|
|
153
|
+
if (Array.isArray(item.agents)) hints.push(...item.agents);
|
|
154
|
+
else if (item.agents) hints.push(item.agents);
|
|
155
|
+
return hints.length > 0 ? hints : null;
|
|
125
156
|
}
|
|
126
157
|
|
|
127
158
|
// Normalize a list of agent-hint inputs. Accepts:
|
|
@@ -158,8 +189,7 @@ function normalizeAgentHints(agentHints, authorAgent = null, agents = null) {
|
|
|
158
189
|
|
|
159
190
|
function resolveAgent(workType, config, opts = {}) {
|
|
160
191
|
const { authorAgent = null, agentHints = null } = opts || {};
|
|
161
|
-
const
|
|
162
|
-
const route = routes[workType] || routes['implement'] || { preferred: '_any_', fallback: '_any_' };
|
|
192
|
+
const route = routeForWorkType(workType);
|
|
163
193
|
const agents = config.agents || {};
|
|
164
194
|
|
|
165
195
|
// Resolve _author_ token
|
|
@@ -191,7 +221,6 @@ function resolveAgent(workType, config, opts = {}) {
|
|
|
191
221
|
for (const id of hintedAgents) {
|
|
192
222
|
if (isAvailable(id)) { _claimedAgents.add(id); return id; }
|
|
193
223
|
}
|
|
194
|
-
return null;
|
|
195
224
|
}
|
|
196
225
|
|
|
197
226
|
// Resolve _any_ token — pick any available agent (#480)
|
|
@@ -230,8 +259,7 @@ function resolveAgent(workType, config, opts = {}) {
|
|
|
230
259
|
|
|
231
260
|
function resolveAgentReservation(workType, config, opts = {}) {
|
|
232
261
|
const { authorAgent = null, agentHints = null } = opts || {};
|
|
233
|
-
const
|
|
234
|
-
const route = routes[workType] || routes['implement'] || { preferred: '_any_', fallback: '_any_' };
|
|
262
|
+
const route = routeForWorkType(workType);
|
|
235
263
|
const agents = config.agents || {};
|
|
236
264
|
const hintedAgents = normalizeAgentHints(agentHints, authorAgent, agents);
|
|
237
265
|
|
|
@@ -274,6 +302,9 @@ module.exports = {
|
|
|
274
302
|
isAgentIdle,
|
|
275
303
|
normalizeAgentHints,
|
|
276
304
|
extractAgentHints,
|
|
305
|
+
isAgentHardPinned,
|
|
306
|
+
getHardPinnedAgent,
|
|
307
|
+
normalizeWorkType,
|
|
277
308
|
_claimedAgents,
|
|
278
309
|
resetClaimedAgents,
|
|
279
310
|
resolveAgent,
|
package/engine/scheduler.js
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
const fs = require('fs');
|
|
25
25
|
const path = require('path');
|
|
26
26
|
const shared = require('./shared');
|
|
27
|
-
const
|
|
27
|
+
const routing = require('./routing');
|
|
28
|
+
const { safeJson, safeWrite, mutateJsonFileLocked, ts, dateStamp, WI_STATUS, WORK_TYPE } = shared;
|
|
28
29
|
|
|
29
30
|
const SCHEDULE_RUNS_PATH = path.join(shared.MINIONS_DIR, 'engine', 'schedule-runs.json');
|
|
30
31
|
|
|
@@ -167,13 +168,14 @@ function discoverScheduledWork(config) {
|
|
|
167
168
|
work.push({
|
|
168
169
|
id: workItemId,
|
|
169
170
|
title: resolveScheduleTemplateVars(sched.title),
|
|
170
|
-
type: sched.type
|
|
171
|
+
type: routing.normalizeWorkType(sched.type, WORK_TYPE.IMPLEMENT),
|
|
171
172
|
priority: sched.priority || 'medium',
|
|
172
173
|
description: resolveScheduleTemplateVars(sched.description || sched.title),
|
|
173
174
|
status: WI_STATUS.PENDING,
|
|
174
175
|
created: ts(),
|
|
175
176
|
createdBy: 'scheduler',
|
|
176
177
|
agent: sched.agent || null,
|
|
178
|
+
...(sched.agentLock === true || sched.hardAgent === true ? { agentLock: true } : {}),
|
|
177
179
|
project: sched.project || null,
|
|
178
180
|
_scheduleId: sched.id,
|
|
179
181
|
});
|
package/engine/timeout.js
CHANGED
|
@@ -147,7 +147,7 @@ function checkTimeouts(config) {
|
|
|
147
147
|
const engineRestartGraceUntil = engine().engineRestartGraceUntil;
|
|
148
148
|
const engineRestartGraceExempt = engine().engineRestartGraceExempt;
|
|
149
149
|
const { completeDispatch } = dispatch();
|
|
150
|
-
const { runPostCompletionHooks, parseAgentOutput, parseStructuredCompletion, detectNonTerminalResultSummary } = require('./lifecycle');
|
|
150
|
+
const { runPostCompletionHooks, parseAgentOutput, parseStructuredCompletion, parseCompletionReportFile, detectNonTerminalResultSummary } = require('./lifecycle');
|
|
151
151
|
|
|
152
152
|
const timeout = config.engine?.agentTimeout || ENGINE_DEFAULTS.agentTimeout;
|
|
153
153
|
const defaultStaleOrphanTimeout = config.engine?.heartbeatTimeout || ENGINE_DEFAULTS.heartbeatTimeout;
|
|
@@ -265,7 +265,7 @@ function checkTimeouts(config) {
|
|
|
265
265
|
outputResultSummary = parseAgentOutput(fullLogForHooks, runtimeName).resultSummary || '';
|
|
266
266
|
const gateSummary = outputResultSummary || (!fullLogForHooks.includes('"type":') ? fullLogForHooks : '');
|
|
267
267
|
completionDetection = isSuccess
|
|
268
|
-
? detectNonTerminalResultSummary(gateSummary, parseStructuredCompletion(fullLogForHooks, runtimeName))
|
|
268
|
+
? detectNonTerminalResultSummary(gateSummary, parseStructuredCompletion(fullLogForHooks, runtimeName), parseCompletionReportFile(item))
|
|
269
269
|
: null;
|
|
270
270
|
} catch (e) { log('warn', 'completion summary gate: ' + e.message); }
|
|
271
271
|
|
package/engine.js
CHANGED
|
@@ -2644,7 +2644,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
2644
2644
|
skipped.gated++; continue;
|
|
2645
2645
|
}
|
|
2646
2646
|
|
|
2647
|
-
let workType = item.type
|
|
2647
|
+
let workType = routing.normalizeWorkType(item.type, WORK_TYPE.IMPLEMENT);
|
|
2648
2648
|
if (workType === WORK_TYPE.IMPLEMENT && (item.complexity === 'large' || item.estimated_complexity === 'large')) {
|
|
2649
2649
|
workType = WORK_TYPE.IMPLEMENT_LARGE;
|
|
2650
2650
|
}
|
|
@@ -2655,16 +2655,17 @@ function discoverFromWorkItems(config, project) {
|
|
|
2655
2655
|
needsWrite = true;
|
|
2656
2656
|
}
|
|
2657
2657
|
const agentHints = routing.extractAgentHints(item);
|
|
2658
|
-
const
|
|
2659
|
-
let agentId =
|
|
2658
|
+
const hardPinRequested = routing.isAgentHardPinned(item);
|
|
2659
|
+
let agentId = routing.getHardPinnedAgent(item, config.agents || {})
|
|
2660
|
+
|| (!hardPinRequested ? resolveAgent(workType, config, { agentHints }) : null);
|
|
2660
2661
|
const cfgAgents = config.agents || {};
|
|
2661
2662
|
const budgetBlocked = Object.keys(cfgAgents).some(id => {
|
|
2662
2663
|
const b = cfgAgents[id].monthlyBudgetUsd;
|
|
2663
2664
|
return b && b > 0 && getMonthlySpend(id) >= b && isAgentIdle(id);
|
|
2664
2665
|
});
|
|
2665
2666
|
if (!agentId) {
|
|
2666
|
-
if (!budgetBlocked && !
|
|
2667
|
-
agentId = resolveAgentReservation(workType, config);
|
|
2667
|
+
if (!budgetBlocked && !hardPinRequested) {
|
|
2668
|
+
agentId = resolveAgentReservation(workType, config, { agentHints });
|
|
2668
2669
|
}
|
|
2669
2670
|
if (agentId) {
|
|
2670
2671
|
delete item._pendingReason;
|
|
@@ -3104,7 +3105,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3104
3105
|
}
|
|
3105
3106
|
if (isOnCooldown(key, 0)) continue;
|
|
3106
3107
|
|
|
3107
|
-
const workType = item.type
|
|
3108
|
+
const workType = routing.normalizeWorkType(item.type, WORK_TYPE.IMPLEMENT);
|
|
3108
3109
|
const isFanOut = item.scope === 'fan-out';
|
|
3109
3110
|
|
|
3110
3111
|
if (isFanOut) {
|
|
@@ -3185,10 +3186,10 @@ function discoverCentralWorkItems(config) {
|
|
|
3185
3186
|
} else {
|
|
3186
3187
|
// ─── Normal: single agent dispatch ──────────────────────────────
|
|
3187
3188
|
const agentHints = routing.extractAgentHints(item);
|
|
3188
|
-
const
|
|
3189
|
-
const agentId = item.
|
|
3190
|
-
|| resolveAgent(workType, config, { agentHints })
|
|
3191
|
-
|| (!
|
|
3189
|
+
const hardPinRequested = routing.isAgentHardPinned(item);
|
|
3190
|
+
const agentId = routing.getHardPinnedAgent(item, config.agents || {})
|
|
3191
|
+
|| (!hardPinRequested ? resolveAgent(workType, config, { agentHints }) : null)
|
|
3192
|
+
|| (!hardPinRequested ? resolveAgentReservation(workType, config, { agentHints }) : null);
|
|
3192
3193
|
if (!agentId) continue;
|
|
3193
3194
|
|
|
3194
3195
|
const agentName = config.agents[agentId]?.name || agentId;
|
|
@@ -3862,7 +3863,7 @@ async function tickInner() {
|
|
|
3862
3863
|
// be of type string. Received undefined` and re-queues — every tick. Try to
|
|
3863
3864
|
// resolve a fallback via routing; if none is available, skip this tick.
|
|
3864
3865
|
if (!item.agent || typeof item.agent !== 'string') {
|
|
3865
|
-
const fallback = resolveAgent(item.type
|
|
3866
|
+
const fallback = resolveAgent(routing.normalizeWorkType(item.type, WORK_TYPE.IMPLEMENT), config, { agentHints: routing.extractAgentHints(item.meta?.item) });
|
|
3866
3867
|
if (!fallback) {
|
|
3867
3868
|
log('warn', `Pending dispatch ${item.id} has no agent and routing returned no fallback — skipping`);
|
|
3868
3869
|
continue;
|
|
@@ -3893,7 +3894,7 @@ async function tickInner() {
|
|
|
3893
3894
|
// them eagerly before the busy check so an idle named agent can pick up.
|
|
3894
3895
|
const isUnspawnedTemp = item.agent?.startsWith('temp-') && !busyAgents.has(item.agent);
|
|
3895
3896
|
if (isUnspawnedTemp) {
|
|
3896
|
-
const altAgent = resolveAgent(item.type, config);
|
|
3897
|
+
const altAgent = resolveAgent(routing.normalizeWorkType(item.type, WORK_TYPE.IMPLEMENT), config);
|
|
3897
3898
|
if (altAgent && altAgent !== item.agent) {
|
|
3898
3899
|
const prevAgent = item.agent;
|
|
3899
3900
|
item.agent = altAgent;
|
|
@@ -3920,12 +3921,12 @@ async function tickInner() {
|
|
|
3920
3921
|
// Agent busy reassignment: if item has been waiting on a busy agent past the threshold,
|
|
3921
3922
|
// try to find an alternative agent via routing. Skip explicitly assigned items.
|
|
3922
3923
|
const reassignMs = config.engine?.agentBusyReassignMs ?? ENGINE_DEFAULTS.agentBusyReassignMs;
|
|
3923
|
-
const
|
|
3924
|
-
if (!
|
|
3924
|
+
const isHardPinned = routing.isAgentHardPinned(item.meta?.item);
|
|
3925
|
+
if (!isHardPinned && reassignMs > 0 && item._agentBusySince) {
|
|
3925
3926
|
const busySinceMs = new Date(item._agentBusySince).getTime();
|
|
3926
3927
|
if (Date.now() - busySinceMs > reassignMs) {
|
|
3927
3928
|
const originalAgent = item.agent;
|
|
3928
|
-
const altAgent = resolveAgent(item.type, config);
|
|
3929
|
+
const altAgent = resolveAgent(routing.normalizeWorkType(item.type, WORK_TYPE.IMPLEMENT), config, { agentHints: routing.extractAgentHints(item.meta?.item) });
|
|
3929
3930
|
if (altAgent && altAgent !== originalAgent && !busyAgents.has(altAgent)) {
|
|
3930
3931
|
log('info', `Reassigning ${item.id} from ${originalAgent} to ${altAgent} — agent busy > ${reassignMs}ms`);
|
|
3931
3932
|
item.agent = altAgent;
|
|
@@ -3959,14 +3960,12 @@ async function tickInner() {
|
|
|
3959
3960
|
// Branch mutex: skip items targeting a branch already locked by an active or newly-dispatched task
|
|
3960
3961
|
const itemBranch = item.meta?.branch ? sanitizeBranch(item.meta.branch) : null;
|
|
3961
3962
|
if (itemBranch && lockedBranches.has(itemBranch)) continue;
|
|
3962
|
-
|
|
3963
|
-
const isExplicitAssignment = !!item.meta?.item?.agent;
|
|
3964
|
-
if (!isExplicitAssignment && generalSlots <= 0) continue;
|
|
3963
|
+
if (generalSlots <= 0) continue;
|
|
3965
3964
|
seenPendingIds.add(item.id);
|
|
3966
3965
|
toDispatch.push(item);
|
|
3967
3966
|
busyAgents.add(item.agent);
|
|
3968
3967
|
if (itemBranch) lockedBranches.add(itemBranch);
|
|
3969
|
-
|
|
3968
|
+
generalSlots--;
|
|
3970
3969
|
}
|
|
3971
3970
|
|
|
3972
3971
|
// Dispatch items — spawnAgent moves each from pending→active on disk.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1658",
|
|
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"
|