@yemi33/minions 0.1.2249 → 0.1.2250

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.
@@ -59,9 +59,13 @@ async function fetchPullRequestsFromDisk(projects) {
59
59
  return out;
60
60
  }
61
61
 
62
+ // Terminal statuses — follow-up WIs in these states are historical, not active.
63
+ var _WI_TERMINAL = new Set(['done', 'cancelled', 'failed', 'decomposed', 'in-pr', 'implemented', 'complete']);
64
+
62
65
  function _countPrFollowups(pr) {
63
- // PR follow-up chip (W-mpej3cox00099466) — counts WIs whose
64
- // meta.pr_followup.parent_pr_url or parent_pr_id matches this PR.
66
+ // PR follow-up chip (W-mpej3cox00099466) — counts ACTIVE (non-terminal) WIs
67
+ // whose meta.pr_followup.parent_pr_url or parent_pr_id matches this PR.
68
+ // Terminal WIs (done/cancelled/failed) are excluded to avoid stale noise.
65
69
  if (!pr) return 0;
66
70
  var wis = (window._lastWorkItems) || [];
67
71
  if (!wis.length) return 0;
@@ -69,7 +73,9 @@ function _countPrFollowups(pr) {
69
73
  var prId = pr.id || '';
70
74
  var n = 0;
71
75
  for (var i = 0; i < wis.length; i++) {
72
- var f = wis[i] && wis[i].meta && wis[i].meta.pr_followup;
76
+ var wi = wis[i];
77
+ if (!wi || _WI_TERMINAL.has(wi.status)) continue;
78
+ var f = wi.meta && wi.meta.pr_followup;
73
79
  if (!f) continue;
74
80
  if (prUrl && f.parent_pr_url === prUrl) { n++; continue; }
75
81
  if (prId && f.parent_pr_id === prId) { n++; }
@@ -114,7 +120,7 @@ function prRow(pr) {
114
120
  : '';
115
121
  var followupCount = _countPrFollowups(pr);
116
122
  var followupChip = followupCount > 0
117
- ? ' <span class="pr-badge draft" style="font-size:var(--text-xs)" title="' + followupCount + ' follow-up work item(s) dispatched from comments on this PR">+' + followupCount + ' follow-up' + (followupCount === 1 ? '' : 's') + '</span>'
123
+ ? ' <span class="pr-badge draft" style="font-size:var(--text-xs)" title="' + followupCount + ' active follow-up work item(s) in progress from comments on this PR">+' + followupCount + ' follow-up' + (followupCount === 1 ? '' : 's') + '</span>'
118
124
  : '';
119
125
  // Issue #2969 — paused-cause chip(s). Prefer the API-enriched _pausedCauses
120
126
  // field; fall back to deriving from _noOpFixes for the /state/ disk-fetch
@@ -210,7 +210,8 @@ function isAdoAuthFailure(err) {
210
210
 
211
211
  function _redactBearer(s) {
212
212
  if (typeof s !== 'string') return s;
213
- return s.replace(/Authorization:\s*Bearer\s+[A-Za-z0-9._\-]+/gi, 'Authorization: Bearer [REDACTED]');
213
+ // W-mqrb8okm — include +/= so base64-padded ADO tokens are fully redacted
214
+ return s.replace(/Authorization:\s*Bearer\s+[A-Za-z0-9+/=._\-]+/gi, 'Authorization: Bearer [REDACTED]');
214
215
  }
215
216
 
216
217
  function _redactErrorInPlace(err) {
package/engine/ado.js CHANGED
@@ -1207,22 +1207,28 @@ async function forEachActivePr(config, token, callback) {
1207
1207
  // 3. The repo-host filter is explicit (repoHost === 'ado') so a future
1208
1208
  // central poll that loops over both forks doesn't accidentally treat
1209
1209
  // github PRs as ADO records.
1210
- // 4. Already-configured PRs are skipped (isPrCompatibleWithProject against
1211
- // any configured ADO project) so we never double-poll PRs that the
1212
- // project loop above already handled.
1210
+ // 4. Already-configured PRs are skipped only when they are actually present
1211
+ // in that project's per-project file, so the project loop will really see
1212
+ // them. Blindly skipping all project-compatible IDs caused a permanent
1213
+ // orphan when the record lived in the central file (#328).
1213
1214
  const centralPath = path.join(shared.MINIONS_DIR, 'pull-requests.json');
1214
1215
  const centralPrs = shared.safeJsonArr(centralPath);
1215
1216
  const configuredAdoProjects = projects.filter(p => !isGitHubProject(p) && p.adoOrg && p.adoProject);
1216
- const isConfiguredAdoCanonical = (canonicalId) => {
1217
- if (!canonicalId) return false;
1218
- return configuredAdoProjects.some(p => shared.isPrCompatibleWithProject(p, { id: canonicalId }, ''));
1219
- };
1220
1217
 
1221
1218
  const activeCentral = centralPrs.filter(pr => {
1222
1219
  if (!shared.PR_POLLABLE_STATUSES.has(pr.status)) return false;
1223
1220
  if (String(pr.repoHost || 'ado').toLowerCase() !== 'ado') return false;
1224
1221
  if (!parseCanonicalAdoPrId(pr.id, 'central ADO PR poll')) return false;
1225
- if (isConfiguredAdoCanonical(pr.id)) return false;
1222
+ // Only defer to the project loop when the PR is ACTUALLY present in that
1223
+ // project's per-project file. Unconditionally skipping all project-compatible
1224
+ // PRs caused a permanent orphan: the project loop never read the central file,
1225
+ // so a PR stored centrally but compatible with a configured project was never
1226
+ // polled (fix for #328).
1227
+ const owningProject = configuredAdoProjects.find(p => shared.isPrCompatibleWithProject(p, { id: pr.id }, ''));
1228
+ if (owningProject) {
1229
+ const projectPrs = shared.safeJsonArr(shared.projectPrPath(owningProject));
1230
+ if (projectPrs.some(p => p.id === pr.id)) return false; // present in per-project file → project loop handles it
1231
+ }
1226
1232
  return true;
1227
1233
  });
1228
1234
 
@@ -602,6 +602,9 @@ function writeFailedAgentReport(item, reason, resultSummary, failureClass) {
602
602
  log('warn', 'Cannot write failed agent report without dispatch id');
603
603
  return;
604
604
  }
605
+ // W-mqrb8okm — redact bearer tokens before writing to inbox notes
606
+ const safeReason = reason ? shared.redactSecrets(reason) : reason;
607
+ const safeSummary = resultSummary ? shared.redactSecrets(resultSummary) : resultSummary;
605
608
  const agentId = item.agent || 'engine';
606
609
  const itemId = item.meta?.item?.id || '';
607
610
  const title = item.meta?.item?.title || item.task || item.id;
@@ -620,9 +623,9 @@ function writeFailedAgentReport(item, reason, resultSummary, failureClass) {
620
623
  `**Type:** ${item.type || 'unknown'}`,
621
624
  `**Result:** ${DISPATCH_RESULT.ERROR}`,
622
625
  failureClass ? `**Failure Class:** ${failureClass}` : '',
623
- reason ? `**Reason:** ${reason}` : '',
626
+ safeReason ? `**Reason:** ${safeReason}` : '',
624
627
  '',
625
- resultSummary ? `## Summary\n${resultSummary}` : '## Summary\n(no agent summary captured)',
628
+ safeSummary ? `## Summary\n${safeSummary}` : '## Summary\n(no agent summary captured)',
626
629
  ].filter(Boolean).join('\n');
627
630
  shared.writeToInbox(agentId, `agent-failure-${item.id}`, content, null, metadata);
628
631
  }
@@ -1164,4 +1167,5 @@ module.exports = {
1164
1167
  isCompletedWorkItemForFailure,
1165
1168
  _isPrdSourcedAndVetted,
1166
1169
  NON_MUTATING_DISPATCH_TYPES,
1170
+ writeFailedAgentReport,
1167
1171
  };
package/engine/github.js CHANGED
@@ -594,7 +594,25 @@ async function forEachActiveGhPr(config, callback) {
594
594
  // Also poll manually-linked PRs from central pull-requests.json (extract slug from URL)
595
595
  const centralPath = path.join(MINIONS_DIR, 'pull-requests.json');
596
596
  const centralPrs = safeJsonArr(centralPath);
597
- const activeCentral = centralPrs.filter(pr => PR_POLLABLE_STATUSES.has(pr.status) && pr.url);
597
+ // Build a slug→project map for configured GitHub projects so we can detect
598
+ // PRs that are actually owned by the project loop (present in per-project file).
599
+ const configuredGhProjectsBySlug = new Map(projects.map(p => [getRepoSlug(p), p]));
600
+ const activeCentral = centralPrs.filter(pr => {
601
+ if (!PR_POLLABLE_STATUSES.has(pr.status)) return false;
602
+ if (!pr.url) return false;
603
+ // Symmetric fix for #328: only defer to the project loop when the PR is
604
+ // ACTUALLY present in that project's per-project file. If it's absent there,
605
+ // the project loop will never see it — the central branch must poll it.
606
+ const ghMatch = pr.url.match(/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/);
607
+ if (ghMatch) {
608
+ const owningProject = configuredGhProjectsBySlug.get(ghMatch[1]);
609
+ if (owningProject) {
610
+ const projectPrs = safeJsonArr(projectPrPath(owningProject));
611
+ if (projectPrs.some(p => p.id === pr.id)) return false; // present → project loop handles it
612
+ }
613
+ }
614
+ return true;
615
+ });
598
616
 
599
617
  // W-mp5trwh60008386d: probe each unique slug in the central list ONCE before iterating PRs.
600
618
  // Without this gate, central PRs would inherit the same per-PR 404 trapdoor that project-local
@@ -720,7 +720,8 @@ function updateWorkItemStatus(meta, status, reason) {
720
720
  delete target._noopReason;
721
721
  }
722
722
  } else if (status === WI_STATUS.FAILED) {
723
- if (reason) target.failReason = reason;
723
+ // W-mqrb8okm — redact bearer tokens from reason before persisting as failReason
724
+ if (reason) target.failReason = shared.redactSecrets(reason);
724
725
  target.failedAt = ts();
725
726
  }
726
727
  }
package/engine/shared.js CHANGED
@@ -403,6 +403,19 @@ function _redactString(s, options = {}) {
403
403
  .replace(_JWT_RE, '[REDACTED_JWT]');
404
404
  }
405
405
 
406
+ // W-mqrb8okm — Redact bearer tokens from git error.message / stdout / stderr
407
+ // in-place. Called from shellSafeGit / shellSafeGitSync catch paths so that ADO
408
+ // -c http.extraHeader=Authorization: Bearer TOKEN args embedded in the
409
+ // "Command failed: git ..." error message are sanitised before the error
410
+ // propagates to callers (who may store it as failReason / inbox notes).
411
+ function _redactGitError(err) {
412
+ if (!err || typeof err !== 'object') return err;
413
+ if (err.message) err.message = _redactString(err.message);
414
+ if (typeof err.stdout === 'string') err.stdout = _redactString(err.stdout);
415
+ if (typeof err.stderr === 'string') err.stderr = _redactString(err.stderr);
416
+ return err;
417
+ }
418
+
406
419
  function redactSecrets(value, options = {}) {
407
420
  if (value == null) return value;
408
421
  if (typeof value === 'string') return _redactString(value, options);
@@ -2342,17 +2355,27 @@ function shellSafeGit(args, opts = {}) {
2342
2355
  // can inject per-invocation `-c key=value` flags (e.g. ADO bearer-token
2343
2356
  // auth header) without rewriting every shellSafeGit call site. Strip the
2344
2357
  // key before delegating so it never reaches Node's execFile options.
2345
- const { timeout, gitExtraArgs, ...rest } = opts;
2358
+ // W-mqrb8okm `_execFileAsync` injection point for unit tests; production
2359
+ // callers leave it unset so the module-level promisified execFile is used.
2360
+ const { timeout, gitExtraArgs, _execFileAsync: _execFileOverride, ...rest } = opts;
2346
2361
  const finalArgs = (Array.isArray(gitExtraArgs) && gitExtraArgs.length > 0)
2347
2362
  ? [...gitExtraArgs, ...args]
2348
2363
  : args;
2349
- return _execFileAsync('git', finalArgs, {
2364
+ const execFn = _execFileOverride || _execFileAsync;
2365
+ return execFn('git', finalArgs, {
2350
2366
  windowsHide: true,
2351
2367
  encoding: 'utf8',
2352
2368
  shell: false,
2353
2369
  ...rest,
2354
2370
  timeout: timeout || 30000,
2355
- }).then(({ stdout }) => stdout);
2371
+ }).then(({ stdout }) => stdout).catch(err => {
2372
+ // W-mqrb8okm — Redact bearer tokens from git errors before they propagate.
2373
+ // The "Command failed: git ..." message produced by Node's execFile includes
2374
+ // every argv element, so -c http.extraHeader=Authorization: Bearer TOKEN
2375
+ // would end up verbatim in failReason / inbox notes without this guard.
2376
+ _redactGitError(err);
2377
+ throw err;
2378
+ });
2356
2379
  }
2357
2380
 
2358
2381
  // Sync argv-form helper for callers that aren't async (e.g. plan
@@ -2368,14 +2391,19 @@ function shellSafeGitSync(args, opts = {}) {
2368
2391
  ? [...gitExtraArgs, ...args]
2369
2392
  : args;
2370
2393
  const { execFileSync: _execFileSync } = require('child_process');
2371
- return _execFileSync('git', finalArgs, {
2372
- windowsHide: true,
2373
- encoding: 'utf8',
2374
- shell: false,
2375
- stdio: 'pipe',
2376
- ...rest,
2377
- timeout: timeout || 30000,
2378
- });
2394
+ try {
2395
+ return _execFileSync('git', finalArgs, {
2396
+ windowsHide: true,
2397
+ encoding: 'utf8',
2398
+ shell: false,
2399
+ stdio: 'pipe',
2400
+ ...rest,
2401
+ timeout: timeout || 30000,
2402
+ });
2403
+ } catch (err) {
2404
+ // W-mqrb8okm — same bearer-token redaction as the async variant.
2405
+ throw _redactGitError(err);
2406
+ }
2379
2407
  }
2380
2408
 
2381
2409
  /**
package/engine.js CHANGED
@@ -8422,7 +8422,7 @@ function discoverCentralWorkItems(config) {
8422
8422
  item_id: item.id,
8423
8423
  item_name: item.title || item.id,
8424
8424
  item_priority: item.priority || 'medium',
8425
- item_description: item.description || '',
8425
+ item_description: shared.redactPromptDescription(item.description || ''),
8426
8426
  work_type: workType,
8427
8427
  additional_context: item.prompt ? `## Additional Context\n\n${item.prompt}` : '',
8428
8428
  scope_section: buildProjectContext(dispatchProjects, assignedProject, true, agent.name, agent.role),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2249",
3
+ "version": "0.1.2250",
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"