@yemi33/minions 0.1.2077 → 0.1.2079
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/dashboard/js/refresh.js +41 -2
- package/dashboard.js +9 -3
- package/engine/shared.js +110 -0
- package/package.json +1 -1
- package/prompts/cc-system.md +13 -0
package/dashboard/js/refresh.js
CHANGED
|
@@ -511,8 +511,17 @@ let _lastStatusOkAt = Date.now();
|
|
|
511
511
|
let _consecutiveStatusFails = 0;
|
|
512
512
|
let _unreachableSince = 0; // 0 = currently reachable
|
|
513
513
|
let _unreachableAgeTimer = null;
|
|
514
|
-
|
|
515
|
-
|
|
514
|
+
// 3 + 20s (was 2 + 12s) — the prior thresholds tripped a banner on a single
|
|
515
|
+
// safeFetch abort (timeout 15s in state.js) because the OR'd age side was
|
|
516
|
+
// already satisfied. With safeFetch=15s, the age budget needs >15s to avoid
|
|
517
|
+
// the trip-on-first-slow-response footgun. 3 consecutive fails ≈ 12s of real
|
|
518
|
+
// outage at the 4s poll cadence, which still surfaces the banner promptly
|
|
519
|
+
// during a real dashboard crash but tolerates one slow rebuild + retry.
|
|
520
|
+
const _UNREACHABLE_FAIL_THRESHOLD = 3;
|
|
521
|
+
const _UNREACHABLE_AGE_MS = 20000;
|
|
522
|
+
// Once the banner is up we throttle polls (exponential, capped at 30s) to
|
|
523
|
+
// avoid hammering a struggling dashboard / network. Reset on recovery.
|
|
524
|
+
let _nextPollAllowedAt = 0;
|
|
516
525
|
|
|
517
526
|
function _formatAge(ms) {
|
|
518
527
|
if (ms < 1000) return 'just now';
|
|
@@ -595,10 +604,26 @@ window._resetDashboardUnreachableForTest = function() {
|
|
|
595
604
|
_lastStatusOkAt = Date.now();
|
|
596
605
|
_consecutiveStatusFails = 0;
|
|
597
606
|
_unreachableSince = 0;
|
|
607
|
+
_nextPollAllowedAt = 0;
|
|
598
608
|
if (_unreachableAgeTimer) { clearInterval(_unreachableAgeTimer); _unreachableAgeTimer = null; }
|
|
599
609
|
delete window._dashboardUnreachable;
|
|
600
610
|
};
|
|
601
611
|
|
|
612
|
+
// Visibility wake-up: Chromium throttles setInterval on hidden tabs to ~1/min,
|
|
613
|
+
// so _lastStatusOkAt can drift far past the age threshold while the tab is
|
|
614
|
+
// backgrounded. When the user refocuses, state.js fires refresh() (line 206)
|
|
615
|
+
// — without this reset the age side of the OR is already satisfied and a
|
|
616
|
+
// single transient post-wake failure (DNS not yet up after suspend, mid-
|
|
617
|
+
// restart server) trips the banner before any real evidence of trouble.
|
|
618
|
+
// Reset BEFORE state.js calls refresh() — listener order is fine because
|
|
619
|
+
// both fire on the same event; this listener registered first runs first.
|
|
620
|
+
document.addEventListener('visibilitychange', function() {
|
|
621
|
+
if (document.visibilityState === 'visible' && !_unreachableSince) {
|
|
622
|
+
_lastStatusOkAt = Date.now();
|
|
623
|
+
_consecutiveStatusFails = 0;
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
602
627
|
// ── Refresh diagnostics (W-mphejzx100081972) ─────────────────────────────
|
|
603
628
|
// Ring buffer capturing the last 50 /api/status poll cycles so a user
|
|
604
629
|
// reporting "the dashboard didn't auto-update when X changed" can paste
|
|
@@ -649,6 +674,11 @@ document.addEventListener('visibilitychange', function() {
|
|
|
649
674
|
|
|
650
675
|
async function refresh() {
|
|
651
676
|
if (_refreshInFlight) return;
|
|
677
|
+
// Backoff gate — only active while the unreachable banner is up. Skips
|
|
678
|
+
// setInterval ticks until _nextPollAllowedAt is reached so a downed
|
|
679
|
+
// dashboard isn't hammered at the steady 4s cadence (which produces
|
|
680
|
+
// console-spam and adds load to whatever's wedged).
|
|
681
|
+
if (_nextPollAllowedAt && Date.now() < _nextPollAllowedAt) return;
|
|
652
682
|
_refreshInFlight = true;
|
|
653
683
|
const _diagOn = _isRefreshDiagOn();
|
|
654
684
|
const _t0 = _diagOn ? Date.now() : 0;
|
|
@@ -730,6 +760,7 @@ async function refresh() {
|
|
|
730
760
|
// instead of just dismissing the banner.
|
|
731
761
|
_lastStatusOkAt = Date.now();
|
|
732
762
|
_consecutiveStatusFails = 0;
|
|
763
|
+
_nextPollAllowedAt = 0;
|
|
733
764
|
if (_unreachableSince) _markDashboardReachable();
|
|
734
765
|
const _renderStart = _diagOn ? Date.now() : 0;
|
|
735
766
|
let _diagChanges = null;
|
|
@@ -762,6 +793,14 @@ async function refresh() {
|
|
|
762
793
|
if (_consecutiveStatusFails >= _UNREACHABLE_FAIL_THRESHOLD || ageMs > _UNREACHABLE_AGE_MS) {
|
|
763
794
|
_markDashboardUnreachable(e);
|
|
764
795
|
}
|
|
796
|
+
// Backoff: once we've tripped the banner, throttle subsequent polls
|
|
797
|
+
// (4s → 8s → 16s → 30s cap). Reset to 0 in the success path below so
|
|
798
|
+
// recovery snaps back to the steady 4s cadence on the next tick.
|
|
799
|
+
if (_unreachableSince) {
|
|
800
|
+
const failsSinceTrip = Math.max(1, _consecutiveStatusFails - _UNREACHABLE_FAIL_THRESHOLD + 1);
|
|
801
|
+
const backoffMs = Math.min(30000, 4000 * Math.pow(2, failsSinceTrip - 1));
|
|
802
|
+
_nextPollAllowedAt = Date.now() + backoffMs;
|
|
803
|
+
}
|
|
765
804
|
}
|
|
766
805
|
finally {
|
|
767
806
|
_refreshInFlight = false;
|
package/dashboard.js
CHANGED
|
@@ -2810,9 +2810,15 @@ function _ccTurnEntryToActionType(kind) {
|
|
|
2810
2810
|
}
|
|
2811
2811
|
}
|
|
2812
2812
|
|
|
2813
|
-
// Hash the system prompt so we can detect changes and invalidate stale sessions
|
|
2814
|
-
|
|
2815
|
-
|
|
2813
|
+
// Hash the system prompt so we can detect changes and invalidate stale sessions.
|
|
2814
|
+
// Hashing the RENDERED prompt (not the raw template) means changes to the
|
|
2815
|
+
// substituted content — most importantly the dynamic {{available_skills}}
|
|
2816
|
+
// registry built from ~/.copilot/skills/ + installed-plugins — also invalidate
|
|
2817
|
+
// pooled sessions on dashboard restart. Without this, installing or removing
|
|
2818
|
+
// a skill would leave warm sessions glued to the old registry until they aged
|
|
2819
|
+
// out naturally.
|
|
2820
|
+
const _ccPromptHash = require('crypto').createHash('md5').update(CC_STATIC_SYSTEM_PROMPT).digest('hex').slice(0, 8);
|
|
2821
|
+
const _docChatPromptHash = require('crypto').createHash('md5').update(DOC_CHAT_SYSTEM_PROMPT).digest('hex').slice(0, 8);
|
|
2816
2822
|
|
|
2817
2823
|
function _sessionExpired(lastActiveAt, ttlMs) {
|
|
2818
2824
|
if (!lastActiveAt || !ttlMs) return false;
|
package/engine/shared.js
CHANGED
|
@@ -3455,6 +3455,106 @@ function describeCcProtectedPaths(liveRoot) {
|
|
|
3455
3455
|
return `READ ONLY in the live checkout at \`${norm}\` — never write/edit: ${basenames}, ${globs}, ${prefixes}. This rule is path-scoped, not basename-scoped. Files with the same basename inside an isolated agent worktree (e.g. \`{worktreeRoot}/W-<id>/dashboard.js\`) are NOT protected — agents working in their own worktrees may edit any repository source the work item requires.`;
|
|
3456
3456
|
}
|
|
3457
3457
|
|
|
3458
|
+
// Enumerate Copilot CLI skills from the conventional install layout so the CC
|
|
3459
|
+
// system prompt can mirror the vanilla runtime's <available_skills> registry.
|
|
3460
|
+
//
|
|
3461
|
+
// The CC system prompt fully replaces Copilot CLI's default — which means the
|
|
3462
|
+
// default's auto-injected <available_skills> block disappears, and the agent
|
|
3463
|
+
// has no idea what skills it has. It then falls back to grep/glob against
|
|
3464
|
+
// well-known paths (typically `.claude/skills/` because the underlying model
|
|
3465
|
+
// is Claude and that path dominates its training data). This helper rebuilds
|
|
3466
|
+
// the registry from the actual Copilot install dirs so the prompt can tell
|
|
3467
|
+
// the agent the truth.
|
|
3468
|
+
//
|
|
3469
|
+
// Locations scanned (matches Copilot CLI's discovery rules):
|
|
3470
|
+
// - ~/.copilot/skills/<name>/SKILL.md → location="user"
|
|
3471
|
+
// - ~/.copilot/installed-plugins/<mkt>/<plugin>/skills/<name>/SKILL.md
|
|
3472
|
+
// → location="plugin"
|
|
3473
|
+
//
|
|
3474
|
+
// Tolerates lowercase `skill.md` (Linux-style installs) and missing dirs
|
|
3475
|
+
// (returns an empty array rather than throwing). User-level entries shadow
|
|
3476
|
+
// plugin entries with the same name (mirrors Copilot CLI's user-wins rule).
|
|
3477
|
+
function listCopilotSkills(opts) {
|
|
3478
|
+
const homeDir = (opts && typeof opts.homeDir === 'string') ? opts.homeDir : os.homedir();
|
|
3479
|
+
const copilotRoot = path.join(homeDir, '.copilot');
|
|
3480
|
+
const collected = [];
|
|
3481
|
+
|
|
3482
|
+
const readSkillDir = (dir, fallbackName, location) => {
|
|
3483
|
+
let content;
|
|
3484
|
+
try {
|
|
3485
|
+
content = fs.readFileSync(path.join(dir, 'SKILL.md'), 'utf8');
|
|
3486
|
+
} catch {
|
|
3487
|
+
try { content = fs.readFileSync(path.join(dir, 'skill.md'), 'utf8'); }
|
|
3488
|
+
catch { return null; }
|
|
3489
|
+
}
|
|
3490
|
+
const fm = parseSkillFrontmatter(content, fallbackName + '.md');
|
|
3491
|
+
return {
|
|
3492
|
+
name: fm.name || fallbackName,
|
|
3493
|
+
description: fm.description || '',
|
|
3494
|
+
location,
|
|
3495
|
+
};
|
|
3496
|
+
};
|
|
3497
|
+
|
|
3498
|
+
const safeReadDirents = (dir) => {
|
|
3499
|
+
try { return fs.readdirSync(dir, { withFileTypes: true }); }
|
|
3500
|
+
catch { return []; }
|
|
3501
|
+
};
|
|
3502
|
+
|
|
3503
|
+
// User-level skills
|
|
3504
|
+
for (const entry of safeReadDirents(path.join(copilotRoot, 'skills'))) {
|
|
3505
|
+
if (!entry.isDirectory()) continue;
|
|
3506
|
+
const skill = readSkillDir(path.join(copilotRoot, 'skills', entry.name), entry.name, 'user');
|
|
3507
|
+
if (skill) collected.push(skill);
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
// Plugin-provided skills (two levels deep: marketplace/plugin)
|
|
3511
|
+
const pluginsRoot = path.join(copilotRoot, 'installed-plugins');
|
|
3512
|
+
for (const mkt of safeReadDirents(pluginsRoot)) {
|
|
3513
|
+
if (!mkt.isDirectory()) continue;
|
|
3514
|
+
for (const plugin of safeReadDirents(path.join(pluginsRoot, mkt.name))) {
|
|
3515
|
+
if (!plugin.isDirectory()) continue;
|
|
3516
|
+
const pluginSkillsDir = path.join(pluginsRoot, mkt.name, plugin.name, 'skills');
|
|
3517
|
+
for (const entry of safeReadDirents(pluginSkillsDir)) {
|
|
3518
|
+
if (!entry.isDirectory()) continue;
|
|
3519
|
+
const skill = readSkillDir(path.join(pluginSkillsDir, entry.name), entry.name, 'plugin');
|
|
3520
|
+
if (skill) collected.push(skill);
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
// De-dupe by name, user wins over plugin (matches Copilot CLI's precedence)
|
|
3526
|
+
const seen = new Map();
|
|
3527
|
+
for (const s of collected) {
|
|
3528
|
+
const existing = seen.get(s.name);
|
|
3529
|
+
if (!existing || (s.location === 'user' && existing.location !== 'user')) {
|
|
3530
|
+
seen.set(s.name, s);
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
return Array.from(seen.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
function _xmlEscapeForPrompt(s) {
|
|
3537
|
+
return String(s == null ? '' : s)
|
|
3538
|
+
.replace(/&/g, '&')
|
|
3539
|
+
.replace(/</g, '<')
|
|
3540
|
+
.replace(/>/g, '>');
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
// Render a list of skills as an <available_skills> XML block matching the
|
|
3544
|
+
// shape Copilot CLI's default system prompt injects. When `skills` is empty
|
|
3545
|
+
// we still emit the block (with a sentinel comment) so the agent learns the
|
|
3546
|
+
// path exists rather than silently inferring nothing's there.
|
|
3547
|
+
function buildAvailableSkillsBlock(skills) {
|
|
3548
|
+
const list = Array.isArray(skills) ? skills : [];
|
|
3549
|
+
if (list.length === 0) {
|
|
3550
|
+
return '<available_skills>\n <!-- No skills found in ~/.copilot/skills/ or ~/.copilot/installed-plugins/<marketplace>/<plugin>/skills/ -->\n</available_skills>';
|
|
3551
|
+
}
|
|
3552
|
+
const items = list.map(s =>
|
|
3553
|
+
`<skill>\n <name>${_xmlEscapeForPrompt(s.name)}</name>\n <description>${_xmlEscapeForPrompt(s.description)}</description>\n <location>${_xmlEscapeForPrompt(s.location)}</location>\n</skill>`
|
|
3554
|
+
).join('\n');
|
|
3555
|
+
return `<available_skills>\n${items}\n</available_skills>`;
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3458
3558
|
function renderCcSystemPrompt(raw, opts) {
|
|
3459
3559
|
const liveRoot = (opts && typeof opts.liveRoot === 'string') ? opts.liveRoot : MINIONS_DIR;
|
|
3460
3560
|
const turnId = (opts && typeof opts.turnId === 'string') ? opts.turnId : '';
|
|
@@ -3467,6 +3567,14 @@ function renderCcSystemPrompt(raw, opts) {
|
|
|
3467
3567
|
if (out.includes('{{cc_protected_paths}}')) {
|
|
3468
3568
|
out = out.replace(/\{\{cc_protected_paths\}\}/g, describeCcProtectedPaths(liveRoot));
|
|
3469
3569
|
}
|
|
3570
|
+
// Available-skills enumeration walks two directory trees and reads each
|
|
3571
|
+
// SKILL.md's frontmatter — only do it when the template asks for it.
|
|
3572
|
+
// Tests pass `opts.skillsHomeDir` to redirect the scan at a fixture dir.
|
|
3573
|
+
if (out.includes('{{available_skills}}')) {
|
|
3574
|
+
const skillsOpts = (opts && typeof opts.skillsHomeDir === 'string')
|
|
3575
|
+
? { homeDir: opts.skillsHomeDir } : undefined;
|
|
3576
|
+
out = out.replace(/\{\{available_skills\}\}/g, buildAvailableSkillsBlock(listCopilotSkills(skillsOpts)));
|
|
3577
|
+
}
|
|
3470
3578
|
return out
|
|
3471
3579
|
.replace(/\{\{cc_turn_id\}\}/g, turnId)
|
|
3472
3580
|
.replace(/\{\{dashboard_port\}\}/g, dashboardPort);
|
|
@@ -5418,6 +5526,8 @@ module.exports = {
|
|
|
5418
5526
|
isLiveCommandCenterPath,
|
|
5419
5527
|
describeCcProtectedPaths,
|
|
5420
5528
|
renderCcSystemPrompt,
|
|
5529
|
+
listCopilotSkills,
|
|
5530
|
+
buildAvailableSkillsBlock,
|
|
5421
5531
|
_CC_PROTECTED_BASENAMES, // exported for testing
|
|
5422
5532
|
_CC_PROTECTED_FILE_GLOBS, // exported for testing
|
|
5423
5533
|
_CC_PROTECTED_PREFIXES, // exported for testing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2079",
|
|
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"
|
package/prompts/cc-system.md
CHANGED
|
@@ -39,6 +39,19 @@ CAN modify: notes, plans, knowledge, work items, pull-requests.json, routing.md,
|
|
|
39
39
|
## Filesystem
|
|
40
40
|
Minions state lives in `{{minions_dir}}/`. Key paths: `config.json` (config), `routing.md` (dispatch rules), `projects/{name}/work-items.json` & `pull-requests.json` (per-project), `agents/{id}/` (charters, output), `plans/` & `prd/` (plans), `knowledge/` (KB), `notes/inbox/` (inbox), `engine/dispatch.json` (queue), `playbooks/` (templates). Use tools to read specifics.
|
|
41
41
|
|
|
42
|
+
## Your skills
|
|
43
|
+
|
|
44
|
+
Your runtime is the **Copilot CLI**, not Claude Code. Your installed skills live in:
|
|
45
|
+
|
|
46
|
+
- `~/.copilot/skills/<name>/SKILL.md` — user-level skills (yours)
|
|
47
|
+
- `~/.copilot/installed-plugins/<marketplace>/<plugin>/skills/<name>/SKILL.md` — plugin-provided skills (also yours)
|
|
48
|
+
|
|
49
|
+
To invoke a skill, call your `skill` tool with the skill name (the directory name). The skill body then guides the next steps.
|
|
50
|
+
|
|
51
|
+
When asked "what skills do you have", answer from the registry below — do NOT grep/glob `.claude/skills/`. Those belong to a different runtime (Claude Code) and are not available to you, even if the directory exists in cwd or `$HOME`.
|
|
52
|
+
|
|
53
|
+
{{available_skills}}
|
|
54
|
+
|
|
42
55
|
## Role: Orchestrator
|
|
43
56
|
You are primarily a dispatcher. Agents have full Claude Code + worktrees + MCP tools and are better suited for real work — but you are not hard-stopped from handling small requests yourself.
|
|
44
57
|
|