@yemi33/minions 0.1.912 → 0.1.914

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.912 (2026-04-13)
3
+ ## 0.1.914 (2026-04-13)
4
4
 
5
5
  ### Features
6
+ - fix dep re-merge failure on retry with existing commits (#977)
6
7
  - audit and harden log buffering implementation (#971)
7
8
  - replace magic string 'active' with PR_STATUS.ACTIVE in lifecycle.js (#969)
8
9
  - fix dashboard plan-pause nested lock violation (#968)
@@ -16,6 +17,8 @@
16
17
  - add red dot notification on CC tab when response completes (#934) (#946)
17
18
 
18
19
  ### Fixes
20
+ - prevent Create Plan from meeting saving doc-chat context bleed (#980)
21
+ - plan completion incorrectly overrides awaiting-approval status (#970) (#978)
19
22
  - CC queued messages sent to wrong tab after tab switch
20
23
  - pass sysPromptPath instead of steerPromptPath as system prompt in steering resume (#962)
21
24
  - PRD item display dispatched/in-progress status conflation (closes #950) (#955)
@@ -438,12 +438,19 @@ async function _createPlanFromMeeting(id, btn) {
438
438
  message: 'Create an actionable implementation plan from this meeting. Extract concrete action items from the conclusion and debates. For each item include: what to do, which files/areas to change, priority (high/medium/low), and estimated complexity (small/medium/large). Structure it as a plan ready for execution. Do NOT include preamble — start with the plan title.' + humanContext,
439
439
  document: meetingDoc,
440
440
  title: 'Meeting: ' + m.title,
441
+ freshSession: true,
441
442
  })
442
443
  });
443
444
  const genData = await genRes.json();
444
445
  if (!genRes.ok || !genData.ok) { resetBtn(); showToast('cmd-toast', 'Failed to generate plan: ' + (genData.error || 'unknown'), false); return; }
445
446
 
446
447
  const planContent = genData.answer || '';
448
+ // Guard: reject doc-chat meta-responses that aren't plan content
449
+ if (!planContent.trim() || !/^(#|\*\*|[-*] )/.test(planContent.trim())) {
450
+ resetBtn();
451
+ showToast('cmd-toast', 'Generated content does not look like a plan — try again', false);
452
+ return;
453
+ }
447
454
  const title = 'Meeting follow-up: ' + (m.title || id);
448
455
  const planRes = await fetch('/api/plans/create', {
449
456
  method: 'POST', headers: { 'Content-Type': 'application/json' },
@@ -81,14 +81,14 @@ function derivePlanStatus(prdFile, mdFile, prdJsonStatus, workItems) {
81
81
  if (prdJsonStatus === 'rejected') return 'rejected';
82
82
  if (prdJsonStatus === 'paused' && !allDone) return 'paused';
83
83
  if (prdJsonStatus === 'revision-requested') return 'revision-requested';
84
+ // PRD awaiting approval overrides WI-derived completion — new items may need materialization (#970)
85
+ if (prdJsonStatus === 'awaiting-approval') return 'awaiting-approval';
84
86
 
85
87
  // Derive from work item progress
86
88
  if (allDone && !hasActiveWork) return 'completed';
87
89
  if (hasActiveWork) return 'dispatched';
88
90
  if (hasPendingPrd) return 'converting';
89
91
  if (hasFailed && !hasActiveWork) return 'has-failures';
90
-
91
- if (prdJsonStatus === 'awaiting-approval' && implementWi.length === 0) return 'awaiting-approval';
92
92
  if (prdJsonStatus === 'approved' && implementWi.length === 0) return 'approved';
93
93
 
94
94
  // plan-to-prd conversion done but no implement items and PRD is gone — truly completed
package/dashboard.js CHANGED
@@ -783,13 +783,21 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
783
783
  }
784
784
 
785
785
  // Doc-specific wrapper — adds document context, parses ---DOCUMENT---
786
- async function ccDocCall({ message, document, title, filePath, selection, canEdit, isJson, model }) {
786
+ async function ccDocCall({ message, document, title, filePath, selection, canEdit, isJson, model, freshSession }) {
787
787
  const sessionKey = filePath || title;
788
788
  const docSlice = document.slice(0, 20000);
789
789
 
790
+ // freshSession: true → discard any prior session for this key so the call starts clean.
791
+ // Used by one-shot generation flows (e.g. Create Plan from meeting) that must not
792
+ // bleed context from earlier conversations.
793
+ if (freshSession && sessionKey) {
794
+ docSessions.delete(sessionKey);
795
+ // Skip persistDocSessions() here — the post-call cleanup below handles persistence.
796
+ }
797
+
790
798
  // Skip re-sending full document on session resume if content unchanged
791
799
  const docHash = require('crypto').createHash('md5').update(docSlice).digest('hex').slice(0, 8);
792
- const existing = resolveSession('doc', sessionKey);
800
+ const existing = freshSession ? null : resolveSession('doc', sessionKey);
793
801
  const docUnchanged = existing?.sessionId && existing._docHash === docHash;
794
802
 
795
803
  let docContext;
@@ -808,8 +816,14 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
808
816
  skipStatePreamble: true,
809
817
  ...(model ? { model } : {}),
810
818
  });
811
- // Store doc hash for next call's unchanged check
812
- if (result.code === 0 && result.sessionId) {
819
+
820
+ if (freshSession && sessionKey) {
821
+ // One-shot call — discard the session ccCall just stored so it cannot
822
+ // bleed into future interactions under the same key.
823
+ docSessions.delete(sessionKey);
824
+ persistDocSessions();
825
+ } else if (result.code === 0 && result.sessionId) {
826
+ // Store doc hash for next call's unchanged check
813
827
  const session = resolveSession('doc', sessionKey);
814
828
  if (session) session._docHash = docHash;
815
829
  }
@@ -2843,6 +2857,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2843
2857
  message: body.message, document: currentContent, title: body.title,
2844
2858
  filePath: body.filePath, selection: body.selection, canEdit, isJson,
2845
2859
  model: body.model || undefined,
2860
+ freshSession: !!body.freshSession,
2846
2861
  });
2847
2862
 
2848
2863
  if (!content) return jsonReply(res, 200, { ok: true, answer, edited: false, actions });
@@ -4105,6 +4120,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4105
4120
  const { title, content, project: projectName, meetingId } = body;
4106
4121
  if (!title || !content) return jsonReply(res, 400, { error: 'title and content required' });
4107
4122
 
4123
+ // Reject meta-responses that aren't actual plan content — e.g. doc-chat
4124
+ // summaries of prior conversations that reference existing plan files.
4125
+ const trimmed = content.trim();
4126
+ if (!/^#/.test(trimmed) && !/^\*\*/.test(trimmed) && !/^[-*] /.test(trimmed)) {
4127
+ return jsonReply(res, 400, { error: 'Plan content must start with a markdown heading (#), bold text (**), or a list item' });
4128
+ }
4129
+
4108
4130
  const plansDir = path.join(MINIONS_DIR, 'plans');
4109
4131
  if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
4110
4132
  const slug = shared.slugify(title);
package/engine.js CHANGED
@@ -524,7 +524,37 @@ async function spawnAgent(dispatchItem, config) {
524
524
  }
525
525
  // Merge successfully-fetched + recovered (local-only pushed) branches sequentially
526
526
  const fetched = fetchable.filter((_, i) => fetchResults[i].status === 'fulfilled' || recoveredBranches.has(fetchable[i].branch));
527
- if (!depMergeFailed) {
527
+ // Skip dep re-merge if worktree HEAD already contains all dep commits (#973)
528
+ let skipDepMerge = false;
529
+ if (!depMergeFailed && fetched.length > 0) {
530
+ const ancestorChecks = await Promise.all(
531
+ fetched.map(async ({ branch: depBranch }) => {
532
+ try {
533
+ await execAsync(`git merge-base --is-ancestor "origin/${depBranch}" HEAD`, { ..._gitOpts, cwd: worktreePath });
534
+ return true;
535
+ } catch (_) { return false; }
536
+ })
537
+ );
538
+ if (ancestorChecks.every(Boolean)) {
539
+ log('info', `All ${fetched.length} dep branch(es) already merged into ${branchName} — skipping dep re-merge`);
540
+ skipDepMerge = true;
541
+ }
542
+ }
543
+ // Stash uncommitted changes before dep merge if worktree is dirty (#973)
544
+ let stashed = false;
545
+ if (!depMergeFailed && !skipDepMerge && fetched.length > 0) {
546
+ try {
547
+ const statusOut = (await execAsync('git status --porcelain', { ..._gitOpts, cwd: worktreePath })).stdout.toString().trim();
548
+ if (statusOut) {
549
+ await execAsync('git stash push --include-untracked -m "engine: stash before dep re-merge"', { ..._gitOpts, cwd: worktreePath });
550
+ stashed = true;
551
+ log('info', `Stashed uncommitted changes in ${branchName} before dep merge`);
552
+ }
553
+ } catch (stashErr) {
554
+ log('warn', `Failed to stash changes in ${branchName} before dep merge: ${stashErr.message}`);
555
+ }
556
+ }
557
+ if (!depMergeFailed && !skipDepMerge) {
528
558
  for (const { branch: depBranch, prId } of fetched) {
529
559
  try {
530
560
  await execAsync(`git merge "origin/${depBranch}" --no-edit`, { ..._gitOpts, cwd: worktreePath });
@@ -573,6 +603,15 @@ async function spawnAgent(dispatchItem, config) {
573
603
  }
574
604
  }
575
605
  }
606
+ // Restore stashed changes after dep merge (#973)
607
+ if (stashed) {
608
+ try {
609
+ await execAsync('git stash pop', { ..._gitOpts, cwd: worktreePath });
610
+ log('info', `Restored stashed changes in ${branchName} after dep merge`);
611
+ } catch (popErr) {
612
+ log('warn', `git stash pop failed in ${branchName}: ${popErr.message} — stash preserved for agent`);
613
+ }
614
+ }
576
615
  if (depMergeFailed) {
577
616
  _cleanupPromptFiles();
578
617
  // Build actionable failReason identifying the conflicting branch and files
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.912",
3
+ "version": "0.1.914",
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"