@yemi33/minions 0.1.1903 → 0.1.1905

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 CHANGED
@@ -73,19 +73,9 @@ function ensureConfiguredProjectStateFiles() {
73
73
  const root = p.localPath ? path.resolve(p.localPath) : null;
74
74
  if (!root || !fs.existsSync(root)) continue;
75
75
  try {
76
- const state = shared.ensureProjectStateFiles(p, { migrateLegacy: true, removeLegacy: true });
77
- if (state.migrated.length > 0 || state.removedLegacy.length > 0 || state.legacyDirRemoved) {
78
- const parts = [];
79
- if (state.migrated.length > 0) parts.push(`merged ${state.migrated.join(', ')}`);
80
- if (state.removedLegacy.length > 0) parts.push(`removed ${state.removedLegacy.join(', ')}`);
81
- if (state.legacyDirRemoved) parts.push(`removed legacy .minions dir`);
82
- console.log(`[dashboard] migrated project state for "${p.name}" → projects/${p.name} (${parts.join('; ')})`);
83
- }
84
- if (state.legacyDirRemoveError) {
85
- console.warn(`[dashboard] failed to remove legacy .minions dir for "${p.name}": ${state.legacyDirRemoveError}`);
86
- }
76
+ shared.ensureProjectStateFiles(p);
87
77
  } catch (e) {
88
- console.warn(`[dashboard] project state migration failed for "${p.name}": ${e.message}`);
78
+ console.warn(`[dashboard] project state setup failed for "${p.name}": ${e.message}`);
89
79
  }
90
80
  }
91
81
  }
@@ -5716,9 +5706,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5716
5706
  prUrlBase: detected.prUrlBase,
5717
5707
  });
5718
5708
 
5719
- // Create centralized project state and migrate any legacy project-local
5720
- // .minions state without leaving repo-local state files behind.
5721
- shared.ensureProjectStateFiles(project, { migrateLegacy: true, removeLegacy: true });
5709
+ // Create centralized project state files.
5710
+ shared.ensureProjectStateFiles(project);
5722
5711
 
5723
5712
  let duplicate = false;
