@yemi33/minions 0.1.1691 → 0.1.1693

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.1693 (2026-05-04)
4
+
5
+ ### Features
6
+ - sync review verdict PR status (#2008)
7
+
3
8
  ## 0.1.1691 (2026-05-04)
4
9
 
5
10
  ### Features
package/README.md CHANGED
@@ -310,6 +310,10 @@ Agents inherit MCP servers directly from `~/.claude.json` as Claude Code process
310
310
 
311
311
  Manually refresh with `minions mcp-sync`.
312
312
 
313
+ ### GitHub Users
314
+
315
+ For GitHub repos, install and authenticate the [GitHub CLI](https://cli.github.com/). Agents should use `gh` for GitHub PR creation, PR lookup, comments, reviews, issues, and workflow checks. If GitHub or Copilot auth fails, refresh GitHub credentials with `gh auth status` and `gh auth login`, or provide `GH_TOKEN`/`COPILOT_GITHUB_TOKEN` from the environment. Azure DevOps authentication and tooling paths do not apply to GitHub repo work.
316
+
313
317
  ### Azure DevOps Users
314
318
 
315
319
  For the best experience with ADO repos, install the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) with the Azure DevOps extension. Agents should use the `az` CLI first for Azure DevOps operations such as PR creation, PR lookup, comments, reviewers, work items, and pipelines. Use the Azure DevOps MCP fallback only when `az` is unavailable in the environment or insufficient for a specific action.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-04T01:58:05.343Z"
4
+ "cachedAt": "2026-05-04T03:30:25.499Z"
5
5
  }
package/engine/github.js CHANGED
@@ -39,6 +39,19 @@ function getRepoSlug(project) {
39
39
  return `${org}/${repo}`;
40
40
  }
41
41
 
