@yemi33/minions 0.1.2017 → 0.1.2018
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/docs/pr-review-fix-loop.md +1 -1
- package/engine/ado.js +48 -15
- package/engine/comment-classifier.js +138 -0
- package/engine/github.js +71 -35
- package/engine/lifecycle.js +11 -1
- package/engine/shared.js +11 -2
- package/engine.js +8 -3
- package/package.json +1 -1
|
@@ -32,7 +32,7 @@ How the engine manages the lifecycle of a PR from creation through review, fix,
|
|
|
32
32
|
5. Build failure (`buildStatus === 'failing'`)
|
|
33
33
|
6. Merge conflict (`_mergeConflict`)
|
|
34
34
|
|
|
35
|
-
When multiple problems coexist, earlier triggers get the first chance to enqueue work. The local `fixDispatched` flag is declared before the
|
|
35
|
+
When multiple problems coexist, earlier triggers get the first chance to enqueue work. The local `fixDispatched` flag is declared before the initial review trigger and set after first-review, human-feedback, review-feedback, and build-failure dispatches. Conflict fixes run last and explicitly require `!fixDispatched`, so any earlier successful review/fix dispatch suppresses the conflict fix for that PR in the same discovery pass. Build fixes are evaluated after human and minion review feedback, but the build-fix condition itself is not gated by `!fixDispatched`.
|
|
36
36
|
|
|
37
37
|
The engine does not cap review→fix cycles or build-fix attempts. Each trigger evaluates its own gates on every discovery pass; loops stop only when the underlying condition clears (reviewer approves, build passes, conflict resolves, human feedback handled). Operators who need to halt automation on a runaway PR should disable the relevant feature flag (`evalLoop`, `autoFixBuilds`, `autoFixConflicts`) or close the PR.
|
|
38
38
|
|
package/engine/ado.js
CHANGED
|
@@ -10,6 +10,7 @@ const { exec, execAsync, getAdoOrgBase, log, ts, dateStamp, PR_STATUS, createThr
|
|
|
10
10
|
const { getPrs } = require('./queries');
|
|
11
11
|
const { mutateJsonFileLocked } = shared;
|
|
12
12
|
const { acquireAdoToken } = require('./ado-token');
|
|
13
|
+
const { isPreviewStatusBody, hasMinionsMarker, hasVerdictPrefix } = require('./comment-classifier');
|
|
13
14
|
const { wrapUntrusted, buildSource } = require('./untrusted-fence');
|
|
14
15
|
|
|
15
16
|
// Lazy require to avoid circular dependency — only needed for engine().handlePostMerge
|
|
@@ -1490,6 +1491,32 @@ async function pollPrHumanComments(config) {
|
|
|
1490
1491
|
const allCommentDates = [];
|
|
1491
1492
|
const ignoredAuthors = (config.engine?.ignoredCommentAuthors || []).map(a => a.toLowerCase());
|
|
1492
1493
|
|
|
1494
|
+
// P-f23classifier (F3, security note): ADO has no `viewerDidAuthor`
|
|
1495
|
+
// equivalent. A human pasting our `<!-- minions:agent=... -->` marker
|
|
1496
|
+
// (e.g. by copy-paste from a fix-summary) would otherwise be
|
|
1497
|
+
// misclassified as agent-authored. Cross-check marker-presence against
|
|
1498
|
+
// the configured Minions identity (uniqueName / displayName / id /
|
|
1499
|
+
// descriptor) so marker-presence alone NEVER suffices on ADO. When
|
|
1500
|
+
// `minionsAdoIdentities` is empty (default), marker-presence is ignored
|
|
1501
|
+
// entirely on ADO — the body still has to match one of the legacy
|
|
1502
|
+
// regexes (Minions(...), by Minions, [minions], VERDICT:) to classify
|
|
1503
|
+
// as agent-authored.
|
|
1504
|
+
const minionsAdoIdentities = new Set(
|
|
1505
|
+
(Array.isArray(config.engine?.minionsAdoIdentities) ? config.engine.minionsAdoIdentities : [])
|
|
1506
|
+
.map(s => String(s || '').toLowerCase())
|
|
1507
|
+
.filter(Boolean)
|
|
1508
|
+
);
|
|
1509
|
+
const isAdoMinionsAuthor = (comment) => {
|
|
1510
|
+
if (minionsAdoIdentities.size === 0) return false;
|
|
1511
|
+
const author = comment?.author || {};
|
|
1512
|
+
const candidates = [author.uniqueName, author.displayName, author.id, author.descriptor];
|
|
1513
|
+
for (const cand of candidates) {
|
|
1514
|
+
if (typeof cand !== 'string' || !cand) continue;
|
|
1515
|
+
if (minionsAdoIdentities.has(cand.toLowerCase())) return true;
|
|
1516
|
+
}
|
|
1517
|
+
return false;
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1493
1520
|
for (const thread of threads) {
|
|
1494
1521
|
// P-8c6a4f2d: collect comments from ALL threads regardless of status.
|
|
1495
1522
|
// Previously we skipped resolved/closed threads here, which dropped
|
|
@@ -1504,25 +1531,31 @@ async function pollPrHumanComments(config) {
|
|
|
1504
1531
|
// Track date for cutoff BEFORE author/body filters so bot/CI/ignored
|
|
1505
1532
|
// comments still advance the cutoff.
|
|
1506
1533
|
if (comment.publishedDate) allCommentDates.push(comment.publishedDate);
|
|
1507
|
-
// Skip explicitly ignored authors
|
|
1534
|
+
// Skip explicitly ignored authors.
|
|
1508
1535
|
const authorName = (comment.author?.displayName || '').toLowerCase();
|
|
1509
1536
|
if (ignoredAuthors.some(a => authorName.includes(a))) continue;
|
|
1510
|
-
|
|
1511
|
-
//
|
|
1512
|
-
//
|
|
1513
|
-
//
|
|
1514
|
-
//
|
|
1515
|
-
//
|
|
1516
|
-
//
|
|
1517
|
-
//
|
|
1518
|
-
|
|
1519
|
-
// (
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
|
|
1537
|
+
// P-f23classifier (F2): filter preview-status / CI-report bot bodies
|
|
1538
|
+
// (Firebase / Appetize / Deploy / Preview / Coverage-Build-Test
|
|
1539
|
+
// headings / shields.io badges) for parity with engine/github.js
|
|
1540
|
+
// _isPreviewStatusComment. ADO has no `user.type === 'bot'` flag,
|
|
1541
|
+
// so the filter is body-only — same behavior the inline ADO CI
|
|
1542
|
+
// filter had before extraction, now broadened to include the GH
|
|
1543
|
+
// preview signatures so a Firebase/Appetize bot comment on an ADO
|
|
1544
|
+
// PR no longer advances pendingFix.
|
|
1545
|
+
if (isPreviewStatusBody(content)) continue;
|
|
1546
|
+
// P-f23classifier (F3): detect agent comments (included in context
|
|
1547
|
+
// but don't trigger fix). Structurally symmetric to
|
|
1548
|
+
// engine/github.js _isAgentComment + _isMinionsAuthoredComment:
|
|
1549
|
+
// - marker-presence + ADO identity cross-check (spoof guard), OR
|
|
1550
|
+
// - legacy body patterns (Minions(...), by Minions, [minions]), OR
|
|
1551
|
+
// - hasVerdictPrefix(content) — engine-owned structured-output
|
|
1552
|
+
// contract emitted by playbooks/review.md.
|
|
1553
|
+
const markerAuthored = hasMinionsMarker(content) && isAdoMinionsAuthor(comment);
|
|
1554
|
+
const isAgent = markerAuthored
|
|
1555
|
+
|| /\bMinions\s*\(/i.test(content)
|
|
1523
1556
|
|| /\bby\s+Minions\b/i.test(content)
|
|
1524
1557
|
|| /\[minions\]/i.test(content)
|
|
1525
|
-
||
|
|
1558
|
+
|| hasVerdictPrefix(content);
|
|
1526
1559
|
|
|
1527
1560
|
const entry = {
|
|
1528
1561
|
threadId: thread.id,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/comment-classifier.js — P-f23classifier (F2 + F3)
|
|
3
|
+
*
|
|
4
|
+
* Host-agnostic PR-comment body classifiers shared by engine/github.js and
|
|
5
|
+
* engine/ado.js pollers. Three pure body-only predicates:
|
|
6
|
+
*
|
|
7
|
+
* isPreviewStatusBody(body) — Firebase / Appetize / Deploy / Preview bot
|
|
8
|
+
* signatures + Coverage/Build/Test/Deploy/Pipeline report headings +
|
|
9
|
+
* shields.io-style badge images. Ported from engine/github.js
|
|
10
|
+
* `_isPreviewStatusComment` body checks. ADO has no `user.type === 'bot'`
|
|
11
|
+
* equivalent, so the GH-side bot-author gate stays in github.js — this
|
|
12
|
+
* module's body match is what both pollers consume.
|
|
13
|
+
*
|
|
14
|
+
* hasMinionsMarker(body) — structural `<!-- minions:agent=… kind=… -->`
|
|
15
|
+
* check using gh-comment.MINIONS_COMMENT_MARKER_RE. Strict: marker must
|
|
16
|
+
* occupy the body's first line entirely (no leading content, no trailing
|
|
17
|
+
* content on the same line). Same semantics as the GH-side
|
|
18
|
+
* `_isMinionsAuthoredComment` body check.
|
|
19
|
+
*
|
|
20
|
+
* hasVerdictPrefix(body) — `VERDICT: APPROVE | REQUEST_CHANGES` prefix
|
|
21
|
+
* check. Tightened in P-f4verdictregex (F4, Wave 3): requires a line
|
|
22
|
+
* that starts (under /m) with optional whitespace, an optional bold
|
|
23
|
+
* (`**` / `__`) or heading (`#+ `) marker, the literal `VERDICT:` token,
|
|
24
|
+
* whitespace, and `APPROVE` or `REQUEST_CHANGES` ending on a word
|
|
25
|
+
* boundary. Case-sensitive; legacy lowercase / `REQUEST-CHANGES` /
|
|
26
|
+
* `REQUEST CHANGES` variants no longer match. Strict body-position-0 is
|
|
27
|
+
* NOT required — real verdict bodies have preambles ~1/3 of the time,
|
|
28
|
+
* so any line-start position counts. Quoted (`> VERDICT:`) and inline
|
|
29
|
+
* mid-paragraph mentions are rejected (line-start anchor + only `**`,
|
|
30
|
+
* `__`, `#+` allowed before `VERDICT:`).
|
|
31
|
+
*
|
|
32
|
+
* IMPORTANT — author identity is intentionally NOT consulted here. These are
|
|
33
|
+
* body-only predicates. Callers MUST combine them with their own identity
|
|
34
|
+
* gates before treating a comment as agent-authored:
|
|
35
|
+
*
|
|
36
|
+
* GitHub: `comment.viewerDidAuthor === true`, backfilled via `gh api user`
|
|
37
|
+
* by engine/github.js `_backfillViewerDidAuthor`. The combined
|
|
38
|
+
* gate (`viewerDidAuthor && hasMinionsMarker`) is what makes the
|
|
39
|
+
* marker non-spoofable on GH.
|
|
40
|
+
*
|
|
41
|
+
* ADO: `comment.author.uniqueName` (or displayName / id / descriptor)
|
|
42
|
+
* matched case-insensitively against
|
|
43
|
+
* `config.engine.minionsAdoIdentities`. ADO has no
|
|
44
|
+
* `viewerDidAuthor` equivalent, so a human pasting our marker
|
|
45
|
+
* would otherwise be misclassified as agent-authored. The combined
|
|
46
|
+
* gate (`isMinionsAdoAuthor && hasMinionsMarker`) is the
|
|
47
|
+
* structural complement that prevents marker spoofing.
|
|
48
|
+
*
|
|
49
|
+
* Marker-presence ALONE never classifies a comment as agent on either
|
|
50
|
+
* platform — that contract is enforced at the caller, not here.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
const { MINIONS_COMMENT_MARKER_RE } = require('./gh-comment');
|
|
54
|
+
|
|
55
|
+
// ── Preview / status / CI-report body patterns ──────────────────────────────
|
|
56
|
+
// Coverage / Build / Test / Deploy / Pipeline report headings (level-1-to-3).
|
|
57
|
+
const _CI_REPORT_HEADING_RE =
|
|
58
|
+
/^#{1,3}\s*(Coverage|Build|Test|Deploy|Pipeline)\s*(Report|Status|Result|Summary)/i;
|
|
59
|
+
|
|
60
|
+
// shields.io / similar badge images.
|
|
61
|
+
const _BADGE_RE = /!\[.*\]\(https?:\/\/.*badge/i;
|
|
62
|
+
|
|
63
|
+
// Firebase App Distribution / Appetize / Preview / Deploy headings.
|
|
64
|
+
const _PREVIEW_HEADING_RE =
|
|
65
|
+
/^#{1,3}\s*(Firebase(?:\s+App\s+Distribution)?|Appetize|Preview|Deploy(?:ment)?|Status)\b/i;
|
|
66
|
+
|
|
67
|
+
const _FIREBASE_BRAND_RE = /\bFirebase\s+App\s+Distribution\b/i;
|
|
68
|
+
const _FIREBASE_URL_RE = /\bappdistribution\.firebase\b/i;
|
|
69
|
+
const _APPETIZE_URL_RE = /\bappetize\.io\b/i;
|
|
70
|
+
|
|
71
|
+
// "deploy ready", "deployment available", "preview succeeded", etc.
|
|
72
|
+
const _DEPLOY_READY_RE =
|
|
73
|
+
/\b(?:deploy|deployment|preview)\s+(?:ready|available|succeeded|complete|completed)\b/i;
|
|
74
|
+
|
|
75
|
+
function isPreviewStatusBody(body) {
|
|
76
|
+
const text = String(body || '');
|
|
77
|
+
if (!text) return false;
|
|
78
|
+
if (_CI_REPORT_HEADING_RE.test(text)) return true;
|
|
79
|
+
if (_BADGE_RE.test(text)) return true;
|
|
80
|
+
if (_PREVIEW_HEADING_RE.test(text)) return true;
|
|
81
|
+
if (_FIREBASE_BRAND_RE.test(text)) return true;
|
|
82
|
+
if (_FIREBASE_URL_RE.test(text)) return true;
|
|
83
|
+
if (_APPETIZE_URL_RE.test(text)) return true;
|
|
84
|
+
if (_DEPLOY_READY_RE.test(text)) return true;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Structural Minions marker ──────────────────────────────────────────────
|
|
89
|
+
//
|
|
90
|
+
// MINIONS_COMMENT_MARKER_RE uses the /m flag so its `^` anchor matches the
|
|
91
|
+
// start of ANY line. For body classification we additionally require the
|
|
92
|
+
// marker to occupy the FIRST line of the body entirely — no leading text, no
|
|
93
|
+
// trailing content on the same line. That matches the body-only contract
|
|
94
|
+
// gh-comment.buildMinionsCommentBody produces (marker, \n\n, body) and
|
|
95
|
+
// prevents quoted/blockquoted markers in human replies from triggering the
|
|
96
|
+
// classifier.
|
|
97
|
+
function hasMinionsMarker(body) {
|
|
98
|
+
const text = String(body || '');
|
|
99
|
+
if (!text) return false;
|
|
100
|
+
const m = MINIONS_COMMENT_MARKER_RE.exec(text);
|
|
101
|
+
if (!m) return false;
|
|
102
|
+
if (m.index !== 0) return false;
|
|
103
|
+
const lineEnd = text.indexOf('\n');
|
|
104
|
+
const firstLine = lineEnd === -1 ? text : text.slice(0, lineEnd);
|
|
105
|
+
return firstLine.trim() === m[0].trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── VERDICT prefix ─────────────────────────────────────────────────────────
|
|
109
|
+
//
|
|
110
|
+
// P-f4verdictregex (F4, Wave 3): tightened from the GH-flavored permissive
|
|
111
|
+
// regex to a strict line-start form. The body must contain a line that
|
|
112
|
+
// starts with optional whitespace, an optional bold (`**` / `__`) or
|
|
113
|
+
// heading (`#+ `) marker, the literal `VERDICT:` token, whitespace, and
|
|
114
|
+
// `APPROVE` or `REQUEST_CHANGES` ending on a word boundary. Case-sensitive
|
|
115
|
+
// (no /i flag) — playbooks/review.md emits the canonical uppercase form.
|
|
116
|
+
// /m flag lets a verdict appear after a preamble (~1/3 of real bodies have
|
|
117
|
+
// a preamble before the verdict line). Quoted (`> VERDICT:`) and inline
|
|
118
|
+
// mid-paragraph mentions are rejected because nothing other than
|
|
119
|
+
// whitespace + `**` / `__` / `#+ ` is allowed before `VERDICT:`.
|
|
120
|
+
//
|
|
121
|
+
// The optional trailing `(?:\*\*|__)?` group lets `__bold__` markdown close
|
|
122
|
+
// cleanly — `_` is a word character, so without consuming the closing
|
|
123
|
+
// `__` the trailing `\b` would not match `__VERDICT: APPROVE__`. The
|
|
124
|
+
// alternation backtracks for `**...**` bodies (trailing `**` consumed then
|
|
125
|
+
// `\b` fails, engine retries with 0-width trailing match and `\b` succeeds
|
|
126
|
+
// at the `E` → `*` transition).
|
|
127
|
+
const _VERDICT_PREFIX_RE =
|
|
128
|
+
/^\s*(?:\*\*|__|#+\s*)?VERDICT:\s+(APPROVE|REQUEST_CHANGES)(?:\*\*|__)?\b/m;
|
|
129
|
+
|
|
130
|
+
function hasVerdictPrefix(body) {
|
|
131
|
+
return _VERDICT_PREFIX_RE.test(String(body || ''));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
isPreviewStatusBody,
|
|
136
|
+
hasMinionsMarker,
|
|
137
|
+
hasVerdictPrefix,
|
|
138
|
+
};
|
package/engine/github.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
const shared = require('./shared');
|
|
8
8
|
const { exec, execAsync, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeJsonArr, safeWrite, mutateJsonFileLocked, mutatePullRequests, MINIONS_DIR, getPrLinks, backfillPrPrdItems, log, ts, dateStamp, PR_STATUS, PR_POLLABLE_STATUSES, ENGINE_DEFAULTS, createThrottleTracker, getProjectOrg } = shared;
|
|
9
9
|
const { getPrs } = require('./queries');
|
|
10
|
-
const {
|
|
10
|
+
const { isPreviewStatusBody, hasMinionsMarker, hasVerdictPrefix } = require('./comment-classifier');
|
|
11
11
|
const { wrapUntrusted, buildSource } = require('./untrusted-fence');
|
|
12
12
|
const ghToken = require('./gh-token');
|
|
13
13
|
const path = require('path');
|
|
@@ -59,6 +59,21 @@ function _setShellSafeGhForTest(fn) {
|
|
|
59
59
|
_shellSafeGhOverride = (typeof fn === 'function') ? fn : null;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// P-f1ghstale: test seam to mock ghApi at the call-site level. ghApi itself
|
|
63
|
+
// always catches (returns null on failure, GH_NOT_FOUND on 404), so the inner
|
|
64
|
+
// `catch` in checkLiveBuildAndConflict cannot be reached by mocking the shell
|
|
65
|
+
// layer alone. This seam lets tests inject a throwing/resolving override that
|
|
66
|
+
// exercises the buildStatusStale branch (parity with ADO's adoFetch testability
|
|
67
|
+
// via globalThis.fetch).
|
|
68
|
+
let _ghApiOverrideForTest = null;
|
|
69
|
+
function _setGhApiForTest(fn) {
|
|
70
|
+
_ghApiOverrideForTest = (typeof fn === 'function') ? fn : null;
|
|
71
|
+
}
|
|
72
|
+
async function _ghApiViaTestSeam(endpoint, slug, opts) {
|
|
73
|
+
if (_ghApiOverrideForTest) return _ghApiOverrideForTest(endpoint, slug, opts);
|
|
74
|
+
return ghApi(endpoint, slug, opts);
|
|
75
|
+
}
|
|
76
|
+
|
|
62
77
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
63
78
|
|
|
64
79
|
// 10 MB — prevents maxBuffer exceeded errors on repos with many open PRs.
|
|
@@ -78,8 +93,14 @@ function getRepoSlug(project) {
|
|
|
78
93
|
return `${org}/${repo}`;
|
|
79
94
|
}
|
|
80
95
|
|
|
96
|
+
// P-f23classifier (F4): delegates to the host-agnostic VERDICT prefix check
|
|
97
|
+
// in engine/comment-classifier.js. The literal regex used to live here;
|
|
98
|
+
// extraction allowed F4 (Wave 3) to tighten the prefix in a single place.
|
|
99
|
+
// The tightened form requires `VERDICT: APPROVE|REQUEST_CHANGES` at line
|
|
100
|
+
// start (under /m), optionally wrapped in `**`, `__`, or `#+ ` markers —
|
|
101
|
+
// see engine/comment-classifier.js for the full contract.
|
|
81
102
|
function _hasMinionsReviewVerdict(body) {
|
|
82
|
-
return
|
|
103
|
+
return hasVerdictPrefix(body);
|
|
83
104
|
}
|
|
84
105
|
|
|
85
106
|
function _isAgentComment(c) {
|
|
@@ -92,29 +113,21 @@ function _isAgentComment(c) {
|
|
|
92
113
|
return false;
|
|
93
114
|
}
|
|
94
115
|
|
|
95
|
-
function _isCiReportCommentBody(body) {
|
|
96
|
-
const text = String(body || '');
|
|
97
|
-
if (/^#{1,3}\s*(Coverage|Build|Test|Deploy|Pipeline)\s*(Report|Status|Result|Summary)/i.test(text)) return true;
|
|
98
|
-
if (/!\[.*\]\(https?:\/\/.*badge/i.test(text)) return true;
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
116
|
function _isGitHubBotComment(c) {
|
|
103
117
|
const login = String(c?.user?.login || '');
|
|
104
118
|
const type = String(c?.user?.type || '');
|
|
105
119
|
return type.toLowerCase() === 'bot' || /\[bot\]$/i.test(login);
|
|
106
120
|
}
|
|
107
121
|
|
|
122
|
+
// P-f23classifier (F2): retains the GH-side bot-author gate (ADO has no
|
|
123
|
+
// `user.type === 'bot'` equivalent) but delegates the body check to the
|
|
124
|
+
// host-agnostic isPreviewStatusBody helper in engine/comment-classifier.js.
|
|
125
|
+
// The body matcher covers Coverage/Build/Test/Deploy/Pipeline report
|
|
126
|
+
// headings + Firebase App Distribution + Appetize + Deploy-ready phrases +
|
|
127
|
+
// shields.io badges (same set as before the extraction).
|
|
108
128
|
function _isPreviewStatusComment(c) {
|
|
109
129
|
if (!_isGitHubBotComment(c)) return false;
|
|
110
|
-
|
|
111
|
-
if (_isCiReportCommentBody(body)) return true;
|
|
112
|
-
if (/^#{1,3}\s*(Firebase(?:\s+App\s+Distribution)?|Appetize|Preview|Deploy(?:ment)?|Status)\b/i.test(body)) return true;
|
|
113
|
-
if (/\bFirebase\s+App\s+Distribution\b/i.test(body)) return true;
|
|
114
|
-
if (/\bappdistribution\.firebase\b/i.test(body)) return true;
|
|
115
|
-
if (/\bappetize\.io\b/i.test(body)) return true;
|
|
116
|
-
if (/\b(?:deploy|deployment|preview)\s+(?:ready|available|succeeded|complete|completed)\b/i.test(body)) return true;
|
|
117
|
-
return false;
|
|
130
|
+
return isPreviewStatusBody(c?.body);
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
function _isNonActionableComment(c, config = {}) {
|
|
@@ -122,8 +135,14 @@ function _isNonActionableComment(c, config = {}) {
|
|
|
122
135
|
const login = String(c?.user?.login || '').toLowerCase();
|
|
123
136
|
if (ignoredAuthors.has(login)) return true;
|
|
124
137
|
if (_isAgentComment(c)) return true;
|
|
125
|
-
|
|
126
|
-
|
|
138
|
+
// P-f23classifier (F2): body-only preview/CI-report check is now host-
|
|
139
|
+
// agnostic. The legacy `_isCiReportCommentBody` body match (Coverage /
|
|
140
|
+
// Build / Test / Deploy / Pipeline headings + shields.io badges) is the
|
|
141
|
+
// strict subset of isPreviewStatusBody, so a single call covers both
|
|
142
|
+
// pre-existing branches. Bot-author gated preview matches still go
|
|
143
|
+
// through _isPreviewStatusComment for callers that want the narrower
|
|
144
|
+
// bot-only signal exported for testing.
|
|
145
|
+
if (isPreviewStatusBody(c?.body)) return true;
|
|
127
146
|
if (_isMinionsAuthoredComment(c)) return true;
|
|
128
147
|
return false;
|
|
129
148
|
}
|
|
@@ -151,20 +170,14 @@ function _isNonActionableComment(c, config = {}) {
|
|
|
151
170
|
//
|
|
152
171
|
// Sub-item -a populates the marker on every minions-posted comment via a helper.
|
|
153
172
|
// Sub-item -c migrates existing engine call sites + playbook examples.
|
|
173
|
+
//
|
|
174
|
+
// P-f23classifier (F3): the body-only "structural marker on own first line"
|
|
175
|
+
// check is now hasMinionsMarker in engine/comment-classifier.js. GH retains
|
|
176
|
+
// the viewerDidAuthor gate as the spoof guard (ADO uses a uniqueName cross-
|
|
177
|
+
// check against config.engine.minionsAdoIdentities instead — see ado.js).
|
|
154
178
|
function _isMinionsAuthoredComment(c) {
|
|
155
179
|
if (!c || c.viewerDidAuthor !== true) return false;
|
|
156
|
-
|
|
157
|
-
const m = MINIONS_COMMENT_MARKER_RE.exec(body);
|
|
158
|
-
if (!m) return false;
|
|
159
|
-
// Strict: marker must be on its OWN first line (no leading content,
|
|
160
|
-
// no trailing content on the same line). MINIONS_COMMENT_MARKER_RE uses
|
|
161
|
-
// /m so it would otherwise match the start of any line; we additionally
|
|
162
|
-
// require the match to be at index 0 AND to occupy the first line
|
|
163
|
-
// exactly (modulo surrounding whitespace).
|
|
164
|
-
if (m.index !== 0) return false;
|
|
165
|
-
const lineEnd = body.indexOf('\n');
|
|
166
|
-
const firstLine = lineEnd === -1 ? body : body.slice(0, lineEnd);
|
|
167
|
-
return firstLine.trim() === m[0].trim();
|
|
180
|
+
return hasMinionsMarker(c.body || '');
|
|
168
181
|
}
|
|
169
182
|
|
|
170
183
|
// ─── Viewer Login Resolution ────────────────────────────────────────────────
|
|
@@ -1371,6 +1384,8 @@ async function dismissPriorViewerChangesRequestedReviews(pr, project) {
|
|
|
1371
1384
|
* {
|
|
1372
1385
|
* buildStatus: 'failing' | 'passing' | 'running' | 'none' | null,
|
|
1373
1386
|
* mergeConflict: boolean,
|
|
1387
|
+
* buildStatusStale?: boolean,
|
|
1388
|
+
* buildStatusDetail?: string,
|
|
1374
1389
|
* }
|
|
1375
1390
|
*
|
|
1376
1391
|
* `mergeConflict` is true only when GitHub explicitly reports `mergeable: false`
|
|
@@ -1379,6 +1394,10 @@ async function dismissPriorViewerChangesRequestedReviews(pr, project) {
|
|
|
1379
1394
|
*
|
|
1380
1395
|
* `buildStatus` is null when we couldn't query check-runs or the PR isn't open;
|
|
1381
1396
|
* caller falls back to cached value.
|
|
1397
|
+
*
|
|
1398
|
+
* `buildStatusStale` (+ `buildStatusDetail`) are set when the inner check-runs
|
|
1399
|
+
* query throws — P-f1ghstale parity with ADO so engine.js's stale-marker branch
|
|
1400
|
+
* fires for GitHub too.
|
|
1382
1401
|
*/
|
|
1383
1402
|
async function checkLiveBuildAndConflict(pr, project) {
|
|
1384
1403
|
try {
|
|
@@ -1386,7 +1405,7 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
1386
1405
|
if (!slug) return null;
|
|
1387
1406
|
const prNum = shared.getPrNumber(pr);
|
|
1388
1407
|
if (!prNum) return null;
|
|
1389
|
-
const prData = await
|
|
1408
|
+
const prData = await _ghApiViaTestSeam(`/pulls/${prNum}`, slug);
|
|
1390
1409
|
if (!prData || prData === GH_NOT_FOUND) return null;
|
|
1391
1410
|
|
|
1392
1411
|
// Conflict signal — only treat `mergeable === false` as a positive
|
|
@@ -1401,9 +1420,17 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
1401
1420
|
// check-runs classification so the live read and cached poll agree on
|
|
1402
1421
|
// 'failing' / 'passing' / 'running' / 'none'.
|
|
1403
1422
|
let buildStatus = null;
|
|
1423
|
+
// P-f1ghstale: stale-signal parity with ADO. When the check-runs query
|
|
1424
|
+
// throws (transient gh CLI failure, network blip), surface a stale signal
|
|
1425
|
+
// so engine.js's `if (live?.buildStatusStale)` branch can persist the
|
|
1426
|
+
// cached state instead of treating a missing buildStatus as recovered.
|
|
1427
|
+
// EXPLICIT NON-GOALS (rejected in plan): no mergeable===null branch,
|
|
1428
|
+
// no wallclock/age branch, no empty check_runs branch.
|
|
1429
|
+
let buildStatusStale = false;
|
|
1430
|
+
let buildStatusDetail = '';
|
|
1404
1431
|
if (prData.state === 'open' && prData.head?.sha) {
|
|
1405
1432
|
try {
|
|
1406
|
-
const checksData = await
|
|
1433
|
+
const checksData = await _ghApiViaTestSeam(`/commits/${prData.head.sha}/check-runs`, slug);
|
|
1407
1434
|
if (checksData && Array.isArray(checksData.check_runs)) {
|
|
1408
1435
|
const runs = checksData.check_runs;
|
|
1409
1436
|
if (runs.length === 0) {
|
|
@@ -1417,10 +1444,18 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
1417
1444
|
else buildStatus = 'running';
|
|
1418
1445
|
}
|
|
1419
1446
|
}
|
|
1420
|
-
} catch (e) {
|
|
1447
|
+
} catch (e) {
|
|
1448
|
+
buildStatusStale = true;
|
|
1449
|
+
buildStatusDetail = `Live check-runs query failed: ${e.message}`;
|
|
1450
|
+
log('warn', `Live build check checks query for ${pr.id}: ${e.message}`);
|
|
1451
|
+
}
|
|
1421
1452
|
}
|
|
1422
1453
|
|
|
1423
|
-
return {
|
|
1454
|
+
return {
|
|
1455
|
+
buildStatus,
|
|
1456
|
+
mergeConflict,
|
|
1457
|
+
...(buildStatusStale ? { buildStatusStale, buildStatusDetail } : {}),
|
|
1458
|
+
};
|
|
1424
1459
|
} catch (e) {
|
|
1425
1460
|
log('warn', `Live build/conflict check for ${pr.id}: ${e.message}`);
|
|
1426
1461
|
return null;
|
|
@@ -1608,5 +1643,6 @@ module.exports = {
|
|
|
1608
1643
|
_backfillViewerDidAuthor, // exported for testing (W-mp3bp0ha000997ab-b backfill)
|
|
1609
1644
|
_setExecAsyncForTest, // W-mp5trwh60008386d: test seam to mock `gh api` shell-outs
|
|
1610
1645
|
_setShellSafeGhForTest, // P-f2-gh-shell (F2): test seam to mock argv-form gh api calls
|
|
1646
|
+
_setGhApiForTest, // P-f1ghstale: test seam to mock ghApi call sites for stale-signal branch coverage
|
|
1611
1647
|
GH_NOT_FOUND, // W-mp5trwh60008386d: exported so tests can assert sentinel propagation
|
|
1612
1648
|
};
|
package/engine/lifecycle.js
CHANGED
|
@@ -1687,11 +1687,21 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
1687
1687
|
// Actively clear the agent's prior negative on flip, then re-check live so we
|
|
1688
1688
|
// never claim approved if some OTHER reviewer (human, different minion
|
|
1689
1689
|
// account) still has a negative vote/review on the PR.
|
|
1690
|
+
//
|
|
1691
|
+
// P-f7splitgate — split the gate into two predicates:
|
|
1692
|
+
// * canDismissOwnPriorNegative: dismissal of the engine's own previously-posted
|
|
1693
|
+
// REQUEST_CHANGES on a verdict flip to APPROVE runs UNCONDITIONALLY. This is
|
|
1694
|
+
// correctness (cleaning up our own stale state), not policy, so it must NOT
|
|
1695
|
+
// be gated by autoApplyReviewVote.
|
|
1696
|
+
// * autoApplyVote: continues to gate POSITIVE auto-actions — mirroring the
|
|
1697
|
+
// live platform status into local reviewStatus below (which can promote the
|
|
1698
|
+
// PR to 'approved' based on platform-side votes).
|
|
1690
1699
|
const prevReviewStatus = reviewPr?.reviewStatus || '';
|
|
1691
1700
|
const wasNegative = prevReviewStatus === 'changes-requested' || prevReviewStatus === 'waiting'
|
|
1692
1701
|
|| liveStatus === 'changes-requested' || liveStatus === 'waiting';
|
|
1693
1702
|
const autoApplyVote = config?.engine?.autoApplyReviewVote ?? ENGINE_DEFAULTS.autoApplyReviewVote;
|
|
1694
|
-
|
|
1703
|
+
const canDismissOwnPriorNegative = verdictRaw === 'approved' && !isSelfReview && wasNegative && projectObjForChecks;
|
|
1704
|
+
if (canDismissOwnPriorNegative) {
|
|
1695
1705
|
try {
|
|
1696
1706
|
const reconcileFn = hostForChecks === 'github'
|
|
1697
1707
|
? require('./github').dismissPriorViewerChangesRequestedReviews
|
package/engine/shared.js
CHANGED
|
@@ -1692,7 +1692,6 @@ const ENGINE_DEFAULTS = {
|
|
|
1692
1692
|
autoReviewPrs: true, // auto-dispatch review agents for newly opened agent PRs
|
|
1693
1693
|
autoReReviewPrs: true, // auto-dispatch review agents after a PR fix is pushed
|
|
1694
1694
|
autoFixReviewFeedback: true, // auto-dispatch fix agents for minions review changes-requested verdicts
|
|
1695
|
-
autoApplyReviewVote: false, // when true, review verdicts (APPROVE / REQUEST_CHANGES) automatically flip the platform vote; when false (default), verdicts are informational only
|
|
1696
1695
|
autoFixHumanComments: true, // auto-dispatch fix agents for actionable human PR comments
|
|
1697
1696
|
prNoOpFixPauseAttempts: 2, // pause one PR automation cause after repeated no-op fixes for unchanged evidence
|
|
1698
1697
|
completionReportRetentionDays: 90, // retain completion report sidecars beyond capped dispatch history
|
|
@@ -1739,6 +1738,16 @@ const ENGINE_DEFAULTS = {
|
|
|
1739
1738
|
autoCompletePrs: false, // auto-merge PRs when builds green + review approved (opt-in)
|
|
1740
1739
|
prMergeMethod: 'squash', // merge method: squash, merge, rebase
|
|
1741
1740
|
ignoredCommentAuthors: [], // comments from these authors are auto-closed and never trigger fixes
|
|
1741
|
+
// P-f23classifier (F3, security note): ADO Minions identity allow-list for
|
|
1742
|
+
// the marker spoof guard in engine/ado.js pollPrHumanComments. ADO has no
|
|
1743
|
+
// viewerDidAuthor equivalent, so a human pasting the <!-- minions:agent=... -->
|
|
1744
|
+
// marker would otherwise be misclassified as agent-authored. The poller
|
|
1745
|
+
// combines marker-presence with comment.author.{uniqueName|displayName|id|
|
|
1746
|
+
// descriptor} matching one of these (case-insensitive) before treating a
|
|
1747
|
+
// body as agent-authored. Empty array (default) means marker-presence alone
|
|
1748
|
+
// NEVER classifies as agent on ADO — the body still has to match one of
|
|
1749
|
+
// the legacy patterns (Minions(...), by Minions, [minions], VERDICT:).
|
|
1750
|
+
minionsAdoIdentities: [],
|
|
1742
1751
|
botCommentLogins: [], // P-a3f9b2c1: opt-in shared-minions GH login list — comments from these logins are suppressed ONLY when body matches positive-signal markers (Verification SUCCESS / VERDICT:APPROVE / noop:true). Narrower than ignoredCommentAuthors which suppresses all comments by login.
|
|
1743
1752
|
// W-mp76pw7a001da7c1 — Per-slug GitHub PAT routing. Map of `<owner>` (or `<owner>/*`,
|
|
1744
1753
|
// or `*` for fleet default) to a `gh auth` account name. `engine/gh-token.js`
|
|
@@ -1752,7 +1761,7 @@ const ENGINE_DEFAULTS = {
|
|
|
1752
1761
|
ccEffort: null, // effort level for CC/doc-chat (null, 'low', 'medium', 'high')
|
|
1753
1762
|
enablePreDispatchEval: true, // P-d2a9f6e5: cheap LLM gate before queueing — on by default. See engine/pre-dispatch-eval.js (Ripley §3 recommendation, 2026-05-11 architecture review). Validates from acceptance_criteria when present, falls back to description when criteria are absent but description is rich (≥80 chars). Fail-open on any validator error.
|
|
1754
1763
|
completionNonceRequired: false, // P-d2a8f6c1 (agent trust boundary F8): when true, a missing `nonce` field in the completion JSON hard-fails the dispatch with failure_class:'completion-nonce-mismatch'. Default false for one release so older agents/runtime caches that haven't picked up the prompt change degrade with a warning instead of breaking. Mismatched nonces hard-fail regardless of this flag. See docs/completion-reports.md → "Trust boundary".
|
|
1755
|
-
autoApplyReviewVote: false, // W-mpea9fyb0010febf: when true,
|
|
1764
|
+
autoApplyReviewVote: false, // W-mpea9fyb0010febf / P-f7splitgate: gates POSITIVE auto-actions only — when true, the engine mirrors live platform vote state into local pull-requests.json reviewStatus (so platform-side approves auto-promote local status). When false (default), the verdict is recorded in pull-requests.json reviewStatus from the agent's completion only — informational. NEGATIVE-correction (dismissing the engine's own prior REQUEST_CHANGES / resetting its own prior negative ADO vote on a verdict flip to APPROVE) is correctness and runs UNCONDITIONALLY regardless of this flag.
|
|
1756
1765
|
|
|
1757
1766
|
// ── Runtime fleet (P-3b8e5f1d) ──────────────────────────────────────────────
|
|
1758
1767
|
// Single source of truth for which CLI runtime + model every spawn uses.
|
package/engine.js
CHANGED
|
@@ -4152,6 +4152,13 @@ async function discoverFromPrs(config, project) {
|
|
|
4152
4152
|
// The poller holds reviewStatus at 'waiting' until the reviewer acts on the new code.
|
|
4153
4153
|
const awaitingReReview = reviewStatus === 'waiting' && !!pr.minionsReview?.fixedAt;
|
|
4154
4154
|
|
|
4155
|
+
// F8 (P-f8firstdispatch): hoisted above the first-review block so a
|
|
4156
|
+
// successful initial-review dispatch suppresses downstream fix dispatches
|
|
4157
|
+
// (human-feedback, review-feedback, build, conflict) in the same PR pass —
|
|
4158
|
+
// matches the canonical flag pattern from the adjacent human-feedback /
|
|
4159
|
+
// review-feedback / build-fix blocks below.
|
|
4160
|
+
let fixDispatched = false;
|
|
4161
|
+
|
|
4155
4162
|
// PRs needing review: evalLoop gates the entire review+fix cycle; pollEnabled ensures reviewStatus is fresh
|
|
4156
4163
|
const reviewEnabled = evalLoopEnabled && pollEnabled && autoReviewPrs;
|
|
4157
4164
|
const reReviewEnabled = evalLoopEnabled && pollEnabled && autoReReviewPrs;
|
|
@@ -4215,11 +4222,9 @@ async function discoverFromPrs(config, project) {
|
|
|
4215
4222
|
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: prBranch,
|
|
4216
4223
|
pr_author: pr.agent || '', pr_url: pr.url || '',
|
|
4217
4224
|
}, `Review ${pr.id}: ${pr.title}`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
4218
|
-
if (item) { newWork.push(item); }
|
|
4225
|
+
if (item) { newWork.push(item); fixDispatched = true; }
|
|
4219
4226
|
}
|
|
4220
4227
|
|
|
4221
|
-
let fixDispatched = false;
|
|
4222
|
-
|
|
4223
4228
|
// Fresh reviewer comments are actionable fixes, even while the PR is otherwise
|
|
4224
4229
|
// awaiting a stale-vote re-review or has build-fix retries escalated.
|
|
4225
4230
|
const humanFixBaseKey = `human-fix-${project?.name || 'default'}-${prDisplayId}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2018",
|
|
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"
|