@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 +4 -15
- package/docs/deprecated.json +1 -10
- package/engine/cli.js +2 -13
- package/engine/github.js +68 -0
- package/engine/shared.js +3 -83
- package/minions.js +2 -5
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
5720
|
-
|
|
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 => {
|
package/docs/deprecated.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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: []
|
|
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
|
-
|
|
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
|
|
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.
|
|
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"
|