@yemi33/minions 0.1.2440 → 0.1.2442
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/settings.js +2 -2
- package/dashboard/slim/body.html +11 -11
- package/dashboard/slim/js/knowledge.js +92 -459
- package/dashboard/slim/styles.css +10 -47
- package/docs/auto-discovery.md +5 -5
- package/docs/pr-auto-fix-dispatch.md +2 -2
- package/engine/ado.js +70 -3
- package/engine/shared.js +6 -2
- package/engine.js +91 -22
- package/package.json +1 -1
package/dashboard/js/settings.js
CHANGED
|
@@ -503,7 +503,7 @@ async function openSettings() {
|
|
|
503
503
|
'<h3>Polling</h3>' +
|
|
504
504
|
'<div class="settings-pane-sub">Cadence for fetching PR build status, votes, and comments from the platforms. Disabling a provider here turns the matching Auto-fix gates into no-ops. The <em>Pause All Polling</em> kill-switch below overrides both provider toggles and inerts every PR auto-dispatch gate (Auto-fix Builds / Conflicts / Review / etc.) until cleared.</div>' +
|
|
505
505
|
'<div class="settings-stack" style="margin-bottom:16px">' +
|
|
506
|
-
settingsToggle('🛑 Pause ALL polling', 'set-pollingPaused', !!e.pollingPaused, 'Halts every PR poll
|
|
506
|
+
settingsToggle('🛑 Pause ALL polling', 'set-pollingPaused', !!e.pollingPaused, 'Halts every PR status/comment poll AND provider reconciliation (ADO + GitHub + shared-branch) on the next tick, and inerts every PR-derived auto-dispatch gate. Reversible without restart. Use when hitting API rate limits.') +
|
|
507
507
|
'</div>' +
|
|
508
508
|
'<div class="settings-pane-sub" style="margin-bottom:8px">Legacy bundle toggles — granular controls below override these when set.</div>' +
|
|
509
509
|
'<div class="settings-stack" style="margin-bottom:12px">' +
|
|
@@ -511,7 +511,7 @@ async function openSettings() {
|
|
|
511
511
|
settingsToggle('GitHub Polling', 'set-ghPollEnabled', e.ghPollEnabled !== false, 'Legacy bundle macro — when OFF, silences all three GitHub axes (status, comments, reconcile) and GitHub PR dispatch gates are inert when this is off. Per-axis flags below take priority when explicitly set. Keep ON unless you want a one-knob GitHub kill.') +
|
|
512
512
|
'</div>' +
|
|
513
513
|
'<details class="settings-collapsible" style="margin-bottom:12px"><summary>Granular per-poller controls (P-c4d8e1a3)</summary>' +
|
|
514
|
-
'<div class="settings-pane-sub" style="margin-top:8px">Override individual ADO/GitHub poll axes + the pending-rebase processor. Explicit values here win; if unset, the legacy ADO/GitHub Polling macros above apply; otherwise the default is ON. <strong>Status</strong>
|
|
514
|
+
'<div class="settings-pane-sub" style="margin-top:8px">Override individual ADO/GitHub poll axes + the pending-rebase processor. Explicit values here win; if unset, the legacy ADO/GitHub Polling macros above apply; otherwise the default is ON. <strong>Status</strong>, <strong>Comments</strong> and <strong>Reconcile</strong> all honor the <em>Pause All Polling</em> kill-switch above (reconcile included since W-ms5eljc60090f0d5 — it issues provider API calls); only <strong>Process Pending Rebases</strong> ignores it (by design — it makes no provider poll call).</div>' +
|
|
515
515
|
'<div class="settings-stack" style="margin-top:8px">' +
|
|
516
516
|
settingsToggle('ADO PR Status Poll', 'set-adoPrStatusPollEnabled', e.adoPrStatusPollEnabled !== false, 'Granular: ADO PR build/merge/review status poll (section 2.6). Default ON. When OFF, ADO PR status will not refresh, but reconcile + comments still run (unless their own granular flag is OFF).') +
|
|
517
517
|
settingsToggle('ADO PR Comments Poll', 'set-adoPrCommentsPollEnabled', e.adoPrCommentsPollEnabled !== false, 'Granular: ADO PR human-comments poll (section 2.7). Default ON. When OFF, ADO PR comments will not surface — auto-fix-human-comments still composes against this flag for ADO.') +
|
package/dashboard/slim/body.html
CHANGED
|
@@ -202,24 +202,24 @@
|
|
|
202
202
|
</div>
|
|
203
203
|
</div>
|
|
204
204
|
|
|
205
|
-
<!-- Knowledge control panel — opened from the "Knowledge" status tile.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
205
|
+
<!-- Knowledge control panel — opened from the "Knowledge" status tile. Two
|
|
206
|
+
tabs: Pinned Context (slim-native, via pinned.js — there is no standalone
|
|
207
|
+
classic route for pinned, it lives on /home) and Notes & Knowledge, which
|
|
208
|
+
embeds the LITERAL classic /inbox screen (Notes Inbox + Team Notes +
|
|
209
|
+
Knowledge Base + memory search) in a chrome-off /inbox?embed=1 iframe
|
|
210
|
+
rather than a slim-native fork (CLAUDE.md "Slim UX vs Classic UX — reuse,
|
|
211
|
+
don't fork"). Rendered lazily by dashboard/slim/js/knowledge.js into
|
|
212
|
+
#slim-knowledge-body. Backed by the existing endpoints (no new routes):
|
|
210
213
|
Pinned — /api/pinned (+ update/remove), surfaced via /api/status.
|
|
211
|
-
Notes
|
|
212
|
-
/api/
|
|
213
|
-
KB — /api/knowledge (list), /api/knowledge/:cat/:file (read),
|
|
214
|
-
/api/knowledge POST (create), /api/kb-pins(+/toggle). -->
|
|
214
|
+
Notes & Knowledge — the classic /inbox page (/api/notes*, /api/knowledge,
|
|
215
|
+
/api/kb-pins, memory search) inside the embedded iframe. -->
|
|
215
216
|
<div class="modal-bg" id="slim-knowledge-modal">
|
|
216
217
|
<div class="modal slim-knowledge-modal-inner">
|
|
217
218
|
<div class="modal-header">
|
|
218
219
|
<h3>Knowledge</h3>
|
|
219
220
|
<div class="kn-tabs" id="slim-kn-tabs" role="tablist">
|
|
220
221
|
<button class="kn-tab active" id="slim-kn-tab-pinned" data-kn-tab="pinned" type="button" role="tab" aria-selected="true" aria-controls="slim-knowledge-body">Pinned Context</button>
|
|
221
|
-
<button class="kn-tab" id="slim-kn-tab-
|
|
222
|
-
<button class="kn-tab" id="slim-kn-tab-kb" data-kn-tab="kb" type="button" role="tab" aria-selected="false" aria-controls="slim-knowledge-body">KB</button>
|
|
222
|
+
<button class="kn-tab" id="slim-kn-tab-inbox" data-kn-tab="inbox" type="button" role="tab" aria-selected="false" aria-controls="slim-knowledge-body">Notes & Knowledge</button>
|
|
223
223
|
</div>
|
|
224
224
|
<button id="slim-knowledge-close" class="icon-btn" title="Close" style="margin-left:auto">×</button>
|
|
225
225
|
</div>
|
|
@@ -1,34 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
// ── Knowledge control panel (Pinned Context + Notes
|
|
3
|
-
// One
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// Notes
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
2
|
+
// ── Knowledge control panel (Pinned Context + Notes & Knowledge) ────
|
|
3
|
+
// One cockpit box over the knowledge surfaces, replacing the old single
|
|
4
|
+
// "Pinned context" tile. Two tabs:
|
|
5
|
+
// Pinned — slim-native (there is no standalone classic route for pinned;
|
|
6
|
+
// it lives on /home). Reuses the editor + list renderer in
|
|
7
|
+
// pinned.js, backed by /api/pinned (+ update/remove).
|
|
8
|
+
// Notes & Knowledge — embeds the LITERAL classic /inbox screen (Notes
|
|
9
|
+
// Inbox + Team Notes + Knowledge Base + memory search) in a
|
|
10
|
+
// chrome-off /inbox?embed=1 iframe, per CLAUDE.md "Slim UX vs
|
|
11
|
+
// Classic UX — reuse, don't fork". Zero forked rendering logic.
|
|
12
12
|
//
|
|
13
13
|
// Counts: pinned is live from the 5s status poll (renderKnowledgeTile is fed
|
|
14
14
|
// by applyStatus). Notes + KB counts are fetched lazily — once shortly after
|
|
15
|
-
// load and again
|
|
15
|
+
// load and again on modal open — so the heavier /api/notes-full and
|
|
16
16
|
// /api/knowledge reads stay off the recurring poll.
|
|
17
17
|
|
|
18
18
|
var _knowledgeCounts = { pinned: 0, notes: null, kb: null };
|
|
19
19
|
var _knActiveTab = 'pinned';
|
|
20
|
-
var
|
|
21
|
-
var _kbPins = []; // cached pinned KB keys ('knowledge/<cat>/<file>')
|
|
22
|
-
|
|
23
|
-
var KB_CAT_LABELS = {
|
|
24
|
-
architecture: 'Architecture', conventions: 'Conventions',
|
|
25
|
-
'project-notes': 'Project Notes', 'build-reports': 'Build Reports',
|
|
26
|
-
reviews: 'Reviews', learnings: 'Learnings', decisions: 'Decisions',
|
|
27
|
-
incidents: 'Incidents', 'api-notes': 'API Notes',
|
|
28
|
-
};
|
|
29
|
-
// Categories offered in the KB create form (mirrors the classic dashboard).
|
|
30
|
-
var KB_CREATE_CATS = ['architecture', 'conventions', 'project-notes', 'build-reports', 'reviews'];
|
|
31
|
-
var KB_LIST_CAP = 60; // cap rendered rows; the KB can hold thousands
|
|
20
|
+
var _knInboxFrame = null; // cached /inbox?embed=1 iframe (created once)
|
|
32
21
|
|
|
33
22
|
// ── Cockpit tile ───────────────────────────────────────────────────
|
|
34
23
|
// Called from applyStatus with the live pinned count each poll, and from the
|
|
@@ -68,35 +57,24 @@
|
|
|
68
57
|
try {
|
|
69
58
|
var kres = await fetch('/api/knowledge', { headers: { 'Accept': 'application/json' } });
|
|
70
59
|
if (kres.ok) {
|
|
71
|
-
|
|
72
|
-
_knowledgeCounts.kb = _countKbEntries(_kbData);
|
|
60
|
+
_knowledgeCounts.kb = _countKbEntries(await kres.json());
|
|
73
61
|
}
|
|
74
62
|
} catch (e) { /* keep last-known */ }
|
|
75
63
|
renderKnowledgeTile();
|
|
76
64
|
}
|
|
77
65
|
|
|
78
|
-
async function loadKbPins() {
|
|
79
|
-
try {
|
|
80
|
-
var res = await fetch('/api/kb-pins', { headers: { 'Accept': 'application/json' } });
|
|
81
|
-
if (!res.ok) return;
|
|
82
|
-
var d = await res.json();
|
|
83
|
-
_kbPins = Array.isArray(d.pins) ? d.pins : [];
|
|
84
|
-
} catch (e) { _kbPins = _kbPins || []; }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function kbPinKey(cat, file) { return 'knowledge/' + cat + '/' + file; }
|
|
88
|
-
function isKbPinned(key) { return _kbPins.indexOf(key) !== -1; }
|
|
89
|
-
|
|
90
66
|
// ── Modal open/close + tab switching ───────────────────────────────
|
|
91
67
|
function openKnowledgeModal() {
|
|
92
68
|
var modal = document.getElementById('slim-knowledge-modal');
|
|
93
69
|
if (!modal) return;
|
|
94
|
-
renderKnowledgeTab();
|
|
70
|
+
renderKnowledgeTab(); // posts visibility=true to the embed if the inbox tab is active
|
|
95
71
|
modal.classList.add('open');
|
|
96
72
|
}
|
|
97
73
|
function closeKnowledgeModal() {
|
|
98
74
|
var modal = document.getElementById('slim-knowledge-modal');
|
|
99
75
|
if (modal) modal.classList.remove('open');
|
|
76
|
+
// Suspend the embedded /inbox poll while the modal is closed.
|
|
77
|
+
_postKnEmbedVisibility(false);
|
|
100
78
|
}
|
|
101
79
|
function setKnowledgeTab(tab) {
|
|
102
80
|
_knActiveTab = tab;
|
|
@@ -112,13 +90,81 @@
|
|
|
112
90
|
if (body && activeTabId) body.setAttribute('aria-labelledby', activeTabId);
|
|
113
91
|
renderKnowledgeTab();
|
|
114
92
|
}
|
|
93
|
+
// Tab routing. The Pinned tab renders slim-native content into a transient
|
|
94
|
+
// container; the Notes & Knowledge tab shows a cached /inbox?embed=1 iframe
|
|
95
|
+
// (created once, kept mounted so a tab switch never reloads it).
|
|
115
96
|
function renderKnowledgeTab() {
|
|
97
|
+
var c = _ensureKnContainers();
|
|
98
|
+
if (!c) return;
|
|
99
|
+
if (_knActiveTab === 'inbox') {
|
|
100
|
+
renderKnowledgeInboxTab();
|
|
101
|
+
} else {
|
|
102
|
+
c.host.hidden = true;
|
|
103
|
+
_postKnEmbedVisibility(false);
|
|
104
|
+
c.transient.textContent = '';
|
|
105
|
+
renderKnowledgePinnedTab(c.transient);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Persistent containers inside #slim-knowledge-body: an embed host that holds
|
|
110
|
+
// the cached iframe (never wiped, so the iframe is never re-parented/reloaded)
|
|
111
|
+
// and a transient container for the Pinned tab's slim-native content. Built
|
|
112
|
+
// with createElement (no innerHTML) to satisfy the no-unsanitized lint gate.
|
|
113
|
+
function _ensureKnContainers() {
|
|
116
114
|
var body = document.getElementById('slim-knowledge-body');
|
|
117
|
-
if (!body) return;
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
if (!body) return null;
|
|
116
|
+
var host = document.getElementById('slim-kn-embed-host');
|
|
117
|
+
if (!host) {
|
|
118
|
+
host = document.createElement('div');
|
|
119
|
+
host.id = 'slim-kn-embed-host';
|
|
120
|
+
host.className = 'slim-kn-embed-host';
|
|
121
|
+
host.hidden = true;
|
|
122
|
+
body.appendChild(host);
|
|
123
|
+
}
|
|
124
|
+
var transient = document.getElementById('slim-kn-transient');
|
|
125
|
+
if (!transient) {
|
|
126
|
+
transient = document.createElement('div');
|
|
127
|
+
transient.id = 'slim-kn-transient';
|
|
128
|
+
transient.className = 'slim-kn-transient';
|
|
129
|
+
body.appendChild(transient);
|
|
130
|
+
}
|
|
131
|
+
return { host: host, transient: transient };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Tell the embedded classic /inbox SPA whether its iframe is visible so it can
|
|
135
|
+
// suspend/resume its own /api/status poll (dashboard/js/refresh.js listens for
|
|
136
|
+
// this exact message shape). Same-origin, guarded defensively.
|
|
137
|
+
function _postKnEmbedVisibility(visible) {
|
|
138
|
+
try {
|
|
139
|
+
if (_knInboxFrame && _knInboxFrame.contentWindow) {
|
|
140
|
+
_knInboxFrame.contentWindow.postMessage(
|
|
141
|
+
{ source: 'minions-slim-embed', type: 'visibility', visible: !!visible }, '*');
|
|
142
|
+
}
|
|
143
|
+
} catch (e) { /* defensive — same-origin means this should never throw */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Tab: Notes & Knowledge (classic /inbox embed) ──────────────────
|
|
147
|
+
// Lazily creates ONE cached iframe pointing at the classic Notes+KB screen
|
|
148
|
+
// (/inbox?embed=1) and mounts it into the persistent embed host. Subsequent
|
|
149
|
+
// activations just re-show the same iframe (never recreated), so switching
|
|
150
|
+
// tabs never reloads it.
|
|
151
|
+
function renderKnowledgeInboxTab() {
|
|
152
|
+
var c = _ensureKnContainers();
|
|
153
|
+
if (!c) return;
|
|
154
|
+
c.transient.textContent = '';
|
|
155
|
+
if (!_knInboxFrame) {
|
|
156
|
+
var frame = document.createElement('iframe');
|
|
157
|
+
frame.className = 'slim-kn-embed';
|
|
158
|
+
frame.src = '/inbox?embed=1';
|
|
159
|
+
frame.title = 'Notes & Knowledge';
|
|
160
|
+
frame.addEventListener('load', function() {
|
|
161
|
+
_postKnEmbedVisibility(_knActiveTab === 'inbox');
|
|
162
|
+
});
|
|
163
|
+
c.host.appendChild(frame);
|
|
164
|
+
_knInboxFrame = frame;
|
|
165
|
+
}
|
|
166
|
+
c.host.hidden = false;
|
|
167
|
+
_postKnEmbedVisibility(true);
|
|
122
168
|
}
|
|
123
169
|
|
|
124
170
|
// ── Tab: Pinned Context ────────────────────────────────────────────
|
|
@@ -143,423 +189,10 @@
|
|
|
143
189
|
renderSlimPinnedList(); // defined in pinned.js; renders into #slim-pinned-list
|
|
144
190
|
}
|
|
145
191
|
|
|
146
|
-
// ── Tab: Notes ─────────────────────────────────────────────────────
|
|
147
|
-
function renderKnowledgeNotesTab(body) {
|
|
148
|
-
var intro = document.createElement('p');
|
|
149
|
-
intro.textContent = 'Consolidated team notes (notes.md). Edit and save, or add a note for the next consolidation sweep.';
|
|
150
|
-
body.appendChild(intro);
|
|
151
|
-
|
|
152
|
-
var msg = document.createElement('div');
|
|
153
|
-
msg.id = 'slim-notes-msg';
|
|
154
|
-
msg.className = 'kn-msg';
|
|
155
|
-
body.appendChild(msg);
|
|
156
|
-
|
|
157
|
-
var ta = document.createElement('textarea');
|
|
158
|
-
ta.id = 'slim-notes-content';
|
|
159
|
-
ta.className = 'linkpr-input kn-notes-textarea';
|
|
160
|
-
ta.rows = 12;
|
|
161
|
-
ta.value = 'Loading notes…';
|
|
162
|
-
ta.disabled = true;
|
|
163
|
-
body.appendChild(ta);
|
|
164
|
-
|
|
165
|
-
var saveRow = document.createElement('div');
|
|
166
|
-
saveRow.className = 'kn-toolbar kn-toolbar-end';
|
|
167
|
-
var saveBtn = document.createElement('button');
|
|
168
|
-
saveBtn.className = 'btn-primary';
|
|
169
|
-
saveBtn.type = 'button';
|
|
170
|
-
saveBtn.textContent = 'Save notes.md';
|
|
171
|
-
saveBtn.disabled = true;
|
|
172
|
-
saveBtn.addEventListener('click', saveKnowledgeNotes);
|
|
173
|
-
saveRow.appendChild(saveBtn);
|
|
174
|
-
body.appendChild(saveRow);
|
|
175
|
-
|
|
176
|
-
// Add-a-note (inbox) sub-form.
|
|
177
|
-
var addHead = document.createElement('div');
|
|
178
|
-
addHead.className = 'kn-section-head';
|
|
179
|
-
addHead.textContent = 'Add a note for consolidation';
|
|
180
|
-
body.appendChild(addHead);
|
|
181
|
-
|
|
182
|
-
var titleLabel = document.createElement('label');
|
|
183
|
-
titleLabel.className = 'linkpr-label';
|
|
184
|
-
titleLabel.textContent = 'Title';
|
|
185
|
-
var titleInput = document.createElement('input');
|
|
186
|
-
titleInput.id = 'slim-note-title';
|
|
187
|
-
titleInput.className = 'linkpr-input';
|
|
188
|
-
titleInput.type = 'text';
|
|
189
|
-
titleInput.placeholder = 'Short summary';
|
|
190
|
-
titleLabel.appendChild(titleInput);
|
|
191
|
-
body.appendChild(titleLabel);
|
|
192
|
-
|
|
193
|
-
var whatLabel = document.createElement('label');
|
|
194
|
-
whatLabel.className = 'linkpr-label';
|
|
195
|
-
whatLabel.textContent = 'Note';
|
|
196
|
-
var whatInput = document.createElement('textarea');
|
|
197
|
-
whatInput.id = 'slim-note-what';
|
|
198
|
-
whatInput.className = 'linkpr-input';
|
|
199
|
-
whatInput.rows = 3;
|
|
200
|
-
whatInput.placeholder = 'What did you learn / decide?';
|
|
201
|
-
whatLabel.appendChild(whatInput);
|
|
202
|
-
body.appendChild(whatLabel);
|
|
203
|
-
|
|
204
|
-
var addRow = document.createElement('div');
|
|
205
|
-
addRow.className = 'kn-toolbar kn-toolbar-end';
|
|
206
|
-
var addNoteMsg = document.createElement('span');
|
|
207
|
-
addNoteMsg.id = 'slim-note-msg';
|
|
208
|
-
addNoteMsg.className = 'kn-msg kn-msg-inline';
|
|
209
|
-
addRow.appendChild(addNoteMsg);
|
|
210
|
-
var addNoteBtn = document.createElement('button');
|
|
211
|
-
addNoteBtn.className = 'btn-secondary';
|
|
212
|
-
addNoteBtn.type = 'button';
|
|
213
|
-
addNoteBtn.textContent = 'Add note';
|
|
214
|
-
addNoteBtn.addEventListener('click', submitKnowledgeNote);
|
|
215
|
-
addRow.appendChild(addNoteBtn);
|
|
216
|
-
body.appendChild(addRow);
|
|
217
|
-
|
|
218
|
-
// Load notes.md into the textarea.
|
|
219
|
-
fetch('/api/notes-full', { headers: { 'Accept': 'text/plain' } })
|
|
220
|
-
.then(function(r) { return r.ok ? r.text() : Promise.reject(new Error('HTTP ' + r.status)); })
|
|
221
|
-
.then(function(text) {
|
|
222
|
-
if (document.getElementById('slim-notes-content') !== ta) return; // tab switched away
|
|
223
|
-
ta.value = (text === 'No notes file found.') ? '' : text;
|
|
224
|
-
ta.disabled = false;
|
|
225
|
-
saveBtn.disabled = false;
|
|
226
|
-
_knowledgeCounts.notes = _countNotesEntries(text);
|
|
227
|
-
renderKnowledgeTile();
|
|
228
|
-
})
|
|
229
|
-
.catch(function(e) {
|
|
230
|
-
ta.value = '';
|
|
231
|
-
ta.disabled = false;
|
|
232
|
-
saveBtn.disabled = false;
|
|
233
|
-
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Could not load notes: ' + (e.message || e); }
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async function saveKnowledgeNotes() {
|
|
238
|
-
var ta = document.getElementById('slim-notes-content');
|
|
239
|
-
var msg = document.getElementById('slim-notes-msg');
|
|
240
|
-
if (!ta) return;
|
|
241
|
-
if (msg) { msg.style.color = 'var(--muted)'; msg.textContent = 'Saving…'; }
|
|
242
|
-
try {
|
|
243
|
-
var res = await fetch('/api/notes-save', {
|
|
244
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
245
|
-
body: JSON.stringify({ content: ta.value, file: 'notes.md' }),
|
|
246
|
-
});
|
|
247
|
-
var d = await res.json().catch(function() { return {}; });
|
|
248
|
-
if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
|
|
249
|
-
if (msg) { msg.style.color = 'var(--green)'; msg.textContent = 'Saved.'; }
|
|
250
|
-
_knowledgeCounts.notes = _countNotesEntries(ta.value);
|
|
251
|
-
renderKnowledgeTile();
|
|
252
|
-
} catch (e) {
|
|
253
|
-
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Error: ' + (e && e.message ? e.message : 'failed'); }
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async function submitKnowledgeNote() {
|
|
258
|
-
var titleEl = document.getElementById('slim-note-title');
|
|
259
|
-
var whatEl = document.getElementById('slim-note-what');
|
|
260
|
-
var msg = document.getElementById('slim-note-msg');
|
|
261
|
-
var title = titleEl ? (titleEl.value || '').trim() : '';
|
|
262
|
-
var what = whatEl ? (whatEl.value || '').trim() : '';
|
|
263
|
-
if (!title) {
|
|
264
|
-
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Title is required.'; }
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
if (msg) { msg.style.color = 'var(--muted)'; msg.textContent = 'Adding…'; }
|
|
268
|
-
try {
|
|
269
|
-
var res = await fetch('/api/notes', {
|
|
270
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
271
|
-
body: JSON.stringify({ title: title, what: what }),
|
|
272
|
-
});
|
|
273
|
-
var d = await res.json().catch(function() { return {}; });
|
|
274
|
-
if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
|
|
275
|
-
if (titleEl) titleEl.value = '';
|
|
276
|
-
if (whatEl) whatEl.value = '';
|
|
277
|
-
if (msg) { msg.style.color = 'var(--green)'; msg.textContent = 'Note added for consolidation.'; }
|
|
278
|
-
} catch (e) {
|
|
279
|
-
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Error: ' + (e && e.message ? e.message : 'failed'); }
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ── Tab: KB ────────────────────────────────────────────────────────
|
|
284
|
-
function renderKnowledgeKbTab(body) {
|
|
285
|
-
var intro = document.createElement('p');
|
|
286
|
-
intro.textContent = 'Knowledge base — categorized entries written by consolidation. Click an entry to read it.';
|
|
287
|
-
body.appendChild(intro);
|
|
288
|
-
|
|
289
|
-
var toolbar = document.createElement('div');
|
|
290
|
-
toolbar.className = 'kn-toolbar';
|
|
291
|
-
var newBtn = document.createElement('button');
|
|
292
|
-
newBtn.className = 'linkpr-chip';
|
|
293
|
-
newBtn.type = 'button';
|
|
294
|
-
newBtn.textContent = '+ New entry';
|
|
295
|
-
newBtn.addEventListener('click', function() { toggleKbCreateForm(body); });
|
|
296
|
-
toolbar.appendChild(newBtn);
|
|
297
|
-
body.appendChild(toolbar);
|
|
298
|
-
|
|
299
|
-
var listMsg = document.createElement('div');
|
|
300
|
-
listMsg.id = 'slim-kb-list-msg';
|
|
301
|
-
listMsg.className = 'kn-msg';
|
|
302
|
-
body.appendChild(listMsg);
|
|
303
|
-
|
|
304
|
-
var listWrap = document.createElement('div');
|
|
305
|
-
listWrap.id = 'slim-kb-list';
|
|
306
|
-
body.appendChild(listWrap);
|
|
307
|
-
|
|
308
|
-
if (_kbData) renderKbList(listWrap);
|
|
309
|
-
else {
|
|
310
|
-
var loading = document.createElement('div');
|
|
311
|
-
loading.className = 'tile-empty';
|
|
312
|
-
loading.textContent = 'Loading knowledge base…';
|
|
313
|
-
listWrap.appendChild(loading);
|
|
314
|
-
}
|
|
315
|
-
// Always refresh list + pins on open so newly-swept entries appear.
|
|
316
|
-
Promise.all([
|
|
317
|
-
fetch('/api/knowledge', { headers: { 'Accept': 'application/json' } }).then(function(r) { return r.ok ? r.json() : null; }).catch(function() { return null; }),
|
|
318
|
-
loadKbPins(),
|
|
319
|
-
]).then(function(results) {
|
|
320
|
-
if (results[0]) {
|
|
321
|
-
_kbData = results[0];
|
|
322
|
-
_knowledgeCounts.kb = _countKbEntries(_kbData);
|
|
323
|
-
renderKnowledgeTile();
|
|
324
|
-
}
|
|
325
|
-
if (_knActiveTab === 'kb') {
|
|
326
|
-
var lw = document.getElementById('slim-kb-list');
|
|
327
|
-
if (lw) renderKbList(lw);
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function _kbAllItems() {
|
|
333
|
-
var items = [];
|
|
334
|
-
if (!_kbData) return items;
|
|
335
|
-
Object.keys(_kbData).forEach(function(cat) {
|
|
336
|
-
if (!Array.isArray(_kbData[cat])) return;
|
|
337
|
-
_kbData[cat].forEach(function(item) {
|
|
338
|
-
items.push({ file: item.file, category: cat, title: item.title, agent: item.agent, date: item.date, sortTs: item.sortTs, preview: item.preview });
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
return items;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function renderKbList(listWrap) {
|
|
345
|
-
listWrap.textContent = '';
|
|
346
|
-
var items = _kbAllItems();
|
|
347
|
-
if (!items.length) {
|
|
348
|
-
var empty = document.createElement('div');
|
|
349
|
-
empty.className = 'tile-empty';
|
|
350
|
-
empty.textContent = 'No knowledge entries yet. Notes are classified here after consolidation.';
|
|
351
|
-
listWrap.appendChild(empty);
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
// Pinned first, then newest.
|
|
355
|
-
items.sort(function(a, b) {
|
|
356
|
-
var ap = isKbPinned(kbPinKey(a.category, a.file));
|
|
357
|
-
var bp = isKbPinned(kbPinKey(b.category, b.file));
|
|
358
|
-
if (ap !== bp) return ap ? -1 : 1;
|
|
359
|
-
return (b.sortTs || 0) - (a.sortTs || 0) || (b.date || '').localeCompare(a.date || '');
|
|
360
|
-
});
|
|
361
|
-
var shown = items.slice(0, KB_LIST_CAP);
|
|
362
|
-
shown.forEach(function(item) {
|
|
363
|
-
listWrap.appendChild(buildKbRow(item));
|
|
364
|
-
});
|
|
365
|
-
if (items.length > shown.length) {
|
|
366
|
-
var more = document.createElement('div');
|
|
367
|
-
more.className = 'tile-empty';
|
|
368
|
-
more.textContent = 'Showing ' + shown.length + ' of ' + items.length + ' entries (newest + pinned first).';
|
|
369
|
-
listWrap.appendChild(more);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function buildKbRow(item) {
|
|
374
|
-
var key = kbPinKey(item.category, item.file);
|
|
375
|
-
var pinned = isKbPinned(key);
|
|
376
|
-
var row = document.createElement('div');
|
|
377
|
-
row.className = 'kb-row' + (pinned ? ' kb-row-pinned' : '');
|
|
378
|
-
|
|
379
|
-
var top = document.createElement('div');
|
|
380
|
-
top.className = 'kb-row-top';
|
|
381
|
-
|
|
382
|
-
var pinBtn = document.createElement('button');
|
|
383
|
-
pinBtn.className = 'kb-pin-btn' + (pinned ? ' on' : '');
|
|
384
|
-
pinBtn.type = 'button';
|
|
385
|
-
pinBtn.title = pinned ? 'Unpin from KB' : 'Pin in KB';
|
|
386
|
-
pinBtn.textContent = pinned ? '★' : '☆';
|
|
387
|
-
pinBtn.addEventListener('click', function(ev) { ev.stopPropagation(); toggleKbPin(key); });
|
|
388
|
-
top.appendChild(pinBtn);
|
|
389
|
-
|
|
390
|
-
var title = document.createElement('span');
|
|
391
|
-
title.className = 'kb-row-title';
|
|
392
|
-
title.textContent = item.title || item.file || '(untitled)';
|
|
393
|
-
title.title = item.title || '';
|
|
394
|
-
top.appendChild(title);
|
|
395
|
-
row.appendChild(top);
|
|
396
|
-
|
|
397
|
-
var meta = document.createElement('div');
|
|
398
|
-
meta.className = 'kb-row-meta';
|
|
399
|
-
meta.textContent = [KB_CAT_LABELS[item.category] || item.category, item.agent || null, item.date || null]
|
|
400
|
-
.filter(Boolean).join(' · ');
|
|
401
|
-
row.appendChild(meta);
|
|
402
|
-
|
|
403
|
-
if (item.preview) {
|
|
404
|
-
var prev = document.createElement('div');
|
|
405
|
-
prev.className = 'kb-row-preview';
|
|
406
|
-
var t = String(item.preview);
|
|
407
|
-
prev.textContent = t.length > 200 ? t.slice(0, 200) + '…' : t;
|
|
408
|
-
row.appendChild(prev);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
row.addEventListener('click', function() { openKbItem(item.category, item.file); });
|
|
412
|
-
return row;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
async function toggleKbPin(key) {
|
|
416
|
-
var listMsg = document.getElementById('slim-kb-list-msg');
|
|
417
|
-
if (listMsg) { listMsg.style.color = ''; listMsg.textContent = ''; }
|
|
418
|
-
try {
|
|
419
|
-
var res = await fetch('/api/kb-pins/toggle', {
|
|
420
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
421
|
-
body: JSON.stringify({ key: key }),
|
|
422
|
-
});
|
|
423
|
-
var d = await res.json().catch(function() { return {}; });
|
|
424
|
-
if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
|
|
425
|
-
if (d.pinned) { if (_kbPins.indexOf(key) === -1) _kbPins.push(key); }
|
|
426
|
-
else { _kbPins = _kbPins.filter(function(k) { return k !== key; }); }
|
|
427
|
-
var lw = document.getElementById('slim-kb-list');
|
|
428
|
-
if (lw) renderKbList(lw);
|
|
429
|
-
} catch (e) {
|
|
430
|
-
if (listMsg) { listMsg.style.color = 'var(--red)'; listMsg.textContent = 'Pin toggle failed: ' + (e && e.message ? e.message : e); }
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
async function openKbItem(category, file) {
|
|
435
|
-
var body = document.getElementById('slim-knowledge-body');
|
|
436
|
-
if (!body) return;
|
|
437
|
-
body.textContent = '';
|
|
438
|
-
var backRow = document.createElement('div');
|
|
439
|
-
backRow.className = 'kn-toolbar';
|
|
440
|
-
var backBtn = document.createElement('button');
|
|
441
|
-
backBtn.className = 'btn-secondary';
|
|
442
|
-
backBtn.type = 'button';
|
|
443
|
-
backBtn.textContent = '← Back to list';
|
|
444
|
-
backBtn.addEventListener('click', function() { renderKnowledgeTab(); });
|
|
445
|
-
backRow.appendChild(backBtn);
|
|
446
|
-
body.appendChild(backRow);
|
|
447
|
-
|
|
448
|
-
var head = document.createElement('div');
|
|
449
|
-
head.className = 'kn-section-head';
|
|
450
|
-
head.textContent = file;
|
|
451
|
-
body.appendChild(head);
|
|
452
|
-
|
|
453
|
-
var pre = document.createElement('pre');
|
|
454
|
-
pre.className = 'kb-entry-content';
|
|
455
|
-
pre.textContent = 'Loading…';
|
|
456
|
-
body.appendChild(pre);
|
|
457
|
-
|
|
458
|
-
try {
|
|
459
|
-
var res = await fetch('/api/knowledge/' + encodeURIComponent(category) + '/' + encodeURIComponent(file));
|
|
460
|
-
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
461
|
-
var text = await res.text();
|
|
462
|
-
pre.textContent = text.replace(/^---[\s\S]*?---\n*/m, '');
|
|
463
|
-
} catch (e) {
|
|
464
|
-
pre.textContent = 'Failed to load entry: ' + (e && e.message ? e.message : e);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function toggleKbCreateForm(body) {
|
|
469
|
-
var existing = document.getElementById('slim-kb-create');
|
|
470
|
-
if (existing) { existing.parentNode.removeChild(existing); return; }
|
|
471
|
-
var form = document.createElement('div');
|
|
472
|
-
form.id = 'slim-kb-create';
|
|
473
|
-
form.className = 'kn-create-form';
|
|
474
|
-
|
|
475
|
-
var catLabel = document.createElement('label');
|
|
476
|
-
catLabel.className = 'linkpr-label';
|
|
477
|
-
catLabel.textContent = 'Category';
|
|
478
|
-
var catSel = document.createElement('select');
|
|
479
|
-
catSel.id = 'slim-kb-category';
|
|
480
|
-
catSel.className = 'linkpr-input';
|
|
481
|
-
KB_CREATE_CATS.forEach(function(c) {
|
|
482
|
-
var opt = document.createElement('option');
|
|
483
|
-
opt.value = c;
|
|
484
|
-
opt.textContent = KB_CAT_LABELS[c] || c;
|
|
485
|
-
catSel.appendChild(opt);
|
|
486
|
-
});
|
|
487
|
-
catLabel.appendChild(catSel);
|
|
488
|
-
form.appendChild(catLabel);
|
|
489
|
-
|
|
490
|
-
var titleLabel = document.createElement('label');
|
|
491
|
-
titleLabel.className = 'linkpr-label';
|
|
492
|
-
titleLabel.textContent = 'Title';
|
|
493
|
-
var titleInput = document.createElement('input');
|
|
494
|
-
titleInput.id = 'slim-kb-title';
|
|
495
|
-
titleInput.className = 'linkpr-input';
|
|
496
|
-
titleInput.type = 'text';
|
|
497
|
-
titleInput.placeholder = 'Entry title';
|
|
498
|
-
titleLabel.appendChild(titleInput);
|
|
499
|
-
form.appendChild(titleLabel);
|
|
500
|
-
|
|
501
|
-
var contentLabel = document.createElement('label');
|
|
502
|
-
contentLabel.className = 'linkpr-label';
|
|
503
|
-
contentLabel.textContent = 'Content';
|
|
504
|
-
var contentInput = document.createElement('textarea');
|
|
505
|
-
contentInput.id = 'slim-kb-content';
|
|
506
|
-
contentInput.className = 'linkpr-input';
|
|
507
|
-
contentInput.rows = 6;
|
|
508
|
-
contentInput.placeholder = 'Write your knowledge entry…';
|
|
509
|
-
contentLabel.appendChild(contentInput);
|
|
510
|
-
form.appendChild(contentLabel);
|
|
511
|
-
|
|
512
|
-
var row = document.createElement('div');
|
|
513
|
-
row.className = 'kn-toolbar kn-toolbar-end';
|
|
514
|
-
var msg = document.createElement('span');
|
|
515
|
-
msg.id = 'slim-kb-create-msg';
|
|
516
|
-
msg.className = 'kn-msg kn-msg-inline';
|
|
517
|
-
row.appendChild(msg);
|
|
518
|
-
var saveBtn = document.createElement('button');
|
|
519
|
-
saveBtn.className = 'btn-primary';
|
|
520
|
-
saveBtn.type = 'button';
|
|
521
|
-
saveBtn.textContent = 'Save entry';
|
|
522
|
-
saveBtn.addEventListener('click', submitKbEntry);
|
|
523
|
-
row.appendChild(saveBtn);
|
|
524
|
-
form.appendChild(row);
|
|
525
|
-
|
|
526
|
-
// Insert the form directly after the toolbar (before the list).
|
|
527
|
-
var listWrap = document.getElementById('slim-kb-list');
|
|
528
|
-
if (listWrap && listWrap.parentNode) listWrap.parentNode.insertBefore(form, listWrap);
|
|
529
|
-
else body.appendChild(form);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
async function submitKbEntry() {
|
|
533
|
-
var catEl = document.getElementById('slim-kb-category');
|
|
534
|
-
var titleEl = document.getElementById('slim-kb-title');
|
|
535
|
-
var contentEl = document.getElementById('slim-kb-content');
|
|
536
|
-
var msg = document.getElementById('slim-kb-create-msg');
|
|
537
|
-
var category = catEl ? catEl.value : '';
|
|
538
|
-
var title = titleEl ? (titleEl.value || '').trim() : '';
|
|
539
|
-
var content = contentEl ? contentEl.value : '';
|
|
540
|
-
if (!title || !content.trim()) {
|
|
541
|
-
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Title and content are required.'; }
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
if (msg) { msg.style.color = 'var(--muted)'; msg.textContent = 'Saving…'; }
|
|
545
|
-
try {
|
|
546
|
-
var res = await fetch('/api/knowledge', {
|
|
547
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
548
|
-
body: JSON.stringify({ category: category, title: title, content: content }),
|
|
549
|
-
});
|
|
550
|
-
var d = await res.json().catch(function() { return {}; });
|
|
551
|
-
if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
|
|
552
|
-
// Refresh the list + count, then re-render the KB tab.
|
|
553
|
-
var kres = await fetch('/api/knowledge', { headers: { 'Accept': 'application/json' } });
|
|
554
|
-
if (kres.ok) { _kbData = await kres.json(); _knowledgeCounts.kb = _countKbEntries(_kbData); renderKnowledgeTile(); }
|
|
555
|
-
if (_knActiveTab === 'kb') renderKnowledgeTab();
|
|
556
|
-
} catch (e) {
|
|
557
|
-
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Error: ' + (e && e.message ? e.message : 'failed'); }
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
192
|
// ── Wiring ─────────────────────────────────────────────────────────
|
|
562
|
-
|
|
193
|
+
// onClose fires on every dismiss path (close-X, backdrop, Esc) so the
|
|
194
|
+
// embedded /inbox poll is suspended whenever the modal closes.
|
|
195
|
+
bindModalClose('slim-knowledge-modal', 'slim-knowledge-close', function() { _postKnEmbedVisibility(false); });
|
|
563
196
|
(function bindKnowledgeUi() {
|
|
564
197
|
var tabBar = document.getElementById('slim-kn-tabs');
|
|
565
198
|
if (tabBar) {
|
|
@@ -873,8 +873,9 @@
|
|
|
873
873
|
.pinned-row-actions .btn-secondary { padding: 4px 12px; font-size: var(--text-base); }
|
|
874
874
|
.pinned-row-unpin { color: var(--red); border-color: var(--red); }
|
|
875
875
|
|
|
876
|
-
/* Knowledge control panel (slim-knowledge-modal): Pinned Context
|
|
877
|
-
|
|
876
|
+
/* Knowledge control panel (slim-knowledge-modal): Pinned Context (slim-
|
|
877
|
+
native) + Notes & Knowledge (embedded classic /inbox) tabs in one box.
|
|
878
|
+
Reuses .tile-* and .pinned-row primitives. */
|
|
878
879
|
.slim-knowledge-modal-inner { width: 720px; max-width: calc(100vw - 32px); }
|
|
879
880
|
.kn-tabs { display: flex; gap: 6px; margin-left: 16px; }
|
|
880
881
|
.kn-tab {
|
|
@@ -905,51 +906,13 @@
|
|
|
905
906
|
}
|
|
906
907
|
.kn-msg { font-size: var(--text-base); min-height: 16px; color: var(--muted); }
|
|
907
908
|
.kn-msg-inline { margin-right: auto; }
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
916
|
-
/* KB list rows */
|
|
917
|
-
.kb-row {
|
|
918
|
-
border: 1px solid var(--border);
|
|
919
|
-
border-radius: var(--radius);
|
|
920
|
-
background: var(--surface2);
|
|
921
|
-
padding: 8px 10px;
|
|
922
|
-
margin-bottom: 8px;
|
|
923
|
-
cursor: pointer;
|
|
924
|
-
}
|
|
925
|
-
.kb-row:hover { border-color: var(--blue); }
|
|
926
|
-
.kb-row-pinned { border-color: var(--amber); }
|
|
927
|
-
.kb-row-top { display: flex; align-items: center; gap: 8px; }
|
|
928
|
-
.kb-row-title {
|
|
929
|
-
flex: 1; min-width: 0;
|
|
930
|
-
font-weight: 600; font-size: var(--text-md); color: var(--text);
|
|
931
|
-
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
932
|
-
}
|
|
933
|
-
.kb-pin-btn {
|
|
934
|
-
flex: 0 0 auto;
|
|
935
|
-
border: none; background: transparent; cursor: pointer;
|
|
936
|
-
color: var(--muted); font-size: var(--text-lg); line-height: 1; padding: 0 2px;
|
|
937
|
-
}
|
|
938
|
-
.kb-pin-btn.on { color: var(--amber); }
|
|
939
|
-
.kb-pin-btn:hover { color: var(--amber); }
|
|
940
|
-
.kb-row-meta { font-size: var(--text-base); color: var(--muted); margin-top: 3px; }
|
|
941
|
-
.kb-row-preview {
|
|
942
|
-
margin-top: 5px;
|
|
943
|
-
font-size: var(--text-base); color: var(--muted); line-height: 1.5;
|
|
944
|
-
word-break: break-word;
|
|
945
|
-
}
|
|
946
|
-
.kb-entry-content {
|
|
947
|
-
white-space: pre-wrap; word-break: break-word;
|
|
948
|
-
font-family: var(--mono, monospace); font-size: var(--text-base);
|
|
949
|
-
color: var(--text); line-height: 1.55;
|
|
950
|
-
background: var(--surface2); border: 1px solid var(--border);
|
|
951
|
-
border-radius: var(--radius); padding: 12px; margin: 0;
|
|
952
|
-
max-height: 60vh; overflow-y: auto;
|
|
909
|
+
/* Notes & Knowledge tab: the embedded classic /inbox screen. The host div
|
|
910
|
+
is toggled via the [hidden] attribute — a plain div defaults to
|
|
911
|
+
display:block when shown and the UA [hidden] rule forces display:none
|
|
912
|
+
when hidden, so no extra host rule is needed. */
|
|
913
|
+
.slim-kn-embed {
|
|
914
|
+
display: block; width: 100%; height: 62vh; min-height: 360px;
|
|
915
|
+
border: 0; background: var(--bg);
|
|
953
916
|
}
|
|
954
917
|
|
|
955
918
|
.chat-thinking {
|
package/docs/auto-discovery.md
CHANGED
|
@@ -28,8 +28,8 @@ tick()
|
|
|
28
28
|
syncPrdFromPrs() Backfill PRD item status from active PRs
|
|
29
29
|
checkPlanCompletion() Mark plans completed when all features are terminal (done/failed/cancelled)
|
|
30
30
|
2.7 pollPrHumanComments() Poll PR threads for human comments (wall-clock cadence from prPollCommentsEvery × tickInterval, default ≈ 12min)
|
|
31
|
-
reconcilePrs() (ADO+GH) Reconciliation sweep (
|
|
32
|
-
reconcileSharedBranchPrs() Backfill the PR store for shared-branch plans whose aggregate remote PR is untracked/under-linked (
|
|
31
|
+
reconcilePrs() (ADO+GH) Reconciliation sweep (honors the pollingPaused master kill-switch + the per-provider *PrReconcileEnabled flags)
|
|
32
|
+
reconcileSharedBranchPrs() Backfill the PR store for shared-branch plans whose aggregate remote PR is untracked/under-linked (skipped while pollingPaused is ON)
|
|
33
33
|
2.9 stalled-dispatch sweep Auto-retry failed items blocking pending deps (every 120 ticks)
|
|
34
34
|
3a. pruneStalePrDispatchesAsync() Clear pending PR dispatches whose underlying PRs no longer warrant action
|
|
35
35
|
3. dispatch existing pending Spawn agents for items already durable in dispatch.pending (up to maxConcurrent) — runs BEFORE discovery so a slow pre-dispatch validator can't starve already-queued work
|
|
@@ -74,7 +74,7 @@ Inside `discoverFromPrs()`, ADO and GitHub projects first resolve their own prov
|
|
|
74
74
|
|
|
75
75
|
**`autoReReviewPrs` gates both re-review paths (`P-e8b1c4d2`).** Re-reviews fire from two places: (1) the open-loop **discovery** path in `engine.js:discoverFromPrs` (PR shows `reviewStatus=waiting` after a fix push), and (2) the **closure-loop** in `engine/lifecycle.js:dispatchReReviewForFix` (a fix WI completes whose meta carries `addresses_review_wi`, queuing the next review against the same PR). The `engine.autoReReviewPrs` toggle is read at BOTH sites so flipping it OFF mutes the entire re-review cycle — not just the discovery sweep. Default ON; flip via Dashboard → Settings → Auto-fix & Review Loop, or `engine.autoReReviewPrs: false` in `config.json`.
|
|
76
76
|
|
|
77
|
-
**Hard-stop kill-switch (`pollingPaused`).** `engine.pollingPaused: true` is a master override that wins over both `adoPollEnabled` and `ghPollEnabled`. When ON, section 2.6/2.7 of the tick cycle skips `pollPrStatus` and `pollPrHumanComments` for both providers,
|
|
77
|
+
**Hard-stop kill-switch (`pollingPaused`).** `engine.pollingPaused: true` is a master override that wins over both `adoPollEnabled` and `ghPollEnabled`. When ON, section 2.6/2.7 of the tick cycle skips `pollPrStatus` and `pollPrHumanComments` for both providers, `discoverFromPrs` forces `pollEnabled=false` for every project so every per-PR auto-dispatch gate (`autoReviewPrs` / `autoFixBuilds` / `autoFixConflicts` / `autoFixReviewFeedback` / `autoFixHumanComments`) becomes inert, and — since W-ms5eljc60090f0d5 — the reconcile tail (`reconcilePrs`, `ghReconcilePrs`, `reconcileSharedBranchPrs`) is skipped too. Reconciliation was previously exempt as a "recovery sweep", but those sweeps walk every tracked PR and issue provider API calls, so the documented emergency pause did not actually stop provider network traffic. The per-provider `*PrReconcileEnabled` flags remain independent whenever the master switch is OFF. The engine logs `[engine] PR polling paused — …` once on the transition from unpaused → paused; routine PR poll log lines resume when cleared. Flip via Dashboard → Settings → Polling, `POST /api/engine/polling/pause`, or set `engine.pollingPaused: true` in `config.json`. Default OFF — fresh installs behave identically to before this knob existed.
|
|
78
78
|
|
|
79
79
|
**Hard-stop kill-switch (`autoFixPaused`).** `engine.autoFixPaused: true` is a narrower master override that wins over every auto-fix dispatch gate. When ON, `discoverFromPrs` forces `autoFixBuilds` / `autoFixConflicts` / `autoFixReviewFeedback` / `autoFixHumanComments` to false for every project, so no fix agent is auto-dispatched against any PR. Review dispatch (`autoReviewPrs` / `autoReReviewPrs`), PR status / human-comment polling, and reconciliation are intentionally not gated — operators can pause a fix-storm during an incident while still seeing fresh review verdicts and build status. The engine logs `[engine] auto-fix paused — …` once on the transition from unpaused → paused; routine discovery resumes when cleared. Flip via Dashboard → Settings → Auto-fix & Review Loop, or set `engine.autoFixPaused: true` in `config.json`. Default OFF — fresh installs behave identically to before this knob existed.
|
|
80
80
|
|
|
@@ -84,10 +84,10 @@ Inside `discoverFromPrs()`, ADO and GitHub projects first resolve their own prov
|
|
|
84
84
|
|------|---------|-------|--------------------------------|
|
|
85
85
|
| `adoPrStatusPollEnabled` | `true` | Section 2.6 (ADO `pollPrStatus`) | Yes — status polls honor the master killswitch |
|
|
86
86
|
| `adoPrCommentsPollEnabled` | `true` | Section 2.7 (ADO `pollPrHumanComments`) | Yes |
|
|
87
|
-
| `adoPrReconcileEnabled` | `true` | Section 2.7 tail (ADO `reconcilePrs`) |
|
|
87
|
+
| `adoPrReconcileEnabled` | `true` | Section 2.7 tail (ADO `reconcilePrs`) | Yes — the master killswitch stops provider reconciliation too (W-ms5eljc60090f0d5) |
|
|
88
88
|
| `ghPrStatusPollEnabled` | `true` | Section 2.6 (GitHub `ghPollPrStatus`) | Yes |
|
|
89
89
|
| `ghPrCommentsPollEnabled` | `true` | Section 2.7 (GitHub `ghPollPrHumanComments`) | Yes |
|
|
90
|
-
| `ghPrReconcileEnabled` | `true` | Section 2.7 tail (GitHub `ghReconcilePrs`) |
|
|
90
|
+
| `ghPrReconcileEnabled` | `true` | Section 2.7 tail (GitHub `ghReconcilePrs`) | Yes — the master killswitch stops provider reconciliation too (W-ms5eljc60090f0d5) |
|
|
91
91
|
| `processPendingRebasesEnabled` | `true` | Section 2.6 tail (`processPendingRebases`) | No — rebase processor has no legacy macro |
|
|
92
92
|
|
|
93
93
|
Resolution order (`shared.resolvePollFlag(engineCfg, granularKey, legacyMacroKey)`): (1) granular flag explicitly set → wins; (2) legacy macro is `false` → propagates `false` to all three axes of that provider (including reconcile); (3) otherwise → `ENGINE_DEFAULTS[granularKey]` (`true`).
|
|
@@ -36,10 +36,10 @@ There are two independent operator-facing master flags:
|
|
|
36
36
|
|
|
37
37
|
| Flag | Default | Effect |
|
|
38
38
|
|------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
39
|
-
| `pollingPaused` | `false` | Skips `pollPrStatus` + `pollPrHumanComments` for both providers. Forces `pollEnabled=false` in `discoverFromPrs` so every per-PR auto-dispatch gate (`autoReviewPrs` / `autoFixBuilds` / …) becomes inert.
|
|
39
|
+
| `pollingPaused` | `false` | Skips `pollPrStatus` + `pollPrHumanComments` for both providers, plus the reconcile tail (`reconcilePrs` / `ghReconcilePrs` / `reconcileSharedBranchPrs`) — the master switch stops ALL provider network polling (W-ms5eljc60090f0d5). Forces `pollEnabled=false` in `discoverFromPrs` so every per-PR auto-dispatch gate (`autoReviewPrs` / `autoFixBuilds` / …) becomes inert. Default OFF — fresh installs behave identically. |
|
|
40
40
|
| `autoFixPaused` | `false` | Narrower: forces `autoFixBuilds` / `autoFixConflicts` / `autoFixReviewFeedback` / `autoFixHumanComments` to `false` for every project. Review dispatch, polling, and reconciliation are intentionally not gated, so operators can stop a fix-storm while still seeing fresh verdicts and build status. Default OFF. |
|
|
41
41
|
|
|
42
|
-
These compose: setting `pollingPaused: true` plus `autoFixPaused: true` halts *all* automated PR work (review + fix + polling). Setting only `autoFixPaused: true` halts only auto-fix dispatch. Setting only `pollingPaused: true` halts polling and effectively halts dispatch (because the per-PR gates rely on cached poll state),
|
|
42
|
+
These compose: setting `pollingPaused: true` plus `autoFixPaused: true` halts *all* automated PR work (review + fix + polling). Setting only `autoFixPaused: true` halts only auto-fix dispatch. Setting only `pollingPaused: true` halts polling *and reconciliation* (all provider network traffic) and effectively halts dispatch (because the per-PR gates rely on cached poll state), while still letting manual `/api/pull-requests/observe` or operator-driven dispatch proceed.
|
|
43
43
|
|
|
44
44
|
Flip via Dashboard → Settings → Polling (`pollingPaused`) / Auto-fix & Review Loop (`autoFixPaused`), or set `engine.<flag>: true` in `config.json`.
|
|
45
45
|
|
package/engine/ado.js
CHANGED
|
@@ -774,6 +774,43 @@ function _hasPendingReReviewWi(pr) {
|
|
|
774
774
|
|
|
775
775
|
let _adoTokenCache = { token: null, expiresAt: 0 };
|
|
776
776
|
|
|
777
|
+
// W-ms5eljc60090f0d5 — Single-flight guard for token acquisition. `forEachActivePr`
|
|
778
|
+
// runs up to five requests concurrently and `adoFetch` re-acquires on every HTML
|
|
779
|
+
// (auth-redirect) body, so without this latch one expired token produced one
|
|
780
|
+
// `az`/`azureauth` shell-out PER in-flight request (a refresh stampede that
|
|
781
|
+
// degrades the whole engine host). Concurrent callers awaiting the same missing
|
|
782
|
+
// token share this promise; it is cleared on BOTH success and failure so a
|
|
783
|
+
// rejected acquisition can never wedge the latch permanently.
|
|
784
|
+
let _adoTokenInflight = null;
|
|
785
|
+
|
|
786
|
+
// Test seam for the acquisition shell-out. Production callers leave this null and
|
|
787
|
+
// get `acquireAdoToken`; tests set it so `adoFetch`'s internal `getAdoToken()`
|
|
788
|
+
// call (which takes no opts) can be exercised without spawning az/azureauth.
|
|
789
|
+
let _adoTokenAcquireOverride = null;
|
|
790
|
+
function _setAdoTokenAcquirerForTest(fn) { _adoTokenAcquireOverride = (typeof fn === 'function') ? fn : null; }
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Generation-safe cache invalidation. An ADO response only proves that the token
|
|
794
|
+
* IT used is dead — a slower request still carrying a superseded token must not
|
|
795
|
+
* evict the token a concurrent refresh already installed, or every late response
|
|
796
|
+
* re-opens the stampede window. Returns true only when the cache was actually
|
|
797
|
+
* cleared.
|
|
798
|
+
*/
|
|
799
|
+
function _invalidateAdoTokenIfCurrent(usedToken) {
|
|
800
|
+
const used = typeof usedToken === 'string' ? usedToken : '';
|
|
801
|
+
if (!used) return false; // request had no token to invalidate
|
|
802
|
+
if (!_adoTokenCache.token) return false; // already cleared by a sibling response
|
|
803
|
+
if (_adoTokenCache.token !== used) return false; // a NEWER token is cached — leave it alone
|
|
804
|
+
_adoTokenCache = { token: null, expiresAt: 0 };
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** Read-only cache peek — exported for testing only. */
|
|
809
|
+
function _peekAdoTokenCacheForTest() { return { ..._adoTokenCache }; }
|
|
810
|
+
|
|
811
|
+
/** Read-only in-flight peek — exported for testing only. */
|
|
812
|
+
function _getAdoTokenInflightForTest() { return _adoTokenInflight; }
|
|
813
|
+
|
|
777
814
|
// ── ADO token acquisition backoff + health (W-mqidhwcc000m897e) ──────────────
|
|
778
815
|
// Graduated backoff replaces the old flat 10-min blackout: a single transient
|
|
779
816
|
// blip (broker hiccup, az cold-start, network) now backs off 30s and doubles
|
|
@@ -965,13 +1002,30 @@ function isAdoAuthError(err) {
|
|
|
965
1002
|
|
|
966
1003
|
async function getAdoToken(opts = {}) {
|
|
967
1004
|
// _acquire is a test seam — production callers pass no args (unchanged).
|
|
968
|
-
const acquire = opts._acquire || acquireAdoToken;
|
|
1005
|
+
const acquire = opts._acquire || _adoTokenAcquireOverride || acquireAdoToken;
|
|
969
1006
|
if (_adoTokenCache.token && Date.now() < _adoTokenCache.expiresAt) {
|
|
970
1007
|
return _adoTokenCache.token;
|
|
971
1008
|
}
|
|
1009
|
+
// Single-flight: an acquisition is already running, so join it instead of
|
|
1010
|
+
// spawning a second `az`/`azureauth` process. Checked BEFORE the backoff gate
|
|
1011
|
+
// because the in-flight attempt has not recorded a failure yet.
|
|
1012
|
+
if (_adoTokenInflight) return _adoTokenInflight;
|
|
972
1013
|
// Graduated backoff — skip acquisition (return null) while backing off so a
|
|
973
1014
|
// transient blip doesn't blackout polling/reconciliation for the full cap.
|
|
974
1015
|
if (_adoTokenBackoff.isBackingOff()) return null;
|
|
1016
|
+
// NOTE: everything from the cache read to this assignment is synchronous, so
|
|
1017
|
+
// concurrent callers cannot interleave and both start an acquisition.
|
|
1018
|
+
const inflight = _acquireAdoTokenOnce(acquire);
|
|
1019
|
+
_adoTokenInflight = inflight;
|
|
1020
|
+
// Clear on BOTH settle paths. `_acquireAdoTokenOnce` swallows its own errors,
|
|
1021
|
+
// but the identity check keeps a late clear from clobbering a newer attempt.
|
|
1022
|
+
const clear = () => { if (_adoTokenInflight === inflight) _adoTokenInflight = null; };
|
|
1023
|
+
inflight.then(clear, clear);
|
|
1024
|
+
return inflight;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/** One acquisition attempt. Never rejects — resolves to a token string or null. */
|
|
1028
|
+
async function _acquireAdoTokenOnce(acquire) {
|
|
975
1029
|
try {
|
|
976
1030
|
const { token } = await acquire({ execAsync, timeout: 15000 });
|
|
977
1031
|
_adoTokenCache = { token, expiresAt: Date.now() + 30 * 60 * 1000 };
|
|
@@ -1025,10 +1079,15 @@ async function adoFetch(url, token, opts = {}) {
|
|
|
1025
1079
|
if (!res.ok) throw new Error(`ADO API ${method} ${res.status}: ${res.statusText}`);
|
|
1026
1080
|
const text = await res.text();
|
|
1027
1081
|
if (!text || text.trimStart().startsWith('<')) {
|
|
1028
|
-
|
|
1082
|
+
// Generation-safe: only evict the cache when THIS request's token is still
|
|
1083
|
+
// the cached one. A late response carrying a superseded token must not wipe
|
|
1084
|
+
// the token a concurrent refresh already installed. Combined with the
|
|
1085
|
+
// single-flight latch in getAdoToken, N concurrent HTML responses on one
|
|
1086
|
+
// stale token cost exactly ONE acquisition, not N.
|
|
1087
|
+
_invalidateAdoTokenIfCurrent(token);
|
|
1029
1088
|
if (_retryCount < MAX_RETRIES) {
|
|
1030
1089
|
const freshToken = await getAdoToken();
|
|
1031
|
-
if (freshToken) {
|
|
1090
|
+
if (freshToken && freshToken !== token) {
|
|
1032
1091
|
log('info', 'ADO token expired mid-session — refreshed and retrying');
|
|
1033
1092
|
return adoFetch(url, freshToken, { ...opts, _retryCount: _retryCount + 1 });
|
|
1034
1093
|
}
|
|
@@ -2889,6 +2948,7 @@ function _setAdoThrottleForTest(state, orgBase = 'dev.azure.com/__test__') {
|
|
|
2889
2948
|
* Lets tests exercise functions that call getAdoToken() without invoking azureauth.
|
|
2890
2949
|
* Pass null to force getAdoToken() to return null synchronously (no exec). */
|
|
2891
2950
|
function _setAdoTokenForTest(token) {
|
|
2951
|
+
_adoTokenInflight = null;
|
|
2892
2952
|
if (token == null) {
|
|
2893
2953
|
// Clear cache AND open a long backoff window so getAdoToken short-circuits
|
|
2894
2954
|
// to null without spawning azureauth — otherwise tests would hang on the
|
|
@@ -2907,6 +2967,8 @@ function _setAdoTokenForTest(token) {
|
|
|
2907
2967
|
/** Reset all token backoff + health + dedup state — exported for testing only. */
|
|
2908
2968
|
function _resetAdoTokenForTest() {
|
|
2909
2969
|
_adoTokenCache = { token: null, expiresAt: 0 };
|
|
2970
|
+
_adoTokenInflight = null;
|
|
2971
|
+
_adoTokenAcquireOverride = null;
|
|
2910
2972
|
_adoTokenBackoff._reset();
|
|
2911
2973
|
_adoTokenHealth = { lastSuccessAt: 0, lastFailureReason: null, classification: null };
|
|
2912
2974
|
_adoTokenAuthNoteSent = false;
|
|
@@ -3099,6 +3161,11 @@ module.exports = {
|
|
|
3099
3161
|
_resetAdoTokenForTest, // exported for testing
|
|
3100
3162
|
_setAdoTokenBackoffForTest, // exported for testing
|
|
3101
3163
|
_setAdoTokenAuthNoteWriterForTest, // exported for testing
|
|
3164
|
+
// W-ms5eljc60090f0d5 — single-flight acquisition + generation-safe invalidation
|
|
3165
|
+
_invalidateAdoTokenIfCurrent, // exported for testing
|
|
3166
|
+
_setAdoTokenAcquirerForTest, // exported for testing
|
|
3167
|
+
_peekAdoTokenCacheForTest, // exported for testing
|
|
3168
|
+
_getAdoTokenInflightForTest, // exported for testing
|
|
3102
3169
|
isAdoThrottled,
|
|
3103
3170
|
getAdoThrottleState,
|
|
3104
3171
|
fetchAdoPrMetadata,
|
package/engine/shared.js
CHANGED
|
@@ -3201,8 +3201,12 @@ const ENGINE_DEFAULTS = {
|
|
|
3201
3201
|
// autoReviewPrs / autoFixReviewFeedback / autoFixHumanComments) all
|
|
3202
3202
|
// become inert. Default false so a fresh install behaves identically to
|
|
3203
3203
|
// before this knob existed. Flip via Dashboard → Settings → Polling, or
|
|
3204
|
-
// set `engine.pollingPaused: true` in config.json.
|
|
3205
|
-
//
|
|
3204
|
+
// set `engine.pollingPaused: true` in config.json. W-ms5eljc60090f0d5: the
|
|
3205
|
+
// section 2.7 reconcile tail (reconcilePrs / ghReconcilePrs /
|
|
3206
|
+
// reconcileSharedBranchPrs) is suppressed too — those sweeps walk every
|
|
3207
|
+
// tracked PR and issue provider API calls, so an emergency pause that left
|
|
3208
|
+
// them running did not actually stop provider network traffic. The
|
|
3209
|
+
// per-provider *PrReconcileEnabled flags stay independent while this is off.
|
|
3206
3210
|
pollingPaused: false, // hard-stop kill-switch / master override (see comment above)
|
|
3207
3211
|
autoReReviewPrs: true, // auto-dispatch review agents after a PR fix is pushed
|
|
3208
3212
|
autoFixReviewFeedback: true, // auto-dispatch fix agents for minions review changes-requested verdicts
|
package/engine.js
CHANGED
|
@@ -12158,13 +12158,69 @@ const TICK_TIMEOUT_MS = 300000; // 5 min — force-release tick lock if stuck
|
|
|
12158
12158
|
// the entire tick loop indefinitely. The outer TICK_TIMEOUT_MS guard only
|
|
12159
12159
|
// rescues the tick lock; withTickTimeout surfaces the hung operation as an
|
|
12160
12160
|
// error on that tick so subsequent phases can still run.
|
|
12161
|
-
|
|
12161
|
+
//
|
|
12162
|
+
// W-ms5eljc60090f0d5 — Promise.race does NOT cancel `fn()`. A phase that blew
|
|
12163
|
+
// its deadline keeps running, so a later tick used to launch a SECOND copy of
|
|
12164
|
+
// the same phase on top of it (two concurrent pollPrStatus sweeps re-issuing
|
|
12165
|
+
// the same provider requests, doubling token/API pressure). `_phaseInFlight`
|
|
12166
|
+
// tracks the real underlying promise per label and makes the next invocation
|
|
12167
|
+
// of a still-running phase a fast rejection (`tick-phase-busy: …`) instead of
|
|
12168
|
+
// an overlapping run. Every call site already catches and logs, so the tick
|
|
12169
|
+
// keeps making progress and pending dispatches are still reached.
|
|
12170
|
+
//
|
|
12171
|
+
// Escape hatch: a promise that NEVER settles would otherwise silence its label
|
|
12172
|
+
// forever (fatal for `discoverWork`). Once a slot is older than
|
|
12173
|
+
// TICK_PHASE_ABANDON_MS the guard abandons tracking of the wedged run, logs
|
|
12174
|
+
// loudly, and lets a fresh invocation through — bounding the pathological
|
|
12175
|
+
// overlap to once per abandon-window instead of once per 10s tick.
|
|
12176
|
+
const _phaseInFlight = new Map(); // label -> { promise, startedAt }
|
|
12177
|
+
|
|
12178
|
+
/** How long a still-running phase may hold its slot before the guard gives up on
|
|
12179
|
+
* it. Deliberately generous (>= the outer tick-lock force-release window) so it
|
|
12180
|
+
* only ever fires for genuinely wedged work, never for a merely slow sweep. */
|
|
12181
|
+
const TICK_PHASE_ABANDON_MS = 300000; // 5 min
|
|
12182
|
+
|
|
12183
|
+
/** True while `label`'s previous invocation is still running (even if it already
|
|
12184
|
+
* blew its tickOpTimeoutMs deadline). Exported for testing. */
|
|
12185
|
+
function isTickPhaseInFlight(label) { return _phaseInFlight.has(label); }
|
|
12186
|
+
|
|
12187
|
+
/** Drop all phase-in-flight tracking — exported for testing only. */
|
|
12188
|
+
function _resetTickPhasesForTest() { _phaseInFlight.clear(); }
|
|
12189
|
+
|
|
12190
|
+
function withTickTimeout(fn, label, ms, opts = {}) {
|
|
12191
|
+
// abandonMs is a test seam — production callers pass no opts.
|
|
12192
|
+
const abandonMs = Number.isFinite(opts.abandonMs) ? opts.abandonMs : TICK_PHASE_ABANDON_MS;
|
|
12193
|
+
const existing = _phaseInFlight.get(label);
|
|
12194
|
+
if (existing) {
|
|
12195
|
+
const ageMs = Date.now() - existing.startedAt;
|
|
12196
|
+
if (ageMs < abandonMs) {
|
|
12197
|
+
return Promise.reject(new Error(`tick-phase-busy: ${label} skipped — a previous invocation is still running (${ageMs}ms)`));
|
|
12198
|
+
}
|
|
12199
|
+
// Wedged past the abandon window: stop waiting on it. The release handler
|
|
12200
|
+
// below is identity-checked, so the abandoned run can never clear the
|
|
12201
|
+
// successor's slot when (if ever) it finally settles.
|
|
12202
|
+
log('warn', `[tick] abandoning wedged phase ${label} after ${ageMs}ms — starting a fresh invocation`);
|
|
12203
|
+
_phaseInFlight.delete(label);
|
|
12204
|
+
}
|
|
12205
|
+
let started;
|
|
12206
|
+
try {
|
|
12207
|
+
started = Promise.resolve(fn());
|
|
12208
|
+
} catch (e) {
|
|
12209
|
+
return Promise.reject(e);
|
|
12210
|
+
}
|
|
12211
|
+
const entry = { promise: started, startedAt: Date.now() };
|
|
12212
|
+
_phaseInFlight.set(label, entry);
|
|
12213
|
+
// Release the slot only when the UNDERLYING work truly settles — not when the
|
|
12214
|
+
// race below rejects. Identity check so a stale settle can't clear a successor.
|
|
12215
|
+
const release = () => { if (_phaseInFlight.get(label) === entry) _phaseInFlight.delete(label); };
|
|
12216
|
+
started.then(release, release);
|
|
12217
|
+
let timer = null;
|
|
12162
12218
|
return Promise.race([
|
|
12163
|
-
|
|
12164
|
-
new Promise((_, reject) =>
|
|
12165
|
-
setTimeout(() => reject(new Error(`tick-timeout: ${label} exceeded ${ms}ms`)), ms)
|
|
12166
|
-
),
|
|
12167
|
-
]);
|
|
12219
|
+
started,
|
|
12220
|
+
new Promise((_, reject) => {
|
|
12221
|
+
timer = setTimeout(() => reject(new Error(`tick-timeout: ${label} exceeded ${ms}ms`)), ms);
|
|
12222
|
+
}),
|
|
12223
|
+
]).finally(() => { if (timer) clearTimeout(timer); });
|
|
12168
12224
|
}
|
|
12169
12225
|
|
|
12170
12226
|
// P-c2e5a1d9-a — Generation counter that is incremented on every tick start
|
|
@@ -12496,9 +12552,14 @@ async function tickInner() {
|
|
|
12496
12552
|
// P-a1f3c2d4: engine.pollingPaused is a hard-stop master switch — it overrides
|
|
12497
12553
|
// both per-provider toggles. When ON, adoPollEnabled and ghPollEnabled are
|
|
12498
12554
|
// forced to false so the status-poll and human-comment-poll gates below
|
|
12499
|
-
// short-circuit without editing every per-call gate.
|
|
12500
|
-
//
|
|
12501
|
-
//
|
|
12555
|
+
// short-circuit without editing every per-call gate. The same gate is mirrored
|
|
12556
|
+
// inside discoverFromPrs.
|
|
12557
|
+
// W-ms5eljc60090f0d5: reconciliation is now gated too. It was previously exempt
|
|
12558
|
+
// ("recovery sweep, not a convenience poll"), but reconcilePrs / ghReconcilePrs
|
|
12559
|
+
// walk every tracked PR and issue provider API calls, so the documented
|
|
12560
|
+
// emergency stop (POST /api/engine/polling/pause) did not actually stop
|
|
12561
|
+
// provider network traffic. The per-provider *PrReconcileEnabled flags stay
|
|
12562
|
+
// independent whenever the master switch is OFF.
|
|
12502
12563
|
const pollingPaused = config.engine?.pollingPaused === true;
|
|
12503
12564
|
if (pollingPaused && _shouldLogPollingPausedOnce()) {
|
|
12504
12565
|
log('info', '[engine] PR polling paused — engine.pollingPaused=true overrides adoPollEnabled/ghPollEnabled');
|
|
@@ -12519,8 +12580,8 @@ async function tickInner() {
|
|
|
12519
12580
|
const ghStatusPollEnabled = !pollingPaused && resolvePollFlag(config.engine, 'ghPrStatusPollEnabled', 'ghPollEnabled');
|
|
12520
12581
|
const adoCommentsPollEnabled = !pollingPaused && resolvePollFlag(config.engine, 'adoPrCommentsPollEnabled', 'adoPollEnabled');
|
|
12521
12582
|
const ghCommentsPollEnabled = !pollingPaused && resolvePollFlag(config.engine, 'ghPrCommentsPollEnabled', 'ghPollEnabled');
|
|
12522
|
-
const adoReconcileEnabled = resolvePollFlag(config.engine, 'adoPrReconcileEnabled', 'adoPollEnabled');
|
|
12523
|
-
const ghReconcileEnabled = resolvePollFlag(config.engine, 'ghPrReconcileEnabled', 'ghPollEnabled');
|
|
12583
|
+
const adoReconcileEnabled = !pollingPaused && resolvePollFlag(config.engine, 'adoPrReconcileEnabled', 'adoPollEnabled');
|
|
12584
|
+
const ghReconcileEnabled = !pollingPaused && resolvePollFlag(config.engine, 'ghPrReconcileEnabled', 'ghPollEnabled');
|
|
12524
12585
|
const rebaseProcessorEnabled = resolvePollFlag(config.engine, 'processPendingRebasesEnabled', null);
|
|
12525
12586
|
const prPollStatusEvery = Math.max(
|
|
12526
12587
|
1,
|
|
@@ -12619,11 +12680,13 @@ async function tickInner() {
|
|
|
12619
12680
|
}
|
|
12620
12681
|
if (commentPolls.length) await Promise.allSettled(commentPolls);
|
|
12621
12682
|
if (_isTickStale(myGeneration)) return;
|
|
12622
|
-
// P-c4d8e1a3:
|
|
12623
|
-
//
|
|
12624
|
-
//
|
|
12625
|
-
//
|
|
12626
|
-
//
|
|
12683
|
+
// P-c4d8e1a3: legacy adoPollEnabled/ghPollEnabled=false also silences
|
|
12684
|
+
// reconcile via resolvePollFlag's legacy-macro fallback (documented
|
|
12685
|
+
// breaking change). ADO and GitHub reconcilers are independent and run in
|
|
12686
|
+
// parallel; both are gated by *PrReconcileEnabled (default true) and by the
|
|
12687
|
+
// engine.pollingPaused master kill-switch (W-ms5eljc60090f0d5, so "pause
|
|
12688
|
+
// polling" means zero provider network traffic), but stay ungated by the
|
|
12689
|
+
// ADO/GH throttle trackers (recovery-sweep contract intact).
|
|
12627
12690
|
const reconcilePolls = [];
|
|
12628
12691
|
if (adoReconcileEnabled) {
|
|
12629
12692
|
reconcilePolls.push(withTickTimeout(() => reconcilePrs(config), 'reconcilePrs', tickOpTimeoutMs).catch(err => { log('warn', `ADO PR reconciliation error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }));
|
|
@@ -12662,12 +12725,16 @@ async function tickInner() {
|
|
|
12662
12725
|
// but is untracked / under-linked, so the dashboard renders the single
|
|
12663
12726
|
// aggregate PR instead of N phantom per-item rows. Host-agnostic and fully
|
|
12664
12727
|
// self-guarding (per-PRD/per-project try/catch, never creates a remote PR),
|
|
12665
|
-
// so it runs
|
|
12666
|
-
//
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
12728
|
+
// so it runs independently of the ADO/GitHub per-provider poll + throttle
|
|
12729
|
+
// gates. W-ms5eljc60090f0d5: it DOES query the remote for an open PR, so the
|
|
12730
|
+
// pollingPaused master kill-switch suppresses it too — "pause polling" must
|
|
12731
|
+
// mean zero provider network traffic.
|
|
12732
|
+
if (!pollingPaused) {
|
|
12733
|
+
try {
|
|
12734
|
+
await withTickTimeout(() => reconcileSharedBranchPrs(config), 'reconcileSharedBranchPrs', tickOpTimeoutMs);
|
|
12735
|
+
} catch (err) {
|
|
12736
|
+
log('warn', `[shared-branch-reconcile] sweep error: ${err?.message || err}`);
|
|
12737
|
+
}
|
|
12671
12738
|
}
|
|
12672
12739
|
if (_isTickStale(myGeneration)) return;
|
|
12673
12740
|
}
|
|
@@ -13618,6 +13685,8 @@ module.exports = {
|
|
|
13618
13685
|
_isTickStale,
|
|
13619
13686
|
// #421 — exported for testing the per-tick hard timeout path
|
|
13620
13687
|
_resolveTickHardTimeoutMs,
|
|
13688
|
+
// W-ms5eljc60090f0d5 — exported for testing the phase-in-flight re-entry guard
|
|
13689
|
+
withTickTimeout, isTickPhaseInFlight, _resetTickPhasesForTest,
|
|
13621
13690
|
_runScheduleBootstrapOnce, // exported for testing
|
|
13622
13691
|
get _scheduleBootstrapDone() { return _scheduleBootstrapDone; },
|
|
13623
13692
|
set _scheduleBootstrapDone(v) { _scheduleBootstrapDone = v; },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2442",
|
|
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"
|