@yemi33/minions 0.1.1801 → 0.1.1803

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1803 (2026-05-08)
4
+
5
+ ### Other
6
+ - Guarantee Copilot steering delivery (#2225)
7
+
3
8
  ## 0.1.1800 (2026-05-08)
4
9
 
5
10
  ### Other
package/dashboard.js CHANGED
@@ -1929,16 +1929,52 @@ function fencedUntrustedBlock(label, content) {
1929
1929
  return `### ${label}\n${fence}text\n${value}\n${fence}`;
1930
1930
  }
1931
1931
 
1932
+ function _messageExplicitlyRequestsMonitoring(message) {
1933
+ const text = String(message || '').toLowerCase();
1934
+ if (!text.trim()) return false;
1935
+ if (/\b(?:do\s+not|don't|dont|never|without|no\s+need\s+to)\s+(?:monitor(?:ing)?|watch(?:ing)?|poll(?:ing)?|check(?:ing)?|notify(?:ing)?|ping(?:ing)?|keep(?:ing)?\s+an\s+eye)\b/.test(text)) {
1936
+ return false;
1937
+ }
1938
+ return [
1939
+ /\bmonitor(?:ing)?\b/,
1940
+ /\bwatch(?:ing)?\b/,
1941
+ /\bkeep(?:ing)?\s+an\s+eye\s+on\b/,
1942
+ /\bcheck(?:ing)?\s+(?:it|this|that|[^.?!\n]+)\s+periodically\b/,
1943
+ /\bperiodically\s+check\b/,
1944
+ /\bcheck(?:ing)?\s+[^.?!\n]+\bevery\s+\d+\s*(?:s|sec|seconds?|m|min|minutes?|h|hr|hours?)\b/,
1945
+ /\bevery\s+\d+\s*(?:s|sec|seconds?|m|min|minutes?|h|hr|hours?)\b[^.?!\n]+\b(?:check|poll|monitor|watch)\b/,
1946
+ /\bping\s+(?:me\s+)?(?:on|when|after)\b/,
1947
+ /\bping\s+on\s+completion\b/,
1948
+ /\bnotify\s+(?:me\s+)?(?:on|when|after)\b/,
1949
+ /\blet\s+me\s+know\s+(?:when|once|if)\b/,
1950
+ /\bpoll(?:ing)?\b/,
1951
+ ].some(pattern => pattern.test(text));
1952
+ }
1953
+
1954
+ function _isDispatchLikeCCAction(action) {
1955
+ const type = String(action?.type || '').trim().toLowerCase();
1956
+ return type === 'dispatch' || ['fix', 'explore', 'review', 'test'].includes(type);
1957
+ }
1958
+
1959
+ function _filterImplicitPostDispatchActions(actions, humanMessage) {
1960
+ if (!Array.isArray(actions) || actions.length === 0) return [];
1961
+ if (_messageExplicitlyRequestsMonitoring(humanMessage)) return actions;
1962
+ const dispatchIdx = actions.findIndex(_isDispatchLikeCCAction);
1963
+ if (dispatchIdx < 0) return actions;
1964
+ return actions.slice(0, dispatchIdx + 1);
1965
+ }
1966
+
1932
1967
  // ── /loop → create-watch safety net ──────────────────────────────────────────
1933
1968
  // CC sometimes invokes the /loop skill instead of emitting a create-watch action.
1934
1969
  // This pure function detects /loop invocation in CC response text and synthesizes
1935
1970
  // a create-watch action as a fallback. Returns null if no conversion needed.
1936
1971
 
1937
- function _detectLoopInvocation(text, actions, toolUses) {
1972
+ function _detectLoopInvocation(text, actions, toolUses, humanMessage) {
1938
1973
  const observedToolUses = Array.isArray(toolUses) ? toolUses : [];
1939
1974
  if (!text && observedToolUses.length === 0) return null;
1940
1975
  // If a create-watch action was already emitted, no fallback needed
1941
1976
  if (actions && actions.some(a => a.type === 'create-watch')) return null;
1977
+ if (humanMessage !== undefined && !_messageExplicitlyRequestsMonitoring(humanMessage)) return null;
1942
1978
 
1943
1979
  function _extractTargetFromValue(value, keyHint) {
1944
1980
  if (value == null) return null;
@@ -6509,12 +6545,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6509
6545
  const parsed = parseCCActions(result.text);
6510
6546
  const toolUses = Array.isArray(result.toolUses) ? result.toolUses : _extractToolUsesFromRaw(result.raw);
6511
6547
  // Safety net: detect /loop invocation and convert to create-watch
6512
- const _loopWatch = _detectLoopInvocation(parsed.text, parsed.actions, toolUses);
6548
+ const _loopWatch = _detectLoopInvocation(parsed.text, parsed.actions, toolUses, body.message);
6513
6549
  if (_loopWatch) {
6514
6550
  parsed.actions.push(_loopWatch);
6515
6551
  console.warn('[CC] /loop invocation detected — converted to create-watch');
6516
6552
  try { shared.log('warn', '/loop invocation detected in CC response — auto-converted to create-watch'); } catch {}
6517
6553
  }
6554
+ parsed.actions = _filterImplicitPostDispatchActions(parsed.actions, body.message);
6518
6555
  if (parsed.actions.length > 0) {
6519
6556
  parsed.actionResults = await executeCCActions(parsed.actions);
6520
6557
  }
@@ -6834,14 +6871,15 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6834
6871
  }
6835
6872
 
6836
6873
  // Send final result with actions — execute server-side first
6837
- const { text: displayText, actions, _actionParseError } = parseCCActions(result.text);
6874
+ let { text: displayText, actions, _actionParseError } = parseCCActions(result.text);
6838
6875
  // Safety net: detect /loop invocation and convert to create-watch
6839
- const _loopWatch = _detectLoopInvocation(displayText, actions, toolUses);
6876
+ const _loopWatch = _detectLoopInvocation(displayText, actions, toolUses, body.message);
6840
6877
  if (_loopWatch) {
6841
6878
  actions.push(_loopWatch);
6842
6879
  console.warn('[CC] /loop invocation detected — converted to create-watch');
6843
6880
  try { shared.log('warn', '/loop invocation detected in CC response — auto-converted to create-watch'); } catch {}
6844
6881
  }
6882
+ actions = _filterImplicitPostDispatchActions(actions, body.message);
6845
6883
  let actionResults;
6846
6884
  if (actions.length > 0) {
6847
6885
  actionResults = await executeCCActions(actions);
@@ -8471,6 +8509,8 @@ module.exports = {
8471
8509
  _resolveSkillReadPath,
8472
8510
  DOC_CHAT_DOCUMENT_DELIMITER,
8473
8511
  _ccValidateAction,
8512
+ _messageExplicitlyRequestsMonitoring,
8513
+ _filterImplicitPostDispatchActions,
8474
8514
  _findDuplicateWorkItemCreate: findDuplicateWorkItemCreate,
8475
8515
  _createWorkItemWithDedup: createWorkItemWithDedup,
8476
8516
  _resolveWorkItemsCreateTarget: resolveWorkItemsCreateTarget,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-08T17:21:22.511Z"
4
+ "cachedAt": "2026-05-08T17:37:37.501Z"
5
5
  }
package/engine.js CHANGED
@@ -407,6 +407,50 @@ function mergePendingSteeringEntries(...groups) {
407
407
  return merged;
408
408
  }
409
409
 
410
+ function promoteCheckpointSteeringForClose(agentId, procInfo, runtime, liveOutputPath) {
411
+ if (!procInfo || procInfo._steeringMessage) return { status: 'none', entries: [] };
412
+ if (runtime?.capabilities?.midRunSessionId !== false) return { status: 'none', entries: [] };
413
+
414
+ const startedAtMs = Date.parse(procInfo.startedAt);
415
+ const runStartMs = Number.isFinite(startedAtMs) ? startedAtMs : 0;
416
+ const pendingPaths = new Set((procInfo._pendingSteeringFiles || []).map(entry => entry?.path || entry).filter(Boolean));
417
+ const deferredPaths = new Set((procInfo._deferredSteeringFiles || []).filter(Boolean));
418
+ const unread = steering.listUnreadSteeringMessages(agentId).filter(entry => entry.message.trim());
419
+ const entriesByPath = new Map(unread.map(entry => [entry.path, entry]));
420
+ const pendingDeferred = Array.from(deferredPaths)
421
+ .map(filePath => entriesByPath.get(filePath))
422
+ .filter(Boolean);
423
+ const lateCheckpoint = unread.filter(entry =>
424
+ entry.createdAtMs >= runStartMs
425
+ && !pendingPaths.has(entry.path)
426
+ && !deferredPaths.has(entry.path)
427
+ );
428
+ const checkpointEntries = mergePendingSteeringEntries(pendingDeferred, lateCheckpoint);
429
+ if (checkpointEntries.length === 0) {
430
+ delete procInfo._deferredSteeringFiles;
431
+ return { status: 'none', entries: [] };
432
+ }
433
+
434
+ if (!procInfo.sessionId) {
435
+ log('warn', `Steering: ${agentId} exited before a resumable sessionId was available - ${checkpointEntries.length} message(s) remain pending`);
436
+ try { fs.appendFileSync(liveOutputPath, `\n[steering-pending] Agent exited before a resumable session was available. Your message remains unread and will be retried on the next dispatch.\n`); } catch {}
437
+ return { status: 'pending', entries: checkpointEntries };
438
+ }
439
+
440
+ if (pendingDeferred.length > 0) {
441
+ log('info', `Steering: delivering ${pendingDeferred.length} deferred message(s) for ${agentId} at resumable checkpoint`);
442
+ }
443
+ if (lateCheckpoint.length > 0) {
444
+ log('info', `Steering: delivering ${lateCheckpoint.length} late checkpoint message(s) for ${agentId} at resumable checkpoint`);
445
+ }
446
+ procInfo._steeringMessage = checkpointEntries.map(entry => entry.message.trim()).join('\n\n');
447
+ procInfo._steeringSessionId = procInfo.sessionId;
448
+ procInfo._steeringEntry = checkpointEntries;
449
+ procInfo._steeringDeferredCheckpoint = true;
450
+ delete procInfo._deferredSteeringFiles;
451
+ return { status: 'promoted', entries: checkpointEntries };
452
+ }
453
+
410
454
  // Resolve dependency plan item IDs to their PR branches
411
455
  function resolveDependencyBranches(depIds, sourcePlan, project, config) {
412
456
  const results = []; // [{ branch, prId }]
@@ -1193,6 +1237,7 @@ async function spawnAgent(dispatchItem, config) {
1193
1237
  const MAX_OUTPUT = 1024 * 1024; // 1MB
1194
1238
  let stdout = '';
1195
1239
  let stderr = '';
1240
+ let steeringAckStdout = '';
1196
1241
  const sessionCaptureState = { sessionLineBuffer: '' };
1197
1242
  let _trustCheckDone = false;
1198
1243
  const _spawnTime = Date.now();
@@ -1207,6 +1252,7 @@ async function spawnAgent(dispatchItem, config) {
1207
1252
  const chunk = data.toString();
1208
1253
  realActivityMap.set(id, Date.now());
1209
1254
  if (stdout.length < MAX_OUTPUT) stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
1255
+ if (steeringAckStdout.length < MAX_OUTPUT) steeringAckStdout += chunk.slice(0, MAX_OUTPUT - steeringAckStdout.length);
1210
1256
  try { fs.appendFileSync(liveOutputPath, chunk); } catch { /* optional */ }
1211
1257
 
1212
1258
  // Trust gate detection: check first 30s of output for trust/permission prompts
@@ -1254,26 +1300,8 @@ async function spawnAgent(dispatchItem, config) {
1254
1300
  }
1255
1301
 
1256
1302
  const procInfo = activeProcesses.get(id);
1257
- ackPendingSteeringFiles(agentId, procInfo, stdout);
1258
-
1259
- if (procInfo?._deferredSteeringFiles?.length && procInfo.sessionId) {
1260
- const deferredPaths = new Set(procInfo._deferredSteeringFiles);
1261
- const pendingDeferred = steering.listUnreadSteeringMessages(agentId)
1262
- .filter(entry => deferredPaths.has(entry.path) && entry.message.trim());
1263
- if (pendingDeferred.length > 0) {
1264
- log('info', `Steering: delivering ${pendingDeferred.length} deferred message(s) for ${agentId} at resumable checkpoint`);
1265
- procInfo._steeringMessage = pendingDeferred.map(entry => entry.message.trim()).join('\n\n');
1266
- procInfo._steeringSessionId = procInfo.sessionId;
1267
- procInfo._steeringEntry = pendingDeferred;
1268
- procInfo._steeringDeferredCheckpoint = true;
1269
- delete procInfo._deferredSteeringFiles;
1270
- } else {
1271
- delete procInfo._deferredSteeringFiles;
1272
- }
1273
- } else if (procInfo?._deferredSteeringFiles?.length) {
1274
- log('warn', `Steering: ${agentId} exited before a resumable sessionId was available — message remains pending`);
1275
- try { fs.appendFileSync(liveOutputPath, `\n[steering-pending] Agent exited before a resumable session was available. Your message remains unread and will be retried on the next dispatch.\n`); } catch {}
1276
- }
1303
+ ackPendingSteeringFiles(agentId, procInfo, steeringAckStdout);
1304
+ promoteCheckpointSteeringForClose(agentId, procInfo, runtime, liveOutputPath);
1277
1305
 
1278
1306
  // Check if this was a steering kill — re-spawn with resume
1279
1307
  if (procInfo?._steeringMessage) {
@@ -1401,6 +1429,7 @@ async function spawnAgent(dispatchItem, config) {
1401
1429
  ),
1402
1430
  });
1403
1431
 
1432
+ steeringAckStdout = '';
1404
1433
  // Live steering kills discard partial old output. Deferred checkpoint
1405
1434
  // steering keeps the completed turn output so completion parsing still
1406
1435
  // sees the original work if the follow-up only acknowledges steering.
@@ -1414,6 +1443,7 @@ async function spawnAgent(dispatchItem, config) {
1414
1443
  const chunk = data.toString();
1415
1444
  realActivityMap.set(id, Date.now());
1416
1445
  if (stdout.length < MAX_OUTPUT) stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
1446
+ if (steeringAckStdout.length < MAX_OUTPUT) steeringAckStdout += chunk.slice(0, MAX_OUTPUT - steeringAckStdout.length);
1417
1447
  try { fs.appendFileSync(liveOutputPath, chunk); } catch { /* optional */ }
1418
1448
  const resumeInfo = activeProcesses.get(id);
1419
1449
  markRuntimeResumeOutputSeen(resumeInfo);
@@ -4719,6 +4749,7 @@ module.exports = {
4719
4749
  parseConflictFiles, pruneAncestorDeps, preflightMergeSimulation, // exported for testing
4720
4750
  isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, // exported for testing
4721
4751
  _maxTurnsForType, buildProjectContext, normalizeAc, _buildAgentSpawnFlags, _classifyAgentFailure, // exported for testing
4752
+ promoteCheckpointSteeringForClose, // exported for testing
4722
4753
  normalizePrBranch, resolvePrBranch, prCausePart, getPrCauseHead, getPrCauseBase, getPrAutomationCauseKey, getPrAutomationDispatchKey, // exported for testing
4723
4754
 
4724
4755
  // Playbooks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1801",
3
+ "version": "0.1.1803",
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"