@yemi33/minions 0.1.2397 → 0.1.2399
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/render-prd.js +8 -3
- package/dashboard/js/render-schedules.js +3 -1
- package/dashboard/js/render-work-items.js +7 -2
- package/dashboard/slim/js/modals-tiles.js +29 -17
- package/dashboard/slim/styles.css +10 -6
- package/dashboard.js +86 -15
- package/engine/resolve-area.js +200 -0
- 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/prompts/cc-system.md +19 -0
|
@@ -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)">' +
|
|
@@ -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 =>
|
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
if (ev.key === 'Escape' && modal.classList.contains('open')) close();
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
+
function clearTileModalBody() {
|
|
16
|
+
var body = document.getElementById('slim-tile-body');
|
|
17
|
+
if (body) body.textContent = '';
|
|
18
|
+
}
|
|
15
19
|
// Stop the agent-detail "Working for" ticker on every dismiss path so the
|
|
16
20
|
// 1s interval started in openAgentDetail can't leak after the modal closes.
|
|
17
21
|
bindModalClose('slim-agent-modal', 'slim-agent-close', _stopAgentDetailRuntime);
|
|
18
22
|
bindModalClose('slim-tools-modal', 'slim-tools-close');
|
|
19
|
-
bindModalClose('slim-tile-modal', 'slim-tile-close');
|
|
23
|
+
bindModalClose('slim-tile-modal', 'slim-tile-close', clearTileModalBody);
|
|
20
24
|
|
|
21
25
|
function tileEmpty(body, text) {
|
|
22
26
|
var d = document.createElement('div');
|
|
@@ -83,16 +87,24 @@
|
|
|
83
87
|
|
|
84
88
|
// Per-tile body renderers for the detail modal. Each reads the latest status
|
|
85
89
|
// snapshot and fills the modal body; openTileModal looks them up by key.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
// Engine tile body — reuses the LITERAL classic dashboard /engine screen
|
|
91
|
+
// instead of a slim-specific key/value readout, so there is one Engine UI, not
|
|
92
|
+
// two (W-mrtdnknm000med95 / "reuse, don't fork" — mirrors renderQueuedWorkBody
|
|
93
|
+
// for the Work tile, renderPlansBody for the Plans tile, and renderPrsBody for
|
|
94
|
+
// the PRs tile). Slim and classic are two separate IIFE bundles with no shared
|
|
95
|
+
// scope, so we embed the real /engine screen in an iframe with the chrome-off
|
|
96
|
+
// ?embed=1 mode. The iframed page IS the classic screen — the full engine
|
|
97
|
+
// dashboard (quick stats, engine log, managed-process panel + its log SSE
|
|
98
|
+
// stream, memory + diagnostics panels), backed by the same /api endpoints and
|
|
99
|
+
// live streams — with zero duplicated rendering logic. Classic is reachable at
|
|
100
|
+
// /engine even with slim-ux ON (only / is taken over). Built with createElement
|
|
101
|
+
// (no innerHTML) to satisfy the dashboard no-unsanitized lint gate.
|
|
102
|
+
function renderEngineBody(body) {
|
|
103
|
+
var frame = document.createElement('iframe');
|
|
104
|
+
frame.className = 'slim-engine-embed';
|
|
105
|
+
frame.src = '/engine?embed=1';
|
|
106
|
+
frame.title = 'Engine';
|
|
107
|
+
body.appendChild(frame);
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
function renderDispatchTileBody(body, data, isActive) {
|
|
@@ -184,7 +196,7 @@
|
|
|
184
196
|
|
|
185
197
|
// tile key -> { title, render }. Adding a tile is a one-line table edit.
|
|
186
198
|
var TILE_VIEWS = {
|
|
187
|
-
engine: { title: 'Engine', render:
|
|
199
|
+
engine: { title: 'Engine', render: function(body) { renderEngineBody(body); } },
|
|
188
200
|
dispatches: { title: 'Active dispatches', render: function(body, data) { renderDispatchTileBody(body, data, true); } },
|
|
189
201
|
queued: { title: 'Queued work', render: function(body) { renderQueuedWorkBody(body); } },
|
|
190
202
|
plans: { title: 'Plans', render: function(body) { renderPlansBody(body); } },
|
|
@@ -209,10 +221,11 @@
|
|
|
209
221
|
// The "+ Link PR" header chip only applies to the PR list.
|
|
210
222
|
var headerChip = document.getElementById('slim-tile-modal-linkpr');
|
|
211
223
|
if (headerChip) headerChip.style.display = (key === 'prs') ? '' : 'none';
|
|
212
|
-
// The queued, plans + prs tiles embed a full classic screen
|
|
213
|
-
// /prs) in an iframe — widen the modal + drop the
|
|
214
|
-
// embedded screen gets real estate. All
|
|
215
|
-
// treatment.
|
|
224
|
+
// The engine, queued, plans + prs tiles embed a full classic screen
|
|
225
|
+
// (/engine, /work, /plans, /prs) in an iframe — widen the modal + drop the
|
|
226
|
+
// body padding so the embedded screen gets real estate. All four reuse the
|
|
227
|
+
// shared wide-modal treatment.
|
|
228
|
+
modal.classList.toggle('tile-modal--engine', key === 'engine');
|
|
216
229
|
modal.classList.toggle('tile-modal--work', key === 'queued');
|
|
217
230
|
modal.classList.toggle('tile-modal--plans', key === 'plans');
|
|
218
231
|
modal.classList.toggle('tile-modal--prs', key === 'prs');
|
|
@@ -251,4 +264,3 @@
|
|
|
251
264
|
(function bindPlansTileCount() {
|
|
252
265
|
if (typeof loadPlansCounts === 'function') setTimeout(loadPlansCounts, 1500);
|
|
253
266
|
})();
|
|
254
|
-
|
|
@@ -677,16 +677,19 @@
|
|
|
677
677
|
max-width: calc(100vw - 32px);
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
-
/* Cockpit-tile detail modal: list of dispatches
|
|
681
|
-
|
|
680
|
+
/* Cockpit-tile detail modal: list of dispatches or watches (reuses
|
|
681
|
+
.agent-detail-row / .tile-item). */
|
|
682
682
|
#slim-tile-modal .modal { width: 720px; max-width: calc(100vw - 32px); }
|
|
683
|
-
/* W-mqrdggys000f94a4 / W-mr28ko750010199f / W-mr3l7y0m0007102a
|
|
684
|
-
Queued-work + Plans + PRs tiles embed a
|
|
685
|
-
/plans, /prs) in an iframe; widen the
|
|
686
|
-
embedded screen gets the full
|
|
683
|
+
/* W-mqrdggys000f94a4 / W-mr28ko750010199f / W-mr3l7y0m0007102a /
|
|
684
|
+
W-mrtdnknm000med95 — the Engine + Queued-work + Plans + PRs tiles embed a
|
|
685
|
+
full classic screen (/engine, /work, /plans, /prs) in an iframe; widen the
|
|
686
|
+
modal + remove body padding so the embedded screen gets the full
|
|
687
|
+
width/height. */
|
|
688
|
+
#slim-tile-modal.tile-modal--engine .modal,
|
|
687
689
|
#slim-tile-modal.tile-modal--work .modal,
|
|
688
690
|
#slim-tile-modal.tile-modal--plans .modal,
|
|
689
691
|
#slim-tile-modal.tile-modal--prs .modal { width: 1180px; }
|
|
692
|
+
#slim-tile-modal.tile-modal--engine .modal-body,
|
|
690
693
|
#slim-tile-modal.tile-modal--work .modal-body,
|
|
691
694
|
#slim-tile-modal.tile-modal--plans .modal-body,
|
|
692
695
|
#slim-tile-modal.tile-modal--prs .modal-body { padding: 0; }
|
|
@@ -696,6 +699,7 @@
|
|
|
696
699
|
full width/height. Mirrors the Queued-work/Plans/PRs tile treatment. */
|
|
697
700
|
#slim-settings-modal .modal { width: 1180px; max-width: calc(100vw - 32px); }
|
|
698
701
|
#slim-settings-modal .modal-body { padding: 0; }
|
|
702
|
+
.slim-engine-embed,
|
|
699
703
|
.slim-work-embed,
|
|
700
704
|
.slim-plans-embed,
|
|
701
705
|
.slim-prs-embed,
|
package/dashboard.js
CHANGED
|
@@ -62,6 +62,7 @@ const createPrWorktree = require('./engine/create-pr-worktree');
|
|
|
62
62
|
const prResolve = require('./engine/pr-resolve');
|
|
63
63
|
const prFixTarget = require('./engine/pr-fix-target');
|
|
64
64
|
const prTrack = require('./engine/pr-track');
|
|
65
|
+
const resolveArea = require('./engine/resolve-area');
|
|
65
66
|
const features = require('./engine/features');
|
|
66
67
|
const ccWorkerPool = require('./engine/cc-worker-pool');
|
|
67
68
|
const diagnosticsMemory = require('./engine/diagnostics-memory');
|
|
@@ -106,6 +107,8 @@ let PORT = REQUESTED_PORT;
|
|
|
106
107
|
const CONFIG_PATH = path.join(MINIONS_DIR, 'config.json');
|
|
107
108
|
const PINNED_PATH = path.join(MINIONS_DIR, 'pinned.md');
|
|
108
109
|
const PINNED_DEFAULT_CONTENT = '# Pinned Context\n\nCritical notes visible to all agents.';
|
|
110
|
+
const WORK_ITEM_PRIORITY_PARAM_HINT = `priority? (${shared.WORK_ITEM_PRIORITY_VALUES.join('|')})`;
|
|
111
|
+
const PIPELINE_STAGES_PARAM_HINT = `stages[] (task/item priority: ${shared.WORK_ITEM_PRIORITY_VALUES.join('|')})`;
|
|
109
112
|
const KB_PINS_PATH = shared.PINNED_ITEMS_PATH;
|
|
110
113
|
const DASHBOARD_BROWSER_PRESENCE_PATH = path.join(ENGINE_DIR, 'dashboard-browser.json');
|
|
111
114
|
const DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS = 45000;
|
|
@@ -1187,6 +1190,8 @@ function findWorkItemsTargetById(id, source, projects = PROJECTS) {
|
|
|
1187
1190
|
|
|
1188
1191
|
function buildPlanWorkItem(body, projects = PROJECTS, options = {}) {
|
|
1189
1192
|
if (!body?.title || !String(body.title).trim()) return { error: 'title is required' };
|
|
1193
|
+
const priorityError = shared.validateWorkItemPriority(body.priority);
|
|
1194
|
+
if (priorityError) return { error: priorityError.error, priorityError };
|
|
1190
1195
|
|
|
1191
1196
|
// Multi-project: body.project may be an array. Each entry must resolve to a
|
|
1192
1197
|
// known project; ≥2 valid entries trigger cross-repo mode (WI lands in central,
|
|
@@ -1225,7 +1230,7 @@ function buildPlanWorkItem(body, projects = PROJECTS, options = {}) {
|
|
|
1225
1230
|
id: options.id || ('W-' + shared.uid()),
|
|
1226
1231
|
title: body.title,
|
|
1227
1232
|
type: 'plan',
|
|
1228
|
-
priority: body.priority
|
|
1233
|
+
priority: body.priority ?? shared.WORK_ITEM_PRIORITY.HIGH,
|
|
1229
1234
|
description,
|
|
1230
1235
|
status: WI_STATUS.PENDING,
|
|
1231
1236
|
created: now.toISOString(),
|
|
@@ -1240,6 +1245,8 @@ function buildPlanWorkItem(body, projects = PROJECTS, options = {}) {
|
|
|
1240
1245
|
|
|
1241
1246
|
function buildManualPrdItemPlan(body, projects = PROJECTS, options = {}) {
|
|
1242
1247
|
if (!body?.name || !String(body.name).trim()) return { error: 'name is required' };
|
|
1248
|
+
const priorityError = shared.validateWorkItemPriority(body.priority);
|
|
1249
|
+
if (priorityError) return { error: priorityError.error, priorityError };
|
|
1243
1250
|
const target = resolveWorkItemsCreateTarget(body.project, projects);
|
|
1244
1251
|
if (target.error) return { error: target.error };
|
|
1245
1252
|
|
|
@@ -1258,7 +1265,7 @@ function buildManualPrdItemPlan(body, projects = PROJECTS, options = {}) {
|
|
|
1258
1265
|
id: body.id || ('M' + String(nowTime).slice(-4)),
|
|
1259
1266
|
name: String(body.name).trim(),
|
|
1260
1267
|
description: body.description || '',
|
|
1261
|
-
priority: body.priority
|
|
1268
|
+
priority: body.priority ?? shared.WORK_ITEM_PRIORITY.MEDIUM,
|
|
1262
1269
|
estimated_complexity: body.estimated_complexity || 'medium',
|
|
1263
1270
|
status: 'missing',
|
|
1264
1271
|
depends_on: [],
|
|
@@ -1325,6 +1332,24 @@ function validatePipelineProjects(pipeline, projects = PROJECTS) {
|
|
|
1325
1332
|
return null;
|
|
1326
1333
|
}
|
|
1327
1334
|
|
|
1335
|
+
function validatePipelineWorkItemPriorities(pipeline) {
|
|
1336
|
+
const visit = (value) => {
|
|
1337
|
+
if (!value || typeof value !== 'object') return null;
|
|
1338
|
+
if (Object.prototype.hasOwnProperty.call(value, 'priority')) {
|
|
1339
|
+
const error = shared.validateWorkItemPriority(value.priority);
|
|
1340
|
+
if (error) return error;
|
|
1341
|
+
}
|
|
1342
|
+
for (const key of ['stages', 'items']) {
|
|
1343
|
+
for (const child of Array.isArray(value[key]) ? value[key] : []) {
|
|
1344
|
+
const error = visit(child);
|
|
1345
|
+
if (error) return error;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return null;
|
|
1349
|
+
};
|
|
1350
|
+
return visit(pipeline);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1328
1353
|
/**
|
|
1329
1354
|
* Aggregate archived work items from central and project SQL scopes.
|
|
1330
1355
|
*
|
|
@@ -1817,11 +1842,12 @@ function buildDashboardHtml() {
|
|
|
1817
1842
|
} catch { /* registry empty or config unreadable — ship empty boot state */ }
|
|
1818
1843
|
|
|
1819
1844
|
const featuresBootstrap = `window.MINIONS_FEATURES = ${JSON.stringify(featuresBoot)};\n`;
|
|
1845
|
+
const workItemPrioritiesBootstrap = `window.MINIONS_WORK_ITEM_PRIORITIES = ${JSON.stringify(shared.WORK_ITEM_PRIORITY_VALUES)};\n`;
|
|
1820
1846
|
|
|
1821
1847
|
let assembled = layout
|
|
1822
1848
|
.replace('/* __CSS__ */', () => css)
|
|
1823
1849
|
.replace('<!-- __PAGES__ -->', () => pageHtml)
|
|
1824
|
-
.replace('/* __JS__ */', () => `window.__MINIONS_HOME = ${JSON.stringify(os.homedir())};\n${featuresBootstrap}${jsHtml}`)
|
|
1850
|
+
.replace('/* __JS__ */', () => `window.__MINIONS_HOME = ${JSON.stringify(os.homedir())};\n${featuresBootstrap}${workItemPrioritiesBootstrap}${jsHtml}`)
|
|
1825
1851
|
.replace(/\{\{favicon_emoji\}\}/g, FAVICON_EMOJI)
|
|
1826
1852
|
.replace(/\{\{title_suffix\}\}/g, TITLE_SUFFIX);
|
|
1827
1853
|
|
|
@@ -6629,6 +6655,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
6629
6655
|
try {
|
|
6630
6656
|
const body = await readBody(req);
|
|
6631
6657
|
if (!body.title || !body.title.trim()) return jsonReply(res, 400, { error: 'title is required' });
|
|
6658
|
+
const priorityError = shared.validateWorkItemPriority(body.priority);
|
|
6659
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
6632
6660
|
if (body.depends_on !== undefined) {
|
|
6633
6661
|
if (!Array.isArray(body.depends_on)) return jsonReply(res, 400, { error: 'depends_on must be an array of strings' });
|
|
6634
6662
|
if (!body.depends_on.every(s => typeof s === 'string')) return jsonReply(res, 400, { error: 'depends_on entries must be strings' });
|
|
@@ -6686,7 +6714,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
6686
6714
|
const id = 'W-' + shared.uid();
|
|
6687
6715
|
const item = {
|
|
6688
6716
|
id, title: body.title.trim(), type: normalizedType,
|
|
6689
|
-
priority: body.priority
|
|
6717
|
+
priority: body.priority ?? shared.WORK_ITEM_PRIORITY.MEDIUM, description: body.description || '',
|
|
6690
6718
|
status: WI_STATUS.PENDING, created: new Date().toISOString(), createdBy: 'dashboard',
|
|
6691
6719
|
};
|
|
6692
6720
|
if (Array.isArray(body.depends_on)) {
|
|
@@ -6835,6 +6863,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
6835
6863
|
const body = await readBody(req);
|
|
6836
6864
|
const { id, source, title, description, type, priority, agent } = body;
|
|
6837
6865
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
6866
|
+
const priorityError = shared.validateWorkItemPriority(priority);
|
|
6867
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
6838
6868
|
if (body.depends_on !== undefined) {
|
|
6839
6869
|
if (!Array.isArray(body.depends_on)) return jsonReply(res, 400, { error: 'depends_on must be an array of strings' });
|
|
6840
6870
|
if (!body.depends_on.every(s => typeof s === 'string')) return jsonReply(res, 400, { error: 'depends_on entries must be strings' });
|
|
@@ -6983,7 +7013,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
6983
7013
|
const body = await readBody(req);
|
|
6984
7014
|
if (!body.title || !body.title.trim()) return jsonReply(res, 400, { error: 'title is required' });
|
|
6985
7015
|
const planWorkItem = buildPlanWorkItem(body);
|
|
6986
|
-
if (planWorkItem.error) return jsonReply(res, 400, { error: planWorkItem.error });
|
|
7016
|
+
if (planWorkItem.error) return jsonReply(res, 400, planWorkItem.priorityError || { error: planWorkItem.error });
|
|
6987
7017
|
// Write as a work item with type 'plan' — user must explicitly execute plan-to-prd after reviewing
|
|
6988
7018
|
mutateWorkItems('central', items => { items.push(planWorkItem.item); });
|
|
6989
7019
|
recordCcTurnIfPresent(req, {
|
|
@@ -6998,7 +7028,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
6998
7028
|
const body = await readBody(req);
|
|
6999
7029
|
if (!body.name || !body.name.trim()) return jsonReply(res, 400, { error: 'name is required' });
|
|
7000
7030
|
const manualPrd = buildManualPrdItemPlan(body);
|
|
7001
|
-
if (manualPrd.error) return jsonReply(res, 400, { error: manualPrd.error });
|
|
7031
|
+
if (manualPrd.error) return jsonReply(res, 400, manualPrd.priorityError || { error: manualPrd.error });
|
|
7002
7032
|
|
|
7003
7033
|
const planFile = 'manual-' + shared.uid() + '.json';
|
|
7004
7034
|
prdStore.writePrd(planFile, manualPrd.plan);
|
|
@@ -7011,6 +7041,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
7011
7041
|
try {
|
|
7012
7042
|
const body = await readBody(req);
|
|
7013
7043
|
if (!body.source || !body.itemId) return jsonReply(res, 400, { error: 'source and itemId required' });
|
|
7044
|
+
const priorityError = shared.validateWorkItemPriority(body.priority);
|
|
7045
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
7014
7046
|
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
7047
|
const preCheck = prdStore.readPrd(body.source);
|
|
7016
7048
|
if (!preCheck) return jsonReply(res, 404, { error: 'plan not found' });
|
|
@@ -10671,6 +10703,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10671
10703
|
const body = await readBody(req);
|
|
10672
10704
|
let { id, cron, title, type, project, agent, description, priority, enabled } = body;
|
|
10673
10705
|
if (!cron || !title) return jsonReply(res, 400, { error: 'cron and title are required' });
|
|
10706
|
+
const priorityError = shared.validateWorkItemPriority(priority);
|
|
10707
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
10674
10708
|
reloadConfig();
|
|
10675
10709
|
const projectTarget = resolveScheduleProjectValue(project, PROJECTS);
|
|
10676
10710
|
if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
|
|
@@ -10709,6 +10743,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10709
10743
|
const body = await readBody(req);
|
|
10710
10744
|
let { id, cron, title, type, project, agent, description, priority, enabled } = body;
|
|
10711
10745
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
10746
|
+
const priorityError = shared.validateWorkItemPriority(priority);
|
|
10747
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
10712
10748
|
reloadConfig();
|
|
10713
10749
|
if (project !== undefined) {
|
|
10714
10750
|
const projectTarget = resolveScheduleProjectValue(project, PROJECTS);
|
|
@@ -10887,6 +10923,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10887
10923
|
const body = await readBody(req);
|
|
10888
10924
|
const { target, targetType, condition, interval, owner, description, project, notify, stopAfter, onNotMet, action, requires } = body;
|
|
10889
10925
|
if (!target) return jsonReply(res, 400, { error: 'target is required' });
|
|
10926
|
+
const priorityError = watchesMod.validateActionWorkItemPriority(action);
|
|
10927
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
10890
10928
|
try {
|
|
10891
10929
|
const watch = watchesMod.createWatch({ target, targetType, condition, interval, owner, description, project, notify, stopAfter, onNotMet, action, requires });
|
|
10892
10930
|
invalidateStatusCache();
|
|
@@ -10903,6 +10941,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10903
10941
|
const body = await readBody(req);
|
|
10904
10942
|
const { id, ...updates } = body;
|
|
10905
10943
|
if (!id) return jsonReply(res, 400, { error: 'id is required' });
|
|
10944
|
+
const priorityError = watchesMod.validateActionWorkItemPriority(updates.action);
|
|
10945
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
10906
10946
|
try {
|
|
10907
10947
|
const watch = watchesMod.updateWatch(id, updates);
|
|
10908
10948
|
if (!watch) return jsonReply(res, 404, { error: 'Watch not found' });
|
|
@@ -13207,6 +13247,33 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13207
13247
|
builder: () => getDispatchQueue(),
|
|
13208
13248
|
});
|
|
13209
13249
|
}},
|
|
13250
|
+
// GET /api/resolve-area — reverse "component name → subproject path" resolver
|
|
13251
|
+
// (WI W-mrpbsbs7000ce8ac). Given a configured project and a user token
|
|
13252
|
+
// ("OCM", "loop"), resolve it to real subproject path(s) via the filesystem
|
|
13253
|
+
// (engine/resolve-area.js reuses the bounded first-level area enumeration) so
|
|
13254
|
+
// CC threads a resolved PATH into scope instead of guessing from PR titles.
|
|
13255
|
+
{ method: 'GET', path: '/api/resolve-area', desc: 'Resolve a monorepo component/subproject NAME to real subproject path(s) for a configured project, via the filesystem (never PR titles). Query: project (configured project name), token (component name e.g. "OCM"). Returns {ok, project, token, area, paths, absPaths, matched:"exact"|"alias"|"none"}. matched:"none" means the caller should ask the user, not guess.', params: 'project (required), token (required)', handler: (req, res) => {
|
|
13256
|
+
const params = new URL(req.url, 'http://localhost').searchParams;
|
|
13257
|
+
const projectName = (params.get('project') || '').trim();
|
|
13258
|
+
const token = (params.get('token') || '').trim();
|
|
13259
|
+
if (!projectName) return jsonReply(res, 400, { error: 'project query param is required' }, req);
|
|
13260
|
+
if (!token) return jsonReply(res, 400, { error: 'token query param is required' }, req);
|
|
13261
|
+
const descriptor = shared.resolveProjectSource(projectName, PROJECTS, { allowCentral: false });
|
|
13262
|
+
const project = descriptor && descriptor.project;
|
|
13263
|
+
if (!project) return jsonReply(res, 404, { error: `unknown project "${projectName}"` }, req);
|
|
13264
|
+
if (!project.localPath) return jsonReply(res, 400, { error: `project "${project.name}" has no localPath (no checkout to resolve against)` }, req);
|
|
13265
|
+
const resolved = resolveArea.resolveAreaFromToken(project.localPath, token);
|
|
13266
|
+
const absPaths = resolved.paths.map(p => path.join(project.localPath, p).replace(/\\/g, '/'));
|
|
13267
|
+
return jsonReply(res, 200, {
|
|
13268
|
+
ok: true,
|
|
13269
|
+
project: project.name,
|
|
13270
|
+
token,
|
|
13271
|
+
area: resolved.area,
|
|
13272
|
+
paths: resolved.paths,
|
|
13273
|
+
absPaths,
|
|
13274
|
+
matched: resolved.matched,
|
|
13275
|
+
}, req);
|
|
13276
|
+
}},
|
|
13210
13277
|
{ method: 'GET', path: '/api/metrics', desc: 'Per-agent metrics with PR/runtime enrichment (joined against pull-requests + dispatch.completed)', handler: (req, res) => {
|
|
13211
13278
|
return serveFreshJson(req, res, {
|
|
13212
13279
|
tag: 'metrics',
|
|
@@ -13391,8 +13458,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13391
13458
|
{ 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
13459
|
|
|
13393
13460
|
// 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:
|
|
13461
|
+
{ 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 },
|
|
13462
|
+
{ 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
13463
|
{ method: 'POST', path: '/api/work-items/retry', desc: 'Reset a failed/dispatched item to pending', params: 'id, source?', handler: handleWorkItemsRetry },
|
|
13397
13464
|
{ method: 'POST', path: '/api/work-items/delete', desc: 'Remove a work item, kill agent, clear dispatch', params: 'id, source?', handler: handleWorkItemsDelete },
|
|
13398
13465
|
{ method: 'POST', path: '/api/work-items/cancel', desc: 'Cancel a work item, kill agent, clear dispatch', params: 'id, source?, reason?', handler: handleWorkItemsCancel },
|
|
@@ -13496,7 +13563,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13496
13563
|
{ method: 'POST', path: '/api/notes-save', desc: 'Save edited notes.md content', params: 'content, file?', handler: handleNotesSave },
|
|
13497
13564
|
|
|
13498
13565
|
// Plans
|
|
13499
|
-
{ method: 'POST', path: '/api/plan', desc: 'Create a plan work item that chains to PRD on completion', params:
|
|
13566
|
+
{ 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
13567
|
{ method: 'GET', path: '/api/plans', desc: 'List plan files (.md drafts + .json PRDs)', handler: handlePlansList },
|
|
13501
13568
|
{ method: 'POST', path: '/api/plans/trigger-verify', desc: 'Manually trigger verification for a completed plan', params: 'file', handler: handlePlansTriggerVerify },
|
|
13502
13569
|
{ method: 'POST', path: '/api/plans/approve', desc: 'Approve a plan for execution', params: 'file, approvedBy?', handler: handlePlansApprove },
|
|
@@ -13513,8 +13580,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13513
13580
|
{ method: 'GET', path: /^\/api\/plans\/([^?]+)$/, template: '/api/plans/:file', desc: 'Read a full plan (JSON from prd/ or markdown from plans/)', handler: handlePlansRead },
|
|
13514
13581
|
|
|
13515
13582
|
// 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:
|
|
13583
|
+
{ 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 },
|
|
13584
|
+
{ 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
13585
|
{ method: 'POST', path: '/api/prd-items/remove', desc: 'Remove a PRD item from plan + cancel materialized work item', params: 'source, itemId', handler: handlePrdItemsRemove },
|
|
13519
13586
|
// /api/prd/regenerate removed — use /api/plans/approve which does diff-aware update
|
|
13520
13587
|
|
|
@@ -14323,8 +14390,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
14323
14390
|
|
|
14324
14391
|
// Schedules
|
|
14325
14392
|
{ 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:
|
|
14393
|
+
{ 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 },
|
|
14394
|
+
{ 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
14395
|
{ method: 'POST', path: '/api/schedules/delete', desc: 'Delete a schedule', params: 'id', handler: handleSchedulesDelete },
|
|
14329
14396
|
{ method: 'POST', path: '/api/schedules/run-now', desc: 'Manually enqueue the work item for a schedule', params: 'id', handler: handleSchedulesRunNow },
|
|
14330
14397
|
|
|
@@ -14345,9 +14412,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
14345
14412
|
const result = pipelines.map(p => ({ ...p, runs: (runs[p.id] || []).slice(-5) }));
|
|
14346
14413
|
return jsonReply(res, 200, result);
|
|
14347
14414
|
}},
|
|
14348
|
-
{ method: 'POST', path: '/api/pipelines', desc: 'Create a pipeline', params:
|
|
14415
|
+
{ method: 'POST', path: '/api/pipelines', desc: 'Create a pipeline', params: `id, title, ${PIPELINE_STAGES_PARAM_HINT}, trigger?, stopWhen?, monitoredResources?`, handler: async (req, res) => {
|
|
14349
14416
|
const body = await readBody(req);
|
|
14350
14417
|
if (!body.id || !body.title || !body.stages) return jsonReply(res, 400, { error: 'id, title, and stages required' });
|
|
14418
|
+
const priorityError = validatePipelineWorkItemPriorities({ stages: body.stages });
|
|
14419
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
14351
14420
|
const { savePipeline, getPipeline } = require('./engine/pipeline');
|
|
14352
14421
|
if (getPipeline(body.id)) return jsonReply(res, 409, { error: 'Pipeline already exists' });
|
|
14353
14422
|
const pipeline = { id: body.id, title: body.title, stages: body.stages, trigger: body.trigger || {}, enabled: body.enabled !== false };
|
|
@@ -14361,12 +14430,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
14361
14430
|
invalidateStatusCache();
|
|
14362
14431
|
return jsonReply(res, 200, { ok: true, id: pipeline.id });
|
|
14363
14432
|
}},
|
|
14364
|
-
{ method: 'POST', path: '/api/pipelines/update', desc: 'Update a pipeline', params:
|
|
14433
|
+
{ 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
14434
|
const body = await readBody(req);
|
|
14366
14435
|
if (!body.id) return jsonReply(res, 400, { error: 'id required' });
|
|
14367
14436
|
const { getPipeline, savePipeline } = require('./engine/pipeline');
|
|
14368
14437
|
const pipeline = getPipeline(body.id);
|
|
14369
14438
|
if (!pipeline) return jsonReply(res, 404, { error: 'Pipeline not found' });
|
|
14439
|
+
const priorityError = validatePipelineWorkItemPriorities({ stages: body.stages });
|
|
14440
|
+
if (priorityError) return jsonReply(res, 400, priorityError);
|
|
14370
14441
|
if (body.title !== undefined) pipeline.title = body.title;
|
|
14371
14442
|
if (body.stages !== undefined) pipeline.stages = body.stages;
|
|
14372
14443
|
if (body.trigger !== undefined) pipeline.trigger = body.trigger;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/resolve-area.js — Reverse "component name → subproject path" resolver.
|
|
3
|
+
*
|
|
4
|
+
* WI W-mrpbsbs7000ce8ac. Command Center (and other callers) frequently receive
|
|
5
|
+
* a monorepo component name in a natural-language request ("audit OCM changes",
|
|
6
|
+
* "Loop Android"). Without a way to turn that name into a real filesystem path,
|
|
7
|
+
* CC used to guess the subproject's location from PR titles — a documented
|
|
8
|
+
* incident where CC repeatedly asked the operator where OCM lived even though
|
|
9
|
+
* `<repo>/ocm/` exists on disk.
|
|
10
|
+
*
|
|
11
|
+
* This module is the reverse of the (now-removed) dominant-subproject detector:
|
|
12
|
+
* that mapped `changed paths → area`; this maps `user token → area path(s)`.
|
|
13
|
+
*
|
|
14
|
+
* NOTE ON REUSE: the original first-level area enumeration lived in
|
|
15
|
+
* `engine/discover-project-skills.js` as `_listAreas`. That entire module (and
|
|
16
|
+
* its area/skill/harness heuristics) was deliberately removed when repository
|
|
17
|
+
* harness discovery was delegated to the native runtimes (PR #817,
|
|
18
|
+
* "Delegate repository harness discovery to native runtimes"). No `_listAreas`
|
|
19
|
+
* remains to import, so this file carries the single, minimal, bounded
|
|
20
|
+
* first-level enumeration — it is NOT a parallel copy of a live helper.
|
|
21
|
+
*
|
|
22
|
+
* Design constraints (match the removed helper's conventions):
|
|
23
|
+
* - Bounded / deadline-guarded filesystem work (no unbounded recursion).
|
|
24
|
+
* - Deterministic, sorted output across OSes.
|
|
25
|
+
* - Cross-platform: paths are emitted as POSIX forward-slash, dir-suffixed.
|
|
26
|
+
* - Zero runtime deps — Node built-ins only.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
|
|
34
|
+
const DEFAULTS = {
|
|
35
|
+
// Cap on first-level directories enumerated (monorepo roots are wide but
|
|
36
|
+
// still O(dozens), so this is a generous ceiling that bounds a pathological
|
|
37
|
+
// directory).
|
|
38
|
+
maxAreas: 400,
|
|
39
|
+
// Wall-clock budget for the whole resolve (enumeration + alias existence
|
|
40
|
+
// probes). Bounded like the removed helper so a slow/again-networked FS can
|
|
41
|
+
// never hang a caller.
|
|
42
|
+
walltimeMs: 750,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Documented alias map for known multi-path components — a single user token
|
|
47
|
+
* that resolves to several real subproject paths (a primary top-level area PLUS
|
|
48
|
+
* wrapper / native / telemetry paths scattered elsewhere in the tree).
|
|
49
|
+
*
|
|
50
|
+
* Keys are lowercase tokens. `primary` is the first-level area used for the
|
|
51
|
+
* returned `area`; `paths` is the full candidate set (repo-relative, POSIX,
|
|
52
|
+
* trailing slash). At resolve time each candidate is verified against the
|
|
53
|
+
* on-disk tree so a token used against a repo that is NOT the monorepo cleanly
|
|
54
|
+
* returns no match instead of inventing paths.
|
|
55
|
+
*/
|
|
56
|
+
const AREA_ALIASES = {
|
|
57
|
+
// OCM (Office Copilot Mobile): the top-level `ocm/` area plus its wrapper /
|
|
58
|
+
// native / telemetry paths under officemobile/.
|
|
59
|
+
ocm: {
|
|
60
|
+
primary: 'ocm',
|
|
61
|
+
paths: [
|
|
62
|
+
'ocm/',
|
|
63
|
+
'officemobile/ocmnative/',
|
|
64
|
+
'officemobile/android/ocmciq/',
|
|
65
|
+
'officemobile/android/JavaKotlin/ocmciq/',
|
|
66
|
+
'officemobile/android/hubframework/ocmsdk-wrapper/',
|
|
67
|
+
'officemobile/android/JavaKotlin/hubframework/ocmsdk-wrapper/',
|
|
68
|
+
'officemobile/ios/OCMWrapper/',
|
|
69
|
+
'officemobile/telemetry/android/ocm/',
|
|
70
|
+
'officemobile/telemetry/tml/ocm/',
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function _now() { return Date.now(); }
|
|
76
|
+
|
|
77
|
+
/** Normalize a repo-relative path to POSIX forward-slash with a trailing slash. */
|
|
78
|
+
function _toPosixDir(p) {
|
|
79
|
+
let s = String(p).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
80
|
+
return s.length ? `${s}/` : s;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Bounded existence check: true iff `rel` resolves to a directory under `projectPath`. */
|
|
84
|
+
function _isDir(projectPath, rel) {
|
|
85
|
+
try {
|
|
86
|
+
const abs = path.join(projectPath, rel);
|
|
87
|
+
return fs.statSync(abs).isDirectory();
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Enumerate first-level subproject directories of `projectPath`.
|
|
95
|
+
*
|
|
96
|
+
* Deterministic (locale-sorted), bounded by `opts.maxAreas` and `deadline`,
|
|
97
|
+
* dot-dir and `node_modules` filtered. Returns bare directory names (no slash).
|
|
98
|
+
*
|
|
99
|
+
* @param {string} projectPath — absolute path to the project checkout / worktree
|
|
100
|
+
* @param {object} [opts] — { maxAreas, walltimeMs } overrides
|
|
101
|
+
* @param {number} [deadline] — absolute ms timestamp; enumeration stops past it
|
|
102
|
+
* @returns {string[]} sorted first-level directory names
|
|
103
|
+
*/
|
|
104
|
+
function listAreas(projectPath, opts, deadline) {
|
|
105
|
+
const o = Object.assign({}, DEFAULTS, opts || {});
|
|
106
|
+
const stopAt = typeof deadline === 'number' ? deadline : _now() + Math.max(1, o.walltimeMs);
|
|
107
|
+
const out = [];
|
|
108
|
+
if (!projectPath || typeof projectPath !== 'string') return out;
|
|
109
|
+
let entries;
|
|
110
|
+
try {
|
|
111
|
+
entries = fs.readdirSync(projectPath, { withFileTypes: true });
|
|
112
|
+
} catch {
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
// Sort for deterministic ordering across OSes.
|
|
116
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
117
|
+
for (const ent of entries) {
|
|
118
|
+
if (out.length >= o.maxAreas) break;
|
|
119
|
+
if (_now() > stopAt) break;
|
|
120
|
+
if (!ent.isDirectory()) continue;
|
|
121
|
+
// Skip dot-dirs (.git, .github, .claude, …) and node_modules.
|
|
122
|
+
if (ent.name.startsWith('.')) continue;
|
|
123
|
+
if (ent.name === 'node_modules') continue;
|
|
124
|
+
out.push(ent.name);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Reverse resolver: map a user-supplied component/subproject token to the real
|
|
131
|
+
* subproject path(s) inside `projectPath`.
|
|
132
|
+
*
|
|
133
|
+
* Resolution order:
|
|
134
|
+
* 1. Alias expansion — if the token is a known multi-path component (see
|
|
135
|
+
* AREA_ALIASES), return every candidate path that actually exists on disk,
|
|
136
|
+
* with `area` set to the primary first-level area. `matched: 'alias'`.
|
|
137
|
+
* 2. Exact match — case-insensitive match of the token against the first-level
|
|
138
|
+
* areas enumerated by `listAreas`. Returns the single `<area>/` path.
|
|
139
|
+
* `matched: 'exact'`.
|
|
140
|
+
* 3. No match — `{ area: null, paths: [], matched: 'none' }`. Callers should
|
|
141
|
+
* then ask the user instead of guessing.
|
|
142
|
+
*
|
|
143
|
+
* All work is bounded by `opts.walltimeMs`. Output paths are deterministic
|
|
144
|
+
* (sorted, de-duped), POSIX forward-slash, trailing-slash directories.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} projectPath — absolute path to the project checkout / worktree
|
|
147
|
+
* @param {string} token — user-supplied component name (e.g. "OCM", "loop")
|
|
148
|
+
* @param {object} [opts] — { maxAreas, walltimeMs } overrides
|
|
149
|
+
* @returns {{area: string|null, paths: string[], matched: 'exact'|'alias'|'none'}}
|
|
150
|
+
*/
|
|
151
|
+
function resolveAreaFromToken(projectPath, token, opts) {
|
|
152
|
+
const result = { area: null, paths: [], matched: 'none' };
|
|
153
|
+
if (!projectPath || typeof projectPath !== 'string') return result;
|
|
154
|
+
if (typeof token !== 'string') return result;
|
|
155
|
+
const t = token.trim().toLowerCase();
|
|
156
|
+
if (!t) return result;
|
|
157
|
+
|
|
158
|
+
const o = Object.assign({}, DEFAULTS, opts || {});
|
|
159
|
+
const deadline = _now() + Math.max(1, o.walltimeMs);
|
|
160
|
+
|
|
161
|
+
const areas = listAreas(projectPath, o, deadline);
|
|
162
|
+
const areasLower = new Map(areas.map(a => [a.toLowerCase(), a]));
|
|
163
|
+
|
|
164
|
+
// 1. Alias expansion (multi-path components like OCM).
|
|
165
|
+
const alias = Object.prototype.hasOwnProperty.call(AREA_ALIASES, t)
|
|
166
|
+
? AREA_ALIASES[t]
|
|
167
|
+
: null;
|
|
168
|
+
if (alias) {
|
|
169
|
+
const verified = [];
|
|
170
|
+
for (const rel of alias.paths) {
|
|
171
|
+
if (_now() > deadline) break;
|
|
172
|
+
if (_isDir(projectPath, rel)) verified.push(_toPosixDir(rel));
|
|
173
|
+
}
|
|
174
|
+
if (verified.length > 0) {
|
|
175
|
+
const primaryDir = areasLower.get(alias.primary.toLowerCase()) || alias.primary;
|
|
176
|
+
return {
|
|
177
|
+
area: primaryDir,
|
|
178
|
+
paths: Array.from(new Set(verified)).sort(),
|
|
179
|
+
matched: 'alias',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// Alias known but none of its paths exist here (token used against a repo
|
|
183
|
+
// that is not the monorepo). Fall through to exact / none.
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 2. Exact case-insensitive match against enumerated first-level areas.
|
|
187
|
+
const exact = areasLower.get(t);
|
|
188
|
+
if (exact) {
|
|
189
|
+
return { area: exact, paths: [_toPosixDir(`${exact}/`)], matched: 'exact' };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
DEFAULTS,
|
|
197
|
+
AREA_ALIASES,
|
|
198
|
+
listAreas,
|
|
199
|
+
resolveAreaFromToken,
|
|
200
|
+
};
|
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.2399",
|
|
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"
|
package/prompts/cc-system.md
CHANGED
|
@@ -204,6 +204,25 @@ curl -s http://localhost:{{dashboard_port}}/api/work-items
|
|
|
204
204
|
curl -s http://localhost:{{dashboard_port}}/api/status
|
|
205
205
|
```
|
|
206
206
|
|
|
207
|
+
## Resolving a monorepo component/subproject by name — GET /api/resolve-area
|
|
208
|
+
|
|
209
|
+
Monorepo projects (e.g. the Office `src` repo) contain many **subprojects** under first-level directories (`ocm/`, `loop/`, `officemobile/`, …). When a request names a component or subproject — "audit OCM changes", "Loop Android", "the LMF area" — that name refers to a **path inside the repo**, not to a PR title.
|
|
210
|
+
|
|
211
|
+
**Rule: resolve the area from the filesystem — NEVER infer a subproject's location from PR titles.** Scanning PR titles/branches to guess where a component lives is wrong (it caused a real incident: CC kept asking the operator where OCM was even though `<repo>/ocm/` exists on disk). Resolve the name to a real path first, then scope your work (`git log -- <path>`, `Glob`, dispatched WI description) to that path.
|
|
212
|
+
|
|
213
|
+
Resolve via `GET /api/resolve-area?project=<name>&token=<component>`:
|
|
214
|
+
```
|
|
215
|
+
curl -s 'http://localhost:{{dashboard_port}}/api/resolve-area?project=src&token=OCM'
|
|
216
|
+
```
|
|
217
|
+
Returns `{ok, project, token, area, paths, absPaths, matched}` where `matched` is:
|
|
218
|
+
- `"exact"` — the token is a first-level area (`paths: ["<area>/"]`).
|
|
219
|
+
- `"alias"` — a known multi-path component; `paths` lists every real subpath that exists on disk.
|
|
220
|
+
- `"none"` — no area matched. **Ask the user where the component lives — do not guess from PR titles.**
|
|
221
|
+
|
|
222
|
+
**Worked example (OCM → `ocm/` + wrapper/native paths).** `token=OCM` resolves to the top-level `ocm/` area PLUS its wrapper/native/telemetry paths (`officemobile/ocmnative/`, `officemobile/android/ocmciq/`, `officemobile/ios/OCMWrapper/`, `officemobile/telemetry/android/ocm/`, …) — only the paths that actually exist in the checkout are returned. Scope OCM audits/fixes to that full path set, not to PRs whose title happens to say "OCM".
|
|
223
|
+
|
|
224
|
+
If the endpoint returns `matched:"none"`, you may also enumerate first-level areas yourself with your `Glob`/`Bash` tools against the project's `localPath` (the reverse resolver just reuses that same first-level directory listing) — but still never fall back to PR titles.
|
|
225
|
+
|
|
207
226
|
## Read-only PR actions on ANY PR — POST /api/pr-action
|
|
208
227
|
|
|
209
228
|
When the user asks you to **review / summarize / comment-on / triage a specific PR by URL or id** — including PRs in repos that are NOT a configured Minions project (e.g. "review this PR https://github.com/some/repo/pull/42", "summarize ado:org/proj/repo#5215493", "triage github:yemi33/minions#2702") — route it through `POST /api/pr-action` instead of dispatching a `review`/`explore` work item. This path is **projectless and read-only**: it resolves the PR, fetches its diff + metadata with NO clone/worktree, and reasons over the (untrusted-fenced) content. Use it for one-off, look-don't-touch asks on an arbitrary PR.
|