@yemi33/minions 0.1.2177 → 0.1.2179

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.
Files changed (44) hide show
  1. package/bin/minions.js +24 -11
  2. package/dashboard/js/command-parser.js +1 -1
  3. package/dashboard/js/memory-panel.js +262 -0
  4. package/dashboard/js/qa.js +2 -2
  5. package/dashboard/js/refresh.js +9 -1
  6. package/dashboard/js/render-dispatch.js +92 -0
  7. package/dashboard/js/render-other.js +1 -1
  8. package/dashboard/js/render-plans.js +82 -13
  9. package/dashboard/js/render-prs.js +2 -1
  10. package/dashboard/js/render-schedules.js +1 -1
  11. package/dashboard/js/render-watches.js +1 -1
  12. package/dashboard/js/settings.js +100 -11
  13. package/dashboard/layout.html +6 -0
  14. package/dashboard/pages/engine-memory-panel.html +49 -0
  15. package/dashboard/pages/engine.html +1 -0
  16. package/dashboard/slim/js/link-pr.js +5 -5
  17. package/dashboard/slim/js/modals-tiles.js +44 -3
  18. package/dashboard/slim/js/projects.js +8 -6
  19. package/dashboard/slim/styles.css +20 -0
  20. package/dashboard/styles.css +39 -0
  21. package/dashboard-build.js +17 -2
  22. package/dashboard.js +469 -21
  23. package/docs/README.md +8 -1
  24. package/docs/auto-discovery.md +40 -0
  25. package/docs/branch-derivation.md +13 -1
  26. package/docs/cross-repo-plans.md +292 -0
  27. package/docs/deprecated.json +4 -4
  28. package/docs/pr-auto-fix-dispatch.md +64 -0
  29. package/docs/pr-review-fix-loop.md +1 -1
  30. package/docs/watches.md +1 -0
  31. package/engine/ado.js +1 -10
  32. package/engine/diagnostics-memory.js +190 -0
  33. package/engine/dispatch.js +53 -0
  34. package/engine/lifecycle.js +155 -191
  35. package/engine/meeting.js +30 -0
  36. package/engine/playbook.js +15 -0
  37. package/engine/queries.js +165 -5
  38. package/engine/runtimes/copilot.js +19 -0
  39. package/engine/shared.js +303 -3
  40. package/engine/watchdog.js +6 -0
  41. package/engine.js +576 -113
  42. package/package.json +2 -2
  43. package/playbooks/plan-to-prd.md +25 -2
  44. package/playbooks/plan.md +4 -2
