@yemi33/minions 0.1.644 → 0.1.646

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,11 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.644 (2026-04-09)
3
+ ## 0.1.646 (2026-04-09)
4
4
 
5
5
  ### Features
6
+ - pipeline plan stage reconciliation and UX fixes
6
7
  - track notes, plans, and PRDs as work item artifacts
7
8
 
8
9
  ### Fixes
10
+ - pipeline plan reconciliation slug match used wrong naming convention
9
11
  - use startsWith for inbox note matching to prevent false matches
10
12
 
11
13
  ## 0.1.642 (2026-04-09)
@@ -380,12 +380,22 @@ function _viewPlanWithBack(file, meetingId) {
380
380
  }
381
381
 
382
382
  function _findLinkedPlan(meeting) {
383
- if (!meeting?.conclusion?.content) return null;
384
- const match = meeting.conclusion.content.match(/plans\/([\w-]+\.md)/);
385
- if (!match) return null;
386
- const file = match[1];
387
- const plans = window._lastStatus?.plans || [];
388
- return plans.find(function(p) { return p.file === file; }) || { file, summary: file };
383
+ var plans = window._lastStatus?.plans || [];
384
+ // Check 1: regex in conclusion text (agent may reference plan path)
385
+ if (meeting?.conclusion?.content) {
386
+ var match = meeting.conclusion.content.match(/plans\/([\w-]+\.md)/);
387
+ if (match) {
388
+ var file = match[1];
389
+ return plans.find(function(p) { return p.file === file; }) || { file, summary: file };
390
+ }
391
+ }
392
+ // Check 2: title-slug filename match (dashboard naming convention: "meeting-follow-up-{title}")
393
+ if (meeting?.title) {
394
+ var titleSlug = ('Meeting follow-up: ' + meeting.title).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50);
395
+ var found = plans.find(function(p) { return p.file && p.file.startsWith(titleSlug); });
396
+ if (found) return found;
397
+ }
398
+ return null;
389
399
  }
390
400
 
