@yemi33/minions 0.1.1852 → 0.1.1853
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 +9 -1
- package/dashboard/js/render-watches.js +91 -25
- package/dashboard.js +5 -0
- package/engine/ado.js +3 -3
- package/engine/cli.js +2 -2
- package/engine/github.js +6 -6
- package/engine/shared.js +37 -3
- package/engine/teams.js +6 -6
- package/engine/watches.js +449 -82
- package/engine.js +40 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1853 (2026-05-10)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- generalize watches infra to support arbitrary target types (#2322)
|
|
7
|
+
- use safeJsonArr in engine/watches.js loadWatches (footgun #2) (#2320)
|
|
4
8
|
|
|
5
9
|
### Fixes
|
|
10
|
+
- use safeJsonObj in engine/teams.js (footgun #2) (#2318)
|
|
11
|
+
- use safeJsonArr in engine/github.js PR poller (#2317)
|
|
12
|
+
- guard empty projects[] in cli.js dispatch helpers (#2316)
|
|
13
|
+
- use safeJsonArr in engine/ado.js PR poller (#2315)
|
|
6
14
|
- use DONE_STATUSES constant in getAgents lastAction (#2314)
|
|
7
15
|
|
|
8
16
|
## 0.1.1851 (2026-05-10)
|
|
@@ -9,9 +9,23 @@ const _WATCH_STATUS_BADGES = {
|
|
|
9
9
|
expired: '<span class="pr-badge" style="background:rgba(139,148,158,0.15);color:var(--muted);border-color:var(--muted)">expired</span>',
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// Cache of target types fetched from /api/watches/target-types. Populated on
|
|
13
|
+
// first modal open and refreshed per modal show. Each entry:
|
|
14
|
+
// { value, label, conditions: [...], description }
|
|
15
|
+
let _watchTargetTypesCache = null;
|
|
16
|
+
|
|
17
|
+
// Built-in fallback labels — used when the API response hasn't loaded yet, or
|
|
18
|
+
// for legacy targetTypes/conditions that may appear on stored watches but not
|
|
19
|
+
// in the live registry. The registry from the API is authoritative when present.
|
|
12
20
|
const _WATCH_TARGET_LABELS = {
|
|
13
|
-
pr: '
|
|
21
|
+
pr: 'Pull Request',
|
|
14
22
|
'work-item': 'Work Item',
|
|
23
|
+
meeting: 'Meeting',
|
|
24
|
+
plan: 'Plan / PRD',
|
|
25
|
+
schedule: 'Schedule',
|
|
26
|
+
pipeline: 'Pipeline',
|
|
27
|
+
dispatch: 'Dispatch',
|
|
28
|
+
agent: 'Agent',
|
|
15
29
|
};
|
|
16
30
|
|
|
17
31
|
const _WATCH_CONDITION_LABELS = {
|
|
@@ -24,8 +38,30 @@ const _WATCH_CONDITION_LABELS = {
|
|
|
24
38
|
any: 'Any Change',
|
|
25
39
|
'new-comments': 'New Comments',
|
|
26
40
|
'vote-change': 'Vote Change',
|
|
41
|
+
concluded: 'Concluded',
|
|
42
|
+
approved: 'Approved',
|
|
43
|
+
rejected: 'Rejected',
|
|
44
|
+
'stage-complete': 'Stage Complete',
|
|
45
|
+
ran: 'Ran',
|
|
46
|
+
enabled: 'Enabled',
|
|
47
|
+
disabled: 'Disabled',
|
|
48
|
+
'activity-change': 'Activity Change',
|
|
27
49
|
};
|
|
28
50
|
|
|
51
|
+
function _conditionLabel(cond) {
|
|
52
|
+
if (_WATCH_CONDITION_LABELS[cond]) return _WATCH_CONDITION_LABELS[cond];
|
|
53
|
+
// Title-case unknown conditions for graceful fallback
|
|
54
|
+
return String(cond || '').replace(/[-_]+/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function _targetTypeLabel(type) {
|
|
58
|
+
if (_watchTargetTypesCache) {
|
|
59
|
+
const hit = _watchTargetTypesCache.find(t => t.value === type);
|
|
60
|
+
if (hit) return hit.label;
|
|
61
|
+
}
|
|
62
|
+
return _WATCH_TARGET_LABELS[type] || (type || '');
|
|
63
|
+
}
|
|
64
|
+
|
|
29
65
|
function _intervalToHuman(ms) {
|
|
30
66
|
if (!ms) return 'default';
|
|
31
67
|
const sec = Math.floor(ms / 1000);
|
|
@@ -83,8 +119,8 @@ function renderWatches(watchesData) {
|
|
|
83
119
|
for (var i = 0; i < pageItems.length; i++) {
|
|
84
120
|
var w = pageItems[i];
|
|
85
121
|
var statusBadge = _WATCH_STATUS_BADGES[w.status] || escHtml(w.status || 'unknown');
|
|
86
|
-
var targetLabel =
|
|
87
|
-
var condLabel =
|
|
122
|
+
var targetLabel = escHtml(_targetTypeLabel(w.targetType));
|
|
123
|
+
var condLabel = escHtml(_conditionLabel(w.condition));
|
|
88
124
|
var lastChecked = w.last_checked ? timeAgo(w.last_checked) : 'never';
|
|
89
125
|
var lastTriggered = w.last_triggered ? timeAgo(w.last_triggered) : 'never';
|
|
90
126
|
var triggerInfo = (w.triggerCount || 0) + (w.stopAfter > 0 ? '/' + w.stopAfter : '');
|
|
@@ -140,8 +176,8 @@ function openWatchDetail(id) {
|
|
|
140
176
|
var lastChecked = w.last_checked ? formatLocalDateTime(w.last_checked) : 'never';
|
|
141
177
|
var lastTriggered = w.last_triggered ? formatLocalDateTime(w.last_triggered) : 'never';
|
|
142
178
|
var createdAt = w.created_at ? formatLocalDateTime(w.created_at) : 'unknown';
|
|
143
|
-
var targetLabel =
|
|
144
|
-
var condLabel =
|
|
179
|
+
var targetLabel = _targetTypeLabel(w.targetType);
|
|
180
|
+
var condLabel = _conditionLabel(w.condition);
|
|
145
181
|
|
|
146
182
|
document.getElementById('modal-title').innerHTML = escHtml(w.description || w.target) +
|
|
147
183
|
' <div style="display:flex;gap:4px;margin-top:4px">' +
|
|
@@ -220,33 +256,33 @@ function deleteWatch(id) {
|
|
|
220
256
|
|
|
221
257
|
// ─── Create Watch Modal ─────────────────────────────────────────────────────
|
|
222
258
|
|
|
259
|
+
// Build the form HTML using the (cached) target-type registry. Target type
|
|
260
|
+
// and condition options come from the live registry — no hardcoded list — so
|
|
261
|
+
// new types registered server-side appear here without UI changes.
|
|
223
262
|
function _watchFormHtml() {
|
|
224
263
|
var inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
|
|
225
264
|
|
|
226
|
-
var targetTypes =
|
|
227
|
-
|
|
228
|
-
{ value: '
|
|
229
|
-
|
|
230
|
-
var
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
];
|
|
241
|
-
var ttOpts = targetTypes.map(function(t) { return '<option value="' + t.value + '">' + t.label + '</option>'; }).join('');
|
|
242
|
-
var condOpts = conditions.map(function(c) { return '<option value="' + c.value + '">' + c.label + '</option>'; }).join('');
|
|
265
|
+
var targetTypes = _watchTargetTypesCache && _watchTargetTypesCache.length
|
|
266
|
+
? _watchTargetTypesCache
|
|
267
|
+
: [{ value: 'pr', label: 'Pull Request', conditions: ['merged','build-fail','build-pass','status-change','any','new-comments','vote-change'] }];
|
|
268
|
+
|
|
269
|
+
var ttOpts = targetTypes.map(function(t) {
|
|
270
|
+
return '<option value="' + escHtml(t.value) + '">' + escHtml(t.label) + '</option>';
|
|
271
|
+
}).join('');
|
|
272
|
+
|
|
273
|
+
// Condition options for the initially-selected target type (first entry).
|
|
274
|
+
var initialConds = (targetTypes[0] && targetTypes[0].conditions) || [];
|
|
275
|
+
var condOpts = initialConds.map(function(c) {
|
|
276
|
+
return '<option value="' + escHtml(c) + '">' + escHtml(_conditionLabel(c)) + '</option>';
|
|
277
|
+
}).join('');
|
|
278
|
+
|
|
243
279
|
var agentOpts = '<option value="">human</option>' + (cmdAgents || []).map(function(a) { return '<option value="' + escHtml(a.id) + '">' + escHtml(a.name) + '</option>'; }).join('');
|
|
244
280
|
var projOpts = '<option value="">Any</option>' + (cmdProjects || []).map(function(p) { return '<option value="' + escHtml(p.name) + '">' + escHtml(p.name) + '</option>'; }).join('');
|
|
245
281
|
|
|
246
282
|
return '<div style="display:flex;flex-direction:column;gap:12px;font-family:inherit">' +
|
|
247
283
|
'<div id="watch-form-error" style="display:none;color:var(--red);font-size:12px;padding:6px 10px;background:rgba(255,50,50,0.1);border-radius:var(--radius-sm)"></div>' +
|
|
248
|
-
'<label style="color:var(--text);font-size:var(--text-md)">Target (PR number
|
|
249
|
-
'<label style="color:var(--text);font-size:var(--text-md)">Target Type<select id="watch-edit-target-type" style="' + inputStyle + '">' + ttOpts + '</select></label>' +
|
|
284
|
+
'<label style="color:var(--text);font-size:var(--text-md)">Target (e.g. PR number, work item ID, meeting/plan/schedule/pipeline/dispatch/agent ID)<input id="watch-edit-target" placeholder="e.g. 1057, W-abc123, M-xyz, schedule-42" style="' + inputStyle + '"></label>' +
|
|
285
|
+
'<label style="color:var(--text);font-size:var(--text-md)">Target Type<select id="watch-edit-target-type" style="' + inputStyle + '" onchange="_updateWatchConditionOptions()">' + ttOpts + '</select></label>' +
|
|
250
286
|
'<label style="color:var(--text);font-size:var(--text-md)">Condition<select id="watch-edit-condition" style="' + inputStyle + '">' + condOpts + '</select></label>' +
|
|
251
287
|
'<label style="color:var(--text);font-size:var(--text-md)">Check Interval <span style="font-size:10px;color:var(--muted)">(e.g. 5m, 15m, 1h — default 5m)</span><input id="watch-edit-interval" placeholder="5m" style="' + inputStyle + '"></label>' +
|
|
252
288
|
'<label style="color:var(--text);font-size:var(--text-md)">Owner (who gets notified)<select id="watch-edit-owner" style="' + inputStyle + '">' + agentOpts + '</select></label>' +
|
|
@@ -257,7 +293,19 @@ function _watchFormHtml() {
|
|
|
257
293
|
'</div>';
|
|
258
294
|
}
|
|
259
295
|
|
|
260
|
-
|
|
296
|
+
// Refresh the condition <select> based on the currently selected target type.
|
|
297
|
+
function _updateWatchConditionOptions() {
|
|
298
|
+
var ttSel = document.getElementById('watch-edit-target-type');
|
|
299
|
+
var condSel = document.getElementById('watch-edit-condition');
|
|
300
|
+
if (!ttSel || !condSel || !_watchTargetTypesCache) return;
|
|
301
|
+
var entry = _watchTargetTypesCache.find(function(t) { return t.value === ttSel.value; });
|
|
302
|
+
var conditions = (entry && entry.conditions) || [];
|
|
303
|
+
condSel.innerHTML = conditions.map(function(c) {
|
|
304
|
+
return '<option value="' + escHtml(c) + '">' + escHtml(_conditionLabel(c)) + '</option>';
|
|
305
|
+
}).join('');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function _renderCreateWatchModal() {
|
|
261
309
|
document.getElementById('modal-title').innerHTML = 'Create Watch' +
|
|
262
310
|
' <button class="pr-pager-btn" style="font-size:10px;padding:2px 12px;color:var(--green);border-color:var(--green);margin-left:8px" onclick="submitWatch()">Create</button>';
|
|
263
311
|
document.getElementById('modal-body').innerHTML = _watchFormHtml();
|
|
@@ -266,6 +314,23 @@ function openCreateWatchModal() {
|
|
|
266
314
|
document.getElementById('modal').classList.add('open');
|
|
267
315
|
}
|
|
268
316
|
|
|
317
|
+
function openCreateWatchModal() {
|
|
318
|
+
// Render an initial form immediately (uses cached target types if present).
|
|
319
|
+
_renderCreateWatchModal();
|
|
320
|
+
// Fetch the live registry and re-render so newly registered target types
|
|
321
|
+
// appear without requiring a page reload.
|
|
322
|
+
fetch('/api/watches/target-types').then(function(res) { return res.json(); }).then(function(data) {
|
|
323
|
+
if (data && Array.isArray(data.targetTypes) && data.targetTypes.length > 0) {
|
|
324
|
+
_watchTargetTypesCache = data.targetTypes;
|
|
325
|
+
// Only re-render if the modal is still showing the create-watch form
|
|
326
|
+
var titleEl = document.getElementById('modal-title');
|
|
327
|
+
if (titleEl && titleEl.textContent.indexOf('Create Watch') === 0) {
|
|
328
|
+
_renderCreateWatchModal();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}).catch(function() { /* fall back to baked-in defaults */ });
|
|
332
|
+
}
|
|
333
|
+
|
|
269
334
|
function submitWatch() {
|
|
270
335
|
var target = (document.getElementById('watch-edit-target') || {}).value || '';
|
|
271
336
|
var targetType = (document.getElementById('watch-edit-target-type') || {}).value || 'pr';
|
|
@@ -324,4 +389,5 @@ window.MinionsWatches = {
|
|
|
324
389
|
deleteWatch: deleteWatch,
|
|
325
390
|
_watchPrev: _watchPrev,
|
|
326
391
|
_watchNext: _watchNext,
|
|
392
|
+
_updateWatchConditionOptions: _updateWatchConditionOptions,
|
|
327
393
|
};
|
package/dashboard.js
CHANGED
|
@@ -6301,6 +6301,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6301
6301
|
return jsonReply(res, 200, { watches: watchesMod.getWatches() });
|
|
6302
6302
|
}
|
|
6303
6303
|
|
|
6304
|
+
async function handleWatchesTargetTypes(req, res) {
|
|
6305
|
+
return jsonReply(res, 200, { targetTypes: watchesMod.listTargetTypes() });
|
|
6306
|
+
}
|
|
6307
|
+
|
|
6304
6308
|
async function handleWatchesCreate(req, res) {
|
|
6305
6309
|
const body = await readBody(req);
|
|
6306
6310
|
const { target, targetType, condition, interval, owner, description, project, notify, stopAfter, onNotMet } = body;
|
|
@@ -7313,6 +7317,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7313
7317
|
|
|
7314
7318
|
// Watches
|
|
7315
7319
|
{ method: 'GET', path: '/api/watches', desc: 'List all watches', handler: handleWatchesList },
|
|
7320
|
+
{ method: 'GET', path: '/api/watches/target-types', desc: 'List registered watch target types and their valid conditions', handler: handleWatchesTargetTypes },
|
|
7316
7321
|
{ method: 'POST', path: '/api/watches', desc: 'Create a new watch', params: 'target, targetType, condition, interval?, owner?, description?, project?, notify?, stopAfter?, onNotMet?', handler: handleWatchesCreate },
|
|
7317
7322
|
{ method: 'POST', path: '/api/watches/update', desc: 'Update a watch (pause/resume/modify)', params: 'id, status?, interval?, description?, notify?, stopAfter?, onNotMet?, condition?', handler: handleWatchesUpdate },
|
|
7318
7323
|
{ method: 'POST', path: '/api/watches/delete', desc: 'Delete a watch', params: 'id', handler: handleWatchesDelete },
|
package/engine/ado.js
CHANGED
|
@@ -1128,7 +1128,7 @@ async function reconcilePrs(config) {
|
|
|
1128
1128
|
if (adoPrs.length === 0) continue;
|
|
1129
1129
|
|
|
1130
1130
|
const prPath = shared.projectPrPath(project);
|
|
1131
|
-
const existingPrs = shared.
|
|
1131
|
+
const existingPrs = shared.safeJsonArr(prPath);
|
|
1132
1132
|
shared.normalizePrRecords(existingPrs, project);
|
|
1133
1133
|
const existingIds = new Set(existingPrs.map(p => p.id));
|
|
1134
1134
|
let projectAdded = 0;
|
|
@@ -1136,9 +1136,9 @@ async function reconcilePrs(config) {
|
|
|
1136
1136
|
|
|
1137
1137
|
// Load work items to match branches to work item IDs
|
|
1138
1138
|
const wiPath = shared.projectWorkItemsPath(project);
|
|
1139
|
-
const workItems = shared.
|
|
1139
|
+
const workItems = shared.safeJsonArr(wiPath);
|
|
1140
1140
|
const centralWiPath = path.join(shared.MINIONS_DIR, 'work-items.json');
|
|
1141
|
-
const centralItems = shared.
|
|
1141
|
+
const centralItems = shared.safeJsonArr(centralWiPath);
|
|
1142
1142
|
const allItems = [...workItems, ...centralItems];
|
|
1143
1143
|
|
|
1144
1144
|
let projectUpdated = 0;
|
package/engine/cli.js
CHANGED
|
@@ -1164,7 +1164,7 @@ const commands = {
|
|
|
1164
1164
|
console.log(target.error);
|
|
1165
1165
|
return;
|
|
1166
1166
|
}
|
|
1167
|
-
const targetProject = target?.project || projects[0];
|
|
1167
|
+
const targetProject = target?.project || (projects.length > 0 ? projects[0] : null);
|
|
1168
1168
|
const wiPath = projectWorkItemsPath(targetProject);
|
|
1169
1169
|
let item;
|
|
1170
1170
|
mutateWorkItems(wiPath, items => {
|
|
@@ -1212,7 +1212,7 @@ const commands = {
|
|
|
1212
1212
|
console.log(target.error);
|
|
1213
1213
|
return;
|
|
1214
1214
|
}
|
|
1215
|
-
const targetProject = target?.project || projects[0];
|
|
1215
|
+
const targetProject = target?.project || (projects.length > 0 ? projects[0] : null);
|
|
1216
1216
|
|
|
1217
1217
|
if (!targetProject) {
|
|
1218
1218
|
console.log('No projects configured. Run: minions add <dir>');
|
package/engine/github.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const shared = require('./shared');
|
|
8
|
-
const { exec, execAsync, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, mutateJsonFileLocked, MINIONS_DIR, getPrLinks, backfillPrPrdItems, log, ts, dateStamp, PR_STATUS, PR_POLLABLE_STATUSES, createThrottleTracker, getProjectOrg } = shared;
|
|
8
|
+
const { exec, execAsync, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeJsonArr, safeWrite, mutateJsonFileLocked, MINIONS_DIR, getPrLinks, backfillPrPrdItems, log, ts, dateStamp, PR_STATUS, PR_POLLABLE_STATUSES, createThrottleTracker, getProjectOrg } = shared;
|
|
9
9
|
const { getPrs } = require('./queries');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
@@ -320,7 +320,7 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
320
320
|
|
|
321
321
|
// Also poll manually-linked PRs from central pull-requests.json (extract slug from URL)
|
|
322
322
|
const centralPath = path.join(MINIONS_DIR, 'pull-requests.json');
|
|
323
|
-
const centralPrs =
|
|
323
|
+
const centralPrs = safeJsonArr(centralPath);
|
|
324
324
|
const activeCentral = centralPrs.filter(pr => PR_POLLABLE_STATUSES.has(pr.status) && pr.url);
|
|
325
325
|
let centralUpdated = 0;
|
|
326
326
|
const updatedCentralRecords = [];
|
|
@@ -727,7 +727,7 @@ async function reconcilePrs(config) {
|
|
|
727
727
|
if (existingPrs.length === 0) {
|
|
728
728
|
try {
|
|
729
729
|
const wiPath = projectWorkItemsPath(project);
|
|
730
|
-
const wis =
|
|
730
|
+
const wis = safeJsonArr(wiPath);
|
|
731
731
|
if (wis.length === 0) continue;
|
|
732
732
|
} catch { continue; }
|
|
733
733
|
}
|
|
@@ -748,7 +748,7 @@ async function reconcilePrs(config) {
|
|
|
748
748
|
if (ghPrs.length === 0) continue;
|
|
749
749
|
|
|
750
750
|
const prPath = projectPrPath(project);
|
|
751
|
-
const currentPrs =
|
|
751
|
+
const currentPrs = safeJsonArr(prPath);
|
|
752
752
|
shared.normalizePrRecords(currentPrs, project);
|
|
753
753
|
const existingIds = new Set(currentPrs.map(p => p.id));
|
|
754
754
|
let metadataUpdated = 0;
|
|
@@ -756,9 +756,9 @@ async function reconcilePrs(config) {
|
|
|
756
756
|
|
|
757
757
|
// Load work items to match branches
|
|
758
758
|
const wiPath = projectWorkItemsPath(project);
|
|
759
|
-
const workItems =
|
|
759
|
+
const workItems = safeJsonArr(wiPath);
|
|
760
760
|
const centralWiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
761
|
-
const centralItems =
|
|
761
|
+
const centralItems = safeJsonArr(centralWiPath);
|
|
762
762
|
const allItems = [...workItems, ...centralItems];
|
|
763
763
|
|
|
764
764
|
for (const ghPr of ghPrs) {
|
package/engine/shared.js
CHANGED
|
@@ -1553,13 +1553,47 @@ const PR_PENDING_REASON = {
|
|
|
1553
1553
|
|
|
1554
1554
|
// Watch statuses — engine-level persistent watches that survive restarts
|
|
1555
1555
|
const WATCH_STATUS = { ACTIVE: 'active', PAUSED: 'paused', TRIGGERED: 'triggered', EXPIRED: 'expired' };
|
|
1556
|
-
|
|
1557
|
-
|
|
1556
|
+
// Built-in target types. Additional types may be registered at runtime via
|
|
1557
|
+
// engine/watches.registerTargetType — this object lists the types shipped in
|
|
1558
|
+
// the engine for use in switch statements and constants comparisons. The
|
|
1559
|
+
// actual allowlist for createWatch/dashboard validation is the registry in
|
|
1560
|
+
// engine/watches.js (see getTargetTypes()).
|
|
1561
|
+
const WATCH_TARGET_TYPE = {
|
|
1562
|
+
PR: 'pr',
|
|
1563
|
+
WORK_ITEM: 'work-item',
|
|
1564
|
+
MEETING: 'meeting',
|
|
1565
|
+
PLAN: 'plan',
|
|
1566
|
+
SCHEDULE: 'schedule',
|
|
1567
|
+
PIPELINE: 'pipeline',
|
|
1568
|
+
DISPATCH: 'dispatch',
|
|
1569
|
+
AGENT: 'agent',
|
|
1570
|
+
};
|
|
1571
|
+
const WATCH_CONDITION = {
|
|
1572
|
+
MERGED: 'merged',
|
|
1573
|
+
BUILD_FAIL: 'build-fail',
|
|
1574
|
+
BUILD_PASS: 'build-pass',
|
|
1575
|
+
COMPLETED: 'completed',
|
|
1576
|
+
FAILED: 'failed',
|
|
1577
|
+
STATUS_CHANGE: 'status-change',
|
|
1578
|
+
ANY: 'any',
|
|
1579
|
+
NEW_COMMENTS: 'new-comments',
|
|
1580
|
+
VOTE_CHANGE: 'vote-change',
|
|
1581
|
+
// ── Generalized target-type conditions ────────────────────────────────────
|
|
1582
|
+
CONCLUDED: 'concluded', // meeting reached terminal status (completed/archived)
|
|
1583
|
+
APPROVED: 'approved', // plan/PRD approved
|
|
1584
|
+
REJECTED: 'rejected', // plan/PRD rejected
|
|
1585
|
+
STAGE_COMPLETE: 'stage-complete', // pipeline run finished a stage (any new stage flipped to terminal)
|
|
1586
|
+
RAN: 'ran', // schedule lastRun changed
|
|
1587
|
+
ENABLED: 'enabled', // schedule enabled
|
|
1588
|
+
DISABLED: 'disabled', // schedule disabled
|
|
1589
|
+
ACTIVITY_CHANGE: 'activity-change', // agent transitioned status (e.g. idle → working)
|
|
1590
|
+
};
|
|
1558
1591
|
// Absolute conditions auto-expire on first trigger when stopAfter=0 (fire-once semantics).
|
|
1559
|
-
// Change-based conditions (status-change, any) run forever when stopAfter=0.
|
|
1592
|
+
// Change-based conditions (status-change, any, *-change) run forever when stopAfter=0.
|
|
1560
1593
|
const WATCH_ABSOLUTE_CONDITIONS = new Set([
|
|
1561
1594
|
WATCH_CONDITION.MERGED, WATCH_CONDITION.BUILD_FAIL, WATCH_CONDITION.BUILD_PASS,
|
|
1562
1595
|
WATCH_CONDITION.COMPLETED, WATCH_CONDITION.FAILED,
|
|
1596
|
+
WATCH_CONDITION.CONCLUDED, WATCH_CONDITION.APPROVED, WATCH_CONDITION.REJECTED,
|
|
1563
1597
|
]);
|
|
1564
1598
|
|
|
1565
1599
|
/** Update per-agent review metrics (prsApproved/prsRejected). Only writes for configured agents. */
|
package/engine/teams.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 { log, safeRead, safeJson, mutateJsonFileLocked, ENGINE_DEFAULTS } = shared;
|
|
11
|
+
const { log, safeRead, safeJson, safeJsonObj, mutateJsonFileLocked, ENGINE_DEFAULTS } = shared;
|
|
12
12
|
const { ENGINE_DIR, getConfig } = queries;
|
|
13
13
|
const cards = require('./teams-cards');
|
|
14
14
|
|
|
@@ -150,7 +150,7 @@ function saveConversationRef(key, ref) {
|
|
|
150
150
|
*/
|
|
151
151
|
function getConversationRef(key) {
|
|
152
152
|
if (!key) return null;
|
|
153
|
-
const state =
|
|
153
|
+
const state = safeJsonObj(TEAMS_STATE_PATH);
|
|
154
154
|
return state.conversations?.[key]?.ref || null;
|
|
155
155
|
}
|
|
156
156
|
|
|
@@ -466,7 +466,7 @@ async function teamsPostCCResponse(userMessage, ccResponse) {
|
|
|
466
466
|
const card = cards.buildCCResponseCard(userMessage, ccResponse);
|
|
467
467
|
|
|
468
468
|
// Find first available conversation ref
|
|
469
|
-
const state =
|
|
469
|
+
const state = safeJsonObj(TEAMS_STATE_PATH);
|
|
470
470
|
const convKeys = Object.keys(state.conversations || {});
|
|
471
471
|
if (convKeys.length === 0) {
|
|
472
472
|
log('info', 'Teams CC mirror skipped — no conversation refs stored');
|
|
@@ -498,7 +498,7 @@ async function teamsNotifyCompletion(dispatchItem, result, agentId) {
|
|
|
498
498
|
const card = cards.buildCompletionCard(agentId, item, result, prUrl || undefined);
|
|
499
499
|
|
|
500
500
|
// Find first available conversation ref
|
|
501
|
-
const state =
|
|
501
|
+
const state = safeJsonObj(TEAMS_STATE_PATH);
|
|
502
502
|
const convKeys = Object.keys(state.conversations || {});
|
|
503
503
|
if (convKeys.length === 0) return;
|
|
504
504
|
|
|
@@ -531,7 +531,7 @@ async function teamsNotifyPrEvent(pr, event, project, prFilePath) {
|
|
|
531
531
|
const card = cards.buildPrCard(pr, event, project);
|
|
532
532
|
|
|
533
533
|
// Find first available conversation ref
|
|
534
|
-
const state =
|
|
534
|
+
const state = safeJsonObj(TEAMS_STATE_PATH);
|
|
535
535
|
const convKeys = Object.keys(state.conversations || {});
|
|
536
536
|
if (convKeys.length === 0) return;
|
|
537
537
|
|
|
@@ -593,7 +593,7 @@ async function teamsNotifyPlanEvent(planInfo, event) {
|
|
|
593
593
|
const planName = planInfo.name || planInfo.file || 'Unknown plan';
|
|
594
594
|
const card = cards.buildPlanCard(planInfo, event);
|
|
595
595
|
|
|
596
|
-
const state =
|
|
596
|
+
const state = safeJsonObj(TEAMS_STATE_PATH);
|
|
597
597
|
const convKeys = Object.keys(state.conversations || {});
|
|
598
598
|
if (convKeys.length === 0) return;
|
|
599
599
|
|
package/engine/watches.js
CHANGED
|
@@ -2,15 +2,28 @@
|
|
|
2
2
|
* engine/watches.js -- Persistent watch jobs that survive engine restarts.
|
|
3
3
|
* Zero dependencies -- uses only Node.js built-ins + engine/shared.js.
|
|
4
4
|
*
|
|
5
|
-
* Watches monitor targets (PRs, work items,
|
|
6
|
-
*
|
|
5
|
+
* Watches monitor targets (PRs, work items, meetings, plans, schedules,
|
|
6
|
+
* pipelines, dispatches, agents, ...) for conditions (merged, build-fail,
|
|
7
|
+
* completed, concluded, approved, etc.) and fire inbox notifications when
|
|
8
|
+
* triggered.
|
|
9
|
+
*
|
|
10
|
+
* Target-type behavior is data-driven via a registry — see TARGET_TYPES below.
|
|
11
|
+
* Each registered target type provides:
|
|
12
|
+
* - label: human-friendly name shown in dashboard pickers
|
|
13
|
+
* - conditions: array of condition keys this target type accepts
|
|
14
|
+
* - fetchEntity: (target, state) => entity-or-null lookup
|
|
15
|
+
* - captureState: (entity) => snapshot for change-detection diffing
|
|
16
|
+
* - evaluate: (condition, entity, prevState, target) => { triggered, message }
|
|
17
|
+
*
|
|
18
|
+
* Built-in types: pr, work-item, meeting, plan, schedule, pipeline, dispatch,
|
|
19
|
+
* agent. Additional types can be added at runtime via registerTargetType().
|
|
7
20
|
*
|
|
8
21
|
* State stored in engine/watches.json — concurrency-safe via mutateJsonFileLocked.
|
|
9
22
|
*/
|
|
10
23
|
|
|
11
24
|
const path = require('path');
|
|
12
25
|
const shared = require('./shared');
|
|
13
|
-
const {
|
|
26
|
+
const { safeJsonArr, mutateJsonFileLocked, ts, uid, log, writeToInbox,
|
|
14
27
|
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS } = shared;
|
|
15
28
|
|
|
16
29
|
// Dynamic path — respects MINIONS_TEST_DIR for test isolation
|
|
@@ -28,12 +41,57 @@ function findPrByTarget(pullRequests, target) {
|
|
|
28
41
|
);
|
|
29
42
|
}
|
|
30
43
|
|
|
44
|
+
// ── Target type registry ─────────────────────────────────────────────────────
|
|
45
|
+
// Built-in target types are registered at module load (see bottom of file).
|
|
46
|
+
// Additional types can be added at runtime via registerTargetType(). The
|
|
47
|
+
// registry IS the allowlist for createWatch / dashboard validation — the old
|
|
48
|
+
// hardcoded "pr or work-item" check is gone.
|
|
49
|
+
const TARGET_TYPES = {};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Register a watch target type.
|
|
53
|
+
* @param {string} type - Unique key (e.g. 'pr', 'meeting', 'custom-thing')
|
|
54
|
+
* @param {object} spec - { label, conditions, fetchEntity, captureState, evaluate }
|
|
55
|
+
* - label: string shown in dashboard pickers
|
|
56
|
+
* - conditions: array of condition strings the type accepts
|
|
57
|
+
* - fetchEntity: (target, state) => entity-or-null
|
|
58
|
+
* - captureState: (entity) => snapshot object
|
|
59
|
+
* - evaluate: (condition, entity, prevState, target) => { triggered, message }
|
|
60
|
+
*/
|
|
61
|
+
function registerTargetType(type, spec) {
|
|
62
|
+
if (!type || typeof type !== 'string') throw new Error('registerTargetType: type must be a non-empty string');
|
|
63
|
+
if (!spec || typeof spec !== 'object') throw new Error('registerTargetType: spec must be an object');
|
|
64
|
+
for (const k of ['label', 'conditions', 'fetchEntity', 'captureState', 'evaluate']) {
|
|
65
|
+
if (spec[k] === undefined) throw new Error(`registerTargetType(${type}): spec.${k} is required`);
|
|
66
|
+
}
|
|
67
|
+
if (!Array.isArray(spec.conditions) || spec.conditions.length === 0) {
|
|
68
|
+
throw new Error(`registerTargetType(${type}): spec.conditions must be a non-empty array`);
|
|
69
|
+
}
|
|
70
|
+
TARGET_TYPES[type] = spec;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Returns the registered spec for a target type, or null. */
|
|
74
|
+
function getTargetType(type) { return TARGET_TYPES[type] || null; }
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns the list of registered target types as a serializable array
|
|
78
|
+
* suitable for `GET /api/watches/target-types` and dashboard pickers.
|
|
79
|
+
*/
|
|
80
|
+
function listTargetTypes() {
|
|
81
|
+
return Object.keys(TARGET_TYPES).sort().map(key => ({
|
|
82
|
+
value: key,
|
|
83
|
+
label: TARGET_TYPES[key].label,
|
|
84
|
+
conditions: [...TARGET_TYPES[key].conditions],
|
|
85
|
+
description: TARGET_TYPES[key].description || '',
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
31
89
|
/**
|
|
32
90
|
* Read all watches from disk.
|
|
33
91
|
* @returns {Array<object>}
|
|
34
92
|
*/
|
|
35
93
|
function getWatches() {
|
|
36
|
-
return
|
|
94
|
+
return safeJsonArr(_watchesPath());
|
|
37
95
|
}
|
|
38
96
|
|
|
39
97
|
/**
|
|
@@ -43,11 +101,12 @@ function getWatches() {
|
|
|
43
101
|
*/
|
|
44
102
|
function createWatch({ target, targetType, condition, interval, owner, description, project, notify, stopAfter, onNotMet }) {
|
|
45
103
|
if (!target) throw new Error('target is required');
|
|
46
|
-
if (!targetType || !
|
|
47
|
-
throw new Error(`targetType must be one of: ${Object.
|
|
104
|
+
if (!targetType || !TARGET_TYPES[targetType]) {
|
|
105
|
+
throw new Error(`targetType must be one of: ${Object.keys(TARGET_TYPES).sort().join(', ')}`);
|
|
48
106
|
}
|
|
49
|
-
|
|
50
|
-
|
|
107
|
+
const tt = TARGET_TYPES[targetType];
|
|
108
|
+
if (!condition || !tt.conditions.includes(condition)) {
|
|
109
|
+
throw new Error(`condition must be one of: ${tt.conditions.join(', ')} (for targetType ${targetType})`);
|
|
51
110
|
}
|
|
52
111
|
|
|
53
112
|
const watch = {
|
|
@@ -131,79 +190,27 @@ function deleteWatch(id) {
|
|
|
131
190
|
|
|
132
191
|
/**
|
|
133
192
|
* Evaluate whether a watch condition is met given current state.
|
|
193
|
+
* Dispatches to the registered target type's evaluator.
|
|
134
194
|
* @param {object} watch - The watch object
|
|
135
|
-
* @param {object} state - { pullRequests, workItems
|
|
195
|
+
* @param {object} state - { pullRequests, workItems, meetings?, plans?, schedules?, scheduleRuns?, pipelineRuns?, dispatch?, agents?, config? }
|
|
136
196
|
* @returns {{ triggered: boolean, message: string }}
|
|
137
197
|
*/
|
|
138
198
|
function evaluateWatch(watch, state) {
|
|
139
199
|
const { target, targetType, condition } = watch;
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
case WATCH_CONDITION.BUILD_PASS:
|
|
154
|
-
return { triggered: pr.buildStatus === 'passing', message: pr.buildStatus === 'passing' ? `PR ${target} build is passing` : '' };
|
|
155
|
-
case WATCH_CONDITION.STATUS_CHANGE: {
|
|
156
|
-
const changed = prevState.status !== undefined && prevState.status !== pr.status;
|
|
157
|
-
return { triggered: changed, message: changed ? `PR ${target} status changed: ${prevState.status} → ${pr.status}` : '' };
|
|
158
|
-
}
|
|
159
|
-
case WATCH_CONDITION.ANY: {
|
|
160
|
-
const anyChanged = prevState.status !== undefined && (
|
|
161
|
-
prevState.status !== pr.status ||
|
|
162
|
-
prevState.buildStatus !== pr.buildStatus ||
|
|
163
|
-
prevState.reviewStatus !== pr.reviewStatus
|
|
164
|
-
);
|
|
165
|
-
return { triggered: anyChanged, message: anyChanged ? `PR ${target} changed` : '' };
|
|
166
|
-
}
|
|
167
|
-
case WATCH_CONDITION.NEW_COMMENTS: {
|
|
168
|
-
const lastCommentDate = pr.humanFeedback?.lastProcessedCommentDate || null;
|
|
169
|
-
const prevCommentDate = prevState.lastCommentDate || null;
|
|
170
|
-
const hasNew = lastCommentDate && lastCommentDate !== prevCommentDate;
|
|
171
|
-
return { triggered: !!hasNew, message: hasNew ? `PR ${target} has a new comment (${lastCommentDate})` : '' };
|
|
172
|
-
}
|
|
173
|
-
case WATCH_CONDITION.VOTE_CHANGE: {
|
|
174
|
-
const changed = prevState.reviewStatus !== undefined && prevState.reviewStatus !== pr.reviewStatus;
|
|
175
|
-
return { triggered: changed, message: changed ? `PR ${target} vote changed: ${prevState.reviewStatus} → ${pr.reviewStatus}` : '' };
|
|
176
|
-
}
|
|
177
|
-
default:
|
|
178
|
-
return { triggered: false, message: `Unknown condition: ${condition}` };
|
|
179
|
-
}
|
|
200
|
+
const tt = TARGET_TYPES[targetType];
|
|
201
|
+
if (!tt) return { triggered: false, message: `Unknown target type: ${targetType}` };
|
|
202
|
+
if (!tt.conditions.includes(condition)) return { triggered: false, message: `Unknown condition: ${condition}` };
|
|
203
|
+
|
|
204
|
+
const entity = tt.fetchEntity(target, state || {});
|
|
205
|
+
if (!entity) return { triggered: false, message: `${tt.label} ${target} not found` };
|
|
206
|
+
|
|
207
|
+
const prevState = watch._lastState || {};
|
|
208
|
+
try {
|
|
209
|
+
return tt.evaluate(condition, entity, prevState, target) || { triggered: false, message: '' };
|
|
210
|
+
} catch (err) {
|
|
211
|
+
log('warn', `evaluateWatch ${targetType}/${condition}: ${err.message}`);
|
|
212
|
+
return { triggered: false, message: `evaluator error: ${err.message}` };
|
|
180
213
|
}
|
|
181
|
-
|
|
182
|
-
if (targetType === WATCH_TARGET_TYPE.WORK_ITEM) {
|
|
183
|
-
const wi = (state.workItems || []).find(w => w.id === target);
|
|
184
|
-
if (!wi) return { triggered: false, message: `Work item ${target} not found` };
|
|
185
|
-
|
|
186
|
-
const prevState = watch._lastState || {};
|
|
187
|
-
|
|
188
|
-
switch (condition) {
|
|
189
|
-
case WATCH_CONDITION.COMPLETED:
|
|
190
|
-
return { triggered: shared.DONE_STATUSES.has(wi.status), message: shared.DONE_STATUSES.has(wi.status) ? `Work item ${target} completed (${wi.status})` : '' };
|
|
191
|
-
case WATCH_CONDITION.FAILED:
|
|
192
|
-
return { triggered: wi.status === shared.WI_STATUS.FAILED, message: wi.status === shared.WI_STATUS.FAILED ? `Work item ${target} failed` : '' };
|
|
193
|
-
case WATCH_CONDITION.STATUS_CHANGE: {
|
|
194
|
-
const changed = prevState.status !== undefined && prevState.status !== wi.status;
|
|
195
|
-
return { triggered: changed, message: changed ? `Work item ${target} status: ${prevState.status} → ${wi.status}` : '' };
|
|
196
|
-
}
|
|
197
|
-
case WATCH_CONDITION.ANY: {
|
|
198
|
-
const anyChanged = prevState.status !== undefined && prevState.status !== wi.status;
|
|
199
|
-
return { triggered: anyChanged, message: anyChanged ? `Work item ${target} changed (${wi.status})` : '' };
|
|
200
|
-
}
|
|
201
|
-
default:
|
|
202
|
-
return { triggered: false, message: `Unknown condition: ${condition}` };
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return { triggered: false, message: `Unknown target type: ${targetType}` };
|
|
207
214
|
}
|
|
208
215
|
|
|
209
216
|
/**
|
|
@@ -293,18 +300,374 @@ function checkWatches(config, state) {
|
|
|
293
300
|
|
|
294
301
|
/**
|
|
295
302
|
* Internal: capture state snapshot for a watch target.
|
|
303
|
+
* Dispatches to the registered target type's captureState.
|
|
296
304
|
*/
|
|
297
305
|
function _captureState(watch, state) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
306
|
+
const tt = TARGET_TYPES[watch.targetType];
|
|
307
|
+
if (!tt) return {};
|
|
308
|
+
const entity = tt.fetchEntity(watch.target, state || {});
|
|
309
|
+
if (!entity) return {};
|
|
310
|
+
try {
|
|
311
|
+
return tt.captureState(entity) || {};
|
|
312
|
+
} catch (err) {
|
|
313
|
+
log('warn', `_captureState ${watch.targetType}: ${err.message}`);
|
|
314
|
+
return {};
|
|
301
315
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Built-in target type registrations ───────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
// PR — pull request status / build / comments / votes
|
|
321
|
+
registerTargetType(WATCH_TARGET_TYPE.PR, {
|
|
322
|
+
label: 'Pull Request',
|
|
323
|
+
description: 'Watch a pull request for merge, build, comment, or vote changes',
|
|
324
|
+
conditions: [
|
|
325
|
+
WATCH_CONDITION.MERGED, WATCH_CONDITION.BUILD_FAIL, WATCH_CONDITION.BUILD_PASS,
|
|
326
|
+
WATCH_CONDITION.STATUS_CHANGE, WATCH_CONDITION.ANY,
|
|
327
|
+
WATCH_CONDITION.NEW_COMMENTS, WATCH_CONDITION.VOTE_CHANGE,
|
|
328
|
+
],
|
|
329
|
+
fetchEntity: (target, state) => findPrByTarget(state.pullRequests, target),
|
|
330
|
+
captureState: (pr) => ({
|
|
331
|
+
status: pr.status, buildStatus: pr.buildStatus, reviewStatus: pr.reviewStatus,
|
|
332
|
+
lastCommentDate: pr.humanFeedback?.lastProcessedCommentDate || null,
|
|
333
|
+
}),
|
|
334
|
+
evaluate: (condition, pr, prevState, target) => {
|
|
335
|
+
switch (condition) {
|
|
336
|
+
case WATCH_CONDITION.MERGED:
|
|
337
|
+
return { triggered: pr.status === 'merged', message: pr.status === 'merged' ? `PR ${target} was merged` : '' };
|
|
338
|
+
case WATCH_CONDITION.BUILD_FAIL:
|
|
339
|
+
return { triggered: pr.buildStatus === 'failing', message: pr.buildStatus === 'failing' ? `PR ${target} build is failing` : '' };
|
|
340
|
+
case WATCH_CONDITION.BUILD_PASS:
|
|
341
|
+
return { triggered: pr.buildStatus === 'passing', message: pr.buildStatus === 'passing' ? `PR ${target} build is passing` : '' };
|
|
342
|
+
case WATCH_CONDITION.STATUS_CHANGE: {
|
|
343
|
+
const changed = prevState.status !== undefined && prevState.status !== pr.status;
|
|
344
|
+
return { triggered: changed, message: changed ? `PR ${target} status changed: ${prevState.status} → ${pr.status}` : '' };
|
|
345
|
+
}
|
|
346
|
+
case WATCH_CONDITION.ANY: {
|
|
347
|
+
const anyChanged = prevState.status !== undefined && (
|
|
348
|
+
prevState.status !== pr.status ||
|
|
349
|
+
prevState.buildStatus !== pr.buildStatus ||
|
|
350
|
+
prevState.reviewStatus !== pr.reviewStatus
|
|
351
|
+
);
|
|
352
|
+
return { triggered: anyChanged, message: anyChanged ? `PR ${target} changed` : '' };
|
|
353
|
+
}
|
|
354
|
+
case WATCH_CONDITION.NEW_COMMENTS: {
|
|
355
|
+
const lastCommentDate = pr.humanFeedback?.lastProcessedCommentDate || null;
|
|
356
|
+
const prevCommentDate = prevState.lastCommentDate || null;
|
|
357
|
+
const hasNew = lastCommentDate && lastCommentDate !== prevCommentDate;
|
|
358
|
+
return { triggered: !!hasNew, message: hasNew ? `PR ${target} has a new comment (${lastCommentDate})` : '' };
|
|
359
|
+
}
|
|
360
|
+
case WATCH_CONDITION.VOTE_CHANGE: {
|
|
361
|
+
const changed = prevState.reviewStatus !== undefined && prevState.reviewStatus !== pr.reviewStatus;
|
|
362
|
+
return { triggered: changed, message: changed ? `PR ${target} vote changed: ${prevState.reviewStatus} → ${pr.reviewStatus}` : '' };
|
|
363
|
+
}
|
|
364
|
+
default:
|
|
365
|
+
return { triggered: false, message: `Unknown condition: ${condition}` };
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Work item — completed/failed/status-change
|
|
371
|
+
registerTargetType(WATCH_TARGET_TYPE.WORK_ITEM, {
|
|
372
|
+
label: 'Work Item',
|
|
373
|
+
description: 'Watch a work item for completion, failure, or status changes',
|
|
374
|
+
conditions: [
|
|
375
|
+
WATCH_CONDITION.COMPLETED, WATCH_CONDITION.FAILED,
|
|
376
|
+
WATCH_CONDITION.STATUS_CHANGE, WATCH_CONDITION.ANY,
|
|
377
|
+
],
|
|
378
|
+
fetchEntity: (target, state) => (state.workItems || []).find(w => w.id === target) || null,
|
|
379
|
+
captureState: (wi) => ({ status: wi.status }),
|
|
380
|
+
evaluate: (condition, wi, prevState, target) => {
|
|
381
|
+
switch (condition) {
|
|
382
|
+
case WATCH_CONDITION.COMPLETED: {
|
|
383
|
+
const done = shared.DONE_STATUSES.has(wi.status);
|
|
384
|
+
return { triggered: done, message: done ? `Work item ${target} completed (${wi.status})` : '' };
|
|
385
|
+
}
|
|
386
|
+
case WATCH_CONDITION.FAILED: {
|
|
387
|
+
const failed = wi.status === shared.WI_STATUS.FAILED;
|
|
388
|
+
return { triggered: failed, message: failed ? `Work item ${target} failed` : '' };
|
|
389
|
+
}
|
|
390
|
+
case WATCH_CONDITION.STATUS_CHANGE: {
|
|
391
|
+
const changed = prevState.status !== undefined && prevState.status !== wi.status;
|
|
392
|
+
return { triggered: changed, message: changed ? `Work item ${target} status: ${prevState.status} → ${wi.status}` : '' };
|
|
393
|
+
}
|
|
394
|
+
case WATCH_CONDITION.ANY: {
|
|
395
|
+
const anyChanged = prevState.status !== undefined && prevState.status !== wi.status;
|
|
396
|
+
return { triggered: anyChanged, message: anyChanged ? `Work item ${target} changed (${wi.status})` : '' };
|
|
397
|
+
}
|
|
398
|
+
default:
|
|
399
|
+
return { triggered: false, message: `Unknown condition: ${condition}` };
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Meeting — concluded (terminal status) / status-change / any
|
|
405
|
+
const MEETING_TERMINAL = new Set(['completed', 'archived']);
|
|
406
|
+
registerTargetType(WATCH_TARGET_TYPE.MEETING, {
|
|
407
|
+
label: 'Meeting',
|
|
408
|
+
description: 'Watch a meeting for conclusion or status changes',
|
|
409
|
+
conditions: [WATCH_CONDITION.CONCLUDED, WATCH_CONDITION.STATUS_CHANGE, WATCH_CONDITION.ANY],
|
|
410
|
+
fetchEntity: (target, state) => (state.meetings || []).find(m => m && m.id === target) || null,
|
|
411
|
+
captureState: (m) => ({ status: m.status }),
|
|
412
|
+
evaluate: (condition, m, prevState, target) => {
|
|
413
|
+
switch (condition) {
|
|
414
|
+
case WATCH_CONDITION.CONCLUDED: {
|
|
415
|
+
const concluded = MEETING_TERMINAL.has(String(m.status || '').toLowerCase());
|
|
416
|
+
return { triggered: concluded, message: concluded ? `Meeting ${target} concluded (${m.status})` : '' };
|
|
417
|
+
}
|
|
418
|
+
case WATCH_CONDITION.STATUS_CHANGE: {
|
|
419
|
+
const changed = prevState.status !== undefined && prevState.status !== m.status;
|
|
420
|
+
return { triggered: changed, message: changed ? `Meeting ${target} status: ${prevState.status} → ${m.status}` : '' };
|
|
421
|
+
}
|
|
422
|
+
case WATCH_CONDITION.ANY: {
|
|
423
|
+
const anyChanged = prevState.status !== undefined && prevState.status !== m.status;
|
|
424
|
+
return { triggered: anyChanged, message: anyChanged ? `Meeting ${target} changed (${m.status})` : '' };
|
|
425
|
+
}
|
|
426
|
+
default:
|
|
427
|
+
return { triggered: false, message: `Unknown condition: ${condition}` };
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Plan / PRD — approved/rejected/completed/status-change/any
|
|
433
|
+
function _findPlan(target, state) {
|
|
434
|
+
const t = String(target);
|
|
435
|
+
return (state.plans || []).find(p => {
|
|
436
|
+
if (!p) return false;
|
|
437
|
+
if (p._source === t || p._source === t + '.json') return true;
|
|
438
|
+
if (p._sourcePlan === t || p._sourcePlan === t + '.md') return true;
|
|
439
|
+
if (p.id === t) return true;
|
|
440
|
+
return false;
|
|
441
|
+
}) || null;
|
|
442
|
+
}
|
|
443
|
+
registerTargetType(WATCH_TARGET_TYPE.PLAN, {
|
|
444
|
+
label: 'Plan / PRD',
|
|
445
|
+
description: 'Watch a plan/PRD for approval, rejection, completion, or status changes',
|
|
446
|
+
conditions: [
|
|
447
|
+
WATCH_CONDITION.APPROVED, WATCH_CONDITION.REJECTED, WATCH_CONDITION.COMPLETED,
|
|
448
|
+
WATCH_CONDITION.STATUS_CHANGE, WATCH_CONDITION.ANY,
|
|
449
|
+
],
|
|
450
|
+
fetchEntity: _findPlan,
|
|
451
|
+
captureState: (p) => ({ status: p.status, planStale: !!p.planStale }),
|
|
452
|
+
evaluate: (condition, p, prevState, target) => {
|
|
453
|
+
const status = String(p.status || '').toLowerCase();
|
|
454
|
+
switch (condition) {
|
|
455
|
+
case WATCH_CONDITION.APPROVED:
|
|
456
|
+
return { triggered: status === shared.PLAN_STATUS.APPROVED, message: status === shared.PLAN_STATUS.APPROVED ? `Plan ${target} approved` : '' };
|
|
457
|
+
case WATCH_CONDITION.REJECTED:
|
|
458
|
+
return { triggered: status === shared.PLAN_STATUS.REJECTED, message: status === shared.PLAN_STATUS.REJECTED ? `Plan ${target} rejected` : '' };
|
|
459
|
+
case WATCH_CONDITION.COMPLETED:
|
|
460
|
+
return { triggered: status === shared.PLAN_STATUS.COMPLETED, message: status === shared.PLAN_STATUS.COMPLETED ? `Plan ${target} completed` : '' };
|
|
461
|
+
case WATCH_CONDITION.STATUS_CHANGE: {
|
|
462
|
+
const changed = prevState.status !== undefined && prevState.status !== p.status;
|
|
463
|
+
return { triggered: changed, message: changed ? `Plan ${target} status: ${prevState.status} → ${p.status}` : '' };
|
|
464
|
+
}
|
|
465
|
+
case WATCH_CONDITION.ANY: {
|
|
466
|
+
const anyChanged = prevState.status !== undefined &&
|
|
467
|
+
(prevState.status !== p.status || prevState.planStale !== !!p.planStale);
|
|
468
|
+
return { triggered: anyChanged, message: anyChanged ? `Plan ${target} changed (${p.status})` : '' };
|
|
469
|
+
}
|
|
470
|
+
default:
|
|
471
|
+
return { triggered: false, message: `Unknown condition: ${condition}` };
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Schedule — ran (lastRun changed) / enabled / disabled / status-change / any
|
|
477
|
+
function _findSchedule(target, state) {
|
|
478
|
+
const schedules = (state.config?.schedules) || state.schedules || [];
|
|
479
|
+
const sched = schedules.find(s => s && s.id === target) || null;
|
|
480
|
+
if (!sched) return null;
|
|
481
|
+
const runEntry = (state.scheduleRuns || {})[target];
|
|
482
|
+
const lastRun = runEntry && typeof runEntry === 'object' ? runEntry.lastRun : (typeof runEntry === 'string' ? runEntry : null);
|
|
483
|
+
// Synthesize an entity that combines schedule config + run state
|
|
484
|
+
return { ...sched, _lastRun: lastRun || null };
|
|
485
|
+
}
|
|
486
|
+
registerTargetType(WATCH_TARGET_TYPE.SCHEDULE, {
|
|
487
|
+
label: 'Schedule',
|
|
488
|
+
description: 'Watch a scheduled task for runs or enable/disable changes',
|
|
489
|
+
conditions: [
|
|
490
|
+
WATCH_CONDITION.RAN, WATCH_CONDITION.ENABLED, WATCH_CONDITION.DISABLED,
|
|
491
|
+
WATCH_CONDITION.STATUS_CHANGE, WATCH_CONDITION.ANY,
|
|
492
|
+
],
|
|
493
|
+
fetchEntity: _findSchedule,
|
|
494
|
+
captureState: (s) => ({ lastRun: s._lastRun || null, enabled: s.enabled !== false }),
|
|
495
|
+
evaluate: (condition, s, prevState, target) => {
|
|
496
|
+
const enabled = s.enabled !== false;
|
|
497
|
+
const lastRun = s._lastRun || null;
|
|
498
|
+
switch (condition) {
|
|
499
|
+
case WATCH_CONDITION.RAN: {
|
|
500
|
+
const ran = !!lastRun && lastRun !== (prevState.lastRun || null);
|
|
501
|
+
return { triggered: ran, message: ran ? `Schedule ${target} ran (${lastRun})` : '' };
|
|
502
|
+
}
|
|
503
|
+
case WATCH_CONDITION.ENABLED: {
|
|
504
|
+
const flipped = prevState.enabled === false && enabled === true;
|
|
505
|
+
return { triggered: flipped, message: flipped ? `Schedule ${target} was enabled` : '' };
|
|
506
|
+
}
|
|
507
|
+
case WATCH_CONDITION.DISABLED: {
|
|
508
|
+
const flipped = prevState.enabled === true && enabled === false;
|
|
509
|
+
return { triggered: flipped, message: flipped ? `Schedule ${target} was disabled` : '' };
|
|
510
|
+
}
|
|
511
|
+
case WATCH_CONDITION.STATUS_CHANGE: {
|
|
512
|
+
const changed = prevState.enabled !== undefined && prevState.enabled !== enabled;
|
|
513
|
+
return { triggered: changed, message: changed ? `Schedule ${target} enabled changed: ${prevState.enabled} → ${enabled}` : '' };
|
|
514
|
+
}
|
|
515
|
+
case WATCH_CONDITION.ANY: {
|
|
516
|
+
const anyChanged = prevState.enabled !== undefined &&
|
|
517
|
+
(prevState.enabled !== enabled || prevState.lastRun !== lastRun);
|
|
518
|
+
return { triggered: anyChanged, message: anyChanged ? `Schedule ${target} changed` : '' };
|
|
519
|
+
}
|
|
520
|
+
default:
|
|
521
|
+
return { triggered: false, message: `Unknown condition: ${condition}` };
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Pipeline — completed/failed/stage-complete/status-change/any. Target is
|
|
527
|
+
// the pipeline ID; we track the latest run.
|
|
528
|
+
function _findPipelineLatestRun(target, state) {
|
|
529
|
+
const runsByPipeline = state.pipelineRuns || {};
|
|
530
|
+
const list = runsByPipeline[target];
|
|
531
|
+
if (!Array.isArray(list) || list.length === 0) return null;
|
|
532
|
+
// The latest run is the last entry (startRun pushes; cleanup keeps last 10).
|
|
533
|
+
return { ...list[list.length - 1], _pipelineId: target };
|
|
534
|
+
}
|
|
535
|
+
function _completedStageCount(run) {
|
|
536
|
+
if (!run || !run.stages) return 0;
|
|
537
|
+
let n = 0;
|
|
538
|
+
for (const st of Object.values(run.stages)) {
|
|
539
|
+
const s = String((st && st.status) || '').toLowerCase();
|
|
540
|
+
if (s === 'completed' || s === 'failed' || s === 'stopped') n += 1;
|
|
305
541
|
}
|
|
306
|
-
return
|
|
542
|
+
return n;
|
|
307
543
|
}
|
|
544
|
+
registerTargetType(WATCH_TARGET_TYPE.PIPELINE, {
|
|
545
|
+
label: 'Pipeline',
|
|
546
|
+
description: 'Watch the latest run of a pipeline for completion, failure, or stage progress',
|
|
547
|
+
conditions: [
|
|
548
|
+
WATCH_CONDITION.COMPLETED, WATCH_CONDITION.FAILED, WATCH_CONDITION.STAGE_COMPLETE,
|
|
549
|
+
WATCH_CONDITION.STATUS_CHANGE, WATCH_CONDITION.ANY,
|
|
550
|
+
],
|
|
551
|
+
fetchEntity: _findPipelineLatestRun,
|
|
552
|
+
captureState: (run) => ({
|
|
553
|
+
runId: run.runId || null, status: run.status || null,
|
|
554
|
+
completedStages: _completedStageCount(run),
|
|
555
|
+
}),
|
|
556
|
+
evaluate: (condition, run, prevState, target) => {
|
|
557
|
+
switch (condition) {
|
|
558
|
+
case WATCH_CONDITION.COMPLETED: {
|
|
559
|
+
const done = run.status === shared.PIPELINE_STATUS.COMPLETED;
|
|
560
|
+
return { triggered: done, message: done ? `Pipeline ${target} completed (run ${run.runId})` : '' };
|
|
561
|
+
}
|
|
562
|
+
case WATCH_CONDITION.FAILED: {
|
|
563
|
+
const failed = run.status === shared.PIPELINE_STATUS.FAILED || run.status === shared.PIPELINE_STATUS.STOPPED;
|
|
564
|
+
return { triggered: failed, message: failed ? `Pipeline ${target} failed (${run.status}, run ${run.runId})` : '' };
|
|
565
|
+
}
|
|
566
|
+
case WATCH_CONDITION.STAGE_COMPLETE: {
|
|
567
|
+
const cur = _completedStageCount(run);
|
|
568
|
+
const prev = prevState.completedStages || 0;
|
|
569
|
+
const advanced = prevState.completedStages !== undefined && cur > prev && (prevState.runId === run.runId);
|
|
570
|
+
return { triggered: advanced, message: advanced ? `Pipeline ${target} stage progress: ${prev} → ${cur}` : '' };
|
|
571
|
+
}
|
|
572
|
+
case WATCH_CONDITION.STATUS_CHANGE: {
|
|
573
|
+
const changed = prevState.status !== undefined && prevState.status !== run.status;
|
|
574
|
+
return { triggered: changed, message: changed ? `Pipeline ${target} status: ${prevState.status} → ${run.status}` : '' };
|
|
575
|
+
}
|
|
576
|
+
case WATCH_CONDITION.ANY: {
|
|
577
|
+
const anyChanged = prevState.status !== undefined &&
|
|
578
|
+
(prevState.status !== run.status || prevState.runId !== run.runId ||
|
|
579
|
+
(prevState.completedStages || 0) !== _completedStageCount(run));
|
|
580
|
+
return { triggered: anyChanged, message: anyChanged ? `Pipeline ${target} changed (${run.status})` : '' };
|
|
581
|
+
}
|
|
582
|
+
default:
|
|
583
|
+
return { triggered: false, message: `Unknown condition: ${condition}` };
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Dispatch — completed/failed/status-change/any. Target is dispatch entry id.
|
|
589
|
+
function _findDispatchEntry(target, state) {
|
|
590
|
+
const d = state.dispatch || {};
|
|
591
|
+
for (const list of ['active', 'pending', 'completed']) {
|
|
592
|
+
const entry = (d[list] || []).find(e => e && e.id === target);
|
|
593
|
+
if (entry) return { ...entry, _list: list };
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
const DISPATCH_TERMINAL = new Set(['done', 'completed', 'failed', 'cancelled']);
|
|
598
|
+
registerTargetType(WATCH_TARGET_TYPE.DISPATCH, {
|
|
599
|
+
label: 'Dispatch',
|
|
600
|
+
description: 'Watch a dispatch entry for terminal status or status changes',
|
|
601
|
+
conditions: [
|
|
602
|
+
WATCH_CONDITION.COMPLETED, WATCH_CONDITION.FAILED,
|
|
603
|
+
WATCH_CONDITION.STATUS_CHANGE, WATCH_CONDITION.ANY,
|
|
604
|
+
],
|
|
605
|
+
fetchEntity: _findDispatchEntry,
|
|
606
|
+
captureState: (e) => ({ status: e.status || e._list || null, list: e._list || null }),
|
|
607
|
+
evaluate: (condition, e, prevState, target) => {
|
|
608
|
+
const status = String(e.status || '').toLowerCase();
|
|
609
|
+
const list = e._list || null;
|
|
610
|
+
switch (condition) {
|
|
611
|
+
case WATCH_CONDITION.COMPLETED: {
|
|
612
|
+
const done = list === 'completed' && (status === '' || (DISPATCH_TERMINAL.has(status) && status !== 'failed'));
|
|
613
|
+
return { triggered: done, message: done ? `Dispatch ${target} completed` : '' };
|
|
614
|
+
}
|
|
615
|
+
case WATCH_CONDITION.FAILED: {
|
|
616
|
+
const failed = status === 'failed' || status === 'error';
|
|
617
|
+
return { triggered: failed, message: failed ? `Dispatch ${target} failed` : '' };
|
|
618
|
+
}
|
|
619
|
+
case WATCH_CONDITION.STATUS_CHANGE: {
|
|
620
|
+
const changed = prevState.status !== undefined && prevState.status !== (e.status || list);
|
|
621
|
+
return { triggered: changed, message: changed ? `Dispatch ${target} status: ${prevState.status} → ${e.status || list}` : '' };
|
|
622
|
+
}
|
|
623
|
+
case WATCH_CONDITION.ANY: {
|
|
624
|
+
const cur = e.status || list;
|
|
625
|
+
const anyChanged = prevState.status !== undefined &&
|
|
626
|
+
(prevState.status !== cur || prevState.list !== list);
|
|
627
|
+
return { triggered: anyChanged, message: anyChanged ? `Dispatch ${target} changed (${cur})` : '' };
|
|
628
|
+
}
|
|
629
|
+
default:
|
|
630
|
+
return { triggered: false, message: `Unknown condition: ${condition}` };
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// Agent — activity-change / status-change / any. State.agents must be an
|
|
636
|
+
// array of agent objects (id + status), as produced by queries.getAgents.
|
|
637
|
+
function _findAgent(target, state) {
|
|
638
|
+
return (state.agents || []).find(a => a && a.id === target) || null;
|
|
639
|
+
}
|
|
640
|
+
registerTargetType(WATCH_TARGET_TYPE.AGENT, {
|
|
641
|
+
label: 'Agent',
|
|
642
|
+
description: 'Watch an agent for activity transitions (idle ↔ working) or status changes',
|
|
643
|
+
conditions: [
|
|
644
|
+
WATCH_CONDITION.ACTIVITY_CHANGE, WATCH_CONDITION.STATUS_CHANGE, WATCH_CONDITION.ANY,
|
|
645
|
+
],
|
|
646
|
+
fetchEntity: _findAgent,
|
|
647
|
+
captureState: (a) => ({ status: a.status || null }),
|
|
648
|
+
evaluate: (condition, a, prevState, target) => {
|
|
649
|
+
const cur = a.status || null;
|
|
650
|
+
switch (condition) {
|
|
651
|
+
case WATCH_CONDITION.ACTIVITY_CHANGE: {
|
|
652
|
+
// Triggers on transitions involving the 'working' status
|
|
653
|
+
const wasWorking = prevState.status === 'working';
|
|
654
|
+
const nowWorking = cur === 'working';
|
|
655
|
+
const changed = prevState.status !== undefined && wasWorking !== nowWorking;
|
|
656
|
+
return { triggered: changed, message: changed ? `Agent ${target} activity: ${prevState.status} → ${cur}` : '' };
|
|
657
|
+
}
|
|
658
|
+
case WATCH_CONDITION.STATUS_CHANGE: {
|
|
659
|
+
const changed = prevState.status !== undefined && prevState.status !== cur;
|
|
660
|
+
return { triggered: changed, message: changed ? `Agent ${target} status: ${prevState.status} → ${cur}` : '' };
|
|
661
|
+
}
|
|
662
|
+
case WATCH_CONDITION.ANY: {
|
|
663
|
+
const anyChanged = prevState.status !== undefined && prevState.status !== cur;
|
|
664
|
+
return { triggered: anyChanged, message: anyChanged ? `Agent ${target} changed (${cur})` : '' };
|
|
665
|
+
}
|
|
666
|
+
default:
|
|
667
|
+
return { triggered: false, message: `Unknown condition: ${condition}` };
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
});
|
|
308
671
|
|
|
309
672
|
module.exports = {
|
|
310
673
|
DEFAULT_WATCH_INTERVAL,
|
|
@@ -314,6 +677,10 @@ module.exports = {
|
|
|
314
677
|
deleteWatch,
|
|
315
678
|
evaluateWatch,
|
|
316
679
|
checkWatches,
|
|
680
|
+
registerTargetType,
|
|
681
|
+
getTargetType,
|
|
682
|
+
listTargetTypes,
|
|
317
683
|
_captureState, // exported for testing
|
|
318
684
|
_watchesPath, // exported for testing — dynamic, respects MINIONS_TEST_DIR
|
|
685
|
+
_TARGET_TYPES: TARGET_TYPES, // exported for testing — direct registry access
|
|
319
686
|
};
|
package/engine.js
CHANGED
|
@@ -4419,7 +4419,46 @@ async function tickInner() {
|
|
|
4419
4419
|
});
|
|
4420
4420
|
// Also include central work items
|
|
4421
4421
|
const centralWi = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
|
|
4422
|
-
|
|
4422
|
+
|
|
4423
|
+
// Gather state for the new generalized target types. Each block is
|
|
4424
|
+
// best-effort — if a module/file is missing the watch evaluator will
|
|
4425
|
+
// simply report "not found" rather than crashing the tick.
|
|
4426
|
+
let meetings = [];
|
|
4427
|
+
try { meetings = require('./engine/meeting').getMeetings(); } catch { /* optional */ }
|
|
4428
|
+
|
|
4429
|
+
// Plans/PRDs — read PRD JSON files directly so plan-level watches see
|
|
4430
|
+
// status transitions (approved/rejected/completed/etc.). Each entry
|
|
4431
|
+
// carries its filename in _source for target lookup.
|
|
4432
|
+
const plans = [];
|
|
4433
|
+
try {
|
|
4434
|
+
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
4435
|
+
for (const dir of [prdDir, path.join(prdDir, 'archive')]) {
|
|
4436
|
+
if (!fs.existsSync(dir)) continue;
|
|
4437
|
+
for (const f of fs.readdirSync(dir).filter(x => x.endsWith('.json'))) {
|
|
4438
|
+
const plan = safeJson(path.join(dir, f));
|
|
4439
|
+
if (plan) plans.push({ ...plan, _source: f, _sourcePlan: plan.source_plan || '' });
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
} catch { /* optional */ }
|
|
4443
|
+
|
|
4444
|
+
let scheduleRuns = {};
|
|
4445
|
+
try { scheduleRuns = safeJson(path.join(MINIONS_DIR, 'engine', 'schedule-runs.json')) || {}; } catch { /* optional */ }
|
|
4446
|
+
|
|
4447
|
+
let pipelineRuns = {};
|
|
4448
|
+
try { pipelineRuns = require('./engine/pipeline').getPipelineRuns(); } catch { /* optional */ }
|
|
4449
|
+
|
|
4450
|
+
let dispatch = { pending: [], active: [], completed: [] };
|
|
4451
|
+
try { dispatch = queries.getDispatch() || dispatch; } catch { /* optional */ }
|
|
4452
|
+
|
|
4453
|
+
let agents = [];
|
|
4454
|
+
try { agents = queries.getAgents(config) || []; } catch { /* optional */ }
|
|
4455
|
+
|
|
4456
|
+
checkWatches(config, {
|
|
4457
|
+
pullRequests,
|
|
4458
|
+
workItems: [...workItems, ...centralWi],
|
|
4459
|
+
meetings, plans, scheduleRuns, pipelineRuns, dispatch, agents,
|
|
4460
|
+
config,
|
|
4461
|
+
});
|
|
4423
4462
|
});
|
|
4424
4463
|
}
|
|
4425
4464
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1853",
|
|
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"
|