@yemi33/minions 0.1.652 → 0.1.654

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.654 (2026-04-09)
4
+
5
+ ### Fixes
6
+ - kill process tree (not just parent) before worktree removal
7
+
8
+ ### Other
9
+ - refactor: extract slugify/formatTranscriptEntry, fix pipeline completion
10
+
3
11
  ## 0.1.652 (2026-04-09)
4
12
 
5
13
  ### Fixes
package/dashboard.js CHANGED
@@ -1266,7 +1266,7 @@ const server = http.createServer(async (req, res) => {
1266
1266
  fs.mkdirSync(inboxDir, { recursive: true });
1267
1267
  const today = new Date().toISOString().slice(0, 10);
1268
1268
  const author = body.author || os.userInfo().username;
1269
- const slug = (body.title || 'note').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40);
1269
+ const slug = shared.slugify(body.title || 'note', 40);
1270
1270
  const filename = `${author}-${slug}-${today}-${shared.uid().slice(-4)}.md`;
1271
1271
  const content = `# ${body.title}\n\n**By:** ${author}\n**Date:** ${today}\n\n${body.what}\n${body.why ? '\n**Why:** ' + body.why + '\n' : ''}`;
1272
1272
  safeWrite(shared.uniquePath(path.join(inboxDir, filename)), content);
@@ -3362,7 +3362,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3362
3362
 
3363
3363
  // Auto-generate ID from title if not provided
3364
3364
  if (!id) {
3365
- id = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40);
3365
+ id = shared.slugify(title, 40);
3366
3366
  if (!id) id = 'schedule';
3367
3367
  }
3368
3368
 
@@ -3888,7 +3888,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3888
3888
 
3889
3889
  const plansDir = path.join(MINIONS_DIR, 'plans');
3890
3890
  if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
3891
- const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50);
3891
+ const slug = shared.slugify(title);
3892
3892
  const date = new Date().toISOString().slice(0, 10);
3893
3893
  const filename = `${slug}-${date}.md`;
3894
3894
  const filePath = shared.uniquePath(path.join(plansDir, filename));
@@ -3954,7 +3954,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3954
3954
  if (!category || !title || !content) return jsonReply(res, 400, { error: 'category, title, and content required' });
3955
3955
  const validCategories = ['architecture', 'conventions', 'project-notes', 'build-reports', 'reviews'];
3956
3956
  if (!validCategories.includes(category)) return jsonReply(res, 400, { error: 'Invalid category. Must be: ' + validCategories.join(', ') });
3957
- const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60);
3957
+ const slug = shared.slugify(title, 60);
3958
3958
  const filePath = path.join(MINIONS_DIR, 'knowledge', category, slug + '.md');
3959
3959
  const dir = path.dirname(filePath);
3960
3960
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
package/engine/cleanup.js CHANGED
@@ -74,14 +74,14 @@ function _killProcessInWorktree(dir, activeProcesses, activeIds) {
74
74
  const taskInfo = exec(`tasklist /FI "PID eq ${pid}" /NH`, { encoding: 'utf8', timeout: 3000, windowsHide: true });
75
75
  const taskLower = taskInfo.toLowerCase();
76
76
  if (!taskLower.includes('node') && !taskLower.includes('claude')) continue;
77
- exec(`taskkill /F /PID ${pid}`, { stdio: 'pipe', timeout: 5000, windowsHide: true });
77
+ exec(`taskkill /F /T /PID ${pid}`, { stdio: 'pipe', timeout: 5000, windowsHide: true });
78
78
  } else {
79
79
  // Verify it's a node process before killing (prevent recycled PID kill)
80
80
  try {
81
81
  const psOut = exec(`ps -p ${pid} -o comm=`, { encoding: 'utf8', timeout: 3000 }).trim();
82
82
  if (!psOut.includes('node') && !psOut.includes('claude')) continue;
83
83
  } catch { continue; } // process dead or ps failed
84
- process.kill(pid, 'SIGKILL');
84
+ try { process.kill(-pid, 'SIGKILL'); } catch { process.kill(pid, 'SIGKILL'); }
85
85
  }
86
86
  log('info', `Killed orphaned PID ${pid} (${f}) before worktree removal`);
