@yemi33/minions 0.1.533 → 0.1.534

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,8 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.533 (2026-04-07)
3
+ ## 0.1.534 (2026-04-07)
4
4
 
5
5
  ### Fixes
6
+ - update pipeline work item status on dispatch (#443)
6
7
  - convert blocking spawnSync/execSync to async execAsync (#447)
7
8
 
8
9
  ## 0.1.532 (2026-04-07)
@@ -184,7 +184,6 @@ function executeTaskStage(stage, stageState, run, config) {
184
184
  createdIds.push(id);
185
185
  }
186
186
  });
187
-
188
187
  return { status: PIPELINE_STATUS.RUNNING, artifacts: { workItems: createdIds } };
189
188
  }
190
189
 
@@ -278,7 +277,7 @@ async function executePlanStage(stage, stageState, run, config) {
278
277
 
279
278
  safeWrite(filePath, content);
280
279
 
281
- // Create plan-to-prd work item
280
+ // Create plan-to-prd work item — atomic write to prevent race with dispatch status updates
282
281
  const wiPath = path.join(__dirname, '..', 'work-items.json');
283
282
  const wiId = `PL-${run.runId.slice(4, 12)}-${stage.id}-prd`;
284
283
  mutateWorkItems(wiPath, workItems => {
package/engine.js CHANGED
@@ -1939,13 +1939,26 @@ function discoverCentralWorkItems(config) {
1939
1939
  const items = safeJson(centralPath) || [];
1940
1940
  const projects = getProjects(config);
1941
1941
  const newWork = [];
1942
+ // Collect mutations to apply atomically inside lock callback (avoids TOCTOU)
1943
+ const mutations = new Map(); // item.id → { field: value, ... }
1942
1944
 
1943
1945
  for (const item of items) {
1944
1946
  try {
1945
1947
  if (item.status !== WI_STATUS.QUEUED && item.status !== WI_STATUS.PENDING) continue;
1946
1948
 
1947
1949
  const key = `central-work-${item.id}`;
1948
- if (isAlreadyDispatched(key) || isOnCooldown(key, 0)) continue;
1950
+ // Self-heal: if already dispatched but work item is still pending, fix the status
1951
+ if (isAlreadyDispatched(key)) {
1952
+ const m = {};
1953
+ if (item.status === WI_STATUS.PENDING) { m.status = WI_STATUS.DISPATCHED; }
1954
+ if (!item.dispatched_to) {
1955
+ const existing = getDispatch().active?.find(d => d.meta?.dispatchKey === key);
1956
+ if (existing?.agent) { m.dispatched_to = existing.agent; }
1957
+ }
1958
+ if (Object.keys(m).length > 0) mutations.set(item.id, m);
1959
+ continue;
1960
+ }
1961
+ if (isOnCooldown(key, 0)) continue;
1949
1962
 
1950
1963
  const workType = item.type || 'implement';
1951
1964
  const isFanOut = item.scope === 'fan-out';
@@ -2029,11 +2042,13 @@ function discoverCentralWorkItems(config) {
2029
2042
  });
2030
2043
  }
2031
2044
 
2032
- item.status = WI_STATUS.DISPATCHED;
2033
- item.dispatched_at = ts();
2034
- item.dispatched_to = idleAgents.map(a => a.id).join(', ');
2035
- item.scope = 'fan-out';
2036
- item.fanOutAgents = idleAgents.map(a => a.id);
2045
+ mutations.set(item.id, {
2046
+ status: WI_STATUS.DISPATCHED,
2047
+ dispatched_at: ts(),
2048
+ dispatched_to: idleAgents.map(a => a.id).join(', '),
2049
+ scope: 'fan-out',
2050
+ fanOutAgents: idleAgents.map(a => a.id),
2051
+ });
2037
2052
  setCooldown(key);
2038
2053
  log('info', `Fan-out: ${item.id} dispatched to ${idleAgents.length} agents: ${idleAgents.map(a => a.name).join(', ')}`);
2039
2054
 
@@ -2085,11 +2100,10 @@ function discoverCentralWorkItems(config) {
2085
2100
  const cpCount = (item._checkpointCount || 0) + 1;
2086
2101
  if (cpCount > 3) {
2087
2102
  log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
2088
- item.status = WI_STATUS.NEEDS_REVIEW;
2089
- item._checkpointCount = cpCount;
2103
+ mutations.set(item.id, { status: WI_STATUS.NEEDS_REVIEW, _checkpointCount: cpCount });
2090
2104
  continue;
2091
2105
  }
2092
- item._checkpointCount = cpCount;
2106
+ mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, { _checkpointCount: cpCount }));
2093
2107
  const cpSummary = [
2094
2108
  `## Checkpoint (Resume #${cpCount}/3)`,
2095
2109
  '',
@@ -2117,7 +2131,7 @@ function discoverCentralWorkItems(config) {
2117
2131
  vars.notes_content = '';
2118
2132
  try { vars.notes_content = fs.readFileSync(path.join(MINIONS_DIR, 'notes.md'), 'utf8'); } catch { /* optional */ }
2119
2133
  // Track expected plan filename in meta for chainPlanToPrd
2120
- item._planFileName = planFileName;
2134
+ mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, { _planFileName: planFileName }));
2121
2135
  }
2122
2136
 
2123
2137
  // Inject plan-to-prd variables — read the plan file content for the playbook
@@ -2172,6 +2186,13 @@ function discoverCentralWorkItems(config) {
2172
2186
  continue;
2173
2187
  }
2174
2188
 
2189
+ const dispatchMutation = {
2190
+ status: WI_STATUS.DISPATCHED,
2191
+ dispatched_at: ts(),
2192
+ dispatched_to: agentId,
2193
+ };
2194
+ mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, dispatchMutation));
2195
+
2175
2196
  newWork.push({
2176
2197
  type: workType,
2177
2198
  agent: agentId,
@@ -2179,18 +2200,25 @@ function discoverCentralWorkItems(config) {
2179
2200
  agentRole,
2180
2201
  task: item.title || item.description?.slice(0, 80) || item.id,
2181
2202
  prompt,
2182
- meta: { dispatchKey: key, source: 'central-work-item', item, planFileName: item.planFile || item._planFileName || null, branch: item.branch || item.featureBranch || `work/${item.id}` }
2203
+ meta: { dispatchKey: key, source: 'central-work-item', item, planFileName: item.planFile || mutations.get(item.id)?._planFileName || null, branch: item.branch || item.featureBranch || `work/${item.id}` }
2183
2204
  });
2184
2205
 
2185
- item.status = WI_STATUS.DISPATCHED;
2186
- item.dispatched_at = ts();
2187
- item.dispatched_to = agentId;
2188
2206
  setCooldown(key);
2189
2207
  }
