@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 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 sourceIcon = (s) => s === 'claude-code' ? '⚡' : s === 'plugin' ? '🔌' : s?.startsWith('project:') ? '📁' : '🔧';
17
- const sourceLabel = (s) => s === 'plugin' ? 'plugin' : s?.startsWith('project:') ? s.replace('project:', '') : 'global';
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 === 'plugin' ? 'plugins' : r.source?.startsWith('project:') ? r.source.replace('project:', '') : 'global';
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
- if (entry.scope === 'claude-code') return 'claude-code';
176
- if (entry.scope === 'plugin') return 'plugin';
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 plan = safeJson(prdPath);
2313
- if (!plan) return jsonReply(res, 500, { error: 'Could not parse PRD file' });
2314
- plan.status = 'approved';
2315
- delete plan.completedAt;
2316
- delete plan.planStale;
2317
- safeWrite(activePath, plan);
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
- const planData = safeJson(activePath);
2351
- if (planData?._completionNotified) {
2352
- planData._completionNotified = false;
2353
- safeWrite(activePath, planData);
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
- const plan = safeJsonObj(planPath);
3576
- const wasStale = !!plan.planStale;
3577
- plan.status = 'approved';
3578
- plan.approvedAt = new Date().toISOString();
3579
- plan.approvedBy = body.approvedBy || os.userInfo().username;
3580
- delete plan.pausedAt;
3581
- delete plan.planStale;
3582
- delete plan._completionNotified;
3583
- safeWrite(planPath, plan);
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
- const plan = safeJsonObj(planPath);
3666
- plan.status = 'paused';
3667
- plan.pausedAt = new Date().toISOString();
3668
- safeWrite(planPath, plan);
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 = safeJsonObj(planPath);
3803
- plan.status = 'rejected';
3804
- plan.rejectedAt = new Date().toISOString();
3805
- plan.rejectedBy = body.rejectedBy || os.userInfo().username;
3806
- if (body.reason) plan.rejectionReason = body.reason;
3807
- safeWrite(planPath, plan);
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 = safeJsonObj(archivePath) || {};
3962
- plan.status = 'archived';
3963
- plan.archivedAt = new Date().toISOString();
3964
- safeWrite(archivePath, plan);
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 backupPath = planPath + '.backup';
3969
- try { fs.unlinkSync(backupPath); } catch {
3970
- try { fs.writeFileSync(backupPath, JSON.stringify({ status: 'archived' })); } catch { /* best-effort */ }
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
- return jsonReply(res, 200, { ok: true, archived: body.file, archivedSource, cancelledItems });
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 = safeJsonObj(planPath);
4051
- plan.status = 'revision-requested';
4052
- plan.revision_feedback = body.feedback;
4053
- plan.revisionRequestedAt = new Date().toISOString();
4054
- plan.revisionRequestedBy = body.requestedBy || os.userInfo().username;
4055
- safeWrite(planPath, plan);
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 claudePath = path.join(home, '.claude', 'skills', file.replace('.md', '').replace('SKILL', ''), 'SKILL.md');
4485
- content = safeRead(claudePath) || '';
4486
- if (!content) {
4487
- if (source.startsWith('project:')) {
4488
- const proj = PROJECTS.find(p => p.name === source.replace('project:', ''));
4489
- if (proj) content = safeRead(path.join(proj.localPath, '.claude', 'skills', file)) || '';
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', '*');
@@ -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 JSON (plan-*.json)
10
- engine chains plan PRD (plan-to-prd)
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. **Plan archived** to `plans/archive/`
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/*.json` | Active plan files with PRD items |
134
- | `plans/archive/` | Completed/archived plans |
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`, `chainPlanToPrd` |
155
+ | `engine/lifecycle.js` | `checkPlanCompletion`, completion hooks, PRD sync |
139
156
  | `engine.js` | `spawnAgent` (dependency merging), `resolveDependencyBranches` |
140
-
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-02T13:32:18.770Z"
4
+ "cachedAt": "2026-05-02T15:15:12.425Z"
5
5
  }