@yemi33/minions 0.1.1714 → 0.1.1716

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,10 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1714 (2026-05-04)
3
+ ## 0.1.1716 (2026-05-04)
4
+
5
+ ### Features
6
+ - add CC post-dispatch stop rule (#2057)
7
+
8
+ ## 0.1.1715 (2026-05-04)
9
+
10
+ ### Features
11
+ - auto-repair ADO project metadata (#2052)
12
+
13
+ ## 0.1.1713 (2026-05-04)
4
14
 
5
15
  ### Features
6
- - loosen work item create dedupe (#2053)
7
- - shorten Windows worktree paths (#2051)
8
16
  - fix ado project metadata discovery (#2048)
9
17
 
10
18
  ## 0.1.1712 (2026-05-04)
package/engine/ado.js CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  const path = require('path');
7
+ const childProcess = require('child_process');
7
8
  const shared = require('./shared');
8
9
  const { exec, execAsync, getAdoOrgBase, log, ts, dateStamp, PR_STATUS, createThrottleTracker } = shared;
9
10
  const { getPrs } = require('./queries');
@@ -74,6 +75,207 @@ function sameAdoProject(a, b) {
74
75
  && String(a.repoHost || 'ado').toLowerCase() === String(b.repoHost || 'ado').toLowerCase();
75
76
  }
76
77
 
78
+ function decodeUrlSegment(segment) {
79
+ try { return decodeURIComponent(segment); } catch { return segment; }
80
+ }
81
+
82
+ function stripGitSuffix(value) {
83
+ return String(value || '').trim().replace(/\.git$/i, '');
84
+ }
85
+
86
+ function encodeAdoPathSegment(value) {
87
+ return encodeURIComponent(String(value || '').trim());
88
+ }
89
+
90
+ function buildAdoPrUrlBase(meta) {
91
+ const repo = meta.repoName || meta.repositoryId || '';
92
+ if (!meta.adoOrg || !meta.adoProject || !repo) return '';
93
+ if (meta.host === 'visualstudio') {
94
+ const collection = meta.defaultCollection ? 'DefaultCollection/' : '';
95
+ return `https://${meta.adoOrg}.visualstudio.com/${collection}${encodeAdoPathSegment(meta.adoProject)}/_git/${encodeAdoPathSegment(repo)}/pullrequest/`;
96
+ }
97
+ return `https://dev.azure.com/${encodeAdoPathSegment(meta.adoOrg)}/${encodeAdoPathSegment(meta.adoProject)}/_git/${encodeAdoPathSegment(repo)}/pullrequest/`;
98
+ }
99
+
100
+ function adoRepoCandidateFromParts(parts, source) {
101
+ const repo = stripGitSuffix(decodeUrlSegment(parts.repo || ''));
102
+ const repositoryId = isAdoGuid(repo) ? repo : '';
103
+ const repoName = repositoryId ? '' : repo;
104
+ const candidate = {
105
+ adoOrg: decodeUrlSegment(parts.org || '').trim(),
106
+ adoProject: decodeUrlSegment(parts.project || '').trim(),
107
+ repoName,
108
+ repositoryId,
109
+ host: parts.host || 'dev.azure',
110
+ defaultCollection: parts.defaultCollection === true,
111
+ source,
112
+ };
113
+ candidate.prUrlBase = buildAdoPrUrlBase(candidate);
114
+ return candidate;
115
+ }
116
+
117
+ function parseAdoRepoMetadata(value, source) {
118
+ const raw = String(value || '').trim();
119
+ if (!raw) return null;
120
+
121
+ const devAzure = raw.match(/https?:\/\/(?:[^/@]+@)?dev\.azure\.com\/([^/?#]+)\/([^/?#]+)\/_git\/([^/?#\s]+)/i);
122
+ if (devAzure) {
123
+ return adoRepoCandidateFromParts({
124
+ host: 'dev.azure',
125
+ org: devAzure[1],
126
+ project: devAzure[2],
127
+ repo: devAzure[3],
128
+ }, source);
129
+ }
130
+
131
+ const visualStudio = raw.match(/https?:\/\/(?:[^/@]+@)?([^/.@]+)\.visualstudio\.com\/(?:(DefaultCollection)\/)?([^/?#]+)\/_git\/([^/?#\s]+)/i);
132
+ if (visualStudio) {
133
+ return adoRepoCandidateFromParts({
134
+ host: 'visualstudio',
135
+ defaultCollection: !!visualStudio[2],
136
+ org: visualStudio[1],
137
+ project: visualStudio[3],
138
+ repo: visualStudio[4],
139
+ }, source);
140
+ }
141
+
142
+ const ssh = raw.match(/^(?:ssh:\/\/)?git@ssh\.dev\.azure\.com(?::|\/)v3\/([^/]+)\/([^/]+)\/([^/\s]+)$/i);
143
+ if (ssh) {
144
+ return adoRepoCandidateFromParts({
145
+ host: 'dev.azure',
146
+ org: ssh[1],
147
+ project: ssh[2],
148
+ repo: ssh[3],
149
+ }, source);
150
+ }
151
+
152
+ return null;
153
+ }
154
+
155
+ function parseCanonicalAdoPrId(value, source) {
156
+ const match = String(value || '').trim().match(/^ado:([^/#]+)\/([^/#]+)\/([^/#]+)#\d+$/i);
157
+ if (!match) return null;
158
+ return adoRepoCandidateFromParts({
159
+ host: 'dev.azure',
160
+ org: match[1],
161
+ project: match[2],
162
+ repo: match[3],
163
+ }, source);
164
+ }
165
+
166
+ function normalizeAdoOrgForCompare(value) {
167
+ return String(value || '').trim().toLowerCase().replace(/\.visualstudio\.com$/i, '');
168
+ }
169
+
170
+ function normalizeAdoFieldForCompare(value) {
171
+ return String(value || '').trim().toLowerCase();
172
+ }
173
+
174
+ function isAdoRepairCandidateCompatible(project, candidate) {
175
+ if (!project || !candidate) return false;
176
+ if (project.adoOrg && candidate.adoOrg
177
+ && normalizeAdoOrgForCompare(project.adoOrg) !== normalizeAdoOrgForCompare(candidate.adoOrg)) return false;
178
+ if (project.adoProject && candidate.adoProject
179
+ && normalizeAdoFieldForCompare(project.adoProject) !== normalizeAdoFieldForCompare(candidate.adoProject)) return false;
180
+ if (project.repoName && candidate.repoName
181
+ && normalizeAdoFieldForCompare(project.repoName) !== normalizeAdoFieldForCompare(candidate.repoName)) return false;
182
+ if (project.repositoryId && candidate.repositoryId
183
+ && normalizeAdoFieldForCompare(project.repositoryId) !== normalizeAdoFieldForCompare(candidate.repositoryId)) return false;
184
+ return true;
185
+ }
186
+
187
+ function getMissingAdoProjectConfigFields(project) {
188
+ return ['adoOrg', 'adoProject', 'repoName', 'prUrlBase', 'repositoryId']
189
+ .filter(field => !String(project?.[field] || '').trim());
190
+ }
191
+
192
+ function getOriginRemoteUrl(project) {
193
+ const localPath = String(project?.localPath || '').trim();
194
+ if (!localPath) return '';
195
+ try {
196
+ const result = childProcess.spawnSync('git', ['-C', localPath, 'config', '--get', 'remote.origin.url'], {
197
+ encoding: 'utf8',
198
+ windowsHide: true,
199
+ timeout: 3000,
200
+ });
201
+ if (result.status !== 0) return '';
202
+ return String(result.stdout || '').trim().split(/\r?\n/)[0] || '';
203
+ } catch {
204
+ return '';
205
+ }
206
+ }
207
+
208
+ function collectAdoRepairCandidates(project, prs = []) {
209
+ const candidates = [];
210
+ const add = (candidate) => {
211
+ if (candidate && isAdoRepairCandidateCompatible(project, candidate)) candidates.push(candidate);
212
+ };
213
+
214
+ if (project?.prUrlBase) add(parseAdoRepoMetadata(project.prUrlBase, 'configured prUrlBase'));
215
+ add(parseAdoRepoMetadata(getOriginRemoteUrl(project), 'origin remote'));
216
+
217
+ for (const pr of Array.isArray(prs) ? prs : []) {
218
+ add(parseAdoRepoMetadata(pr?.url, 'tracked PR URL'));
219
+ add(parseCanonicalAdoPrId(pr?.id, 'tracked canonical PR ID'));
220
+ }
221
+
222
+ return candidates;
223
+ }
224
+
225
+ function persistAdoProjectRepair(project, repairs, source) {
226
+ Object.assign(project, repairs);
227
+
228
+ try {
229
+ let persisted = false;
230
+ mutateJsonFileLocked(adoConfigPath(), (config) => {
231
+ if (!config || typeof config !== 'object' || Array.isArray(config)) return config;
232
+ if (!Array.isArray(config.projects)) return config;
233
+ const target = config.projects.find(p => sameAdoProject(p, project));
234
+ if (!target) return config;
235
+ for (const [field, value] of Object.entries(repairs)) {
236
+ if (!String(target[field] || '').trim()) target[field] = value;
237
+ }
238
+ persisted = true;
239
+ return config;
240
+ }, { defaultValue: { projects: [] }, skipWriteIfUnchanged: true });
241
+ const repairedFields = Object.keys(repairs).join(', ');
242
+ if (persisted) {
243
+ log('info', `Auto-repaired ADO project config for ${getAdoProjectLabel(project)} from ${source}: ${repairedFields}`);
244
+ } else {
245
+ log('warn', `Auto-repaired ADO project config for ${getAdoProjectLabel(project)} from ${source} in memory but could not find the project in config.json to persist it`);
246
+ }
247
+ } catch (e) {
248
+ log('warn', `Auto-repaired ADO project config for ${getAdoProjectLabel(project)} but failed to persist it: ${e.message}`);
249
+ }
250
+ }
251
+
252
+ function repairAdoProjectConfig(project, purpose, prs = null) {
253
+ if (!project || isGitHubProject(project)) return false;
254
+ const missingBefore = getMissingAdoProjectConfigFields(project);
255
+ if (missingBefore.length === 0) return false;
256
+
257
+ const trackedPrs = prs || shared.safeJson(shared.projectPrPath(project)) || [];
258
+ for (const candidate of collectAdoRepairCandidates(project, trackedPrs)) {
259
+ const repairs = {};
260
+ if (!project.adoOrg && candidate.adoOrg) repairs.adoOrg = candidate.adoOrg;
261
+ if (!project.adoProject && candidate.adoProject) repairs.adoProject = candidate.adoProject;
262
+ if (!project.repoName && candidate.repoName) repairs.repoName = candidate.repoName;
263
+ if (!project.repositoryId && candidate.repositoryId) repairs.repositoryId = candidate.repositoryId;
264
+ if (!project.prUrlBase && candidate.prUrlBase) repairs.prUrlBase = candidate.prUrlBase;
265
+ if (Object.keys(repairs).length > 0) {
266
+ persistAdoProjectRepair(project, repairs, candidate.source);
267
+ return true;
268
+ }
269
+ }
270
+
271
+ const missingAfter = getMissingAdoProjectConfigFields(project)
272
+ .filter(field => field !== 'repositoryId' || !project.repoName);
273
+ if (missingAfter.some(field => field === 'adoOrg' || field === 'adoProject' || field === 'repoName')) {
274
+ log('warn', `${purpose} cannot auto-repair ADO project config for ${getAdoProjectLabel(project)}: missing ${missingAfter.map(f => `project.${f}`).join(', ')} and no trusted origin remote, tracked PR URL, or canonical ADO PR ID supplied enough metadata`);
275
+ }
276
+ return false;
277
+ }
278
+
77
279
  function persistAdoRepositoryGuid(project, guid, repoName) {
78
280
  if (!isAdoGuid(guid)) return;
79
281
  const previous = String(project?.repositoryId || '').trim();
@@ -387,6 +589,7 @@ async function forEachActivePr(config, token, callback) {
387
589
 
388
590
  for (const project of projects) {
389
591
  if (isGitHubProject(project)) continue;
592
+ repairAdoProjectConfig(project, 'ADO PR polling');
390
593
  if (!project.adoOrg || !project.adoProject) continue;
391
594
 
392
595
  const prs = getPrs(project);
@@ -870,6 +1073,7 @@ async function reconcilePrs(config) {
870
1073
 
871
1074
  for (const project of projects) {
872
1075
  if (isGitHubProject(project)) continue;
1076
+ repairAdoProjectConfig(project, 'ADO PR reconciliation');
873
1077
  if (!project.adoOrg || !project.adoProject) continue;
874
1078
  const adoRepositoryId = getAdoRepositoryId(project);
875
1079
  if (!adoRepositoryId) {
@@ -1020,6 +1224,8 @@ async function reconcilePrs(config) {
1020
1224
  */
1021
1225
  async function checkLiveReviewStatus(pr, project) {
1022
1226
  try {
1227
+ repairAdoProjectConfig(project, 'ADO live review check', pr ? [pr] : null);
1228
+ if (!project.adoOrg || !project.adoProject) return null;
1023
1229
  const token = await getAdoToken();
1024
1230
  if (!token) return null;
1025
1231
  const orgBase = shared.getAdoOrgBase(project);
@@ -1066,6 +1272,8 @@ async function checkLiveReviewStatus(pr, project) {
1066
1272
  */
1067
1273
  async function checkLiveBuildAndConflict(pr, project) {
1068
1274
  try {
1275
+ repairAdoProjectConfig(project, 'ADO live build/conflict check', pr ? [pr] : null);
1276
+ if (!project.adoOrg || !project.adoProject) return null;
1069
1277
  const token = await getAdoToken();
1070
1278
  if (!token) return null;
1071
1279
  const orgBase = shared.getAdoOrgBase(project);
@@ -1163,6 +1371,9 @@ async function fetchAdoPrMetadata(prNum, adoOrg, adoProj, adoRepo) {
1163
1371
  * mergeConflict, url, project } or null on auth failure.
1164
1372
  */
1165
1373
  async function fetchSinglePrBuildStatus(project, prNumber) {
1374
+ repairAdoProjectConfig(project, 'ADO single PR status fetch');
1375
+ if (!project.adoOrg || !project.adoProject) return null;
1376
+
1166
1377
  const token = await getAdoToken();
1167
1378
  if (!token) return null;
1168
1379
 
@@ -1261,6 +1472,7 @@ const getAdoThrottleState = () => _adoThrottle.getState();
1261
1472
  * @returns {{ prNumber: number, url: string }|null}
1262
1473
  */
1263
1474
  async function findOpenPrOnBranch(project, branch) {
1475
+ repairAdoProjectConfig(project, 'ADO branch PR lookup');
1264
1476
  if (!project.adoOrg || !project.adoProject || !branch) return null;
1265
1477
  const adoRepositoryId = getAdoRepositoryId(project);
1266
1478
  if (!adoRepositoryId) {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-04T18:45:33.215Z"
4
+ "cachedAt": "2026-05-04T22:24:13.376Z"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1714",
3
+ "version": "0.1.1716",
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"
@@ -90,6 +90,7 @@ Core action types:
90
90
  workTypes: `explore` (research/report only, NO PR), `ask` (answer/report, NO PR), `implement` (new code, PR REQUIRED), `fix` (standalone bug fix creates a PR; include `pr` when fixing review comments/build failures on an existing PR), `review` (code review, NO PR), `test` (tests, PR if new), `verify` (merge/build/maintenance, NO PR)
91
91
  If the user wants a design/architecture artifact committed through a PR, dispatch `implement` or `docs` rather than `explore`.
92
92
  When the user names a specific agent ("assign this to lambert"), put exactly that one name in `agents` (e.g. `"agents": ["lambert"]`). A single-agent assignment is hard-pinned by the server — it will queue for that agent only and skip the routing table. If the user explicitly asks for fan-out/all agents, set `scope: "fan-out"`.
93
+ After emitting a dispatch, fix, or implement action, return immediately; do not poll, monitor, watch, wait, or check until completion, and do not add follow-up status actions. Only create a watch, monitor, poll, or periodically check when the human explicitly asks for monitoring, watching, periodic checks, or notification on completion.
93
94
  - **build-and-test**: pr, project (optional), agent (optional) — Run the build-and-test playbook against a PR. The agent will checkout the PR branch, run the project's build/test commands, and report results. Use when the user asks to "run tests on PR X" or "build PR X" or after a fix to verify nothing regressed.
94
95
  Example: user says "run build and test on PR 1834" → `{"type":"build-and-test","pr":"1834"}`
95
96
  - **note**: title, content — save to inbox