@yemi33/minions 0.1.644 → 0.1.645

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.644 (2026-04-09)
3
+ ## 0.1.645 (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
@@ -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,91 @@ 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, planSlug, plansDir) {
300
+ const files = safeReadDir(plansDir).filter(f => f.endsWith('.md'));
301
+ const slugPrefix = planSlug.slice(0, 30);
302
+ let slugMatch = null;
303
+ for (const file of files) {
304
+ // Fallback: title-slug filename match (checked first to avoid unnecessary reads)
305
+ if (!slugMatch && file.startsWith(slugPrefix)) slugMatch = file;
306
+ // Primary: explicit Source Meeting header (written by dashboard /api/plans/create)
307
+ const content = safeRead(path.join(plansDir, file));
308
+ if (!content) continue;
309
+ for (const mid of meetingIds) {
310
+ if (content.includes('**Source Meeting:** ' + mid)) return file;
311
+ }
312
+ }
313
+ return slugMatch;
314
+ }
315
+
287
316
  async function executePlanStage(stage, stageState, run, config) {
288
- // Create a plan .md file from the stage config + previous stage output
289
317
  const plansDir = path.join(__dirname, '..', 'plans');
290
318
  if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
291
319
 
292
320
  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));
321
+ const wiPath = path.join(__dirname, '..', 'work-items.json');
322
+ const wiId = `PL-${run.runId.slice(4, 12)}-${stage.id}-prd`;
295
323
 
296
- // Build meeting context from dependency stages
324
+ // ── Reconciliation: check if a plan already exists for a meeting in this run ──
325
+ const meetingIds = _findMeetingsInRun(run);
326
+ if (meetingIds.length > 0) {
327
+ const existingPlanFile = _findExistingPlanForMeeting(meetingIds, slug, plansDir);
328
+ if (existingPlanFile) {
329
+ log('info', `Pipeline ${run.pipelineId}: reconciling plan stage — adopting existing plan "${existingPlanFile}"`);
330
+ // Adopt or create plan-to-prd WI atomically under lock
331
+ let adoptedWiId = wiId;
332
+ mutateWorkItems(wiPath, workItems => {
333
+ const existing = workItems.find(w => w.type === WORK_TYPE.PLAN_TO_PRD && w.planFile === existingPlanFile);
334
+ if (existing) {
335
+ existing._pipelineRun = run.runId;
336
+ existing._pipelineStage = stage.id;
337
+ adoptedWiId = existing.id;
338
+ } else if (!workItems.some(w => w.id === wiId)) {
339
+ workItems.push({
340
+ id: wiId, title: `Convert plan to PRD: ${existingPlanFile}`,
341
+ type: WORK_TYPE.PLAN_TO_PRD, priority: 'high', status: WI_STATUS.PENDING,
342
+ planFile: existingPlanFile, created: ts(), createdBy: 'pipeline:' + run.pipelineId,
343
+ branch: `pipeline/${run.pipelineId}/${stage.id}`, _pipelineRun: run.runId, _pipelineStage: stage.id,
344
+ });
345
+ }
346
+ });
347
+ return {
348
+ status: PIPELINE_STATUS.RUNNING,
349
+ artifacts: { plans: [existingPlanFile], workItems: [adoptedWiId], prds: [], prs: [] },
350
+ };
351
+ }
352
+ }
353
+
354
+ // ── No existing plan — build meeting context from ALL run stages (not just direct deps) ──
297
355
  let meetingContext = '';
356
+ for (const mid of meetingIds) {
357
+ try {
358
+ const mtg = safeJson(path.join(__dirname, '..', 'meetings', mid + '.json'));
359
+ if (mtg) {
360
+ const transcript = (mtg.transcript || []).map(t =>
361
+ '### ' + (t.agent || 'agent') + ' (' + (t.type || '') + ', Round ' + (t.round || '?') + ')\n\n' + (t.content || '')
362
+ ).join('\n\n---\n\n');
363
+ meetingContext += '# Meeting: ' + (mtg.title || mid) + '\n\n**Agenda:** ' + (mtg.agenda || '') + '\n\n' + transcript + '\n\n';
364
+ }
365
+ } catch (e) { log('warn', `Pipeline plan: failed to read meeting ${mid}: ${e.message}`); }
366
+ }
367
+ // Also include direct dep output (for non-meeting stages)
298
368
  if (stage.dependsOn) {
299
369
  for (const depId of stage.dependsOn) {
300
370
  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) {
371
+ if (depStage?.output && !depStage.artifacts?.meetings?.length) {
320
372
  meetingContext += '## From: ' + depId + '\n\n' + depStage.output + '\n\n';
321
373
  }
322
374
  }
@@ -350,11 +402,11 @@ async function executePlanStage(stage, stageState, run, config) {
350
402
  if (stage.description) content += stage.description + '\n';
351
403
  }
352
404
 
405
+ const filename = `${slug}-${dateStamp()}.md`;
406
+ const filePath = shared.uniquePath(path.join(plansDir, filename));
353
407
  safeWrite(filePath, content);
354
408
 
355
409
  // 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
410
  mutateWorkItems(wiPath, workItems => {
359
411
  if (!workItems.some(w => w.id === wiId)) {
360
412
  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.645",
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"