5724
5713
  mutateDashboardConfig(config => {
@@ -1,10 +1 @@
1
- [
2
- {
3
- "id": "project-local-minions-state",
4
- "summary": "Project-local .minions directories are migrated to central projects/<name>/ state and removed",
5
- "deprecated": "2026-05-06",
6
- "reason": "Project repos should not receive Minions runtime state because .minions can leak into source repositories.",
7
- "locations": ["engine/shared.js ensureProjectStateFiles legacy migration", "dashboard.js handleProjectsAdd/dashboard startup", "minions.js addProject/scanAndAdd", "engine/cli.js start migration", "bin/minions.js user-scoped init root"],
8
- "cleanup": "Remove legacyProjectStateDir/legacyProjectStatePath migration/deletion after existing project-local .minions directories have been migrated."
9
- }
10
- ]
1
+ []
package/engine/cli.js CHANGED
@@ -468,20 +468,9 @@ const commands = {
468
468
  console.log(` WARNING: ${p.name} path not found: ${p.localPath}`);
469
469
  } else {
470
470
  try {
471
- const state = shared.ensureProjectStateFiles(p, { migrateLegacy: true, removeLegacy: true });
472
- if (state.migrated.length > 0 || state.removedLegacy.length > 0 || state.legacyDirRemoved) {
473
- const parts = [];
474
- if (state.migrated.length > 0) parts.push(`merged ${state.migrated.join(', ')}`);
475
- if (state.removedLegacy.length > 0) parts.push(`removed ${state.removedLegacy.join(', ')}`);
476
- if (state.legacyDirRemoved) parts.push(`removed legacy .minions dir`);
477
- e.log('info', `Migrated project state for "${p.name}" → projects/${p.name} (${parts.join('; ')})`);
478
- }
479
- if (state.legacyDirRemoveError) {
480
- e.log('warn', `Failed to remove legacy .minions dir for "${p.name}": ${state.legacyDirRemoveError}`);
481
- console.log(` WARNING: legacy .minions dir for "${p.name}" could not be removed: ${state.legacyDirRemoveError}`);
482
- }
471
+ shared.ensureProjectStateFiles(p);
483
472
  } catch (err) {
484
- e.log('warn', `Project state migration failed for "${p.name}": ${err.message}`);
473
+ e.log('warn', `Project state setup failed for "${p.name}": ${err.message}`);
485
474
  }
486
475
  console.log(` Project: ${p.name} (${root})`);
487
476
  }
package/engine/github.js CHANGED
@@ -85,9 +85,54 @@ function _isNonActionableComment(c, config = {}) {
85
85
  if (_isAgentComment(c)) return true;
86
86
  if (_isCiReportCommentBody(c?.body)) return true;
87
87
  if (_isPreviewStatusComment(c)) return true;
88
+ if (_isAgentPositiveSignalComment(c, config)) return true;
88
89
  return false;
89
90
  }
90
91
 
92
+ // P-a3f9b2c1 — Detect agent self-positive-signal comments (verification SUCCESS reports,
93
+ // VERDICT:APPROVE recaps, noop:true confessions) posted via the shared `gh` PAT identity.
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).
98
+ //
99
+ // Configuration:
100
+ // engine.botCommentLogins: string[] — primary opt-in list (e.g. ['yemi33'])
101
+ // engine.ignoredCommentAuthors: string[] — fallback (back-compat with existing knob)
102
+ //
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));
109
+ }
110
+
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;
125
+ }
126
+
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);
134
+ }
135
+
91
136
  // W-mp2h696g000a7bc0 — Detect agent self-review-declined no-op comments.
92
137
  //
93
138
  // Per the documented contract (memory `subject: self-review`, docs/completion-reports.md:83-105):
@@ -838,6 +883,26 @@ async function pollPrHumanComments(config) {
838
883
  }
839
884
  if (newComments.length === 0) return false;
840
885
 
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
+
841
906
  // Sort all comments chronologically and build full context for the fix agent
842
907
  allCommentEntries.sort((a, b) => a.date.localeCompare(b.date));
843
908
  newComments.sort((a, b) => a.date.localeCompare(b.date));
@@ -1136,5 +1201,8 @@ module.exports = {
1136
1201
  _isAgentComment, // exported for testing
1137
1202
  _isNonActionableComment, // exported for testing
1138
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)
1139
1207
  _isPreviewStatusComment, // exported for testing
1140
1208
  };
