@yemi33/minions 0.1.1626 → 0.1.1628

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1628 (2026-04-29)
4
+
5
+ ### Features
6
+ - prevent stale PR redispatch
7
+
8
+ ### Fixes
9
+ - ADO PR helpers prefer az CLI with MCP fallback (#1833)
10
+
3
11
  ## 0.1.1626 (2026-04-29)
4
12
 
5
13
  ### Fixes
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-04-29T17:24:34.532Z"
4
+ "cachedAt": "2026-04-29T18:06:23.658Z"
5
5
  }
@@ -12,7 +12,7 @@ const { setCooldownFailure } = require('./cooldown');
12
12
  const { safeJson, safeWrite, safeReadDir, mutateJsonFileLocked, mutateWorkItems,
13
13
  mutatePullRequests, getProjects, projectWorkItemsPath, projectPrPath, log, ts, dateStamp,
14
14
  sidecarDispatchPrompt, deleteDispatchPromptSidecar,
15
- WI_STATUS, DISPATCH_RESULT, ENGINE_DEFAULTS, AGENT_STATUS, FAILURE_CLASS } = shared;
15
+ WI_STATUS, DISPATCH_RESULT, ENGINE_DEFAULTS, AGENT_STATUS, FAILURE_CLASS, PR_STATUS } = shared;
16
16
  const { getConfig, getDispatch, DISPATCH_PATH, INBOX_DIR } = queries;
17
17
 
18
18
  const MINIONS_DIR = shared.MINIONS_DIR;
@@ -91,6 +91,64 @@ function addToDispatch(item) {
91
91
  return item.id;
92
92
  }
93
93
 
94
+ function _resolveDispatchProject(projectRef, config) {
95
+ if (!projectRef) return null;
96
+ const projects = getProjects(config);
97
+ if (projectRef.name) {
98
+ const byName = projects.find(p => p.name === projectRef.name);
99
+ if (byName) return byName;
100
+ }
101
+ if (projectRef.localPath) {
102
+ const refPath = path.resolve(projectRef.localPath);
103
+ const byPath = projects.find(p => p.localPath && path.resolve(p.localPath) === refPath);
104
+ if (byPath) return byPath;
105
+ }
106
+ return projectRef;
107
+ }
108
+
109
+ function _isPrBackedDispatch(entry) {
110
+ return !!(entry?.meta?.pr && entry.meta?.project);
111
+ }
112
+
113
+ function getStalePrDispatchReason(entry, config) {
114
+ if (!_isPrBackedDispatch(entry)) return '';
115
+ const project = _resolveDispatchProject(entry.meta.project, config);
116
+ if (!project) return 'missing project metadata';
117
+
118
+ const tracked = shared.findPrRecord(queries.getPrs(project), entry.meta.pr, project);
119
+ const prLabel = entry.meta.pr?.id || entry.meta.pr?.url || entry.id;
120
+ if (!tracked) return `PR ${prLabel} is no longer tracked`;
121
+ if (tracked.status !== PR_STATUS.ACTIVE) return `PR ${tracked.id || prLabel} is ${tracked.status || 'missing status'}`;
122
+ if (tracked._contextOnly) return `PR ${tracked.id || prLabel} is context-only`;
123
+
124
+ const queuedBranch = entry.meta.branch || entry.meta.pr?.branch || '';
125
+ const trackedBranch = tracked.branch || '';
126
+ if (queuedBranch && trackedBranch && shared.sanitizeBranch(queuedBranch) !== shared.sanitizeBranch(trackedBranch)) {
127
+ return `PR ${tracked.id || prLabel} branch changed from ${queuedBranch} to ${trackedBranch}`;
128
+ }
129
+
130
+ return '';
131
+ }
132
+
133
+ function pruneStalePrDispatches(config = queries.getConfig()) {
134
+ const removed = [];
135
+ mutateDispatch((dispatch) => {
136
+ dispatch.pending = (dispatch.pending || []).filter(entry => {
137
+ const reason = getStalePrDispatchReason(entry, config);
138
+ if (!reason) return true;
139
+ removed.push({ entry, reason });
140
+ return false;
141
+ });
142
+ return dispatch;
143
+ });
144
+
145
+ for (const { entry, reason } of removed) {
146
+ try { deleteDispatchPromptSidecar(entry); } catch { /* cleanup best-effort */ }
147
+ log('info', `Dropped stale PR dispatch ${entry.id}: ${reason}`);
148
+ }
149
+ return removed.length;
150
+ }
151
+
94
152
  // ─── Retryable Failure Classification ────────────────────────────────────────
95
153
 
