@yemi33/minions 0.1.99 → 0.1.100
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 +6 -1
- package/engine/cleanup.js +1 -0
- package/engine/consolidation.js +18 -9
- package/engine/dispatch.js +3 -1
- package/engine/queries.js +5 -5
- package/engine/shared.js +1 -1
- package/engine.js +72 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.100 (2026-04-01)
|
|
4
4
|
|
|
5
5
|
### Engine
|
|
6
|
+
- engine.js
|
|
6
7
|
- engine/ado.js
|
|
8
|
+
- engine/cleanup.js
|
|
9
|
+
- engine/consolidation.js
|
|
10
|
+
- engine/dispatch.js
|
|
7
11
|
- engine/github.js
|
|
8
12
|
- engine/lifecycle.js
|
|
9
13
|
- engine/preflight.js
|
|
14
|
+
- engine/queries.js
|
|
10
15
|
- engine/shared.js
|
|
11
16
|
|
|
12
17
|
### Other
|
package/engine/cleanup.js
CHANGED
package/engine/consolidation.js
CHANGED
|
@@ -179,8 +179,8 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
179
179
|
|
|
180
180
|
proc.on('close', (code) => {
|
|
181
181
|
clearTimeout(timeout);
|
|
182
|
-
safeUnlink(promptPath);
|
|
183
|
-
safeUnlink(sysPromptPath);
|
|
182
|
+
try { safeUnlink(promptPath); } catch (err) { log('warn', `Temp file cleanup failed: ${promptPath} — ${err.message}`); }
|
|
183
|
+
try { safeUnlink(sysPromptPath); } catch (err) { log('warn', `Temp file cleanup failed: ${sysPromptPath} — ${err.message}`); }
|
|
184
184
|
|
|
185
185
|
const parsed = parseStreamJsonOutput(stdout);
|
|
186
186
|
const extractedText = parsed.text;
|
|
@@ -231,8 +231,8 @@ function consolidateWithLLM(items, existingNotes, files, config) {
|
|
|
231
231
|
proc.on('error', (err) => {
|
|
232
232
|
clearTimeout(timeout);
|
|
233
233
|
log('warn', `LLM consolidation spawn error: ${err.message} — falling back to regex`);
|
|
234
|
-
safeUnlink(promptPath);
|
|
235
|
-
safeUnlink(sysPromptPath);
|
|
234
|
+
try { safeUnlink(promptPath); } catch (unlinkErr) { log('warn', `Temp file cleanup failed: ${promptPath} — ${unlinkErr.message}`); }
|
|
235
|
+
try { safeUnlink(sysPromptPath); } catch (unlinkErr) { log('warn', `Temp file cleanup failed: ${sysPromptPath} — ${unlinkErr.message}`); }
|
|
236
236
|
consolidateWithRegex(items, files);
|
|
237
237
|
_clearProcessingState();
|
|
238
238
|
});
|
|
@@ -296,13 +296,15 @@ function consolidateWithRegex(items, files) {
|
|
|
296
296
|
const deduped = [];
|
|
297
297
|
for (const insight of allInsights) {
|
|
298
298
|
const fpWords = insight.fingerprint.split(' ').filter(w => w.length > 4).slice(0, 5);
|
|
299
|
-
|
|
299
|
+
// Use word-boundary regex to avoid substring false positives (e.g. 'fix' matching 'prefix')
|
|
300
|
+
if (fpWords.length >= 3 && fpWords.every(w => new RegExp(`\\b${w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(existingNotes))) continue;
|
|
300
301
|
const existing = seen.get(insight.fingerprint);
|
|
301
302
|
if (existing) { if (!existing.sources.includes(insight.agent)) existing.sources.push(insight.agent); continue; }
|
|
302
303
|
let isDup = false;
|
|
303
304
|
for (const [fp, entry] of seen) {
|
|
304
|
-
|
|
305
|
-
|
|
305
|
+
// Filter to meaningful words (>4 chars) to avoid short-word false positives like 'fix' vs 'prefix'
|
|
306
|
+
const a = new Set(fp.split(' ').filter(w => w.length > 2)), b = new Set(insight.fingerprint.split(' ').filter(w => w.length > 2));
|
|
307
|
+
// Require at least 3 meaningful words in both fingerprints for similarity check
|
|
306
308
|
if (a.size >= 3 && b.size >= 3 && [...a].filter(w => b.has(w)).length / Math.max(a.size, b.size) > 0.7) {
|
|
307
309
|
if (!entry.sources.includes(insight.agent)) entry.sources.push(insight.agent); isDup = true; break;
|
|
308
310
|
}
|
|
@@ -352,7 +354,9 @@ function classifyToKnowledgeBase(items) {
|
|
|
352
354
|
if (!fs.existsSync(KNOWLEDGE_DIR)) fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
|
|
353
355
|
|
|
354
356
|
const categoryDirs = {};
|
|
355
|
-
|
|
357
|
+
// Include 'general' as fallback category even if not in KB_CATEGORIES
|
|
358
|
+
const allCategories = KB_CATEGORIES.includes('general') ? KB_CATEGORIES : [...KB_CATEGORIES, 'general'];
|
|
359
|
+
for (const cat of allCategories) {
|
|
356
360
|
categoryDirs[cat] = path.join(KNOWLEDGE_DIR, cat);
|
|
357
361
|
if (!fs.existsSync(categoryDirs[cat])) fs.mkdirSync(categoryDirs[cat], { recursive: true });
|
|
358
362
|
}
|
|
@@ -360,7 +364,12 @@ function classifyToKnowledgeBase(items) {
|
|
|
360
364
|
let classified = 0;
|
|
361
365
|
for (const item of items) {
|
|
362
366
|
const content = item.content || '';
|
|
363
|
-
const
|
|
367
|
+
const rawCategory = classifyInboxItem(item.name, content);
|
|
368
|
+
// Fallback to 'general' if the classified category isn't in our known category map
|
|
369
|
+
const category = categoryDirs[rawCategory] ? rawCategory : 'general';
|
|
370
|
+
if (rawCategory !== category) {
|
|
371
|
+
log('warn', `Unknown KB category '${rawCategory}' for ${item.name} — falling back to 'general'`);
|
|
372
|
+
}
|
|
364
373
|
|
|
365
374
|
const agentMatch = item.name.match(/^(\w+)-/);
|
|
366
375
|
const agent = agentMatch ? agentMatch[1] : 'unknown';
|
package/engine/dispatch.js
CHANGED
|
@@ -38,6 +38,7 @@ function addToDispatch(item) {
|
|
|
38
38
|
item.created_at = ts();
|
|
39
39
|
mutateDispatch((dispatch) => {
|
|
40
40
|
dispatch.pending.push(item);
|
|
41
|
+
return dispatch;
|
|
41
42
|
});
|
|
42
43
|
log('info', `Queued dispatch: ${item.id} (${item.type} → ${item.agent})`);
|
|
43
44
|
return item.id;
|
|
@@ -79,7 +80,7 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
|
|
|
79
80
|
if (idx >= 0) item = dispatch.pending.splice(idx, 1)[0];
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
if (!item) return;
|
|
83
|
+
if (!item) return dispatch;
|
|
83
84
|
item.completed_at = ts();
|
|
84
85
|
item.result = result;
|
|
85
86
|
if (reason) item.reason = reason;
|
|
@@ -89,6 +90,7 @@ function completeDispatch(id, result = 'success', reason = '', resultSummary = '
|
|
|
89
90
|
dispatch.completed = dispatch.completed.slice(-99);
|
|
90
91
|
}
|
|
91
92
|
dispatch.completed.push(item);
|
|
93
|
+
return dispatch;
|
|
92
94
|
});
|
|
93
95
|
|
|
94
96
|
if (item) {
|
package/engine/queries.js
CHANGED
|
@@ -540,8 +540,8 @@ function getPrdInfo(config) {
|
|
|
540
540
|
const planFiles = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
541
541
|
for (const pf of planFiles) {
|
|
542
542
|
try {
|
|
543
|
-
const plan =
|
|
544
|
-
if (!plan.missing_features) continue;
|
|
543
|
+
const plan = safeJson(path.join(dir, pf));
|
|
544
|
+
if (!plan || !plan.missing_features) continue;
|
|
545
545
|
const stat = fs.statSync(path.join(dir, pf));
|
|
546
546
|
if (!latestStat || stat.mtimeMs > latestStat.mtimeMs) latestStat = stat;
|
|
547
547
|
// Staleness: compare source plan mtime to recorded sourcePlanModifiedAt
|
|
@@ -577,13 +577,13 @@ function getPrdInfo(config) {
|
|
|
577
577
|
for (const project of projects) {
|
|
578
578
|
try {
|
|
579
579
|
const workItems = safeJson(projectWorkItemsPath(project)) || [];
|
|
580
|
-
for (const wi of workItems) { if (wi.sourcePlan) wiById[wi.id] = wi; }
|
|
580
|
+
for (const wi of workItems) { if (!wi.id) { console.warn(`[queries] Skipping work item without id in ${project.name}:`, JSON.stringify(wi).slice(0, 120)); continue; } if (wi.sourcePlan) wiById[wi.id] = wi; }
|
|
581
581
|
} catch { /* optional */ }
|
|
582
582
|
}
|
|
583
583
|
// Also check central work-items.json
|
|
584
584
|
try {
|
|
585
585
|
const centralWi = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
586
|
-
for (const wi of centralWi) { if (wi.sourcePlan && !wiById[wi.id]) wiById[wi.id] = wi; }
|
|
586
|
+
for (const wi of centralWi) { if (!wi.id) { console.warn('[queries] Skipping central work item without id:', JSON.stringify(wi).slice(0, 120)); continue; } if (wi.sourcePlan && !wiById[wi.id]) wiById[wi.id] = wi; }
|
|
587
587
|
} catch { /* optional */ }
|
|
588
588
|
|
|
589
589
|
// PR-to-PRD linking — primary source is pr-links.json (single-writer, never clobbered by polling)
|
|
@@ -595,7 +595,7 @@ function getPrdInfo(config) {
|
|
|
595
595
|
const prLinks = shared.getPrLinks(); // { "PR-xxxx": "P-xxxx" }
|
|
596
596
|
for (const [prId, itemId] of Object.entries(prLinks)) {
|
|
597
597
|
const pr = prById[prId];
|
|
598
|
-
const project = projects.find(p => p.name === pr?._project) || projects[0];
|
|
598
|
+
const project = projects.find(p => p.name === pr?._project) || projects[0] || null;
|
|
599
599
|
const url = pr?.url || (project?.prUrlBase ? project.prUrlBase + prId.replace('PR-', '') : '');
|
|
600
600
|
if (!prdToPr[itemId]) prdToPr[itemId] = [];
|
|
601
601
|
prdToPr[itemId].push({ id: prId, url, title: pr?.title || '', status: pr?.status || 'active', _project: pr?._project || '' });
|
package/engine/shared.js
CHANGED
|
@@ -25,7 +25,7 @@ function log(level, msg, meta = {}) {
|
|
|
25
25
|
let logData = safeJson(LOG_PATH) || [];
|
|
26
26
|
if (!Array.isArray(logData)) logData = logData.entries || [];
|
|
27
27
|
logData.push(entry);
|
|
28
|
-
if (logData.length
|
|
28
|
+
if (logData.length >= 2500) logData.splice(0, logData.length - 2000);
|
|
29
29
|
safeWrite(LOG_PATH, logData);
|
|
30
30
|
}
|
|
31
31
|
|
package/engine.js
CHANGED
|
@@ -708,13 +708,14 @@ function spawnAgent(dispatchItem, config) {
|
|
|
708
708
|
// Move pending -> active under a lock to avoid cross-process lost updates (engine/dashboard)
|
|
709
709
|
mutateDispatch((dispatch) => {
|
|
710
710
|
const idx = dispatch.pending.findIndex(d => d.id === id);
|
|
711
|
-
if (idx < 0) return;
|
|
711
|
+
if (idx < 0) return dispatch;
|
|
712
712
|
const item = dispatch.pending.splice(idx, 1)[0];
|
|
713
713
|
item.started_at = startedAt;
|
|
714
714
|
delete item.skipReason;
|
|
715
715
|
if (!dispatch.active.some(d => d.id === id)) {
|
|
716
716
|
dispatch.active.push(item);
|
|
717
717
|
}
|
|
718
|
+
return dispatch;
|
|
718
719
|
});
|
|
719
720
|
|
|
720
721
|
return proc;
|
|
@@ -1382,9 +1383,8 @@ function discoverFromWorkItems(config, project) {
|
|
|
1382
1383
|
// This protects against persisted state drift from old runtime versions.
|
|
1383
1384
|
try {
|
|
1384
1385
|
mutateDispatch((dp) => {
|
|
1385
|
-
const before = Array.isArray(dp.completed) ? dp.completed.length : 0;
|
|
1386
1386
|
dp.completed = Array.isArray(dp.completed) ? dp.completed.filter(d => d.meta?.dispatchKey !== key) : [];
|
|
1387
|
-
return dp
|
|
1387
|
+
return dp;
|
|
1388
1388
|
});
|
|
1389
1389
|
dispatchCooldowns.delete(key);
|
|
1390
1390
|
} catch (e) { log('warn', 'self-heal dispatch state: ' + e.message); }
|
|
@@ -1973,19 +1973,29 @@ function discoverWork(config) {
|
|
|
1973
1973
|
|
|
1974
1974
|
// Periodic plan completion sweep — catch PRDs that completed while engine was down
|
|
1975
1975
|
// or where checkPlanCompletion missed the completion event
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1976
|
+
// Throttled to every 10 ticks (~5 min) to reduce call volume (P3 decision)
|
|
1977
|
+
if (tickCount % 10 === 0) {
|
|
1978
|
+
try {
|
|
1979
|
+
const lifecycle = require('./engine/lifecycle');
|
|
1980
|
+
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
1981
|
+
if (fs.existsSync(prdDir)) {
|
|
1982
|
+
for (const f of fs.readdirSync(prdDir).filter(f => f.endsWith('.json'))) {
|
|
1983
|
+
if (completedPlanCache.has(f)) continue;
|
|
1984
|
+
const plan = safeJson(path.join(prdDir, f));
|
|
1985
|
+
if (!plan?.missing_features || plan.status === 'completed') {
|
|
1986
|
+
if (plan?.status === 'completed') completedPlanCache.add(f);
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
if (plan.status !== 'approved' && plan.status !== 'active') continue;
|
|
1990
|
+
// Simulate the meta object checkPlanCompletion expects
|
|
1991
|
+
lifecycle.checkPlanCompletion({ item: { sourcePlan: f } }, config);
|
|
1992
|
+
// If plan transitioned to completed, cache it
|
|
1993
|
+
const after = safeJson(path.join(prdDir, f));
|
|
1994
|
+
if (after?.status === 'completed') completedPlanCache.add(f);
|
|
1995
|
+
}
|
|
1986
1996
|
}
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1997
|
+
} catch (e) { log('warn', 'plan completion sweep: ' + e.message); }
|
|
1998
|
+
}
|
|
1989
1999
|
|
|
1990
2000
|
// Gate reviews and fixes: do not dispatch until all implement items are complete
|
|
1991
2001
|
const hasIncompleteImplements = projects.some(project => {
|
|
@@ -2023,6 +2033,10 @@ function discoverWork(config) {
|
|
|
2023
2033
|
|
|
2024
2034
|
let tickCount = 0;
|
|
2025
2035
|
|
|
2036
|
+
// In-memory cache of plan filenames confirmed completed — avoids redundant
|
|
2037
|
+
// checkPlanCompletion calls. Cleared automatically on engine restart.
|
|
2038
|
+
const completedPlanCache = new Set();
|
|
2039
|
+
|
|
2026
2040
|
let tickRunning = false;
|
|
2027
2041
|
|
|
2028
2042
|
async function tick() {
|
|
@@ -2086,9 +2100,15 @@ async function tickInner() {
|
|
|
2086
2100
|
try {
|
|
2087
2101
|
const prdFiles = safeReadDir(PRD_DIR).filter(f => f.endsWith('.json'));
|
|
2088
2102
|
for (const file of prdFiles) {
|
|
2103
|
+
if (completedPlanCache.has(file)) continue;
|
|
2089
2104
|
const plan = safeJson(path.join(PRD_DIR, file));
|
|
2090
2105
|
if (plan && plan.missing_features && plan.status !== 'completed') {
|
|
2091
2106
|
checkPlanCompletion({ item: { sourcePlan: file } }, config);
|
|
2107
|
+
// If plan transitioned to completed, cache it
|
|
2108
|
+
const after = safeJson(path.join(PRD_DIR, file));
|
|
2109
|
+
if (after?.status === 'completed') completedPlanCache.add(file);
|
|
2110
|
+
} else if (plan?.status === 'completed') {
|
|
2111
|
+
completedPlanCache.add(file);
|
|
2092
2112
|
}
|
|
2093
2113
|
}
|
|
2094
2114
|
} catch (err) { log('warn', `Plan completion check error: ${err?.message || err}`); }
|
|
@@ -2146,9 +2166,8 @@ async function tickInner() {
|
|
|
2146
2166
|
try {
|
|
2147
2167
|
const key = `work-${project.name}-${item.id}`;
|
|
2148
2168
|
mutateDispatch((dp) => {
|
|
2149
|
-
const before = dp.completed.length;
|
|
2150
2169
|
dp.completed = dp.completed.filter(d => d.meta?.dispatchKey !== key);
|
|
2151
|
-
|
|
2170
|
+
return dp;
|
|
2152
2171
|
});
|
|
2153
2172
|
} catch (e) { log('warn', 'stall recovery clear dispatch: ' + e.message); }
|
|
2154
2173
|
|
|
@@ -2182,6 +2201,7 @@ async function tickInner() {
|
|
|
2182
2201
|
const key = `work-${project.name}-${dep.id}`;
|
|
2183
2202
|
mutateDispatch((dp) => {
|
|
2184
2203
|
dp.completed = dp.completed.filter(d => d.meta?.dispatchKey !== key);
|
|
2204
|
+
return dp;
|
|
2185
2205
|
});
|
|
2186
2206
|
} catch (e) { log('warn', 'stall recovery clear dependent dispatch: ' + e.message); }
|
|
2187
2207
|
}
|
|
@@ -2226,6 +2246,7 @@ async function tickInner() {
|
|
|
2226
2246
|
mutateDispatch((dp) => {
|
|
2227
2247
|
dp.pending = dispatch.pending;
|
|
2228
2248
|
dp.active = dispatch.active || dp.active;
|
|
2249
|
+
return dp;
|
|
2229
2250
|
});
|
|
2230
2251
|
|
|
2231
2252
|
// Only dispatch to agents that aren't already busy (one task per agent at a time).
|
|
@@ -2244,8 +2265,39 @@ async function tickInner() {
|
|
|
2244
2265
|
const dispatched = new Set();
|
|
2245
2266
|
for (const item of toDispatch) {
|
|
2246
2267
|
if (!dispatched.has(item.id)) {
|
|
2247
|
-
spawnAgent(item, config);
|
|
2248
|
-
|
|
2268
|
+
const proc = spawnAgent(item, config);
|
|
2269
|
+
if (proc === null) {
|
|
2270
|
+
// spawnAgent failed (e.g., worktree creation error). It already called
|
|
2271
|
+
// completeDispatch internally which handles retry logic, but log at the
|
|
2272
|
+
// dispatch-loop level for visibility and handle any edge cases where
|
|
2273
|
+
// completeDispatch wasn't called.
|
|
2274
|
+
log('error', `spawnAgent returned null for ${item.id} (${item.type} → ${item.agent}) — spawn failed`);
|
|
2275
|
+
// Defensive: ensure the work item is re-queued if completeDispatch didn't fire
|
|
2276
|
+
if (item.meta?.item?.id) {
|
|
2277
|
+
try {
|
|
2278
|
+
const wiPath = item.meta.source === 'central-work-item' || item.meta.source === 'central-work-item-fanout'
|
|
2279
|
+
? path.join(ENGINE_DIR, '..', 'work-items.json')
|
|
2280
|
+
: item.meta.project?.name ? projectWorkItemsPath({ name: item.meta.project.name, localPath: item.meta.project.localPath }) : null;
|
|
2281
|
+
if (wiPath) {
|
|
2282
|
+
const items = safeJson(wiPath) || [];
|
|
2283
|
+
const wi = items.find(i => i.id === item.meta.item.id);
|
|
2284
|
+
if (wi && wi.status === 'dispatched') {
|
|
2285
|
+
// completeDispatch didn't update the work item — re-queue manually
|
|
2286
|
+
wi.status = 'pending';
|
|
2287
|
+
wi._retryCount = (wi._retryCount || 0) + 1;
|
|
2288
|
+
wi._lastRetryReason = 'spawnAgent returned null';
|
|
2289
|
+
wi._lastRetryAt = ts();
|
|
2290
|
+
delete wi.dispatched_at;
|
|
2291
|
+
delete wi.dispatched_to;
|
|
2292
|
+
safeWrite(wiPath, items);
|
|
2293
|
+
log('info', `Re-queued ${item.meta.item.id} as pending (retry ${wi._retryCount})`);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
} catch (e) { log('warn', `Failed to re-queue work item after spawn failure: ${e.message}`); }
|
|
2297
|
+
}
|
|
2298
|
+
} else {
|
|
2299
|
+
dispatched.add(item.id);
|
|
2300
|
+
}
|
|
2249
2301
|
}
|
|
2250
2302
|
}
|
|
2251
2303
|
|
|
@@ -2268,7 +2320,7 @@ async function tickInner() {
|
|
|
2268
2320
|
}
|
|
2269
2321
|
}
|
|
2270
2322
|
if (skipReasonChanged) {
|
|
2271
|
-
mutateDispatch((dp) => { dp.pending = postDispatch.pending; });
|
|
2323
|
+
mutateDispatch((dp) => { dp.pending = postDispatch.pending; return dp; });
|
|
2272
2324
|
}
|
|
2273
2325
|
}
|
|
2274
2326
|
|
package/package.json
CHANGED