@yemi33/minions 0.1.2041 → 0.1.2042
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/refresh.js +9 -1
- package/dashboard/js/settings.js +2 -0
- package/dashboard.js +127 -2
- package/engine/dispatch.js +19 -5
- package/engine/github.js +29 -0
- package/engine/shared.js +13 -0
- package/engine.js +28 -0
- package/package.json +1 -1
package/dashboard/js/refresh.js
CHANGED
|
@@ -14,7 +14,15 @@ const _pageCounters = {
|
|
|
14
14
|
// triggers or the count changes; triggerCount removed because it advances
|
|
15
15
|
// on the same event as last_triggered (F9/S4).
|
|
16
16
|
watches: function(d) { return (d.watches || []).length + '|' + (d.watches || []).reduce(function(m, w) { return Math.max(m, new Date(w.last_triggered || 0).getTime() || 0); }, 0); },
|
|
17
|
-
|
|
17
|
+
// meetings signature: full count + sum of all rounds. Uses meetingsTotal
|
|
18
|
+
// (top-level full count of meetings on disk) NOT meetings.length — the
|
|
19
|
+
// latter is the slim slice which drops terminal meetings >7d via
|
|
20
|
+
// statusMeetingsRetentionDays, so an archived meeting reaching round 3
|
|
21
|
+
// would silently fail to flip the dot. Round-sum stays on the slim slice
|
|
22
|
+
// (we don't track per-meeting round in meetingsTotal) — operators who
|
|
23
|
+
// care about archived-meeting round transitions can crank the retention
|
|
24
|
+
// window or set it to 0. (W-mphlrxx6000a8760)
|
|
25
|
+
meetings: function(d) { return (d.meetingsTotal ?? (d.meetings || []).length) + '|' + (d.meetings || []).reduce(function(s, m) { return s + (m.round || 0); }, 0); },
|
|
18
26
|
pipelines: function(d) { return (d.pipelines || []).length + '|' + (d.pipelines || []).reduce(function(s, p) { return s + (p.runs || []).length; }, 0); },
|
|
19
27
|
schedule: function(d) { return (d.schedules || []).length; },
|
|
20
28
|
// tools signature: skills count + mcp servers count.
|
package/dashboard/js/settings.js
CHANGED
|
@@ -89,6 +89,7 @@ async function openSettings() {
|
|
|
89
89
|
settingsField('Meeting Round Timeout', 'set-meetingRoundTimeout', e.meetingRoundTimeout || 900000, 'ms', 'Auto-advance meeting round after this') +
|
|
90
90
|
settingsField('Operator login (used in branch names)', 'set-operatorLogin', e.operatorLogin || '', '', 'Override the human operator login used in user/<loginname>/<wi-id>-<slug> branches. Empty = auto-resolve via gh / git email / OS username (currently resolves to: ' + (e._resolvedOperatorLogin || 'unknown') + ')') +
|
|
91
91
|
settingsField('Status WorkItems Retention', 'set-statusWorkItemsRetentionDays', e.statusWorkItemsRetentionDays ?? 7, 'days', 'Trim done/failed/cancelled work items older than N days from the /api/status workItems slice (active items are always shipped). Cuts SPA payload from ~3MB to <500KB. Set to 0 to disable trimming (full list shipped, restoring legacy behavior).') +
|
|
92
|
+
settingsField('Status Meetings Retention', 'set-statusMeetingsRetentionDays', e.statusMeetingsRetentionDays ?? 7, 'days', 'Trim completed/archived meetings older than N days from the /api/status meetings slice (active meetings are always shipped). Cuts SPA payload from ~4.3MB to <500KB. Detail modal still fetches full transcripts via /api/meetings/:id. Set to 0 to disable trimming (full list shipped, restoring legacy behavior).') +
|
|
92
93
|
'</div>' +
|
|
93
94
|
'<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Automation</h3>' +
|
|
94
95
|
'<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:16px">' +
|
|
@@ -599,6 +600,7 @@ async function saveSettings() {
|
|
|
599
600
|
meetingRoundTimeout: document.getElementById('set-meetingRoundTimeout').value,
|
|
600
601
|
operatorLogin: (document.getElementById('set-operatorLogin')?.value ?? '').trim(),
|
|
601
602
|
statusWorkItemsRetentionDays: document.getElementById('set-statusWorkItemsRetentionDays').value,
|
|
603
|
+
statusMeetingsRetentionDays: document.getElementById('set-statusMeetingsRetentionDays').value,
|
|
602
604
|
autoApprovePlans: document.getElementById('set-autoApprovePlans').checked,
|
|
603
605
|
evalLoop: document.getElementById('set-evalLoop').checked,
|
|
604
606
|
autoDecompose: document.getElementById('set-autoDecompose').checked,
|
package/dashboard.js
CHANGED
|
@@ -23,6 +23,7 @@ const shared = require('./engine/shared');
|
|
|
23
23
|
const queries = require('./engine/queries');
|
|
24
24
|
const ado = require('./engine/ado');
|
|
25
25
|
const gh = require('./engine/github');
|
|
26
|
+
const ghToken = require('./engine/gh-token');
|
|
26
27
|
const issues = require('./engine/issues');
|
|
27
28
|
const watchesMod = require('./engine/watches');
|
|
28
29
|
const meetingMod = require('./engine/meeting');
|
|
@@ -1710,7 +1711,14 @@ function _buildStatusFastState() {
|
|
|
1710
1711
|
metrics: getMetrics(),
|
|
1711
1712
|
workItems: _slimWorkItemsForStatus(getWorkItems()),
|
|
1712
1713
|
watches: watchesMod.getWatches(),
|
|
1713
|
-
meetings: _safeStatusSlice('meetings', () => meetingMod.getMeetings(), []),
|
|
1714
|
+
meetings: _safeStatusSlice('meetings', () => _slimMeetingsForStatus(meetingMod.getMeetings()), []),
|
|
1715
|
+
// Top-level full meeting count (NOT slim slice length). Surfaced so the
|
|
1716
|
+
// sidebar activity-dot counter (dashboard/js/refresh.js _pageCounters.meetings)
|
|
1717
|
+
// still fires when ANY meeting — including old/archived ones dropped from
|
|
1718
|
+
// the slim slice by statusMeetingsRetentionDays — gains a new round.
|
|
1719
|
+
// Without this, completing the third round of an archived meeting would
|
|
1720
|
+
// silently fail to light the sidebar dot. (W-mphlrxx6000a8760)
|
|
1721
|
+
meetingsTotal: _safeStatusSlice('meetingsTotal', () => _countMeetingsForStatus(), 0),
|
|
1714
1722
|
// QA runs — surfaced for the sidebar activity-dot counter and any future
|
|
1715
1723
|
// CC/aggregate view. Tab-level rendering keeps its own /api/qa/runs poll
|
|
1716
1724
|
// (5 s while the QA page is mounted). qa-runs.json is in the mtime tracker
|
|
@@ -1844,6 +1852,101 @@ function _slimWorkItemsForStatus(items) {
|
|
|
1844
1852
|
return surviving;
|
|
1845
1853
|
}
|
|
1846
1854
|
|
|
1855
|
+
// ── /api/status meetings slimming (W-mphlrxx6000a8760) ──────────────────────
|
|
1856
|
+
// Mirrors the workItems trim above (PR #2816). Meetings are the second
|
|
1857
|
+
// largest /api/status slice after workItems — live measurement: 22 meetings
|
|
1858
|
+
// / 4.3MB (60% of the 7.2MB payload). The list renderer in
|
|
1859
|
+
// dashboard/js/render-meetings.js:renderMeetings only needs:
|
|
1860
|
+
// - id, title, status, round, participants, agenda(short), createdAt,
|
|
1861
|
+
// completedAt
|
|
1862
|
+
// - per-participant booleans of findings/debate (used to pick the
|
|
1863
|
+
// ✓/⏳/○ icon — `m.findings?.[p]` truthy check, line 48-50)
|
|
1864
|
+
// The detail modal calls `/api/meetings/:id` which serves the full record
|
|
1865
|
+
// (findings.content + debate.content + conclusion + transcript bodies), so
|
|
1866
|
+
// dropping those from the slice is safe.
|
|
1867
|
+
//
|
|
1868
|
+
// Active meetings (investigating/debating/concluding) are ALWAYS kept
|
|
1869
|
+
// regardless of age. Terminal meetings (completed/archived) only survive
|
|
1870
|
+
// if their completedAt/roundStartedAt/createdAt is within the window.
|
|
1871
|
+
// Set engine.statusMeetingsRetentionDays = 0 to disable trimming entirely
|
|
1872
|
+
// (returns the full list — but still slim-shaped — restoring legacy size).
|
|
1873
|
+
const _ACTIVE_MEETING_STATUSES_FOR_STATUS = new Set(['investigating', 'debating', 'concluding']);
|
|
1874
|
+
const _TERMINAL_MEETING_STATUSES_FOR_STATUS = new Set(['completed', 'archived']);
|
|
1875
|
+
function _resolveStatusMeetingsRetentionDays() {
|
|
1876
|
+
const raw = CONFIG?.engine?.statusMeetingsRetentionDays;
|
|
1877
|
+
if (raw === 0 || raw === '0') return 0;
|
|
1878
|
+
const n = Number(raw);
|
|
1879
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
1880
|
+
return shared.ENGINE_DEFAULTS.statusMeetingsRetentionDays;
|
|
1881
|
+
}
|
|
1882
|
+
function _slimMeetingForStatus(meeting) {
|
|
1883
|
+
// Reduce findings/debate objects to {agentId: true} sentinels — the list
|
|
1884
|
+
// renderer only checks `m.findings?.[p]` for truthiness when picking the
|
|
1885
|
+
// participant-badge icon. Keeping just the keys preserves that contract
|
|
1886
|
+
// while dropping the per-round agent transcript bodies (~95KB+ each).
|
|
1887
|
+
const findingsKeys = meeting.findings && typeof meeting.findings === 'object'
|
|
1888
|
+
? Object.keys(meeting.findings) : [];
|
|
1889
|
+
const debateKeys = meeting.debate && typeof meeting.debate === 'object'
|
|
1890
|
+
? Object.keys(meeting.debate) : [];
|
|
1891
|
+
const findings = {};
|
|
1892
|
+
for (const k of findingsKeys) findings[k] = true;
|
|
1893
|
+
const debate = {};
|
|
1894
|
+
for (const k of debateKeys) debate[k] = true;
|
|
1895
|
+
const slim = {
|
|
1896
|
+
id: meeting.id,
|
|
1897
|
+
title: meeting.title,
|
|
1898
|
+
status: meeting.status,
|
|
1899
|
+
round: meeting.round,
|
|
1900
|
+
participants: Array.isArray(meeting.participants) ? meeting.participants : [],
|
|
1901
|
+
agenda: meeting.agenda,
|
|
1902
|
+
createdAt: meeting.createdAt,
|
|
1903
|
+
findings,
|
|
1904
|
+
debate,
|
|
1905
|
+
};
|
|
1906
|
+
if (meeting.completedAt !== undefined) slim.completedAt = meeting.completedAt;
|
|
1907
|
+
if (meeting.roundStartedAt !== undefined) slim.roundStartedAt = meeting.roundStartedAt;
|
|
1908
|
+
if (meeting.createdBy !== undefined) slim.createdBy = meeting.createdBy;
|
|
1909
|
+
return slim;
|
|
1910
|
+
}
|
|
1911
|
+
function _slimMeetingsForStatus(meetings) {
|
|
1912
|
+
if (!Array.isArray(meetings)) return meetings;
|
|
1913
|
+
const retentionDays = _resolveStatusMeetingsRetentionDays();
|
|
1914
|
+
if (retentionDays <= 0) {
|
|
1915
|
+
// Trimming disabled — keep full list but still flatten via slim shape
|
|
1916
|
+
// so wire format is consistent and the heavy bodies never ship via
|
|
1917
|
+
// /api/status regardless of operator config.
|
|
1918
|
+
return meetings.map(_slimMeetingForStatus);
|
|
1919
|
+
}
|
|
1920
|
+
const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
1921
|
+
const surviving = [];
|
|
1922
|
+
for (const meeting of meetings) {
|
|
1923
|
+
if (!meeting) continue;
|
|
1924
|
+
const status = meeting.status || 'investigating';
|
|
1925
|
+
if (_TERMINAL_MEETING_STATUSES_FOR_STATUS.has(status)) {
|
|
1926
|
+
const ts = meeting.completedAt || meeting.roundStartedAt || meeting.createdAt || '';
|
|
1927
|
+
const tsMs = ts ? Date.parse(ts) : NaN;
|
|
1928
|
+
// Drop only when we have a parseable timestamp and it's beyond the
|
|
1929
|
+
// window. Meetings with missing/unparseable timestamps stay visible —
|
|
1930
|
+
// we'd rather over-include than silently hide them.
|
|
1931
|
+
if (Number.isFinite(tsMs) && tsMs < cutoffMs) {
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
} else if (!_ACTIVE_MEETING_STATUSES_FOR_STATUS.has(status)) {
|
|
1935
|
+
// Unknown status — keep, so a future round name isn't silently
|
|
1936
|
+
// hidden until the constant set is updated.
|
|
1937
|
+
}
|
|
1938
|
+
surviving.push(_slimMeetingForStatus(meeting));
|
|
1939
|
+
}
|
|
1940
|
+
return surviving;
|
|
1941
|
+
}
|
|
1942
|
+
// Count meetings on disk without rehydrating bodies — backs the sidebar
|
|
1943
|
+
// activity dot signature so new rounds in trimmed/archived meetings still
|
|
1944
|
+
// flip the counter (refresh.js _pageCounters.meetings reads meetingsTotal).
|
|
1945
|
+
function _countMeetingsForStatus() {
|
|
1946
|
+
const meetings = meetingMod.getMeetings();
|
|
1947
|
+
return Array.isArray(meetings) ? meetings.length : 0;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1847
1950
|
// Build the slow-state slice (rarely-changing data: ~60s TTL).
|
|
1848
1951
|
function _buildStatusSlowState() {
|
|
1849
1952
|
const prdInfo = getPrdInfo();
|
|
@@ -8538,6 +8641,20 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
8538
8641
|
_setEngineConfig('statusWorkItemsRetentionDays', val);
|
|
8539
8642
|
}
|
|
8540
8643
|
}
|
|
8644
|
+
// W-mphlrxx6000a8760: /api/status meetings retention window. Same
|
|
8645
|
+
// shape as statusWorkItemsRetentionDays — 0 must persist literally
|
|
8646
|
+
// (disables trim), so handled outside the numericFields loop.
|
|
8647
|
+
if (e.statusMeetingsRetentionDays !== undefined) {
|
|
8648
|
+
const raw = e.statusMeetingsRetentionDays;
|
|
8649
|
+
if (raw === '' || raw === null) {
|
|
8650
|
+
_deleteEngineConfig('statusMeetingsRetentionDays');
|
|
8651
|
+
} else {
|
|
8652
|
+
let val = Number(raw);
|
|
8653
|
+
if (!Number.isFinite(val) || val < 0) val = D.statusMeetingsRetentionDays;
|
|
8654
|
+
if (val > 365) { _clamped.push(`statusMeetingsRetentionDays: ${val} → 365 (range: 0–365)`); val = 365; }
|
|
8655
|
+
_setEngineConfig('statusMeetingsRetentionDays', val);
|
|
8656
|
+
}
|
|
8657
|
+
}
|
|
8541
8658
|
// W-mpejf0fq000e84d6: operator login override. Empty string clears
|
|
8542
8659
|
// the override (engine falls back to gh/git/os resolution); any other
|
|
8543
8660
|
// value pins the login used in `user/<login>/<wi-id>-<slug>` branches.
|
|
@@ -9915,7 +10032,15 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9915
10032
|
// a regex match against untrusted PR-link input (the body of POST
|
|
9916
10033
|
// /api/pull-requests/link); validate before exec. `prNum` is already
|
|
9917
10034
|
// a number; coerce to string for argv.
|
|
9918
|
-
|
|
10035
|
+
//
|
|
10036
|
+
// W-mphm0kt0000cebc3 (Bug A): route the GH PAT per slug. Without this,
|
|
10037
|
+
// cross-account scopes (e.g. opg-microsoft/minions when active gh is
|
|
10038
|
+
// yemi33) 404 silently and enrichment never lands — title stays
|
|
10039
|
+
// "PR #N (polling...)" indefinitely. Mirrors engine/github.js:316.
|
|
10040
|
+
const token = ghToken.resolveTokenForSlug(slug);
|
|
10041
|
+
const ghOpts = { timeout: 15000 };
|
|
10042
|
+
if (token) ghOpts.env = { ...process.env, GH_TOKEN: token };
|
|
10043
|
+
const result = await shared.shellSafeGh(['api', `repos/${shared.validateGhSlug(slug)}/pulls/${String(prNum)}`], ghOpts);
|
|
9919
10044
|
const d = JSON.parse(result);
|
|
9920
10045
|
prData = { title: d.title, description: d.body, branch: d.head?.ref, author: d.user?.login };
|
|
9921
10046
|
} else if (adoTarget && !initialPrData) {
|
package/engine/dispatch.js
CHANGED
|
@@ -111,14 +111,28 @@ function getBranchDispatchLockKey(entry) {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
function findActivePrOrBranchLock(dispatch, item) {
|
|
114
|
-
if (item?.type !== WORK_TYPE.FIX) return null;
|
|
115
114
|
const active = dispatch.active || [];
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
115
|
+
|
|
116
|
+
// PR-target dedup is FIX-only: a FIX shouldn't stack on top of another FIX
|
|
117
|
+
// for the same PR, but a REVIEW + FIX pair targeting the same PR is the
|
|
118
|
+
// normal review-then-fix flow and must not be dedup'd here.
|
|
119
|
+
if (item?.type === WORK_TYPE.FIX) {
|
|
120
|
+
const prTargetKey = getPrDispatchTargetKey(item);
|
|
121
|
+
if (prTargetKey) {
|
|
122
|
+
const existing = active.find(d => getPrDispatchTargetKey(d) === prTargetKey);
|
|
123
|
+
if (existing) return { existing, reason: `active PR dispatch ${prTargetKey}` };
|
|
124
|
+
}
|
|
120
125
|
}
|
|
121
126
|
|
|
127
|
+
// Branch-lock applies to EVERY type, not just FIX (W-mphll3py0006234e —
|
|
128
|
+
// issue #2817). Any dispatch that carries meta.branch is, by definition,
|
|
129
|
+
// claiming ownership of that branch for the duration of its run — if two
|
|
130
|
+
// such dispatches overlap on the same branch they race the eventual
|
|
131
|
+
// `git push` and the spawn-time stale-HEAD guard fails whichever push
|
|
132
|
+
// lost the race. Mirrors the dispatch-loop's `lockedBranches` mutex
|
|
133
|
+
// (engine.js ~6577-6731) at queue-time so maintenance-class dispatches
|
|
134
|
+
// (setup / test / docs / implement / verify / decompose / review …) get
|
|
135
|
+
// the same coordination FIX already had.
|
|
122
136
|
const branchLockKey = getBranchDispatchLockKey(item);
|
|
123
137
|
if (!branchLockKey) return null;
|
|
124
138
|
const existing = active.find(d => getBranchDispatchLockKey(d) === branchLockKey);
|
package/engine/github.js
CHANGED
|
@@ -669,6 +669,35 @@ async function pollPrStatus(config) {
|
|
|
669
669
|
}
|
|
670
670
|
}
|
|
671
671
|
|
|
672
|
+
// W-mphm0kt0000cebc3 (Bug B): backfill title/description/agent for
|
|
673
|
+
// project-local PRs that are still on the link-time placeholder. The
|
|
674
|
+
// central poller (above, ~lines 561-583) does this already; without
|
|
675
|
+
// parity here a manually-linked project-local PR whose initial
|
|
676
|
+
// enrichment IIFE failed (cross-account auth, transient blip) would
|
|
677
|
+
// stay stuck on "PR #N (polling...)" forever. `prData` is already in
|
|
678
|
+
// hand from line 622 — no extra API call needed.
|
|
679
|
+
const currentTitleForBackfill = pr.title || '';
|
|
680
|
+
if (!currentTitleForBackfill
|
|
681
|
+
|| currentTitleForBackfill.includes('polling...')
|
|
682
|
+
|| /[{}"\[\]]/.test(currentTitleForBackfill)
|
|
683
|
+
|| /^[0-9a-f-]{8,}$/i.test(currentTitleForBackfill)) {
|
|
684
|
+
if (prData.title) {
|
|
685
|
+
const nextTitle = String(prData.title).slice(0, 120);
|
|
686
|
+
if (pr.title !== nextTitle) {
|
|
687
|
+
pr.title = nextTitle;
|
|
688
|
+
updated = true;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (pr.description === undefined) {
|
|
693
|
+
pr.description = (prData.body || '').slice(0, 500);
|
|
694
|
+
updated = true;
|
|
695
|
+
}
|
|
696
|
+
if (pr.agent === 'human' && prData.user?.login) {
|
|
697
|
+
pr.agent = prData.user.login;
|
|
698
|
+
updated = true;
|
|
699
|
+
}
|
|
700
|
+
|
|
672
701
|
// Map GitHub PR state to minions status
|
|
673
702
|
let newStatus = pr.status;
|
|
674
703
|
if (prData.merged) newStatus = PR_STATUS.MERGED;
|
package/engine/shared.js
CHANGED
|
@@ -2041,6 +2041,19 @@ const ENGINE_DEFAULTS = {
|
|
|
2041
2041
|
// via GET /api/work-items/<id> when description/references/AC are needed.
|
|
2042
2042
|
// 0 disables the trim (full list shipped, restoring legacy behavior).
|
|
2043
2043
|
statusWorkItemsRetentionDays: 7,
|
|
2044
|
+
|
|
2045
|
+
// ── /api/status meetings retention (W-mphlrxx6000a8760) ─────────────────────
|
|
2046
|
+
// Same shape as statusWorkItemsRetentionDays — mirrors the trim+slim pass
|
|
2047
|
+
// for the meetings slice (live: 22 meetings / 4.3MB → ~5 meetings / <500KB
|
|
2048
|
+
// typical). Active meetings (investigating/debating/concluding) are ALWAYS
|
|
2049
|
+
// shipped regardless of age — only terminal meetings (completed/archived)
|
|
2050
|
+
// past the window are dropped. The detail modal fetches the full record
|
|
2051
|
+
// (findings, debate, conclusion, transcript bodies) on demand via
|
|
2052
|
+
// GET /api/meetings/<id> when opened. A top-level meetingsTotal field is
|
|
2053
|
+
// synthesized so the sidebar activity dot still fires when ANY meeting
|
|
2054
|
+
// (including those dropped from the slim slice) gains a new round.
|
|
2055
|
+
// 0 disables the trim (full list shipped, restoring legacy behavior).
|
|
2056
|
+
statusMeetingsRetentionDays: 7,
|
|
2044
2057
|
};
|
|
2045
2058
|
|
|
2046
2059
|
// ─── Runtime Fleet Resolution (P-3b8e5f1d) ──────────────────────────────────
|
package/engine.js
CHANGED
|
@@ -4151,6 +4151,18 @@ function ensurePrBranchForDispatch(project, pr, automationType) {
|
|
|
4151
4151
|
}
|
|
4152
4152
|
return branch;
|
|
4153
4153
|
}
|
|
4154
|
+
// W-mphm0kt0000cebc3 (Bug C): suppress the red "missing branch" badge during
|
|
4155
|
+
// the link grace window. User reports the badge "is so loud as a warning -
|
|
4156
|
+
// makes the user think something is wrong when it's just taking its time".
|
|
4157
|
+
// A just-linked PR has `created` set within the last few seconds and has
|
|
4158
|
+
// never been polled (no `headSha`); the enrichment IIFE + first poll cycle
|
|
4159
|
+
// need 10-30s to land the real `branch`. During that window, return ''
|
|
4160
|
+
// silently so dispatch is deferred without flipping any UI state. The next
|
|
4161
|
+
// tick retries; if branch still missing past the grace window, fall through
|
|
4162
|
+
// to the existing _branchResolutionError path (red badge as before).
|
|
4163
|
+
if (isWithinLinkGraceWindow(pr)) {
|
|
4164
|
+
return '';
|
|
4165
|
+
}
|
|
4154
4166
|
const reason = `Cannot dispatch ${automationType} for ${shared.getPrDisplayId(pr)}: missing pr_branch/source branch metadata. Link or refresh the PR so the source branch is known.`;
|
|
4155
4167
|
if (updatePrBranchResolutionState(project, pr, { reason })) {
|
|
4156
4168
|
log('warn', `PR ${pr.id}: ${reason}`);
|
|
@@ -4158,6 +4170,21 @@ function ensurePrBranchForDispatch(project, pr, automationType) {
|
|
|
4158
4170
|
return '';
|
|
4159
4171
|
}
|
|
4160
4172
|
|
|
4173
|
+
// W-mphm0kt0000cebc3 (Bug C): a PR is "freshly linked" if it was created in
|
|
4174
|
+
// the last 120s AND has never been successfully polled (no `headSha`). The
|
|
4175
|
+
// enrichment IIFE in dashboard.js's POST /api/pull-requests/link runs async
|
|
4176
|
+
// after returning the response, and the first GH/ADO poll completes within
|
|
4177
|
+
// 10-30s. The 120s window comfortably covers both without masking PRs whose
|
|
4178
|
+
// branch is genuinely missing.
|
|
4179
|
+
const PR_LINK_GRACE_WINDOW_MS = 120 * 1000;
|
|
4180
|
+
function isWithinLinkGraceWindow(pr) {
|
|
4181
|
+
if (!pr || !pr.created) return false;
|
|
4182
|
+
if (pr.headSha) return false; // poll has run successfully — past the grace window
|
|
4183
|
+
const createdMs = Date.parse(pr.created);
|
|
4184
|
+
if (!Number.isFinite(createdMs)) return false;
|
|
4185
|
+
return (Date.now() - createdMs) < PR_LINK_GRACE_WINDOW_MS;
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4161
4188
|
function prCausePart(value, fallback = 'unknown') {
|
|
4162
4189
|
const raw = String(value || '').trim();
|
|
4163
4190
|
return shared.safeSlugComponent(raw || fallback, 80);
|
|
@@ -6983,6 +7010,7 @@ module.exports = {
|
|
|
6983
7010
|
_maxTurnsForType, buildProjectContext, normalizeAc, _buildAgentSpawnFlags, _classifyAgentFailure, // exported for testing
|
|
6984
7011
|
promoteCheckpointSteeringForClose, // exported for testing
|
|
6985
7012
|
normalizePrBranch, resolvePrBranch, prCausePart, getPrCauseHead, getPrCauseBase, getPrAutomationCauseKey, getPrAutomationDispatchKey, // exported for testing
|
|
7013
|
+
ensurePrBranchForDispatch, isWithinLinkGraceWindow, PR_LINK_GRACE_WINDOW_MS, // exported for testing (W-mphm0kt0000cebc3)
|
|
6986
7014
|
|
|
6987
7015
|
// Playbooks
|
|
6988
7016
|
renderPlaybook, validatePlaybookVars, PLAYBOOK_REQUIRED_VARS, buildWorkItemDispatchVars,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2042",
|
|
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"
|