@yemi33/minions 0.1.1707 → 0.1.1709

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,14 +1,20 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1707 (2026-05-04)
3
+ ## 0.1.1709 (2026-05-04)
4
+
5
+ ### Fixes
6
+ - auto-heal projects with missing workSources at engine boot
7
+
8
+ ## 0.1.1708 (2026-05-04)
9
+
10
+ ### Fixes
11
+ - surface silent-discovery footgun when projects miss workSources
12
+
13
+ ## 0.1.1706 (2026-05-04)
4
14
 
5
15
  ### Features
6
16
  - preserve CC fan-out dedup scope (#2037)
7
17
 
8
- ### Other
9
- - test(llm): add unit tests for resolution helper edge cases and bin cache reset (#2039)
10
- - test(ado): add unit tests for pure helpers (project label, GUID, metadata apply, stale flag) (#2038)
11
-
12
18
  ## 0.1.1705 (2026-05-04)
13
19
 
14
20
  ### Features
package/engine/cli.js CHANGED
@@ -379,6 +379,28 @@ const commands = {
379
379
  // refactor. No disk write — the on-disk config still carries `ccModel`.
380
380
  try { shared.applyLegacyCcModelMigration(config, { logger: e.log }); }
381
381
  catch (err) { e.log('warn', `legacy ccModel migration failed: ${err.message}`); }
382
+
383
+ // Auto-heal projects missing workSources (cloned-repo / hand-rolled-config
384
+ // footgun): without this block, discoverFromWorkItems / discoverFromPrs
385
+ // bail silently and the engine looks healthy but never dispatches. The
386
+ // disk-side mutation re-derives heal state from the on-disk copy so we
387
+ // don't clobber a concurrent dashboard write between the in-memory check
388
+ // and the lock acquire. skipWriteIfUnchanged makes the write a no-op when
389
+ // nothing needed healing (e.g. dashboard already fixed it).
390
+ try {
391
+ const heal = shared.backfillProjectWorkSourceDefaults(config);
392
+ if (heal.changed) {
393
+ const configPath = path.join(shared.MINIONS_DIR, 'config.json');
394
+ shared.mutateJsonFileLocked(configPath, (onDisk) => {
395
+ shared.backfillProjectWorkSourceDefaults(onDisk);
396
+ return onDisk;
397
+ }, { defaultValue: {}, skipWriteIfUnchanged: true });
398
+ for (const h of heal.healed) {
399
+ e.log('info', `Auto-healed project "${h.project}" — backfilled missing workSources: ${h.sources.join(', ')}`);
400
+ }
401
+ console.log(` Auto-healed ${heal.healed.length} project(s) with missing workSources defaults — engine will now dispatch their work.`);
402
+ }
403
+ } catch (err) { e.log('warn', `workSources auto-heal failed: ${err.message}`); }
382
404
  const interval = config.engine?.tickInterval || shared.ENGINE_DEFAULTS.tickInterval;
383
405
 
384
406
  const { getProjects } = require('./shared');
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-04T15:04:13.291Z"
4
+ "cachedAt": "2026-05-04T16:53:06.222Z"
5
5
  }
@@ -226,6 +226,24 @@ function runPreflight(opts = {}) {
226
226
  results.push({ name: `Runtime config (${w.id})`, ok: 'warn', message: w.message });
227
227
  }
228
228
  } catch { /* defensive — preflight must never throw */ }
229
+
230
+ // Project workSources warnings — catches the silent-discovery footgun
231
+ // where a project (often added by a hand-rolled config or by cloning the
232
+ // repo without `minions init`) has no workSources block, so engine.js
233
+ // discoverFromWorkItems / discoverFromPrs return [] without logging.
234
+ try {
235
+ const projectWarns = shared.projectWorkSourceWarnings(opts.config, (project) => {
236
+ const wiPath = shared.projectWorkItemsPath(project);
237
+ const prPath = shared.projectPrPath(project);
238
+ const safeJson = shared.safeJson;
239
+ const wi = (() => { try { const a = safeJson(wiPath); return Array.isArray(a) ? a.length : 0; } catch { return 0; } })();
240
+ const pr = (() => { try { const a = safeJson(prPath); return Array.isArray(a) ? a.length : 0; } catch { return 0; } })();
241
+ return { workItems: wi, pullRequests: pr };
242
+ });
243
+ for (const w of projectWarns) {
244
+ results.push({ name: `Project config (${w.id})`, ok: 'warn', message: w.message });
245
+ }
246
+ } catch { /* defensive */ }
229
247
  }
230
248
 
231
249
  return { passed: allOk, results };
package/engine/shared.js CHANGED
@@ -1120,6 +1120,109 @@ function runtimeConfigWarnings(config, registeredRuntimes) {
1120
1120
  return warnings;
1121
1121
  }
1122
1122
 
1123
+ /**
1124
+ * Detect projects whose discovery would silently no-op because the
1125
+ * `workSources` block is missing or its sub-flags are disabled. Catches the
1126
+ * common "I cloned the repo and ran it without `minions init`" footgun where
1127
+ * `engine.js` `discoverFromWorkItems` / `discoverFromPrs` bail on
1128
+ * `if (!src?.enabled) return [];` with no log output.
1129
+ *
1130
+ * Pure helper — pass `getDataCounts(project) → { workItems: N, pullRequests: N }`
1131
+ * so the caller controls disk reads (preflight reads files; tests inject counts).
1132
+ * If `getDataCounts` is omitted, every project with a missing/disabled source
1133
+ * is reported (caller decides whether to surface).
1134
+ *
1135
+ * Returns: `{ id, message, project }[]` — `id` is one of:
1136
+ * - `project-worksources-missing` — no workSources block at all
1137
+ * - `project-worksources-disabled` — block exists but a sub-source is disabled
1138
+ */
1139
+ function projectWorkSourceWarnings(config, getDataCounts) {
1140
+ const warnings = [];
1141
+ if (!config || typeof config !== 'object') return warnings;
1142
+ const projects = Array.isArray(config.projects) ? config.projects : [];
1143
+ const topSources = config.workSources || null;
1144
+ for (const project of projects) {
1145
+ if (!project || typeof project !== 'object') continue;
1146
+ const projSources = project.workSources || null;
1147
+ const counts = (typeof getDataCounts === 'function')
1148
+ ? (getDataCounts(project) || {})
1149
+ : { workItems: Infinity, pullRequests: Infinity };
1150
+ const wiCount = Number(counts.workItems) || 0;
1151
+ const prCount = Number(counts.pullRequests) || 0;
1152
+
1153
+ // Case 1: project has no workSources AND no top-level fallback. Discovery
1154
+ // will return [] for everything. This is the cloned-repo footgun.
1155
+ if (!projSources && !topSources) {
1156
+ if (wiCount > 0 || prCount > 0) {
1157
+ warnings.push({
1158
+ id: 'project-worksources-missing',
1159
+ project: project.name,
1160
+ message: `Project "${project.name}" has no workSources block — work-item and PR discovery are silently disabled (${wiCount} work item(s), ${prCount} PR(s) waiting). Run \`minions init\` if you cloned the repo directly, or re-link the project: \`minions add ${project.localPath || project.name}\`.`,
1161
+ });
1162
+ }
1163
+ continue;
1164
+ }
1165
+
1166
+ // Case 2: workSources exists but a sub-source is disabled while data sits
1167
+ // unprocessed. Could be intentional, but worth surfacing.
1168
+ const wiSrc = projSources?.workItems || topSources?.workItems;
1169
+ if ((!wiSrc || wiSrc.enabled === false) && wiCount > 0) {
1170
+ warnings.push({
1171
+ id: 'project-worksources-disabled',
1172
+ project: project.name,
1173
+ message: `Project "${project.name}" has ${wiCount} unprocessed work item(s) but workSources.workItems.enabled is not true — engine will not dispatch them. Toggle in Dashboard → Settings → Project, or set \`workSources.workItems.enabled: true\` in config.json.`,
1174
+ });
1175
+ }
1176
+ const prSrc = projSources?.pullRequests || topSources?.pullRequests;
1177
+ if ((!prSrc || prSrc.enabled === false) && prCount > 0) {
1178
+ warnings.push({
1179
+ id: 'project-worksources-disabled',
1180
+ project: project.name,
1181
+ message: `Project "${project.name}" has ${prCount} pull-request record(s) but workSources.pullRequests.enabled is not true — engine will not poll or review them. Toggle in Dashboard → Settings → Project, or set \`workSources.pullRequests.enabled: true\` in config.json.`,
1182
+ });
1183
+ }
1184
+ }
1185
+ return warnings;
1186
+ }
1187
+
1188
+ /**
1189
+ * Boot-time auto-heal for the cloned-repo / hand-rolled-config footgun: any
1190
+ * project missing a `workSources.workItems` or `workSources.pullRequests`
1191
+ * sub-block gets the dashboard's default backfilled. We only touch *missing*
1192
+ * sub-blocks — an explicit `enabled: false` is treated as user intent and
1193
+ * left alone.
1194
+ *
1195
+ * Pure helper — mutates `config` in place and returns
1196
+ * `{ changed: boolean, healed: Array<{ project, sources }> }` so the caller
1197
+ * can decide whether to persist + log. Returns `{ changed: false, healed: [] }`
1198
+ * for null/empty/malformed input.
1199
+ */
1200
+ function backfillProjectWorkSourceDefaults(config) {
1201
+ const result = { changed: false, healed: [] };
1202
+ if (!config || typeof config !== 'object') return result;
1203
+ const projects = Array.isArray(config.projects) ? config.projects : [];
1204
+ for (const project of projects) {
1205
+ if (!project || typeof project !== 'object') continue;
1206
+ const filled = [];
1207
+ if (!project.workSources || typeof project.workSources !== 'object') {
1208
+ project.workSources = {};
1209
+ }
1210
+ if (!project.workSources.pullRequests) {
1211
+ project.workSources.pullRequests = { enabled: true, cooldownMinutes: 30 };
1212
+ filled.push('pullRequests');
1213
+ }
1214
+ if (!project.workSources.workItems) {
1215
+ project.workSources.workItems = { enabled: true, cooldownMinutes: 0 };
1216
+ filled.push('workItems');
1217
+ }
1218
+ if (filled.length > 0) {
1219
+ result.changed = true;
1220
+ result.healed.push({ project: project.name, sources: filled });
1221
+ }
1222
+ }
1223
+ return result;
1224
+ }
1225
+
1123
1226
  // ─── Status & Type Constants ─────────────────────────────────────────────────
1124
1227
 
1125
1228
  const WI_STATUS = {
@@ -2515,6 +2618,8 @@ module.exports = {
2515
2618
  resolveAgentMaxBudget, resolveAgentBareMode,
2516
2619
  applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
2517
2620
  runtimeConfigWarnings,
2621
+ projectWorkSourceWarnings,
2622
+ backfillProjectWorkSourceDefaults,
2518
2623
  WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, PR_PENDING_REASON, DISPATCH_RESULT, trackReviewMetric, queuePlanToPrd,
2519
2624
  WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS,
2520
2625
  PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
package/engine.js CHANGED
@@ -2252,12 +2252,35 @@ function ensurePrBranchForDispatch(project, pr, automationType) {
2252
2252
  }
2253
2253
 
2254
2254
 
2255
+ // Tracks per-process which silent-discovery warnings have already been logged
2256
+ // so we don't spam the log every tick. Cleared on process exit (no need to
2257
+ // persist — the warning is for the operator at engine startup/run time).
2258
+ const _warnedSilentDiscovery = new Set();
2259
+
2260
+ function _warnSilentDiscoveryOnce(kind, project, dataPath, config) {
2261
+ const key = `${kind}:${project?.name || project?.localPath || 'default'}`;
2262
+ if (_warnedSilentDiscovery.has(key)) return;
2263
+ let count = 0;
2264
+ try { const arr = safeJson(dataPath); if (Array.isArray(arr)) count = arr.length; } catch {}
2265
+ if (count <= 0) return; // empty file is fine — no warning worth surfacing
2266
+ _warnedSilentDiscovery.add(key);
2267
+ const projName = project?.name || '(unnamed)';
2268
+ const hasBlock = !!(project?.workSources?.[kind] || config?.workSources?.[kind]);
2269
+ const hint = hasBlock
2270
+ ? `Toggle in Dashboard → Settings → Project, or set \`workSources.${kind}.enabled: true\` in config.json.`
2271
+ : `Run \`minions doctor\` to inspect, \`minions init\` if you cloned the repo without setup, or re-link the project: \`minions add ${project?.localPath || projName}\`.`;
2272
+ log('warn', `Silent-discovery footgun: project "${projName}" has ${count} record(s) in ${path.basename(dataPath)} but workSources.${kind}.enabled is not true — engine will not pick them up. ${hint}`);
2273
+ }
2274
+
2255
2275
  /**
2256
2276
  * Scan pull-requests.json for PRs needing review or fixes
2257
2277
  */
2258
2278
  async function discoverFromPrs(config, project) {
2259
2279
  const src = project?.workSources?.pullRequests || config.workSources?.pullRequests;
2260
- if (!src?.enabled) return [];
2280
+ if (!src?.enabled) {
2281
+ _warnSilentDiscoveryOnce('pullRequests', project, projectPrPath(project), config);
2282
+ return [];
2283
+ }
2261
2284
 
2262
2285
  const prs = queries.getPrs(project);
2263
2286
  const cooldownMs = (src.cooldownMinutes || 30) * 60 * 1000;
@@ -2757,7 +2780,10 @@ function refreshDeferredWorkItemPrompt(item, config) {
2757
2780
 
2758
2781
  function discoverFromWorkItems(config, project) {
2759
2782
  const src = project?.workSources?.workItems || config.workSources?.workItems;
2760
- if (!src?.enabled) return [];
2783
+ if (!src?.enabled) {
2784
+ _warnSilentDiscoveryOnce('workItems', project, projectWorkItemsPath(project), config);
2785
+ return [];
2786
+ }
2761
2787
 
2762
2788
  const root = project?.localPath ? path.resolve(project.localPath) : path.resolve(MINIONS_DIR, '..');
2763
2789
  const items = safeJson(projectWorkItemsPath(project)) || [];
package/minions.js CHANGED
@@ -173,6 +173,13 @@ function buildProjectEntry({ name, description, localPath, repoHost, repositoryI
173
173
  repoName: repoName || name,
174
174
  mainBranch: mainBranch || 'main',
175
175
  prUrlBase: buildPrUrlBase({ repoHost, org, project, repoName }),
176
+ // Discovery defaults must mirror dashboard.js POST /api/projects — without
177
+ // these, discoverFromWorkItems / discoverFromPrs silently no-op (the engine
178
+ // looks healthy but never dispatches anything).
179
+ workSources: {
180
+ pullRequests: { enabled: true, cooldownMinutes: 30 },
181
+ workItems: { enabled: true, cooldownMinutes: 0 },
182
+ },
176
183
  };
177
184
  }
178
185
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1707",
3
+ "version": "0.1.1709",
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"