@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1682 (2026-05-02)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- index Copilot plugin skills and surface as separate dashboard tab
|
|
7
|
+
|
|
8
|
+
### Other
|
|
9
|
+
- test(meeting): add direct unit tests for internal scoring/formatting/path-resolution helpers (#1985)
|
|
10
|
+
- docs: document new optional runtime adapter methods for skills
|
|
11
|
+
- Keep runtime skills adapter-owned
|
|
12
|
+
- docs: update copilot instructions
|
|
13
|
+
|
|
14
|
+
## 0.1.1681 (2026-05-02)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
- plan completion lifecycle audit fixes (#1982)
|
|
18
|
+
|
|
3
19
|
## 0.1.1680 (2026-05-02)
|
|
4
20
|
|
|
5
21
|
### Other
|
|
@@ -13,13 +13,28 @@ function renderSkills(skills) {
|
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
|
|
16
|
+
const SOURCE_META = {
|
|
17
|
+
'claude-code': { icon: '⚡', label: 'global', group: 'global' },
|
|
18
|
+
'copilot': { icon: '🤖', label: 'copilot', group: 'copilot' },
|
|
19
|
+
'agent-skill': { icon: '🤝', label: 'agent', group: 'agent' },
|
|
20
|
+
'plugin': { icon: '🔌', label: 'plugin', group: 'plugins' },
|
|
21
|
+
'copilot-plugin': { icon: '🤖', label: 'copilot', group: 'copilot' },
|
|
22
|
+
};
|
|
23
|
+
const metaOf = (s) => {
|
|
24
|
+
if (SOURCE_META[s]) return SOURCE_META[s];
|
|
25
|
+
if (s && s.startsWith('project:')) {
|
|
26
|
+
const name = s.slice('project:'.length);
|
|
27
|
+
return { icon: '📁', label: name, group: name };
|
|
28
|
+
}
|
|
29
|
+
return { icon: '🔧', label: 'global', group: 'global' };
|
|
30
|
+
};
|
|
31
|
+
const sourceIcon = (s) => metaOf(s).icon;
|
|
32
|
+
const sourceLabel = (s) => metaOf(s).label;
|
|
18
33
|
|
|
19
34
|
// Group by source
|
|
20
35
|
const groups = {};
|
|
21
36
|
for (const r of skills) {
|
|
22
|
-
const key = r.source
|
|
37
|
+
const key = metaOf(r.source).group;
|
|
23
38
|
if (!groups[key]) groups[key] = [];
|
|
24
39
|
groups[key].push(r);
|
|
25
40
|
}
|
package/dashboard.js
CHANGED
|
@@ -170,10 +170,20 @@ function _normalizeSkillDirForCompare(dir) {
|
|
|
170
170
|
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
// Mirrors the source string emitted by `getSkills()` so `_resolveSkillReadPath`
|
|
174
|
+
// can match a request's `source` param against the entries collectSkillFiles returns.
|
|
175
|
+
const _SKILL_ENTRY_SOURCE_BY_SCOPE = {
|
|
176
|
+
'claude-code': 'claude-code',
|
|
177
|
+
'copilot': 'copilot',
|
|
178
|
+
'agent-skill': 'agent-skill',
|
|
179
|
+
'plugin': 'plugin',
|
|
180
|
+
'copilot-plugin': 'copilot-plugin',
|
|
181
|
+
};
|
|
182
|
+
|
|
173
183
|
function _skillEntrySource(entry) {
|
|
174
184
|
if (!entry) return '';
|
|
175
|
-
|
|
176
|
-
if (
|
|
185
|
+
const mapped = _SKILL_ENTRY_SOURCE_BY_SCOPE[entry.scope];
|
|
186
|
+
if (mapped) return mapped;
|
|
177
187
|
if (entry.scope === 'project') return 'project:' + entry.projectName;
|
|
178
188
|
return entry.scope || '';
|
|
179
189
|
}
|
|
@@ -2309,12 +2319,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
2309
2319
|
// If archived, temporarily restore to active so checkPlanCompletion can find it
|
|
2310
2320
|
const activePath = path.join(prdDir, body.file);
|
|
2311
2321
|
if (fromArchive) {
|
|
2312
|
-
const
|
|
2313
|
-
if (!
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2322
|
+
const archivedPlan = safeJson(prdPath);
|
|
2323
|
+
if (!archivedPlan) return jsonReply(res, 500, { error: 'Could not parse PRD file' });
|
|
2324
|
+
mutateJsonFileLocked(activePath, () => {
|
|
2325
|
+
archivedPlan.status = 'approved';
|
|
2326
|
+
delete archivedPlan.completedAt;
|
|
2327
|
+
delete archivedPlan.planStale;
|
|
2328
|
+
return archivedPlan;
|
|
2329
|
+
}, { defaultValue: archivedPlan });
|
|
2318
2330
|
}
|
|
2319
2331
|
|
|
2320
2332
|
const config = queries.getConfig();
|
|
@@ -2347,11 +2359,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
2347
2359
|
}
|
|
2348
2360
|
|
|
2349
2361
|
// No existing verify — clear completion flag and trigger fresh creation
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
planData
|
|
2353
|
-
|
|
2354
|
-
}
|
|
2362
|
+
mutateJsonFileLocked(activePath, (planData) => {
|
|
2363
|
+
if (planData?._completionNotified) planData._completionNotified = false;
|
|
2364
|
+
return planData;
|
|
2365
|
+
}, { defaultValue: {}, skipWriteIfUnchanged: true });
|
|
2355
2366
|
|
|
2356
2367
|
const lifecycle = require('./engine/lifecycle');
|
|
2357
2368
|
lifecycle.checkPlanCompletion({ item: { sourcePlan: body.file, id: 'manual' } }, config);
|
|
@@ -3572,15 +3583,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
3572
3583
|
const body = await readBody(req);
|
|
3573
3584
|
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
3574
3585
|
const planPath = resolvePlanPath(body.file);
|
|
3575
|
-
|
|
3576
|
-
const
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3586
|
+
let wasStale = false;
|
|
3587
|
+
const plan = mutateJsonFileLocked(planPath, (data) => {
|
|
3588
|
+
if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
|
|
3589
|
+
wasStale = !!data.planStale;
|
|
3590
|
+
data.status = 'approved';
|
|
3591
|
+
data.approvedAt = new Date().toISOString();
|
|
3592
|
+
data.approvedBy = body.approvedBy || os.userInfo().username;
|
|
3593
|
+
delete data.pausedAt;
|
|
3594
|
+
delete data.planStale;
|
|
3595
|
+
delete data._completionNotified;
|
|
3596
|
+
return data;
|
|
3597
|
+
}, { defaultValue: {} });
|
|
3584
3598
|
|
|
3585
3599
|
// Resume paused work items across all projects
|
|
3586
3600
|
let resumed = 0;
|
|
@@ -3662,10 +3676,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
3662
3676
|
const body = await readBody(req);
|
|
3663
3677
|
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
3664
3678
|
const planPath = resolvePlanPath(body.file);
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3679
|
+
mutateJsonFileLocked(planPath, (plan) => {
|
|
3680
|
+
if (!plan || Array.isArray(plan) || typeof plan !== 'object') plan = {};
|
|
3681
|
+
plan.status = 'paused';
|
|
3682
|
+
plan.pausedAt = new Date().toISOString();
|
|
3683
|
+
return plan;
|
|
3684
|
+
}, { defaultValue: {} });
|
|
3669
3685
|
|
|
3670
3686
|
// Propagate pause to materialized work items across all projects:
|
|
3671
3687
|
// kill any active agent process and reset non-completed items to paused.
|
|
@@ -3799,12 +3815,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
3799
3815
|
const body = await readBody(req);
|
|
3800
3816
|
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
3801
3817
|
const planPath = resolvePlanPath(body.file);
|
|
3802
|
-
const plan =
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3818
|
+
const plan = mutateJsonFileLocked(planPath, (data) => {
|
|
3819
|
+
if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
|
|
3820
|
+
data.status = 'rejected';
|
|
3821
|
+
data.rejectedAt = new Date().toISOString();
|
|
3822
|
+
data.rejectedBy = body.rejectedBy || os.userInfo().username;
|
|
3823
|
+
if (body.reason) data.rejectionReason = body.reason;
|
|
3824
|
+
return data;
|
|
3825
|
+
}, { defaultValue: {} });
|
|
3808
3826
|
|
|
3809
3827
|
// Teams notification for plan rejection — non-blocking
|
|
3810
3828
|
try { teams.teamsNotifyPlanEvent({ name: plan.plan_summary || body.file, file: body.file }, 'plan-rejected').catch(() => {}); } catch {}
|
|
@@ -3956,18 +3974,23 @@ const server = http.createServer(async (req, res) => {
|
|
|
3956
3974
|
|
|
3957
3975
|
let archivedSource = null;
|
|
3958
3976
|
let plan = {};
|
|
3977
|
+
const archiveWarnings = [];
|
|
3959
3978
|
if (isPrd) {
|
|
3960
3979
|
try {
|
|
3961
|
-
plan =
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3980
|
+
plan = mutateJsonFileLocked(archivePath, (data) => {
|
|
3981
|
+
if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
|
|
3982
|
+
data.status = 'archived';
|
|
3983
|
+
data.archivedAt = new Date().toISOString();
|
|
3984
|
+
return data;
|
|
3985
|
+
}, { defaultValue: {} }) || {};
|
|
3965
3986
|
// Without removing the .backup sidecar, safeJson would auto-restore the
|
|
3966
3987
|
// pre-completion snapshot on engine restart, re-triggering plan completion
|
|
3967
3988
|
// and spawning duplicate verify tasks (regression of #f28162b0).
|
|
3968
|
-
const
|
|
3969
|
-
|
|
3970
|
-
|
|
3989
|
+
const backupCleanup = shared.neutralizeJsonBackupSidecar(planPath);
|
|
3990
|
+
if (!backupCleanup.ok) {
|
|
3991
|
+
const warning = `Archive backup cleanup failed for ${body.file}: unlink failed (${backupCleanup.unlinkError}); fallback neutralize failed (${backupCleanup.writeError})`;
|
|
3992
|
+
archiveWarnings.push(warning);
|
|
3993
|
+
console.warn(warning);
|
|
3971
3994
|
}
|
|
3972
3995
|
if (plan.source_plan) {
|
|
3973
3996
|
const mdPath = path.join(PLANS_DIR, plan.source_plan);
|
|
@@ -4006,7 +4029,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4006
4029
|
} catch (e) { console.error('plan worktree cleanup:', e.message); }
|
|
4007
4030
|
|
|
4008
4031
|
invalidateStatusCache();
|
|
4009
|
-
|
|
4032
|
+
const payload = { ok: true, archived: body.file, archivedSource, cancelledItems };
|
|
4033
|
+
if (archiveWarnings.length > 0) payload.warnings = archiveWarnings;
|
|
4034
|
+
return jsonReply(res, 200, payload);
|
|
4010
4035
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
4011
4036
|
}
|
|
4012
4037
|
|
|
@@ -4047,12 +4072,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
4047
4072
|
const body = await readBody(req);
|
|
4048
4073
|
if (!body.file || !body.feedback) return jsonReply(res, 400, { error: 'file and feedback required' });
|
|
4049
4074
|
const planPath = resolvePlanPath(body.file);
|
|
4050
|
-
const plan =
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4075
|
+
const plan = mutateJsonFileLocked(planPath, (data) => {
|
|
4076
|
+
if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
|
|
4077
|
+
data.status = 'revision-requested';
|
|
4078
|
+
data.revision_feedback = body.feedback;
|
|
4079
|
+
data.revisionRequestedAt = new Date().toISOString();
|
|
4080
|
+
data.revisionRequestedBy = body.requestedBy || os.userInfo().username;
|
|
4081
|
+
return data;
|
|
4082
|
+
}, { defaultValue: {} });
|
|
4056
4083
|
|
|
4057
4084
|
// Create a work item to revise the plan
|
|
4058
4085
|
const wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
@@ -4478,17 +4505,35 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4478
4505
|
let content = '';
|
|
4479
4506
|
const skillPath = _resolveSkillReadPath({ file, dir, source, config: CONFIG });
|
|
4480
4507
|
if (skillPath) content = safeRead(skillPath) || '';
|
|
4508
|
+
// Fallback when caller didn't supply `dir`: try the source's known native
|
|
4509
|
+
// locations. `_resolveSkillReadPath` only matches entries returned by
|
|
4510
|
+
// `collectSkillFiles`, so a skill that already has `dir` will resolve there.
|
|
4481
4511
|
if (!content && !dir) {
|
|
4482
|
-
// Fallback: search Claude Code skills, then project skills
|
|
4483
4512
|
const home = os.homedir();
|
|
4484
|
-
const
|
|
4485
|
-
|
|
4486
|
-
if (!
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4513
|
+
const skillStem = file.replace(/\.md$/, '').replace(/^SKILL$/, '');
|
|
4514
|
+
const candidates = [];
|
|
4515
|
+
if (source === 'claude-code' || !source) {
|
|
4516
|
+
candidates.push(path.join(home, '.claude', 'skills', skillStem, 'SKILL.md'));
|
|
4517
|
+
}
|
|
4518
|
+
if (source === 'copilot') {
|
|
4519
|
+
candidates.push(path.join(home, '.copilot', 'skills', skillStem, 'SKILL.md'));
|
|
4520
|
+
}
|
|
4521
|
+
if (source === 'agent-skill') {
|
|
4522
|
+
candidates.push(path.join(home, '.agents', 'skills', skillStem, 'SKILL.md'));
|
|
4523
|
+
}
|
|
4524
|
+
if (source.startsWith('project:')) {
|
|
4525
|
+
const proj = PROJECTS.find(p => p.name === source.slice('project:'.length));
|
|
4526
|
+
if (proj?.localPath) {
|
|
4527
|
+
for (const sub of ['.claude', '.github', '.agents']) {
|
|
4528
|
+
candidates.push(path.join(proj.localPath, sub, 'skills', file));
|
|
4529
|
+
candidates.push(path.join(proj.localPath, sub, 'skills', skillStem, 'SKILL.md'));
|
|
4530
|
+
}
|
|
4490
4531
|
}
|
|
4491
4532
|
}
|
|
4533
|
+
for (const c of candidates) {
|
|
4534
|
+
content = safeRead(c) || '';
|
|
4535
|
+
if (content) break;
|
|
4536
|
+
}
|
|
4492
4537
|
}
|
|
4493
4538
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
4494
4539
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
package/docs/plan-lifecycle.md
CHANGED
|
@@ -6,8 +6,8 @@ How plans go from idea to verified, running code.
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
/plan "feature description"
|
|
9
|
-
→ agent writes plan
|
|
10
|
-
→
|
|
9
|
+
→ agent writes plan markdown (plans/plan-*.md)
|
|
10
|
+
→ human reviews and explicitly runs plan-to-prd
|
|
11
11
|
→ agent converts plan into structured PRD items with acceptance criteria
|
|
12
12
|
→ engine materializes PRD items as work items (PL-W001, PL-W002, ...)
|
|
13
13
|
→ engine dispatches work items respecting dependency order
|
|
@@ -15,6 +15,7 @@ How plans go from idea to verified, running code.
|
|
|
15
15
|
→ engine auto-reviews PRs, handles feedback loops
|
|
16
16
|
→ all items done → engine creates VERIFY task
|
|
17
17
|
→ verify agent builds everything, starts webapp, writes testing guide
|
|
18
|
+
→ human archives the PRD/plan from the dashboard when ready
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
## Dependency Management
|
|
@@ -66,7 +67,7 @@ When all PRD items reach `done` or `failed` status, `checkPlanCompletion()` fire
|
|
|
66
67
|
1. **Completion summary** written to `notes/inbox/` with results, timing, and PR list
|
|
67
68
|
2. **Verification task** created (type `verify`, priority `high`) — see below
|
|
68
69
|
3. **PR work item** created for shared-branch plans (to merge the feature branch)
|
|
69
|
-
4. **
|
|
70
|
+
4. **PRD status** persisted as `completed` with `_completionNotified`; files stay active until manual archive
|
|
70
71
|
|
|
71
72
|
## Verification Task
|
|
72
73
|
|
|
@@ -109,6 +110,21 @@ git merge "origin/feat/PL-W005" --no-edit
|
|
|
109
110
|
|
|
110
111
|
The verify agent and the user can then build and run from these paths.
|
|
111
112
|
|
|
113
|
+
## Manual Archive Lifecycle
|
|
114
|
+
|
|
115
|
+
Archiving is a deliberate dashboard action, not a post-verify side effect. When a PRD completes, `checkPlanCompletion()` writes the completion summary, persists `status: "completed"` plus `_completionNotified`, and creates the verify work item. The PRD remains in `prd/` and its source plan remains in `plans/` so humans can review the guide, inspect the final state, resume stale work, or reopen items before hiding the plan from active views.
|
|
116
|
+
|
|
117
|
+
The verify agent does not call `archivePlan()`. After the testing guide and any follow-up PR work are satisfactory, use the dashboard archive action, which calls `POST /api/plans/archive` with the active PRD or plan filename.
|
|
118
|
+
|
|
119
|
+
Manual archive behavior:
|
|
120
|
+
|
|
121
|
+
1. For PRD JSON files, the dashboard moves `prd/<file>.json` to `prd/archive/<file>.json`, marks the archived JSON with `status: "archived"` and `archivedAt`, and removes or neutralizes the active `.backup` sidecar so `safeJson()` cannot restore a stale completed PRD on restart.
|
|
122
|
+
2. If the PRD has `source_plan`, the matching `plans/<source>.md` is moved to `plans/archive/<source>.md`.
|
|
123
|
+
3. Pending or queued work items linked to the archived PRD are cancelled with `_cancelledBy: "plan-archived"`; completed work items remain as history.
|
|
124
|
+
4. Plan worktrees are cleaned up by the archive handler/lifecycle cleanup path.
|
|
125
|
+
|
|
126
|
+
Use unarchive only when the archived PRD/plan should reappear in active views. Reopening completed work should happen before archive, or after unarchive, so the materializer can see the active PRD file.
|
|
127
|
+
|
|
112
128
|
## Human Feedback on PRs
|
|
113
129
|
|
|
114
130
|
After PRs are created, humans can leave comments containing `@minions` to trigger fix tasks. If you're the only human commenting, any comment triggers a fix — no keyword needed. See `pollPrHumanComments()` in `engine/ado.js`.
|
|
@@ -130,11 +146,11 @@ After PRs are created, humans can leave comments containing `@minions` to trigge
|
|
|
130
146
|
|
|
131
147
|
| File | Purpose |
|
|
132
148
|
|------|---------|
|
|
133
|
-
| `plans/*.
|
|
134
|
-
| `
|
|
149
|
+
| `plans/*.md` | Source plans awaiting review or linked to active PRDs |
|
|
150
|
+
| `prd/*.json` | Active PRDs with materializable items |
|
|
151
|
+
| `plans/archive/`, `prd/archive/` | Manually archived plans and PRDs |
|
|
135
152
|
| `playbooks/verify.md` | Verification task playbook |
|
|
136
153
|
| `playbooks/implement.md` | Implementation playbook |
|
|
137
154
|
| `playbooks/plan-to-prd.md` | Plan → PRD conversion playbook |
|
|
138
|
-
| `engine/lifecycle.js` | `checkPlanCompletion`,
|
|
155
|
+
| `engine/lifecycle.js` | `checkPlanCompletion`, completion hooks, PRD sync |
|
|
139
156
|
| `engine.js` | `spawnAgent` (dependency merging), `resolveDependencyBranches` |
|
|
140
|
-
|