@yemi33/minions 0.1.1702 → 0.1.1703

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1703 (2026-05-04)
4
+
5
+ ### Features
6
+ - isolate review metadata
7
+
3
8
  ## 0.1.1702 (2026-05-04)
4
9
 
5
10
  ### Other
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-04T07:21:09.935Z"
4
+ "cachedAt": "2026-05-04T07:34:20.525Z"
5
5
  }
@@ -1273,12 +1273,31 @@ function reviewPrRefFromCompletion(completion) {
1273
1273
  return value;
1274
1274
  }
1275
1275
 
1276
+ function reviewPrRefMatchesDispatchTarget(reportedPr, dispatchPr, project) {
1277
+ if (!reportedPr || !dispatchPr) return true;
1278
+ const reportedUrl = typeof reportedPr === 'object' ? reportedPr.url || '' : String(reportedPr || '');
1279
+ const dispatchUrl = typeof dispatchPr === 'object' ? dispatchPr.url || '' : String(dispatchPr || '');
1280
+ const reportedId = shared.getCanonicalPrId(project, reportedPr, reportedUrl);
1281
+ const dispatchId = shared.getCanonicalPrId(project, dispatchPr, dispatchUrl);
1282
+ if (!reportedId || !dispatchId || reportedId === dispatchId) return true;
1283
+
1284
+ const reportedNumber = shared.getPrNumber(reportedPr);
1285
+ const dispatchNumber = shared.getPrNumber(dispatchPr);
1286
+ if (reportedNumber == null || dispatchNumber == null || reportedNumber !== dispatchNumber) return false;
1287
+
1288
+ const reportedScoped = !/^PR-\d+$/i.test(reportedId);
1289
+ const dispatchScoped = !/^PR-\d+$/i.test(dispatchId);
1290
+ return !(reportedScoped && dispatchScoped);
1291
+ }
1292
+
1276
1293
  function centralPrPath() {
1277
1294
  return path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json');
1278
1295
  }
1279
1296
 
