@yemi33/minions 0.1.1911 → 0.1.1912

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.
@@ -0,0 +1,5 @@
1
+ {
2
+ "runtime": "copilot",
3
+ "models": null,
4
+ "cachedAt": "2026-05-13T19:11:26.190Z"
5
+ }
@@ -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 (_isAgentPositiveSignalComment(c, config)) return true;
89
+ if (_isMinionsAuthoredComment(c)) return true;
89
90
  return false;
90
91
  }
91
92
 
92
- // P-a3f9b2c1Detect agent self-positive-signal comments (verification SUCCESS reports,
93
- // VERDICT:APPROVE recaps, noop:true confessions) posted via the shared `gh` PAT identity.
93
+ // W-mp3bp0ha000997ab-bStructural classifier for Minions-authored PR comments.
94
94
  //
95
- // Sibling to PR #2431's `_isAgentSelfReviewDeclinedComment` but keys off body-shape markers
96
- // + an opt-in shared-minions login allowlist (REST `/issues/:n/comments` doesn't expose
97
- // GraphQL's `viewerDidAuthor` flag, so we approximate via configured logins).
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
- // Configuration:
100
- // engine.botCommentLogins: string[] — primary opt-in list (e.g. ['yemi33'])
101
- // engine.ignoredCommentAuthors: string[] fallback (back-compat with existing knob)
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
- // Reference: PR #2433 (3 consecutive noop fix-dispatches when verify agents posted
104
- // "Result: SUCCESS" comments via the shared yemi33 PAT identity).
105
- function _sharedMinionsLogins(config = {}) {
106
- const primary = Array.isArray(config?.engine?.botCommentLogins) ? config.engine.botCommentLogins : [];
107
- const fallback = Array.isArray(config?.engine?.ignoredCommentAuthors) ? config.engine.ignoredCommentAuthors : [];
108
- return new Set([...primary, ...fallback].map(s => String(s || '').toLowerCase()).filter(Boolean));
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
- // Pure body-shape matcher for agent positive-signal markers in PR comments.
112
- // Triggers on:
113
- // - "Verification SUCCESS" / "Verification SUCCEEDED" / "Verification ran" (verify-dispatch reports)
114
- // - "Result: SUCCESS" (verify-dispatch report header per playbooks/verify.md)
115
- // - "VERDICT: APPROVE" (review-loop positive verdict defense-in-depth alongside _hasMinionsReviewVerdict)
116
- // - `noop: true` / `"noop": true` (completion-report-style noop confession copied to comment)
117
- function _hasAgentPositiveSignalBody(body) {
118
- const text = String(body || '');
119
- if (!text) return false;
120
- if (/(?:^|\n)\s*\*{0,2}VERDICT[:\s]+\*{0,2}APPROVE\b/i.test(text)) return true;
121
- if (/"?\bnoop\b"?\s*:\s*true\b/i.test(text)) return true;
122
- if (/\bVerification\s+(?:SUCCESS(?:FUL|FULLY)?|SUCCEEDED|ran)\b/i.test(text)) return true;
123
- if (/(?:^|\n)\s*#{0,3}\s*Result\s*:\s*SUCCESS\b/i.test(text)) return true;
124
- return false;
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
- function _isAgentPositiveSignalComment(c, config = {}) {
128
- if (!c) return false;
129
- const sharedLogins = _sharedMinionsLogins(config);
130
- if (sharedLogins.size === 0) return false;
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
- // W-mp2h696g000a7bc0 Detect agent self-review-declined no-op comments.
137
- //
138
- // Per the documented contract (memory `subject: self-review`, docs/completion-reports.md:83-105):
139
- // when an agent is dispatched to review a PR they implemented, they post a `gh pr comment`
140
- // (no `VERDICT:` line) explaining the recusal and complete with `noop:true, verdict:null,
141
- // needs_rerun:true`. Because `gh` authenticates as the shared PAT user (`yemi33`), the
142
- // classifier can't tell the noop comment from a real human comment by author alone.
143
- //
144
- // Detection requires a stronger signal than author identity. Returns true only when BOTH:
145
- // 1. Body has NO `VERDICT:` line (we never want to swallow a real verdict comment), AND
146
- // 2. EITHER the body contains the canonical "Self-review declined" phrase AND references
147
- // a dispatch id matching `<agent>-<type>-<uid>` that resolves to a same-agent
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
- // Used by `_isAgentSelfReviewDeclinedComment` to verify dispatch ids referenced in
825
- // candidate noop comments belong to the PR author.
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-mp2h696g000a7bc0agent self-review-declined no-op comments are posted via
857
- // `gh pr comment` and authenticate as the shared PAT user, so author identity alone
858
- // can't distinguish them from real human feedback. The narrow allowlist check uses
859
- // the comment body + dispatch history to identify the documented noop pattern.
860
- const isNonActionable = _isNonActionableComment(c, config)
861
- || _isAgentSelfReviewDeclinedComment(c, { pr, dispatch });
830
+ // W-mp3bp0ha000997ab-bAll 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
- _isAgentSelfReviewDeclinedComment, // exported for testing
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1911",
3
+ "version": "0.1.1912",
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"