@yemi33/minions 0.1.299 → 0.1.301

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,14 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.299 (2026-04-03)
3
+ ## 0.1.301 (2026-04-03)
4
4
 
5
5
  ### Fixes
6
+ - PR dedup strict equality, work-items parse logging, undefined minionsVerdict
7
+
8
+ ## 0.1.300 (2026-04-03)
9
+
10
+ ### Fixes
11
+ - re-apply verify workflow — defer archiving until verify completes
6
12
  - work item agent column falls back to item.agent field
7
13
  - CC workType descriptions restored + verify for maintenance/merge tasks
8
14
 
@@ -5,8 +5,9 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
+ const os = require('os');
8
9
  const shared = require('./shared');
9
- const { safeRead, safeJson, safeWrite, execSilent, projectPrPath, getPrLinks, addPrLink,
10
+ const { safeRead, safeJson, safeWrite, mutateJsonFileLocked, execSilent, projectPrPath, getPrLinks, addPrLink,
10
11
  log, ts, dateStamp } = shared;
11
12
  const { trackEngineUsage } = require('./llm');
12
13
  const queries = require('./queries');
@@ -21,7 +22,10 @@ function checkPlanCompletion(meta, config) {
21
22
  const planPath = path.join(PRD_DIR, planFile);
22
23
  const plan = safeJson(planPath);
23
24
  if (!plan?.missing_features) return;
24
- if (plan.status === 'completed') return;
25
+ if (plan.status === 'completed') {
26
+ if (plan._completionNotified) return;
27
+ // Crash recovery: status=completed but _completionNotified not set — fall through
28
+ }
25
29
 
26
30
  const projects = shared.getProjects(config);
27
31
 
@@ -132,10 +136,20 @@ function checkPlanCompletion(meta, config) {
132
136
  ...uniquePrs.map(pr => `- ${pr.id}: ${pr.title || ''} ${pr.url || ''}`),
133
137
  ].filter(Boolean).join('\n');
134
138
 
135
- // Write summary to notes/inbox
136
- const summaryFile = `prd-completion-${planFile.replace('.json', '')}-${ts().slice(0, 10)}.md`;
137
- shared.safeWrite(shared.uniquePath(path.join(MINIONS_DIR, 'notes', 'inbox', summaryFile)), summary);
138
- log('info', `PRD completion summary written to notes/inbox/${summaryFile}`);
139
+ // Write summary to notes/inbox (slug-based dedup prevents duplicates on same day)
140
+ const slug = `prd-completion-${planFile.replace('.json', '')}`;
141
+ const wrote = shared.writeToInbox('engine', slug, summary);
142
+ if (wrote) log('info', `PRD completion summary written to notes/inbox/`);
143
+
144
+ // Persist status and _completionNotified atomically BEFORE creating work items
145
+ plan._completionNotified = true;
146
+ mutateJsonFileLocked(planPath, (data) => {
147
+ data.status = 'completed';
148
+ data.completedAt = plan.completedAt;
149
+ data._completionNotified = true;
150
+ if (plan._timing) data._timing = plan._timing;
151
+ return data;
152
+ });
139
153
 
140
154
  // Resolve the primary project for writing new work items (PR, verify)
141
155
  const projectName = plan.project;
@@ -186,11 +200,12 @@ function checkPlanCompletion(meta, config) {
186
200
 
187
201
  // Build per-project checkout commands: one worktree, merge all PR branches into it
188
202
  const checkoutBlocks = Object.entries(projectPrs).map(([name, { project: p, prs, mainBranch }]) => {
189
- const wtPath = `${p.localPath}/../worktrees/verify-${name}-${planSlug}-${shared.uid()}`;
203
+ const localPath = p.localPath.replace(/\\/g, '/');
204
+ const wtPath = `${localPath}/../worktrees/verify-${name}-${planSlug}-${shared.uid()}`;
190
205
  const branches = prs.map(pr => pr.branch).filter(Boolean);
191
206
  const lines = [
192
207
  `# ${name} — merge ${branches.length} PR branch(es) into one worktree`,
193
- `cd "${p.localPath}"`,
208
+ `cd "${localPath}"`,
194
209
  `git fetch origin ${branches.map(b => `"${b}"`).join(' ')} "${mainBranch}"`,
195
210
  `git worktree add "${wtPath}" "origin/${mainBranch}" 2>/dev/null || (cd "${wtPath}" && git checkout "${mainBranch}" && git pull origin "${mainBranch}")`,
196
211
  `cd "${wtPath}"`,
@@ -211,9 +226,10 @@ function checkPlanCompletion(meta, config) {
211
226
  ).join('\n');
212
227
 
213
228
  // List projects and their worktree paths for the agent
214
- const projectWorktrees = Object.entries(projectPrs).map(([name, { project: p }]) =>
215
- `- **${name}**: \`${p.localPath}/../worktrees/verify-${planSlug}\``
216
- ).join('\n');
229
+ const projectWorktrees = Object.entries(projectPrs).map(([name, { project: p }]) => {
230
+ const lp = p.localPath.replace(/\\/g, '/');
231
+ return `- **${name}**: see setup commands below (\`${lp}/../worktrees/verify-${name}-${planSlug}-*\`)`;
232
+ }).join('\n');
217
233
 
218
234
  const description = [
219
235
  `Verification task for completed plan \`${planFile}\`.`,
@@ -258,46 +274,75 @@ function checkPlanCompletion(meta, config) {
258
274
  log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
259
275
  }
260
276
 
261
- // 5. Archive: move PRD .json to prd/archive/ and source .md plan to plans/archive/
277
+ // 5. Archive deferred until verify completes (see runPostCompletionHooks).
278
+ // Plan stays active until verification finishes so artifacts are visible.
279
+
280
+ log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
281
+ }
282
+
283
+ // ─── Plan Archiving (called after verify completes) ─────────────────────────
284
+
285
+ function archivePlan(planFile, plan, projects, config) {
286
+ const planPath = path.join(PRD_DIR, planFile);
287
+ const projectName = plan.project || '';
288
+
289
+ // Archive PRD .json to prd/archive/
262
290
  const prdArchiveDir = path.join(PRD_DIR, 'archive');
263
291
  if (!fs.existsSync(prdArchiveDir)) fs.mkdirSync(prdArchiveDir, { recursive: true });
264
- shared.safeWrite(planPath, plan); // save completed status first
265
- try {
266
- fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
267
- log('info', `Archived completed PRD: prd/archive/${planFile}`);
268
- } catch (err) {
269
- log('warn', `Failed to archive PRD ${planFile}: ${err.message}`);
292
+ if (fs.existsSync(planPath)) {
270
293
  shared.safeWrite(planPath, plan);
294
+ try {
295
+ fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
296
+ log('info', `Archived completed PRD: prd/archive/${planFile}`);
297
+ } catch (err) { log('warn', `Failed to archive PRD ${planFile}: ${err.message}`); }
271
298
  }
272
299
 
273
- // Also archive the source .md plan if it exists
300
+ // Archive the source .md plan
274
301
  const planArchiveDir = path.join(PLANS_DIR, 'archive');
275
302
  if (!fs.existsSync(planArchiveDir)) fs.mkdirSync(planArchiveDir, { recursive: true });
276
- try {
277
- const mdFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
278
- for (const md of mdFiles) {
279
- const mdContent = shared.safeRead(path.join(PLANS_DIR, md)) || '';
280
- // Match by project name or plan summary appearing in the .md content
281
- if (mdContent.includes(projectName) || mdContent.includes(plan.plan_summary?.slice(0, 40) || '___nomatch___')) {
282
- try {
283
- fs.renameSync(path.join(PLANS_DIR, md), path.join(planArchiveDir, md));
284
- log('info', `Archived source plan: plans/archive/${md}`);
285
- } catch (err) { log('warn', `Failed to archive plan ${md}: ${err.message}`); }
286
- break;
287
- }
303
+ if (plan.source_plan) {
304
+ const mdPath = path.join(PLANS_DIR, plan.source_plan);
305
+ if (fs.existsSync(mdPath)) {
306
+ try {
307
+ fs.renameSync(mdPath, path.join(planArchiveDir, plan.source_plan));
308
+ log('info', `Archived source plan: plans/archive/${plan.source_plan}`);
309
+ } catch (err) { log('warn', `Failed to archive source plan ${plan.source_plan}: ${err.message}`); }
288
310
  }
289
- } catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
311
+ } else {
312
+ try {
313
+ const mdFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
314
+ for (const md of mdFiles) {
315
+ const mdContent = shared.safeRead(path.join(PLANS_DIR, md)) || '';
316
+ if (mdContent.includes(projectName) || mdContent.includes(plan.plan_summary?.slice(0, 40) || '___nomatch___')) {
317
+ try {
318
+ fs.renameSync(path.join(PLANS_DIR, md), path.join(planArchiveDir, md));
319
+ log('info', `Archived source plan: plans/archive/${md}`);
320
+ } catch (err) { log('warn', `Failed to archive plan ${md}: ${err.message}`); }
321
+ break;
322
+ }
323
+ }
324
+ } catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
325
+ }
290
326
 
291
- // 6. Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
327
+ // Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
292
328
  try {
293
- // Collect all branch slugs: shared-branch + per-item branches + item IDs
329
+ let allWi = [];
330
+ for (const p of projects) {
331
+ try { allWi = allWi.concat(safeJson(shared.projectWorkItemsPath(p)) || []); } catch {}
332
+ }
333
+ const planWi = allWi.filter(w => w.sourcePlan === planFile && w.itemType !== 'verify');
334
+ const allPrs = [];
335
+ for (const p of projects) {
336
+ try { allPrs.push(...(safeJson(shared.projectPrPath(p)) || [])); } catch {}
337
+ }
338
+
294
339
  const branchSlugs = new Set();
295
340
  if (plan.feature_branch) branchSlugs.add(shared.sanitizeBranch(plan.feature_branch).toLowerCase());
296
- for (const w of doneItems) {
341
+ for (const w of planWi) {
297
342
  if (w.branch) branchSlugs.add(shared.sanitizeBranch(w.branch).toLowerCase());
298
343
  if (w.id) branchSlugs.add(w.id.toLowerCase());
299
344
  }
300
- for (const pr of uniquePrs) {
345
+ for (const pr of allPrs.filter(pr => (pr.prdItems || []).some(id => planWi.find(w => w.id === id)))) {
301
346
  if (pr.branch) branchSlugs.add(shared.sanitizeBranch(pr.branch).toLowerCase());
302
347
  }
303
348
 
@@ -319,10 +364,8 @@ function checkPlanCompletion(meta, config) {
319
364
  }
320
365
  }
321
366
  }
322
- if (cleanedWt > 0) log('info', `Plan completion: cleaned ${cleanedWt} worktree(s)`);
367
+ if (cleanedWt > 0) log('info', `Plan archive: cleaned ${cleanedWt} worktree(s)`);
323
368
  } catch (err) { log('warn', `Worktree cleanup: ${err.message}`); }
324
-
325
- log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
326
369
  }
327
370
 
328
371
  // ─── Plan → PRD Chaining ─────────────────────────────────────────────────────
@@ -393,7 +436,10 @@ function chainPlanToPrd(dispatchItem, meta, config) {
393
436
  log('info', `Plan chaining: queuing plan-to-prd for next tick (chained from ${dispatchItem.id})`);
394
437
  const wiPath = path.join(MINIONS_DIR, 'work-items.json');
395
438
  let items = [];
396
- try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch {}
439
+ try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch (err) {
440
+ log('warn', `Failed to parse ${wiPath}: ${err.message} — creating .bak and starting fresh`);
441
+ try { fs.copyFileSync(wiPath, wiPath + '.bak'); } catch {}
442
+ }
397
443
  items.push({
398
444
  id: 'W-' + shared.uid(),
399
445
  title: `Convert plan to PRD: ${meta?.item?.title || planFile.name}`,
@@ -580,7 +626,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
580
626
  dirtyTargets.set(targetName, { prs: safeJson(prPath) || [], prPath });
581
627
  }
582
628
  const entry = dirtyTargets.get(targetName);
583
- if (entry.prs.some(p => p.id === fullId || String(p.id).includes(prId))) continue;
629
+ if (entry.prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
584
630
 
585
631
  let title = meta?.item?.title || '';
586
632
  const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
@@ -651,7 +697,7 @@ function updatePrAfterReview(agentId, pr, project) {
651
697
  }
652
698
 
653
699
  shared.safeWrite(project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json'), prs);
654
- log('info', `Updated ${pr.id} → minions review: ${minionsVerdict} by ${reviewerName}`);
700
+ log('info', `Updated ${pr.id} → minions review: ${target.reviewStatus} by ${reviewerName}`);
655
701
  createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
656
702
  }
657
703
 
@@ -839,7 +885,7 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
839
885
  }
840
886
  } else {
841
887
  // Write in Claude Code native format: ~/.claude/skills/<name>/SKILL.md
842
- const claudeSkillsDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude', 'skills');
888
+ const claudeSkillsDir = path.join(os.homedir(), '.claude', 'skills');
843
889
  const skillDir = path.join(claudeSkillsDir, name.replace(/[^a-z0-9-]/g, '-'));
844
890
  const skillPath = path.join(skillDir, 'SKILL.md');
845
891
  if (!fs.existsSync(skillPath)) {
@@ -1126,6 +1172,19 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1126
1172
  let prsCreatedCount = 0;
1127
1173
  if (isSuccess) prsCreatedCount = syncPrsFromOutput(stdout, agentId, meta, config) || 0;
1128
1174
 
1175
+ // Archive plan after verify task completes (AFTER PR sync so E2E PR is linked)
1176
+ if (meta?.item?.itemType === 'verify' && meta?.item?.sourcePlan) {
1177
+ try {
1178
+ const vPlanFile = meta.item.sourcePlan;
1179
+ const vPlanPath = path.join(PRD_DIR, vPlanFile);
1180
+ const vPlan = safeJson(vPlanPath);
1181
+ if (vPlan) {
1182
+ const vProjects = shared.getProjects(config);
1183
+ archivePlan(vPlanFile, vPlan, vProjects, config);
1184
+ }
1185
+ } catch (err) { log('warn', `Verify archive: ${err.message}`); }
1186
+ }
1187
+
1129
1188
  // Clean up worktree for non-shared-branch tasks after completion
1130
1189
  if (meta?.branch && meta?.branchStrategy !== 'shared-branch') {
1131
1190
  try {
@@ -1252,6 +1311,7 @@ function syncPrdFromPrs(config) {
1252
1311
 
1253
1312
  module.exports = {
1254
1313
  checkPlanCompletion,
1314
+ archivePlan,
1255
1315
  updateWorkItemStatus,
1256
1316
  syncPrdItemStatus,
1257
1317
  syncPrsFromOutput,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.299",
3
+ "version": "0.1.301",
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"