96
154
  function isRetryableFailureReason(reason = '', failureClass = '') {
@@ -253,11 +311,16 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
253
311
  if (prId && project) {
254
312
  try {
255
313
  const prsPath = projectPrPath(project);
314
+ let restored = false;
256
315
  mutatePullRequests(prsPath, prs => {
257
316
  const target = shared.findPrRecord(prs, { id: prId }, project);
258
- if (target?.humanFeedback) target.humanFeedback.pendingFix = true;
317
+ if (target?.humanFeedback) {
318
+ target.humanFeedback.pendingFix = true;
319
+ restored = true;
320
+ }
259
321
  });
260
- log('info', `Restored pendingFix=true on ${prId} after failed human-feedback fix`);
322
+ if (restored) log('info', `Restored pendingFix=true on ${prId} after failed human-feedback fix`);
323
+ else log('info', `Skipped pendingFix restore for ${prId} — PR is no longer tracked`);
261
324
  } catch (e) { log('warn', `restore pendingFix: ${e.message}`); }
262
325
  }
263
326
  // Clear completed dispatch entry so dedup doesn't block re-dispatch
@@ -424,6 +487,8 @@ module.exports = {
424
487
  completeDispatch,
425
488
  writeInboxAlert,
426
489
  updateAgentStatus,
490
+ getStalePrDispatchReason,
491
+ pruneStalePrDispatches,
427
492
  cancelPendingDispatchesForPr,
428
493
  cleanDispatchEntries,
429
494
  cancelPendingWorkItems,
@@ -38,8 +38,17 @@ function getPrCreateInstructions(project) {
38
38
  `- Use --head to specify your feature branch name\n` +
39
39
  `- Include a meaningful --title and body file describing the changes`;
40
40
  }
41
- // Default: Azure DevOps
42
- return `Use \`mcp__azure-ado__repo_create_pull_request\`:\n- repositoryId: \`${repoId}\``;
41
+ // Default: Azure DevOps — prefer `az` CLI first, ADO MCP only as fallback
42
+ const adoOrg = project?.adoOrg || '';
43
+ const adoProject = project?.adoProject || '';
44
+ const repoName = project?.repoName || '';
45
+ const mainBranch = project?.localPath ? shared.resolveMainBranch(project.localPath, project.mainBranch) : (project?.mainBranch || 'main');
46
+ return `For Azure DevOps, use the \`az\` CLI first to create a pull request:\n` +
47
+ `- Run \`az devops configure --defaults organization=https://dev.azure.com/${adoOrg} project="${adoProject}"\` once per session if defaults are not yet set\n` +
48
+ `- Then: \`az repos pr create --repository "${repoName}" --source-branch <your-branch> --target-branch ${mainBranch} --title "PR title" --description @<body-file.md>\`\n` +
49
+ `- Use \`@<file>\` syntax for \`--description\` so Markdown, quotes, and newlines pass safely\n` +
50
+ `- Always set --target-branch to \`${mainBranch}\` (the main branch)\n\n` +
51
+ `If \`az\` is unavailable or insufficient for this operation, fall back to \`mcp__azure-ado__repo_create_pull_request\` with repositoryId \`${repoId}\`. Do not use \`gh\` for Azure DevOps repositories.`;
43
52
  }
44
53
 
45
54
  function getPrCommentInstructions(project) {
@@ -54,7 +63,12 @@ function getPrCommentInstructions(project) {
54
63
  `- Always set --repo to \`${org}/${repo}\` to target the correct repository\n` +
55
64
  `- Use --body-file so Markdown, quotes, and newlines are passed safely`;
56
65
  }
57
- return `Use \`mcp__azure-ado__repo_create_pull_request_thread\`:\n- repositoryId: \`${repoId}\``;
66
+ // Azure DevOps — prefer `az` CLI first, ADO MCP only as fallback
67
+ const repoName = project?.repoName || '';
68
+ return `For Azure DevOps, use the \`az\` CLI first to post a comment on the PR:\n` +
69
+ `- Write the Markdown comment to a temporary file, then run: \`az repos pr comment create --pull-request-id <number> --content @<body-file.md>\` (substitute your project's repo \`${repoName}\` if not using \`az devops configure\` defaults)\n` +
70
+ `- Use \`@<file>\` syntax for \`--content\` so Markdown, quotes, and newlines pass safely\n\n` +
71
+ `If \`az repos pr comment\` is unavailable or insufficient (e.g. older az-devops extension, thread/status semantics needed), fall back to \`mcp__azure-ado__repo_create_pull_request_thread\` with repositoryId \`${repoId}\`. Do not use \`gh\` for Azure DevOps repositories.`;
58
72
  }
59
73
 
60
74
  function getPrFetchInstructions(project) {
@@ -72,7 +86,13 @@ function getPrFetchInstructions(project) {
72
86
  `- Or use \`gh pr checkout <number> --repo ${org}/${repo}\` to fetch and checkout in one step\n` +
73
87
  `- The base branch is \`${mainBranch}\``;
74
88
  }
75
- return `Use \`mcp__azure-ado__repo_get_pull_request_by_id\` to fetch PR status.`;
89
+ // Azure DevOps — prefer `az` CLI first, ADO MCP only as fallback
90
+ const mainBranch = project?.localPath ? shared.resolveMainBranch(project.localPath, project.mainBranch) : (project?.mainBranch || 'main');
91
+ return `For Azure DevOps, use the \`az\` CLI first to fetch PR status:\n` +
92
+ `- \`az repos pr show --id <number>\` returns PR state, mergeStatus, source/target branches, vote summary, and policy/build evaluations\n` +
93
+ `- For the local branch: \`git fetch origin <branch-name>\` then inspect via \`git show\`/\`git diff\` (do NOT checkout in your main working tree)\n` +
94
+ `- The base branch is \`${mainBranch}\`\n\n` +
95
+ `If \`az\` is unavailable or insufficient, fall back to \`mcp__azure-ado__repo_get_pull_request_by_id\`. Do not use \`gh\` for Azure DevOps repositories.`;
76
96
  }
77
97
 
78
98
  function getPrVoteInstructions(project) {
@@ -90,7 +110,11 @@ function getPrVoteInstructions(project) {
90
110
  `- **Your comment body MUST start with \`VERDICT: APPROVE\` or \`VERDICT: REQUEST_CHANGES\`** on its own line — the engine parses this to record your vote\n` +
91
111
  `- Do NOT use \`--approve\` or \`--request-changes\` flags — they will fail`;
92
112
  }
93
- return `Use \`mcp__azure-ado__repo_update_pull_request_reviewers\`:\n- repositoryId: \`${repoId}\`\n- Set your reviewer vote on the PR (10=approve, 5=approve-with-suggestions, -10=reject)`;
113
+ // Azure DevOps prefer `az` CLI first, ADO MCP only as fallback
114
+ return `For Azure DevOps, use the \`az\` CLI first to set your reviewer vote:\n` +
115
+ `- \`az repos pr set-vote --id <number> --vote {approve | approve-with-suggestions | reject | reset | wait-for-author}\`\n` +
116
+ `- Pair the vote with \`az repos pr comment create --pull-request-id <number> --content @<verdict.md>\` so the verdict body is recorded as a thread comment\n\n` +
117
+ `If \`az\` is unavailable or insufficient, fall back to \`mcp__azure-ado__repo_update_pull_request_reviewers\` with repositoryId \`${repoId}\` (vote integers: 10=approve, 5=approve-with-suggestions, 0=no-vote, -5=wait-for-author, -10=reject). Do not use \`gh\` for Azure DevOps repositories.`;
94
118
  }
95
119
 
96
120
  function getRepoHostLabel(project) {
@@ -102,7 +126,7 @@ function getRepoHostLabel(project) {
102
126
  function getRepoHostToolRule(project) {
103
127
  const host = getRepoHost(project);
104
128
  if (host === 'github') return 'Use GitHub MCP tools or `gh` CLI for PR operations';
105
- return 'Use Azure DevOps MCP tools (mcp__azure-ado__*) for PR operations NEVER use gh CLI';
129
+ return 'For Azure DevOps, use the `az` CLI first for PR operations (e.g. `az repos pr create`, `az repos pr show`, `az repos pr comment`, `az repos pr set-vote`); use ADO MCP tools (`mcp__azure-ado__*`) only as a fallback when `az` is unavailable or insufficient. Do not use `gh` for Azure DevOps repositories.';
106
130
  }
107
131
 
108
132
  // ─── Task Context Resolution ────────────────────────────────────────────────
package/engine.js CHANGED
@@ -102,7 +102,7 @@ const withFileLock = shared.withFileLock;
102
102
  // ─── Dispatch Management (extracted to engine/dispatch.js) ───────────────────
103
103
 
104
104
  const { mutateDispatch, addToDispatch, isRetryableFailureReason, completeDispatch,
105
- writeInboxAlert, updateAgentStatus } = require('./engine/dispatch');
105
+ writeInboxAlert, updateAgentStatus, pruneStalePrDispatches } = require('./engine/dispatch');
106
106
 
107
107
  // ─── Timeout / Steering / Idle (extracted to engine/timeout.js) ──────────────
108
108
 
@@ -3639,6 +3639,7 @@ async function tickInner() {
3639
3639
  const maxC = config.engine?.maxConcurrent ?? ENGINE_DEFAULTS.maxConcurrent;
3640
3640
  setTempBudget(Math.max(0, maxC - activeCountPre));
3641
3641
  }
3642
+ try { pruneStalePrDispatches(config); } catch (e) { log('warn', 'prune stale PR dispatches: ' + e.message); }
3642
3643
  let discoveryOk = true;
3643
3644
  try { await discoverWork(config); } catch (e) { log('warn', 'discoverWork: ' + e.message); discoveryOk = false; }
3644
3645
 
@@ -3915,7 +3916,7 @@ module.exports = {
3915
3916
  validateConfig,
3916
3917
 
3917
3918
  // Dispatch management (re-exported from engine/dispatch.js)
3918
- mutateDispatch, addToDispatch, isRetryableFailureReason, completeDispatch, writeInboxAlert, updateAgentStatus,
3919
+ mutateDispatch, addToDispatch, isRetryableFailureReason, completeDispatch, writeInboxAlert, updateAgentStatus, pruneStalePrDispatches,
3919
3920
  activeProcesses, realActivityMap, engineRestartGraceExempt,
3920
3921
  get engineRestartGraceUntil() { return engineRestartGraceUntil; },
3921
3922
  set engineRestartGraceUntil(v) { engineRestartGraceUntil = v; },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1626",
3
+ "version": "0.1.1628",
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"