@yemi33/minions 0.1.1702 → 0.1.1704
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 +7 -0
- package/dashboard/js/render-schedules.js +23 -1
- package/dashboard.js +210 -36
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +109 -30
- package/engine/scheduler.js +49 -18
- package/engine/shared.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -302,6 +302,7 @@ function renderSchedules(schedules) {
|
|
|
302
302
|
'<td>' + enabledBadge + '</td>' +
|
|
303
303
|
'<td><span class="pr-date">' + escHtml(lastRun) + '</span></td>' +
|
|
304
304
|
'<td style="white-space:nowrap">' +
|
|
305
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();runScheduleNow(\'' + escHtml(s.id) + '\',this)" title="Run now">Run now</button>' +
|
|
305
306
|
'<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:' + (s.enabled ? 'var(--yellow)' : 'var(--green)') + ';border-color:' + (s.enabled ? 'var(--yellow)' : 'var(--green)') + ';margin-right:4px" onclick="event.stopPropagation();toggleScheduleEnabled(\'' + escHtml(s.id) + '\',' + !s.enabled + ')" title="' + (s.enabled ? 'Disable' : 'Enable') + '">' + (s.enabled ? '⏸' : '▶') + '</button>' +
|
|
306
307
|
'<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();openEditScheduleModal(\'' + escHtml(s.id) + '\')" title="Edit">✎</button>' +
|
|
307
308
|
'<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();deleteSchedule(\'' + escHtml(s.id) + '\')" title="Delete">✕</button>' +
|
|
@@ -336,6 +337,7 @@ function openScheduleDetail(id) {
|
|
|
336
337
|
|
|
337
338
|
document.getElementById('modal-title').innerHTML = escHtml(s.title || s.id) +
|
|
338
339
|
' <div style="display:flex;gap:4px;margin-top:4px">' +
|
|
340
|
+
'<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--green)" onclick="runScheduleNow(\'' + escHtml(s.id) + '\',this)">Run now</button>' +
|
|
339
341
|
'<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--blue)" onclick="closeModal();openEditScheduleModal(\'' + escHtml(s.id) + '\')">Edit</button>' +
|
|
340
342
|
'<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:' + (s.enabled ? 'var(--yellow)' : 'var(--green)') + '" onclick="toggleScheduleEnabled(\'' + escHtml(s.id) + '\',' + !s.enabled + ');closeModal()">' + (s.enabled ? 'Disable' : 'Enable') + '</button>' +
|
|
341
343
|
'<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--red)" onclick="deleteSchedule(\'' + escHtml(s.id) + '\');closeModal()">Delete</button>' +
|
|
@@ -544,6 +546,26 @@ async function toggleScheduleEnabled(id, enabled) {
|
|
|
544
546
|
} catch (e) { _showScheduleError('Toggle error: ' + e.message); refresh(); }
|
|
545
547
|
}
|
|
546
548
|
|
|
549
|
+
async function runScheduleNow(id, btn) {
|
|
550
|
+
if (btn) { btn.textContent = 'Running...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
|
|
551
|
+
try {
|
|
552
|
+
const res = await fetch('/api/schedules/run-now', {
|
|
553
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
554
|
+
body: JSON.stringify({ id })
|
|
555
|
+
});
|
|
556
|
+
if (res.ok) {
|
|
557
|
+
showToast('cmd-toast', 'Schedule run queued', true);
|
|
558
|
+
refresh();
|
|
559
|
+
} else {
|
|
560
|
+
const d = await res.json().catch(() => ({}));
|
|
561
|
+
showToast('cmd-toast', 'Run failed: ' + (d.error || 'unknown'), false);
|
|
562
|
+
}
|
|
563
|
+
} catch (e) {
|
|
564
|
+
showToast('cmd-toast', 'Run error: ' + e.message, false);
|
|
565
|
+
}
|
|
566
|
+
if (btn) { btn.textContent = 'Run now'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
|
|
567
|
+
}
|
|
568
|
+
|
|
547
569
|
async function deleteSchedule(id) {
|
|
548
570
|
if (!confirm('Delete scheduled task "' + id + '"?')) return;
|
|
549
571
|
showToast('cmd-toast', 'Schedule deleted', true);
|
|
@@ -562,4 +584,4 @@ async function deleteSchedule(id) {
|
|
|
562
584
|
// Expose _generateScheduleId globally for the inline oninput handler
|
|
563
585
|
window._generateScheduleId = _generateScheduleId;
|
|
564
586
|
|
|
565
|
-
window.MinionsSchedules = { renderSchedules, openCreateScheduleModal, openEditScheduleModal, openScheduleDetail, submitSchedule, toggleScheduleEnabled, deleteSchedule, _cronToHuman, _parseNaturalCron, _toggleCronMode, _quickSelectDays, _toggleDayPill, _updateCronPreview, _schedPrev, _schedNext, _schedSetView };
|
|
587
|
+
window.MinionsSchedules = { renderSchedules, openCreateScheduleModal, openEditScheduleModal, openScheduleDetail, submitSchedule, toggleScheduleEnabled, runScheduleNow, deleteSchedule, _cronToHuman, _parseNaturalCron, _toggleCronMode, _quickSelectDays, _toggleDayPill, _updateCronPreview, _schedPrev, _schedNext, _schedSetView };
|
package/dashboard.js
CHANGED
|
@@ -126,6 +126,124 @@ function copyWorkItemPrFields(item, input, pr = null) {
|
|
|
126
126
|
if (pr?.title || input.prTitle) item.prTitle = pr?.title || input.prTitle;
|
|
127
127
|
if (pr?.url || input.prUrl) item.prUrl = pr?.url || input.prUrl;
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
function normalizeWorkItemDedupText(value) {
|
|
131
|
+
return String(value == null ? '' : value)
|
|
132
|
+
.replace(/\r\n/g, '\n')
|
|
133
|
+
.replace(/[ \t]+$/gm, '')
|
|
134
|
+
.trim();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveWorkItemDedupProject(item, wiPath = '') {
|
|
138
|
+
const projectName = normalizeWorkItemDedupText(item?.project || item?._project || item?._source);
|
|
139
|
+
if (projectName) {
|
|
140
|
+
const namedProject = PROJECTS.find(p => p?.name === projectName);
|
|
141
|
+
if (namedProject) return namedProject;
|
|
142
|
+
}
|
|
143
|
+
if (!wiPath) return null;
|
|
144
|
+
const resolvedWiPath = path.resolve(wiPath);
|
|
145
|
+
return PROJECTS.find(p => path.resolve(shared.projectWorkItemsPath(p)) === resolvedWiPath) || null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getWorkItemPrRefCandidates(item) {
|
|
149
|
+
return [
|
|
150
|
+
item?.targetPr,
|
|
151
|
+
item?.pr,
|
|
152
|
+
item?.prId,
|
|
153
|
+
item?.pullRequest,
|
|
154
|
+
item?.sourcePr,
|
|
155
|
+
item?.prUrl,
|
|
156
|
+
item?.pr_id,
|
|
157
|
+
item?.prNumber,
|
|
158
|
+
].filter(value => value != null && value !== '');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizeWorkItemDedupPrIdentity(item, project = null) {
|
|
162
|
+
if (!item || typeof item !== 'object') return '';
|
|
163
|
+
const candidates = getWorkItemPrRefCandidates(item);
|
|
164
|
+
for (const candidate of candidates) {
|
|
165
|
+
const scoped = shared.getPrScopeInfo(candidate);
|
|
166
|
+
if (scoped) return `${scoped.scope}#${scoped.prNumber}`;
|
|
167
|
+
}
|
|
168
|
+
const prNumber = candidates.reduce((found, candidate) => (
|
|
169
|
+
found ?? shared.getPrNumber(candidate)
|
|
170
|
+
), null);
|
|
171
|
+
if (project && prNumber != null) {
|
|
172
|
+
const canonical = shared.getCanonicalPrId(project, prNumber);
|
|
173
|
+
if (canonical) return canonical;
|
|
174
|
+
}
|
|
175
|
+
if (prNumber != null) return `PR-${prNumber}`;
|
|
176
|
+
const prRef = getWorkItemPrRef(item) || item.pr_id || item.targetPr || item.prUrl || '';
|
|
177
|
+
return normalizeWorkItemDedupText(prRef).toLowerCase();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function workItemCreateFingerprint(item, options = {}) {
|
|
181
|
+
const project = resolveWorkItemDedupProject(item, options.wiPath);
|
|
182
|
+
return {
|
|
183
|
+
title: normalizeWorkItemDedupText(item?.title),
|
|
184
|
+
type: routing.normalizeWorkType(item?.type || item?.workType, WORK_TYPE.IMPLEMENT),
|
|
185
|
+
priority: normalizeWorkItemDedupText(item?.priority || 'medium').toLowerCase(),
|
|
186
|
+
description: normalizeWorkItemDedupText(item?.description),
|
|
187
|
+
prIdentity: normalizeWorkItemDedupPrIdentity(item, project),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isActiveWorkItemCreateStatus(status) {
|
|
192
|
+
return status === WI_STATUS.PENDING || status === WI_STATUS.DISPATCHED || status === WI_STATUS.QUEUED;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isWithinWorkItemCreateDedupWindow(item, nowMs, windowMs) {
|
|
196
|
+
const createdMs = Date.parse(item?.created || item?.createdAt || item?.created_at || '');
|
|
197
|
+
if (!Number.isFinite(createdMs)) return true;
|
|
198
|
+
return nowMs - createdMs <= windowMs;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function findDuplicateWorkItemCreate(items, candidate, options = {}) {
|
|
202
|
+
if (!Array.isArray(items)) return null;
|
|
203
|
+
const nowMs = Number.isFinite(options.nowMs) ? options.nowMs : Date.now();
|
|
204
|
+
const windowMs = Number.isFinite(options.windowMs) ? options.windowMs : shared.ENGINE_DEFAULTS.workItemCreateDedupWindowMs;
|
|
205
|
+
const candidateFingerprint = workItemCreateFingerprint(candidate, options);
|
|
206
|
+
return items.find(item => {
|
|
207
|
+
if (!isActiveWorkItemCreateStatus(item?.status)) return false;
|
|
208
|
+
if (!isWithinWorkItemCreateDedupWindow(item, nowMs, windowMs)) return false;
|
|
209
|
+
const existingFingerprint = workItemCreateFingerprint(item, options);
|
|
210
|
+
return existingFingerprint.title === candidateFingerprint.title &&
|
|
211
|
+
existingFingerprint.type === candidateFingerprint.type &&
|
|
212
|
+
existingFingerprint.priority === candidateFingerprint.priority &&
|
|
213
|
+
existingFingerprint.description === candidateFingerprint.description &&
|
|
214
|
+
existingFingerprint.prIdentity === candidateFingerprint.prIdentity;
|
|
215
|
+
}) || null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function createWorkItemWithDedup(wiPath, item, options = {}) {
|
|
219
|
+
let result = null;
|
|
220
|
+
mutateWorkItems(wiPath, items => {
|
|
221
|
+
const existing = findDuplicateWorkItemCreate(items, item, { ...options, wiPath });
|
|
222
|
+
if (existing) {
|
|
223
|
+
result = { created: false, item: existing, duplicateOf: existing.id };
|
|
224
|
+
return items;
|
|
225
|
+
}
|
|
226
|
+
items.push(item);
|
|
227
|
+
result = { created: true, item };
|
|
228
|
+
return items;
|
|
229
|
+
});
|
|
230
|
+
return result || { created: false, item: null };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function resolveWorkItemsCreateTarget(projectName, projects = PROJECTS) {
|
|
234
|
+
const project = String(projectName || '').trim();
|
|
235
|
+
let targetProject = null;
|
|
236
|
+
if (project) {
|
|
237
|
+
targetProject = projects.find(p => p.name === project) || (projects.length > 0 ? projects[0] : null);
|
|
238
|
+
if (!targetProject) return { error: 'No projects configured' };
|
|
239
|
+
} else if (projects.length === 1) {
|
|
240
|
+
targetProject = projects[0];
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
project: targetProject,
|
|
244
|
+
wiPath: targetProject ? shared.projectWorkItemsPath(targetProject) : path.join(MINIONS_DIR, 'work-items.json'),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
129
247
|
function linkPullRequestForTracking({ url, title, project: projectName, autoObserve, context, workItemId }, config = CONFIG, options = {}) {
|
|
130
248
|
if (!url) {
|
|
131
249
|
const err = new Error('url required');
|
|
@@ -1531,20 +1649,21 @@ async function executeCCActions(actions) {
|
|
|
1531
1649
|
// Mark oneShot so any discovered PR is tagged _contextOnly (skips eval loop).
|
|
1532
1650
|
const ccOneShotTypes = new Set(['review', 'explore', 'test']);
|
|
1533
1651
|
const isOneShot = action.oneShot === true || (action.oneShot !== false && ccOneShotTypes.has(workType));
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1652
|
+
const item = {
|
|
1653
|
+
id, title: action.title.trim(), type: workType,
|
|
1654
|
+
priority: action.priority || 'medium', description: action.description || '',
|
|
1655
|
+
status: WI_STATUS.PENDING, created: new Date().toISOString(),
|
|
1656
|
+
createdBy: 'command-center', project: targetProject?.name || project,
|
|
1657
|
+
...(agentHints.length ? { preferred_agent: agentHints[0], agents: agentHints } : {}),
|
|
1658
|
+
...(isOneShot ? { oneShot: true } : {}),
|
|
1659
|
+
};
|
|
1660
|
+
copyWorkItemPrFields(item, action, linkedPr);
|
|
1661
|
+
const createResult = createWorkItemWithDedup(wiPath, item);
|
|
1662
|
+
if (!createResult.created) {
|
|
1663
|
+
const duplicateId = createResult.duplicateOf || createResult.item?.id;
|
|
1664
|
+
results.push({ type: action.type, id: duplicateId, ok: true, duplicate: true, duplicateOf: duplicateId });
|
|
1665
|
+
break;
|
|
1666
|
+
}
|
|
1548
1667
|
results.push({ type: action.type, id, ok: true });
|
|
1549
1668
|
|
|
1550
1669
|
// Pre-flight routing check: warn the user if no agent is currently available so the new
|
|
@@ -2840,22 +2959,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
2840
2959
|
try {
|
|
2841
2960
|
const body = await readBody(req);
|
|
2842
2961
|
if (!body.title || !body.title.trim()) return jsonReply(res, 400, { error: 'title is required' });
|
|
2843
|
-
|
|
2844
|
-
if (
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
if (!targetProject) return jsonReply(res, 400, { error: 'No projects configured' });
|
|
2848
|
-
wiPath = shared.projectWorkItemsPath(targetProject);
|
|
2849
|
-
} else {
|
|
2850
|
-
// Write to central queue — agent decides which project
|
|
2851
|
-
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
2852
|
-
}
|
|
2962
|
+
const target = resolveWorkItemsCreateTarget(body.project);
|
|
2963
|
+
if (target.error) return jsonReply(res, 400, { error: target.error });
|
|
2964
|
+
const wiPath = target.wiPath;
|
|
2965
|
+
const targetProject = target.project;
|
|
2853
2966
|
const id = 'W-' + shared.uid();
|
|
2854
2967
|
const item = {
|
|
2855
2968
|
id, title: body.title.trim(), type: routing.normalizeWorkType(body.type, WORK_TYPE.IMPLEMENT),
|
|
2856
2969
|
priority: body.priority || 'medium', description: body.description || '',
|
|
2857
2970
|
status: WI_STATUS.PENDING, created: new Date().toISOString(), createdBy: 'dashboard',
|
|
2858
2971
|
};
|
|
2972
|
+
if (targetProject) item.project = targetProject.name;
|
|
2859
2973
|
if (body.scope) item.scope = body.scope;
|
|
2860
2974
|
// Agent assignment normalization: `agent` and `agents` are routing hints.
|
|
2861
2975
|
// Use agentLock/hardAgent only for the rare case where an item must wait
|
|
@@ -2870,18 +2984,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
2870
2984
|
if (body.skipPr) item.skipPr = true;
|
|
2871
2985
|
if (body.oneShot) item.oneShot = true;
|
|
2872
2986
|
copyWorkItemPrFields(item, body);
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
(i.status === WI_STATUS.PENDING || i.status === WI_STATUS.DISPATCHED || i.status === WI_STATUS.QUEUED)
|
|
2879
|
-
);
|
|
2880
|
-
if (existing) { dupId = existing.id; return items; }
|
|
2881
|
-
items.push(item);
|
|
2882
|
-
return items;
|
|
2883
|
-
});
|
|
2884
|
-
if (dupId) return jsonReply(res, 200, { ok: true, id: dupId, duplicate: true });
|
|
2987
|
+
const createResult = createWorkItemWithDedup(wiPath, item);
|
|
2988
|
+
if (!createResult.created) {
|
|
2989
|
+
const duplicateId = createResult.duplicateOf || createResult.item?.id;
|
|
2990
|
+
return jsonReply(res, 200, { ok: true, id: duplicateId, duplicate: true, duplicateOf: duplicateId });
|
|
2991
|
+
}
|
|
2885
2992
|
return jsonReply(res, 200, { ok: true, id });
|
|
2886
2993
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
2887
2994
|
}
|
|
@@ -5446,6 +5553,69 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5446
5553
|
return jsonReply(res, 200, { ok: true });
|
|
5447
5554
|
}
|
|
5448
5555
|
|
|
5556
|
+
async function handleSchedulesRunNow(req, res) {
|
|
5557
|
+
const body = await readBody(req);
|
|
5558
|
+
const { id } = body;
|
|
5559
|
+
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
5560
|
+
|
|
5561
|
+
reloadConfig();
|
|
5562
|
+
const sched = (CONFIG.schedules || []).find(s => s.id === id);
|
|
5563
|
+
if (!sched) return jsonReply(res, 404, { error: 'Schedule not found' });
|
|
5564
|
+
|
|
5565
|
+
const schedulerMod = require('./engine/scheduler');
|
|
5566
|
+
let item;
|
|
5567
|
+
try {
|
|
5568
|
+
item = schedulerMod.createScheduledWorkItem(sched);
|
|
5569
|
+
} catch (e) {
|
|
5570
|
+
return jsonReply(res, 400, { error: e.message });
|
|
5571
|
+
}
|
|
5572
|
+
|
|
5573
|
+
let meeting = null;
|
|
5574
|
+
if (item.type === WORK_TYPE.MEETING) {
|
|
5575
|
+
const { createMeeting } = require('./engine/meeting');
|
|
5576
|
+
meeting = createMeeting({
|
|
5577
|
+
title: item.title,
|
|
5578
|
+
agenda: item.description,
|
|
5579
|
+
participants: Array.isArray(sched.participants) ? sched.participants : [],
|
|
5580
|
+
});
|
|
5581
|
+
} else {
|
|
5582
|
+
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
5583
|
+
let duplicate = null;
|
|
5584
|
+
mutateJsonFileLocked(centralPath, (items) => {
|
|
5585
|
+
if (!Array.isArray(items)) items = [];
|
|
5586
|
+
duplicate = items.find(i =>
|
|
5587
|
+
i._scheduleId === item._scheduleId &&
|
|
5588
|
+
i.status !== WI_STATUS.DONE &&
|
|
5589
|
+
i.status !== WI_STATUS.FAILED
|
|
5590
|
+
);
|
|
5591
|
+
if (!duplicate) items.push(item);
|
|
5592
|
+
return items;
|
|
5593
|
+
}, { defaultValue: [] });
|
|
5594
|
+
if (duplicate) {
|
|
5595
|
+
return jsonReply(res, 409, {
|
|
5596
|
+
error: 'Schedule already has an active work item',
|
|
5597
|
+
id: duplicate.id,
|
|
5598
|
+
scheduleId: sched.id,
|
|
5599
|
+
});
|
|
5600
|
+
}
|
|
5601
|
+
}
|
|
5602
|
+
|
|
5603
|
+
const runEntry = schedulerMod.recordScheduleRun(sched.id, item.id);
|
|
5604
|
+
try {
|
|
5605
|
+
shared.mutateControl(control => ({ ...control, _wakeupAt: Date.now() }));
|
|
5606
|
+
} catch (e) {
|
|
5607
|
+
shared.log('warn', `Schedule run-now wakeup failed for ${sched.id}: ${e.message}`);
|
|
5608
|
+
}
|
|
5609
|
+
invalidateStatusCache();
|
|
5610
|
+
return jsonReply(res, 200, {
|
|
5611
|
+
ok: true,
|
|
5612
|
+
id: item.id,
|
|
5613
|
+
scheduleId: sched.id,
|
|
5614
|
+
lastRun: runEntry?.lastRun || null,
|
|
5615
|
+
...(meeting ? { meetingId: meeting.id } : {}),
|
|
5616
|
+
});
|
|
5617
|
+
}
|
|
5618
|
+
|
|
5449
5619
|
async function handleSchedulesParseNatural(req, res) {
|
|
5450
5620
|
const body = await readBody(req);
|
|
5451
5621
|
const { text } = body;
|
|
@@ -6427,6 +6597,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6427
6597
|
{ method: 'POST', path: '/api/schedules', desc: 'Create a new schedule', params: 'cron, title, id?, type?, project?, agent?, description?, priority?, enabled?', handler: handleSchedulesCreate },
|
|
6428
6598
|
{ method: 'POST', path: '/api/schedules/update', desc: 'Update an existing schedule', params: 'id, cron?, title?, type?, project?, agent?, description?, priority?, enabled?', handler: handleSchedulesUpdate },
|
|
6429
6599
|
{ method: 'POST', path: '/api/schedules/delete', desc: 'Delete a schedule', params: 'id', handler: handleSchedulesDelete },
|
|
6600
|
+
{ method: 'POST', path: '/api/schedules/run-now', desc: 'Manually enqueue the work item for a schedule', params: 'id', handler: handleSchedulesRunNow },
|
|
6430
6601
|
|
|
6431
6602
|
// Watches
|
|
6432
6603
|
{ method: 'GET', path: '/api/watches', desc: 'List all watches', handler: handleWatchesList },
|
|
@@ -6757,6 +6928,9 @@ module.exports = {
|
|
|
6757
6928
|
_resolveSkillReadPath,
|
|
6758
6929
|
DOC_CHAT_DOCUMENT_DELIMITER,
|
|
6759
6930
|
_ccValidateAction,
|
|
6931
|
+
_findDuplicateWorkItemCreate: findDuplicateWorkItemCreate,
|
|
6932
|
+
_createWorkItemWithDedup: createWorkItemWithDedup,
|
|
6933
|
+
_resolveWorkItemsCreateTarget: resolveWorkItemsCreateTarget,
|
|
6760
6934
|
executeCCActions,
|
|
6761
6935
|
};
|
|
6762
6936
|
|
package/engine/lifecycle.js
CHANGED
|
@@ -1273,12 +1273,31 @@ function reviewPrRefFromCompletion(completion) {
|
|
|
1273
1273
|
return value;
|
|
1274
1274
|
}
|
|
1275
1275
|
|
|
1276
|
+
function reviewPrRefMatchesDispatchTarget(reportedPr, dispatchPr, project) {
|
|
1277
|
+
if (!reportedPr || !dispatchPr) return true;
|
|
1278
|
+
const reportedUrl = typeof reportedPr === 'object' ? reportedPr.url || '' : String(reportedPr || '');
|
|
1279
|
+
const dispatchUrl = typeof dispatchPr === 'object' ? dispatchPr.url || '' : String(dispatchPr || '');
|
|
1280
|
+
const reportedId = shared.getCanonicalPrId(project, reportedPr, reportedUrl);
|
|
1281
|
+
const dispatchId = shared.getCanonicalPrId(project, dispatchPr, dispatchUrl);
|
|
1282
|
+
if (!reportedId || !dispatchId || reportedId === dispatchId) return true;
|
|
1283
|
+
|
|
1284
|
+
const reportedNumber = shared.getPrNumber(reportedPr);
|
|
1285
|
+
const dispatchNumber = shared.getPrNumber(dispatchPr);
|
|
1286
|
+
if (reportedNumber == null || dispatchNumber == null || reportedNumber !== dispatchNumber) return false;
|
|
1287
|
+
|
|
1288
|
+
const reportedScoped = !/^PR-\d+$/i.test(reportedId);
|
|
1289
|
+
const dispatchScoped = !/^PR-\d+$/i.test(dispatchId);
|
|
1290
|
+
return !(reportedScoped && dispatchScoped);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1276
1293
|
function centralPrPath() {
|
|
1277
1294
|
return path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json');
|
|
1278
1295
|
}
|
|
1279
1296
|
|
|
1280
1297
|
function resolveReviewPrContext(pr, project, config, structuredCompletion = null) {
|
|
1281
|
-
const
|
|
1298
|
+
const reportedPr = reviewPrRefFromCompletion(structuredCompletion);
|
|
1299
|
+
if (reportedPr && pr && !reviewPrRefMatchesDispatchTarget(reportedPr, pr, project)) return null;
|
|
1300
|
+
const refs = reportedPr ? [reportedPr] : [pr].filter(Boolean);
|
|
1282
1301
|
if (refs.length === 0) return null;
|
|
1283
1302
|
|
|
1284
1303
|
const projects = shared.getProjects(config);
|
|
@@ -1292,6 +1311,8 @@ function resolveReviewPrContext(pr, project, config, structuredCompletion = null
|
|
|
1292
1311
|
const prPath = shared.projectPrPath(candidateProject);
|
|
1293
1312
|
const prs = safeJson(prPath) || [];
|
|
1294
1313
|
for (const ref of refs) {
|
|
1314
|
+
const refUrl = typeof ref === 'object' ? ref.url || '' : String(ref || '');
|
|
1315
|
+
if (!shared.isPrCompatibleWithProject(candidateProject, ref, refUrl)) continue;
|
|
1295
1316
|
const target = shared.findPrRecord(prs, ref, candidateProject);
|
|
1296
1317
|
if (target) return { pr: { ...target }, project: candidateProject, prPath };
|
|
1297
1318
|
}
|
|
@@ -1299,11 +1320,14 @@ function resolveReviewPrContext(pr, project, config, structuredCompletion = null
|
|
|
1299
1320
|
|
|
1300
1321
|
const centralPath = centralPrPath();
|
|
1301
1322
|
const centralPrs = safeJson(centralPath) || [];
|
|
1302
|
-
|
|
1323
|
+
const centralRefs = reportedPr ? [reportedPr] : refs;
|
|
1324
|
+
for (const ref of centralRefs) {
|
|
1303
1325
|
const target = shared.findPrRecord(centralPrs, ref, null);
|
|
1304
1326
|
if (target) return { pr: { ...target }, project: null, prPath: centralPath };
|
|
1305
1327
|
}
|
|
1306
1328
|
|
|
1329
|
+
if (reportedPr) return null;
|
|
1330
|
+
|
|
1307
1331
|
return pr?.id
|
|
1308
1332
|
? { pr, project: project || null, prPath: project ? shared.projectPrPath(project) : centralPath }
|
|
1309
1333
|
: null;
|
|
@@ -1312,6 +1336,12 @@ function resolveReviewPrContext(pr, project, config, structuredCompletion = null
|
|
|
1312
1336
|
async function updatePrAfterReview(agentId, pr, project, config, resultSummary, structuredCompletion = null, dispatchItem = null) {
|
|
1313
1337
|
|
|
1314
1338
|
if (!config) config = getConfig();
|
|
1339
|
+
const completionStatus = normalizeCompletionStatus(structuredCompletion?.status);
|
|
1340
|
+
if (completionStatus && NON_TERMINAL_COMPLETION_STATUSES.has(completionStatus)) {
|
|
1341
|
+
const target = pr?.id || reviewPrRefFromCompletion(structuredCompletion) || 'unknown PR';
|
|
1342
|
+
log('warn', `Skipping review update for ${target}: completion status is ${structuredCompletion.status}`);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1315
1345
|
const reviewContext = resolveReviewPrContext(pr, project, config, structuredCompletion);
|
|
1316
1346
|
if (!reviewContext?.pr?.id) {
|
|
1317
1347
|
const reportedPr = reviewPrRefFromCompletion(structuredCompletion);
|
|
@@ -1349,6 +1379,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
1349
1379
|
}
|
|
1350
1380
|
|
|
1351
1381
|
let updatedTarget = null;
|
|
1382
|
+
const reviewNote = String(resultSummary || '').trim();
|
|
1352
1383
|
shared.mutateJsonFileLocked(prPath, (prs) => {
|
|
1353
1384
|
if (!Array.isArray(prs)) return prs;
|
|
1354
1385
|
const target = shared.findPrRecord(prs, reviewPr, reviewProject);
|
|
@@ -1365,7 +1396,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
1365
1396
|
target.minionsReview = {
|
|
1366
1397
|
reviewer: reviewerName,
|
|
1367
1398
|
reviewedAt: ts(),
|
|
1368
|
-
note:
|
|
1399
|
+
note: reviewNote,
|
|
1369
1400
|
dispatchId: dispatchItem?.id || structuredCompletion?.dispatchId || null,
|
|
1370
1401
|
sourceItem: dispatchItem?.meta?.item?.id || null,
|
|
1371
1402
|
// Preserve fixedAt across re-reviews so the poller guard knows a fix was pushed.
|
|
@@ -1388,7 +1419,14 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
1388
1419
|
}
|
|
1389
1420
|
|
|
1390
1421
|
log('info', `Updated ${reviewPr.id} → minions review: ${postReviewStatus || 'waiting'} by ${reviewerName}`);
|
|
1391
|
-
if (updatedTarget)
|
|
1422
|
+
if (updatedTarget) {
|
|
1423
|
+
createReviewFeedbackForAuthor(agentId, updatedTarget, config, {
|
|
1424
|
+
reviewContent: reviewNote,
|
|
1425
|
+
project: reviewProject,
|
|
1426
|
+
dispatchId: dispatchItem?.id || structuredCompletion?.dispatchId || null,
|
|
1427
|
+
sourceItem: dispatchItem?.meta?.item?.id || null,
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1392
1430
|
}
|
|
1393
1431
|
|
|
1394
1432
|
function updatePrAfterFix(pr, project, source) {
|
|
@@ -1838,28 +1876,35 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config, opts = {}) {
|
|
|
1838
1876
|
const authorAgentId = pr.agent.toLowerCase();
|
|
1839
1877
|
if (!config.agents[authorAgentId]) return;
|
|
1840
1878
|
const today = dateStamp();
|
|
1841
|
-
const
|
|
1842
|
-
|
|
1843
|
-
if (
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1879
|
+
const project = opts.project || opts.dispatchItem?.meta?.project || null;
|
|
1880
|
+
let reviewContent = String(opts.reviewContent || '').trim();
|
|
1881
|
+
if (reviewContent) {
|
|
1882
|
+
if (!reviewContentMatchesPr(reviewContent, pr, project)) {
|
|
1883
|
+
log('warn', `Skipped review feedback for ${pr.id}: review content references a different PR`);
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
} else {
|
|
1887
|
+
const inboxFiles = getInboxFiles();
|
|
1888
|
+
const reviewFiles = inboxFiles.filter(f => f.includes(reviewerAgentId) && f.includes(today));
|
|
1889
|
+
if (reviewFiles.length === 0) return;
|
|
1890
|
+
const matchedReviewContent = [];
|
|
1891
|
+
for (const f of reviewFiles) {
|
|
1892
|
+
const content = safeRead(path.join(INBOX_DIR, f));
|
|
1893
|
+
if (!content) continue;
|
|
1894
|
+
if (!reviewFeedbackSourceMatches({
|
|
1895
|
+
fileName: f,
|
|
1896
|
+
content,
|
|
1897
|
+
reviewerAgentId,
|
|
1898
|
+
pr,
|
|
1899
|
+
dispatchItem: opts.dispatchItem,
|
|
1900
|
+
structuredCompletion: opts.structuredCompletion,
|
|
1901
|
+
})) continue;
|
|
1902
|
+
matchedReviewContent.push(content);
|
|
1903
|
+
}
|
|
1904
|
+
if (matchedReviewContent.length === 0) return;
|
|
1905
|
+
reviewContent = matchedReviewContent.join('\n\n');
|
|
1906
|
+
}
|
|
1860
1907
|
const prSlug = shared.safeSlugComponent(pr.id, 60);
|
|
1861
|
-
const feedbackFile = `feedback-${authorAgentId}-from-${reviewerAgentId}-${prSlug}-${today}.md`;
|
|
1862
|
-
const feedbackPath = shared.uniquePath(path.join(INBOX_DIR, feedbackFile));
|
|
1863
1908
|
const content = `# Review Feedback for ${config.agents[authorAgentId]?.name || authorAgentId}\n\n` +
|
|
1864
1909
|
`**PR:** ${pr.id} — ${pr.title || ''}\n` +
|
|
1865
1910
|
`**Reviewer:** ${config.agents[reviewerAgentId]?.name || reviewerAgentId}\n` +
|
|
@@ -1868,7 +1913,14 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config, opts = {}) {
|
|
|
1868
1913
|
`## Action Required\n\nRead this feedback carefully. When you work on similar tasks in the future, ` +
|
|
1869
1914
|
`avoid the patterns flagged here. If you are assigned to fix this PR, ` +
|
|
1870
1915
|
`address every point raised above.\n`;
|
|
1871
|
-
shared.
|
|
1916
|
+
shared.writeToInbox('feedback', `${authorAgentId}-from-${reviewerAgentId}-${prSlug}`, content, null, {
|
|
1917
|
+
sourcePr: pr.id,
|
|
1918
|
+
reviewer: reviewerAgentId,
|
|
1919
|
+
author: authorAgentId,
|
|
1920
|
+
dispatchId: opts.dispatchId || opts.dispatchItem?.id || opts.structuredCompletion?.dispatchId || null,
|
|
1921
|
+
sourceItem: opts.sourceItem || opts.dispatchItem?.meta?.item?.id || null,
|
|
1922
|
+
project: project?.name || null,
|
|
1923
|
+
});
|
|
1872
1924
|
log('info', `Created review feedback for ${authorAgentId} from ${reviewerAgentId} on ${pr.id}`);
|
|
1873
1925
|
}
|
|
1874
1926
|
|
|
@@ -2235,6 +2287,33 @@ function reviewVerdictFromCompletion(completion) {
|
|
|
2235
2287
|
return normalizeReviewVerdict(completion.verdict || completion.review_verdict || completion.reviewVerdict);
|
|
2236
2288
|
}
|
|
2237
2289
|
|
|
2290
|
+
function reviewContentMatchesPr(content, pr, project) {
|
|
2291
|
+
const text = String(content || '').trim();
|
|
2292
|
+
if (!text) return false;
|
|
2293
|
+
const targetId = shared.getCanonicalPrId(project, pr, pr?.url || '');
|
|
2294
|
+
const targetNumber = shared.getPrNumber(pr);
|
|
2295
|
+
if (!targetId) return true;
|
|
2296
|
+
|
|
2297
|
+
const explicitRefs = new Set();
|
|
2298
|
+
for (const match of text.matchAll(/\b(?:github|ado):[A-Za-z0-9._~/-]+#\d+\b/g)) {
|
|
2299
|
+
explicitRefs.add(shared.getCanonicalPrId(project, match[0]));
|
|
2300
|
+
}
|
|
2301
|
+
for (const match of text.matchAll(/https?:\/\/[^\s)>"]+(?:\/pull\/|\/pullrequest\/)\d+[^\s)>"]*/gi)) {
|
|
2302
|
+
const url = match[0].replace(/[.,;:]+$/g, '');
|
|
2303
|
+
explicitRefs.add(shared.getCanonicalPrId(project, url, url));
|
|
2304
|
+
}
|
|
2305
|
+
if (explicitRefs.size > 0) return explicitRefs.size === 1 && explicitRefs.has(targetId);
|
|
2306
|
+
|
|
2307
|
+
const mentionedNumbers = new Set();
|
|
2308
|
+
for (const match of text.matchAll(/\bPR\s*(?:#|-)\s*(\d+)\b/gi)) {
|
|
2309
|
+
mentionedNumbers.add(parseInt(match[1], 10));
|
|
2310
|
+
}
|
|
2311
|
+
if (mentionedNumbers.size > 0 && targetNumber != null) {
|
|
2312
|
+
return mentionedNumbers.size === 1 && mentionedNumbers.has(targetNumber);
|
|
2313
|
+
}
|
|
2314
|
+
return true;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2238
2317
|
function writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredCompletion, resultSummary, exitCode) {
|
|
2239
2318
|
if (!dispatchItem?.id || !outcome) {
|
|
2240
2319
|
log('warn', 'Cannot write non-clean agent report without dispatch id and outcome');
|
|
@@ -2663,7 +2742,10 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
2663
2742
|
// (retryCount was being deleted by done-marking before the check could read it)
|
|
2664
2743
|
// Review verdict check similarly moved before updateWorkItemStatus(DONE) — same root cause.
|
|
2665
2744
|
|
|
2666
|
-
|
|
2745
|
+
const hardContractFail = completionContractFailure?.severity === 'hard'
|
|
2746
|
+
|| completionContractFailure?.nonTerminal === true;
|
|
2747
|
+
const finalResult = hardContractFail ? DISPATCH_RESULT.ERROR : (effectiveSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR);
|
|
2748
|
+
if (type === WORK_TYPE.REVIEW && finalResult === DISPATCH_RESULT.SUCCESS && !skipDoneStatus) {
|
|
2667
2749
|
await updatePrAfterReview(agentId, meta?.pr, meta?.project, config, resultSummary, structuredCompletion, dispatchItem);
|
|
2668
2750
|
} else if (type === WORK_TYPE.REVIEW) {
|
|
2669
2751
|
log('warn', `Skipping PR review metadata update for ${meta?.pr?.id || meta?.pr?.url || '(unknown PR)'} because review dispatch ${dispatchItem.id} did not complete cleanly`);
|
|
@@ -2686,9 +2768,6 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
2686
2768
|
}
|
|
2687
2769
|
}
|
|
2688
2770
|
checkForLearnings(agentId, config.agents[agentId], dispatchItem.task);
|
|
2689
|
-
const hardContractFail = completionContractFailure?.severity === 'hard'
|
|
2690
|
-
|| completionContractFailure?.nonTerminal === true;
|
|
2691
|
-
const finalResult = hardContractFail ? DISPATCH_RESULT.ERROR : (effectiveSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR);
|
|
2692
2771
|
if (finalResult === DISPATCH_RESULT.SUCCESS) {
|
|
2693
2772
|
extractSkillsFromOutput(stdout, agentId, dispatchItem, config);
|
|
2694
2773
|
// Also scan inbox notes for skill blocks — agents often write skills to inbox, not stdout
|
package/engine/scheduler.js
CHANGED
|
@@ -140,6 +140,42 @@ function shouldRunNow(schedule, lastRunAt) {
|
|
|
140
140
|
return true;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
function createScheduledWorkItem(sched) {
|
|
144
|
+
if (!sched || !sched.id || !sched.title) {
|
|
145
|
+
throw new Error('schedule id and title are required');
|
|
146
|
+
}
|
|
147
|
+
const workItemId = `sched-${sched.id}-${Date.now()}`;
|
|
148
|
+
return {
|
|
149
|
+
id: workItemId,
|
|
150
|
+
title: resolveScheduleTemplateVars(sched.title),
|
|
151
|
+
type: routing.normalizeWorkType(sched.type, WORK_TYPE.IMPLEMENT),
|
|
152
|
+
priority: sched.priority || 'medium',
|
|
153
|
+
description: resolveScheduleTemplateVars(sched.description || sched.title),
|
|
154
|
+
status: WI_STATUS.PENDING,
|
|
155
|
+
created: ts(),
|
|
156
|
+
createdBy: 'scheduler',
|
|
157
|
+
agent: sched.agent || null,
|
|
158
|
+
...(sched.agentLock === true || sched.hardAgent === true ? { agentLock: true } : {}),
|
|
159
|
+
project: sched.project || null,
|
|
160
|
+
_scheduleId: sched.id,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function writeScheduleRunEntry(runs, scheduleId, workItemId) {
|
|
165
|
+
const existing = typeof runs[scheduleId] === 'object' && runs[scheduleId] ? runs[scheduleId] : {};
|
|
166
|
+
runs[scheduleId] = { ...existing, lastRun: ts(), lastWorkItemId: workItemId };
|
|
167
|
+
return runs[scheduleId];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function recordScheduleRun(scheduleId, workItemId) {
|
|
171
|
+
let entry = null;
|
|
172
|
+
mutateJsonFileLocked(SCHEDULE_RUNS_PATH, (runs) => {
|
|
173
|
+
entry = writeScheduleRunEntry(runs, scheduleId, workItemId);
|
|
174
|
+
return runs;
|
|
175
|
+
}, { defaultValue: {} });
|
|
176
|
+
return entry;
|
|
177
|
+
}
|
|
178
|
+
|
|
143
179
|
/**
|
|
144
180
|
* Discover work items from configured schedules.
|
|
145
181
|
* @param {object} config -- full config object
|
|
@@ -164,21 +200,8 @@ function discoverScheduledWork(config) {
|
|
|
164
200
|
// Substitute schedule-time template vars (e.g. {{date}}) before the work
|
|
165
201
|
// item is written — single-pass playbook rendering can't reach placeholders
|
|
166
202
|
// embedded inside task_description, so they must be resolved up front.
|
|
167
|
-
const
|
|
168
|
-
work.push(
|
|
169
|
-
id: workItemId,
|
|
170
|
-
title: resolveScheduleTemplateVars(sched.title),
|
|
171
|
-
type: routing.normalizeWorkType(sched.type, WORK_TYPE.IMPLEMENT),
|
|
172
|
-
priority: sched.priority || 'medium',
|
|
173
|
-
description: resolveScheduleTemplateVars(sched.description || sched.title),
|
|
174
|
-
status: WI_STATUS.PENDING,
|
|
175
|
-
created: ts(),
|
|
176
|
-
createdBy: 'scheduler',
|
|
177
|
-
agent: sched.agent || null,
|
|
178
|
-
...(sched.agentLock === true || sched.hardAgent === true ? { agentLock: true } : {}),
|
|
179
|
-
project: sched.project || null,
|
|
180
|
-
_scheduleId: sched.id,
|
|
181
|
-
});
|
|
203
|
+
const workItem = createScheduledWorkItem(sched);
|
|
204
|
+
work.push(workItem);
|
|
182
205
|
|
|
183
206
|
// Record run time AND work-item ID at dispatch time — preserve existing
|
|
184
207
|
// completion fields (lastResult, lastCompletedAt). Writing lastWorkItemId
|
|
@@ -186,12 +209,20 @@ function discoverScheduledWork(config) {
|
|
|
186
209
|
// the dispatched work item crashes or the engine restarts before
|
|
187
210
|
// lifecycle.runPostCompletionHooks runs. This is the fix that closes
|
|
188
211
|
// the double-fire window alongside the same-minute guard (W-mo3zu273f8tm).
|
|
189
|
-
|
|
190
|
-
runs[sched.id] = { ...existing, lastRun: ts(), lastWorkItemId: workItemId };
|
|
212
|
+
writeScheduleRunEntry(runs, sched.id, workItem.id);
|
|
191
213
|
}
|
|
192
214
|
}, { defaultValue: {} });
|
|
193
215
|
|
|
194
216
|
return work;
|
|
195
217
|
}
|
|
196
218
|
|
|
197
|
-
module.exports = {
|
|
219
|
+
module.exports = {
|
|
220
|
+
parseCronExpr,
|
|
221
|
+
parseCronField,
|
|
222
|
+
shouldRunNow,
|
|
223
|
+
discoverScheduledWork,
|
|
224
|
+
createScheduledWorkItem,
|
|
225
|
+
recordScheduleRun,
|
|
226
|
+
resolveScheduleTemplateVars,
|
|
227
|
+
SCHEDULE_RUNS_PATH,
|
|
228
|
+
};
|
package/engine/shared.js
CHANGED
|
@@ -770,6 +770,7 @@ const ENGINE_DEFAULTS = {
|
|
|
770
770
|
worktreeCreateRetries: 1, // retry once on transient timeout/lock races
|
|
771
771
|
worktreeRoot: '../worktrees',
|
|
772
772
|
worktreeCountCacheTtl: 30000, // 30s — TTL for cached _countWorktrees() result in dashboard
|
|
773
|
+
workItemCreateDedupWindowMs: 15 * 60 * 1000, // 15min — collapse duplicate CC/API create races
|
|
773
774
|
idleAlertMinutes: 15,
|
|
774
775
|
fanOutTimeout: null, // falls back to agentTimeout
|
|
775
776
|
restartGracePeriod: 1200000, // 20min
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1704",
|
|
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"
|