@yemi33/minions 0.1.1911 → 0.1.1913
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 -0
- package/dashboard.js +128 -1
- package/engine/cli.js +1 -0
- package/engine/gh-comment.js +7 -0
- package/engine/github.js +98 -144
- package/engine/shared.js +1 -0
- package/package.json +1 -1
package/dashboard/js/settings.js
CHANGED
|
@@ -230,6 +230,7 @@ async function openSettings() {
|
|
|
230
230
|
settingsToggle('Copilot: suppress AGENTS.md', 'set-copilotSuppressAgentsMd', e.copilotSuppressAgentsMd !== false, '--no-custom-instructions: stops AGENTS.md auto-load from fighting Minions playbook prompts') +
|
|
231
231
|
settingsToggle('Copilot: reasoning summaries', 'set-copilotReasoningSummaries', !!e.copilotReasoningSummaries, '--enable-reasoning-summaries (Anthropic-family models only)') +
|
|
232
232
|
settingsToggle('Disable model discovery', 'set-disableModelDiscovery', !!e.disableModelDiscovery, 'Skip /api/runtimes/<name>/models REST calls fleet-wide. Settings UI falls back to free-text.') +
|
|
233
|
+
settingsToggle('Use persistent Copilot worker pool (faster CC responses)', 'set-ccUseWorkerPool', !!e.ccUseWorkerPool, 'Experimental — sub-task C of W-mp2w003600196c51 (CC perf). When ON, Command Center routes through engine/cc-worker-pool.js (one persistent `copilot --acp` process per CC tab) instead of spawning a fresh CLI per turn. Saves ~14s of cold-start cost on warm follow-up turns. Engine/agent dispatch path is unchanged. Off by default.') +
|
|
233
234
|
'</div>' +
|
|
234
235
|
'<div style="display:grid;grid-template-columns:1fr 3fr;gap:8px;margin-top:8px">' +
|
|
235
236
|
'<div>' +
|
|
@@ -625,6 +626,7 @@ async function saveSettings() {
|
|
|
625
626
|
copilotReasoningSummaries: !!document.getElementById('set-copilotReasoningSummaries')?.checked,
|
|
626
627
|
maxBudgetUsd: (document.getElementById('set-maxBudgetUsd')?.value ?? '').trim(),
|
|
627
628
|
disableModelDiscovery: !!document.getElementById('set-disableModelDiscovery')?.checked,
|
|
629
|
+
ccUseWorkerPool: !!document.getElementById('set-ccUseWorkerPool')?.checked,
|
|
628
630
|
maxTurnsByType: (function() {
|
|
629
631
|
var mbt = {};
|
|
630
632
|
var types = ['explore', 'ask', 'review', 'implement', 'fix', 'test', 'verify', 'plan', 'decompose'];
|
package/dashboard.js
CHANGED
|
@@ -32,6 +32,7 @@ const dispatchMod = require('./engine/dispatch');
|
|
|
32
32
|
const steering = require('./engine/steering');
|
|
33
33
|
const projectDiscovery = require('./engine/project-discovery');
|
|
34
34
|
const features = require('./engine/features');
|
|
35
|
+
const ccWorkerPool = require('./engine/cc-worker-pool');
|
|
35
36
|
const os = require('os');
|
|
36
37
|
|
|
37
38
|
const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeJsonNoRestore, safeUnlink, mutateJsonFileLocked, mutateTextFileLocked, mutateControl, mutateCooldowns, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS, WORK_TYPE, reopenWorkItem } = shared;
|
|
@@ -5826,6 +5827,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5826
5827
|
const abort = ccInFlightAborts.get(tabId);
|
|
5827
5828
|
if (abort) { try { abort(); } catch {} }
|
|
5828
5829
|
}
|
|
5830
|
+
// Sub-task C of W-mp2w003600196c51: when the worker pool is on, abort
|
|
5831
|
+
// must also fire `session/cancel` on the persistent ACP process so the
|
|
5832
|
+
// remote daemon stops generating into a torn-down session. The pool
|
|
5833
|
+
// exposes cancellation via the SessionHandle returned from getSession;
|
|
5834
|
+
// we don't keep that handle around here, so route through closeTab to
|
|
5835
|
+
// both cancel inflight and tear down the worker (cheaper than tracking
|
|
5836
|
+
// per-tab handles in dashboard state, and matches "tab close" semantics
|
|
5837
|
+
// — if the user explicitly aborted, we don't owe them a warm process).
|
|
5838
|
+
// Off when the flag is off so legacy SIGTERM-only behavior is preserved.
|
|
5839
|
+
if (CONFIG.engine && CONFIG.engine.ccUseWorkerPool) {
|
|
5840
|
+
try { ccWorkerPool.closeTab(tabId); } catch { /* swallow */ }
|
|
5841
|
+
}
|
|
5829
5842
|
_clearCcLiveStream(tabId);
|
|
5830
5843
|
_releaseCCTab(tabId);
|
|
5831
5844
|
return jsonReply(res, 200, { ok: true });
|
|
@@ -5846,6 +5859,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5846
5859
|
const sessions = _filterCcTabSessions(raw);
|
|
5847
5860
|
return sessions.filter(s => s.id !== id);
|
|
5848
5861
|
}, { defaultValue: [] });
|
|
5862
|
+
// Sub-task C of W-mp2w003600196c51: tear down the persistent ACP worker
|
|
5863
|
+
// for this tab so we don't leak a Copilot process after the user closes
|
|
5864
|
+
// the tab. closeTab is a no-op when the pool has no entry for the tabId,
|
|
5865
|
+
// so it's safe to call regardless of whether the flag is on.
|
|
5866
|
+
try { ccWorkerPool.closeTab(id); } catch { /* swallow */ }
|
|
5849
5867
|
return jsonReply(res, 200, { ok: true });
|
|
5850
5868
|
}
|
|
5851
5869
|
|
|
@@ -5875,6 +5893,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5875
5893
|
if (body.sessionId && ccSession._promptHash && ccSession._promptHash !== _ccPromptHash) {
|
|
5876
5894
|
ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
|
|
5877
5895
|
sessionReset = true;
|
|
5896
|
+
// Sub-task C of W-mp2w003600196c51: drop the persistent ACP worker
|
|
5897
|
+
// for this tab so the next turn rebuilds against the new prompt.
|
|
5898
|
+
// The pool's getSession() handles systemPromptHash deltas via
|
|
5899
|
+
// newSession() (warm-process reuse), but evicting the tab here is
|
|
5900
|
+
// belt-and-suspenders — matches the spec's "call closeTab on
|
|
5901
|
+
// _ccPromptHash change" requirement and matches the way we just
|
|
5902
|
+
// dropped ccSession entirely.
|
|
5903
|
+
try { ccWorkerPool.closeTab(tabId); } catch { /* swallow */ }
|
|
5878
5904
|
}
|
|
5879
5905
|
const wasResume = !!(body.sessionId && body.sessionId === ccSession.sessionId && ccSessionValid());
|
|
5880
5906
|
|
|
@@ -5943,8 +5969,21 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5943
5969
|
* onChunk/onToolUse shape — only `sessionId` differs (set on
|
|
5944
5970
|
* initial call, undefined on retry). Hoisted to keep the two call sites
|
|
5945
5971
|
* in lock-step.
|
|
5972
|
+
*
|
|
5973
|
+
* Sub-task C of W-mp2w003600196c51 (CC perf): when
|
|
5974
|
+
* `engineConfig.ccUseWorkerPool` is true the call routes through
|
|
5975
|
+
* engine/cc-worker-pool.js (`copilot --acp`, one persistent process per
|
|
5976
|
+
* CC tab) instead of spawning a fresh CLI per turn. The pool path
|
|
5977
|
+
* preserves the existing onChunk/onToolUse/result shape so callers
|
|
5978
|
+
* (handleCommandCenterStream, retry path) need no change. Engine/agent
|
|
5979
|
+
* dispatch path is intentionally NOT routed through the pool; that
|
|
5980
|
+
* lives on the per-dispatch _spawnProcess model in engine.js (regression
|
|
5981
|
+
* test enforces engine.js does not import cc-worker-pool).
|
|
5946
5982
|
*/
|
|
5947
|
-
function _invokeCcStream({ prompt, sessionId, liveState, toolUses, model, effort, maxTurns, engineConfig, systemPrompt = CC_STATIC_SYSTEM_PROMPT }) {
|
|
5983
|
+
function _invokeCcStream({ prompt, sessionId, liveState, toolUses, model, effort, maxTurns, engineConfig, systemPrompt = CC_STATIC_SYSTEM_PROMPT, tabId }) {
|
|
5984
|
+
if (engineConfig && engineConfig.ccUseWorkerPool) {
|
|
5985
|
+
return _invokeCcStreamViaPool({ prompt, liveState, model, effort, engineConfig, systemPrompt, tabId });
|
|
5986
|
+
}
|
|
5948
5987
|
const { callLLMStreaming } = require('./engine/llm');
|
|
5949
5988
|
return callLLMStreaming(prompt, systemPrompt, {
|
|
5950
5989
|
timeout: CC_CALL_TIMEOUT_MS, label: 'command-center', model, maxTurns,
|
|
@@ -5965,6 +6004,87 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5965
6004
|
});
|
|
5966
6005
|
}
|
|
5967
6006
|
|
|
6007
|
+
/**
|
|
6008
|
+
* Pool-routed implementation of _invokeCcStream. Keeps the public contract
|
|
6009
|
+
* identical: returns a Promise that resolves to a result envelope shaped like
|
|
6010
|
+
* callLLMStreaming's (`{ text, sessionId, code, usage, raw, stderr,
|
|
6011
|
+
* missingRuntime }`) and exposes an `.abort` property so the SSE handler can
|
|
6012
|
+
* cancel the in-flight stream by calling `_ccStreamAbort()`.
|
|
6013
|
+
*
|
|
6014
|
+
* Differences vs the direct path:
|
|
6015
|
+
* - Pool's `onChunk(text)` from agent_message_chunk is a DELTA, but
|
|
6016
|
+
* callLLMStreaming's contract is "full accumulated text"; we accumulate
|
|
6017
|
+
* here so `liveState.text` and downstream chunk events keep the same
|
|
6018
|
+
* semantics consumers already depend on.
|
|
6019
|
+
* - Tool calls are not surfaced in sub-task B (the pool ignores
|
|
6020
|
+
* `tool_call` notifications). `toolUses` stays empty on this path; if
|
|
6021
|
+
* sub-task C/D adds tool_call surfacing in the pool we'll plumb a
|
|
6022
|
+
* callback here too.
|
|
6023
|
+
* - `usage` is reported as an empty object — ACP doesn't expose token
|
|
6024
|
+
* counts in the in-flight session/update notifications, and the pool's
|
|
6025
|
+
* long-lived process makes per-turn usage attribution non-trivial.
|
|
6026
|
+
* trackEngineUsage is a no-op on `{}`.
|
|
6027
|
+
*/
|
|
6028
|
+
function _invokeCcStreamViaPool({ prompt, liveState, model, effort, engineConfig, systemPrompt, tabId }) {
|
|
6029
|
+
const resolvedTabId = tabId || 'default';
|
|
6030
|
+
let cancelled = false;
|
|
6031
|
+
let accumulated = '';
|
|
6032
|
+
let sessionHandle = null;
|
|
6033
|
+
let resolveResult;
|
|
6034
|
+
const promise = new Promise((resolve) => { resolveResult = resolve; });
|
|
6035
|
+
promise.abort = () => {
|
|
6036
|
+
cancelled = true;
|
|
6037
|
+
try { sessionHandle && sessionHandle.cancel(); } catch { /* swallow */ }
|
|
6038
|
+
};
|
|
6039
|
+
(async () => {
|
|
6040
|
+
try {
|
|
6041
|
+
sessionHandle = await ccWorkerPool.getSession({
|
|
6042
|
+
tabId: resolvedTabId,
|
|
6043
|
+
model,
|
|
6044
|
+
effort,
|
|
6045
|
+
mcpServers: (engineConfig && engineConfig.mcpServers) || [],
|
|
6046
|
+
systemPromptHash: _ccPromptHash,
|
|
6047
|
+
});
|
|
6048
|
+
} catch (err) {
|
|
6049
|
+
return resolveResult({
|
|
6050
|
+
text: '',
|
|
6051
|
+
sessionId: null,
|
|
6052
|
+
code: 1,
|
|
6053
|
+
usage: {},
|
|
6054
|
+
raw: '',
|
|
6055
|
+
stderr: String((err && err.message) || err || 'cc-worker-pool spawn failed'),
|
|
6056
|
+
});
|
|
6057
|
+
}
|
|
6058
|
+
if (cancelled) {
|
|
6059
|
+
try { sessionHandle.cancel(); } catch { /* swallow */ }
|
|
6060
|
+
return resolveResult({ text: accumulated, sessionId: sessionHandle.sessionId, code: 0, usage: {}, raw: accumulated, stderr: '' });
|
|
6061
|
+
}
|
|
6062
|
+
await sessionHandle.stream(prompt, {
|
|
6063
|
+
systemPromptText: systemPrompt,
|
|
6064
|
+
onChunk: (delta) => {
|
|
6065
|
+
accumulated += delta;
|
|
6066
|
+
_touchCcLiveStream(liveState);
|
|
6067
|
+
liveState.text = accumulated;
|
|
6068
|
+
if (liveState.writer) liveState.writer({ type: 'chunk', text: accumulated });
|
|
6069
|
+
},
|
|
6070
|
+
onDone: () => {
|
|
6071
|
+
resolveResult({ text: accumulated, sessionId: sessionHandle.sessionId, code: 0, usage: {}, raw: accumulated, stderr: '' });
|
|
6072
|
+
},
|
|
6073
|
+
onError: (err) => {
|
|
6074
|
+
resolveResult({
|
|
6075
|
+
text: accumulated,
|
|
6076
|
+
sessionId: sessionHandle.sessionId,
|
|
6077
|
+
code: cancelled ? 0 : 1,
|
|
6078
|
+
usage: {},
|
|
6079
|
+
raw: accumulated,
|
|
6080
|
+
stderr: String((err && err.message) || err || 'cc-worker-pool stream error'),
|
|
6081
|
+
});
|
|
6082
|
+
},
|
|
6083
|
+
});
|
|
6084
|
+
})();
|
|
6085
|
+
return promise;
|
|
6086
|
+
}
|
|
6087
|
+
|
|
5968
6088
|
async function handleCommandCenterStream(req, res) {
|
|
5969
6089
|
// SSE Origin gate (belt-and-suspenders: the top-level dispatcher has
|
|
5970
6090
|
// already rejected disallowed origins on POST, but validate again here
|
|
@@ -6106,6 +6226,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6106
6226
|
tabSessionId = null;
|
|
6107
6227
|
sessionReset = true;
|
|
6108
6228
|
sessionResetReason = 'promptChanged';
|
|
6229
|
+
// Sub-task C of W-mp2w003600196c51: matched dashboard reload with
|
|
6230
|
+
// a new prompt template — evict the persistent ACP worker so the
|
|
6231
|
+
// next turn rebuilds against the new prompt. (See same hook in
|
|
6232
|
+
// handleCommandCenter above.)
|
|
6233
|
+
try { ccWorkerPool.closeTab(body.tabId || 'default'); } catch { /* swallow */ }
|
|
6109
6234
|
} else if (tabEntry.runtime && tabEntry.runtime !== currentRuntime) {
|
|
6110
6235
|
tabSessionId = null;
|
|
6111
6236
|
sessionReset = true;
|
|
@@ -6149,6 +6274,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6149
6274
|
model: streamModel, effort: streamEffort, maxTurns: ccMaxTurns,
|
|
6150
6275
|
engineConfig: CONFIG.engine,
|
|
6151
6276
|
systemPrompt: turnSystemPrompt,
|
|
6277
|
+
tabId,
|
|
6152
6278
|
});
|
|
6153
6279
|
_ccStreamAbort = llmPromise.abort;
|
|
6154
6280
|
liveState.abortFn = _ccStreamAbort;
|
|
@@ -6174,6 +6300,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6174
6300
|
model: streamModel, effort: streamEffort, maxTurns: ccMaxTurns,
|
|
6175
6301
|
engineConfig: CONFIG.engine,
|
|
6176
6302
|
systemPrompt: turnSystemPrompt,
|
|
6303
|
+
tabId,
|
|
6177
6304
|
});
|
|
6178
6305
|
_ccStreamAbort = retryPromise.abort;
|
|
6179
6306
|
liveState.abortFn = _ccStreamAbort;
|
package/engine/cli.js
CHANGED
|
@@ -992,6 +992,7 @@ const commands = {
|
|
|
992
992
|
'claudeBareMode', 'claudeFallbackModel',
|
|
993
993
|
'copilotDisableBuiltinMcps', 'copilotSuppressAgentsMd', 'copilotStreamMode', 'copilotReasoningSummaries',
|
|
994
994
|
'maxBudgetUsd', 'disableModelDiscovery',
|
|
995
|
+
'ccUseWorkerPool',
|
|
995
996
|
];
|
|
996
997
|
const activeFlags = [];
|
|
997
998
|
for (const f of flagFields) {
|
package/engine/gh-comment.js
CHANGED
|
@@ -46,6 +46,12 @@ const REPO_SLUG_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
|
46
46
|
const MINIONS_COMMENT_MARKER_RE =
|
|
47
47
|
/^<!--\s*minions:agent=([^\s]+)\s+kind=([^\s]+)(?:\s+wi=([^\s]+))?\s*-->/m;
|
|
48
48
|
|
|
49
|
+
// Sample marker constant for tests / fixtures that need a body with a
|
|
50
|
+
// canonical-format marker but don't care about the specific agent / kind /
|
|
51
|
+
// wi fields. Matches MINIONS_COMMENT_MARKER_RE. Production comments build
|
|
52
|
+
// their marker via _buildMarker / buildMinionsCommentBody.
|
|
53
|
+
const MINIONS_COMMENT_MARKER = '<!-- minions:agent=minions kind=marker -->';
|
|
54
|
+
|
|
49
55
|
// Cheaper "is this body already marked?" check that matches only at position 0
|
|
50
56
|
// (for idempotency in buildMinionsCommentBody). Kept separate from the
|
|
51
57
|
// exported regex so the public regex can be used by downstream classifiers
|
|
@@ -231,6 +237,7 @@ module.exports = {
|
|
|
231
237
|
// Builders / parsers (pure functions — usable from anywhere)
|
|
232
238
|
buildMinionsCommentBody,
|
|
233
239
|
parseMinionsMarker,
|
|
240
|
+
MINIONS_COMMENT_MARKER,
|
|
234
241
|
MINIONS_COMMENT_MARKER_RE,
|
|
235
242
|
// Validation regexes (exported for downstream consumers)
|
|
236
243
|
AGENT_ID_RE,
|
package/engine/github.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
const shared = require('./shared');
|
|
8
8
|
const { exec, execAsync, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeJsonArr, safeWrite, mutateJsonFileLocked, MINIONS_DIR, getPrLinks, backfillPrPrdItems, log, ts, dateStamp, PR_STATUS, PR_POLLABLE_STATUSES, createThrottleTracker, getProjectOrg } = shared;
|
|
9
9
|
const { getPrs } = require('./queries');
|
|
10
|
+
const { MINIONS_COMMENT_MARKER_RE } = require('./gh-comment');
|
|
10
11
|
const path = require('path');
|
|
11
12
|
|
|
12
13
|
// Lazy require to avoid circular dependency — only needed for engine().handlePostMerge
|
|
@@ -85,131 +86,94 @@ function _isNonActionableComment(c, config = {}) {
|
|
|
85
86
|
if (_isAgentComment(c)) return true;
|
|
86
87
|
if (_isCiReportCommentBody(c?.body)) return true;
|
|
87
88
|
if (_isPreviewStatusComment(c)) return true;
|
|
88
|
-
if (
|
|
89
|
+
if (_isMinionsAuthoredComment(c)) return true;
|
|
89
90
|
return false;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
//
|
|
93
|
-
// VERDICT:APPROVE recaps, noop:true confessions) posted via the shared `gh` PAT identity.
|
|
93
|
+
// W-mp3bp0ha000997ab-b — Structural classifier for Minions-authored PR comments.
|
|
94
94
|
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
95
|
+
// Replaces the body-shape classifier chain (PR #2431's _isAgentSelfReviewDeclinedComment,
|
|
96
|
+
// PR #2442's _isAgentPositiveSignalComment + _hasAgentPositiveSignalBody) with a single
|
|
97
|
+
// structural check: a Minions-authored comment carries `MINIONS_COMMENT_MARKER` (defined
|
|
98
|
+
// in engine/gh-comment.js) on its own first line AND was authored by the viewer (the
|
|
99
|
+
// shared PAT identity used by all minions agents).
|
|
98
100
|
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
101
|
+
// Both gates are required:
|
|
102
|
+
// - `viewerDidAuthor: true` prevents humans from spoofing the marker by quoting it.
|
|
103
|
+
// - `MINIONS_COMMENT_MARKER_RE` enforces "own first line" — quoted markers in the
|
|
104
|
+
// middle of a comment, or markers with trailing content on the same line, do NOT
|
|
105
|
+
// match. This is the structural complement to the viewer check.
|
|
102
106
|
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
// Production note: the GitHub REST endpoints we poll (/issues/:n/comments and
|
|
108
|
+
// /pulls/:n/comments) do NOT populate `viewerDidAuthor` — that field is GraphQL-
|
|
109
|
+
// only. `pollPrHumanComments` therefore backfills it via `_backfillViewerDidAuthor`
|
|
110
|
+
// using the viewer login resolved by `_resolveViewerLogin` (one `gh api user` call
|
|
111
|
+
// per process). This keeps the classifier alive in production without forcing a
|
|
112
|
+
// GraphQL migration on every fetch site.
|
|
113
|
+
//
|
|
114
|
+
// Sub-item -a populates the marker on every minions-posted comment via a helper.
|
|
115
|
+
// Sub-item -c migrates existing engine call sites + playbook examples.
|
|
116
|
+
function _isMinionsAuthoredComment(c) {
|
|
117
|
+
if (!c || c.viewerDidAuthor !== true) return false;
|
|
118
|
+
const body = c.body || '';
|
|
119
|
+
const m = MINIONS_COMMENT_MARKER_RE.exec(body);
|
|
120
|
+
if (!m) return false;
|
|
121
|
+
// Strict: marker must be on its OWN first line (no leading content,
|
|
122
|
+
// no trailing content on the same line). MINIONS_COMMENT_MARKER_RE uses
|
|
123
|
+
// /m so it would otherwise match the start of any line; we additionally
|
|
124
|
+
// require the match to be at index 0 AND to occupy the first line
|
|
125
|
+
// exactly (modulo surrounding whitespace).
|
|
126
|
+
if (m.index !== 0) return false;
|
|
127
|
+
const lineEnd = body.indexOf('\n');
|
|
128
|
+
const firstLine = lineEnd === -1 ? body : body.slice(0, lineEnd);
|
|
129
|
+
return firstLine.trim() === m[0].trim();
|
|
109
130
|
}
|
|
110
131
|
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return
|
|
132
|
+
// ─── Viewer Login Resolution ────────────────────────────────────────────────
|
|
133
|
+
// `_isMinionsAuthoredComment` (above) gates on `c.viewerDidAuthor === true`.
|
|
134
|
+
// That field is populated by GitHub's GraphQL API but NOT by the REST
|
|
135
|
+
// `/issues/:n/comments` and `/pulls/:n/comments` endpoints we use in
|
|
136
|
+
// `pollPrHumanComments` (REST returns `viewerDidAuthor: null`). To make the
|
|
137
|
+
// classifier alive in production without changing every fetch site to
|
|
138
|
+
// GraphQL, we backfill the field by comparing the comment author's login to
|
|
139
|
+
// the viewer login resolved via `gh api user` once per process. The shared
|
|
140
|
+
// PAT identity does not change during a process lifetime, so a simple
|
|
141
|
+
// in-memory cache is sufficient.
|
|
142
|
+
let _cachedViewerLogin = null;
|
|
143
|
+
|
|
144
|
+
async function _resolveViewerLogin() {
|
|
145
|
+
if (_cachedViewerLogin) return _cachedViewerLogin;
|
|
146
|
+
try {
|
|
147
|
+
const result = await execAsync('gh api user', { timeout: 10000, encoding: 'utf-8', maxBuffer: GH_MAX_BUFFER });
|
|
148
|
+
const parsed = JSON.parse(String(result || ''));
|
|
149
|
+
const login = parsed?.login ? String(parsed.login).toLowerCase() : null;
|
|
150
|
+
if (login) _cachedViewerLogin = login;
|
|
151
|
+
return login;
|
|
152
|
+
} catch (e) {
|
|
153
|
+
log('warn', `GitHub viewer-login resolution failed: ${e?.message || e}`);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
125
156
|
}
|
|
126
157
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const login = String(c?.user?.login || '').toLowerCase();
|
|
132
|
-
if (!login || !sharedLogins.has(login)) return false;
|
|
133
|
-
return _hasAgentPositiveSignalBody(c.body);
|
|
158
|
+
// Test hook: lets tests prime or clear the cached viewer login without
|
|
159
|
+
// shelling out. Pass `null` to force a re-resolve on the next call.
|
|
160
|
+
function _setCachedViewerLogin(login) {
|
|
161
|
+
_cachedViewerLogin = login ? String(login).toLowerCase() : null;
|
|
134
162
|
}
|
|
135
163
|
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// review/implement dispatch in the completed history,
|
|
149
|
-
// OR the most recent review dispatch on this PR was assigned to the same agent as the
|
|
150
|
-
// PR author and completed with the noop:true / verdict:null / needs_rerun:true contract.
|
|
151
|
-
//
|
|
152
|
-
// This is a narrow allowlist for the noop pattern — generic PAT-user comments still flow
|
|
153
|
-
// through `_isNonActionableComment` and trigger `humanFeedback.pendingFix=true` as before.
|
|
154
|
-
function _isAgentSelfReviewDeclinedComment(c, { pr, dispatch } = {}) {
|
|
155
|
-
if (!c || !pr) return false;
|
|
156
|
-
const body = String(c.body || '');
|
|
157
|
-
if (!body) return false;
|
|
158
|
-
if (_hasMinionsReviewVerdict(body)) return false;
|
|
159
|
-
|
|
160
|
-
const prAuthorAgent = String(pr.agent || '').toLowerCase();
|
|
161
|
-
if (!prAuthorAgent) return false;
|
|
162
|
-
|
|
163
|
-
const completed = (dispatch && Array.isArray(dispatch.completed)) ? dispatch.completed : [];
|
|
164
|
-
const isNoopContract = (sc) => {
|
|
165
|
-
if (!sc || typeof sc !== 'object') return false;
|
|
166
|
-
if (sc.noop !== true && sc.noop !== 'true') return false;
|
|
167
|
-
if (sc.verdict !== null && sc.verdict !== undefined && sc.verdict !== '') return false;
|
|
168
|
-
if (sc.needs_rerun !== true && sc.needsRerun !== true) return false;
|
|
169
|
-
return true;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
// Signal A — explicit "Self-review declined" phrase + verifiable dispatch id pointing
|
|
173
|
-
// at a same-agent review/implement/fix dispatch in the completed history.
|
|
174
|
-
const phraseMatch = /self[\s\-_]+review\s+declined/i.test(body);
|
|
175
|
-
if (phraseMatch) {
|
|
176
|
-
const dispatchIdMatches = body.match(/\b[a-z][a-z0-9]*-[a-z][a-z0-9]*-[a-z0-9]{8,}\b/g) || [];
|
|
177
|
-
for (const id of dispatchIdMatches) {
|
|
178
|
-
const entry = completed.find(d => d && d.id === id);
|
|
179
|
-
if (!entry) continue;
|
|
180
|
-
const agent = String(entry.agent || '').toLowerCase();
|
|
181
|
-
const t = String(entry.type || '').toLowerCase();
|
|
182
|
-
if (agent !== prAuthorAgent) continue;
|
|
183
|
-
if (t === 'review' || t === 'implement' || t === 'implement-large' || t === 'fix') {
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
164
|
+
// Backfill `c.viewerDidAuthor` for an array of REST-fetched comments using
|
|
165
|
+
// the resolved viewer login. Only sets the field when REST returned
|
|
166
|
+
// null/undefined — never overrides a value that GraphQL or the test fixture
|
|
167
|
+
// already supplied. Mutates the comment objects in place.
|
|
168
|
+
function _backfillViewerDidAuthor(comments, viewerLogin) {
|
|
169
|
+
if (!Array.isArray(comments) || !viewerLogin) return;
|
|
170
|
+
for (const c of comments) {
|
|
171
|
+
if (!c || typeof c !== 'object') continue;
|
|
172
|
+
if (c.viewerDidAuthor === true || c.viewerDidAuthor === false) continue;
|
|
173
|
+
const login = c.user?.login;
|
|
174
|
+
if (typeof login !== 'string' || !login) continue;
|
|
175
|
+
c.viewerDidAuthor = login.toLowerCase() === viewerLogin;
|
|
187
176
|
}
|
|
188
|
-
|
|
189
|
-
// Signal B — most recent review dispatch on this PR was assigned to the PR author
|
|
190
|
-
// and completed with the documented noop contract. Catches the case where the agent
|
|
191
|
-
// forgot the canonical phrase but the completion-report contract still flags a noop.
|
|
192
|
-
const prId = String(pr.id || '');
|
|
193
|
-
const prNumberRaw = pr.prNumber;
|
|
194
|
-
const prNumber = prNumberRaw == null ? null : Number(prNumberRaw);
|
|
195
|
-
const reviewDispatches = completed.filter(d => {
|
|
196
|
-
if (!d || String(d.type || '').toLowerCase() !== 'review') return false;
|
|
197
|
-
const dpr = d.meta && d.meta.pr;
|
|
198
|
-
if (!dpr) return false;
|
|
199
|
-
if (prId && String(dpr.id || '') === prId) return true;
|
|
200
|
-
if (prNumber != null && Number(dpr.prNumber) === prNumber) return true;
|
|
201
|
-
return false;
|
|
202
|
-
});
|
|
203
|
-
if (reviewDispatches.length === 0) return false;
|
|
204
|
-
reviewDispatches.sort((a, b) => {
|
|
205
|
-
const ta = String(a.completed_at || a.created_at || '');
|
|
206
|
-
const tb = String(b.completed_at || b.created_at || '');
|
|
207
|
-
return tb.localeCompare(ta);
|
|
208
|
-
});
|
|
209
|
-
const latest = reviewDispatches[0];
|
|
210
|
-
if (!latest) return false;
|
|
211
|
-
if (String(latest.agent || '').toLowerCase() !== prAuthorAgent) return false;
|
|
212
|
-
return isNoopContract(latest.structuredCompletion);
|
|
213
177
|
}
|
|
214
178
|
|
|
215
179
|
// ─── Per-Repo Poll Backoff ──────────────────────────────────────────────────
|
|
@@ -821,13 +785,18 @@ async function pollPrStatus(config) {
|
|
|
821
785
|
|
|
822
786
|
async function pollPrHumanComments(config) {
|
|
823
787
|
// Load dispatch state once per poll cycle (cached for ~2s by queries.getDispatch).
|
|
824
|
-
//
|
|
825
|
-
//
|
|
788
|
+
// (Previously used by `_isAgentSelfReviewDeclinedComment` to verify dispatch ids;
|
|
789
|
+
// kept loaded in case downstream phases need dispatch context — cheap on cache hit.)
|
|
826
790
|
const queries = require('./queries');
|
|
827
791
|
const dispatch = (() => {
|
|
828
792
|
try { return queries.getDispatch(); }
|
|
829
793
|
catch { return { pending: [], active: [], completed: [] }; }
|
|
830
794
|
})();
|
|
795
|
+
// Resolve viewer login once per poll cycle so we can backfill
|
|
796
|
+
// `viewerDidAuthor` on REST comments — the field GraphQL would have
|
|
797
|
+
// populated. `_resolveViewerLogin` caches process-wide, so repeated calls
|
|
798
|
+
// across PRs are free.
|
|
799
|
+
const viewerLogin = await _resolveViewerLogin();
|
|
831
800
|
const totalUpdated = await forEachActiveGhPr(config, async (project, pr, prNum, slug) => {
|
|
832
801
|
// Get issue comments (general PR comments)
|
|
833
802
|
const comments = await ghApi(`/issues/${prNum}/comments`, slug);
|
|
@@ -840,6 +809,11 @@ async function pollPrHumanComments(config) {
|
|
|
840
809
|
...(Array.isArray(reviewComments) ? reviewComments : []).map(c => ({ ...c, _type: 'review' }))
|
|
841
810
|
];
|
|
842
811
|
|
|
812
|
+
// Backfill viewerDidAuthor on each comment so _isMinionsAuthoredComment
|
|
813
|
+
// can actually fire on production traffic. No-op when GraphQL or a test
|
|
814
|
+
// fixture already populated the field.
|
|
815
|
+
_backfillViewerDidAuthor(allComments, viewerLogin);
|
|
816
|
+
|
|
843
817
|
const cutoffStr = pr.humanFeedback?.lastProcessedCommentDate || pr.created || '1970-01-01';
|
|
844
818
|
const cutoffMs = new Date(cutoffStr).getTime() || 0;
|
|
845
819
|
|
|
@@ -853,12 +827,12 @@ async function pollPrHumanComments(config) {
|
|
|
853
827
|
for (const c of allComments) {
|
|
854
828
|
const date = c.created_at || c.updated_at || '';
|
|
855
829
|
const dateMs = date ? new Date(date).getTime() : 0;
|
|
856
|
-
// W-
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
|
|
861
|
-
|
|
830
|
+
// W-mp3bp0ha000997ab-b — All non-actionable filtering routes through
|
|
831
|
+
// _isNonActionableComment, which now composes _isMinionsAuthoredComment
|
|
832
|
+
// (structural marker check). The previous OR chain that appended
|
|
833
|
+
// _isAgentSelfReviewDeclinedComment is gone — that helper was deleted
|
|
834
|
+
// with this PR.
|
|
835
|
+
const isNonActionable = _isNonActionableComment(c, config);
|
|
862
836
|
if (dateMs) allCommentDates.push(date);
|
|
863
837
|
if (isNonActionable) continue;
|
|
864
838
|
const entry = {
|
|
@@ -883,26 +857,6 @@ async function pollPrHumanComments(config) {
|
|
|
883
857
|
}
|
|
884
858
|
if (newComments.length === 0) return false;
|
|
885
859
|
|
|
886
|
-
// P-a3f9b2c1 — Defense-in-depth: when the PR is already approved AND every new comment
|
|
887
|
-
// is from a configured shared-minions PAT identity AND every new comment matches the
|
|
888
|
-
// positive-signal body shape, suppress pendingFix and advance cutoff. Belt-and-suspenders
|
|
889
|
-
// against future regressions in `_isAgentPositiveSignalComment`. This is intentionally
|
|
890
|
-
// narrow — a single human-shaped comment in newComments (e.g. "rename _foo to bar") will
|
|
891
|
-
// disqualify the entire batch and let the normal human-feedback flow run.
|
|
892
|
-
if (String(pr.reviewStatus || '').toLowerCase() === 'approved') {
|
|
893
|
-
const sharedLogins = _sharedMinionsLogins(config);
|
|
894
|
-
if (sharedLogins.size > 0) {
|
|
895
|
-
const allFromSharedPat = newComments.every(nc => sharedLogins.has(String(nc.author || '').toLowerCase()));
|
|
896
|
-
const allPositiveShape = newComments.every(nc => _hasAgentPositiveSignalBody(nc.content));
|
|
897
|
-
if (allFromSharedPat && allPositiveShape) {
|
|
898
|
-
const cutoff = allNewDates.sort().pop() || newComments[newComments.length - 1].date;
|
|
899
|
-
pr.humanFeedback = { ...(pr.humanFeedback || {}), lastProcessedCommentDate: cutoff };
|
|
900
|
-
log('info', `PR ${pr.id}: ${newComments.length} new shared-PAT positive-signal comment(s) on approved PR — defense-in-depth suppression`);
|
|
901
|
-
return true;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
860
|
// Sort all comments chronologically and build full context for the fix agent
|
|
907
861
|
allCommentEntries.sort((a, b) => a.date.localeCompare(b.date));
|
|
908
862
|
newComments.sort((a, b) => a.date.localeCompare(b.date));
|
|
@@ -1200,9 +1154,9 @@ module.exports = {
|
|
|
1200
1154
|
_hasMinionsReviewVerdict, // exported for testing
|
|
1201
1155
|
_isAgentComment, // exported for testing
|
|
1202
1156
|
_isNonActionableComment, // exported for testing
|
|
1203
|
-
|
|
1204
|
-
_isAgentPositiveSignalComment, // exported for testing (P-a3f9b2c1)
|
|
1205
|
-
_hasAgentPositiveSignalBody, // exported for testing (P-a3f9b2c1)
|
|
1206
|
-
_sharedMinionsLogins, // exported for testing (P-a3f9b2c1)
|
|
1157
|
+
_isMinionsAuthoredComment, // exported for testing (W-mp3bp0ha000997ab-b)
|
|
1207
1158
|
_isPreviewStatusComment, // exported for testing
|
|
1159
|
+
_resolveViewerLogin, // exported for testing (W-mp3bp0ha000997ab-b backfill)
|
|
1160
|
+
_setCachedViewerLogin, // exported for testing (W-mp3bp0ha000997ab-b backfill)
|
|
1161
|
+
_backfillViewerDidAuthor, // exported for testing (W-mp3bp0ha000997ab-b backfill)
|
|
1208
1162
|
};
|
package/engine/shared.js
CHANGED
|
@@ -1119,6 +1119,7 @@ const ENGINE_DEFAULTS = {
|
|
|
1119
1119
|
copilotSuppressAgentsMd: true, // Copilot --no-custom-instructions: stop AGENTS.md auto-load from fighting Minions playbook prompts
|
|
1120
1120
|
copilotStreamMode: 'on', // Copilot --stream <on|off>: 'on' streams assistant.message_delta events live; 'off' batches them
|
|
1121
1121
|
copilotReasoningSummaries: false, // Copilot --enable-reasoning-summaries (Anthropic-family models only)
|
|
1122
|
+
ccUseWorkerPool: false, // Sub-task C of W-mp2w003600196c51 (CC perf): when true, _invokeCcStream routes through engine/cc-worker-pool.js (persistent `copilot --acp` per CC tab) instead of spawning a fresh CLI per turn. Off by default — opt-in feature flag. Engine/agent dispatch path stays per-process regardless.
|
|
1122
1123
|
maxBudgetUsd: undefined, // fleet USD ceiling for --max-budget-usd (per-agent override: agents.<id>.maxBudgetUsd). Honors 0 via ?? so a literal cap of $0 works
|
|
1123
1124
|
disableModelDiscovery: false, // skip runtime.listModels() REST calls fleet-wide (settings UI falls back to free-text)
|
|
1124
1125
|
maxPendingContexts: 20, // cap pendingContexts arrays in cooldowns.json to prevent unbounded growth
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1913",
|
|
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"
|