package/engine/queries.js CHANGED
@@ -734,6 +734,118 @@ function getPrs(project) {
734
734
  let _prsCache = null;
735
735
  let _prsCacheAt = 0;
736
736
 
737
+ // ── W-mqba5ulq000nd255 — cross-project PR de-dupe helpers ────────────────────
738
+ //
739
+ // When a dispatching agent in project A reports a PR URL that actually lives
740
+ // on project B's repo, the engine historically wrote the PR into BOTH project
741
+ // files. The A copy got stamped with `_invalidProjectScope` (a tracking
742
+ // breadcrumb) but was never pruned. getPullRequests' legacy first-write-wins
743
+ // dedupe then surfaced the wrong scope first, masking the real record. These
744
+ // helpers collapse those duplicates at read time, with field-merge so we
745
+ // never drop prdItems, sourcePlan, itemType, minionsReview, or
746
+ // _automationFixCauses that lived on the loser.
747
+
748
+ // Timestamps that signal "this record was touched recently". Highest of the
749
+ // three breaks ties when neither record has a scope advantage.
750
+ function _prFreshnessMs(pr) {
751
+ if (!pr) return 0;
752
+ const cands = [pr.lastPushedAt, pr.mergedAt, pr.lastPolledAt, pr.lastPolled, pr._attachedAt, pr.created];
753
+ let best = 0;
754
+ for (const c of cands) {
755
+ if (!c) continue;
756
+ const t = typeof c === 'number' ? c : Date.parse(c);
757
+ if (Number.isFinite(t) && t > best) best = t;
758
+ }
759
+ return best;
760
+ }
761
+
762
+ function _groupPrRecordsById(prs) {
763
+ const groups = new Map();
764
+ const order = [];
765
+ for (const pr of prs) {
766
+ if (!pr || !pr.id) continue;
767
+ if (!groups.has(pr.id)) {
768
+ const g = [];
769
+ groups.set(pr.id, g);
770
+ order.push(g);
771
+ }
772
+ groups.get(pr.id).push(pr);
773
+ }
774
+ return order;
775
+ }
776
+
777
+ // Choose the winning record from a group of cross-scope duplicates. Fields
778
+ // the winner left empty are filled from the loser(s). Returns the winner +
779
+ // the dropped sibling records so the caller can log the pruning decision.
780
+ function _pickPrDedupeWinner(group, projects) {
781
+ if (group.length === 1) return { winner: group[0], dropped: [] };
782
+
783
+ // Map each project name -> its canonical scope, so we can detect when a
784
+ // record's _scope matches the PR url-derived scope (preference #2 below).
785
+ const projectScopeByName = new Map();
786
+ for (const p of projects || []) {
787
+ if (!p || !p.name) continue;
788
+ const sc = shared.getProjectPrScope(p);
789
+ if (sc) projectScopeByName.set(p.name, sc);
790
+ }
791
+
792
+ // Resolve the PR's "correct" scope from its URL (independent of _scope).
793
+ // Falls back to the canonical id when the URL is absent. Group members
794
+ // share the same id so any non-empty url is sufficient.
795
+ let urlScope = '';
796
+ for (const pr of group) {
797
+ const info = shared.getPrScopeInfo(pr, pr.url || '');
798
+ if (info && info.scope) { urlScope = info.scope; break; }
799
+ }
800
+
801
+ // Score each candidate. Higher is better. Composite key:
802
+ // bit 2: no _invalidProjectScope stamp (best signal)
803
+ // bit 1: _scope matches url-derived scope
804
+ // bit 0: freshness rank (broken into a separate tiebreak after)
805
+ function scoreOf(pr) {
806
+ const scopeOk = !pr._invalidProjectScope ? 1 : 0;
807
+ const projectScope = projectScopeByName.get(pr._scope) || '';
808
+ const matchesUrl = urlScope && projectScope && projectScope === urlScope ? 1 : 0;
809
+ return (scopeOk << 1) | matchesUrl;
810
+ }
811
+
812
+ let winnerIdx = 0;
813
+ let winnerScore = scoreOf(group[0]);
814
+ let winnerFresh = _prFreshnessMs(group[0]);
815
+ for (let i = 1; i < group.length; i++) {
816
+ const s = scoreOf(group[i]);
817
+ const f = _prFreshnessMs(group[i]);
818
+ if (s > winnerScore || (s === winnerScore && f > winnerFresh)) {
819
+ winnerIdx = i;
820
+ winnerScore = s;
821
+ winnerFresh = f;
822
+ }
823
+ }
824
+
825
+ const winner = group[winnerIdx];
826
+ const dropped = group.filter((_, i) => i !== winnerIdx);
827
+
828
+ // Field-merge: fill empty winner fields from any loser. Winner wins on
829
+ // every field it has; losers only contribute where the winner is missing
830
+ // the field entirely (undefined, null, '', or empty array).
831
+ function isEmpty(v) {
832
+ if (v == null) return true;
833
+ if (typeof v === 'string') return v === '';
834
+ if (Array.isArray(v)) return v.length === 0;
835
+ return false;
836
+ }
837
+ const mergeKeys = ['prdItems', 'sourcePlan', 'itemType', 'minionsReview', '_automationFixCauses'];
838
+ for (const loser of dropped) {
839
+ for (const key of mergeKeys) {
840
+ if (isEmpty(winner[key]) && !isEmpty(loser[key])) {
841
+ winner[key] = loser[key];
842
+ }
843
+ }
844
+ }
845
+
846
+ return { winner, dropped };
847
+ }
848
+
737
849
  function getPullRequests(config) {
738
850
  const now = Date.now();
739
851
  if (_prsCache && (now - _prsCacheAt) < 1000) return _prsCache;
@@ -741,14 +853,34 @@ function getPullRequests(config) {
741
853
  const projects = getProjects(config);
742
854
  const projectByName = new Map(projects.map(p => [p.name, p]));
743
855
  const allPrs = [];
744
- const seenIds = new Set();
745
856
 
746
857
  // SQL is the canonical (and only) PR store after Phase 9.
747
858
  const store = require('./pull-requests-store');
748
859
  const sqlPrs = store.readAllPullRequests() || [];
749
860
 
750
- for (const pr of sqlPrs) {
751
- if (!pr?.id || seenIds.has(pr.id)) continue;
861
+ // W-mqba5ulq000nd255 — Read-side de-dupe across project scopes. When the
862
+ // same canonical pr.id appears under multiple scopes (e.g. an agent in
863
+ // project A reported a PR URL that actually lives on project B's repo,
864
+ // landing a stub stamped with _invalidProjectScope in A while the real
865
+ // record lives in B), collapse to one record using the preference order:
866
+ // 1. record where `_invalidProjectScope` is FALSY
867
+ // 2. record whose scope matches the PR url-derived scope
868
+ // 3. most-recently-updated (lastPushedAt / mergedAt / _attachedAt)
869
+ // 4. first-seen
870
+ // Then merge non-conflicting fields from the loser into the winner so we
871
+ // never lose prdItems, sourcePlan, itemType, minionsReview, or
872
+ // _automationFixCauses if only the loser carried them.
873
+ const groups = _groupPrRecordsById(sqlPrs);
874
+ for (const group of groups) {
875
+ const { winner, dropped } = _pickPrDedupeWinner(group, projects);
876
+ if (dropped.length > 0) {
877
+ try {
878
+ const droppedScopes = dropped.map(d => d._scope || 'unknown').join(', ');
879
+ shared.log('info', `[pull-requests] de-duped ${winner.id} across ${group.length} records, kept _project=${winner._scope || 'unknown'}, dropped [${droppedScopes}]`);
880
+ } catch { /* logging is best-effort */ }
881
+ }
882
+
883
+ const pr = winner;
752
884
  const scope = pr._scope;
753
885
  delete pr._scope;
754
886
  if (scope === 'central') {
@@ -769,7 +901,6 @@ function getPullRequests(config) {
769
901
  // _noOpFixes themselves.
770
902
  pr._pausedCauses = shared.getPrPausedCauses(pr);
771
903
  allPrs.push(pr);
772
- seenIds.add(pr.id);
773
904
  }
774
905
  allPrs.sort((a, b) => {
775
906
  // W-mpej044m00076d63: sort by the full ISO `created` timestamp DESC so
@@ -1521,6 +1652,21 @@ function _getPrdInputHash(projects) {
1521
1652
  }
1522
1653
  // Static pr-links.json overrides (affect shared.getPrLinks(); missing project mtimes otherwise)
1523
1654
  try { mtimes.push(fs.statSync(path.join(MINIONS_DIR, 'engine', 'pr-links.json')).mtimeMs); } catch { mtimes.push(0); }
1655
+ // W-mqacrzis0003df4a — Source-plan markdown mtimes. Without this, the
1656
+ // per-PRD planStale fresh-computation at lines ~1568-1576 is never reached
1657
+ // after the user revises plans/<x>.md until the engine tick (~10s) flips
1658
+ // the persisted plan.planStale flag. The cached `_prdResultCache` hits the
1659
+ // early-return and serves the prior (stale) `planStale: false` to readers
1660
+ // long enough for a fast user to Approve and silently bypass diff-aware
1661
+ // regen. Push the PLANS_DIR directory mtime first (catches add/delete on
1662
+ // POSIX) then every .md file's mtime in sorted order for determinism.
1663
+ try { mtimes.push(fs.statSync(PLANS_DIR).mtimeMs); } catch { mtimes.push(0); }
1664
+ try {
1665
+ const planFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md')).sort();
1666
+ for (const f of planFiles) {
1667
+ try { mtimes.push(fs.statSync(path.join(PLANS_DIR, f)).mtimeMs); } catch { mtimes.push(0); }
1668
+ }
1669
+ } catch { /* plans/ may not exist in some test/init states */ }
1524
1670
  return { hash: mtimes.join(','), prdDirMtime, archiveDirMtime };
1525
1671
  }
1526
1672
 
@@ -1770,7 +1916,17 @@ function getPrdInfo(config) {
1770
1916
  items: items.map(i => ({
1771
1917
  id: i.id, name: i.name || i.title, priority: i.priority,
1772
1918
  complexity: i.estimated_complexity || i.size, status: i.status || 'missing',
1773
- description: i.description || '', projects: i.projects || [],
1919
+ description: i.description || '',
1920
+ // P-e8d49105 — projects[] feeds the per-item badge in
1921
+ // dashboard/js/render-prd.js:250-252 + the plan-group rollup at :243.
1922
+ // Honor a pre-populated projects[] (forward-compat for any future
1923
+ // multi-repo single item); otherwise derive a single-element array
1924
+ // from i.project so cross-repo PRDs (every item.project may differ)
1925
+ // light up the dormant badge code without changing single-project
1926
+ // behavior — i.project keeps being emitted unchanged below.
1927
+ projects: (Array.isArray(i.projects) && i.projects.length > 0)
1928
+ ? i.projects
1929
+ : (i.project ? [i.project] : []),
1774
1930
  prs: prdToPr[i.id] || [], depends_on: i.depends_on || [],
1775
1931
  project: i.project || '', source: i._source || '', planSummary: i._planSummary || '', planProject: i._planProject || '', planStatus: i._planStatus || 'active', _archived: i._archived || false, sourcePlan: i._sourcePlan || '',
1776
1932
  branchStrategy: i._branchStrategy || 'parallel',
@@ -2572,6 +2728,10 @@ module.exports = {
2572
2728
 
2573
2729
  // Pull requests
2574
2730
  getPrs, getPullRequests,
2731
+ // W-mqba5ulq000nd255 — exported for direct unit testing of the cross-project
2732
+ // PR dedupe helpers.
2733
+ _pickPrDedupeWinner,
2734
+ _groupPrRecordsById,
2575
2735
 
2576
2736
  // Skills
2577
2737
  collectSkillFiles, getSkills, getSkillIndex, invalidateSkillsCache,
@@ -444,6 +444,7 @@ function buildArgs(opts = {}) {
444
444
  disableBuiltinMcps,
445
445
  suppressAgentsMd,
446
446
  reasoningSummaries,
447
+ disabledMcpServers,
447
448
  } = opts;
448
449
 
449
450
  const args = [
@@ -478,6 +479,16 @@ function buildArgs(opts = {}) {
478
479
  if (suppressAgentsMd === true) args.push('--no-custom-instructions');
479
480
  if (reasoningSummaries === true) args.push('--enable-reasoning-summaries');
480
481
 
482
+ // P-mcp-storm — Disable named user-scope MCP servers per spawn. Copilot loads
483
+ // ~/.copilot/mcp-config.json unconditionally (not gated by --add-dir/cwd), so
484
+ // this --disable-mcp-server <name> (repeatable) is the only lever to keep an
485
+ // operator's interactive MCP servers out of autonomous agent dispatches.
486
+ if (Array.isArray(disabledMcpServers)) {
487
+ for (const name of disabledMcpServers) {
488
+ if (name) args.push('--disable-mcp-server', String(name));
489
+ }
490
+ }
491
+
481
492
  // --stream takes a value: 'on' or 'off'. Caller passes that exact value.
482
493
  if (stream === 'on' || stream === 'off') {
483
494
  args.push('--stream', stream);
@@ -504,6 +515,14 @@ function buildSpawnFlags(opts = {}) {
504
515
  if (opts.disableBuiltinMcps === true) flags.push('--disable-builtin-mcps');
505
516
  if (opts.suppressAgentsMd === true) flags.push('--no-custom-instructions');
506
517
  if (opts.reasoningSummaries === true) flags.push('--enable-reasoning-summaries');
518
+ // P-mcp-storm — forward each disabled user MCP server so spawn-agent.js can
519
+ // re-emit it on the copilot CLI (parseSpawnArgs accumulates these into
520
+ // opts.disabledMcpServers, which buildArgs turns back into --disable-mcp-server).
521
+ if (Array.isArray(opts.disabledMcpServers)) {
522
+ for (const name of opts.disabledMcpServers) {
523
+ if (name) flags.push('--disable-mcp-server', String(name));
524
+ }
525
+ }
507
526
  return flags;
508
527
  }
509
528