42
+ function _hasMinionsReviewVerdict(body) {
43
+ return /(?:^|\n)\s*\*{0,2}VERDICT[:\s]+\*{0,2}(?:APPROVE|REQUEST[_\s-]?CHANGES)\*{0,2}\b/i.test(String(body || ''));
44
+ }
45
+
46
+ function _isAgentComment(c) {
47
+ const body = c.body || '';
48
+ if (_hasMinionsReviewVerdict(body)) return true;
49
+ if (/\bMinions\s*\(/i.test(body)) return true;
50
+ if (/\bby\s+Minions\b/i.test(body)) return true;
51
+ if (/\[minions\]/i.test(body)) return true;
52
+ return false;
53
+ }
54
+
42
55
  // ─── Per-Repo Poll Backoff ──────────────────────────────────────────────────
43
56
  // Tracks consecutive poll failures per repo slug to avoid spamming logs when
44
57
  // a repo is inaccessible. Backoff doubles each failure: 2min, 4min, 8min, 16min, max 30min.
@@ -572,13 +585,6 @@ async function pollPrHumanComments(config) {
572
585
  if (/!\[.*\]\(https?:\/\/.*badge/i.test(body)) return true;
573
586
  return false;
574
587
  }
575
- function _isAgentComment(c) {
576
- const body = c.body || '';
577
- if (/\bMinions\s*\(/i.test(body)) return true;
578
- if (/\bby\s+Minions\b/i.test(body)) return true;
579
- if (/\[minions\]/i.test(body)) return true;
580
- return false;
581
- }
582
588
  const actionableComments = allComments.filter(c => !_isIgnoredComment(c));
583
589
 
584
590
  const cutoffStr = pr.humanFeedback?.lastProcessedCommentDate || pr.created || '1970-01-01';
@@ -907,4 +913,6 @@ module.exports = {
907
913
  GH_MAX_BUFFER, // exported for testing
908
914
  GH_POLL_BACKOFF_BASE_MS, // exported for testing
909
915
  GH_POLL_BACKOFF_MAX_MS, // exported for testing
916
+ _hasMinionsReviewVerdict, // exported for testing
917
+ _isAgentComment, // exported for testing
910
918
  };
@@ -1266,11 +1266,61 @@ function isReviewBailout(text) {
1266
1266
  return /bail(ing)?\s+out/i.test(text) || /already\s+posted/i.test(text);
1267
1267
  }
1268
1268
 
1269
- async function updatePrAfterReview(agentId, pr, project, config, resultSummary, structuredCompletion = null) {
1269
+ function reviewPrRefFromCompletion(completion) {
1270
+ if (!completion || typeof completion !== 'object') return null;
1271
+ const value = String(completion.pr || completion.pull_request || completion.pullRequest || '').trim();
1272
+ if (!value || /^N\/?A$/i.test(value)) return null;
1273
+ return value;
1274
+ }
1270
1275
 
1271
- if (!pr?.id) return;
1276
+ function centralPrPath() {
1277
+ return path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json');
1278
+ }
1279
+
1280
+ function resolveReviewPrContext(pr, project, config, structuredCompletion = null) {
1281
+ const refs = [pr, reviewPrRefFromCompletion(structuredCompletion)].filter(Boolean);
1282
+ if (refs.length === 0) return null;
1283
+
1284
+ const projects = shared.getProjects(config);
1285
+ const projectCandidates = [];
1286
+ if (project) projectCandidates.push(project);
1287
+ for (const p of projects) {
1288
+ if (!projectCandidates.some(existing => existing?.name === p.name)) projectCandidates.push(p);
1289
+ }
1290
+
1291
+ for (const candidateProject of projectCandidates) {
1292
+ const prPath = shared.projectPrPath(candidateProject);
1293
+ const prs = safeJson(prPath) || [];
1294
+ for (const ref of refs) {
1295
+ const target = shared.findPrRecord(prs, ref, candidateProject);
1296
+ if (target) return { pr: { ...target }, project: candidateProject, prPath };
1297
+ }
1298
+ }
1299
+
1300
+ const centralPath = centralPrPath();
1301
+ const centralPrs = safeJson(centralPath) || [];
1302
+ for (const ref of refs) {
1303
+ const target = shared.findPrRecord(centralPrs, ref, null);
1304
+ if (target) return { pr: { ...target }, project: null, prPath: centralPath };
1305
+ }
1306
+
1307
+ return pr?.id
1308
+ ? { pr, project: project || null, prPath: project ? shared.projectPrPath(project) : centralPath }
1309
+ : null;
1310
+ }
1311
+
1312
+ async function updatePrAfterReview(agentId, pr, project, config, resultSummary, structuredCompletion = null) {
1272
1313
 
1273
1314
  if (!config) config = getConfig();
1315
+ const reviewContext = resolveReviewPrContext(pr, project, config, structuredCompletion);
1316
+ if (!reviewContext?.pr?.id) {
1317
+ const reportedPr = reviewPrRefFromCompletion(structuredCompletion);
1318
+ if (reportedPr) log('warn', `Review completion reported PR ${reportedPr}, but no tracked PR record was found`);
1319
+ return;
1320
+ }
1321
+ const reviewPr = reviewContext.pr;
1322
+ const reviewProject = reviewContext.project;
1323
+ const prPath = reviewContext.prPath;
1274
1324
  const reviewerName = config.agents?.[agentId]?.name || agentId;
1275
1325
  const dispatch = getDispatch();
1276
1326
  const completedEntry = (dispatch.completed || []).find(d => d.agent === agentId && d.type === 'review');
@@ -1280,31 +1330,30 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
1280
1330
  // The poller will pick up the real status on the next cycle (~3 min).
1281
1331
  let postReviewStatus = null; // null = don't change
1282
1332
  try {
1283
- const projectObj = project || shared.getProjects(config)[0];
1333
+ const projectObj = reviewProject || shared.getProjects(config)[0];
1284
1334
  if (projectObj) {
1285
1335
  const host = projectObj.repoHost || 'ado';
1286
1336
  const checkFn = host === 'github'
1287
1337
  ? require('./github').checkLiveReviewStatus
1288
1338
  : require('./ado').checkLiveReviewStatus;
1289
- const liveStatus = await checkFn(pr, projectObj);
1339
+ const liveStatus = await checkFn(reviewPr, projectObj);
1290
1340
  if (liveStatus && liveStatus !== 'pending') postReviewStatus = liveStatus;
1291
1341
  }
1292
- } catch (e) { log('warn', `Post-review status check for ${pr.id}: ${e.message}`); }
1342
+ } catch (e) { log('warn', `Post-review status check for ${reviewPr.id}: ${e.message}`); }
1293
1343
 
1294
1344
  // Fallback: if live check returned pending (e.g., GitHub self-approval blocked), use the agent's completion report.
1295
1345
  if (!postReviewStatus) {
1296
1346
  const verdict = reviewVerdictFromCompletion(structuredCompletion) || parseReviewVerdict(resultSummary);
1297
1347
  if (verdict) {
1298
1348
  postReviewStatus = verdict;
1299
- log('info', `Read review verdict from agent completion for ${pr.id}: ${verdict}`);
1349
+ log('info', `Read review verdict from agent completion for ${reviewPr.id}: ${verdict}`);
1300
1350
  }
1301
1351
  }
1302
1352
 
1303
- const prPath = project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json');
1304
1353
  let updatedTarget = null;
1305
1354
  shared.mutateJsonFileLocked(prPath, (prs) => {
1306
1355
  if (!Array.isArray(prs)) return prs;
1307
- const target = shared.findPrRecord(prs, pr, project);
1356
+ const target = shared.findPrRecord(prs, reviewPr, reviewProject);
1308
1357
  if (!target) return prs;
1309
1358
  // Once approved, stays approved — only changes-requested can override
1310
1359
  if (postReviewStatus) {
@@ -1323,12 +1372,12 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
1323
1372
  // Drop it when reviewer requests changes again — that starts a new fix cycle.
1324
1373
  ...(target.minionsReview?.fixedAt && postReviewStatus !== 'changes-requested' ? { fixedAt: target.minionsReview.fixedAt } : {}),
1325
1374
  };
1326
- updatedTarget = { ...pr, ...target };
1375
+ updatedTarget = { ...reviewPr, ...target };
1327
1376
  return prs;
1328
1377
  }, { defaultValue: [] });
1329
1378
 
1330
1379
  // Track reviewer for metrics purposes (separate file, separate lock)
1331
- const authorAgentId = (pr.agent || '').toLowerCase();
1380
+ const authorAgentId = (reviewPr.agent || '').toLowerCase();
1332
1381
  if (authorAgentId && config.agents?.[authorAgentId]) {
1333
1382
  shared.mutateJsonFileLocked(path.join(ENGINE_DIR, 'metrics.json'), (metrics) => {
1334
1383
  if (!metrics[authorAgentId]) metrics[authorAgentId] = { ...DEFAULT_AGENT_METRICS };
@@ -1338,7 +1387,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
1338
1387
  }, { defaultValue: {} });
1339
1388
  }
1340
1389
 
1341
- log('info', `Updated ${pr.id} → minions review: ${postReviewStatus || 'waiting'} by ${reviewerName}`);
1390
+ log('info', `Updated ${reviewPr.id} → minions review: ${postReviewStatus || 'waiting'} by ${reviewerName}`);
1342
1391
  if (updatedTarget) createReviewFeedbackForAuthor(agentId, updatedTarget, config);
1343
1392
  }
1344
1393
 
@@ -407,16 +407,27 @@ function renderPlaybook(type, vars) {
407
407
  const dispatchProject = (vars.repo_id && projects.find(p => p.repositoryId === vars.repo_id))
408
408
  || (vars.repo_name && projects.find(p => p.repoName === vars.repo_name))
409
409
  || projects[0] || {};
410
+ const renderProject = {
411
+ ...dispatchProject,
412
+ adoOrg: vars.ado_org || vars.adoOrg || dispatchProject.adoOrg,
413
+ adoProject: vars.ado_project || vars.adoProject || dispatchProject.adoProject,
414
+ repoName: vars.repo_name || vars.repoName || dispatchProject.repoName,
415
+ repoHost: vars.repo_host || vars.repoHost || dispatchProject.repoHost,
416
+ };
417
+ const repoHost = getRepoHost(renderProject);
410
418
  const projectVars = {
411
- project_name: dispatchProject.name || 'Unknown Project',
412
- ado_org: dispatchProject.adoOrg || 'Unknown',
413
- ado_project: dispatchProject.adoProject || 'Unknown',
414
- repo_name: dispatchProject.repoName || 'Unknown',
415
- pr_create_instructions: getPrCreateInstructions(dispatchProject),
416
- pr_comment_instructions: getPrCommentInstructions(dispatchProject),
417
- pr_fetch_instructions: getPrFetchInstructions(dispatchProject),
418
- pr_vote_instructions: getPrVoteInstructions(dispatchProject),
419
- repo_host_label: getRepoHostLabel(dispatchProject),
419
+ project_name: renderProject.name || 'Unknown Project',
420
+ ado_org: renderProject.adoOrg || 'Unknown',
421
+ ado_project: renderProject.adoProject || 'Unknown',
422
+ repo_name: renderProject.repoName || 'Unknown',
423
+ repo_host: repoHost,
424
+ ado_shared_rules: repoHost === 'ado' ? '1' : '',
425
+ github_shared_rules: repoHost === 'github' ? '1' : '',
426
+ pr_create_instructions: getPrCreateInstructions(renderProject),
427
+ pr_comment_instructions: getPrCommentInstructions(renderProject),
428
+ pr_fetch_instructions: getPrFetchInstructions(renderProject),
429
+ pr_vote_instructions: getPrVoteInstructions(renderProject),
430
+ repo_host_label: getRepoHostLabel(renderProject),
420
431
  };
421
432
  const allVars = { ...projectVars, ...vars };
422
433
 
@@ -640,6 +651,7 @@ function buildBaseVars(agentId, config, project) {
640
651
  ado_org: project?.adoOrg || 'Unknown',
641
652
  ado_project: project?.adoProject || 'Unknown',
642
653
  repo_name: project?.repoName || 'Unknown',
654
+ repo_host: getRepoHost(project),
643
655
  main_branch: project?.mainBranch || 'main',
644
656
  date: dateStamp(),
645
657
  };
@@ -545,7 +545,7 @@ function parseError(rawOutput) {
545
545
  const lower = text.toLowerCase();
546
546
 
547
547
  if (/not authenticated|copilot login|please.*log.*in|401|403 forbidden|unauthorized/i.test(text)) {
548
- return { message: 'Copilot authentication failed', code: 'auth-failure', retriable: false };
548
+ return { message: 'Copilot/GitHub authentication failed. Run `gh auth login` or provide GH_TOKEN/COPILOT_GITHUB_TOKEN with Copilot access.', code: 'auth-failure', retriable: false };
549
549
  }
550
550
  if (/rate limit|too many requests|\b429\b/i.test(text)) {
551
551
  return { message: 'Copilot rate limit hit', code: 'rate-limit', retriable: true };
package/engine/shared.js CHANGED
@@ -751,6 +751,7 @@ const ENGINE_DEFAULTS = {
751
751
  inboxConsolidateThreshold: 5,
752
752
  agentTimeout: 18000000, // 5h
753
753
  heartbeatTimeout: 300000, // 5min — stale-orphan grace after process tracking is lost
754
+ resumeHeartbeatTimeout: 300000, // 5min — max wait for a resumed runtime to emit its first output
754
755
  // Per-type stale-orphan overrides (merged with config.engine.heartbeatTimeouts at runtime — see timeout.js).
755
756
  // Heavy work types (multi-file edits, builds, test suites, full verify cycles) routinely go quiet for
756
757
  // longer than the 5-min default when the engine has lost their tracked handle (e.g. across an engine
@@ -1309,11 +1310,27 @@ function projectPrPath(project) {
1309
1310
  return path.join(projectStateDir(project), 'pull-requests.json');
1310
1311
  }
1311
1312
 
1313
+ function comparablePath(filePath) {
1314
+ const resolved = path.resolve(filePath);
1315
+ try {
1316
+ return fs.realpathSync.native(resolved);
1317
+ } catch {
1318
+ try {
1319
+ return path.join(fs.realpathSync.native(path.dirname(resolved)), path.basename(resolved));
1320
+ } catch {
1321
+ return resolved;
1322
+ }
1323
+ }
1324
+ }
1325
+
1312
1326
  function resolveProjectForPrPath(filePath, config = null) {
1313
- const resolvedPath = path.resolve(filePath);
1327
+ const resolvedPaths = new Set([comparablePath(filePath)]);
1328
+ if (filePath && !path.isAbsolute(filePath)) {
1329
+ resolvedPaths.add(comparablePath(path.resolve(MINIONS_DIR, filePath)));
1330
+ }
1314
1331
  const projects = getProjects(config);
1315
1332
  for (const project of projects) {
1316
- if (path.resolve(projectPrPath(project)) === resolvedPath) return project;
1333
+ if (resolvedPaths.has(comparablePath(projectPrPath(project)))) return project;
1317
1334
  }
1318
1335
  if (projects.length === 1) return projects[0];
1319
1336
  return null;
@@ -129,6 +129,10 @@ function normalizeRuntimeExit(code, signal) {
129
129
  return 1;
130
130
  }
131
131
 
132
+ function shouldInjectAdoTokenEnv(env = process.env) {
133
+ return String(env.MINIONS_REPO_HOST || '').trim().toLowerCase() === 'ado';
134
+ }
135
+
132
136
  function injectAdoTokenEnv(env, { execSync: _execSync, acquireToken, warn = (msg) => process.stderr.write(msg + '\n') } = {}) {
133
137
  let result;
134
138
  try {
@@ -149,6 +153,11 @@ function injectAdoTokenEnv(env, { execSync: _execSync, acquireToken, warn = (msg
149
153
  return true;
150
154
  }
151
155
 
156
+ function injectAdoTokenEnvForRepoHost(env, opts) {
157
+ if (!shouldInjectAdoTokenEnv(env)) return false;
158
+ return injectAdoTokenEnv(env, opts);
159
+ }
160
+
152
161
  const PROCESS_EXIT_SENTINEL_FLUSH_TIMEOUT_MS = 2000;
153
162
 
154
163
  function formatProcessExitSentinel(exitCode, signal) {
@@ -251,7 +260,7 @@ function main() {
251
260
  const { promptFile, sysPromptFile, runtimeName, opts, passthrough } = parsed;
252
261
 
253
262
  const env = cleanChildEnv();
254
- injectAdoTokenEnv(env);
263
+ injectAdoTokenEnvForRepoHost(env);
255
264
 
256
265
  let runtime;
257
266
  try { runtime = resolveRuntime(runtimeName); }
@@ -399,6 +408,6 @@ function main() {
399
408
  });
400
409
  }
401
410
 
402
- module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit, injectAdoTokenEnv, writeProcessExitSentinel, computeAddDirs };
411
+ module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit, shouldInjectAdoTokenEnv, injectAdoTokenEnv, injectAdoTokenEnvForRepoHost, writeProcessExitSentinel, computeAddDirs };
403
412
 
404
413
  if (require.main === module) main();
package/engine/timeout.js CHANGED
@@ -9,7 +9,7 @@ const queries = require('./queries');
9
9
  const steering = require('./steering');
10
10
 
11
11
  const { safeRead, safeWrite, safeJson, mutateJsonFileLocked, getProjects, projectWorkItemsPath, log, ts,
12
- ENGINE_DEFAULTS, ENGINE_DIR, WI_STATUS, WORK_TYPE, DISPATCH_RESULT, AGENT_STATUS } = shared;
12
+ ENGINE_DEFAULTS, ENGINE_DIR, WI_STATUS, WORK_TYPE, DISPATCH_RESULT, AGENT_STATUS, FAILURE_CLASS } = shared;
13
13
  const { getDispatch, getAgentStatus } = queries;
14
14
  const AGENTS_DIR = queries.AGENTS_DIR;
15
15
  const MINIONS_DIR = shared.MINIONS_DIR;
@@ -258,6 +258,7 @@ function checkTimeouts(config) {
258
258
 
259
259
  const timeout = config.engine?.agentTimeout || ENGINE_DEFAULTS.agentTimeout;
260
260
  const defaultStaleOrphanTimeout = config.engine?.heartbeatTimeout || ENGINE_DEFAULTS.heartbeatTimeout;
261
+ const runtimeResumeHeartbeatTimeout = config.engine?.resumeHeartbeatTimeout || ENGINE_DEFAULTS.resumeHeartbeatTimeout || defaultStaleOrphanTimeout;
261
262
 
262
263
  // Optional per-type stale-orphan timeouts: merge ENGINE_DEFAULTS ← config overrides.
263
264
  const perTypeStaleOrphanTimeouts = { ...ENGINE_DEFAULTS.heartbeatTimeouts, ...(config.engine?.heartbeatTimeouts || {}) };
@@ -274,8 +275,10 @@ function checkTimeouts(config) {
274
275
  }
275
276
 
276
277
  // 2. Stale-orphan check — for ALL active dispatch items (catches lost process handles after restart).
277
- // Silence is not a failure for tracked live processes: long CLI commands can legitimately
278
- // produce no stdout/stderr for extended periods.
278
+ // Silence is not a failure for tracked live processes once a runtime has emitted output:
279
+ // long CLI commands can legitimately produce no stdout/stderr for extended periods.
280
+ // The exception is a resumed runtime that has not produced its first stdout/stderr
281
+ // heartbeat after spawn; that is the "alive but stuck in --resume" failure mode.
279
282
  const dispatchData = getDispatch();
280
283
  const deadItems = [];
281
284
  const legacyAnnotationClears = new Set();
@@ -332,8 +335,9 @@ function checkTimeouts(config) {
332
335
  const liveLogPath = path.join(AGENTS_DIR, item.agent, 'live-output.log');
333
336
  let lastActivity = item.started_at ? new Date(item.started_at).getTime() : 0;
334
337
 
335
- // live-output.log mtime is only used for stale-orphan cleanup and completion recovery.
336
- // It is not used as an output-silence timeout for live tracked processes.
338
+ // live-output.log mtime is used for stale-orphan cleanup, completion recovery,
339
+ // and the resume first-output watchdog. It is not a general output-silence
340
+ // timeout for live tracked processes.
337
341
  try {
338
342
  const stat = fs.statSync(liveLogPath);
339
343
  lastActivity = Math.max(lastActivity, stat.mtimeMs);
@@ -394,6 +398,23 @@ function checkTimeouts(config) {
394
398
  if (procInfo?._steeringAt && Date.now() - procInfo._steeringAt < 60000) continue;
395
399
 
396
400
  if (processAlive) {
401
+ if (procInfo?._runtimeResumeAwaitingFirstOutput) {
402
+ const resumeStartedAt = Number(procInfo._runtimeResumeAt || 0);
403
+ const resumeHeartbeatAt = Math.max(lastActivity, resumeStartedAt);
404
+ const resumeSilentMs = Date.now() - resumeHeartbeatAt;
405
+ if (resumeSilentMs > runtimeResumeHeartbeatTimeout) {
406
+ const resumeSilentSec = Math.round(resumeSilentMs / 1000);
407
+ const reason = `Runtime resume stalled — no output heartbeat for ${resumeSilentSec}s`;
408
+ log('warn', `Runtime resume stalled: ${item.agent} (${item.id}) — no output heartbeat for ${resumeSilentSec}s; killing and retrying fresh`);
409
+ dispatch().updateAgentStatus(item.id, AGENT_STATUS.TIMED_OUT, reason);
410
+ try { fs.appendFileSync(liveLogPath, `\n[runtime-resume-timeout] ${reason}. Killing this resume attempt and retrying with a fresh session.\n`); } catch { /* optional */ }
411
+ // Clear the cached session so retry does not re-enter the same stuck --resume path.
412
+ try { shared.safeUnlink(path.join(AGENTS_DIR, item.agent, 'session.json')); } catch {}
413
+ activeProcesses.delete(item.id);
414
+ shared.killGracefully(procInfo.proc, 5000);
415
+ deadItems.push({ item, reason, failureClass: FAILURE_CLASS.TIMEOUT });
416
+ }
417
+ }
397
418
  continue;
398
419
  }
399
420
 
@@ -462,8 +483,8 @@ function checkTimeouts(config) {
462
483
  }
463
484
 
464
485
  // Clean up dead items
465
- for (const { item, reason } of deadItems) {
466
- completeDispatch(item.id, DISPATCH_RESULT.ERROR, reason);
486
+ for (const { item, reason, failureClass } of deadItems) {
487
+ completeDispatch(item.id, DISPATCH_RESULT.ERROR, reason, '', failureClass ? { failureClass } : {});
467
488
  }
468
489
 
469
490
  // Clear legacy blocking-tool annotations; process liveness no longer depends on tool parsing.
package/engine.js CHANGED
@@ -135,7 +135,7 @@ const { getRouting, parseRoutingTable, getRoutingTableCached, getMonthlySpend,
135
135
  const { renderPlaybook, validatePlaybookVars, PLAYBOOK_REQUIRED_VARS,
136
136
  buildSystemPrompt, buildAgentContext, selectPlaybook,
137
137
  buildBaseVars, buildPrDispatch, resolveTaskContext,
138
- getRepoHostLabel, getRepoHostToolRule } = require('./engine/playbook');
138
+ getRepoHost, getRepoHostLabel, getRepoHostToolRule } = require('./engine/playbook');
139
139
 
140
140
  // sanitizeBranch imported from shared.js
141
141
 
@@ -975,13 +975,16 @@ async function spawnAgent(dispatchItem, config) {
975
975
  // Spawn the claude process
976
976
  const childEnv = shared.cleanChildEnv();
977
977
  if (completionReportPath) childEnv.MINIONS_COMPLETION_REPORT = completionReportPath;
978
+ childEnv.MINIONS_REPO_HOST = getRepoHost(project);
978
979
 
979
- // Inject cached ADO token so agents skip re-authentication (#998)
980
- // getAdoToken() returns cached token (30-min TTL) or null never blocks on browser auth
981
- try {
982
- const adoToken = await getAdoToken();
983
- if (adoToken) childEnv.MINIONS_ADO_TOKEN = adoToken;
984
- } catch { /* non-fatal — agent can still authenticate on its own */ }
980
+ if (getRepoHost(project) === 'ado') {
981
+ // Inject cached ADO token so ADO agents skip re-authentication (#998).
982
+ // getAdoToken() returns cached token (30-min TTL) or null — never blocks on browser auth.
983
+ try {
984
+ const adoToken = await getAdoToken();
985
+ if (adoToken) childEnv.MINIONS_ADO_TOKEN = adoToken;
986
+ } catch { /* non-fatal — agent can still authenticate on its own */ }
987
+ }
985
988
 
986
989
  // Spawn via wrapper script — node directly (no bash intermediary)
987
990
  // spawn-agent.js handles CLAUDECODE env cleanup and claude binary resolution
@@ -1052,6 +1055,10 @@ async function spawnAgent(dispatchItem, config) {
1052
1055
  startedAt,
1053
1056
  runtimeName,
1054
1057
  sessionId: cachedSessionId,
1058
+ ...(cachedSessionId ? {
1059
+ _runtimeResumeAt: Date.now(),
1060
+ _runtimeResumeAwaitingFirstOutput: true,
1061
+ } : {}),
1055
1062
  _pendingSteeringFiles: pendingSteering.entries,
1056
1063
  };
1057
1064
  activeProcesses.set(id, initialProcInfo);
@@ -1063,6 +1070,12 @@ async function spawnAgent(dispatchItem, config) {
1063
1070
  let _trustCheckDone = false;
1064
1071
  const _spawnTime = Date.now();
1065
1072
 
1073
+ function markRuntimeResumeOutputSeen(procInfo) {
1074
+ if (!procInfo?._runtimeResumeAwaitingFirstOutput) return;
1075
+ procInfo._runtimeResumeAwaitingFirstOutput = false;
1076
+ procInfo.lastRealOutputAt = Date.now();
1077
+ }
1078
+
1066
1079
  proc.stdout.on('data', (data) => {
1067
1080
  const chunk = data.toString();
1068
1081
  realActivityMap.set(id, Date.now());
@@ -1086,6 +1099,7 @@ async function spawnAgent(dispatchItem, config) {
1086
1099
  // Capture sessionId early for mid-session steering. Claude emits session_id;
1087
1100
  // Copilot emits sessionId, so use the runtime-neutral steering helper.
1088
1101
  const procInfo = activeProcesses.get(id);
1102
+ markRuntimeResumeOutputSeen(procInfo);
1089
1103
  captureSessionIdFromStdoutChunk(agentId, id, branchName, runtime, procInfo, chunk, sessionCaptureState);
1090
1104
 
1091
1105
  ackPendingSteeringFiles(agentId, procInfo, chunk);
@@ -1096,6 +1110,7 @@ async function spawnAgent(dispatchItem, config) {
1096
1110
  realActivityMap.set(id, Date.now());
1097
1111
  if (stderr.length < MAX_OUTPUT) stderr += chunk.slice(0, MAX_OUTPUT - stderr.length);
1098
1112
  try { fs.appendFileSync(liveOutputPath, '[stderr] ' + chunk); } catch { /* optional */ }
1113
+ markRuntimeResumeOutputSeen(activeProcesses.get(id));
1099
1114
  });
1100
1115
 
1101
1116
  async function onAgentClose(code) {
@@ -1188,11 +1203,14 @@ async function spawnAgent(dispatchItem, config) {
1188
1203
  const childEnv = shared.cleanChildEnv();
1189
1204
  if (completionReportPath) childEnv.MINIONS_COMPLETION_REPORT = completionReportPath;
1190
1205
  childEnv.MINIONS_LIVE_OUTPUT_PATH = liveOutputPath;
1191
- // Inject cached ADO token for steering session too (#998)
1192
- try {
1193
- const adoToken = await getAdoToken();
1194
- if (adoToken) childEnv.MINIONS_ADO_TOKEN = adoToken;
1195
- } catch { /* non-fatal */ }
1206
+ childEnv.MINIONS_REPO_HOST = getRepoHost(project);
1207
+ if (getRepoHost(project) === 'ado') {
1208
+ // Inject cached ADO token for steering session too (#998)
1209
+ try {
1210
+ const adoToken = await getAdoToken();
1211
+ if (adoToken) childEnv.MINIONS_ADO_TOKEN = adoToken;
1212
+ } catch { /* non-fatal */ }
1213
+ }
1196
1214
  let resumeProc;
1197
1215
  try {
1198
1216
  resumeProc = runFile(process.execPath, [spawnScript, steerPromptPath, sysPromptPath, ...resumeArgs], {
@@ -1220,7 +1238,8 @@ async function spawnAgent(dispatchItem, config) {
1220
1238
  startedAt: procInfo.startedAt,
1221
1239
  runtimeName,
1222
1240
  sessionId: steerSessionId,
1223
- lastRealOutputAt: Date.now(),
1241
+ _runtimeResumeAt: Date.now(),
1242
+ _runtimeResumeAwaitingFirstOutput: true,
1224
1243
  _pendingSteeringFiles: mergePendingSteeringEntries(
1225
1244
  procInfo._pendingSteeringFiles,
1226
1245
  pendingForResume.entries,
@@ -1239,6 +1258,7 @@ async function spawnAgent(dispatchItem, config) {
1239
1258
  if (stdout.length < MAX_OUTPUT) stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
1240
1259
  try { fs.appendFileSync(liveOutputPath, chunk); } catch { /* optional */ }
1241
1260
  const resumeInfo = activeProcesses.get(id);
1261
+ markRuntimeResumeOutputSeen(resumeInfo);
1242
1262
  captureSessionIdFromStdoutChunk(agentId, id, branchName, runtime, resumeInfo, chunk, sessionCaptureState);
1243
1263
  ackPendingSteeringFiles(agentId, resumeInfo, chunk);
1244
1264
  });
@@ -1247,6 +1267,7 @@ async function spawnAgent(dispatchItem, config) {
1247
1267
  realActivityMap.set(id, Date.now());
1248
1268
  if (stderr.length < MAX_OUTPUT) stderr += chunk.slice(0, MAX_OUTPUT - stderr.length);
1249
1269
  try { fs.appendFileSync(liveOutputPath, '[stderr] ' + chunk); } catch { /* optional */ }
1270
+ markRuntimeResumeOutputSeen(activeProcesses.get(id));
1250
1271
  });
1251
1272
 
1252
1273
  // Re-wire close handler for the resumed process
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1691",
3
+ "version": "0.1.1693",
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"
@@ -78,6 +78,38 @@ Concretely:
78
78
  - If you skipped local validation, say so in the completion JSON (e.g. `tests: skipped — relying on PR pipeline`) and still exit.
79
79
  - Holding a slot to watch a pipeline is wasted capacity; the engine has its own pipeline-monitoring path.
80
80
 
81
+ {{#github_shared_rules}}
82
+ ## Checking PR and Build Status
83
+
84
+ When asked to check build status, CI results, or review state for a PR:
85
+
86
+ **Preferred — read cached state (refreshed every `prPollStatusEvery` ticks, default ~12 min when engine is running):**
87
+ Find the PR in `projects/<project-name>/pull-requests.json` by `prNumber`. Key fields:
88
+ - `buildStatus` — `passing` | `failing` | `running` | `none`
89
+ - `buildFailReason` — failing check/pipeline name when `buildStatus` is `failing`; inspect live CI logs yourself for details
90
+ - `reviewStatus` — `approved` | `changes-requested` | `waiting` | `pending`
91
+ - `status` — `active` | `merged` | `abandoned`
92
+ - `url` — link to the PR on GitHub
93
+
94
+ **Live status (when engine isn't running or you need up-to-the-moment results):**
95
+ ```bash
96
+ gh pr view <prNumber> --json number,title,state,mergeable,reviewDecision,headRefName,baseRefName,statusCheckRollup --repo OWNER/REPO
97
+ ```
98
+
99
+ ## GitHub Tooling and Auth
100
+
101
+ For GitHub repo operations, use GitHub MCP tools or the `gh` CLI. Prefer commands such as `gh pr create`, `gh pr view`, `gh pr comment`, `gh pr review --comment`, `gh issue view`, and `gh run view`.
102
+
103
+ If GitHub or Copilot auth fails, check GitHub/Copilot credentials only:
104
+ - `gh auth status`
105
+ - `gh auth login`
106
+ - `gh auth token` (for token visibility checks only; do not paste tokens into logs)
107
+ - Set `GH_TOKEN` or `COPILOT_GITHUB_TOKEN` only when the environment already provides an appropriate GitHub token.
108
+
109
+ Only GitHub/Copilot authentication guidance applies to GitHub repository work.
110
+ {{/github_shared_rules}}
111
+
112
+ {{#ado_shared_rules}}
81
113
  ## Checking PR and Build Status
82
114
 
83
115
  When asked to check build status, CI results, or review state for a PR:
@@ -107,3 +139,4 @@ Output is JSON with the same fields. Exit 0 on success, 1 if not found.
107
139
  For Azure DevOps repo operations, use the `az` CLI first. Prefer commands such as `az repos pr create`, `az repos pr show`, `az repos pr list`, `az repos pr comment`, `az repos pr reviewer`, `az boards work-item`, and `az pipelines` after setting defaults with `az devops configure`.
108
140
 
109
141
  Use ADO MCP fallback tools (`mcp__azure-ado__*`) only when `az` is unavailable in the environment or insufficient for a specific operation. Do not choose MCP first just because it exists, and do not use `gh` for Azure DevOps repositories.
142
+ {{/ado_shared_rules}}