@yemi33/minions 0.1.2346 → 0.1.2348
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 +67 -9
- package/docs/command-center.md +3 -1
- package/engine/metrics-store.js +66 -0
- package/package.json +1 -1
package/dashboard.js
CHANGED
|
@@ -6973,12 +6973,35 @@ const server = http.createServer(async (req, res) => {
|
|
|
6973
6973
|
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
6974
6974
|
const wiPath = target.wiPath;
|
|
6975
6975
|
|
|
6976
|
+
// W-mrazefzz000358e3 — body.project reassigns which projects/<name>/
|
|
6977
|
+
// work-items.json (or central) file owns this item. Resolve + validate
|
|
6978
|
+
// it up front (mirrors resolveWorkItemsCreateTarget's create-path
|
|
6979
|
+
// contract) so an unknown project name 400s cleanly instead of the
|
|
6980
|
+
// historical silent no-op. `projectMove` stays null when body.project
|
|
6981
|
+
// is omitted (no location change requested).
|
|
6982
|
+
let projectMove = null;
|
|
6983
|
+
if (body.project !== undefined) {
|
|
6984
|
+
const projTarget = resolveProjectSourceTarget(body.project, PROJECTS);
|
|
6985
|
+
if (projTarget.error) {
|
|
6986
|
+
return jsonReply(res, 400, {
|
|
6987
|
+
error: projTarget.error,
|
|
6988
|
+
knownProjects: PROJECTS.map(p => p.name),
|
|
6989
|
+
});
|
|
6990
|
+
}
|
|
6991
|
+
projectMove = { wiPath: projTarget.wiPath, projectName: projTarget.project ? projTarget.project.name : null };
|
|
6992
|
+
}
|
|
6993
|
+
|
|
6976
6994
|
let result = null;
|
|
6977
6995
|
let agentChanged = false;
|
|
6996
|
+
// Set inside the source-file lock below when a project move is needed;
|
|
6997
|
+
// inserted into the target file's own lock scope afterward so we never
|
|
6998
|
+
// hold both files' locks at once.
|
|
6999
|
+
let movedItem = null;
|
|
6978
7000
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
6979
7001
|
if (!Array.isArray(items)) items = [];
|
|
6980
|
-
const
|
|
6981
|
-
if (
|
|
7002
|
+
const idx = items.findIndex(i => i.id === id);
|
|
7003
|
+
if (idx === -1) { result = { code: 404, body: { error: 'item not found' } }; return items; }
|
|
7004
|
+
const item = items[idx];
|
|
6982
7005
|
if (item.status === WI_STATUS.DISPATCHED) { result = { code: 400, body: { error: 'Cannot edit dispatched items' } }; return items; }
|
|
6983
7006
|
|
|
6984
7007
|
if (title !== undefined) item.title = title;
|
|
@@ -7010,10 +7033,32 @@ const server = http.createServer(async (req, res) => {
|
|
|
7010
7033
|
}
|
|
7011
7034
|
}
|
|
7012
7035
|
item.updatedAt = new Date().toISOString();
|
|
7036
|
+
|
|
7037
|
+
// W-mrazefzz000358e3 — actually MOVE the item between work-items.json
|
|
7038
|
+
// files when the resolved target differs from where it lives today,
|
|
7039
|
+
// rather than just stamping item.project in place. Only trip this
|
|
7040
|
+
// when the resolved wiPath actually changes — re-assigning the same
|
|
7041
|
+
// project the item already lives in is a no-op move.
|
|
7042
|
+
if (projectMove && path.resolve(projectMove.wiPath) !== path.resolve(wiPath)) {
|
|
7043
|
+
if (projectMove.projectName) item.project = projectMove.projectName;
|
|
7044
|
+
else delete item.project;
|
|
7045
|
+
movedItem = item;
|
|
7046
|
+
items.splice(idx, 1);
|
|
7047
|
+
}
|
|
7048
|
+
|
|
7013
7049
|
result = { code: 200, body: { ok: true, item } };
|
|
7014
7050
|
return items;
|
|
7015
7051
|
});
|
|
7016
7052
|
if (!result) return jsonReply(res, 500, { error: 'unexpected state' });
|
|
7053
|
+
|
|
7054
|
+
if (movedItem) {
|
|
7055
|
+
mutateJsonFileLocked(projectMove.wiPath, (targetItems) => {
|
|
7056
|
+
if (!Array.isArray(targetItems)) targetItems = [];
|
|
7057
|
+
targetItems.push(movedItem);
|
|
7058
|
+
return targetItems;
|
|
7059
|
+
});
|
|
7060
|
+
}
|
|
7061
|
+
|
|
7017
7062
|
// Clear stale pending dispatch entries outside lock
|
|
7018
7063
|
if (agentChanged) cleanDispatchEntries(d => d.meta?.item?.id === id);
|
|
7019
7064
|
return jsonReply(res, result.code, result.body);
|
|
@@ -9800,8 +9845,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9800
9845
|
const body = await readBody(req);
|
|
9801
9846
|
const message = body && typeof body.message === 'string' ? body.message : '';
|
|
9802
9847
|
if (!message.trim()) return jsonReply(res, 400, { error: 'message required' });
|
|
9803
|
-
|
|
9804
|
-
|
|
9848
|
+
// W-mrb1a3950003d9ea — flat, stable label (no per-watch-id interpolation).
|
|
9849
|
+
// Every other _engine category (kb-sweep, consolidation, command-center, …)
|
|
9850
|
+
// uses one flat label; a dynamic watchId suffix here used to mint a brand-new
|
|
9851
|
+
// permanent metrics.json._engine key per watch firing (20+ confirmed live).
|
|
9852
|
+
const label = 'watch-cc-triage';
|
|
9805
9853
|
|
|
9806
9854
|
const model = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
|
|
9807
9855
|
const maxTurns = CONFIG.engine?.ccMaxTurns || shared.ENGINE_DEFAULTS.ccMaxTurns;
|
|
@@ -9886,10 +9934,20 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9886
9934
|
* test enforces engine.js does not import cc-worker-pool).
|
|
9887
9935
|
*/
|
|
9888
9936
|
function _invokeCcStream({ prompt, sessionId, liveState, toolUses, model, effort, maxTurns, engineConfig, systemPrompt = CC_STATIC_SYSTEM_PROMPT, tabId, images }) {
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
|
|
9892
|
-
|
|
9937
|
+
// W-mray463s001zcee2 — image-bearing turns bypass the worker pool.
|
|
9938
|
+
// Copilot itself supports images fine (imageInput: true, `--attachment
|
|
9939
|
+
// <path>` in engine/runtimes/copilot.js buildArgs), but the ACP worker-pool
|
|
9940
|
+
// protocol (engine/cc-worker-pool.js session/prompt) only ever sends
|
|
9941
|
+
// `[{ type: 'text', text: prompt }]` — it has no image/attachment
|
|
9942
|
+
// content-block support today. That is a worker-pool PROTOCOL gap, not a
|
|
9943
|
+
// Copilot capability gap (the old comment here claiming "copilot has
|
|
9944
|
+
// imageInput:false" was factually wrong). Until the pool's ACP session
|
|
9945
|
+
// protocol is extended to carry attachments, route image turns around the
|
|
9946
|
+
// pool for THIS TURN ONLY: cold-spawn a one-off CLI process via the
|
|
9947
|
+
// already-working --attachment path below, while image-less turns keep
|
|
9948
|
+
// using the fast warm pool.
|
|
9949
|
+
const hasImages = Array.isArray(images) && images.length > 0;
|
|
9950
|
+
if (!hasImages && shared.resolveCcUseWorkerPool(engineConfig)) {
|
|
9893
9951
|
return _invokeCcStreamViaPool({ prompt, liveState, toolUses, model, effort, engineConfig, systemPrompt, tabId });
|
|
9894
9952
|
}
|
|
9895
9953
|
const { callLLMStreaming } = require('./engine/llm');
|
|
@@ -13514,7 +13572,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13514
13572
|
|
|
13515
13573
|
// Work items
|
|
13516
13574
|
{ method: 'POST', path: '/api/work-items', desc: 'Create a new work item', params: 'title, type?, description?, priority?, project?, agent?, agents?, scope?, references?, acceptanceCriteria?, skipPr?, oneShot?, meta?, meta.pr_followup?, meta.workdir?, X-Minions-Agent?, X-Minions-Origin-Wi?', handler: handleWorkItemsCreate },
|
|
13517
|
-
{ method: 'POST', path: '/api/work-items/update', desc: 'Edit a pending/failed work item', params: 'id, source?, title?, description?, type?, priority?, agent?, references?, acceptanceCriteria?, depends_on?, workdir?, meta?', handler: handleWorkItemsUpdate },
|
|
13575
|
+
{ method: 'POST', path: '/api/work-items/update', desc: 'Edit a pending/failed work item', params: 'id, source?, title?, description?, type?, priority?, agent?, project?, references?, acceptanceCriteria?, depends_on?, workdir?, meta?', handler: handleWorkItemsUpdate },
|
|
13518
13576
|
{ method: 'POST', path: '/api/work-items/retry', desc: 'Reset a failed/dispatched item to pending', params: 'id, source?', handler: handleWorkItemsRetry },
|
|
13519
13577
|
{ method: 'POST', path: '/api/work-items/delete', desc: 'Remove a work item, kill agent, clear dispatch', params: 'id, source?', handler: handleWorkItemsDelete },
|
|
13520
13578
|
{ method: 'POST', path: '/api/work-items/cancel', desc: 'Cancel a work item, kill agent, clear dispatch', params: 'id, source?, reason?', handler: handleWorkItemsCancel },
|
package/docs/command-center.md
CHANGED
|
@@ -65,9 +65,11 @@ Any violation rejects the **whole turn** with a typed error envelope (`code: 'in
|
|
|
65
65
|
| Runtime | `imageInput` | Behavior |
|
|
66
66
|
|---------|--------------|----------|
|
|
67
67
|
| claude | `true` | Images delivered as an Anthropic Messages `content[]` envelope of base64 blocks over stream-json (`claude.buildPrompt` + `--input-format stream-json` in `claude.buildArgs`). |
|
|
68
|
-
| copilot | `true` | Images materialized to tmp files and passed as `--attachment <path>` (W-mqv7324u0021db5d).
|
|
68
|
+
| copilot | `true` | Images materialized to tmp files and passed as `--attachment <path>` (W-mqv7324u0021db5d). |
|
|
69
69
|
| codex | `false` | Degraded — same typed `model-unavailable` envelope. |
|
|
70
70
|
|
|
71
|
+
**Worker-pool interaction (W-mray463s001zcee2).** The opt-in Copilot ACP worker pool (`engine.ccUseWorkerPool: true`, default ON for Copilot CC) is a **separate protocol path** from the table above — `engine/cc-worker-pool.js`'s `session/prompt` call only ever sends a single text content block (`[{ type: 'text', text: prompt }]`) and has no image/attachment content-block support today. This used to mean image turns were silently dropped whenever the pool was active, with a comment incorrectly blaming a Copilot capability gap (`imageInput:false`) that doesn't exist — Copilot's `imageInput` is `true` and fully supports images on the direct spawn path. The fix: `_invokeCcStream` (dashboard.js) now checks for a non-empty `images` array **before** routing through `resolveCcUseWorkerPool` — image-bearing turns bypass the warm pool for that turn only and cold-spawn a one-off CLI process via the working `--attachment` path (same as the non-pooled path always used), while image-less turns keep using the fast warm pool. If `engine/cc-worker-pool.js`'s ACP session protocol is later extended to carry attachments (Option A), this per-turn bypass in `_invokeCcStream` should be revisited/removed.
|
|
72
|
+
|
|
71
73
|
`_resolveImageOpts` (in `engine/llm.js`) is pure and unit-tested: it forwards the `images` list only when `runtime.capabilities.imageInput` is truthy, otherwise returns the typed error so CC/doc-chat surface the envelope instead of a silently-text-only reply. `_spawnProcess` re-applies the same gate (`if (!caps.imageInput) adapterOpts.images = undefined`) as defense in depth. Image **filenames** are run through the untrusted-input fence (`buildSource('cc-image-filename', …)`) before they reach prompt text, since they are user-supplied.
|
|
72
74
|
|
|
73
75
|
**No new `engine.*` flag.** The limits above are module-level constants in `dashboard.js` (`CC_IMAGE_MAX_COUNT`, `CC_IMAGE_MAX_DECODED_BYTES`, `CC_IMAGE_MIME_ALLOWLIST`), not config keys; `engine/shared.js` `ENGINE_DEFAULTS` was not touched. Per CLAUDE.md Best Practice #9 (Settings parity), no Settings toggle is added because no new `engine.*` flag was introduced. Runtime selection that determines whether images are accepted is the existing `engine.ccCli` / `ccModel` override, which already has a Settings control.
|
package/engine/metrics-store.js
CHANGED
|
@@ -172,6 +172,8 @@ function readMetrics() {
|
|
|
172
172
|
try { db = getDb(); }
|
|
173
173
|
catch { return _readJsonObjectFallback(); }
|
|
174
174
|
|
|
175
|
+
migrateWatchCcTriageKeysOnce();
|
|
176
|
+
|
|
175
177
|
_resyncIfJsonDiverged(db);
|
|
176
178
|
|
|
177
179
|
const rows = _readAllRows(db);
|
|
@@ -285,9 +287,73 @@ function _resetMetricsTableForTest() {
|
|
|
285
287
|
_lastMirrorHash = null;
|
|
286
288
|
}
|
|
287
289
|
|
|
290
|
+
// W-mrb1a3950003d9ea — one-time collapse of legacy `watch-cc-triage:<watchId>`
|
|
291
|
+
// per-watch _engine keys (minted before dashboard.js's handleCommandCenterTriage
|
|
292
|
+
// stopped interpolating watchId into the trackEngineUsage label) into a single
|
|
293
|
+
// flat `watch-cc-triage` bucket, matching every other _engine category. Sums
|
|
294
|
+
// calls/cost/tokens/duration/errorsByCode across all matching keys, then deletes
|
|
295
|
+
// the per-id keys. Idempotent — a no-op (no SQL/JSON write) once no key matches
|
|
296
|
+
// /^watch-cc-triage:/, so it's safe to call unconditionally on every process
|
|
297
|
+
// startup via migrateWatchCcTriageKeysOnce().
|
|
298
|
+
const WATCH_CC_TRIAGE_LEGACY_KEY_RE = /^watch-cc-triage:/;
|
|
299
|
+
const WATCH_CC_TRIAGE_FLAT_KEY = 'watch-cc-triage';
|
|
300
|
+
|
|
301
|
+
function migrateWatchCcTriageKeys() {
|
|
302
|
+
return applyMetricsMutation((metrics) => {
|
|
303
|
+
const engineMetrics = metrics._engine;
|
|
304
|
+
if (!engineMetrics || typeof engineMetrics !== 'object') return metrics;
|
|
305
|
+
const staleKeys = Object.keys(engineMetrics).filter((k) => WATCH_CC_TRIAGE_LEGACY_KEY_RE.test(k));
|
|
306
|
+
if (staleKeys.length === 0) return metrics;
|
|
307
|
+
|
|
308
|
+
if (!engineMetrics[WATCH_CC_TRIAGE_FLAT_KEY]) {
|
|
309
|
+
engineMetrics[WATCH_CC_TRIAGE_FLAT_KEY] = { calls: 0, costUsd: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheCreation: 0 };
|
|
310
|
+
}
|
|
311
|
+
const flat = engineMetrics[WATCH_CC_TRIAGE_FLAT_KEY];
|
|
312
|
+
for (const key of staleKeys) {
|
|
313
|
+
const cat = engineMetrics[key] || {};
|
|
314
|
+
flat.calls = (flat.calls || 0) + (cat.calls || 0);
|
|
315
|
+
flat.costUsd = (flat.costUsd || 0) + (cat.costUsd || 0);
|
|
316
|
+
flat.inputTokens = (flat.inputTokens || 0) + (cat.inputTokens || 0);
|
|
317
|
+
flat.outputTokens = (flat.outputTokens || 0) + (cat.outputTokens || 0);
|
|
318
|
+
flat.cacheRead = (flat.cacheRead || 0) + (cat.cacheRead || 0);
|
|
319
|
+
flat.cacheCreation = (flat.cacheCreation || 0) + (cat.cacheCreation || 0);
|
|
320
|
+
if (cat.totalDurationMs) flat.totalDurationMs = (flat.totalDurationMs || 0) + cat.totalDurationMs;
|
|
321
|
+
if (cat.timedCalls) flat.timedCalls = (flat.timedCalls || 0) + cat.timedCalls;
|
|
322
|
+
if (cat.errorsByCode) {
|
|
323
|
+
if (!flat.errorsByCode) flat.errorsByCode = {};
|
|
324
|
+
for (const [code, count] of Object.entries(cat.errorsByCode)) {
|
|
325
|
+
flat.errorsByCode[code] = (flat.errorsByCode[code] || 0) + count;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
delete engineMetrics[key];
|
|
329
|
+
}
|
|
330
|
+
return metrics;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let _watchCcTriageMigratedThisProcess = false;
|
|
335
|
+
|
|
336
|
+
// Startup-safe wrapper: runs migrateWatchCcTriageKeys() at most once per
|
|
337
|
+
// process (subsequent calls, e.g. on every readMetrics(), are a cheap
|
|
338
|
+
// no-op flag check). Failures (e.g. SQLite unavailable) are swallowed and
|
|
339
|
+
// retried on the next process start rather than surfaced to callers.
|
|
340
|
+
function migrateWatchCcTriageKeysOnce() {
|
|
341
|
+
if (_watchCcTriageMigratedThisProcess) return;
|
|
342
|
+
_watchCcTriageMigratedThisProcess = true;
|
|
343
|
+
try { migrateWatchCcTriageKeys(); }
|
|
344
|
+
catch { /* best-effort — next process start retries */ }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function _resetWatchCcTriageMigrationFlagForTest() {
|
|
348
|
+
_watchCcTriageMigratedThisProcess = false;
|
|
349
|
+
}
|
|
350
|
+
|
|
288
351
|
module.exports = {
|
|
289
352
|
readMetrics,
|
|
290
353
|
applyMetricsMutation,
|
|
291
354
|
_mirrorJsonFromSql,
|
|
292
355
|
_resetMetricsTableForTest,
|
|
356
|
+
migrateWatchCcTriageKeys,
|
|
357
|
+
migrateWatchCcTriageKeysOnce,
|
|
358
|
+
_resetWatchCcTriageMigrationFlagForTest,
|
|
293
359
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2348",
|
|
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"
|