87
87
  } catch {} // process may already be dead
@@ -140,7 +140,7 @@ function consolidateWithLLM(items, existingNotes, files, config) {
140
140
  const agentMatch = item.name.match(/^(\w+)-/);
141
141
  const agent = agentMatch ? agentMatch[1] : 'unknown';
142
142
  const titleMatch = (item.content || '').match(/^#\s+(.+)/m);
143
- const titleSlug = titleMatch ? titleMatch[1].toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50) : item.name.replace(/\.md$/, '');
143
+ const titleSlug = titleMatch ? shared.slugify(titleMatch[1]) : item.name.replace(/\.md$/, '');
144
144
  return { file: item.name, category: cat, kbPath: path.join('knowledge', cat, `${dateStamp()}-${agent}-${titleSlug}.md`) };
145
145
  });
146
146
 
@@ -415,7 +415,7 @@ function classifyToKnowledgeBase(items) {
415
415
  const agent = agentMatch ? agentMatch[1] : 'unknown';
416
416
  const titleMatch = content.match(/^#\s+(.+)/m);
417
417
  const titleSlug = titleMatch
418
- ? titleMatch[1].toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50)
418
+ ? shared.slugify(titleMatch[1])
419
419
  : item.name.replace(/\.md$/, '');
420
420
  const kbFilename = `${dateStamp()}-${agent}-${titleSlug}.md`;
421
421
  const kbPath = shared.uniquePath(path.join(categoryDirs[category], kbFilename));
@@ -7,7 +7,7 @@
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
9
  const shared = require('./shared');
10
- const { safeJson, safeWrite, safeRead, safeReadDir, uid, log, ts, dateStamp, mutateJsonFileLocked, mutateWorkItems, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, ENGINE_DEFAULTS } = shared;
10
+ const { safeJson, safeWrite, safeRead, safeReadDir, uid, log, ts, dateStamp, mutateJsonFileLocked, mutateWorkItems, slugify, formatTranscriptEntry, WI_STATUS, WORK_TYPE, PLAN_STATUS, PR_STATUS, PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, ENGINE_DEFAULTS } = shared;
11
11
  const http = require('http');
12
12
  const { parseCronExpr, shouldRunNow } = require('./scheduler');
13
13
 
@@ -304,7 +304,7 @@ function _findExistingPlanForMeeting(meetingIds, plansDir) {
304
304
  const mtg = safeJson(path.join(__dirname, '..', 'meetings', mid + '.json'));
305
305
  if (mtg?.title) {
306
306
  // Dashboard convention: "Meeting follow-up: {title}" → slug
307
- const dashSlug = ('meeting-follow-up-' + mtg.title).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50);
307
+ const dashSlug = slugify('meeting-follow-up-' + mtg.title);
308
308
  slugPrefixes.push(dashSlug);
309
309
  }
310
310
  }
@@ -324,7 +324,7 @@ async function executePlanStage(stage, stageState, run, config) {
324
324
  const plansDir = path.join(__dirname, '..', 'plans');
325
325
  if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
326
326
 
327
- const slug = (stage.title || 'pipeline-plan').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50);
327
+ const slug = slugify(stage.title || 'pipeline-plan');
328
328
  const wiPath = path.join(__dirname, '..', 'work-items.json');
329
329
  const wiId = `PL-${run.runId.slice(4, 12)}-${stage.id}-prd`;
330
330
 
@@ -364,9 +364,7 @@ async function executePlanStage(stage, stageState, run, config) {
364
364
  try {
365
365
  const mtg = safeJson(path.join(__dirname, '..', 'meetings', mid + '.json'));
366
366
  if (mtg) {
367
- const transcript = (mtg.transcript || []).map(t =>
368
- '### ' + (t.agent || 'agent') + ' (' + (t.type || '') + ', Round ' + (t.round || '?') + ')\n\n' + (t.content || '')
369
- ).join('\n\n---\n\n');
367
+ const transcript = (mtg.transcript || []).map(formatTranscriptEntry).join('\n\n---\n\n');
370
368
  meetingContext += '# Meeting: ' + (mtg.title || mid) + '\n\n**Agenda:** ' + (mtg.agenda || '') + '\n\n' + transcript + '\n\n';
371
369
  }
372
370
  } catch (e) { log('warn', `Pipeline plan: failed to read meeting ${mid}: ${e.message}`); }
@@ -600,29 +598,32 @@ function isStageComplete(stage, stageState, run, config) {
600
598
  });
