@yemi33/minions 0.1.977 → 0.1.979
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 +3 -3
- package/dashboard/js/command-center.js +3 -4
- package/dashboard/js/settings.js +2 -0
- package/engine/playbook.js +29 -3
- package/engine/shared.js +2 -3
- package/engine.js +7 -45
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.979 (2026-04-15)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- gate build failure auto-fix behind autoFixBuilds flag
|
|
6
7
|
- free-form interval input + CC create-watch action
|
|
7
8
|
- flush queued CC messages as single combined request
|
|
8
9
|
- add quality standard reminder to all agent and CC prompts
|
|
@@ -22,9 +23,9 @@
|
|
|
22
23
|
- add adoPollEnabled/ghPollEnabled engine settings
|
|
23
24
|
- doc-chat abort kills LLM process + queued messages auto-process
|
|
24
25
|
- add /api/work-items/reopen endpoint and reopen-work-item CC action (#982)
|
|
25
|
-
- CC tab unread dot + reopened badge on work items
|
|
26
26
|
|
|
27
27
|
### Fixes
|
|
28
|
+
- ENGINE_DEFAULTS undefined in tick + playbook empty-var false positives
|
|
28
29
|
- defer review setCooldown to post-gating in discoverWork
|
|
29
30
|
- move review verdict check before updateWorkItemStatus(DONE)
|
|
30
31
|
- address review feedback — move writeToInbox outside lock, add absolute condition auto-expire
|
|
@@ -44,7 +45,6 @@
|
|
|
44
45
|
- defer setCooldown to post-gating in discoverWork
|
|
45
46
|
- fix CC queued message 'already processing' and thinking UX stacking
|
|
46
47
|
- enforce --timeout 1 on all azureauth calls to prevent agent orphans (#1063)
|
|
47
|
-
- cancel steering kill watcher on resume spawn (#1052) (#1062)
|
|
48
48
|
|
|
49
49
|
### Other
|
|
50
50
|
- refactor(watches): rename maxTriggers→stopAfter, add onNotMet per-poll action
|
|
@@ -388,15 +388,14 @@ async function ccSend() {
|
|
|
388
388
|
}
|
|
389
389
|
var wasAborted = await _ccDoSend(message, false, originTabId);
|
|
390
390
|
|
|
391
|
-
// Flush
|
|
392
|
-
// Loop in case user queues more messages while the combined flush is processing.
|
|
391
|
+
// Flush queued messages to the ORIGINAL tab, even if user switched tabs
|
|
393
392
|
while (tab._queue && tab._queue.length > 0) {
|
|
394
393
|
if (wasAborted) {
|
|
395
394
|
await new Promise(function(r) { setTimeout(r, 1500); });
|
|
396
395
|
}
|
|
397
|
-
var
|
|
396
|
+
var next = tab._queue.shift();
|
|
398
397
|
_renderQueueIndicator();
|
|
399
|
-
wasAborted = await _ccDoSend(
|
|
398
|
+
wasAborted = await _ccDoSend(next, false, originTabId);
|
|
400
399
|
}
|
|
401
400
|
}
|
|
402
401
|
|
package/dashboard/js/settings.js
CHANGED
|
@@ -49,6 +49,7 @@ async function openSettings() {
|
|
|
49
49
|
settingsToggle('Auto-decompose', 'set-autoDecompose', e.autoDecompose !== false, 'Large implement items are auto-split into sub-tasks') +
|
|
50
50
|
settingsToggle('Allow Temp Agents', 'set-allowTempAgents', !!e.allowTempAgents, 'Spawn ephemeral agents when all permanent agents are busy') +
|
|
51
51
|
settingsToggle('Auto-archive Plans', 'set-autoArchive', !!e.autoArchive, 'Automatically archive plans after verify completes (off = manual archive via dashboard)') +
|
|
52
|
+
settingsToggle('Auto-fix Builds', 'set-autoFixBuilds', e.autoFixBuilds !== false, 'Auto-dispatch fix agents when a PR build fails') +
|
|
52
53
|
settingsToggle('Auto-fix Conflicts', 'set-autoFixConflicts', e.autoFixConflicts !== false, 'Auto-dispatch fix agents when a PR has merge conflicts') +
|
|
53
54
|
settingsToggle('ADO Polling', 'set-adoPollEnabled', e.adoPollEnabled !== false, 'Poll ADO PR status and comments each tick (reconciliation always runs)') +
|
|
54
55
|
settingsToggle('GitHub Polling', 'set-ghPollEnabled', e.ghPollEnabled !== false, 'Poll GitHub PR status and comments each tick (reconciliation always runs)') +
|
|
@@ -239,6 +240,7 @@ async function saveSettings() {
|
|
|
239
240
|
autoDecompose: document.getElementById('set-autoDecompose').checked,
|
|
240
241
|
allowTempAgents: document.getElementById('set-allowTempAgents').checked,
|
|
241
242
|
autoArchive: document.getElementById('set-autoArchive').checked,
|
|
243
|
+
autoFixBuilds: document.getElementById('set-autoFixBuilds').checked,
|
|
242
244
|
autoFixConflicts: document.getElementById('set-autoFixConflicts').checked,
|
|
243
245
|
adoPollEnabled: document.getElementById('set-adoPollEnabled').checked,
|
|
244
246
|
ghPollEnabled: document.getElementById('set-ghPollEnabled').checked,
|
package/engine/playbook.js
CHANGED
|
@@ -268,10 +268,32 @@ function validatePlaybookVars(playbookName, vars) {
|
|
|
268
268
|
return { valid: missing.length === 0, missing };
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
// ─── Playbook Path Resolution ───────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Resolve playbook file path, checking project-local override first.
|
|
275
|
+
* If projects/<projectName>/playbooks/<playbookType>.md exists, use it.
|
|
276
|
+
* Otherwise fall back to the global playbooks/<playbookType>.md.
|
|
277
|
+
* @param {string|null|undefined} projectName — project name (directory under projects/)
|
|
278
|
+
* @param {string} playbookType — playbook type name (e.g. 'implement', 'review')
|
|
279
|
+
* @returns {string} absolute path to the playbook file
|
|
280
|
+
*/
|
|
281
|
+
function resolvePlaybookPath(projectName, playbookType) {
|
|
282
|
+
if (projectName) {
|
|
283
|
+
const localPath = path.join(MINIONS_DIR, 'projects', projectName, 'playbooks', `${playbookType}.md`);
|
|
284
|
+
try {
|
|
285
|
+
fs.accessSync(localPath, fs.constants.R_OK);
|
|
286
|
+
return localPath;
|
|
287
|
+
} catch { /* no local override — fall through to global */ }
|
|
288
|
+
}
|
|
289
|
+
return path.join(PLAYBOOKS_DIR, `${playbookType}.md`);
|
|
290
|
+
}
|
|
291
|
+
|
|
271
292
|
// ─── Playbook Renderer ──────────────────────────────────────────────────────
|
|
272
293
|
|
|
273
294
|
function renderPlaybook(type, vars) {
|
|
274
|
-
const
|
|
295
|
+
const projectName = vars.project_name || null;
|
|
296
|
+
const pbPath = resolvePlaybookPath(projectName, type);
|
|
275
297
|
let content;
|
|
276
298
|
try { content = fs.readFileSync(pbPath, 'utf8'); } catch {
|
|
277
299
|
log('warn', `Playbook not found: ${type}`);
|
|
@@ -361,6 +383,9 @@ function renderPlaybook(type, vars) {
|
|
|
361
383
|
return null;
|
|
362
384
|
}
|
|
363
385
|
|
|
386
|
+
// Capture which vars are actually referenced in the template before substitution
|
|
387
|
+
const referencedVars = new Set((content.match(/\{\{(\w+)\}\}/g) || []).map(m => m.slice(2, -2)));
|
|
388
|
+
|
|
364
389
|
// Process conditional blocks: {{#key}}...{{/key}} — include block only if key is truthy
|
|
365
390
|
content = content.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, block) => {
|
|
366
391
|
const val = allVars[key];
|
|
@@ -380,9 +405,9 @@ function renderPlaybook(type, vars) {
|
|
|
380
405
|
log('warn', `Playbook "${type}": substituted values contain unresolved {{...}} patterns (potential self-reference): ${selfRefVars.join(', ')}`);
|
|
381
406
|
}
|
|
382
407
|
|
|
383
|
-
// Warn on variables that resolved to empty string
|
|
408
|
+
// Warn on variables that resolved to empty string — only for vars actually used in the template
|
|
384
409
|
const emptyVars = Object.entries(allVars)
|
|
385
|
-
.filter(([key, val]) => String(val) === '' && !PLAYBOOK_OPTIONAL_VARS.has(key))
|
|
410
|
+
.filter(([key, val]) => String(val) === '' && referencedVars.has(key) && !PLAYBOOK_OPTIONAL_VARS.has(key))
|
|
386
411
|
.map(([key]) => key);
|
|
387
412
|
if (emptyVars.length > 0) {
|
|
388
413
|
log('warn', `Playbook "${type}": template variables resolved to empty string: ${emptyVars.join(', ')}`);
|
|
@@ -565,6 +590,7 @@ function buildPrDispatch(agentId, config, project, pr, type, extraVars, taskLabe
|
|
|
565
590
|
|
|
566
591
|
module.exports = {
|
|
567
592
|
renderPlaybook,
|
|
593
|
+
resolvePlaybookPath,
|
|
568
594
|
validatePlaybookVars,
|
|
569
595
|
PLAYBOOK_REQUIRED_VARS,
|
|
570
596
|
PLAYBOOK_OPTIONAL_VARS,
|
package/engine/shared.js
CHANGED
|
@@ -534,6 +534,7 @@ const ENGINE_DEFAULTS = {
|
|
|
534
534
|
autoArchive: false, // opt-in: auto-archive plans after verify completes (false = mark ready, user archives manually)
|
|
535
535
|
autoReview: true, // auto-dispatch review agents for new PRs (disable for manual review workflow)
|
|
536
536
|
autoFixConflicts: true, // auto-dispatch fix agents when a PR has merge conflicts
|
|
537
|
+
autoFixBuilds: true, // auto-dispatch fix agents when a PR build fails
|
|
537
538
|
meetingRoundTimeout: 600000, // 10min per meeting round before auto-advance
|
|
538
539
|
evalLoop: true, // enable review→fix loop after implementation completes
|
|
539
540
|
evalMaxIterations: 3, // max review→fix cycles before escalating to human
|
|
@@ -617,8 +618,6 @@ const PR_POLLABLE_STATUSES = new Set([PR_STATUS.ACTIVE, PR_STATUS.LINKED]);
|
|
|
617
618
|
const WATCH_STATUS = { ACTIVE: 'active', PAUSED: 'paused', TRIGGERED: 'triggered', EXPIRED: 'expired' };
|
|
618
619
|
const WATCH_TARGET_TYPE = { PR: 'pr', WORK_ITEM: 'work-item' };
|
|
619
620
|
const WATCH_CONDITION = { MERGED: 'merged', BUILD_FAIL: 'build-fail', BUILD_PASS: 'build-pass', COMPLETED: 'completed', FAILED: 'failed', STATUS_CHANGE: 'status-change', ANY: 'any' };
|
|
620
|
-
// Absolute conditions check point-in-time state, not transitions — auto-expire after first trigger when stopAfter is 0
|
|
621
|
-
const WATCH_ABSOLUTE_CONDITIONS = new Set([WATCH_CONDITION.MERGED, WATCH_CONDITION.BUILD_FAIL, WATCH_CONDITION.BUILD_PASS, WATCH_CONDITION.COMPLETED, WATCH_CONDITION.FAILED]);
|
|
622
621
|
|
|
623
622
|
/** Update per-agent review metrics (prsApproved/prsRejected). Only writes for configured agents. */
|
|
624
623
|
function trackReviewMetric(pr, newReviewStatus, config) {
|
|
@@ -1145,7 +1144,7 @@ module.exports = {
|
|
|
1145
1144
|
classifyInboxItem,
|
|
1146
1145
|
ENGINE_DEFAULTS,
|
|
1147
1146
|
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, DISPATCH_RESULT, trackReviewMetric, queuePlanToPrd,
|
|
1148
|
-
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION,
|
|
1147
|
+
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION,
|
|
1149
1148
|
PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
|
|
1150
1149
|
FAILURE_CLASS, ESCALATION_POLICY, COMPLETION_FIELDS,
|
|
1151
1150
|
DEFAULT_AGENT_METRICS,
|
package/engine.js
CHANGED
|
@@ -1984,46 +1984,6 @@ async function discoverFromPrs(config, project) {
|
|
|
1984
1984
|
if (item) { newWork.push(item); }
|
|
1985
1985
|
}
|
|
1986
1986
|
|
|
1987
|
-
// Re-review after fix: only trigger when fixedAt > lastReviewedAt (not a broad !alreadyReviewed
|
|
1988
|
-
// fallback — that caused infinite re-review loops on GitHub where self-approval is blocked)
|
|
1989
|
-
const fixedAfterReview = !!(pr.minionsReview?.fixedAt &&
|
|
1990
|
-
pr.lastReviewedAt && pr.minionsReview.fixedAt > pr.lastReviewedAt);
|
|
1991
|
-
const needsReReview = autoReview && reviewStatus === 'waiting' &&
|
|
1992
|
-
fixedAfterReview && !evalEscalated;
|
|
1993
|
-
if (needsReReview) {
|
|
1994
|
-
const key = `review-${project?.name || 'default'}-${pr.id}`;
|
|
1995
|
-
// Skip isAlreadyDispatched — fixedAfterReview/alreadyReviewed already dedup; the 1hr
|
|
1996
|
-
// completed-dispatch window would block legitimate re-reviews within the hour after a fix
|
|
1997
|
-
if (isOnCooldown(key, cooldownMs)) continue;
|
|
1998
|
-
|
|
1999
|
-
// Pre-dispatch live vote check — cached 'waiting' may be stale if reviewer already acted
|
|
2000
|
-
try {
|
|
2001
|
-
const checkFn = project.repoHost === 'github' ? ghCheckLiveReview : adoCheckLiveReview;
|
|
2002
|
-
const liveStatus = await checkFn(pr, project);
|
|
2003
|
-
if (liveStatus && liveStatus !== 'waiting') {
|
|
2004
|
-
log('info', `Pre-dispatch vote check: ${pr.id} is ${liveStatus} (cached was waiting) — skipping re-review`);
|
|
2005
|
-
if (pr.reviewStatus !== 'approved') pr.reviewStatus = liveStatus;
|
|
2006
|
-
try {
|
|
2007
|
-
mutateJsonFileLocked(projectPrPath(project), data => {
|
|
2008
|
-
if (!Array.isArray(data)) return data;
|
|
2009
|
-
const target = data.find(p => p.id === pr.id);
|
|
2010
|
-
if (target && target.reviewStatus !== 'approved') target.reviewStatus = liveStatus;
|
|
2011
|
-
return data;
|
|
2012
|
-
});
|
|
2013
|
-
} catch {}
|
|
2014
|
-
continue;
|
|
2015
|
-
}
|
|
2016
|
-
} catch (e) { log('warn', `Pre-dispatch vote check for ${pr.id}: ${e.message}`); }
|
|
2017
|
-
|
|
2018
|
-
const agentId = resolveAgent('review', config);
|
|
2019
|
-
if (!agentId) continue;
|
|
2020
|
-
const item = buildPrDispatch(agentId, config, project, pr, 'review', {
|
|
2021
|
-
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: pr.branch || '',
|
|
2022
|
-
pr_author: pr.agent || '', pr_url: pr.url || '',
|
|
2023
|
-
}, `Review ${pr.id}: ${pr.title}`, { dispatchKey: key, source: 'pr', pr, branch: pr.branch, project: projMeta });
|
|
2024
|
-
if (item) { newWork.push(item); }
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
1987
|
// PRs with changes requested → route back to author for fix
|
|
2028
1988
|
let fixDispatched = false;
|
|
2029
1989
|
if (reviewStatus === 'changes-requested' && !awaitingReReview && !evalEscalated) {
|
|
@@ -2037,7 +1997,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2037
1997
|
review_note: pr.minionsReview?.note || pr.reviewNote || 'See PR thread comments',
|
|
2038
1998
|
}, `Fix ${pr.id}: ${pr.title || ''} — review feedback`, { dispatchKey: key, source: 'pr', pr, branch: pr.branch, project: projMeta });
|
|
2039
1999
|
if (item) {
|
|
2040
|
-
newWork.push(item); fixDispatched = true;
|
|
2000
|
+
newWork.push(item); setCooldown(key); fixDispatched = true;
|
|
2041
2001
|
// Increment review→fix cycle counter
|
|
2042
2002
|
try {
|
|
2043
2003
|
mutatePullRequests(projectPrPath(project), prs => {
|
|
@@ -2075,7 +2035,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2075
2035
|
reviewer: 'Human Reviewer',
|
|
2076
2036
|
review_note: reviewNote,
|
|
2077
2037
|
}, `Fix ${pr.id}: ${pr.title || ''} — human feedback`, { dispatchKey: key, source: 'pr-human-feedback', pr, branch: pr.branch, project: projMeta });
|
|
2078
|
-
if (item) { newWork.push(item); fixDispatched = true; }
|
|
2038
|
+
if (item) { newWork.push(item); setCooldown(key); fixDispatched = true; }
|
|
2079
2039
|
}
|
|
2080
2040
|
|
|
2081
2041
|
// PRs with build failures — route to author (has session context from implementing)
|
|
@@ -2085,7 +2045,8 @@ async function discoverFromPrs(config, project) {
|
|
|
2085
2045
|
const gracePeriodMs = config.engine?.buildFixGracePeriod ?? DEFAULTS.buildFixGracePeriod;
|
|
2086
2046
|
if (Date.now() - new Date(pr._buildFixPushedAt).getTime() < gracePeriodMs) continue;
|
|
2087
2047
|
}
|
|
2088
|
-
|
|
2048
|
+
const autoFixBuilds = config.engine?.autoFixBuilds ?? DEFAULTS.autoFixBuilds;
|
|
2049
|
+
if (autoFixBuilds && pr.status === PR_STATUS.ACTIVE && pr.buildStatus === 'failing') {
|
|
2089
2050
|
const maxBuildFix = config.engine?.maxBuildFixAttempts ?? DEFAULTS.maxBuildFixAttempts;
|
|
2090
2051
|
|
|
2091
2052
|
// Check if max retry cap reached — escalate to human instead of dispatching another fix
|
|
@@ -2124,7 +2085,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2124
2085
|
review_note: reviewNote,
|
|
2125
2086
|
}, `Fix build failure on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: pr.branch, project: projMeta });
|
|
2126
2087
|
if (item) {
|
|
2127
|
-
newWork.push(item);
|
|
2088
|
+
newWork.push(item); setCooldown(key);
|
|
2128
2089
|
// Increment build fix attempts counter
|
|
2129
2090
|
try {
|
|
2130
2091
|
const prPath = projectPrPath(project);
|
|
@@ -2181,6 +2142,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2181
2142
|
}, `Fix merge conflicts on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: pr.branch, project: projMeta });
|
|
2182
2143
|
if (item) {
|
|
2183
2144
|
newWork.push(item);
|
|
2145
|
+
setCooldown(key);
|
|
2184
2146
|
// Record dispatch timestamp so re-dispatch is suppressed during ADO lag window
|
|
2185
2147
|
try {
|
|
2186
2148
|
mutatePullRequests(projectPrPath(project), prs => {
|
|
@@ -3390,7 +3352,7 @@ async function tickInner() {
|
|
|
3390
3352
|
if (busyAgents.has(item.agent)) {
|
|
3391
3353
|
// Agent busy reassignment: if item has been waiting on a busy agent past the threshold,
|
|
3392
3354
|
// try to find an alternative agent via routing. Skip explicitly assigned items.
|
|
3393
|
-
const reassignMs = config.engine?.agentBusyReassignMs ??
|
|
3355
|
+
const reassignMs = config.engine?.agentBusyReassignMs ?? DEFAULTS.agentBusyReassignMs;
|
|
3394
3356
|
const isExplicitReassign = !!item.meta?.item?.agent;
|
|
3395
3357
|
if (!isExplicitReassign && reassignMs > 0 && item._agentBusySince) {
|
|
3396
3358
|
const busySinceMs = new Date(item._agentBusySince).getTime();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.979",
|
|
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"
|