@yemi33/minions 0.1.1680 → 0.1.1682
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 +16 -0
- package/dashboard/js/render-skills.js +18 -3
- package/dashboard.js +98 -53
- package/docs/plan-lifecycle.md +23 -7
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +105 -165
- package/engine/meeting.js +36 -13
- package/engine/playbook.js +36 -12
- package/engine/queries.js +254 -67
- package/engine/runtimes/claude.js +52 -0
- package/engine/runtimes/copilot.js +69 -0
- package/engine/shared.js +28 -0
- package/engine/spawn-agent.js +10 -7
- package/engine.js +50 -31
- package/package.json +1 -1
package/engine/lifecycle.js
CHANGED
|
@@ -69,6 +69,12 @@ function checkPlanCompletion(meta, config) {
|
|
|
69
69
|
|
|
70
70
|
const doneItems = planItems.filter(w => DONE_STATUSES.has(w.status));
|
|
71
71
|
const failedItems = planItems.filter(w => w.status === WI_STATUS.FAILED || w.status === WI_STATUS.CANCELLED);
|
|
72
|
+
const isActiveVerify = (wi) => wi && (
|
|
73
|
+
wi.status === WI_STATUS.PENDING ||
|
|
74
|
+
wi.status === WI_STATUS.QUEUED ||
|
|
75
|
+
wi.status === WI_STATUS.DISPATCHED
|
|
76
|
+
);
|
|
77
|
+
const isReopenableVerify = (wi) => wi && (DONE_STATUSES.has(wi.status) || wi.status === WI_STATUS.FAILED);
|
|
72
78
|
|
|
73
79
|
if (failedItems.length > 0) {
|
|
74
80
|
const failDetails = failedItems.map(w =>
|
|
@@ -166,6 +172,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
166
172
|
const mainBranch = shared.resolveMainBranch(primaryProject.localPath, primaryProject.mainBranch);
|
|
167
173
|
const itemSummary = doneItems.map(w => '- ' + w.id + ': ' + w.title.replace('Implement: ', '')).join('\n');
|
|
168
174
|
mutateWorkItems(wiPath, workItems => {
|
|
175
|
+
if (workItems.some(w => w.sourcePlan === planFile && w.itemType === 'pr')) return workItems;
|
|
169
176
|
workItems.push({
|
|
170
177
|
id, title: `Create PR for plan: ${plan.plan_summary || planFile}`,
|
|
171
178
|
type: 'implement', priority: 'high',
|
|
@@ -181,9 +188,23 @@ function checkPlanCompletion(meta, config) {
|
|
|
181
188
|
// 4. Create verification work item (build, test, start webapp, write testing guide)
|
|
182
189
|
// Only one verify per PRD — skip if pending/dispatched, re-open if done/failed (PRD was modified)
|
|
183
190
|
const existingVerify = allWorkItems.find(w => w.sourcePlan === planFile && w.itemType === 'verify');
|
|
184
|
-
if (
|
|
191
|
+
if (isActiveVerify(existingVerify)) {
|
|
185
192
|
log('info', `Plan ${planFile}: verify WI ${existingVerify.id} already ${existingVerify.status} — skipping`);
|
|
186
|
-
} else if (doneItems.length > 0) {
|
|
193
|
+
} else if (isReopenableVerify(existingVerify) && doneItems.length > 0) {
|
|
194
|
+
const verifyProject = existingVerify.project || projectName;
|
|
195
|
+
const vWiPath = shared.projectWorkItemsPath(
|
|
196
|
+
projects.find(p => p.name?.toLowerCase() === verifyProject?.toLowerCase()) || primaryProject
|
|
197
|
+
);
|
|
198
|
+
let reopenedVerify = false;
|
|
199
|
+
mutateWorkItems(vWiPath, items => {
|
|
200
|
+
const v = items.find(w => w.id === existingVerify.id);
|
|
201
|
+
if (isReopenableVerify(v)) {
|
|
202
|
+
shared.reopenWorkItem(v);
|
|
203
|
+
reopenedVerify = true;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
if (reopenedVerify) log('info', `Re-opened verification work item ${existingVerify.id} for modified plan ${planFile}`);
|
|
207
|
+
} else if (!existingVerify && doneItems.length > 0) {
|
|
187
208
|
const verifyId = 'PL-' + shared.uid();
|
|
188
209
|
const planSlug = planFile.replace('.json', '');
|
|
189
210
|
|
|
@@ -276,7 +297,17 @@ function checkPlanCompletion(meta, config) {
|
|
|
276
297
|
prSummary,
|
|
277
298
|
].join('\n');
|
|
278
299
|
|
|
300
|
+
let createdVerify = false;
|
|
301
|
+
let reopenedVerifyId = null;
|
|
279
302
|
mutateWorkItems(wiPath, workItems => {
|
|
303
|
+
const v = workItems.find(w => w.sourcePlan === planFile && w.itemType === 'verify');
|
|
304
|
+
if (v) {
|
|
305
|
+
if (isReopenableVerify(v)) {
|
|
306
|
+
shared.reopenWorkItem(v);
|
|
307
|
+
reopenedVerifyId = v.id;
|
|
308
|
+
}
|
|
309
|
+
return workItems;
|
|
310
|
+
}
|
|
280
311
|
workItems.push({
|
|
281
312
|
id: verifyId,
|
|
282
313
|
title: `Verify plan: ${(plan.plan_summary || planFile).slice(0, 80)}`,
|
|
@@ -290,32 +321,19 @@ function checkPlanCompletion(meta, config) {
|
|
|
290
321
|
itemType: 'verify',
|
|
291
322
|
project: projectName,
|
|
292
323
|
});
|
|
324
|
+
createdVerify = true;
|
|
293
325
|
});
|
|
294
|
-
|
|
326
|
+
if (createdVerify) {
|
|
327
|
+
log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
|
|
295
328
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const vWiPath = shared.projectWorkItemsPath(
|
|
305
|
-
projects.find(p => p.name?.toLowerCase() === verifyProject?.toLowerCase()) || primaryProject
|
|
306
|
-
);
|
|
307
|
-
mutateWorkItems(vWiPath, items => {
|
|
308
|
-
const v = items.find(w => w.id === existingVerify.id);
|
|
309
|
-
if (v && DONE_STATUSES.has(v.status)) {
|
|
310
|
-
v.status = WI_STATUS.PENDING;
|
|
311
|
-
v._reopened = true;
|
|
312
|
-
delete v.completedAt;
|
|
313
|
-
delete v.dispatched_to;
|
|
314
|
-
delete v.dispatched_at;
|
|
315
|
-
v._retryCount = 0;
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
log('info', `Re-opened verification work item ${existingVerify.id} for modified plan ${planFile}`);
|
|
329
|
+
// Teams notification for verify creation — non-blocking
|
|
330
|
+
try {
|
|
331
|
+
const teams = require('./teams');
|
|
332
|
+
teams.teamsNotifyPlanEvent({ name: plan.plan_summary || planFile, file: planFile }, 'verify-created').catch(() => {});
|
|
333
|
+
} catch {}
|
|
334
|
+
} else if (reopenedVerifyId) {
|
|
335
|
+
log('info', `Re-opened verification work item ${reopenedVerifyId} for modified plan ${planFile}`);
|
|
336
|
+
}
|
|
319
337
|
}
|
|
320
338
|
|
|
321
339
|
// Archive deferred until verify completes
|
|
@@ -350,9 +368,9 @@ function archivePlan(planFile, plan, projects, config) {
|
|
|
350
368
|
// plan completion and spawning duplicate verify tasks for already-archived plans.
|
|
351
369
|
// On Windows, the unlink can fail due to file locking; overwrite with archived status
|
|
352
370
|
// as a fallback so a restored backup is inert even if deletion fails.
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
371
|
+
const backupCleanup = shared.neutralizeJsonBackupSidecar(planPath);
|
|
372
|
+
if (!backupCleanup.ok) {
|
|
373
|
+
log('warn', `Archive backup cleanup failed for ${planFile}: unlink failed (${backupCleanup.unlinkError}); fallback neutralize failed (${backupCleanup.writeError})`);
|
|
356
374
|
}
|
|
357
375
|
} catch (err) {
|
|
358
376
|
log('warn', `Failed to archive PRD ${planFile}: ${err.message}`);
|
|
@@ -440,95 +458,6 @@ function cleanupPlanWorktrees(planFile, plan, projects, config) {
|
|
|
440
458
|
} catch (err) { log('warn', `Plan worktree cleanup: ${err.message}`); }
|
|
441
459
|
}
|
|
442
460
|
|
|
443
|
-
// ─── Plan → PRD Chaining ─────────────────────────────────────────────────────
|
|
444
|
-
function chainPlanToPrd(dispatchItem, meta, config) {
|
|
445
|
-
|
|
446
|
-
const planDir = path.join(MINIONS_DIR, 'plans');
|
|
447
|
-
if (!fs.existsSync(planDir)) fs.mkdirSync(planDir, { recursive: true });
|
|
448
|
-
|
|
449
|
-
let planFileName = meta?.planFileName || meta?.item?._planFileName;
|
|
450
|
-
if (planFileName && fs.existsSync(path.join(planDir, planFileName))) {
|
|
451
|
-
// Exact match from meta
|
|
452
|
-
} else {
|
|
453
|
-
const planFiles = fs.readdirSync(planDir)
|
|
454
|
-
.filter(f => f.endsWith('.md') || f.endsWith('.json'))
|
|
455
|
-
.map(f => ({ name: f, mtime: fs.statSync(path.join(planDir, f)).mtimeMs }))
|
|
456
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
457
|
-
planFileName = planFiles[0]?.name;
|
|
458
|
-
if (!planFileName) {
|
|
459
|
-
log('warn', `Plan chaining: no plan files found in plans/ after task ${dispatchItem.id}`);
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
log('info', `Plan chaining: using mtime fallback — found ${planFileName}`);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if (planFileName.endsWith('.json')) {
|
|
466
|
-
const mdName = planFileName.replace(/\.json$/, '.md');
|
|
467
|
-
// Check plans/ first, then prd/ for .json files
|
|
468
|
-
const jsonPath = fs.existsSync(path.join(planDir, planFileName))
|
|
469
|
-
? path.join(planDir, planFileName)
|
|
470
|
-
: path.join(MINIONS_DIR, 'prd', planFileName);
|
|
471
|
-
const mdPath = path.join(planDir, mdName);
|
|
472
|
-
try {
|
|
473
|
-
const content = fs.readFileSync(jsonPath, 'utf8');
|
|
474
|
-
const parsed = JSON.parse(content);
|
|
475
|
-
if (!parsed.missing_features) {
|
|
476
|
-
fs.renameSync(jsonPath, mdPath);
|
|
477
|
-
planFileName = mdName;
|
|
478
|
-
log('info', `Plan chaining: renamed ${planFileName} → ${mdName} (plans must be .md)`);
|
|
479
|
-
}
|
|
480
|
-
} catch {
|
|
481
|
-
try {
|
|
482
|
-
if (fs.existsSync(jsonPath)) fs.renameSync(jsonPath, path.join(planDir, mdName));
|
|
483
|
-
planFileName = mdName;
|
|
484
|
-
log('info', `Plan chaining: renamed to .md (not valid JSON)`);
|
|
485
|
-
} catch (err) { log('warn', `Plan rename fallback: ${err.message}`); }
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const planFile = { name: planFileName };
|
|
490
|
-
const planPath = path.join(planDir, planFileName);
|
|
491
|
-
let planContent;
|
|
492
|
-
try { planContent = fs.readFileSync(planPath, 'utf8'); } catch (err) {
|
|
493
|
-
log('error', `Plan chaining: failed to read plan file ${planFile.name}: ${err.message}`);
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const projectName = meta?.item?.project || meta?.project?.name;
|
|
498
|
-
const projects = shared.getProjects(config);
|
|
499
|
-
if (projects.length === 0) {
|
|
500
|
-
log('error', 'Plan chaining: no projects configured');
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
const targetProject = projectName
|
|
504
|
-
? projects.find(p => p.name === projectName) || projects[0]
|
|
505
|
-
: projects[0];
|
|
506
|
-
|
|
507
|
-
if (!targetProject) {
|
|
508
|
-
log('error', 'Plan chaining: no target project available');
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
log('info', `Plan chaining: queuing plan-to-prd for next tick (chained from ${dispatchItem.id})`);
|
|
513
|
-
const wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
514
|
-
shared.mutateJsonFileLocked(wiPath, (items) => {
|
|
515
|
-
if (!Array.isArray(items)) items = [];
|
|
516
|
-
items.push({
|
|
517
|
-
id: 'W-' + shared.uid(),
|
|
518
|
-
title: `Convert plan to PRD: ${meta?.item?.title || planFile.name}`,
|
|
519
|
-
type: 'plan-to-prd',
|
|
520
|
-
priority: meta?.item?.priority || 'high',
|
|
521
|
-
description: `Plan file: plans/${planFile.name}\nChained from plan task ${dispatchItem.id}`,
|
|
522
|
-
status: WI_STATUS.PENDING,
|
|
523
|
-
created: ts(),
|
|
524
|
-
createdBy: 'engine:chain',
|
|
525
|
-
project: targetProject.name,
|
|
526
|
-
planFile: planFile.name,
|
|
527
|
-
});
|
|
528
|
-
return items;
|
|
529
|
-
}, { defaultValue: [] });
|
|
530
|
-
}
|
|
531
|
-
|
|
532
461
|
// ─── Work Item Path Resolution ───────────────────────────────────────────────
|
|
533
462
|
|
|
534
463
|
/** Resolve the work-items.json path from dispatch meta. Reused by retry paths. */
|
|
@@ -642,15 +571,14 @@ function syncPrdItemStatus(itemId, status, sourcePlan) {
|
|
|
642
571
|
const feature = plan?.missing_features?.find(f => f.id === itemId);
|
|
643
572
|
if (!feature || feature.status === status) continue;
|
|
644
573
|
let updated = false;
|
|
645
|
-
|
|
646
|
-
const fresh = safeJson(fpath);
|
|
574
|
+
mutateJsonFileLocked(fpath, (fresh) => {
|
|
647
575
|
const f = fresh?.missing_features?.find(x => x.id === itemId);
|
|
648
576
|
if (f && f.status !== status) {
|
|
649
577
|
f.status = status;
|
|
650
|
-
safeWrite(fpath, fresh);
|
|
651
578
|
updated = true;
|
|
652
579
|
}
|
|
653
|
-
|
|
580
|
+
return fresh;
|
|
581
|
+
}, { skipWriteIfUnchanged: true });
|
|
654
582
|
if (updated) return;
|
|
655
583
|
}
|
|
656
584
|
} catch (err) { log('warn', `PRD status sync: ${err.message}`); }
|
|
@@ -682,31 +610,28 @@ function reconcilePrdStatuses(config) {
|
|
|
682
610
|
for (const file of prdFiles) {
|
|
683
611
|
try {
|
|
684
612
|
const fpath = path.join(PRD_DIR, file);
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
let modified = false;
|
|
691
|
-
for (const feature of plan.missing_features) {
|
|
692
|
-
if (feature.status === PRD_ITEM_STATUS.MISSING && doneWiById.has(feature.id)) {
|
|
693
|
-
feature.status = PRD_ITEM_STATUS.UPDATED;
|
|
694
|
-
modified = true;
|
|
695
|
-
log('info', `PRD backward-scan: promoted ${feature.id} from missing→updated in ${file} (done work item exists)`);
|
|
696
|
-
}
|
|
697
|
-
// (#984) Stale status: PRD item stuck at dispatched/failed/pending while WI is done —
|
|
698
|
-
// happens when fix work items complete with a different ID than the original PRD feature
|
|
699
|
-
else if (_STALE_PRD_STATUSES.has(feature.status) && doneWiById.has(feature.id)) {
|
|
700
|
-
const prev = feature.status;
|
|
701
|
-
feature.status = WI_STATUS.DONE;
|
|
702
|
-
modified = true;
|
|
703
|
-
log('info', `PRD backward-scan: promoted ${feature.id} from ${prev}→done in ${file} (done work item exists)`);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
613
|
+
const logMessages = [];
|
|
614
|
+
mutateJsonFileLocked(fpath, (plan) => {
|
|
615
|
+
if (!plan?.missing_features) return plan;
|
|
616
|
+
// Skip completed/archived PRDs — no reconciliation needed
|
|
617
|
+
if (plan.status === PLAN_STATUS.COMPLETED) return plan;
|
|
706
618
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
619
|
+
for (const feature of plan.missing_features) {
|
|
620
|
+
if (feature.status === PRD_ITEM_STATUS.MISSING && doneWiById.has(feature.id)) {
|
|
621
|
+
feature.status = PRD_ITEM_STATUS.UPDATED;
|
|
622
|
+
logMessages.push(`PRD backward-scan: promoted ${feature.id} from missing→updated in ${file} (done work item exists)`);
|
|
623
|
+
}
|
|
624
|
+
// (#984) Stale status: PRD item stuck at dispatched/failed/pending while WI is done —
|
|
625
|
+
// happens when fix work items complete with a different ID than the original PRD feature
|
|
626
|
+
else if (_STALE_PRD_STATUSES.has(feature.status) && doneWiById.has(feature.id)) {
|
|
627
|
+
const prev = feature.status;
|
|
628
|
+
feature.status = WI_STATUS.DONE;
|
|
629
|
+
logMessages.push(`PRD backward-scan: promoted ${feature.id} from ${prev}→done in ${file} (done work item exists)`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return plan;
|
|
633
|
+
}, { skipWriteIfUnchanged: true });
|
|
634
|
+
for (const message of logMessages) log('info', message);
|
|
710
635
|
} catch (err) { log('warn', `PRD backward-scan for ${file}: ${err.message}`); }
|
|
711
636
|
}
|
|
712
637
|
}
|
|
@@ -1608,19 +1533,17 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
1608
1533
|
const planFiles = fs.readdirSync(prdDir).filter(f => f.endsWith('.json'));
|
|
1609
1534
|
let updated = 0;
|
|
1610
1535
|
for (const pf of planFiles) {
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1536
|
+
const planPath = path.join(prdDir, pf);
|
|
1537
|
+
mutateJsonFileLocked(planPath, (plan) => {
|
|
1538
|
+
if (!plan?.missing_features) return plan;
|
|
1539
|
+
for (const feature of plan.missing_features) {
|
|
1540
|
+
if (mergedItemSet.has(feature.id) && feature.status !== WI_STATUS.DONE) {
|
|
1541
|
+
feature.status = WI_STATUS.DONE;
|
|
1542
|
+
updated++;
|
|
1543
|
+
}
|
|
1619
1544
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
shared.safeWrite(path.join(prdDir, pf), plan);
|
|
1623
|
-
}
|
|
1545
|
+
return plan;
|
|
1546
|
+
}, { skipWriteIfUnchanged: true });
|
|
1624
1547
|
}
|
|
1625
1548
|
if (updated > 0) log('info', `Post-merge: marked ${mergedItemIds.join(', ')} as done for ${pr.id}`);
|
|
1626
1549
|
} catch (err) { log('warn', `Post-merge PRD update: ${err.message}`); }
|
|
@@ -1703,9 +1626,23 @@ function checkForLearnings(agentId, agentInfo, taskDesc) {
|
|
|
1703
1626
|
log('warn', `${agentInfo?.name || agentId} didn't write learnings — no follow-up queued`);
|
|
1704
1627
|
}
|
|
1705
1628
|
|
|
1706
|
-
function
|
|
1629
|
+
function skillWriteTargets(runtimeName, project = null) {
|
|
1630
|
+
try {
|
|
1631
|
+
const runtime = resolveRuntime(runtimeName || 'claude');
|
|
1632
|
+
if (typeof runtime.getSkillWriteTargets === 'function') {
|
|
1633
|
+
return runtime.getSkillWriteTargets({ homeDir: os.homedir(), project });
|
|
1634
|
+
}
|
|
1635
|
+
} catch { /* fall through to Claude-compatible legacy target */ }
|
|
1636
|
+
return {
|
|
1637
|
+
personal: path.join(os.homedir(), '.claude', 'skills'),
|
|
1638
|
+
project: project?.localPath ? path.resolve(project.localPath, '.claude', 'skills') : null,
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
function extractSkillsFromOutput(output, agentId, dispatchItem, config, runtimeName = null) {
|
|
1707
1643
|
|
|
1708
1644
|
if (!output) return;
|
|
1645
|
+
const effectiveRuntime = runtimeName || dispatchItem?.meta?.runtimeName || dispatchItem?.runtimeName || 'claude';
|
|
1709
1646
|
let fullText = '';
|
|
1710
1647
|
for (const line of output.split('\n')) {
|
|
1711
1648
|
try {
|
|
@@ -1743,6 +1680,9 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
|
|
|
1743
1680
|
if (scope === 'project' && project) {
|
|
1744
1681
|
const proj = shared.getProjects(config).find(p => p.name === project);
|
|
1745
1682
|
if (proj) {
|
|
1683
|
+
const projectSkillRoot = skillWriteTargets(effectiveRuntime, proj).project
|
|
1684
|
+
|| path.resolve(proj.localPath, '.claude', 'skills');
|
|
1685
|
+
const projectSkillPath = path.join(projectSkillRoot, skillDirName, 'SKILL.md');
|
|
1746
1686
|
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
1747
1687
|
let skillId = null;
|
|
1748
1688
|
mutateJsonFileLocked(centralPath, data => {
|
|
@@ -1750,7 +1690,7 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
|
|
|
1750
1690
|
if (data.some(i => i.title === `Add skill: ${name}` && i.status !== WI_STATUS.FAILED)) return data;
|
|
1751
1691
|
skillId = `SK${String(data.filter(i => i.id?.startsWith('SK')).length + 1).padStart(3, '0')}`;
|
|
1752
1692
|
data.push({ id: skillId, type: 'implement', title: `Add skill: ${name}`,
|
|
1753
|
-
description: `Create project-level skill \`${skillDirName}/SKILL.md\` in ${project}.\n\nWrite this file to \`${
|
|
1693
|
+
description: `Create project-level skill \`${skillDirName}/SKILL.md\` in ${project}.\n\nWrite this file to \`${projectSkillPath}\` via a PR.\n\n## Skill Content\n\n\`\`\`\n${enrichedBlock}\n\`\`\``,
|
|
1754
1694
|
priority: 'low', status: WI_STATUS.QUEUED, created: ts(), createdBy: `engine:skill-extraction:${agentName}` });
|
|
1755
1695
|
return data;
|
|
1756
1696
|
}, { skipWriteIfUnchanged: true });
|
|
@@ -1759,18 +1699,18 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
|
|
|
1759
1699
|
}
|
|
1760
1700
|
}
|
|
1761
1701
|
} else {
|
|
1762
|
-
|
|
1763
|
-
const
|
|
1764
|
-
const skillDir = path.join(claudeSkillsDir, name.replace(/[^a-z0-9-]/g, '-'));
|
|
1702
|
+
const personalSkillRoot = skillWriteTargets(effectiveRuntime).personal;
|
|
1703
|
+
const skillDir = path.join(personalSkillRoot, name.replace(/[^a-z0-9-]/g, '-'));
|
|
1765
1704
|
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
1766
1705
|
if (!fs.existsSync(skillPath)) {
|
|
1767
|
-
//
|
|
1706
|
+
// Native skill format: only name + description in frontmatter.
|
|
1768
1707
|
const description = m('description') || m('trigger') || `Auto-extracted skill from ${agentName}`;
|
|
1769
1708
|
const body = fmMatch[2] || '';
|
|
1770
1709
|
const ccContent = `---\nname: ${name}\ndescription: ${description}\n---\n\n${body.trim()}\n`;
|
|
1771
1710
|
if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
|
|
1772
1711
|
shared.safeWrite(skillPath, ccContent);
|
|
1773
|
-
|
|
1712
|
+
try { require('./queries').invalidateSkillsCache(); } catch {}
|
|
1713
|
+
log('info', `Extracted skill "${name}" from ${agentName} → ${skillPath}`);
|
|
1774
1714
|
} else {
|
|
1775
1715
|
log('info', `Skill "${name}" already exists, skipping`);
|
|
1776
1716
|
}
|
package/engine/meeting.js
CHANGED
|
@@ -45,7 +45,7 @@ function getStructuredNoteArtifacts(structuredCompletion) {
|
|
|
45
45
|
|
|
46
46
|
function isPathInside(parent, child) {
|
|
47
47
|
const rel = path.relative(parent, child);
|
|
48
|
-
return rel && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
48
|
+
return Boolean(rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function resolveMeetingNoteArtifactPath(artifactPath) {
|
|
@@ -114,27 +114,34 @@ function formatMeetingContributions(entries, agents, emptyText, label, maxBytes)
|
|
|
114
114
|
return truncateMeetingContext(combined, maxBytes, label);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
function
|
|
117
|
+
function stripMeetingSummaryMarkdown(text) {
|
|
118
118
|
return String(text || '')
|
|
119
119
|
.replace(/\r/g, '')
|
|
120
|
-
.replace(/```[\s\S]*?```/g, '
|
|
120
|
+
.replace(/```[\s\S]*?```/g, '\n')
|
|
121
121
|
.replace(/`([^`]+)`/g, '$1')
|
|
122
122
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
123
|
-
.replace(
|
|
123
|
+
.replace(/^\s*(?:[#>*-]+|\d+[.)])\s*/gm, '');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cleanMeetingSummaryText(text) {
|
|
127
|
+
return stripMeetingSummaryMarkdown(text)
|
|
124
128
|
.replace(/\s+/g, ' ')
|
|
125
129
|
.trim();
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
function splitMeetingSummaryFragments(text) {
|
|
129
|
-
return
|
|
130
|
-
.split(/\n+|
|
|
131
|
-
.map(s => s.trim())
|
|
133
|
+
return stripMeetingSummaryMarkdown(text)
|
|
134
|
+
.split(/\n+|[.!?]+\s*|;\s*/)
|
|
135
|
+
.map(s => s.replace(/\s+/g, ' ').trim())
|
|
132
136
|
.filter(Boolean);
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
function truncateMeetingSummary(text, maxLen) {
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
const value = String(text || '');
|
|
141
|
+
if (!value) return '';
|
|
142
|
+
if (!Number.isFinite(maxLen) || maxLen <= 0) return '';
|
|
143
|
+
if (value.length < maxLen) return value;
|
|
144
|
+
return value.slice(0, Math.max(0, maxLen - 1)) + '…';
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
function formatMeetingSummaryBullets(entries, agents, emptyText, maxLen) {
|
|
@@ -148,13 +155,14 @@ function formatMeetingSummaryBullets(entries, agents, emptyText, maxLen) {
|
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
function scoreMeetingTakeaway(fragment) {
|
|
151
|
-
const
|
|
158
|
+
const value = String(fragment || '');
|
|
159
|
+
const lower = value.toLowerCase();
|
|
152
160
|
let score = 0;
|
|
153
161
|
if (/(should|must|need to|needs to|recommend|recommended|action|next step|follow up|fix|mitigat|investigat|verify|test|block)/.test(lower)) score += 4;
|
|
154
162
|
if (/(agree|aligned|consensus|support|prefer)/.test(lower)) score += 3;
|
|
155
163
|
if (/(disagree|however|but|risk|risky|concern|trade-off|question|uncertain|worry)/.test(lower)) score += 3;
|
|
156
|
-
if (
|
|
157
|
-
if (
|
|
164
|
+
if (value.length >= 40 && value.length <= 180) score += 2;
|
|
165
|
+
if (value.length > 220) score -= 1;
|
|
158
166
|
return score;
|
|
159
167
|
}
|
|
160
168
|
|
|
@@ -195,7 +203,9 @@ function collectMeetingNextSteps(meeting) {
|
|
|
195
203
|
}
|
|
196
204
|
}
|
|
197
205
|
}
|
|
198
|
-
return
|
|
206
|
+
return steps.length
|
|
207
|
+
? steps
|
|
208
|
+
: ['- Review the findings and debate, then add a human-written conclusion if more nuance is needed.'];
|
|
199
209
|
}
|
|
200
210
|
|
|
201
211
|
function buildTimedOutMeetingConclusion(meeting, agents) {
|
|
@@ -615,4 +625,17 @@ module.exports = {
|
|
|
615
625
|
discoverMeetingWork, collectMeetingFindings, checkMeetingTimeouts,
|
|
616
626
|
addMeetingNote, advanceMeetingRound, endMeeting, archiveMeeting, unarchiveMeeting, deleteMeeting,
|
|
617
627
|
EMPTY_OUTPUT_PATTERNS,
|
|
628
|
+
// exported for testing — engine code MUST go through
|
|
629
|
+
// getMeetings/discoverMeetingWork/collectMeetingFindings/checkMeetingTimeouts,
|
|
630
|
+
// never these helpers directly.
|
|
631
|
+
isPathInside,
|
|
632
|
+
resolveMeetingNoteArtifactPath,
|
|
633
|
+
cleanMeetingSummaryText,
|
|
634
|
+
splitMeetingSummaryFragments,
|
|
635
|
+
truncateMeetingSummary,
|
|
636
|
+
formatMeetingSummaryBullets,
|
|
637
|
+
scoreMeetingTakeaway,
|
|
638
|
+
collectMeetingTakeaways,
|
|
639
|
+
collectMeetingNextSteps,
|
|
640
|
+
buildTimedOutMeetingConclusion,
|
|
618
641
|
};
|
package/engine/playbook.js
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
8
9
|
const path = require('path');
|
|
9
10
|
const shared = require('./shared');
|
|
10
11
|
const queries = require('./queries');
|
|
11
12
|
|
|
12
13
|
const { safeJson, safeRead, getProjects, log, ts, dateStamp, truncateTextBytes, ENGINE_DEFAULTS, WI_STATUS, WORK_TYPE, PR_STATUS, DISPATCH_RESULT } = shared;
|
|
13
|
-
const { getConfig, getDispatch, getNotes, getAgentCharter, getPrs, AGENTS_DIR } = queries;
|
|
14
|
+
const { getConfig, getDispatch, getNotes, getAgentCharter, getPrs, getKnowledgeBaseIndex, AGENTS_DIR } = queries;
|
|
14
15
|
|
|
15
16
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
16
17
|
const PLAYBOOKS_DIR = path.join(MINIONS_DIR, 'playbooks');
|
|
@@ -394,8 +395,8 @@ function renderPlaybook(type, vars) {
|
|
|
394
395
|
content += '````\n```skill\n';
|
|
395
396
|
content += `---\nname: short-descriptive-name\ndescription: One-line description of what this skill does\nallowed-tools: Bash, Read, Edit\ntrigger: when should an agent use this\nscope: minions\nproject: any\n---\n\n# Skill Title\n\n## Steps\n1. ...\n2. ...\n\n## Notes\n...\n`;
|
|
396
397
|
content += '```\n````\n\n';
|
|
397
|
-
content += `- Set \`scope: minions\` for cross-project or Minions-wide skills; the engine writes them to
|
|
398
|
-
content += `- Set \`scope: project\` + \`project: <name>\` only for repo-specific skills; the engine queues a PR to
|
|
398
|
+
content += `- Set \`scope: minions\` for cross-project or Minions-wide skills; the engine writes them to the selected runtime's native personal skills directory\n`;
|
|
399
|
+
content += `- Set \`scope: project\` + \`project: <name>\` only for repo-specific skills; the engine queues a PR to the selected runtime's native project skills directory\n`;
|
|
399
400
|
content += `- Emit at most one skill block per task unless you uncovered two clearly distinct reusable workflows\n`;
|
|
400
401
|
content += `- Do NOT create a skill for one-off bug fixes, isolated command output, obvious repo facts, or anything already covered by existing docs/playbooks/skills\n`;
|
|
401
402
|
|
|
@@ -503,7 +504,7 @@ function buildSystemPrompt(agentId, config, project) {
|
|
|
503
504
|
prompt += `3. Follow the project conventions in CLAUDE.md if present\n`;
|
|
504
505
|
prompt += `4. Write learnings to the path specified in the task prompt (format: \`notes/inbox/{agent}-{work-item-id}-{date}-{time}.md\`)\n`;
|
|
505
506
|
prompt += `5. Agent status is managed by the engine via dispatch.json — agents do not need to track their own status\n`;
|
|
506
|
-
prompt += `6. If you discover a repeatable workflow, output it as a \\\`\\\`\\\`skill fenced block — minions-scoped skills are auto-extracted to
|
|
507
|
+
prompt += `6. If you discover a repeatable workflow, output it as a \\\`\\\`\\\`skill fenced block — minions-scoped skills are auto-extracted to the selected runtime's native personal skills directory\n\n`;
|
|
507
508
|
|
|
508
509
|
return prompt;
|
|
509
510
|
}
|
|
@@ -514,6 +515,26 @@ function buildAgentContext(agentId, config, project) {
|
|
|
514
515
|
project = project || getProjects(config)[0] || {};
|
|
515
516
|
let context = '';
|
|
516
517
|
|
|
518
|
+
function appendContextFile(heading, filePath, maxBytes, extra = '') {
|
|
519
|
+
if (!filePath) return;
|
|
520
|
+
const content = safeRead(filePath);
|
|
521
|
+
if (!content || !content.trim()) return;
|
|
522
|
+
const truncated = Buffer.byteLength(content, 'utf8') > maxBytes
|
|
523
|
+
? truncateTextBytes(content, maxBytes, '\n\n_...truncated; read the full file if needed_')
|
|
524
|
+
: content;
|
|
525
|
+
context += `## ${heading}\n\n`;
|
|
526
|
+
if (extra) context += `${extra}\n\n`;
|
|
527
|
+
context += `${truncated}\n\n`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function appendIndex(heading, body, maxBytes) {
|
|
531
|
+
if (!body || !String(body).trim()) return;
|
|
532
|
+
const truncated = Buffer.byteLength(body, 'utf8') > maxBytes
|
|
533
|
+
? truncateTextBytes(body, maxBytes, '\n\n_...index truncated; use Glob/Read for the full list_')
|
|
534
|
+
: body;
|
|
535
|
+
context += `## ${heading}\n\n${truncated.replace(/^## .+\n\n/, '')}\n`;
|
|
536
|
+
}
|
|
537
|
+
|
|
517
538
|
|
|
518
539
|
// Agent history — last 5 tasks only (keeps it relevant, avoids 37KB dumps)
|
|
519
540
|
const history = safeRead(path.join(AGENTS_DIR, agentId, 'history.md'));
|
|
@@ -527,16 +548,19 @@ function buildAgentContext(agentId, config, project) {
|
|
|
527
548
|
|
|
528
549
|
// Project conventions (from CLAUDE.md) — always relevant for code quality
|
|
529
550
|
if (project.localPath) {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
551
|
+
appendContextFile('Project Conventions (from CLAUDE.md)', path.join(project.localPath, 'CLAUDE.md'), 8192);
|
|
552
|
+
appendContextFile('Project Agent Instructions (from AGENTS.md)', path.join(project.localPath, 'AGENTS.md'), 8192,
|
|
553
|
+
'These instructions are explicitly injected because some runtimes suppress automatic AGENTS.md loading. Follow them unless they conflict with the Minions task contract or playbook.');
|
|
554
|
+
appendContextFile('Project Copilot Instructions (from .github/copilot-instructions.md)', path.join(project.localPath, '.github', 'copilot-instructions.md'), 8192,
|
|
555
|
+
'Follow these repository instructions unless they conflict with the Minions task contract or playbook.');
|
|
535
556
|
}
|
|
536
557
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
558
|
+
appendContextFile('User Claude Instructions (from ~/.claude/CLAUDE.md)', path.join(os.homedir(), '.claude', 'CLAUDE.md'), 8192,
|
|
559
|
+
'These are the user-level Claude Code instructions available in regular Claude usage. Follow them unless they conflict with the Minions task contract or playbook.');
|
|
560
|
+
|
|
561
|
+
appendIndex('Knowledge Base Reference', getKnowledgeBaseIndex(), 8192);
|
|
562
|
+
|
|
563
|
+
context += `## Reference Files\n\nKnowledge base entries are in \`knowledge/{category}/*.md\`, and project-local playbooks live in \`projects/<project>/playbooks/\`. Runtime-native skills and commands are left to the selected CLI runtime; Minions does not inject their contents into the task prompt.\n\n`;
|
|
540
564
|
|
|
541
565
|
// Minions awareness: what's in flight, who's doing what
|
|
542
566
|
const dispatch = getDispatch();
|