@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 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 item = items.find(i => i.id === id);
6981
- if (!item) { result = { code: 404, body: { error: 'item not found' } }; return items; }
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
- const watchId = body.watchId ? String(body.watchId) : '';
9804
- const label = watchId ? `watch-cc-triage:${watchId}` : 'watch-cc-triage';
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
- if (shared.resolveCcUseWorkerPool(engineConfig)) {
9890
- // The ACP worker pool is copilot-only (shared.resolveCcUseWorkerPool
9891
- // forces it OFF for non-copilot CC) and copilot has imageInput:false, so
9892
- // image turns never reach this branch — drop `images` here intentionally.
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 },
@@ -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). **Exception:** when the opt-in Copilot worker pool is active (`engine.ccUseWorkerPool: true`), images are silently dropped — the pool's ACP `stream()` does not forward images; a note in `_invokeCcStream` marks this intentional. |
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.
@@ -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.2346",
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"