601
599
  if (!prdDone) return false;
602
600
 
603
- // Discover PRDs and their work items
601
+ // Discover PRDs and their work items — collect into local arrays, then merge into artifacts
604
602
  const prdDir = path.join(__dirname, '..', 'prd');
605
603
  const plans = artifacts.plans || [];
604
+ const discoveredPrds = [];
605
+ const discoveredWiIds = [];
606
606
  for (const planFile of plans) {
607
607
  const prdFiles = fs.existsSync(prdDir) ? safeReadDir(prdDir).filter(f => f.endsWith('.json')) : [];
608
608
  for (const pf of prdFiles) {
609
609
  const prd = safeJson(path.join(prdDir, pf));
610
- if (prd?.source_plan === planFile && !(artifacts.prds || []).includes(pf)) {
611
- artifacts.prds = artifacts.prds || [];
612
- artifacts.prds.push(pf);
610
+ if (prd?.source_plan === planFile && !(artifacts.prds || []).includes(pf) && !discoveredPrds.includes(pf)) {
611
+ discoveredPrds.push(pf);
613
612
  }
614
613
  }
615
- // Find materialized work items for discovered PRDs
616
- for (const prdFile of (artifacts.prds || [])) {
614
+ const allPrds = [...(artifacts.prds || []), ...discoveredPrds];
615
+ for (const prdFile of allPrds) {
617
616
  const prdItems = all.filter(w => w.sourcePlan === prdFile && w.type !== WORK_TYPE.PLAN_TO_PRD);
618
617
  for (const wi of prdItems) {
619
- if (!(artifacts.workItems || []).includes(wi.id)) {
620
- artifacts.workItems = artifacts.workItems || [];
621
- artifacts.workItems.push(wi.id);
618
+ if (!(artifacts.workItems || []).includes(wi.id) && !discoveredWiIds.includes(wi.id)) {
619
+ discoveredWiIds.push(wi.id);
622
620
  }
623
621
  }
624
622
  }
625
623
  }
624
+ // Merge discovered artifacts (caller persists via updateRunStage)
625
+ if (discoveredPrds.length > 0) { artifacts.prds = [...(artifacts.prds || []), ...discoveredPrds]; }
626
+ if (discoveredWiIds.length > 0) { artifacts.workItems = [...(artifacts.workItems || []), ...discoveredWiIds]; }
626
627
 
627
628
  // Auto-approve if configured
628
629
  if (stage.autoApprove && artifacts.prds?.length > 0) {
@@ -792,7 +793,18 @@ async function discoverPipelineWork(config) {
792
793
  const result = await executeStage(stage, activeRun, pipeline, config);
793
794
  updateRunStage(pipeline.id, activeRun.runId, stage.id, { ...result, startedAt: ts() });
794
795
  Object.assign(stageState, result, { startedAt: ts() });
795
- if (result.status === PIPELINE_STATUS.RUNNING) anyRunning = true;
796
+ if (result.status === PIPELINE_STATUS.RUNNING) {
797
+ // Check if stage is already complete (e.g. reconciled plan with done WI)
798
+ if (isStageComplete(stage, stageState, activeRun, config)) {
799
+ updateRunStage(pipeline.id, activeRun.runId, stage.id, {
800
+ status: PIPELINE_STATUS.COMPLETED, completedAt: ts(), artifacts: stageState.artifacts,
801
+ });
802
+ stageState.status = PIPELINE_STATUS.COMPLETED;
803
+ log('info', `Pipeline ${pipeline.id}: stage ${stage.id} completed immediately after start`);
804
+ } else {
805
+ anyRunning = true;
806
+ }
807
+ }
796
808
  // Condition stage signaled pipeline stop — complete the run immediately
797
809
  if (result._stopPipeline) {
798
810
  completeRun(pipeline.id, activeRun.runId, PIPELINE_STATUS.STOPPED);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.652",
3
+ "version": "0.1.654",
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"