@yemi33/minions 0.1.2040 → 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 +233 -97
- 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
|
@@ -698,6 +698,35 @@ async function runWorktreeAdd(rootDir, worktreePath, addArgs, gitOpts, worktreeC
|
|
|
698
698
|
if (lastErr) throw lastErr;
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
+
// W-mphnm6a1000281b8: probe whether origin already advertises a head for
|
|
702
|
+
// `branchName`. Used by the fresh-create worktree path to choose between
|
|
703
|
+
// `git worktree add <path> <branch>` (checkout, local-track origin) and
|
|
704
|
+
// `git worktree add -b <branch> origin/<mainRef>` (fresh branch off main).
|
|
705
|
+
// Without this probe, PR-targeted fix/review/test/verify dispatches whose
|
|
706
|
+
// source branch is N commits ahead of main upstream (mirror branches like
|
|
707
|
+
// `sync/yemi33-master`, force-pushed PR branches) start their worktree on
|
|
708
|
+
// origin/<mainRef>; the stale-HEAD guard at the top of spawn-agent then
|
|
709
|
+
// trips on every dispatch and the cooldown machinery starves the PR.
|
|
710
|
+
//
|
|
711
|
+
// Returns true only on a confirmed advertised head; ls-remote exit code 2
|
|
712
|
+
// (no matching ref) and any other failure (network/auth) return false so
|
|
713
|
+
// the caller falls back to the existing `-b origin/<mainRef>` path.
|
|
714
|
+
async function probeBranchOnRemote(rootDir, branchName, gitOpts) {
|
|
715
|
+
if (!branchName || !rootDir) return false;
|
|
716
|
+
try {
|
|
717
|
+
await shared.shellSafeGit(
|
|
718
|
+
['ls-remote', '--exit-code', '--heads', 'origin', branchName],
|
|
719
|
+
{ ...gitOpts, cwd: rootDir, timeout: 10000 },
|
|
720
|
+
);
|
|
721
|
+
return true;
|
|
722
|
+
} catch (e) {
|
|
723
|
+
if (e && e.code !== 2) {
|
|
724
|
+
log('warn', `probeBranchOnRemote: ls-remote --heads origin ${branchName} failed: ${(e.message || '').split('\n')[0].slice(0, 200)} — treating as not-on-remote`);
|
|
725
|
+
}
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
701
730
|
// Detect and remove worktree registrations whose backing directory is missing
|
|
702
731
|
// on disk. `git worktree add` fails with "branch is already used by worktree
|
|
703
732
|
// at <path>" when a prior crash left such an entry behind, sometimes still in
|
|
@@ -1188,84 +1217,143 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1188
1217
|
} else {
|
|
1189
1218
|
log('info', `Creating worktree: ${worktreePath} on branch ${branchName}`);
|
|
1190
1219
|
const mainRef = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
|
|
1191
|
-
|
|
1192
|
-
//
|
|
1193
|
-
//
|
|
1194
|
-
//
|
|
1195
|
-
//
|
|
1196
|
-
//
|
|
1197
|
-
//
|
|
1198
|
-
//
|
|
1199
|
-
//
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
log('error', `Worktree -b retry also failed for ${branchName}: ${e1b.message?.split('\n')[0]}`);
|
|
1223
|
-
throw e1b;
|
|
1224
|
-
}
|
|
1225
|
-
} else {
|
|
1226
|
-
// Branch already exists — try checkout without -b
|
|
1227
|
-
try { await shared.shellSafeGit(['fetch', 'origin', branchName], { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
1228
|
-
try {
|
|
1229
|
-
await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, worktreeCreateRetries);
|
|
1230
|
-
log('info', `Reusing existing branch: ${branchName}`);
|
|
1231
|
-
} catch (e2) {
|
|
1232
|
-
// "already checked out" or "already used by worktree" — find and reuse or recover
|
|
1233
|
-
const alreadyUsed = e2.message?.includes('already checked out') || e2.message?.includes('already used by worktree')
|
|
1234
|
-
|| e1.message?.includes('already checked out') || e1.message?.includes('already used by worktree');
|
|
1235
|
-
if (alreadyUsed) {
|
|
1236
|
-
const existingWtPath = await findExistingWorktree(rootDir, branchName);
|
|
1237
|
-
if (existingWtPath && fs.existsSync(existingWtPath)) {
|
|
1238
|
-
// Bug fix: read dispatch under file lock so check-and-act is atomic
|
|
1239
|
-
let activelyUsed = false;
|
|
1240
|
-
mutateDispatch((dp) => {
|
|
1241
|
-
activelyUsed = (dp.active || []).some(d => {
|
|
1242
|
-
const dBranch = d.meta?.branch ? sanitizeBranch(d.meta.branch) : '';
|
|
1243
|
-
return dBranch === branchName && d.id !== id;
|
|
1244
|
-
});
|
|
1245
|
-
return dp;
|
|
1220
|
+
|
|
1221
|
+
// W-mphnm6a1000281b8: probe whether origin already has this branch.
|
|
1222
|
+
// If yes, prefer `git worktree add <path> <branch>` (checkout +
|
|
1223
|
+
// local-track origin/<branch>) over `-b <branch> origin/<mainRef>`.
|
|
1224
|
+
// The shared-branch path (~line 1158) already does this. PR-targeted
|
|
1225
|
+
// fix/review/test/verify dispatches hit this path with branchName =
|
|
1226
|
+
// the PR's source branch; branching off main when the PR branch is
|
|
1227
|
+
// N commits ahead upstream guarantees the stale-HEAD guard
|
|
1228
|
+
// (~line 1777) trips and the dispatch errors. Live repro:
|
|
1229
|
+
// opg-microsoft/minions PR #57 (sync/yemi33-master, 109 commits
|
|
1230
|
+
// ahead of main) — two consecutive fix dispatches errored on
|
|
1231
|
+
// STALE_HEAD over 10 min and the engine then starved the PR for
|
|
1232
|
+
// the cooldown window (60 min effective).
|
|
1233
|
+
const _branchOnRemote = await probeBranchOnRemote(rootDir, branchName, _gitOpts);
|
|
1234
|
+
|
|
1235
|
+
if (_branchOnRemote) {
|
|
1236
|
+
// Mirror shared-branch fetch+add (~line 1157-1159).
|
|
1237
|
+
log('info', `origin/${branchName} exists — checking out remote branch instead of -b from ${mainRef}`);
|
|
1238
|
+
try { await shared.shellSafeGit(['fetch', 'origin', branchName], { ..._gitOpts, cwd: rootDir, timeout: 30000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
1239
|
+
try {
|
|
1240
|
+
await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, worktreeCreateRetries);
|
|
1241
|
+
} catch (eRemote) {
|
|
1242
|
+
const alreadyUsed = eRemote.message?.includes('already used by worktree') || eRemote.message?.includes('already checked out');
|
|
1243
|
+
if (alreadyUsed) {
|
|
1244
|
+
const existingWtPath = await findExistingWorktree(rootDir, branchName);
|
|
1245
|
+
if (existingWtPath && fs.existsSync(existingWtPath)) {
|
|
1246
|
+
let activelyUsed = false;
|
|
1247
|
+
mutateDispatch((dp) => {
|
|
1248
|
+
activelyUsed = (dp.active || []).some(d => {
|
|
1249
|
+
const dBranch = d.meta?.branch ? sanitizeBranch(d.meta.branch) : '';
|
|
1250
|
+
return dBranch === branchName && d.id !== id;
|
|
1246
1251
|
});
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1252
|
+
return dp;
|
|
1253
|
+
});
|
|
1254
|
+
if (activelyUsed) {
|
|
1255
|
+
log('warn', `Branch ${branchName} actively used by another agent at ${existingWtPath} — cannot create worktree`);
|
|
1256
|
+
throw eRemote;
|
|
1257
|
+
}
|
|
1258
|
+
try { shared.assertWorktreeOutsideProject(existingWtPath, rootDir); }
|
|
1259
|
+
catch (assertErr) {
|
|
1260
|
+
if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
|
|
1261
|
+
throw assertErr;
|
|
1262
|
+
}
|
|
1263
|
+
log('info', `Branch ${branchName} already checked out at ${existingWtPath} — reusing`);
|
|
1264
|
+
worktreePath = existingWtPath;
|
|
1265
|
+
} else {
|
|
1266
|
+
const pruned = await pruneStaleWorktreeForBranch(rootDir, branchName, _gitOpts);
|
|
1267
|
+
if (pruned > 0) {
|
|
1268
|
+
log('info', `Pruned ${pruned} stale worktree entry(ies) for ${branchName}; retrying worktree add`);
|
|
1269
|
+
await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, 0);
|
|
1270
|
+
} else { throw eRemote; }
|
|
1271
|
+
}
|
|
1272
|
+
} else { throw eRemote; }
|
|
1273
|
+
}
|
|
1274
|
+
} else {
|
|
1275
|
+
// Branch isn't on origin (or probe failed) — start a fresh local
|
|
1276
|
+
// branch from origin/<mainRef>. This is the dominant path for new
|
|
1277
|
+
// implement/decompose dispatches.
|
|
1278
|
+
// W-mph6n4p00006ce38: mirror the pool-borrow path (~line 1110-1114)
|
|
1279
|
+
// — fetch fresh origin/<mainRef> and start the new branch off it,
|
|
1280
|
+
// not the local ref. Without this, fresh-create dispatches inherit
|
|
1281
|
+
// whatever stale local master the engine clone happens to be on
|
|
1282
|
+
// (most painful: long-lived engine processes between restarts).
|
|
1283
|
+
// Non-fatal: if the fetch fails (network blip, transient auth),
|
|
1284
|
+
// fall back to local mainRef so the dispatch still progresses;
|
|
1285
|
+
// the dep-merge phase's own fetch + the on-failure
|
|
1286
|
+
// `git reset --hard origin/<mainRef>` recovery remain as safety nets.
|
|
1287
|
+
let _freshCreateBase = mainRef;
|
|
1288
|
+
try {
|
|
1289
|
+
await shared.shellSafeGit(['fetch', 'origin', mainRef], { ..._gitOpts, cwd: rootDir, timeout: 30000 });
|
|
1290
|
+
_freshCreateBase = `origin/${mainRef}`;
|
|
1291
|
+
} catch (mainFetchErr) {
|
|
1292
|
+
log('warn', `Failed to fetch origin/${mainRef} before fresh-create worktree for ${branchName}: ${mainFetchErr.message} — falling back to local ${mainRef}`);
|
|
1293
|
+
}
|
|
1294
|
+
try {
|
|
1295
|
+
await runWorktreeAdd(rootDir, worktreePath, ['-b', branchName, _freshCreateBase], _worktreeGitOpts, worktreeCreateRetries);
|
|
1296
|
+
} catch (e1) {
|
|
1297
|
+
const branchExists = e1.message?.includes('already exists');
|
|
1298
|
+
log('warn', `Worktree -b failed for ${branchName}: ${e1.message?.split('\n')[0]}`);
|
|
1299
|
+
if (!branchExists) {
|
|
1300
|
+
// Transient error (lock, timeout) — prune, clean, and retry -b once more
|
|
1301
|
+
log('info', `Retrying -b create after prune for ${branchName}`);
|
|
1302
|
+
try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 15000 }); } catch { /* optional */ }
|
|
1303
|
+
removeStaleIndexLock(rootDir);
|
|
1304
|
+
// Clean up partial worktree directory from failed attempt
|
|
1305
|
+
try { if (fs.existsSync(worktreePath)) fs.rmSync(worktreePath, { recursive: true, force: true }); } catch { /* optional */ }
|
|
1306
|
+
try {
|
|
1307
|
+
await runWorktreeAdd(rootDir, worktreePath, ['-b', branchName, _freshCreateBase], _worktreeGitOpts, 0);
|
|
1308
|
+
} catch (e1b) {
|
|
1309
|
+
log('error', `Worktree -b retry also failed for ${branchName}: ${e1b.message?.split('\n')[0]}`);
|
|
1310
|
+
throw e1b;
|
|
1311
|
+
}
|
|
1312
|
+
} else {
|
|
1313
|
+
// Branch already exists — try checkout without -b
|
|
1314
|
+
try { await shared.shellSafeGit(['fetch', 'origin', branchName], { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
1315
|
+
try {
|
|
1316
|
+
await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, worktreeCreateRetries);
|
|
1317
|
+
log('info', `Reusing existing branch: ${branchName}`);
|
|
1318
|
+
} catch (e2) {
|
|
1319
|
+
// "already checked out" or "already used by worktree" — find and reuse or recover
|
|
1320
|
+
const alreadyUsed = e2.message?.includes('already checked out') || e2.message?.includes('already used by worktree')
|
|
1321
|
+
|| e1.message?.includes('already checked out') || e1.message?.includes('already used by worktree');
|
|
1322
|
+
if (alreadyUsed) {
|
|
1323
|
+
const existingWtPath = await findExistingWorktree(rootDir, branchName);
|
|
1324
|
+
if (existingWtPath && fs.existsSync(existingWtPath)) {
|
|
1325
|
+
// Bug fix: read dispatch under file lock so check-and-act is atomic
|
|
1326
|
+
let activelyUsed = false;
|
|
1327
|
+
mutateDispatch((dp) => {
|
|
1328
|
+
activelyUsed = (dp.active || []).some(d => {
|
|
1329
|
+
const dBranch = d.meta?.branch ? sanitizeBranch(d.meta.branch) : '';
|
|
1330
|
+
return dBranch === branchName && d.id !== id;
|
|
1331
|
+
});
|
|
1332
|
+
return dp;
|
|
1333
|
+
});
|
|
1334
|
+
if (activelyUsed) {
|
|
1335
|
+
log('warn', `Branch ${branchName} actively used by another agent at ${existingWtPath} — cannot create worktree`);
|
|
1336
|
+
throw e2;
|
|
1337
|
+
}
|
|
1338
|
+
try { shared.assertWorktreeOutsideProject(existingWtPath, rootDir); }
|
|
1339
|
+
catch (assertErr) {
|
|
1340
|
+
if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
|
|
1341
|
+
throw assertErr;
|
|
1342
|
+
}
|
|
1343
|
+
log('info', `Branch ${branchName} already checked out at ${existingWtPath} — reusing`);
|
|
1344
|
+
worktreePath = existingWtPath;
|
|
1345
|
+
} else if (existingWtPath && !fs.existsSync(existingWtPath)) {
|
|
1346
|
+
log('warn', `Branch ${branchName} tracked in missing dir ${existingWtPath} — pruning and recreating`);
|
|
1347
|
+
try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
1348
|
+
await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, worktreeCreateRetries);
|
|
1349
|
+
log('info', `Recovered worktree for ${branchName} after stale entry prune`);
|
|
1350
|
+
} else {
|
|
1351
|
+
try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
1352
|
+
await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, worktreeCreateRetries);
|
|
1255
1353
|
}
|
|
1256
|
-
log('info', `Branch ${branchName} already checked out at ${existingWtPath} — reusing`);
|
|
1257
|
-
worktreePath = existingWtPath;
|
|
1258
|
-
} else if (existingWtPath && !fs.existsSync(existingWtPath)) {
|
|
1259
|
-
log('warn', `Branch ${branchName} tracked in missing dir ${existingWtPath} — pruning and recreating`);
|
|
1260
|
-
try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
1261
|
-
await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, worktreeCreateRetries);
|
|
1262
|
-
log('info', `Recovered worktree for ${branchName} after stale entry prune`);
|
|
1263
1354
|
} else {
|
|
1264
|
-
|
|
1265
|
-
await runWorktreeAdd(rootDir, worktreePath, [branchName], _worktreeGitOpts, worktreeCreateRetries);
|
|
1355
|
+
throw e2;
|
|
1266
1356
|
}
|
|
1267
|
-
} else {
|
|
1268
|
-
throw e2;
|
|
1269
1357
|
}
|
|
1270
1358
|
}
|
|
1271
1359
|
}
|
|
@@ -4063,6 +4151,18 @@ function ensurePrBranchForDispatch(project, pr, automationType) {
|
|
|
4063
4151
|
}
|
|
4064
4152
|
return branch;
|
|
4065
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
|
+
}
|
|
4066
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.`;
|
|
4067
4167
|
if (updatePrBranchResolutionState(project, pr, { reason })) {
|
|
4068
4168
|
log('warn', `PR ${pr.id}: ${reason}`);
|
|
@@ -4070,6 +4170,21 @@ function ensurePrBranchForDispatch(project, pr, automationType) {
|
|
|
4070
4170
|
return '';
|
|
4071
4171
|
}
|
|
4072
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
|
+
|
|
4073
4188
|
function prCausePart(value, fallback = 'unknown') {
|
|
4074
4189
|
const raw = String(value || '').trim();
|
|
4075
4190
|
return shared.safeSlugComponent(raw || fallback, 80);
|
|
@@ -4502,29 +4617,48 @@ async function discoverFromPrs(config, project) {
|
|
|
4502
4617
|
&& !isPrNoOpFixCauseSuppressed(pr, shared.PR_FIX_CAUSE.REVIEW_FEEDBACK)) {
|
|
4503
4618
|
const reviewCauseKey = getPrAutomationCauseKey('review-feedback', pr);
|
|
4504
4619
|
const key = getPrAutomationDispatchKey(`fix-${project?.name || 'default'}-${prDisplayId}`, reviewCauseKey);
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4620
|
+
// W-mphnm6a1000281b8: cause-local skip mirroring the human-feedback
|
|
4621
|
+
// (#2632) and build-failure (#2632 audit) guards above. A previous bare
|
|
4622
|
+
// `continue` here aborted the entire PR iteration when the
|
|
4623
|
+
// review-feedback dispatch was throttled, on cooldown, or already
|
|
4624
|
+
// dispatched — starving the build-fix and conflict-fix blocks below
|
|
4625
|
+
// even though their own dedupe keys had not been hit. Live repro on
|
|
4626
|
+
// opg-microsoft/minions PR #57: two STALE_HEAD-errored review-feedback
|
|
4627
|
+
// dispatches stamped a 60-min effective cooldown, and the PR received
|
|
4628
|
+
// zero build-fix dispatches for the rest of the cooldown window even
|
|
4629
|
+
// though buildStatus stayed `failing` and the build-fix key was clean.
|
|
4630
|
+
// Skip ONLY this cause; let iteration fall through to downstream
|
|
4631
|
+
// blocks (re-review already ran above; build-fix + conflict-fix run
|
|
4632
|
+
// below).
|
|
4633
|
+
const skipReviewFeedback =
|
|
4634
|
+
isPrAutomationCauseHandledOrPending(project, pr, reviewCauseKey)
|
|
4635
|
+
|| fixThrottled
|
|
4636
|
+
|| isAlreadyDispatched(key)
|
|
4637
|
+
|| isOnCooldown(key, cooldownMs);
|
|
4638
|
+
if (!skipReviewFeedback) {
|
|
4639
|
+
const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
|
|
4640
|
+
if (agentId) {
|
|
4641
|
+
const prBranch = ensurePrBranchForDispatch(project, pr, 'fix');
|
|
4642
|
+
if (prBranch) {
|
|
4643
|
+
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
4644
|
+
pr_id: pr.id, pr_branch: prBranch,
|
|
4645
|
+
review_note: pr.minionsReview?.note || pr.reviewNote || 'See PR thread comments',
|
|
4646
|
+
}, `Fix ${pr.id}: ${pr.title || ''} — review feedback`, {
|
|
4647
|
+
dispatchKey: key, cooldownKey: key, automationCauseKey: reviewCauseKey, source: 'pr', pr, branch: prBranch, project: projMeta,
|
|
4648
|
+
// W-mpg58wv3 — closure-loop binding. Carries the originating minion review
|
|
4649
|
+
// WI id (and any ADO thread ids it cited) onto the fix WI so the
|
|
4650
|
+
// post-completion path in lifecycle.js can auto-dispatch a re-review
|
|
4651
|
+
// against the same PR. Both fields fall through to null/[] when the
|
|
4652
|
+
// upstream review didn't expose them.
|
|
4653
|
+
addresses_review_wi: pr.minionsReview?.sourceItem || null,
|
|
4654
|
+
addresses_threads: Array.isArray(pr.minionsReview?.threads) ? pr.minionsReview.threads.slice() : [],
|
|
4655
|
+
});
|
|
4656
|
+
if (item) {
|
|
4657
|
+
newWork.push(item); fixDispatched = true;
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
} // end if (!skipReviewFeedback) — cause-local guard for W-mphnm6a1000281b8
|
|
4528
4662
|
}
|
|
4529
4663
|
|
|
4530
4664
|
// PRs with build failures — route to author (has session context from implementing)
|
|
@@ -6872,9 +7006,11 @@ module.exports = {
|
|
|
6872
7006
|
isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, assertCleanSharedWorktree, // exported for testing
|
|
6873
7007
|
pruneStaleWorktreeForBranch, // exported for testing
|
|
6874
7008
|
findExistingWorktree, // exported for testing
|
|
7009
|
+
probeBranchOnRemote, // exported for testing (W-mphnm6a1000281b8)
|
|
6875
7010
|
_maxTurnsForType, buildProjectContext, normalizeAc, _buildAgentSpawnFlags, _classifyAgentFailure, // exported for testing
|
|
6876
7011
|
promoteCheckpointSteeringForClose, // exported for testing
|
|
6877
7012
|
normalizePrBranch, resolvePrBranch, prCausePart, getPrCauseHead, getPrCauseBase, getPrAutomationCauseKey, getPrAutomationDispatchKey, // exported for testing
|
|
7013
|
+
ensurePrBranchForDispatch, isWithinLinkGraceWindow, PR_LINK_GRACE_WINDOW_MS, // exported for testing (W-mphm0kt0000cebc3)
|
|
6878
7014
|
|
|
6879
7015
|
// Playbooks
|
|
6880
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"
|