2190
2208
  } catch (err) { log('warn', `discoverCentralWorkItems: skipping ${item.id}: ${err.message}`); }
2191
2209
  }
2192
2210
 
2193
- if (newWork.length > 0) safeWrite(centralPath, items);
2211
+ if (mutations.size > 0) {
2212
+ // True atomic read-modify-write — applies mutations to fresh locked data
2213
+ mutateJsonFileLocked(centralPath, (freshItems) => {
2214
+ if (!Array.isArray(freshItems)) freshItems = [];
2215
+ for (const fi of freshItems) {
2216
+ const m = mutations.get(fi.id);
2217
+ if (m) Object.assign(fi, m);
2218
+ }
2219
+ return freshItems;
2220
+ }, { defaultValue: [] });
2221
+ }
2194
2222
  return newWork;
2195
2223
  }
2196
2224
 
@@ -2238,24 +2266,33 @@ function discoverWork(config) {
2238
2266
  if (scheduledWork.length > 0) {
2239
2267
  const { createMeeting, getMeetings } = require('./engine/meeting');
2240
2268
  const centralPath = path.join(MINIONS_DIR, 'work-items.json');
2241
- const items = safeJson(centralPath) || [];
2242
- let added = 0;
2269
+ // Separate meetings (no work-items write) from task items
2270
+ const taskItems = [];
2243
2271
  for (const item of scheduledWork) {
2244
2272
  if (item.type === WORK_TYPE.MEETING) {
2245
- // Create a real multi-agent meeting instead of a single-agent work item
2246
2273
  const sched = (config.schedules || []).find(s => s.id === item._scheduleId);
2247
2274
  const participants = (sched && sched.participants) || [];
2248
2275
  const meeting = createMeeting({ title: item.title, agenda: item.description, participants });
2249
2276
  log('info', `Scheduled meeting created: ${item._scheduleId} → ${meeting.id} (${participants.length} participants)`);
2250
2277
  } else {
2251
- if (!items.some(i => i._scheduleId === item._scheduleId && i.status !== WI_STATUS.DONE && i.status !== WI_STATUS.FAILED)) {
2252
- items.push(item);
2253
- added++;
2254
- log('info', `Scheduled task fired: ${item._scheduleId} → ${item.title}`);
2255
- }
2278
+ taskItems.push(item);
2256
2279
  }
2257
2280
  }
2258
- if (added > 0) safeWrite(centralPath, items);
2281
+ if (taskItems.length > 0) {
2282
+ // Atomic write — prevents race with dispatch status updates on central work-items.json
2283
+ mutateJsonFileLocked(centralPath, (items) => {
2284
+ if (!Array.isArray(items)) items = [];
2285
+ let added = 0;
2286
+ for (const item of taskItems) {
2287
+ if (!items.some(i => i._scheduleId === item._scheduleId && i.status !== WI_STATUS.DONE && i.status !== WI_STATUS.FAILED)) {
2288
+ items.push(item);
2289
+ added++;
2290
+ log('info', `Scheduled task fired: ${item._scheduleId} → ${item.title}`);
2291
+ }
2292
+ }
2293
+ return items;
2294
+ }, { defaultValue: [] });
2295
+ }
2259
2296
  }
2260
2297
  } catch (e) { log('warn', 'discover scheduled work: ' + e.message); }
2261
2298
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.533",
3
+ "version": "0.1.534",
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"