@yemi33/minions 0.1.2168 → 0.1.2170

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 CHANGED
@@ -5805,8 +5805,16 @@ const server = http.createServer(async (req, res) => {
5805
5805
  fs.mkdirSync(inboxDir, { recursive: true });
5806
5806
  const today = new Date().toISOString().slice(0, 10);
5807
5807
  const author = body.author || os.userInfo().username;
5808
+ // BUG-H15 / P-h15-traversal: sanitize `author` before composing the
5809
+ // filename. Without slugify, a body like `{ author: "../../escape" }`
5810
+ // collapsed via path.join and wrote outside the inbox directory.
5811
+ // The Origin gate (dashboard.js handleCors above) explicitly allows
5812
+ // missing-Origin requests (curl / CLI tooling), so this sink is
5813
+ // reachable from any local process — including a malicious npm
5814
+ // postinstall or an agent processing an <UNTRUSTED-INPUT> block.
5815
+ const safeAuthor = shared.slugify(author, 40) || 'unknown';
5808
5816
  const slug = shared.slugify(body.title || 'note', 40);
5809
- const filename = `${author}-${slug}-${today}-${shared.uid().slice(-4)}.md`;
5817
+ const filename = `${safeAuthor}-${slug}-${today}-${shared.uid().slice(-4)}.md`;
5810
5818
  const content = `# ${body.title}\n\n**By:** ${author}\n**Date:** ${today}\n\n${body.what}\n${body.why ? '\n**Why:** ' + body.why + '\n' : ''}`;
5811
5819
  safeWrite(shared.uniquePath(path.join(inboxDir, filename)), content);
5812
5820
  invalidateStatusCache();
package/engine/ado.js CHANGED
@@ -1057,6 +1057,131 @@ async function forEachActivePr(config, token, callback) {
1057
1057
  }
1058
1058
  }
1059
1059
 
1060
+ // BUG-C2 — Also poll manually-linked ADO PRs from central pull-requests.json.
1061
+ // Mirror of engine/github.js:540-633. Without this block, any ADO PR enrolled
1062
+ // into central pull-requests.json via the dashboard attach UI or
1063
+ // lifecycle.enrollPrFromCanonicalId is never polled (status, build, review,
1064
+ // comments, abandoned-reconciliation all freeze permanently).
1065
+ //
1066
+ // MIRROR-SYMMETRY CONTRACT: any future divergence between
1067
+ // engine/github.js:540-633 and this branch is itself a bug class. The
1068
+ // canonical 'mirror-ado-github-poll-fix' skill applies. Keep filter, gating,
1069
+ // per-PR iteration, and merge-back semantics symmetric.
1070
+ //
1071
+ // Notable host-specific differences vs github.js:
1072
+ // 1. Tick-scoped probe gating uses per-org ADO throttle state (isAdoThrottled
1073
+ // keyed by orgBase) rather than ghApi('', slug). Rationale: adoFetch throws
1074
+ // on 404/auth failure (no GH_NOT_FOUND-equivalent sentinel), and an extra
1075
+ // per-org probe would only burn throttle budget. Throttle skip mirrors the
1076
+ // same gate used by the project-loop above (lines 991-994).
1077
+ // 2. The project arg is synthesized from parseCanonicalAdoPrId per AC
1078
+ // option (b) — the existing callbacks at lines 1076 and 1618 dereference
1079
+ // project.adoProject; tolerate-null would require touching both call sites
1080
+ // whereas synthesizing keeps the callback contract identical.
1081
+ // 3. The repo-host filter is explicit (repoHost === 'ado') so a future
1082
+ // central poll that loops over both forks doesn't accidentally treat
1083
+ // github PRs as ADO records.
1084
+ // 4. Already-configured PRs are skipped (isPrCompatibleWithProject against
1085
+ // any configured ADO project) so we never double-poll PRs that the
1086
+ // project loop above already handled.
1087
+ const centralPath = path.join(shared.MINIONS_DIR, 'pull-requests.json');
1088
+ const centralPrs = shared.safeJsonArr(centralPath);
1089
+ const configuredAdoProjects = projects.filter(p => !isGitHubProject(p) && p.adoOrg && p.adoProject);
1090
+ const isConfiguredAdoCanonical = (canonicalId) => {
1091
+ if (!canonicalId) return false;
1092
+ return configuredAdoProjects.some(p => shared.isPrCompatibleWithProject(p, { id: canonicalId }, ''));
1093
+ };
1094
+
1095
+ const activeCentral = centralPrs.filter(pr => {
1096
+ if (!shared.PR_POLLABLE_STATUSES.has(pr.status)) return false;
1097
+ if (String(pr.repoHost || 'ado').toLowerCase() !== 'ado') return false;
1098
+ if (!parseCanonicalAdoPrId(pr.id, 'central ADO PR poll')) return false;
1099
+ if (isConfiguredAdoCanonical(pr.id)) return false;
1100
+ return true;
1101
+ });
1102
+
1103
+ // Per-org throttle probe (mirrors github.js:563-581 slug-probe gating).
1104
+ // Cached per tick (Map cleared each forEachActivePr invocation) so multiple
1105
+ // central PRs sharing one orgBase do a single throttle check.
1106
+ const centralOrgProbes = new Map(); // orgBase → 'ok' | 'throttled'
1107
+ for (const pr of activeCentral) {
1108
+ const parsed = parseCanonicalAdoPrId(pr.id, 'central ADO PR poll');
1109
+ if (!parsed) continue;
1110
+ const orgBase = getAdoOrgBase(parsed);
1111
+ if (centralOrgProbes.has(orgBase)) continue;
1112
+ if (isAdoThrottled(orgBase)) {
1113
+ log('info', `[ado] central PR poll skipped for ${orgBase} — throttled`);
1114
+ centralOrgProbes.set(orgBase, 'throttled');
1115
+ } else {
1116
+ centralOrgProbes.set(orgBase, 'ok');
1117
+ }
1118
+ }
1119
+
1120
+ let centralUpdated = 0;
1121
+ const updatedCentralRecords = [];
1122
+ for (const pr of activeCentral) {
1123
+ const parsed = parseCanonicalAdoPrId(pr.id, 'central ADO PR poll');
1124
+ if (!parsed) continue;
1125
+ const orgBase = getAdoOrgBase(parsed);
1126
+ if (centralOrgProbes.get(orgBase) !== 'ok') continue;
1127
+ const prNum = shared.getPrNumber(pr);
1128
+ if (!prNum) continue;
1129
+
1130
+ // Option (b) per BUG-C2 AC: synthesize a minimal project object from the
1131
+ // parsed canonical id. Mirrors the shape the existing callbacks expect
1132
+ // (project.adoOrg / project.adoProject / project.repoName / project.repositoryId)
1133
+ // so neither pollPrStatus nor pollPrHumanComments needs a tolerate-null branch.
1134
+ const synthesizedProject = {
1135
+ adoOrg: parsed.adoOrg,
1136
+ adoProject: parsed.adoProject,
1137
+ repoName: parsed.repoName,
1138
+ repositoryId: parsed.repositoryId,
1139
+ repoHost: 'ado',
1140
+ prUrlBase: parsed.prUrlBase || '',
1141
+ name: `central:${parsed.adoOrg}/${parsed.adoProject}/${parsed.repoName || parsed.repositoryId || 'unknown'}`,
1142
+ _synthesizedFromCentralPr: true,
1143
+ };
1144
+ const adoRepositoryId = getAdoRepositoryId(synthesizedProject);
1145
+ if (!adoRepositoryId) {
1146
+ log('warn', `[ado] central PR ${pr.id}: cannot resolve adoRepositoryId from canonical id — skipping`);
1147
+ continue;
1148
+ }
1149
+
1150
+ try {
1151
+ const before = shared.snapshotPrRecord(pr);
1152
+ const updated = await callback(synthesizedProject, pr, prNum, orgBase, adoRepositoryId);
1153
+ if (updated) {
1154
+ centralUpdated++;
1155
+ updatedCentralRecords.push({ before, after: shared.snapshotPrRecord(pr) });
1156
+ }
1157
+ } catch (err) {
1158
+ log('warn', `ADO: failed to poll central PR ${pr.id}: ${err.message}`);
1159
+ }
1160
+ }
1161
+
1162
+ if (centralUpdated > 0) {
1163
+ mutateJsonFileLocked(centralPath, (currentPrs) => {
1164
+ for (const { before, after } of updatedCentralRecords) {
1165
+ const idx = currentPrs.findIndex(p => p.id === after.id);
1166
+ if (idx >= 0) {
1167
+ // Never downgrade reviewStatus from 'approved' — permanent terminal state
1168
+ // (mirrors project-loop guard at lines 1038-1040).
1169
+ if (currentPrs[idx].reviewStatus === REVIEW_STATUS.APPROVED && after.reviewStatus !== REVIEW_STATUS.APPROVED) {
1170
+ after.reviewStatus = REVIEW_STATUS.APPROVED;
1171
+ }
1172
+ // Never downgrade status from 'merged' — permanent terminal state
1173
+ // (mirrors github.js:634-636 central merged-status guard).
1174
+ if (currentPrs[idx].status === PR_STATUS.MERGED && after.status !== PR_STATUS.MERGED) {
1175
+ after.status = PR_STATUS.MERGED;
1176
+ }
1177
+ shared.applyPrFieldDelta(currentPrs[idx], before, after);
1178
+ }
1179
+ }
1180
+ return currentPrs;
1181
+ }, { defaultValue: [] });
1182
+ totalUpdated += centralUpdated;
1183
+ }
1184
+
1060
1185
  return totalUpdated;
1061
1186
  }
1062
1187
 
@@ -2603,6 +2728,7 @@ async function reconcileAbandonedPrs(config) {
2603
2728
  module.exports = {
2604
2729
  getAdoToken,
2605
2730
  adoFetch,
2731
+ forEachActivePr, // BUG-C2 — exported for central-PR mirror-symmetry tests
2606
2732
  pollPrStatus,
2607
2733
  pollPrHumanComments,
2608
2734
  reconcilePrs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2168",
3
+ "version": "0.1.2170",
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"