package/engine/shared.js CHANGED
@@ -1099,6 +1099,7 @@ const ENGINE_DEFAULTS = {
1099
1099
  autoCompletePrs: false, // auto-merge PRs when builds green + review approved (opt-in)
1100
1100
  prMergeMethod: 'squash', // merge method: squash, merge, rebase
1101
1101
  ignoredCommentAuthors: [], // comments from these authors are auto-closed and never trigger fixes
1102
+ 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.
1102
1103
  agentBusyReassignMs: 600000, // 10min — reassign work item to another agent if preferred agent is busy beyond this threshold
1103
1104
  ccEffort: null, // effort level for CC/doc-chat (null, 'low', 'medium', 'high')
1104
1105
  enablePreDispatchEval: false, // opt-in: cheap LLM gate before queueing — see engine/pre-dispatch-eval.js (Ripley §3 recommendation, 2026-05-11 architecture review)
@@ -1967,9 +1968,6 @@ function resolveProjectSource(source, projectsOrConfig, options = {}) {
1967
1968
  projectStateDir(project),
1968
1969
  projectWorkItemsPath(project),
1969
1970
  projectPrPath(project),
1970
- legacyProjectStateDir(project),
1971
- legacyProjectStatePath(project, 'work-items.json'),
1972
- legacyProjectStatePath(project, 'pull-requests.json'),
1973
1971
  ].filter(Boolean);
1974
1972
  if (candidates.some(candidate => _sameSourcePath(value, candidate, minionsDir))) {
1975
1973
  return _projectSourceDescriptor(project, value, explicit, minionsDir);
@@ -2023,36 +2021,6 @@ function projectPrPath(project) {
2023
2021
  return path.join(projectStateDir(project), 'pull-requests.json');
2024
2022
  }
2025
2023
 
2026
- function legacyProjectStateDir(project) {
2027
- if (!project?.localPath) return null;
2028
- return path.join(path.resolve(project.localPath), '.minions');
2029
- }
2030
-
2031
- function legacyProjectStatePath(project, fileName) {
2032
- const dir = legacyProjectStateDir(project);
2033
- return dir ? path.join(dir, fileName) : null;
2034
- }
2035
-
2036
- function projectStateRecordKey(record) {
2037
- if (record && typeof record === 'object') {
2038
- const id = record.id ?? record.prId ?? record.workItemId ?? record.url ?? record.number;
2039
- if (id !== undefined && id !== null && String(id).trim()) return String(id);
2040
- }
2041
- try { return JSON.stringify(record); } catch { return String(record); }
2042
- }
2043
-
2044
- function mergeProjectStateArrays(current, legacy) {
2045
- const merged = Array.isArray(current) ? current.slice() : [];
2046
- const seen = new Set(merged.map(projectStateRecordKey));
2047
- for (const entry of Array.isArray(legacy) ? legacy : []) {
2048
- const key = projectStateRecordKey(entry);
2049
- if (seen.has(key)) continue;
2050
- merged.push(entry);
2051
- seen.add(key);
2052
- }
2053
- return merged;
2054
- }
2055
-
2056
2024
  function sameResolvedPath(a, b) {
2057
2025
  if (!a || !b) return false;
2058
2026
  try {
@@ -2064,64 +2032,20 @@ function sameResolvedPath(a, b) {
2064
2032
  }
2065
2033
  }
2066
2034
 
2067
- function removeLegacyProjectStateDir(project) {
2068
- const dir = legacyProjectStateDir(project);
2069
- if (!dir) return { removed: false, error: null };
2070
- if (sameResolvedPath(dir, MINIONS_DIR)) return { removed: false, error: null };
2071
- if (!fs.existsSync(dir)) return { removed: false, error: null };
2072
- try {
2073
- fs.rmSync(dir, { recursive: true, force: true });
2074
- return { removed: true, error: null };
2075
- } catch (err) {
2076
- return { removed: false, error: err && err.message ? err.message : String(err) };
2077
- }
2078
- }
2079
-
2080
- function ensureProjectStateFiles(project, options = {}) {
2081
- const migrateLegacy = options.migrateLegacy !== false;
2082
- const removeLegacy = options.removeLegacy === true;
2035
+ function ensureProjectStateFiles(project) {
2083
2036
  const files = [
2084
2037
  { name: 'pull-requests.json', centralPath: projectPrPath(project) },
2085
2038
  { name: 'work-items.json', centralPath: projectWorkItemsPath(project) },
2086
2039
  ];
2087
- const result = { created: [], migrated: [], removedLegacy: [], legacyDirRemoved: false, legacyDirRemoveError: null };
2040
+ const result = { created: [] };
2088
2041
 
2089
2042
  projectStateDirEnsure(project);
2090
2043
  for (const file of files) {
2091
- const legacyPath = legacyProjectStatePath(project, file.name);
2092
- const hasLegacyState = legacyPath && (fs.existsSync(legacyPath) || fs.existsSync(legacyPath + '.backup'));
2093
- const legacyData = migrateLegacy && hasLegacyState ? safeJson(legacyPath) : null;
2094
-
2095
- if (Array.isArray(legacyData)) {
2096
- let changed = false;
2097
- mutateJsonFileLocked(file.centralPath, current => {
2098
- const merged = mergeProjectStateArrays(Array.isArray(current) ? current : [], legacyData);
2099
- changed = JSON.stringify(merged) !== JSON.stringify(current);
2100
- return merged;
2101
- }, { defaultValue: [], skipWriteIfUnchanged: true });
2102
- if (changed && legacyData.length > 0) result.migrated.push(file.name);
2103
- if (removeLegacy) {
2104
- try {
2105
- fs.unlinkSync(legacyPath);
2106
- result.removedLegacy.push(file.name);
2107
- } catch (err) {
2108
- if (!err || err.code !== 'ENOENT') throw err;
2109
- }
2110
- safeUnlink(legacyPath + '.backup');
2111
- }
2112
- }
2113
-
2114
2044
  if (!fs.existsSync(file.centralPath)) {
2115
2045
  mutateJsonFileLocked(file.centralPath, data => Array.isArray(data) ? data : [], { defaultValue: [] });
2116
2046
  result.created.push(file.name);
2117
2047
  }
2118
2048
  }
2119
-
2120
- if (removeLegacy) {
2121
- const res = removeLegacyProjectStateDir(project);
2122
- result.legacyDirRemoved = res.removed;
2123
- result.legacyDirRemoveError = res.error;
2124
- }
2125
2049
  return result;
2126
2050
  }
2127
2051
 
@@ -3620,12 +3544,8 @@ module.exports = {
3620
3544
  projectStateDirEnsure,
3621
3545
  projectWorkItemsPath,
3622
3546
  projectPrPath,
3623
- legacyProjectStateDir,
3624
- legacyProjectStatePath,
3625
3547
  ensureProjectStateFiles,
3626
3548
  sameResolvedPath,
3627
- projectStateRecordKey, // exported for testing
3628
- mergeProjectStateArrays, // exported for testing
3629
3549
  realPathForComparison, // exported for testing
3630
3550
  prPathComparisonCandidates, // exported for testing
3631
3551
  resolveProjectForPrPath, // exported for testing
package/minions.js CHANGED
@@ -135,14 +135,11 @@ async function addProject(targetDir) {
135
135
  name, description, localPath: target, repoHost, repositoryId, org, project, repoName, mainBranch,
136
136
  prUrlBase: detected.prUrlBase,
137
137
  });
138
- const state = shared.ensureProjectStateFiles(projectEntry, { migrateLegacy: true, removeLegacy: true });
138
+ shared.ensureProjectStateFiles(projectEntry);
139
139
  config.projects.push(projectEntry);
140
140
  saveConfig(config);
141
141
 
142
142
  console.log(`\n Linked "${name}" (${target})`);
143
- if (state.migrated.length > 0 || state.removedLegacy.length > 0) {
144
- console.log(` Migrated project state to ${shared.projectStateDir(projectEntry)}`);
145
- }
146
143
  console.log(` Total projects: ${config.projects.length}`);
147
144
  console.log(`\n Start the minions from anywhere:`);
148
145
  console.log(` node ${MINIONS_HOME}/engine.js # Engine`);
@@ -359,7 +356,7 @@ async function scanAndAdd({ root, depth } = {}) {
359
356
  repoHost: repo.host, repositoryId: repo.repositoryId, org: repo.org, project: repo.project,
360
357
  repoName: repo.repoName, mainBranch: repo.mainBranch, prUrlBase: repo.prUrlBase,
361
358
  });
362
- shared.ensureProjectStateFiles(projectEntry, { migrateLegacy: true, removeLegacy: true });
359
+ shared.ensureProjectStateFiles(projectEntry);
363
360
  config.projects.push(projectEntry);
364
361
  console.log(` + ${name} (${repo.path})`);
365
362
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1903",
3
+ "version": "0.1.1905",
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"