@yemi33/minions 0.1.2396 → 0.1.2398
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/dashboard/js/refresh.js +2 -1
- package/dashboard/js/render-prd.js +8 -3
- package/dashboard/js/render-prs.js +102 -11
- package/dashboard/js/render-schedules.js +3 -1
- package/dashboard/js/render-work-items.js +7 -2
- package/dashboard/pages/prs.html +29 -0
- package/dashboard/shared/pr-filters.js +166 -0
- package/dashboard/styles.css +29 -1
- package/dashboard-build.js +1 -1
- package/dashboard.js +59 -16
- package/engine/shared.js +22 -0
- package/engine/watch-actions.js +28 -3
- package/engine/watches.js +1 -0
- package/package.json +1 -1
package/dashboard/js/refresh.js
CHANGED
|
@@ -809,6 +809,13 @@ async function prdItemEdit(source, itemId) {
|
|
|
809
809
|
if (!item) return;
|
|
810
810
|
|
|
811
811
|
_prdDescRawCache = item.description || '';
|
|
812
|
+
const priorities = Array.isArray(window.MINIONS_WORK_ITEM_PRIORITIES)
|
|
813
|
+
? window.MINIONS_WORK_ITEM_PRIORITIES
|
|
814
|
+
: [];
|
|
815
|
+
const priorityOptions = priorities.map(function(priority) {
|
|
816
|
+
const label = priority.charAt(0).toUpperCase() + priority.slice(1);
|
|
817
|
+
return '<option value="' + priority + '"' + (item.priority === priority ? ' selected' : '') + '>' + label + '</option>';
|
|
818
|
+
}).join('');
|
|
812
819
|
|
|
813
820
|
// Look up work item and dispatch completion info. Issue #2949 —
|
|
814
821
|
// dispatch moved off /api/status to /api/dispatch; refresh.js stashes
|
|
@@ -888,9 +895,7 @@ async function prdItemEdit(source, itemId) {
|
|
|
888
895
|
'<div style="display:flex;gap:12px;margin-bottom:12px">' +
|
|
889
896
|
'<div><label style="font-size:var(--text-base);color:var(--muted);display:block;margin-bottom:4px">Priority</label>' +
|
|
890
897
|
'<select id="prd-edit-priority" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text)">' +
|
|
891
|
-
|
|
892
|
-
'<option value="medium"' + (item.priority === 'medium' ? ' selected' : '') + '>Medium</option>' +
|
|
893
|
-
'<option value="low"' + (item.priority === 'low' ? ' selected' : '') + '>Low</option>' +
|
|
898
|
+
priorityOptions +
|
|
894
899
|
'</select></div>' +
|
|
895
900
|
'<div><label style="font-size:var(--text-base);color:var(--muted);display:block;margin-bottom:4px">Complexity</label>' +
|
|
896
901
|
'<select id="prd-edit-complexity" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text)">' +
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
// render-prs.js — PR tracker rendering functions extracted from dashboard.html
|
|
2
2
|
|
|
3
3
|
let allPrs = [];
|
|
4
|
+
let filteredPrs = [];
|
|
4
5
|
let prPage = 0;
|
|
6
|
+
let prFilterOptionsSignature = '';
|
|
5
7
|
const PR_PER_PAGE = 25;
|
|
8
|
+
const PR_FILTER_CONTROL_IDS = {
|
|
9
|
+
author: 'pr-filter-author',
|
|
10
|
+
lifecycle: 'pr-filter-lifecycle',
|
|
11
|
+
project: 'pr-filter-project',
|
|
12
|
+
review: 'pr-filter-review',
|
|
13
|
+
build: 'pr-filter-build',
|
|
14
|
+
};
|
|
15
|
+
const PR_FILTER_ALL_LABELS = {
|
|
16
|
+
author: 'All authors',
|
|
17
|
+
lifecycle: 'All lifecycle statuses',
|
|
18
|
+
project: 'All projects',
|
|
19
|
+
review: 'All review statuses',
|
|
20
|
+
build: 'All build statuses',
|
|
21
|
+
};
|
|
6
22
|
|
|
7
23
|
// Fetch each project's pull-requests.json straight off disk through the
|
|
8
24
|
// /state/projects/<name>/pull-requests.json static-file passthrough. This
|
|
@@ -373,32 +389,105 @@ function _restorePrScrollState(el, state) {
|
|
|
373
389
|
tableWrap.scrollTop = state.top || 0;
|
|
374
390
|
}
|
|
375
391
|
|
|
392
|
+
function _syncPrFilterOptions(prs) {
|
|
393
|
+
const options = getPrFilterOptions(prs);
|
|
394
|
+
const controls = Object.keys(PR_FILTER_CONTROL_IDS).map(function(dimension) {
|
|
395
|
+
return {
|
|
396
|
+
dimension,
|
|
397
|
+
select: document.getElementById(PR_FILTER_CONTROL_IDS[dimension]),
|
|
398
|
+
};
|
|
399
|
+
}).filter(function(entry) { return entry.select; });
|
|
400
|
+
if (!controls.length) return;
|
|
401
|
+
const signature = JSON.stringify(options);
|
|
402
|
+
if (signature === prFilterOptionsSignature) return;
|
|
403
|
+
prFilterOptionsSignature = signature;
|
|
404
|
+
controls.forEach(function(entry) {
|
|
405
|
+
const dimension = entry.dimension;
|
|
406
|
+
const select = entry.select;
|
|
407
|
+
const selected = select.value;
|
|
408
|
+
const all = document.createElement('option');
|
|
409
|
+
all.value = '';
|
|
410
|
+
all.textContent = PR_FILTER_ALL_LABELS[dimension];
|
|
411
|
+
const nodes = [all].concat((options[dimension] || []).map(function(option) {
|
|
412
|
+
const node = document.createElement('option');
|
|
413
|
+
node.value = option.value;
|
|
414
|
+
node.textContent = option.label;
|
|
415
|
+
return node;
|
|
416
|
+
}));
|
|
417
|
+
select.replaceChildren(...nodes);
|
|
418
|
+
if (nodes.some(function(node) { return node.value === selected; })) select.value = selected;
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function _readPrFilters() {
|
|
423
|
+
const filters = {};
|
|
424
|
+
Object.keys(PR_FILTER_CONTROL_IDS).forEach(function(dimension) {
|
|
425
|
+
const select = document.getElementById(PR_FILTER_CONTROL_IDS[dimension]);
|
|
426
|
+
filters[dimension] = select ? select.value : '';
|
|
427
|
+
});
|
|
428
|
+
return filters;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function _hasActivePrFilters(filters) {
|
|
432
|
+
return Object.keys(PR_FILTER_CONTROL_IDS).some(function(dimension) {
|
|
433
|
+
return Boolean(filters[dimension]);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function applyPrFilters() {
|
|
438
|
+
prPage = 0;
|
|
439
|
+
renderPrs(allPrs, { preserveScroll: false });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function resetPrFilters() {
|
|
443
|
+
Object.keys(PR_FILTER_CONTROL_IDS).forEach(function(dimension) {
|
|
444
|
+
const select = document.getElementById(PR_FILTER_CONTROL_IDS[dimension]);
|
|
445
|
+
if (select) select.value = '';
|
|
446
|
+
});
|
|
447
|
+
applyPrFilters();
|
|
448
|
+
}
|
|
449
|
+
|
|
376
450
|
function renderPrs(prs, opts) {
|
|
377
451
|
opts = opts || {};
|
|
378
|
-
prs = prs.filter(p => !isDeleted('pr:' + p.id));
|
|
452
|
+
prs = Array.isArray(prs) ? prs.filter(p => p && !isDeleted('pr:' + p.id)) : [];
|
|
379
453
|
allPrs = prs;
|
|
454
|
+
_syncPrFilterOptions(allPrs);
|
|
455
|
+
const filters = _readPrFilters();
|
|
456
|
+
filteredPrs = filterPullRequests(allPrs, filters);
|
|
380
457
|
const el = document.getElementById('pr-content');
|
|
381
458
|
const count = document.getElementById('pr-count');
|
|
382
|
-
|
|
383
|
-
|
|
459
|
+
const summary = document.getElementById('pr-filter-summary');
|
|
460
|
+
count.textContent = filteredPrs.length;
|
|
461
|
+
if (summary) {
|
|
462
|
+
summary.textContent = _hasActivePrFilters(filters)
|
|
463
|
+
? 'Showing ' + filteredPrs.length + ' of ' + allPrs.length
|
|
464
|
+
: '';
|
|
465
|
+
}
|
|
466
|
+
if (!allPrs.length) {
|
|
467
|
+
prPage = 0;
|
|
384
468
|
el.innerHTML = '<p class="pr-empty">No pull requests yet. PRs created by agents will appear here with review, build, and merge status.</p>';
|
|
385
469
|
return;
|
|
386
470
|
}
|
|
387
|
-
|
|
471
|
+
if (!filteredPrs.length) {
|
|
472
|
+
prPage = 0;
|
|
473
|
+
el.innerHTML = '<p class="pr-empty">No pull requests match the selected filters.</p>';
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const totalPages = Math.ceil(filteredPrs.length / PR_PER_PAGE);
|
|
388
477
|
const previousPage = prPage;
|
|
389
478
|
if (prPage >= totalPages) prPage = totalPages - 1;
|
|
390
479
|
const start = prPage * PR_PER_PAGE;
|
|
391
|
-
const pagePrs =
|
|
480
|
+
const pagePrs = filteredPrs.slice(start, start + PR_PER_PAGE);
|
|
392
481
|
const rows = pagePrs.map(prRow).join('');
|
|
393
482
|
|
|
394
483
|
let pager = '';
|
|
395
|
-
if (
|
|
484
|
+
if (filteredPrs.length > PR_PER_PAGE) {
|
|
396
485
|
pager = '<div class="pr-pager">' +
|
|
397
|
-
'<span class="pr-page-info">Showing ' + (start+1) + ' to ' + Math.min(start+PR_PER_PAGE,
|
|
486
|
+
'<span class="pr-page-info">Showing ' + (start+1) + ' to ' + Math.min(start+PR_PER_PAGE, filteredPrs.length) + ' of ' + filteredPrs.length + '</span>' +
|
|
398
487
|
'<div class="pr-pager-btns">' +
|
|
399
488
|
'<button class="pr-pager-btn ' + (prPage === 0 ? 'disabled' : '') + '" onclick="prPrev()">Prev</button>' +
|
|
400
489
|
'<button class="pr-pager-btn ' + (prPage >= totalPages-1 ? 'disabled' : '') + '" onclick="prNext()">Next</button>' +
|
|
401
|
-
'<button class="pr-pager-btn see-all" onclick="openAllPrs()">See all ' +
|
|
490
|
+
'<button class="pr-pager-btn see-all" onclick="openAllPrs()">See all ' + filteredPrs.length + ' PRs</button>' +
|
|
402
491
|
'</div>' +
|
|
403
492
|
'</div>';
|
|
404
493
|
}
|
|
@@ -412,14 +501,14 @@ function renderPrs(prs, opts) {
|
|
|
412
501
|
}
|
|
413
502
|
|
|
414
503
|
function prPrev() { if (prPage > 0) { prPage--; renderPrs(allPrs, { preserveScroll: false }); } }
|
|
415
|
-
function prNext() { const totalPages = Math.ceil(
|
|
504
|
+
function prNext() { const totalPages = Math.ceil(filteredPrs.length / PR_PER_PAGE); if (prPage < totalPages-1) { prPage++; renderPrs(allPrs, { preserveScroll: false }); } }
|
|
416
505
|
|
|
417
506
|
function openAllPrs() {
|
|
418
507
|
const modalEl = document.querySelector('#modal .modal');
|
|
419
508
|
if (modalEl) modalEl.classList.add('modal-wide');
|
|
420
|
-
document.getElementById('modal-title').textContent = 'All Pull Requests (' +
|
|
509
|
+
document.getElementById('modal-title').textContent = 'All Pull Requests (' + filteredPrs.length + ')';
|
|
421
510
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all PR user data wrapped in escapeHtml() by prRow() (fields: PR id, title, description, agent, branch, review/build/status labels)
|
|
422
|
-
document.getElementById('modal-body').innerHTML = prTableHtml(
|
|
511
|
+
document.getElementById('modal-body').innerHTML = prTableHtml(filteredPrs.map(prRow).join(''));
|
|
423
512
|
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
424
513
|
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
425
514
|
document.getElementById('modal').classList.add('open');
|
|
@@ -862,6 +951,8 @@ window.MinionsPrs = {
|
|
|
862
951
|
prRow,
|
|
863
952
|
prTableHtml,
|
|
864
953
|
renderPrs,
|
|
954
|
+
applyPrFilters,
|
|
955
|
+
resetPrFilters,
|
|
865
956
|
renderPrDetail: _renderPrDetail, // exported for testing
|
|
866
957
|
prPrev,
|
|
867
958
|
prNext,
|
|
@@ -431,7 +431,9 @@ function openScheduleDetail(id) {
|
|
|
431
431
|
|
|
432
432
|
function _scheduleFormHtml(sched, isEdit) {
|
|
433
433
|
const types = ['implement', 'test', 'explore', 'ask', 'review', 'fix'];
|
|
434
|
-
const priorities =
|
|
434
|
+
const priorities = Array.isArray(window.MINIONS_WORK_ITEM_PRIORITIES)
|
|
435
|
+
? window.MINIONS_WORK_ITEM_PRIORITIES
|
|
436
|
+
: [];
|
|
435
437
|
const typeOpts = types.map(t => '<option value="' + t + '"' + ((sched.type || 'implement') === t ? ' selected' : '') + '>' + t + '</option>').join('');
|
|
436
438
|
const priOpts = priorities.map(p => '<option value="' + p + '"' + ((sched.priority || 'medium') === p ? ' selected' : '') + '>' + p + '</option>').join('');
|
|
437
439
|
const projOpts = '<option value="">Any</option>' + (cmdProjects || []).map(p => '<option value="' + escHtml(p.name) + '"' + (sched.project === p.name ? ' selected' : '') + '>' + escHtml(p.displayName || p.name) + '</option>').join('');
|
|
@@ -353,7 +353,9 @@ async function editWorkItem(id, source) {
|
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
const types = ['implement', 'fix', 'review', 'plan', 'verify', 'decompose', 'meeting', 'investigate', 'refactor', 'test', 'explore', 'ask', 'docs', 'setup'];
|
|
356
|
-
const priorities =
|
|
356
|
+
const priorities = Array.isArray(window.MINIONS_WORK_ITEM_PRIORITIES)
|
|
357
|
+
? window.MINIONS_WORK_ITEM_PRIORITIES
|
|
358
|
+
: [];
|
|
357
359
|
const agentOpts = (cmdAgents || []).map(a => '<option value="' + escapeHtml(a.id) + '"' + (item.agent === a.id ? ' selected' : '') + '>' + escapeHtml(a.name) + '</option>').join('');
|
|
358
360
|
const typeOpts = types.map(t => '<option value="' + t + '"' + ((item.type || 'implement') === t ? ' selected' : '') + '>' + t + '</option>').join('');
|
|
359
361
|
const priOpts = priorities.map(p => '<option value="' + p + '"' + ((item.priority || 'medium') === p ? ' selected' : '') + '>' + p + '</option>').join('');
|
|
@@ -586,7 +588,10 @@ function openCreateWorkItemModal() {
|
|
|
586
588
|
const typeOpts = ['implement', 'fix', 'explore', 'test', 'review', 'ask', 'plan', 'verify', 'decompose', 'meeting', 'docs', 'setup'].map(t =>
|
|
587
589
|
'<option value="' + t + '"' + (t === 'implement' ? ' selected' : '') + '>' + t + '</option>'
|
|
588
590
|
).join('');
|
|
589
|
-
const
|
|
591
|
+
const priorities = Array.isArray(window.MINIONS_WORK_ITEM_PRIORITIES)
|
|
592
|
+
? window.MINIONS_WORK_ITEM_PRIORITIES
|
|
593
|
+
: [];
|
|
594
|
+
const priOpts = priorities.map(p =>
|
|
590
595
|
'<option value="' + p + '"' + (p === 'medium' ? ' selected' : '') + '>' + p + '</option>'
|
|
591
596
|
).join('');
|
|
592
597
|
const agentOpts = (typeof cmdAgents !== 'undefined' ? cmdAgents : []).map(a =>
|
package/dashboard/pages/prs.html
CHANGED
|
@@ -4,5 +4,34 @@
|
|
|
4
4
|
<span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">created by agents, tracked with review + build status</span>
|
|
5
5
|
</h2>
|
|
6
6
|
<div id="pr-toast" class="cmd-toast cmd-toast-inline" style="margin:6px 0"></div>
|
|
7
|
+
<div class="pr-filters" role="group" aria-label="Pull request filters">
|
|
8
|
+
<label class="pr-filter">Author
|
|
9
|
+
<select id="pr-filter-author" onchange="applyPrFilters()">
|
|
10
|
+
<option value="">All authors</option>
|
|
11
|
+
</select>
|
|
12
|
+
</label>
|
|
13
|
+
<label class="pr-filter">Lifecycle
|
|
14
|
+
<select id="pr-filter-lifecycle" onchange="applyPrFilters()">
|
|
15
|
+
<option value="">All lifecycle statuses</option>
|
|
16
|
+
</select>
|
|
17
|
+
</label>
|
|
18
|
+
<label class="pr-filter">Project
|
|
19
|
+
<select id="pr-filter-project" onchange="applyPrFilters()">
|
|
20
|
+
<option value="">All projects</option>
|
|
21
|
+
</select>
|
|
22
|
+
</label>
|
|
23
|
+
<label class="pr-filter">Review
|
|
24
|
+
<select id="pr-filter-review" onchange="applyPrFilters()">
|
|
25
|
+
<option value="">All review statuses</option>
|
|
26
|
+
</select>
|
|
27
|
+
</label>
|
|
28
|
+
<label class="pr-filter">Build
|
|
29
|
+
<select id="pr-filter-build" onchange="applyPrFilters()">
|
|
30
|
+
<option value="">All build statuses</option>
|
|
31
|
+
</select>
|
|
32
|
+
</label>
|
|
33
|
+
<button class="pr-filter-reset" type="button" onclick="resetPrFilters()">Reset filters</button>
|
|
34
|
+
<span class="pr-filter-summary" id="pr-filter-summary" aria-live="polite"></span>
|
|
35
|
+
</div>
|
|
7
36
|
<div id="pr-content"><p class="pr-empty">No pull requests yet.</p></div>
|
|
8
37
|
</section>
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Shared PR filter semantics. Slim embeds the classic /prs screen, and both
|
|
2
|
+
// bundles load this helper so lifecycle and legacy-field normalization stay
|
|
3
|
+
// identical across surfaces.
|
|
4
|
+
var PR_FILTER_DIMENSIONS = ['author', 'lifecycle', 'project', 'review', 'build'];
|
|
5
|
+
var PR_LIFECYCLE_FILTER_LABELS = {
|
|
6
|
+
open: 'Open / active',
|
|
7
|
+
merged: 'Merged',
|
|
8
|
+
closed: 'Closed',
|
|
9
|
+
};
|
|
10
|
+
var PR_FILTER_OPTION_ORDER = {
|
|
11
|
+
lifecycle: ['open', 'merged', 'closed'],
|
|
12
|
+
review: ['approved', 'changes-requested', 'waiting', 'pending'],
|
|
13
|
+
build: ['passing', 'failing', 'running', 'none'],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function _prFilterText(value) {
|
|
17
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function _prFilterName(value) {
|
|
21
|
+
var direct = _prFilterText(value);
|
|
22
|
+
if (direct) return direct;
|
|
23
|
+
if (!value || typeof value !== 'object') return '';
|
|
24
|
+
return _prFilterText(value.login)
|
|
25
|
+
|| _prFilterText(value.displayName)
|
|
26
|
+
|| _prFilterText(value.uniqueName)
|
|
27
|
+
|| _prFilterText(value.name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function _prFilterKey(value) {
|
|
31
|
+
return _prFilterText(value).toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizePrAuthor(pr) {
|
|
35
|
+
if (!pr || typeof pr !== 'object') return '';
|
|
36
|
+
return _prFilterName(pr.author)
|
|
37
|
+
|| _prFilterName(pr.createdBy)
|
|
38
|
+
|| _prFilterName(pr.created_by)
|
|
39
|
+
|| _prFilterName(pr.agent);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizePrLifecycleStatus(pr) {
|
|
43
|
+
if (!pr || typeof pr !== 'object') return '';
|
|
44
|
+
var status = _prFilterKey(pr.status || pr.state);
|
|
45
|
+
if (pr.merged === true || pr.isMerged === true || pr.mergedAt || pr.merged_at
|
|
46
|
+
|| status === 'merged' || status === 'completed') {
|
|
47
|
+
return 'merged';
|
|
48
|
+
}
|
|
49
|
+
if (status === 'active' || status === 'open' || status === 'linked') return 'open';
|
|
50
|
+
if (status === 'closed' || status === 'abandoned' || status === 'declined') return 'closed';
|
|
51
|
+
return status;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizePrProject(pr) {
|
|
55
|
+
if (!pr || typeof pr !== 'object') return '';
|
|
56
|
+
return _prFilterText(pr._project)
|
|
57
|
+
|| _prFilterText(pr.project)
|
|
58
|
+
|| _prFilterText(pr.sourceProject);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizePrReviewStatus(pr) {
|
|
62
|
+
if (!pr || typeof pr !== 'object') return '';
|
|
63
|
+
var minionsStatus = pr.minionsReview && _prFilterText(pr.minionsReview.status);
|
|
64
|
+
var status = _prFilterKey(minionsStatus || pr.reviewStatus || pr.review_status);
|
|
65
|
+
if (!minionsStatus && status === 'waiting') {
|
|
66
|
+
if (normalizePrLifecycleStatus(pr) === 'merged') return 'approved';
|
|
67
|
+
if (_prFilterKey(pr.status) === 'abandoned') return 'pending';
|
|
68
|
+
}
|
|
69
|
+
if (status === 'approve') return 'approved';
|
|
70
|
+
if (status === 'rejected' || status === 'changes_requested'
|
|
71
|
+
|| status === 'requested-changes' || status === 'changesrequired') {
|
|
72
|
+
return 'changes-requested';
|
|
73
|
+
}
|
|
74
|
+
if (status === 'reviewing') return 'waiting';
|
|
75
|
+
return status;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizePrBuildStatus(pr) {
|
|
79
|
+
if (!pr || typeof pr !== 'object') return '';
|
|
80
|
+
var status = _prFilterKey(pr.buildStatus || pr.build_status);
|
|
81
|
+
if (status === 'success' || status === 'succeeded') return 'passing';
|
|
82
|
+
if (status === 'failed' || status === 'error') return 'failing';
|
|
83
|
+
if (status === 'in-progress' || status === 'in_progress' || status === 'queued') return 'running';
|
|
84
|
+
if (status === 'not-run' || status === 'not_run') return 'none';
|
|
85
|
+
return status;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function _prFilterEntry(pr, dimension) {
|
|
89
|
+
var label = '';
|
|
90
|
+
var value = '';
|
|
91
|
+
if (dimension === 'author') {
|
|
92
|
+
label = normalizePrAuthor(pr);
|
|
93
|
+
value = _prFilterKey(label);
|
|
94
|
+
} else if (dimension === 'lifecycle') {
|
|
95
|
+
value = normalizePrLifecycleStatus(pr);
|
|
96
|
+
label = PR_LIFECYCLE_FILTER_LABELS[value] || _formatPrFilterLabel(value);
|
|
97
|
+
} else if (dimension === 'project') {
|
|
98
|
+
label = normalizePrProject(pr);
|
|
99
|
+
value = _prFilterKey(label);
|
|
100
|
+
} else if (dimension === 'review') {
|
|
101
|
+
value = normalizePrReviewStatus(pr);
|
|
102
|
+
label = _formatPrFilterLabel(value);
|
|
103
|
+
} else if (dimension === 'build') {
|
|
104
|
+
value = normalizePrBuildStatus(pr);
|
|
105
|
+
label = _formatPrFilterLabel(value);
|
|
106
|
+
}
|
|
107
|
+
return value ? { value: value, label: label || value } : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _formatPrFilterLabel(value) {
|
|
111
|
+
var text = _prFilterText(value).replace(/[-_]+/g, ' ');
|
|
112
|
+
return text ? text.charAt(0).toUpperCase() + text.slice(1) : '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getPrFilterValue(pr, dimension) {
|
|
116
|
+
var entry = _prFilterEntry(pr, dimension);
|
|
117
|
+
return entry ? entry.value : '';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function matchesPullRequestFilters(pr, filters) {
|
|
121
|
+
filters = filters || {};
|
|
122
|
+
for (var i = 0; i < PR_FILTER_DIMENSIONS.length; i++) {
|
|
123
|
+
var dimension = PR_FILTER_DIMENSIONS[i];
|
|
124
|
+
var selected = _prFilterKey(filters[dimension]);
|
|
125
|
+
if (selected && getPrFilterValue(pr, dimension) !== selected) return false;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function filterPullRequests(prs, filters) {
|
|
131
|
+
if (!Array.isArray(prs)) return [];
|
|
132
|
+
return prs.filter(function(pr) { return matchesPullRequestFilters(pr, filters); });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getPrFilterOptions(prs) {
|
|
136
|
+
var result = {};
|
|
137
|
+
var records = Array.isArray(prs) ? prs : [];
|
|
138
|
+
PR_FILTER_DIMENSIONS.forEach(function(dimension) {
|
|
139
|
+
var seen = new Map();
|
|
140
|
+
records.forEach(function(pr) {
|
|
141
|
+
var entry = _prFilterEntry(pr, dimension);
|
|
142
|
+
if (entry && !seen.has(entry.value)) seen.set(entry.value, entry);
|
|
143
|
+
});
|
|
144
|
+
var order = PR_FILTER_OPTION_ORDER[dimension] || [];
|
|
145
|
+
result[dimension] = Array.from(seen.values()).sort(function(a, b) {
|
|
146
|
+
var ai = order.indexOf(a.value);
|
|
147
|
+
var bi = order.indexOf(b.value);
|
|
148
|
+
if (ai !== -1 || bi !== -1) {
|
|
149
|
+
if (ai === -1) return 1;
|
|
150
|
+
if (bi === -1) return -1;
|
|
151
|
+
if (ai !== bi) return ai - bi;
|
|
152
|
+
}
|
|
153
|
+
return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' });
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
window.MinionsPrFilters = {
|
|
160
|
+
author: normalizePrAuthor,
|
|
161
|
+
normalizeLifecycle: normalizePrLifecycleStatus,
|
|
162
|
+
value: getPrFilterValue,
|
|
163
|
+
matches: matchesPullRequestFilters,
|
|
164
|
+
filter: filterPullRequests,
|
|
165
|
+
options: getPrFilterOptions,
|
|
166
|
+
};
|
package/dashboard/styles.css
CHANGED
|
@@ -322,6 +322,34 @@
|
|
|
322
322
|
.prd-pending { color: var(--yellow); font-style: italic; }
|
|
323
323
|
|
|
324
324
|
/* PR Tracker */
|
|
325
|
+
.pr-filters {
|
|
326
|
+
display: flex; flex-wrap: wrap; align-items: end; gap: var(--space-4);
|
|
327
|
+
padding: var(--space-4) 0 var(--space-6);
|
|
328
|
+
}
|
|
329
|
+
.pr-filter {
|
|
330
|
+
display: grid; gap: var(--space-1); min-width: 140px;
|
|
331
|
+
color: var(--muted); font-size: var(--text-sm); font-weight: 600;
|
|
332
|
+
}
|
|
333
|
+
.pr-filter select {
|
|
334
|
+
min-height: 32px; padding: var(--space-2) var(--space-4);
|
|
335
|
+
color: var(--text); background: var(--surface2);
|
|
336
|
+
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
|
337
|
+
font: inherit; font-weight: 400;
|
|
338
|
+
}
|
|
339
|
+
.pr-filter select:focus-visible, .pr-filter-reset:focus-visible {
|
|
340
|
+
outline: 2px solid var(--blue); outline-offset: 2px;
|
|
341
|
+
}
|
|
342
|
+
.pr-filter-reset {
|
|
343
|
+
min-height: 32px; padding: var(--space-2) var(--space-5);
|
|
344
|
+
color: var(--blue); background: transparent;
|
|
345
|
+
border: 1px solid var(--blue); border-radius: var(--radius-sm);
|
|
346
|
+
cursor: pointer; font-size: var(--text-base);
|
|
347
|
+
}
|
|
348
|
+
.pr-filter-reset:hover { background: rgba(88,166,255,0.1); }
|
|
349
|
+
.pr-filter-summary {
|
|
350
|
+
min-height: 32px; display: inline-flex; align-items: center;
|
|
351
|
+
color: var(--muted); font-size: var(--text-base);
|
|
352
|
+
}
|
|
325
353
|
.pr-table-wrap { overflow-x: auto; }
|
|
326
354
|
.pr-table { width: 100%; border-collapse: collapse; font-size: var(--text-md); table-layout: auto; }
|
|
327
355
|
.pr-table th:last-child, .pr-table td:last-child { width: 36px; min-width: 36px; text-align: center; }
|
|
@@ -348,7 +376,7 @@
|
|
|
348
376
|
wrap) stay visible while the user is on this page. The modal "see all"
|
|
349
377
|
view uses the same colgroup but is unaffected — modal-body handles its
|
|
350
378
|
own scrolling. */
|
|
351
|
-
#pr-content .pr-table-wrap--prs { max-height: calc(100vh -
|
|
379
|
+
#pr-content .pr-table-wrap--prs { max-height: calc(100vh - 270px); overflow: auto; }
|
|
352
380
|
#pr-content .pr-table--prs thead th { position: sticky; top: 0; background: var(--surface); z-index: 1; }
|
|
353
381
|
.pr-table th { text-align: left; color: var(--muted); font-weight: 500; font-size: var(--text-base); text-transform: uppercase; letter-spacing: 0.5px; padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--border); }
|
|
354
382
|
.pr-table td { padding: var(--space-5); border-bottom: 1px solid var(--border); vertical-align: middle; white-space: nowrap; }
|
package/dashboard-build.js
CHANGED
|
@@ -8,7 +8,7 @@ const shared = require('./engine/shared');
|
|
|
8
8
|
const { safeRead } = shared;
|
|
9
9
|
|
|
10
10
|
const MINIONS_DIR = __dirname;
|
|
11
|
-
const DASHBOARD_SHARED_JS = ['pr-merge-state'];
|
|
11
|
+
const DASHBOARD_SHARED_JS = ['pr-merge-state', 'pr-filters'];
|
|
12
12
|
|
|
13
13
|
function buildDashboardHtml() {
|
|
14
14
|
const dashDir = path.join(MINIONS_DIR, 'dashboard');
|
package/dashboard.js
CHANGED
|
@@ -106,6 +106,8 @@ let PORT = REQUESTED_PORT;
|
|
|
106
106
|
const CONFIG_PATH = path.join(MINIONS_DIR, 'config.json');
|
|
107
107
|
const PINNED_PATH = path.join(MINIONS_DIR, 'pinned.md');
|
|
108
108
|
const PINNED_DEFAULT_CONTENT = '# Pinned Context\n\nCritical notes visible to all agents.';
|
|
109
|
+
const WORK_ITEM_PRIORITY_PARAM_HINT = `priority? (${shared.WORK_ITEM_PRIORITY_VALUES.join('|')})`;
|
|
110
|
+
const PIPELINE_STAGES_PARAM_HINT = `stages[] (task/item priority: ${shared.WORK_ITEM_PRIORITY_VALUES.join('|')})`;
|
|
109
111
|
const KB_PINS_PATH = shared.PINNED_ITEMS_PATH;
|
|
110
112
|
const DASHBOARD_BROWSER_PRESENCE_PATH = path.join(ENGINE_DIR, 'dashboard-browser.json');
|
|
111
113
|
const DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS = 45000;
|
|
@@ -1187,6 +1189,8 @@ function findWorkItemsTargetById(id, source, projects = PROJECTS) {
|
|
|
1187
1189
|
|
|
1188
1190
|
function buildPlanWorkItem(body, projects = PROJECTS, options = {}) {
|
|
1189
1191
|
if (!body?.title || !String(body.title).trim()) return { error: 'title is required' };
|
|
1192
|
+
const priorityError = shared.validateWorkItemPriority(body.priority);
|
|
1193
|
+
if (priorityError) return { error: priorityError.error, priorityError };
|
|
1190
1194
|
|
|
1191
1195
|
// Multi-project: body.project may be an array. Each entry must resolve to a
|
|
1192
1196
|
// known project; ≥2 valid entries trigger cross-repo mode (WI lands in central,
|
|
@@ -1225,7 +1229,7 @@ function buildPlanWorkItem(body, projects = PROJECTS, options = {}) {
|
|
|
1225
1229
|
id: options.id || ('W-' + shared.uid()),
|
|
1226
1230
|
title: body.title,
|
|
1227
1231
|
type: 'plan',
|
|
1228
|
-
priority: body.priority
|
|
1232
|
+
priority: body.priority ?? shared.WORK_ITEM_PRIORITY.HIGH,
|
|
1229
1233
|
description,
|
|
1230
1234
|
status: WI_STATUS.PENDING,
|
|
1231
1235
|
created: now.toISOString(),
|
|
@@ -1240,6 +1244,8 @@ function buildPlanWorkItem(body, projects = PROJECTS, options = {}) {
|
|
|
1240
1244
|
|
|
1241
1245
|
function buildManualPrdItemPlan(body, projects = PROJECTS, options = {}) {
|
|
1242
1246
|
if (!body?.name || !String(body.name).trim()) return { error: 'name is required' };
|
|
1247
|
+
const priorityError = shared.validateWorkItemPriority(body.priority);
|
|
1248
|
+
if (priorityError) return { error: priorityError.error, priorityError };
|
|
1243
1249
|
const target = resolveWorkItemsCreateTarget(body.project, projects);
|
|
1244
1250
|
if (target.error) return { error: target.error };
|
|
1245
1251
|
|
|
@@ -1258,7 +1264,7 @@ function buildManualPrdItemPlan(body, projects = PROJECTS, options = {}) {
|
|
|
1258
1264
|
id: body.id || ('M' + String(nowTime).slice(-4)),
|
|
1259
1265
|
name: String(body.name).trim(),
|
|
1260
1266
|
description: body.description || '',
|
|
1261
|
-
priority: body.priority
|
|
1267
|
+
priority: body.priority ?? shared.WORK_ITEM_PRIORITY.MEDIUM,
|
|
1262
1268
|
estimated_complexity: body.estimated_complexity || 'medium',
|
|
1263
1269
|
status: 'missing',
|
|
1264
1270
|
depends_on: [],
|
|
@@ -1325,6 +1331,24 @@ function validatePipelineProjects(pipeline, projects = PROJECTS) {
|
|
|
1325
1331
|
return null;
|
|
1326
1332
|
}
|
|
1327
1333
|
|
|
1334
|
+
function validatePipelineWorkItemPriorities(pipeline) {
|
|
1335
|
+
const visit = (value) => {
|
|
1336
|
+
if (!value || typeof value !== 'object') return null;
|
|
1337
|
+
if (Object.prototype.hasOwnProperty.call(value, 'priority')) {
|
|
1338
|
+
const error = shared.validateWorkItemPriority(value.priority);
|
|
1339
|
+
if (error) return error;
|
|
1340
|
+
}
|
|
1341
|
+
for (const key of ['stages', 'items']) {
|
|
1342
|
+
for (const child of Array.isArray(value[key]) ? value[key] : []) {
|
|
1343
|
+
const error = visit(child);
|
|
1344
|
+
if (error) return error;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return null;
|
|
1348
|
+
};
|
|
1349
|
+
return visit(pipeline);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1328
1352
|
/**
|
|
1329
1353
|
* Aggregate archived work items from central and project SQL scopes.
|
|
1330
1354
|
*
|
|
@@ -1796,7 +1820,7 @@ function buildDashboardHtml() {
|
|
|
1796
1820
|
'confirm-dialog', 'modal', 'modal-qa', 'settings', 'qa', 'fre', 'refresh'
|
|
1797
1821
|
];
|
|
1798
1822
|
let jsHtml = '';
|
|
1799
|
-
for (const f of ['pr-merge-state']) {
|
|
1823
|
+
for (const f of ['pr-merge-state', 'pr-filters']) {
|
|
1800
1824
|
const content = safeRead(path.join(dashDir, 'shared', f + '.js'));
|
|
1801
1825
|
jsHtml += `\n// ─── shared/${f}.js ────────────────────────────────────────\n${content}\n`;
|
|
1802
1826
|
}
|
|
@@ -1817,11 +1841,12 @@ function buildDashboardHtml() {
|
|
|
1817
1841
|
} catch { /* registry empty or config unreadable — ship empty boot state */ }
|
|
1818
1842
|
|
|
1819
1843
|
const featuresBootstrap = `window.MINIONS_FEATURES = ${JSON.stringify(featuresBoot)};\n`;
|
|
1844
|
+
const workItemPrioritiesBootstrap = `window.MINIONS_WORK_ITEM_PRIORITIES = ${JSON.stringify(shared.WORK_ITEM_PRIORITY_VALUES)};\n`;
|
|
1820
1845
|
|
|
1821
1846
|
let assembled = layout
|
|
1822
1847
|
.replace('/* __CSS__ */', () => css)
|
|
1823
1848
|
.replace('<!-- __PAGES__ -->', () => pageHtml)
|
|
1824
|
-
.replace('/* __JS__ */', () => `window.__MINIONS_HOME = ${JSON.stringify(os.homedir())};\n${featuresBootstrap}${jsHtml}`)
|
|
1849
|
+
.replace('/* __JS__ */', () => `window.__MINIONS_HOME = ${JSON.stringify(os.homedir())};\n${featuresBootstrap}${workItemPrioritiesBootstrap}${jsHtml}`)
|
|
1825
1850
|
.replace(/\{\{favicon_emoji\}\}/g, FAVICON_EMOJI)
|
|
1826
1851
|
.replace(/\{\{title_suffix\}\}/g, TITLE_SUFFIX);
|
|
1827
1852
|
|
|
@@ -6629,6 +6654,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
6629
6654
|
try {
|
|
6630
6655
|
const body = await readBody(req);
|
|
6631
6656
|
if (!body.title || !body.title.trim()) return jsonReply(res, 400, { error: 'title is required' });
|
|
6657
|
+
const priorityError = shared.validateWorkItemPriority(body.priority);
|
|
6658
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
6632
6659
|
if (body.depends_on !== undefined) {
|
|
6633
6660
|
if (!Array.isArray(body.depends_on)) return jsonReply(res, 400, { error: 'depends_on must be an array of strings' });
|
|
6634
6661
|
if (!body.depends_on.every(s => typeof s === 'string')) return jsonReply(res, 400, { error: 'depends_on entries must be strings' });
|
|
@@ -6686,7 +6713,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
6686
6713
|
const id = 'W-' + shared.uid();
|
|
6687
6714
|
const item = {
|
|
6688
6715
|
id, title: body.title.trim(), type: normalizedType,
|
|
6689
|
-
priority: body.priority
|
|
6716
|
+
priority: body.priority ?? shared.WORK_ITEM_PRIORITY.MEDIUM, description: body.description || '',
|
|
6690
6717
|
status: WI_STATUS.PENDING, created: new Date().toISOString(), createdBy: 'dashboard',
|
|
6691
6718
|
};
|
|
6692
6719
|
if (Array.isArray(body.depends_on)) {
|
|
@@ -6835,6 +6862,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
6835
6862
|
const body = await readBody(req);
|
|
6836
6863
|
const { id, source, title, description, type, priority, agent } = body;
|
|
6837
6864
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
6865
|
+
const priorityError = shared.validateWorkItemPriority(priority);
|
|
6866
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
6838
6867
|
if (body.depends_on !== undefined) {
|
|
6839
6868
|
if (!Array.isArray(body.depends_on)) return jsonReply(res, 400, { error: 'depends_on must be an array of strings' });
|
|
6840
6869
|
if (!body.depends_on.every(s => typeof s === 'string')) return jsonReply(res, 400, { error: 'depends_on entries must be strings' });
|
|
@@ -6983,7 +7012,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
6983
7012
|
const body = await readBody(req);
|
|
6984
7013
|
if (!body.title || !body.title.trim()) return jsonReply(res, 400, { error: 'title is required' });
|
|
6985
7014
|
const planWorkItem = buildPlanWorkItem(body);
|
|
6986
|
-
if (planWorkItem.error) return jsonReply(res, 400, { error: planWorkItem.error });
|
|
7015
|
+
if (planWorkItem.error) return jsonReply(res, 400, planWorkItem.priorityError || { error: planWorkItem.error });
|
|
6987
7016
|
// Write as a work item with type 'plan' — user must explicitly execute plan-to-prd after reviewing
|
|
6988
7017
|
mutateWorkItems('central', items => { items.push(planWorkItem.item); });
|
|
6989
7018
|
recordCcTurnIfPresent(req, {
|
|
@@ -6998,7 +7027,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
6998
7027
|
const body = await readBody(req);
|
|
6999
7028
|
if (!body.name || !body.name.trim()) return jsonReply(res, 400, { error: 'name is required' });
|
|
7000
7029
|
const manualPrd = buildManualPrdItemPlan(body);
|
|
7001
|
-
if (manualPrd.error) return jsonReply(res, 400, { error: manualPrd.error });
|
|
7030
|
+
if (manualPrd.error) return jsonReply(res, 400, manualPrd.priorityError || { error: manualPrd.error });
|
|
7002
7031
|
|
|
7003
7032
|
const planFile = 'manual-' + shared.uid() + '.json';
|
|
7004
7033
|
prdStore.writePrd(planFile, manualPrd.plan);
|
|
@@ -7011,6 +7040,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
7011
7040
|
try {
|
|
7012
7041
|
const body = await readBody(req);
|
|
7013
7042
|
if (!body.source || !body.itemId) return jsonReply(res, 400, { error: 'source and itemId required' });
|
|
7043
|
+
const priorityError = shared.validateWorkItemPriority(body.priority);
|
|
7044
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
7014
7045
|
if (!body.source.endsWith('.json')) return jsonReply(res, 400, { error: 'expected a PRD JSON filename in `source` (got `' + body.source + '`). Pass prd/<plan>.json, not plans/<plan>.md.' });
|
|
7015
7046
|
const preCheck = prdStore.readPrd(body.source);
|
|
7016
7047
|
if (!preCheck) return jsonReply(res, 404, { error: 'plan not found' });
|
|
@@ -10671,6 +10702,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10671
10702
|
const body = await readBody(req);
|
|
10672
10703
|
let { id, cron, title, type, project, agent, description, priority, enabled } = body;
|
|
10673
10704
|
if (!cron || !title) return jsonReply(res, 400, { error: 'cron and title are required' });
|
|
10705
|
+
const priorityError = shared.validateWorkItemPriority(priority);
|
|
10706
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
10674
10707
|
reloadConfig();
|
|
10675
10708
|
const projectTarget = resolveScheduleProjectValue(project, PROJECTS);
|
|
10676
10709
|
if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
|
|
@@ -10709,6 +10742,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10709
10742
|
const body = await readBody(req);
|
|
10710
10743
|
let { id, cron, title, type, project, agent, description, priority, enabled } = body;
|
|
10711
10744
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
10745
|
+
const priorityError = shared.validateWorkItemPriority(priority);
|
|
10746
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
10712
10747
|
reloadConfig();
|
|
10713
10748
|
if (project !== undefined) {
|
|
10714
10749
|
const projectTarget = resolveScheduleProjectValue(project, PROJECTS);
|
|
@@ -10887,6 +10922,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10887
10922
|
const body = await readBody(req);
|
|
10888
10923
|
const { target, targetType, condition, interval, owner, description, project, notify, stopAfter, onNotMet, action, requires } = body;
|
|
10889
10924
|
if (!target) return jsonReply(res, 400, { error: 'target is required' });
|
|
10925
|
+
const priorityError = watchesMod.validateActionWorkItemPriority(action);
|
|
10926
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
10890
10927
|
try {
|
|
10891
10928
|
const watch = watchesMod.createWatch({ target, targetType, condition, interval, owner, description, project, notify, stopAfter, onNotMet, action, requires });
|
|
10892
10929
|
invalidateStatusCache();
|
|
@@ -10903,6 +10940,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10903
10940
|
const body = await readBody(req);
|
|
10904
10941
|
const { id, ...updates } = body;
|
|
10905
10942
|
if (!id) return jsonReply(res, 400, { error: 'id is required' });
|
|
10943
|
+
const priorityError = watchesMod.validateActionWorkItemPriority(updates.action);
|
|
10944
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
10906
10945
|
try {
|
|
10907
10946
|
const watch = watchesMod.updateWatch(id, updates);
|
|
10908
10947
|
if (!watch) return jsonReply(res, 404, { error: 'Watch not found' });
|
|
@@ -13391,8 +13430,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13391
13430
|
{ method: 'POST', path: '/api/qa/runners/reload', desc: 'Clear the in-process runner registry, re-register built-ins, and re-scan qa-runners.d/ for plugin edits.', handler: handleQaRunnersReload },
|
|
13392
13431
|
|
|
13393
13432
|
// Work items
|
|
13394
|
-
{ method: 'POST', path: '/api/work-items', desc: 'Create a new work item', params:
|
|
13395
|
-
{ method: 'POST', path: '/api/work-items/update', desc: 'Edit a pending/failed work item', params:
|
|
13433
|
+
{ method: 'POST', path: '/api/work-items', desc: 'Create a new work item', params: `title, type?, description?, ${WORK_ITEM_PRIORITY_PARAM_HINT}, project?, agent?, agents?, scope?, references?, acceptanceCriteria?, skipPr?, oneShot?, meta?, meta.pr_followup?, meta.workdir?, X-Minions-Agent?, X-Minions-Origin-Wi?`, handler: handleWorkItemsCreate },
|
|
13434
|
+
{ method: 'POST', path: '/api/work-items/update', desc: 'Edit a pending/failed work item', params: `id, source?, title?, description?, type?, ${WORK_ITEM_PRIORITY_PARAM_HINT}, agent?, project?, references?, acceptanceCriteria?, depends_on?, workdir?, meta?`, handler: handleWorkItemsUpdate },
|
|
13396
13435
|
{ method: 'POST', path: '/api/work-items/retry', desc: 'Reset a failed/dispatched item to pending', params: 'id, source?', handler: handleWorkItemsRetry },
|
|
13397
13436
|
{ method: 'POST', path: '/api/work-items/delete', desc: 'Remove a work item, kill agent, clear dispatch', params: 'id, source?', handler: handleWorkItemsDelete },
|
|
13398
13437
|
{ method: 'POST', path: '/api/work-items/cancel', desc: 'Cancel a work item, kill agent, clear dispatch', params: 'id, source?, reason?', handler: handleWorkItemsCancel },
|
|
@@ -13496,7 +13535,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13496
13535
|
{ method: 'POST', path: '/api/notes-save', desc: 'Save edited notes.md content', params: 'content, file?', handler: handleNotesSave },
|
|
13497
13536
|
|
|
13498
13537
|
// Plans
|
|
13499
|
-
{ method: 'POST', path: '/api/plan', desc: 'Create a plan work item that chains to PRD on completion', params:
|
|
13538
|
+
{ method: 'POST', path: '/api/plan', desc: 'Create a plan work item that chains to PRD on completion', params: `title, description?, ${WORK_ITEM_PRIORITY_PARAM_HINT}, project? (string OR array for cross-repo plans), agent?, branch_strategy? or branchStrategy?`, handler: handlePlanCreate },
|
|
13500
13539
|
{ method: 'GET', path: '/api/plans', desc: 'List plan files (.md drafts + .json PRDs)', handler: handlePlansList },
|
|
13501
13540
|
{ method: 'POST', path: '/api/plans/trigger-verify', desc: 'Manually trigger verification for a completed plan', params: 'file', handler: handlePlansTriggerVerify },
|
|
13502
13541
|
{ method: 'POST', path: '/api/plans/approve', desc: 'Approve a plan for execution', params: 'file, approvedBy?', handler: handlePlansApprove },
|
|
@@ -13513,8 +13552,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13513
13552
|
{ method: 'GET', path: /^\/api\/plans\/([^?]+)$/, template: '/api/plans/:file', desc: 'Read a full plan (JSON from prd/ or markdown from plans/)', handler: handlePlansRead },
|
|
13514
13553
|
|
|
13515
13554
|
// PRD items
|
|
13516
|
-
{ method: 'POST', path: '/api/prd-items', desc: 'Create a PRD item as a plan file in prd/ (auto-approved)', params:
|
|
13517
|
-
{ method: 'POST', path: '/api/prd-items/update', desc: 'Edit a PRD item in its source plan JSON', params:
|
|
13555
|
+
{ method: 'POST', path: '/api/prd-items', desc: 'Create a PRD item as a plan file in prd/ (auto-approved)', params: `name, description?, ${WORK_ITEM_PRIORITY_PARAM_HINT}, estimated_complexity?, project?, item_project?, id?`, handler: handlePrdItemsCreate },
|
|
13556
|
+
{ method: 'POST', path: '/api/prd-items/update', desc: 'Edit a PRD item in its source plan JSON', params: `source, itemId, name?, description?, ${WORK_ITEM_PRIORITY_PARAM_HINT}, estimated_complexity?, status?`, handler: handlePrdItemsUpdate },
|
|
13518
13557
|
{ method: 'POST', path: '/api/prd-items/remove', desc: 'Remove a PRD item from plan + cancel materialized work item', params: 'source, itemId', handler: handlePrdItemsRemove },
|
|
13519
13558
|
// /api/prd/regenerate removed — use /api/plans/approve which does diff-aware update
|
|
13520
13559
|
|
|
@@ -14323,8 +14362,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
14323
14362
|
|
|
14324
14363
|
// Schedules
|
|
14325
14364
|
{ method: 'POST', path: '/api/schedules/parse-natural', desc: 'Parse natural language schedule text into cron expression', params: 'text', handler: handleSchedulesParseNatural },
|
|
14326
|
-
{ method: 'POST', path: '/api/schedules', desc: 'Create a new schedule', params:
|
|
14327
|
-
{ method: 'POST', path: '/api/schedules/update', desc: 'Update an existing schedule', params:
|
|
14365
|
+
{ method: 'POST', path: '/api/schedules', desc: 'Create a new schedule', params: `cron, title, id?, type?, project?, agent?, description?, ${WORK_ITEM_PRIORITY_PARAM_HINT}, enabled?`, handler: handleSchedulesCreate },
|
|
14366
|
+
{ method: 'POST', path: '/api/schedules/update', desc: 'Update an existing schedule', params: `id, cron?, title?, type?, project?, agent?, description?, ${WORK_ITEM_PRIORITY_PARAM_HINT}, enabled?`, handler: handleSchedulesUpdate },
|
|
14328
14367
|
{ method: 'POST', path: '/api/schedules/delete', desc: 'Delete a schedule', params: 'id', handler: handleSchedulesDelete },
|
|
14329
14368
|
{ method: 'POST', path: '/api/schedules/run-now', desc: 'Manually enqueue the work item for a schedule', params: 'id', handler: handleSchedulesRunNow },
|
|
14330
14369
|
|
|
@@ -14345,9 +14384,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
14345
14384
|
const result = pipelines.map(p => ({ ...p, runs: (runs[p.id] || []).slice(-5) }));
|
|
14346
14385
|
return jsonReply(res, 200, result);
|
|
14347
14386
|
}},
|
|
14348
|
-
{ method: 'POST', path: '/api/pipelines', desc: 'Create a pipeline', params:
|
|
14387
|
+
{ method: 'POST', path: '/api/pipelines', desc: 'Create a pipeline', params: `id, title, ${PIPELINE_STAGES_PARAM_HINT}, trigger?, stopWhen?, monitoredResources?`, handler: async (req, res) => {
|
|
14349
14388
|
const body = await readBody(req);
|
|
14350
14389
|
if (!body.id || !body.title || !body.stages) return jsonReply(res, 400, { error: 'id, title, and stages required' });
|
|
14390
|
+
const priorityError = validatePipelineWorkItemPriorities({ stages: body.stages });
|
|
14391
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
14351
14392
|
const { savePipeline, getPipeline } = require('./engine/pipeline');
|
|
14352
14393
|
if (getPipeline(body.id)) return jsonReply(res, 409, { error: 'Pipeline already exists' });
|
|
14353
14394
|
const pipeline = { id: body.id, title: body.title, stages: body.stages, trigger: body.trigger || {}, enabled: body.enabled !== false };
|
|
@@ -14361,12 +14402,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
14361
14402
|
invalidateStatusCache();
|
|
14362
14403
|
return jsonReply(res, 200, { ok: true, id: pipeline.id });
|
|
14363
14404
|
}},
|
|
14364
|
-
{ method: 'POST', path: '/api/pipelines/update', desc: 'Update a pipeline', params:
|
|
14405
|
+
{ method: 'POST', path: '/api/pipelines/update', desc: 'Update a pipeline', params: `id, title?, ${PIPELINE_STAGES_PARAM_HINT.replace('stages[]', 'stages[]?')}, trigger?, enabled?, stopWhen?, monitoredResources?`, handler: async (req, res) => {
|
|
14365
14406
|
const body = await readBody(req);
|
|
14366
14407
|
if (!body.id) return jsonReply(res, 400, { error: 'id required' });
|
|
14367
14408
|
const { getPipeline, savePipeline } = require('./engine/pipeline');
|
|
14368
14409
|
const pipeline = getPipeline(body.id);
|
|
14369
14410
|
if (!pipeline) return jsonReply(res, 404, { error: 'Pipeline not found' });
|
|
14411
|
+
const priorityError = validatePipelineWorkItemPriorities({ stages: body.stages });
|
|
14412
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
14370
14413
|
if (body.title !== undefined) pipeline.title = body.title;
|
|
14371
14414
|
if (body.stages !== undefined) pipeline.stages = body.stages;
|
|
14372
14415
|
if (body.trigger !== undefined) pipeline.trigger = body.trigger;
|
package/engine/shared.js
CHANGED
|
@@ -4048,6 +4048,27 @@ function backfillProjectWorkSourceDefaults(config) {
|
|
|
4048
4048
|
|
|
4049
4049
|
// ─── Status & Type Constants ─────────────────────────────────────────────────
|
|
4050
4050
|
|
|
4051
|
+
const WORK_ITEM_PRIORITY = Object.freeze({
|
|
4052
|
+
CRITICAL: 'critical',
|
|
4053
|
+
HIGH: 'high',
|
|
4054
|
+
MEDIUM: 'medium',
|
|
4055
|
+
LOW: 'low',
|
|
4056
|
+
});
|
|
4057
|
+
const WORK_ITEM_PRIORITY_VALUES = Object.freeze(Object.values(WORK_ITEM_PRIORITY));
|
|
4058
|
+
const _WORK_ITEM_PRIORITY_SET = new Set(WORK_ITEM_PRIORITY_VALUES);
|
|
4059
|
+
const WORK_ITEM_PRIORITY_ERROR_CODE = 'invalid-work-item-priority';
|
|
4060
|
+
|
|
4061
|
+
function validateWorkItemPriority(priority) {
|
|
4062
|
+
if (priority === undefined || _WORK_ITEM_PRIORITY_SET.has(priority)) return null;
|
|
4063
|
+
const rejected = JSON.stringify(priority);
|
|
4064
|
+
return {
|
|
4065
|
+
error: `Invalid work-item priority ${rejected}. Allowed values: ${WORK_ITEM_PRIORITY_VALUES.join(', ')}.`,
|
|
4066
|
+
code: WORK_ITEM_PRIORITY_ERROR_CODE,
|
|
4067
|
+
rejectedValue: priority,
|
|
4068
|
+
allowedValues: [...WORK_ITEM_PRIORITY_VALUES],
|
|
4069
|
+
};
|
|
4070
|
+
}
|
|
4071
|
+
|
|
4051
4072
|
const WI_STATUS = {
|
|
4052
4073
|
PENDING: 'pending', DISPATCHED: 'dispatched', DONE: 'done', FAILED: 'failed',
|
|
4053
4074
|
PAUSED: 'paused', QUEUED: 'queued',
|
|
@@ -8781,6 +8802,7 @@ module.exports = {
|
|
|
8781
8802
|
runtimeConfigWarnings,
|
|
8782
8803
|
projectWorkSourceWarnings,
|
|
8783
8804
|
backfillProjectWorkSourceDefaults,
|
|
8805
|
+
WORK_ITEM_PRIORITY, WORK_ITEM_PRIORITY_VALUES, WORK_ITEM_PRIORITY_ERROR_CODE, validateWorkItemPriority,
|
|
8784
8806
|
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, isPrdArchived, isDefunctPrd, WORK_TYPE, isFixLikeWorkType, WORKTREE_REQUIRING_TYPES, LIVE_VALIDATION_WORK_TYPES, VALID_WORK_TYPES, resolveWorkItemTypeFromPrdItem, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, PR_PENDING_REASON, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, RETRY_DELAY_MS, ADO_TOKEN_REFRESH_MAX_RETRIES, DISPATCH_RESULT, mutateMetrics, mutateWatches, mutateScheduleRuns, mutatePipelineRuns, mutateManagedProcesses, mutateWorktreePool, mutateQaRuns, mutateQaSessions, trackReviewMetric, queuePlanToPrd, extractPlanDeclaredProject, extractPlanTargetProjects,
|
|
8785
8807
|
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS, WATCH_ACTION_TYPE,
|
|
8786
8808
|
WATCH_STALLED_DEFAULT_TICKS, WATCH_STUCK_STAGE_DEFAULT_TICKS,
|
package/engine/watch-actions.js
CHANGED
|
@@ -397,12 +397,16 @@ registerActionType(WATCH_ACTION_TYPE.DISPATCH_WORK_ITEM, {
|
|
|
397
397
|
{ key: 'type', required: false, description: 'Work type (implement|fix|review|...). Defaults to implement.' },
|
|
398
398
|
{ key: 'agent', required: false, description: 'Specific agent to route to.' },
|
|
399
399
|
{ key: 'project', required: false, description: 'Target project (defaults to watch.project, or central).' },
|
|
400
|
-
{ key: 'priority', required: false, description: '
|
|
400
|
+
{ key: 'priority', required: false, description: `${shared.WORK_ITEM_PRIORITY_VALUES.join('|')}. Defaults to medium.` },
|
|
401
401
|
],
|
|
402
402
|
handler: async (watch, ctx) => {
|
|
403
403
|
const p = ctx.params || {};
|
|
404
404
|
const title = String(p.title || '').trim();
|
|
405
405
|
if (!title) return { ok: false, summary: 'dispatch-work-item: title is required' };
|
|
406
|
+
const priorityError = shared.validateWorkItemPriority(p.priority);
|
|
407
|
+
if (priorityError) {
|
|
408
|
+
return { ok: false, summary: priorityError.error, error: priorityError };
|
|
409
|
+
}
|
|
406
410
|
|
|
407
411
|
const project = p.project || watch.project || null;
|
|
408
412
|
const scope = project || 'central';
|
|
@@ -412,7 +416,7 @@ registerActionType(WATCH_ACTION_TYPE.DISPATCH_WORK_ITEM, {
|
|
|
412
416
|
id,
|
|
413
417
|
title,
|
|
414
418
|
type: p.type || WORK_TYPE.IMPLEMENT,
|
|
415
|
-
priority: p.priority
|
|
419
|
+
priority: p.priority ?? shared.WORK_ITEM_PRIORITY.MEDIUM,
|
|
416
420
|
description: p.description || '',
|
|
417
421
|
status: WI_STATUS.PENDING,
|
|
418
422
|
created: ts(),
|
|
@@ -473,6 +477,7 @@ registerActionType(WATCH_ACTION_TYPE.RUN_SKILL, {
|
|
|
473
477
|
{ key: 'args', required: false, description: 'Args/instructions to pass to the skill.' },
|
|
474
478
|
{ key: 'agent', required: false, description: 'Specific agent to route to.' },
|
|
475
479
|
{ key: 'project', required: false, description: 'Target project.' },
|
|
480
|
+
{ key: 'priority', required: false, description: `${shared.WORK_ITEM_PRIORITY_VALUES.join('|')}. Defaults to medium.` },
|
|
476
481
|
],
|
|
477
482
|
handler: async (watch, ctx) => {
|
|
478
483
|
const p = ctx.params || {};
|
|
@@ -488,7 +493,7 @@ registerActionType(WATCH_ACTION_TYPE.RUN_SKILL, {
|
|
|
488
493
|
type: WORK_TYPE.IMPLEMENT,
|
|
489
494
|
agent: p.agent,
|
|
490
495
|
project: p.project,
|
|
491
|
-
priority: p.priority
|
|
496
|
+
priority: p.priority ?? shared.WORK_ITEM_PRIORITY.MEDIUM,
|
|
492
497
|
},
|
|
493
498
|
});
|
|
494
499
|
},
|
|
@@ -1092,6 +1097,8 @@ function validateAction(action) {
|
|
|
1092
1097
|
return `action.params.${p.key} is required for action type ${type}`;
|
|
1093
1098
|
}
|
|
1094
1099
|
}
|
|
1100
|
+
const priorityError = validateActionWorkItemPriority(action);
|
|
1101
|
+
if (priorityError) return priorityError.error;
|
|
1095
1102
|
// P-w7c5d8b3 — Phase 3.2: optional `guard` field. Shape is locked here
|
|
1096
1103
|
// (object with a string `expr`), but the expression itself is NOT
|
|
1097
1104
|
// pre-parsed at validation time — safe-expr.evaluate is the single
|
|
@@ -1178,6 +1185,23 @@ function validateAction(action) {
|
|
|
1178
1185
|
return null;
|
|
1179
1186
|
}
|
|
1180
1187
|
|
|
1188
|
+
function validateActionWorkItemPriority(action) {
|
|
1189
|
+
if (action === null || action === undefined) return null;
|
|
1190
|
+
if (Array.isArray(action)) {
|
|
1191
|
+
for (const step of action) {
|
|
1192
|
+
const error = validateActionWorkItemPriority(step);
|
|
1193
|
+
if (error) return error;
|
|
1194
|
+
}
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
if (typeof action !== 'object') return null;
|
|
1198
|
+
const type = String(action.type || '').toLowerCase();
|
|
1199
|
+
if (type !== WATCH_ACTION_TYPE.DISPATCH_WORK_ITEM && type !== WATCH_ACTION_TYPE.RUN_SKILL) return null;
|
|
1200
|
+
const priority = action.params?.priority;
|
|
1201
|
+
if (typeof priority === 'string' && priority.includes('{{')) return null;
|
|
1202
|
+
return shared.validateWorkItemPriority(priority);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1181
1205
|
module.exports = {
|
|
1182
1206
|
registerActionType,
|
|
1183
1207
|
getActionType,
|
|
@@ -1187,6 +1211,7 @@ module.exports = {
|
|
|
1187
1211
|
buildTriggerContext,
|
|
1188
1212
|
substituteTemplate,
|
|
1189
1213
|
validateAction,
|
|
1214
|
+
validateActionWorkItemPriority,
|
|
1190
1215
|
computeWatchDedupKey, // exported for testing (BUG-H10)
|
|
1191
1216
|
WATCH_DEDUP_ACTIVE_STATUSES, // exported for testing (BUG-H10)
|
|
1192
1217
|
};
|
package/engine/watches.js
CHANGED
|
@@ -1455,6 +1455,7 @@ module.exports = {
|
|
|
1455
1455
|
runWatchAction: watchActions.runWatchAction,
|
|
1456
1456
|
runWatchActionChain: watchActions.runWatchActionChain,
|
|
1457
1457
|
validateAction: watchActions.validateAction,
|
|
1458
|
+
validateActionWorkItemPriority: watchActions.validateActionWorkItemPriority,
|
|
1458
1459
|
_captureState, // exported for testing
|
|
1459
1460
|
_runActionTask, // exported for testing — invoked by checkWatches per fired action
|
|
1460
1461
|
_TARGET_TYPES: TARGET_TYPES, // exported for testing — direct registry access
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2398",
|
|
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"
|