@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.
- package/bin/minions.js +24 -11
- package/dashboard/js/command-parser.js +1 -1
- package/dashboard/js/memory-panel.js +262 -0
- package/dashboard/js/qa.js +2 -2
- package/dashboard/js/refresh.js +9 -1
- package/dashboard/js/render-dispatch.js +92 -0
- package/dashboard/js/render-other.js +1 -1
- package/dashboard/js/render-plans.js +82 -13
- package/dashboard/js/render-prs.js +2 -1
- package/dashboard/js/render-schedules.js +1 -1
- package/dashboard/js/render-watches.js +1 -1
- package/dashboard/js/settings.js +100 -11
- package/dashboard/layout.html +6 -0
- package/dashboard/pages/engine-memory-panel.html +49 -0
- package/dashboard/pages/engine.html +1 -0
- package/dashboard/slim/js/link-pr.js +5 -5
- package/dashboard/slim/js/modals-tiles.js +44 -3
- package/dashboard/slim/js/projects.js +8 -6
- package/dashboard/slim/styles.css +20 -0
- package/dashboard/styles.css +39 -0
- package/dashboard-build.js +17 -2
- package/dashboard.js +469 -21
- package/docs/README.md +8 -1
- package/docs/auto-discovery.md +40 -0
- package/docs/branch-derivation.md +13 -1
- package/docs/cross-repo-plans.md +292 -0
- package/docs/deprecated.json +4 -4
- package/docs/pr-auto-fix-dispatch.md +64 -0
- package/docs/pr-review-fix-loop.md +1 -1
- package/docs/watches.md +1 -0
- package/engine/ado.js +1 -10
- package/engine/diagnostics-memory.js +190 -0
- package/engine/dispatch.js +53 -0
- package/engine/lifecycle.js +155 -191
- package/engine/meeting.js +30 -0
- package/engine/playbook.js +15 -0
- package/engine/queries.js +165 -5
- package/engine/runtimes/copilot.js +19 -0
- package/engine/shared.js +303 -3
- package/engine/watchdog.js +6 -0
- package/engine.js +576 -113
- package/package.json +2 -2
- package/playbooks/plan-to-prd.md +25 -2
- 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
|
-
|
|
751
|
-
|
|
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 || '',
|
|
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
|
|