@yemi33/minions 0.1.1043 → 0.1.1045

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,8 +1,10 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1043 (2026-04-16)
3
+ ## 0.1.1045 (2026-04-16)
4
4
 
5
5
  ### Other
6
+ - Harden loop watch conversion
7
+ - Add watches sidebar activity badge
6
8
  - test(cli): add unit tests for handleCommand, start, stop, kill, spawn (#1191)
7
9
  - chore: untrack pipeline files — local config only
8
10
  - restore: recover daily-arch-improvement and weekly-dead-code-cleanup pipelines
@@ -8,6 +8,7 @@ const _pageCounters = {
8
8
  plans: function(d) { return (d.prdProgress?.complete || 0) + '|' + (d.plans || []).length + '|' + (d.plans || []).map(function(p) { return p.status || ''; }).join(','); },
9
9
  prs: function(d) { return (d.pullRequests || []).length + '|' + (d.pullRequests || []).filter(function(p) { return p.status === 'merged'; }).length; },
10
10
  inbox: function(d) { return (d.inbox || []).length + '|' + (d.notes?.content || '').length; },
11
+ watches: function(d) { return (d.watches || []).length + '|' + (d.watches || []).map(function(w) { return [w.id || '', w.status || '', w.triggerCount || 0, w.last_triggered || ''].join(':'); }).join(','); },
11
12
  meetings: function(d) { return (d.meetings || []).length + '|' + (d.meetings || []).reduce(function(s, m) { return s + (m.round || 0); }, 0); },
12
13
  pipelines: function(d) { return (d.pipelines || []).length + '|' + (d.pipelines || []).reduce(function(s, p) { return s + (p.runs || []).length; }, 0); },
13
14
  schedule: function(d) { return (d.schedules || []).length; },
package/dashboard.js CHANGED
@@ -672,11 +672,110 @@ function parseCCActions(text) {
672
672
  // This pure function detects /loop invocation in CC response text and synthesizes
673
673
  // a create-watch action as a fallback. Returns null if no conversion needed.
674
674
 
675
- function _detectLoopInvocation(text, actions) {
676
- if (!text) return null;
675
+ function _detectLoopInvocation(text, actions, toolUses) {
676
+ const observedToolUses = Array.isArray(toolUses) ? toolUses : [];
677
+ if (!text && observedToolUses.length === 0) return null;
677
678
  // If a create-watch action was already emitted, no fallback needed
678
679
  if (actions && actions.some(a => a.type === 'create-watch')) return null;
679
680
 
681
+ function _extractTargetFromValue(value, keyHint) {
682
+ if (value == null) return null;
683
+ const hint = String(keyHint || '').toLowerCase();
684
+ if (Array.isArray(value)) {
685
+ for (const item of value) {
686
+ const nested = _extractTargetFromValue(item, hint);
687
+ if (nested) return nested;
688
+ }
689
+ return null;
690
+ }
691
+ if (typeof value === 'object') {
692
+ for (const [k, v] of Object.entries(value)) {
693
+ const nested = _extractTargetFromValue(v, k);
694
+ if (nested) return nested;
695
+ }
696
+ return null;
697
+ }
698
+ const str = String(value).trim();
699
+ if (!str) return null;
700
+ const prUrlMatch = str.match(/\/pull\/(\d+)\b/i) || str.match(/\/pullrequest\/(\d+)\b/i);
701
+ if (prUrlMatch) return { target: prUrlMatch[1], targetType: 'pr' };
702
+ const prMatch = str.match(/\bPR[- #:]?(\d+)\b/i) || str.match(/\bpull[- ]request[- #:]?(\d+)\b/i);
703
+ if (prMatch) return { target: prMatch[1], targetType: 'pr' };
704
+ const wiMatch = str.match(/\bW-([a-z0-9]+)\b/i);
705
+ if (wiMatch) return { target: 'W-' + wiMatch[1], targetType: 'work-item' };
706
+ if ((hint.includes('pr') || hint.includes('pull')) && /^\d+$/.test(str)) return { target: str, targetType: 'pr' };
707
+ if ((hint.includes('work') || hint.includes('item') || hint === 'id') && /^W-[a-z0-9]+$/i.test(str)) return { target: str.toUpperCase().startsWith('W-') ? 'W-' + str.slice(2) : str, targetType: 'work-item' };
708
+ if (hint.includes('target')) {
709
+ if (/^\d+$/.test(str)) return { target: str, targetType: 'pr' };
710
+ if (/^W-[a-z0-9]+$/i.test(str)) return { target: 'W-' + str.slice(2), targetType: 'work-item' };
711
+ }
712
+ return null;
713
+ }
714
+
715
+ function _extractIntervalFromValue(value, keyHint) {
716
+ if (value == null) return null;
717
+ const hint = String(keyHint || '').toLowerCase();
718
+ if (Array.isArray(value)) {
719
+ for (const item of value) {
720
+ const nested = _extractIntervalFromValue(item, hint);
721
+ if (nested) return nested;
722
+ }
723
+ return null;
724
+ }
725
+ if (typeof value === 'object') {
726
+ for (const [k, v] of Object.entries(value)) {
727
+ const nested = _extractIntervalFromValue(v, k);
728
+ if (nested) return nested;
729
+ }
730
+ return null;
731
+ }
732
+ if (!(hint.includes('interval') || hint.includes('every') || hint.includes('frequency'))) return null;
733
+ const str = String(value).trim().toLowerCase();
734
+ if (!str) return null;
735
+ if (/^\d+$/.test(str)) return str;
736
+ const match = str.match(/^(\d+(?:\.\d+)?)\s*(s|sec|seconds?|m|min|minutes?|h|hr|hours?)$/i);
737
+ if (!match) return null;
738
+ return match[1] + match[2][0].toLowerCase();
739
+ }
740
+
741
+ function _extractConditionFromValue(value, keyHint) {
742
+ if (value == null) return null;
743
+ const hint = String(keyHint || '').toLowerCase();
744
+ if (Array.isArray(value)) {
745
+ for (const item of value) {
746
+ const nested = _extractConditionFromValue(item, hint);
747
+ if (nested) return nested;
748
+ }
749
+ return null;
750
+ }
751
+ if (typeof value === 'object') {
752
+ for (const [k, v] of Object.entries(value)) {
753
+ const nested = _extractConditionFromValue(v, k);
754
+ if (nested) return nested;
755
+ }
756
+ return null;
757
+ }
758
+ if (!(hint.includes('condition') || hint.includes('until') || hint.includes('goal') || hint.includes('status'))) return null;
759
+ const str = String(value).trim().toLowerCase();
760
+ if (['merged', 'build-pass', 'build-fail', 'completed', 'failed', 'status-change', 'any', 'new-comments', 'vote-change'].includes(str)) return str;
761
+ if (/\b(?:pass(?:es|ing|ed)?|green|succeed(?:s|ed)?|success)\b/i.test(str)) return 'build-pass';
762
+ if (/\b(?:fail(?:s|ing|ed)?|red|broken|broke)\b/i.test(str)) return 'build-fail';
763
+ if (/\bmerge(?:d)?\b/i.test(str)) return 'merged';
764
+ if (/\bcomplete(?:d)?\b/i.test(str)) return 'completed';
765
+ if (/\bfail(?:ed)?\b/i.test(str)) return 'failed';
766
+ if (/\bcomment/i.test(str)) return 'new-comments';
767
+ if (/\bvote|review/i.test(str)) return 'vote-change';
768
+ if (/\bstatus/i.test(str)) return 'any';
769
+ return null;
770
+ }
771
+
772
+ const loopToolSeen = observedToolUses.some(t => /\bloop\b/i.test(String(t?.name || '')));
773
+ const toolText = observedToolUses.map(t => {
774
+ try { return [String(t?.name || ''), JSON.stringify(t?.input || {})].filter(Boolean).join(' '); }
775
+ catch { return String(t?.name || ''); }
776
+ }).join('\n');
777
+ const combinedText = [text || '', toolText].filter(Boolean).join('\n');
778
+
680
779
  // Check for /loop invocation patterns in CC response
681
780
  const loopPatterns = [
682
781
  /\/loop\b/i,
@@ -686,14 +785,21 @@ function _detectLoopInvocation(text, actions) {
686
785
  /\bmonitoring.*\bloop\b/i,
687
786
  /\binvok(?:e|ed|ing).*\bloop\b/i,
688
787
  ];
689
- if (!loopPatterns.some(p => p.test(text))) return null;
788
+ if (!loopToolSeen && !loopPatterns.some(p => p.test(combinedText))) return null;
690
789
 
691
790
  // Extract target — PR number or work item ID
692
- const prMatch = text.match(/\bPR[- #]?(\d+)\b/i) || text.match(/\bpull[- ]request[- #]?(\d+)/i);
693
- const wiMatch = text.match(/\bW-([a-z0-9]+)\b/);
791
+ const directTarget = observedToolUses.map(t => _extractTargetFromValue(t && t.input, t && t.name)).find(Boolean);
792
+ const prMatch = combinedText.match(/\/pull\/(\d+)\b/i) ||
793
+ combinedText.match(/\/pullrequest\/(\d+)\b/i) ||
794
+ combinedText.match(/\bPR[- #:]?(\d+)\b/i) ||
795
+ combinedText.match(/\bpull[- ]request[- #:]?(\d+)/i);
796
+ const wiMatch = combinedText.match(/\bW-([a-z0-9]+)\b/i);
694
797
 
695
798
  let target = null, targetType = 'pr';
696
- if (prMatch) {
799
+ if (directTarget) {
800
+ target = directTarget.target;
801
+ targetType = directTarget.targetType;
802
+ } else if (prMatch) {
697
803
  target = prMatch[1];
698
804
  targetType = 'pr';
699
805
  } else if (wiMatch) {
@@ -703,16 +809,20 @@ function _detectLoopInvocation(text, actions) {
703
809
  if (!target) return null; // Can't synthesize without a target
704
810
 
705
811
  // Extract interval (e.g. "every 15 minutes", "every 5m")
706
- const intervalMatch = text.match(/every\s+(\d+)\s*(s|sec|seconds?|m|min|minutes?|h|hr|hours?)\b/i);
812
+ const directInterval = observedToolUses.map(t => _extractIntervalFromValue(t && t.input, t && t.name)).find(Boolean);
813
+ const intervalMatch = combinedText.match(/every\s+(\d+)\s*(s|sec|seconds?|m|min|minutes?|h|hr|hours?)\b/i);
707
814
  let interval = '5m';
708
- if (intervalMatch) interval = intervalMatch[1] + intervalMatch[2][0];
815
+ if (directInterval) interval = directInterval;
816
+ else if (intervalMatch) interval = intervalMatch[1] + intervalMatch[2][0];
709
817
 
710
818
  // Infer condition from keywords
711
- let condition = 'any';
712
- if (/\bbuild\b/i.test(text) && /\b(?:pass(?:es|ing|ed)?|green|succeed(?:s|ed)?|success)\b/i.test(text)) condition = 'build-pass';
713
- else if (/\bbuild\b/i.test(text) && /\b(?:fail(?:s|ing|ed)?|red|broken|broke)\b/i.test(text)) condition = 'build-fail';
714
- else if (/\bmerge[d]?\b/i.test(text)) condition = 'merged';
715
- else if (/\bcomplete[d]?\b/i.test(text)) condition = 'completed';
819
+ let condition = observedToolUses.map(t => _extractConditionFromValue(t && t.input, t && t.name)).find(Boolean) || 'any';
820
+ if (condition === 'any') {
821
+ if (/\bbuild\b/i.test(combinedText) && /\b(?:pass(?:es|ing|ed)?|green|succeed(?:s|ed)?|success)\b/i.test(combinedText)) condition = 'build-pass';
822
+ else if (/\bbuild\b/i.test(combinedText) && /\b(?:fail(?:s|ing|ed)?|red|broken|broke)\b/i.test(combinedText)) condition = 'build-fail';
823
+ else if (/\bmerge[d]?\b/i.test(combinedText)) condition = 'merged';
824
+ else if (/\bcomplete[d]?\b/i.test(combinedText)) condition = 'completed';
825
+ }
716
826
 
717
827
  return {
718
828
  type: 'create-watch',
@@ -726,6 +836,23 @@ function _detectLoopInvocation(text, actions) {
726
836
  };
727
837
  }
728
838
 
839
+ function _extractToolUsesFromRaw(raw) {
840
+ const toolUses = [];
841
+ if (!raw) return toolUses;
842
+ for (const line of String(raw).split('\n')) {
843
+ const trimmed = line.trim();
844
+ if (!trimmed || !trimmed.startsWith('{')) continue;
845
+ try {
846
+ const obj = JSON.parse(trimmed);
847
+ if (obj.type !== 'assistant' || !Array.isArray(obj.message?.content)) continue;
848
+ for (const block of obj.message.content) {
849
+ if (block?.type === 'tool_use' && block.name) toolUses.push({ name: block.name, input: block.input || {} });
850
+ }
851
+ } catch {}
852
+ }
853
+ return toolUses;
854
+ }
855
+
729
856
  // ── Server-side CC action execution ──────────────────────────────────────────
730
857
  // Actions are executed server-side so all clients (frontend, curl, Teams) get the same behavior.
731
858
  // The frontend still shows status toasts but no longer needs to fire the API calls.
@@ -3758,8 +3885,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3758
3885
  }
3759
3886
 
3760
3887
  const parsed = parseCCActions(result.text);
3888
+ const toolUses = _extractToolUsesFromRaw(result.raw);
3761
3889
  // Safety net: detect /loop invocation and convert to create-watch
3762
- const _loopWatch = _detectLoopInvocation(parsed.text, parsed.actions);
3890
+ const _loopWatch = _detectLoopInvocation(parsed.text, parsed.actions, toolUses);
3763
3891
  if (_loopWatch) {
3764
3892
  parsed.actions.push(_loopWatch);
3765
3893
  console.warn('[CC] /loop invocation detected — converted to create-watch');
@@ -3869,6 +3997,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3869
3997
  const streamModel = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
3870
3998
  const streamEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
3871
3999
  const ccMaxTurns = CONFIG.engine?.ccMaxTurns || shared.ENGINE_DEFAULTS.ccMaxTurns;
4000
+ let toolUses = [];
3872
4001
  const llmPromise = callLLMStreaming(prompt, CC_STATIC_SYSTEM_PROMPT, {
3873
4002
  timeout: 900000, label: 'command-center', model: streamModel, maxTurns: ccMaxTurns,
3874
4003
  allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
@@ -3879,6 +4008,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3879
4008
  writeCcEvent({ type: 'chunk', text: display });
3880
4009
  },
3881
4010
  onToolUse: (name, input) => {
4011
+ toolUses.push({ name, input: input || {} });
3882
4012
  writeCcEvent({ type: 'tool', name, input: _lightToolInput(input) });
3883
4013
  }
3884
4014
  });
@@ -3893,6 +4023,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3893
4023
  console.log(`[CC-stream] Resume failed (code=${result.code}) — retrying fresh`);
3894
4024
  const freshPreamble = buildCCStatePreamble();
3895
4025
  const freshPrompt = (freshPreamble ? freshPreamble + '\n\n---\n\n' : '') + body.message;
4026
+ toolUses = []; // discard stale metadata from the failed resume attempt
3896
4027
  const retryPromise = callLLMStreaming(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
3897
4028
  timeout: 900000, label: 'command-center', model: streamModel, maxTurns: ccMaxTurns,
3898
4029
  allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
@@ -3903,6 +4034,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3903
4034
  writeCcEvent({ type: 'chunk', text: display });
3904
4035
  },
3905
4036
  onToolUse: (name, input) => {
4037
+ toolUses.push({ name, input: input || {} });
3906
4038
  writeCcEvent({ type: 'tool', name, input: _lightToolInput(input) });
3907
4039
  }
3908
4040
  });
@@ -3952,7 +4084,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3952
4084
  // Send final result with actions — execute server-side first
3953
4085
  const { text: displayText, actions } = parseCCActions(result.text);
3954
4086
  // Safety net: detect /loop invocation and convert to create-watch
3955
- const _loopWatch = _detectLoopInvocation(displayText, actions);
4087
+ const _loopWatch = _detectLoopInvocation(displayText, actions, toolUses);
3956
4088
  if (_loopWatch) {
3957
4089
  actions.push(_loopWatch);
3958
4090
  console.warn('[CC] /loop invocation detected — converted to create-watch');
@@ -4142,8 +4274,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4142
4274
  try {
4143
4275
  const config = queries.getConfig();
4144
4276
  const routing = safeRead(path.join(MINIONS_DIR, 'routing.md')) || '';
4277
+ const engine = { ...shared.ENGINE_DEFAULTS, ...(config.engine || {}) };
4278
+ if (engine.prPollStatusEvery === undefined && engine.adoPollStatusEvery !== undefined) engine.prPollStatusEvery = engine.adoPollStatusEvery;
4279
+ if (engine.prPollCommentsEvery === undefined && engine.adoPollCommentsEvery !== undefined) engine.prPollCommentsEvery = engine.adoPollCommentsEvery;
4145
4280
  return jsonReply(res, 200, {
4146
- engine: { ...shared.ENGINE_DEFAULTS, ...(config.engine || {}) },
4281
+ engine,
4147
4282
  claude: { ...shared.DEFAULT_CLAUDE, ...(config.claude || {}) },
4148
4283
  agents: config.agents || {},
4149
4284
  teams: { ...shared.ENGINE_DEFAULTS.teams, ...(config.teams || {}) },
@@ -4172,6 +4307,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4172
4307
  if (body.engine) {
4173
4308
  const e = body.engine;
4174
4309
  const D = shared.ENGINE_DEFAULTS;
4310
+ if (e.prPollStatusEvery === undefined && e.adoPollStatusEvery !== undefined) e.prPollStatusEvery = e.adoPollStatusEvery;
4311
+ if (e.prPollCommentsEvery === undefined && e.adoPollCommentsEvery !== undefined) e.prPollCommentsEvery = e.adoPollCommentsEvery;
4175
4312
  // Numeric fields: { key: [min, max?] }
4176
4313
  const numericFields = {
4177
4314
  tickInterval: [10000], maxConcurrent: [1, 50], inboxConsolidateThreshold: [1],
@@ -4181,7 +4318,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4181
4318
  meetingRoundTimeout: [60000],
4182
4319
  versionCheckInterval: [60000],
4183
4320
  maxBuildFixAttempts: [1, 10],
4184
- adoPollStatusEvery: [1], adoPollCommentsEvery: [1],
4321
+ prPollStatusEvery: [1], prPollCommentsEvery: [1],
4185
4322
  agentBusyReassignMs: [0],
4186
4323
  };
4187
4324
  for (const [key, [min, max]] of Object.entries(numericFields)) {
@@ -4194,6 +4331,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4194
4331
  config.engine[key] = val;
4195
4332
  }
4196
4333
  }
4334
+ delete config.engine.adoPollStatusEvery;
4335
+ delete config.engine.adoPollCommentsEvery;
4197
4336
  // String fields
4198
4337
  if (e.worktreeRoot !== undefined) config.engine.worktreeRoot = String(e.worktreeRoot || D.worktreeRoot);
4199
4338
  // CC model/effort
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1043",
3
+ "version": "0.1.1045",
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"