391
401
  async function _createPlanFromMeeting(id, btn) {
@@ -435,7 +445,7 @@ async function _createPlanFromMeeting(id, btn) {
435
445
  const title = 'Meeting follow-up: ' + (m.title || id);
436
446
  const planRes = await fetch('/api/plans/create', {
437
447
  method: 'POST', headers: { 'Content-Type': 'application/json' },
438
- body: JSON.stringify({ title, content: planContent })
448
+ body: JSON.stringify({ title, content: planContent, meetingId: id })
439
449
  });
440
450
  const planData = await planRes.json();
441
451
  if (planRes.ok && planData.ok) {
@@ -39,7 +39,7 @@
39
39
  .timestamp { color: var(--muted); font-size: var(--text-md); font-variant-numeric: tabular-nums; }
40
40
 
41
41
  .layout { display: none; } /* Replaced by page-layout */
42
- section { padding: var(--space-8) var(--space-9); border-bottom: 1px solid var(--border); overflow: hidden; min-width: 0; }
42
+ section { padding: var(--space-8) var(--space-9); border-bottom: 1px solid var(--border); overflow: visible; min-width: 0; }
43
43
 
44
44
  /* Sidebar navigation */
45
45
  .page-layout { display: flex; flex: 1; min-height: 0; overflow: hidden; }
@@ -284,39 +284,98 @@ function executeMeetingStage(stage, stageState, run, config) {
284
284
  return { status: PIPELINE_STATUS.RUNNING, artifacts: { meetings: createdIds } };
285
285
  }
286
286
 
287
+ // Find meeting artifacts from any stage in the run (not just direct deps)
288
+ function _findMeetingsInRun(run) {
289
+ const meetings = [];
290
+ for (const [, stageState] of Object.entries(run.stages || {})) {
291
+ for (const mid of (stageState.artifacts?.meetings || [])) {
292
+ if (!meetings.includes(mid)) meetings.push(mid);
293
+ }
294
+ }
295
+ return meetings;
296
+ }
297
+
298
+ // Check if a plan already exists for a given meeting (created manually via dashboard)
299
+ function _findExistingPlanForMeeting(meetingIds, plansDir) {
300
+ const files = safeReadDir(plansDir).filter(f => f.endsWith('.md'));
301
+ // Build slug prefixes for both pipeline and dashboard naming conventions
302
+ const slugPrefixes = [];
303
+ for (const mid of meetingIds) {
304
+ const mtg = safeJson(path.join(__dirname, '..', 'meetings', mid + '.json'));
305
+ if (mtg?.title) {
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);
308
+ slugPrefixes.push(dashSlug.slice(0, 30));
309
+ }
310
+ }
311
+ let slugMatch = null;
312
+ for (const file of files) {
313
+ if (!slugMatch && slugPrefixes.some(p => file.startsWith(p))) slugMatch = file;
314
+ const content = safeRead(path.join(plansDir, file));
315
+ if (!content) continue;
316
+ for (const mid of meetingIds) {
317
+ if (content.includes('**Source Meeting:** ' + mid)) return file;
318
+ }
319
+ }
320
+ return slugMatch;
321
+ }
322
+
287
323
  async function executePlanStage(stage, stageState, run, config) {
288
- // Create a plan .md file from the stage config + previous stage output
289
324
  const plansDir = path.join(__dirname, '..', 'plans');
290
325
  if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
291
326
 
292
327
  const slug = (stage.title || 'pipeline-plan').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50);
293
- const filename = `${slug}-${dateStamp()}.md`;
294
- const filePath = shared.uniquePath(path.join(plansDir, filename));
328
+ const wiPath = path.join(__dirname, '..', 'work-items.json');
329
+ const wiId = `PL-${run.runId.slice(4, 12)}-${stage.id}-prd`;
295
330
 
296
- // Build meeting context from dependency stages
331
+ // ── Reconciliation: check if a plan already exists for a meeting in this run ──
332
+ const meetingIds = _findMeetingsInRun(run);
333
+ if (meetingIds.length > 0) {
334
+ const existingPlanFile = _findExistingPlanForMeeting(meetingIds, plansDir);
335
+ if (existingPlanFile) {
336
+ log('info', `Pipeline ${run.pipelineId}: reconciling plan stage — adopting existing plan "${existingPlanFile}"`);
337
+ // Adopt or create plan-to-prd WI atomically under lock
338
+ let adoptedWiId = wiId;
339
+ mutateWorkItems(wiPath, workItems => {
340
+ const existing = workItems.find(w => w.type === WORK_TYPE.PLAN_TO_PRD && w.planFile === existingPlanFile);
341
+ if (existing) {
342
+ existing._pipelineRun = run.runId;
343
+ existing._pipelineStage = stage.id;
344
+ adoptedWiId = existing.id;
345
+ } else if (!workItems.some(w => w.id === wiId)) {
346
+ workItems.push({
347
+ id: wiId, title: `Convert plan to PRD: ${existingPlanFile}`,
348
+ type: WORK_TYPE.PLAN_TO_PRD, priority: 'high', status: WI_STATUS.PENDING,
349
+ planFile: existingPlanFile, created: ts(), createdBy: 'pipeline:' + run.pipelineId,
350
+ branch: `pipeline/${run.pipelineId}/${stage.id}`, _pipelineRun: run.runId, _pipelineStage: stage.id,
351
+ });
352
+ }
353
+ });
354
+ return {
355
+ status: PIPELINE_STATUS.RUNNING,
356
+ artifacts: { plans: [existingPlanFile], workItems: [adoptedWiId], prds: [], prs: [] },
357
+ };
358
+ }
359
+ }
360
+
361
+ // ── No existing plan — build meeting context from ALL run stages (not just direct deps) ──
297
362
  let meetingContext = '';
363
+ for (const mid of meetingIds) {
364
+ try {
365
+ const mtg = safeJson(path.join(__dirname, '..', 'meetings', mid + '.json'));
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');
370
+ meetingContext += '# Meeting: ' + (mtg.title || mid) + '\n\n**Agenda:** ' + (mtg.agenda || '') + '\n\n' + transcript + '\n\n';
371
+ }
372
+ } catch (e) { log('warn', `Pipeline plan: failed to read meeting ${mid}: ${e.message}`); }
373
+ }
374
+ // Also include direct dep output (for non-meeting stages)
298
375
  if (stage.dependsOn) {
299
376
  for (const depId of stage.dependsOn) {
300
377
  const depStage = run.stages[depId];
301
- if (!depStage) continue;
302
-
303
- const meetingId = depStage.artifacts?.meetings?.[0];
304
- if (meetingId) {
305
- try {
306
- const mtgPath = path.join(__dirname, '..', 'meetings', meetingId + '.json');
307
- const mtg = safeJson(mtgPath);
308
- if (mtg) {
309
- // Build full meeting document (same as dashboard "Create Plan from Meeting")
310
- const transcript = (mtg.transcript || []).map(t =>
311
- '### ' + (t.agent || 'agent') + ' (' + (t.type || '') + ', Round ' + (t.round || '?') + ')\n\n' + (t.content || '')
312
- ).join('\n\n---\n\n');
313
- meetingContext += '# Meeting: ' + (mtg.title || depId) + '\n\n**Agenda:** ' + (mtg.agenda || '') + '\n\n' + transcript + '\n\n';
314
- continue;
315
- }
316
- } catch { /* fall through */ }
317
- }
318
-
319
- if (depStage.output) {
378
+ if (depStage?.output && !depStage.artifacts?.meetings?.length) {
320
379
  meetingContext += '## From: ' + depId + '\n\n' + depStage.output + '\n\n';
321
380
  }
322
381
  }
@@ -350,11 +409,11 @@ async function executePlanStage(stage, stageState, run, config) {
350
409
  if (stage.description) content += stage.description + '\n';
351
410
  }
352
411
 
412
+ const filename = `${slug}-${dateStamp()}.md`;
413
+ const filePath = shared.uniquePath(path.join(plansDir, filename));
353
414
  safeWrite(filePath, content);
354
415
 
355
416
  // Create plan-to-prd work item — atomic write to prevent race with dispatch status updates
356
- const wiPath = path.join(__dirname, '..', 'work-items.json');
357
- const wiId = `PL-${run.runId.slice(4, 12)}-${stage.id}-prd`;
358
417
  mutateWorkItems(wiPath, workItems => {
359
418
  if (!workItems.some(w => w.id === wiId)) {
360
419
  workItems.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.644",
3
+ "version": "0.1.646",
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"