1280
1297
  function resolveReviewPrContext(pr, project, config, structuredCompletion = null) {
1281
- const refs = [pr, reviewPrRefFromCompletion(structuredCompletion)].filter(Boolean);
1298
+ const reportedPr = reviewPrRefFromCompletion(structuredCompletion);
1299
+ if (reportedPr && pr && !reviewPrRefMatchesDispatchTarget(reportedPr, pr, project)) return null;
1300
+ const refs = reportedPr ? [reportedPr] : [pr].filter(Boolean);
1282
1301
  if (refs.length === 0) return null;
1283
1302
 
1284
1303
  const projects = shared.getProjects(config);
@@ -1292,6 +1311,8 @@ function resolveReviewPrContext(pr, project, config, structuredCompletion = null
1292
1311
  const prPath = shared.projectPrPath(candidateProject);
1293
1312
  const prs = safeJson(prPath) || [];
1294
1313
  for (const ref of refs) {
1314
+ const refUrl = typeof ref === 'object' ? ref.url || '' : String(ref || '');
1315
+ if (!shared.isPrCompatibleWithProject(candidateProject, ref, refUrl)) continue;
1295
1316
  const target = shared.findPrRecord(prs, ref, candidateProject);
1296
1317
  if (target) return { pr: { ...target }, project: candidateProject, prPath };
1297
1318
  }
@@ -1299,11 +1320,14 @@ function resolveReviewPrContext(pr, project, config, structuredCompletion = null
1299
1320
 
1300
1321
  const centralPath = centralPrPath();
1301
1322
  const centralPrs = safeJson(centralPath) || [];
1302
- for (const ref of refs) {
1323
+ const centralRefs = reportedPr ? [reportedPr] : refs;
1324
+ for (const ref of centralRefs) {
1303
1325
  const target = shared.findPrRecord(centralPrs, ref, null);
1304
1326
  if (target) return { pr: { ...target }, project: null, prPath: centralPath };
1305
1327
  }
1306
1328
 
1329
+ if (reportedPr) return null;
1330
+
1307
1331
  return pr?.id
1308
1332
  ? { pr, project: project || null, prPath: project ? shared.projectPrPath(project) : centralPath }
1309
1333
  : null;
@@ -1312,6 +1336,12 @@ function resolveReviewPrContext(pr, project, config, structuredCompletion = null
1312
1336
  async function updatePrAfterReview(agentId, pr, project, config, resultSummary, structuredCompletion = null, dispatchItem = null) {
1313
1337
 
1314
1338
  if (!config) config = getConfig();
1339
+ const completionStatus = normalizeCompletionStatus(structuredCompletion?.status);
1340
+ if (completionStatus && NON_TERMINAL_COMPLETION_STATUSES.has(completionStatus)) {
1341
+ const target = pr?.id || reviewPrRefFromCompletion(structuredCompletion) || 'unknown PR';
1342
+ log('warn', `Skipping review update for ${target}: completion status is ${structuredCompletion.status}`);
1343
+ return;
1344
+ }
1315
1345
  const reviewContext = resolveReviewPrContext(pr, project, config, structuredCompletion);
1316
1346
  if (!reviewContext?.pr?.id) {
1317
1347
  const reportedPr = reviewPrRefFromCompletion(structuredCompletion);
@@ -1349,6 +1379,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
1349
1379
  }
1350
1380
 
1351
1381
  let updatedTarget = null;
1382
+ const reviewNote = String(resultSummary || '').trim();
1352
1383
  shared.mutateJsonFileLocked(prPath, (prs) => {
1353
1384
  if (!Array.isArray(prs)) return prs;
1354
1385
  const target = shared.findPrRecord(prs, reviewPr, reviewProject);
@@ -1365,7 +1396,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
1365
1396
  target.minionsReview = {
1366
1397
  reviewer: reviewerName,
1367
1398
  reviewedAt: ts(),
1368
- note: resultSummary || '',
1399
+ note: reviewNote,
1369
1400
  dispatchId: dispatchItem?.id || structuredCompletion?.dispatchId || null,
1370
1401
  sourceItem: dispatchItem?.meta?.item?.id || null,
1371
1402
  // Preserve fixedAt across re-reviews so the poller guard knows a fix was pushed.
@@ -1388,7 +1419,14 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
1388
1419
  }
1389
1420
 
1390
1421
  log('info', `Updated ${reviewPr.id} → minions review: ${postReviewStatus || 'waiting'} by ${reviewerName}`);
1391
- if (updatedTarget) createReviewFeedbackForAuthor(agentId, updatedTarget, config, { dispatchItem, structuredCompletion });
1422
+ if (updatedTarget) {
1423
+ createReviewFeedbackForAuthor(agentId, updatedTarget, config, {
1424
+ reviewContent: reviewNote,
1425
+ project: reviewProject,
1426
+ dispatchId: dispatchItem?.id || structuredCompletion?.dispatchId || null,
1427
+ sourceItem: dispatchItem?.meta?.item?.id || null,
1428
+ });
1429
+ }
1392
1430
  }
1393
1431
 
1394
1432
  function updatePrAfterFix(pr, project, source) {
@@ -1838,28 +1876,35 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config, opts = {}) {
1838
1876
  const authorAgentId = pr.agent.toLowerCase();
1839
1877
  if (!config.agents[authorAgentId]) return;
1840
1878
  const today = dateStamp();
1841
- const inboxFiles = getInboxFiles();
1842
- const reviewFiles = inboxFiles.filter(f => f.includes(reviewerAgentId) && f.includes(today));
1843
- if (reviewFiles.length === 0) return;
1844
- const matchedReviewContent = [];
1845
- for (const f of reviewFiles) {
1846
- const content = safeRead(path.join(INBOX_DIR, f));
1847
- if (!content) continue;
1848
- if (!reviewFeedbackSourceMatches({
1849
- fileName: f,
1850
- content,
1851
- reviewerAgentId,
1852
- pr,
1853
- dispatchItem: opts.dispatchItem,
1854
- structuredCompletion: opts.structuredCompletion,
1855
- })) continue;
1856
- matchedReviewContent.push(content);
1857
- }
1858
- if (matchedReviewContent.length === 0) return;
1859
- const reviewContent = matchedReviewContent.join('\n\n');
1879
+ const project = opts.project || opts.dispatchItem?.meta?.project || null;
1880
+ let reviewContent = String(opts.reviewContent || '').trim();
1881
+ if (reviewContent) {
1882
+ if (!reviewContentMatchesPr(reviewContent, pr, project)) {
1883
+ log('warn', `Skipped review feedback for ${pr.id}: review content references a different PR`);
1884
+ return;
1885
+ }
1886
+ } else {
1887
+ const inboxFiles = getInboxFiles();
1888
+ const reviewFiles = inboxFiles.filter(f => f.includes(reviewerAgentId) && f.includes(today));
1889
+ if (reviewFiles.length === 0) return;
1890
+ const matchedReviewContent = [];
1891
+ for (const f of reviewFiles) {
1892
+ const content = safeRead(path.join(INBOX_DIR, f));
1893
+ if (!content) continue;
1894
+ if (!reviewFeedbackSourceMatches({
1895
+ fileName: f,
1896
+ content,
1897
+ reviewerAgentId,
1898
+ pr,
1899
+ dispatchItem: opts.dispatchItem,
1900
+ structuredCompletion: opts.structuredCompletion,
1901
+ })) continue;
1902
+ matchedReviewContent.push(content);
1903
+ }
1904
+ if (matchedReviewContent.length === 0) return;
1905
+ reviewContent = matchedReviewContent.join('\n\n');
1906
+ }
1860
1907
  const prSlug = shared.safeSlugComponent(pr.id, 60);
1861
- const feedbackFile = `feedback-${authorAgentId}-from-${reviewerAgentId}-${prSlug}-${today}.md`;
1862
- const feedbackPath = shared.uniquePath(path.join(INBOX_DIR, feedbackFile));
1863
1908
  const content = `# Review Feedback for ${config.agents[authorAgentId]?.name || authorAgentId}\n\n` +
1864
1909
  `**PR:** ${pr.id} — ${pr.title || ''}\n` +
1865
1910
  `**Reviewer:** ${config.agents[reviewerAgentId]?.name || reviewerAgentId}\n` +
@@ -1868,7 +1913,14 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config, opts = {}) {
1868
1913
  `## Action Required\n\nRead this feedback carefully. When you work on similar tasks in the future, ` +
1869
1914
  `avoid the patterns flagged here. If you are assigned to fix this PR, ` +
1870
1915
  `address every point raised above.\n`;
1871
- shared.safeWrite(feedbackPath, content);
1916
+ shared.writeToInbox('feedback', `${authorAgentId}-from-${reviewerAgentId}-${prSlug}`, content, null, {
1917
+ sourcePr: pr.id,
1918
+ reviewer: reviewerAgentId,
1919
+ author: authorAgentId,
1920
+ dispatchId: opts.dispatchId || opts.dispatchItem?.id || opts.structuredCompletion?.dispatchId || null,
1921
+ sourceItem: opts.sourceItem || opts.dispatchItem?.meta?.item?.id || null,
1922
+ project: project?.name || null,
1923
+ });
1872
1924
  log('info', `Created review feedback for ${authorAgentId} from ${reviewerAgentId} on ${pr.id}`);
1873
1925
  }
1874
1926
 
@@ -2235,6 +2287,33 @@ function reviewVerdictFromCompletion(completion) {
2235
2287
  return normalizeReviewVerdict(completion.verdict || completion.review_verdict || completion.reviewVerdict);
2236
2288
  }
2237
2289
 
2290
+ function reviewContentMatchesPr(content, pr, project) {
2291
+ const text = String(content || '').trim();
2292
+ if (!text) return false;
2293
+ const targetId = shared.getCanonicalPrId(project, pr, pr?.url || '');
2294
+ const targetNumber = shared.getPrNumber(pr);
2295
+ if (!targetId) return true;
2296
+
2297
+ const explicitRefs = new Set();
2298
+ for (const match of text.matchAll(/\b(?:github|ado):[A-Za-z0-9._~/-]+#\d+\b/g)) {
2299
+ explicitRefs.add(shared.getCanonicalPrId(project, match[0]));
2300
+ }
2301
+ for (const match of text.matchAll(/https?:\/\/[^\s)>"]+(?:\/pull\/|\/pullrequest\/)\d+[^\s)>"]*/gi)) {
2302
+ const url = match[0].replace(/[.,;:]+$/g, '');
2303
+ explicitRefs.add(shared.getCanonicalPrId(project, url, url));
2304
+ }
2305
+ if (explicitRefs.size > 0) return explicitRefs.size === 1 && explicitRefs.has(targetId);
2306
+
2307
+ const mentionedNumbers = new Set();
2308
+ for (const match of text.matchAll(/\bPR\s*(?:#|-)\s*(\d+)\b/gi)) {
2309
+ mentionedNumbers.add(parseInt(match[1], 10));
2310
+ }
2311
+ if (mentionedNumbers.size > 0 && targetNumber != null) {
2312
+ return mentionedNumbers.size === 1 && mentionedNumbers.has(targetNumber);
2313
+ }
2314
+ return true;
2315
+ }
2316
+
2238
2317
  function writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredCompletion, resultSummary, exitCode) {
2239
2318
  if (!dispatchItem?.id || !outcome) {
2240
2319
  log('warn', 'Cannot write non-clean agent report without dispatch id and outcome');
@@ -2663,7 +2742,10 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
2663
2742
  // (retryCount was being deleted by done-marking before the check could read it)
2664
2743
  // Review verdict check similarly moved before updateWorkItemStatus(DONE) — same root cause.
2665
2744
 
2666
- if (type === WORK_TYPE.REVIEW && effectiveSuccess && !skipDoneStatus) {
2745
+ const hardContractFail = completionContractFailure?.severity === 'hard'
2746
+ || completionContractFailure?.nonTerminal === true;
2747
+ const finalResult = hardContractFail ? DISPATCH_RESULT.ERROR : (effectiveSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR);
2748
+ if (type === WORK_TYPE.REVIEW && finalResult === DISPATCH_RESULT.SUCCESS && !skipDoneStatus) {
2667
2749
  await updatePrAfterReview(agentId, meta?.pr, meta?.project, config, resultSummary, structuredCompletion, dispatchItem);
2668
2750
  } else if (type === WORK_TYPE.REVIEW) {
2669
2751
  log('warn', `Skipping PR review metadata update for ${meta?.pr?.id || meta?.pr?.url || '(unknown PR)'} because review dispatch ${dispatchItem.id} did not complete cleanly`);
@@ -2686,9 +2768,6 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
2686
2768
  }
2687
2769
  }
2688
2770
  checkForLearnings(agentId, config.agents[agentId], dispatchItem.task);
2689
- const hardContractFail = completionContractFailure?.severity === 'hard'
2690
- || completionContractFailure?.nonTerminal === true;
2691
- const finalResult = hardContractFail ? DISPATCH_RESULT.ERROR : (effectiveSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR);
2692
2771
  if (finalResult === DISPATCH_RESULT.SUCCESS) {
2693
2772
  extractSkillsFromOutput(stdout, agentId, dispatchItem, config);
2694
2773
  // Also scan inbox notes for skill blocks — agents often write skills to inbox, not stdout
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1702",
3
+ "version": "0.1.1703",
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"