@yemi33/minions 0.1.1
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 +819 -0
- package/LICENSE +21 -0
- package/README.md +598 -0
- package/agents/dallas/charter.md +56 -0
- package/agents/lambert/charter.md +67 -0
- package/agents/ralph/charter.md +45 -0
- package/agents/rebecca/charter.md +57 -0
- package/agents/ripley/charter.md +47 -0
- package/bin/minions.js +467 -0
- package/config.template.json +28 -0
- package/dashboard.html +4822 -0
- package/dashboard.js +2623 -0
- package/docs/auto-discovery.md +416 -0
- package/docs/blog-first-successful-dispatch.md +128 -0
- package/docs/command-center.md +156 -0
- package/docs/demo/01-dashboard-overview.gif +0 -0
- package/docs/demo/02-command-center.gif +0 -0
- package/docs/demo/03-work-items.gif +0 -0
- package/docs/demo/04-plan-docchat.gif +0 -0
- package/docs/demo/05-prd-progress.gif +0 -0
- package/docs/demo/06-inbox-metrics.gif +0 -0
- package/docs/deprecated.json +83 -0
- package/docs/distribution.md +96 -0
- package/docs/engine-restart.md +92 -0
- package/docs/human-vs-automated.md +108 -0
- package/docs/index.html +221 -0
- package/docs/plan-lifecycle.md +140 -0
- package/docs/self-improvement.md +344 -0
- package/engine/ado-mcp-wrapper.js +42 -0
- package/engine/ado.js +383 -0
- package/engine/check-status.js +23 -0
- package/engine/cli.js +754 -0
- package/engine/consolidation.js +417 -0
- package/engine/github.js +331 -0
- package/engine/lifecycle.js +1113 -0
- package/engine/llm.js +116 -0
- package/engine/queries.js +677 -0
- package/engine/shared.js +397 -0
- package/engine/spawn-agent.js +151 -0
- package/engine.js +3227 -0
- package/minions.js +556 -0
- package/package.json +48 -0
- package/playbooks/ask.md +49 -0
- package/playbooks/build-and-test.md +155 -0
- package/playbooks/explore.md +64 -0
- package/playbooks/fix.md +57 -0
- package/playbooks/implement-shared.md +68 -0
- package/playbooks/implement.md +95 -0
- package/playbooks/plan-to-prd.md +104 -0
- package/playbooks/plan.md +99 -0
- package/playbooks/review.md +68 -0
- package/playbooks/test.md +75 -0
- package/playbooks/verify.md +190 -0
- package/playbooks/work-item.md +74 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/queries.js — Shared read-only state queries for Minions engine + dashboard.
|
|
3
|
+
* Single source of truth for all data reading/aggregation.
|
|
4
|
+
* Both engine.js and dashboard.js require() this module.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const shared = require('./shared');
|
|
10
|
+
|
|
11
|
+
const { safeRead, safeReadDir, safeJson, safeWrite, getProjects,
|
|
12
|
+
projectWorkItemsPath, projectPrPath, parseSkillFrontmatter, KB_CATEGORIES } = shared;
|
|
13
|
+
|
|
14
|
+
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
17
|
+
const AGENTS_DIR = path.join(MINIONS_DIR, 'agents');
|
|
18
|
+
const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
|
|
19
|
+
const INBOX_DIR = path.join(MINIONS_DIR, 'notes', 'inbox');
|
|
20
|
+
const PLANS_DIR = path.join(MINIONS_DIR, 'plans');
|
|
21
|
+
const PRD_DIR = path.join(MINIONS_DIR, 'prd');
|
|
22
|
+
const SKILLS_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude', 'skills');
|
|
23
|
+
const KNOWLEDGE_DIR = path.join(MINIONS_DIR, 'knowledge');
|
|
24
|
+
const ARCHIVE_DIR = path.join(MINIONS_DIR, 'notes', 'archive');
|
|
25
|
+
|
|
26
|
+
const CONFIG_PATH = path.join(MINIONS_DIR, 'config.json');
|
|
27
|
+
const CONTROL_PATH = path.join(ENGINE_DIR, 'control.json');
|
|
28
|
+
const DISPATCH_PATH = path.join(ENGINE_DIR, 'dispatch.json');
|
|
29
|
+
const LOG_PATH = path.join(ENGINE_DIR, 'log.json');
|
|
30
|
+
const NOTES_PATH = path.join(MINIONS_DIR, 'notes.md');
|
|
31
|
+
|
|
32
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function timeSince(ms) {
|
|
35
|
+
const s = Math.floor((Date.now() - ms) / 1000);
|
|
36
|
+
if (s < 60) return `${s}s ago`;
|
|
37
|
+
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
|
38
|
+
return `${Math.floor(s / 3600)}h ago`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Core State Readers ──────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function getConfig() {
|
|
44
|
+
return safeJson(CONFIG_PATH) || {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getControl() {
|
|
48
|
+
return safeJson(CONTROL_PATH) || { state: 'stopped', pid: null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getDispatch() {
|
|
52
|
+
return safeJson(DISPATCH_PATH) || { pending: [], active: [], completed: [] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getDispatchQueue() {
|
|
56
|
+
const d = getDispatch();
|
|
57
|
+
d.completed = (d.completed || []).slice(-20);
|
|
58
|
+
return d;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getNotes() {
|
|
62
|
+
return safeRead(NOTES_PATH);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getNotesWithMeta() {
|
|
66
|
+
const content = safeRead(NOTES_PATH) || '';
|
|
67
|
+
try {
|
|
68
|
+
const stat = fs.statSync(NOTES_PATH);
|
|
69
|
+
return { content, updatedAt: stat.mtimeMs };
|
|
70
|
+
} catch { return { content, updatedAt: null }; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getEngineLog() {
|
|
74
|
+
const logJson = safeRead(LOG_PATH);
|
|
75
|
+
if (!logJson) return [];
|
|
76
|
+
try {
|
|
77
|
+
const entries = JSON.parse(logJson);
|
|
78
|
+
const arr = Array.isArray(entries) ? entries : (entries.entries || []);
|
|
79
|
+
return arr.slice(-50);
|
|
80
|
+
} catch { return []; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getMetrics() {
|
|
84
|
+
return safeJson(path.join(ENGINE_DIR, 'metrics.json')) || {};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Inbox ───────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function getInboxFiles() {
|
|
90
|
+
try { return fs.readdirSync(INBOX_DIR).filter(f => f.endsWith('.md')); } catch { return []; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getInbox() {
|
|
94
|
+
return safeReadDir(INBOX_DIR)
|
|
95
|
+
.filter(f => f.endsWith('.md'))
|
|
96
|
+
.map(f => {
|
|
97
|
+
const fullPath = path.join(INBOX_DIR, f);
|
|
98
|
+
try {
|
|
99
|
+
const stat = fs.statSync(fullPath);
|
|
100
|
+
const content = safeRead(fullPath) || '';
|
|
101
|
+
return { name: f, age: timeSince(stat.mtimeMs), mtime: stat.mtimeMs, content };
|
|
102
|
+
} catch { return null; }
|
|
103
|
+
})
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Agents ──────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
// Agent status is DERIVED from dispatch.json — single source of truth.
|
|
111
|
+
// dispatch.active entry for this agent → working
|
|
112
|
+
// dispatch.completed (most recent) → done/error
|
|
113
|
+
// neither → idle
|
|
114
|
+
// Metadata (resultSummary, verdict, pr) is carried on dispatch entries.
|
|
115
|
+
function getAgentStatus(agentId) {
|
|
116
|
+
const dispatch = getDispatch();
|
|
117
|
+
|
|
118
|
+
// Check active dispatch
|
|
119
|
+
const active = (dispatch.active || []).find(d => d.agent === agentId);
|
|
120
|
+
if (active) {
|
|
121
|
+
return {
|
|
122
|
+
status: 'working',
|
|
123
|
+
task: active.task || '',
|
|
124
|
+
dispatch_id: active.id,
|
|
125
|
+
type: active.type || '',
|
|
126
|
+
branch: active.meta?.branch || '',
|
|
127
|
+
started_at: active.started_at || active.created_at || null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check most recent completed dispatch (within last 5 minutes → show done/error)
|
|
132
|
+
const completed = (dispatch.completed || [])
|
|
133
|
+
.filter(d => d.agent === agentId)
|
|
134
|
+
.sort((a, b) => (b.completed_at || '').localeCompare(a.completed_at || ''));
|
|
135
|
+
if (completed.length > 0) {
|
|
136
|
+
const latest = completed[0];
|
|
137
|
+
const ageMs = latest.completed_at ? Date.now() - new Date(latest.completed_at).getTime() : Infinity;
|
|
138
|
+
if (ageMs < 300000) { // 5 minutes
|
|
139
|
+
return {
|
|
140
|
+
status: latest.result === 'error' ? 'error' : 'done',
|
|
141
|
+
task: latest.task || '',
|
|
142
|
+
dispatch_id: latest.id,
|
|
143
|
+
type: latest.type || '',
|
|
144
|
+
completed_at: latest.completed_at,
|
|
145
|
+
resultSummary: latest.resultSummary || latest.reason || '',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Fallback: derive active state from work-item markers.
|
|
151
|
+
// This protects UI status when dispatch.json briefly desyncs from work-item files.
|
|
152
|
+
try {
|
|
153
|
+
const config = getConfig();
|
|
154
|
+
const allItems = getWorkItems(config);
|
|
155
|
+
const latestInFlight = allItems
|
|
156
|
+
.filter(w =>
|
|
157
|
+
(w.dispatched_to || '').toLowerCase() === String(agentId).toLowerCase() &&
|
|
158
|
+
(w.status === 'dispatched' || w.status === 'in-progress')
|
|
159
|
+
)
|
|
160
|
+
.sort((a, b) => (b.dispatched_at || '').localeCompare(a.dispatched_at || ''))[0];
|
|
161
|
+
if (latestInFlight) {
|
|
162
|
+
return {
|
|
163
|
+
status: 'working',
|
|
164
|
+
task: latestInFlight.title || latestInFlight.id || '',
|
|
165
|
+
dispatch_id: null,
|
|
166
|
+
type: latestInFlight.type || '',
|
|
167
|
+
branch: latestInFlight.branch || '',
|
|
168
|
+
started_at: latestInFlight.dispatched_at || latestInFlight.created || null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
} catch {}
|
|
172
|
+
|
|
173
|
+
return { status: 'idle', task: null, started_at: null, completed_at: null };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// setAgentStatus removed — agent status is derived from dispatch.json.
|
|
177
|
+
// Status.json files no longer exist.
|
|
178
|
+
|
|
179
|
+
function getAgentCharter(agentId) {
|
|
180
|
+
return safeRead(path.join(AGENTS_DIR, agentId, 'charter.md'));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getAgents(config) {
|
|
184
|
+
config = config || getConfig();
|
|
185
|
+
// Fall back to DEFAULT_AGENTS if config has no agents (uninitialized repo)
|
|
186
|
+
const agents = (config.agents && Object.keys(config.agents).length > 0)
|
|
187
|
+
? config.agents
|
|
188
|
+
: shared.DEFAULT_AGENTS;
|
|
189
|
+
const roster = Object.entries(agents).map(([id, info]) => ({ id, ...info }));
|
|
190
|
+
const allInboxFiles = safeReadDir(INBOX_DIR);
|
|
191
|
+
|
|
192
|
+
return roster.map(a => {
|
|
193
|
+
const inboxFiles = allInboxFiles.filter(f => f.includes(a.id));
|
|
194
|
+
const s = getAgentStatus(a.id); // derives from dispatch.json
|
|
195
|
+
|
|
196
|
+
let lastAction = 'Waiting for assignment';
|
|
197
|
+
if (s.status === 'working') lastAction = `Working: ${s.task}`;
|
|
198
|
+
else if (s.status === 'done') lastAction = `Done: ${s.task}`;
|
|
199
|
+
else if (s.status === 'error') lastAction = `Error: ${s.task}`;
|
|
200
|
+
else if (inboxFiles.length > 0) {
|
|
201
|
+
const lastOutput = path.join(INBOX_DIR, inboxFiles[inboxFiles.length - 1]);
|
|
202
|
+
try { lastAction = `Output: ${path.basename(lastOutput)} (${timeSince(fs.statSync(lastOutput).mtimeMs)})`; } catch {}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const chartered = fs.existsSync(path.join(AGENTS_DIR, a.id, 'charter.md'));
|
|
206
|
+
if (lastAction.length > 120) lastAction = lastAction.slice(0, 120) + '...';
|
|
207
|
+
return {
|
|
208
|
+
...a, status: s.status, lastAction,
|
|
209
|
+
currentTask: (s.task || '').slice(0, 200),
|
|
210
|
+
resultSummary: (s.resultSummary || '').slice(0, 500),
|
|
211
|
+
chartered, inboxCount: inboxFiles.length
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getAgentDetail(id) {
|
|
217
|
+
const agentDir = path.join(AGENTS_DIR, id);
|
|
218
|
+
const charter = safeRead(path.join(agentDir, 'charter.md')) || 'No charter found.';
|
|
219
|
+
const history = safeRead(path.join(agentDir, 'history.md')) || 'No history yet.';
|
|
220
|
+
const outputLog = safeRead(path.join(agentDir, 'output.log')) || '';
|
|
221
|
+
|
|
222
|
+
const statusData = getAgentStatus(id); // derives from dispatch.json
|
|
223
|
+
|
|
224
|
+
const inboxContents = safeReadDir(INBOX_DIR)
|
|
225
|
+
.filter(f => f.includes(id))
|
|
226
|
+
.map(f => ({ name: f, content: safeRead(path.join(INBOX_DIR, f)) || '' }));
|
|
227
|
+
|
|
228
|
+
let recentDispatches = [];
|
|
229
|
+
try {
|
|
230
|
+
const dispatch = getDispatch();
|
|
231
|
+
recentDispatches = (dispatch.completed || [])
|
|
232
|
+
.filter(d => d.agent === id)
|
|
233
|
+
.slice(-10)
|
|
234
|
+
.reverse()
|
|
235
|
+
.map(d => ({
|
|
236
|
+
id: d.id, task: d.task || '', type: d.type || '',
|
|
237
|
+
result: d.result || '', reason: d.reason || '',
|
|
238
|
+
completed_at: d.completed_at || '',
|
|
239
|
+
}));
|
|
240
|
+
} catch {}
|
|
241
|
+
|
|
242
|
+
return { charter, history, statusData, outputLog, inboxContents, recentDispatches };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Pull Requests ───────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
function getPrs(project) {
|
|
248
|
+
if (project) return safeJson(projectPrPath(project)) || [];
|
|
249
|
+
const config = getConfig();
|
|
250
|
+
const all = [];
|
|
251
|
+
for (const p of getProjects(config)) all.push(...getPrs(p));
|
|
252
|
+
return all;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getPullRequests(config) {
|
|
256
|
+
config = config || getConfig();
|
|
257
|
+
const projects = getProjects(config);
|
|
258
|
+
const allPrs = [];
|
|
259
|
+
for (const project of projects) {
|
|
260
|
+
const prs = safeJson(projectPrPath(project));
|
|
261
|
+
if (!prs) continue;
|
|
262
|
+
const base = project.prUrlBase || '';
|
|
263
|
+
for (const pr of prs) {
|
|
264
|
+
if (!pr.url && base && pr.id) pr.url = base + String(pr.id).replace('PR-', '');
|
|
265
|
+
pr._project = project.name || 'Project';
|
|
266
|
+
allPrs.push(pr);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
allPrs.sort((a, b) => (b.created || '').localeCompare(a.created || ''));
|
|
270
|
+
return allPrs;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Skills ──────────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
function collectSkillFiles(config) {
|
|
276
|
+
config = config || getConfig();
|
|
277
|
+
const skillFiles = [];
|
|
278
|
+
const seen = new Set(); // dedup by name
|
|
279
|
+
|
|
280
|
+
// 1. Claude Code native skills: ~/.claude/skills/<name>/SKILL.md
|
|
281
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
282
|
+
const claudeSkillsDir = path.join(homeDir, '.claude', 'skills');
|
|
283
|
+
try {
|
|
284
|
+
const dirs = fs.readdirSync(claudeSkillsDir).filter(d => {
|
|
285
|
+
try { return fs.statSync(path.join(claudeSkillsDir, d)).isDirectory(); } catch { return false; }
|
|
286
|
+
});
|
|
287
|
+
for (const d of dirs) {
|
|
288
|
+
const skillFile = path.join(claudeSkillsDir, d, 'SKILL.md');
|
|
289
|
+
if (fs.existsSync(skillFile)) {
|
|
290
|
+
skillFiles.push({ file: 'SKILL.md', dir: path.join(claudeSkillsDir, d), scope: 'claude-code', skillName: d });
|
|
291
|
+
seen.add(d);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} catch {}
|
|
295
|
+
|
|
296
|
+
// 1b. Installed plugin skills: ~/.claude/plugins/installed_plugins.json → cache/<marketplace>/<plugin>/<version>/commands/*.md
|
|
297
|
+
try {
|
|
298
|
+
const pluginsFile = path.join(homeDir, '.claude', 'plugins', 'installed_plugins.json');
|
|
299
|
+
const registry = JSON.parse(safeRead(pluginsFile) || '{}');
|
|
300
|
+
for (const [pluginKey, installs] of Object.entries(registry.plugins || {})) {
|
|
301
|
+
if (!Array.isArray(installs) || installs.length === 0) continue;
|
|
302
|
+
const install = installs[0];
|
|
303
|
+
if (!install.installPath) continue;
|
|
304
|
+
const commandsDir = path.join(install.installPath, 'commands');
|
|
305
|
+
try {
|
|
306
|
+
const commands = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
|
|
307
|
+
for (const cmd of commands) {
|
|
308
|
+
const name = pluginKey.split('@')[0] + ':' + cmd.replace('.md', '');
|
|
309
|
+
if (seen.has(name)) continue;
|
|
310
|
+
skillFiles.push({ file: cmd, dir: commandsDir, scope: 'plugin', skillName: name });
|
|
311
|
+
seen.add(name);
|
|
312
|
+
}
|
|
313
|
+
} catch {}
|
|
314
|
+
}
|
|
315
|
+
} catch {}
|
|
316
|
+
|
|
317
|
+
// 2. Project-specific skills: <project>/.claude/skills/<name>.md or <name>/SKILL.md
|
|
318
|
+
for (const project of getProjects(config)) {
|
|
319
|
+
const projectSkillsDir = path.resolve(project.localPath, '.claude', 'skills');
|
|
320
|
+
try {
|
|
321
|
+
const entries = fs.readdirSync(projectSkillsDir);
|
|
322
|
+
for (const entry of entries) {
|
|
323
|
+
if (entry === 'README.md') continue;
|
|
324
|
+
const entryPath = path.join(projectSkillsDir, entry);
|
|
325
|
+
const stat = fs.statSync(entryPath);
|
|
326
|
+
if (stat.isDirectory()) {
|
|
327
|
+
const skillFile = path.join(entryPath, 'SKILL.md');
|
|
328
|
+
if (fs.existsSync(skillFile)) {
|
|
329
|
+
skillFiles.push({ file: 'SKILL.md', dir: entryPath, scope: 'project', projectName: project.name, skillName: entry });
|
|
330
|
+
}
|
|
331
|
+
} else if (entry.endsWith('.md')) {
|
|
332
|
+
skillFiles.push({ file: entry, dir: projectSkillsDir, scope: 'project', projectName: project.name });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch {}
|
|
336
|
+
}
|
|
337
|
+
return skillFiles;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function getSkills(config) {
|
|
341
|
+
const all = [];
|
|
342
|
+
for (const { file: f, dir, scope, projectName, skillName } of collectSkillFiles(config)) {
|
|
343
|
+
try {
|
|
344
|
+
const content = safeRead(path.join(dir, f)) || '';
|
|
345
|
+
const meta = parseSkillFrontmatter(content, skillName || f);
|
|
346
|
+
if (scope === 'project' && meta.project === 'any') meta.project = projectName;
|
|
347
|
+
// Check if auto-generated by an agent
|
|
348
|
+
const isAutoGenerated = content.includes('Auto-extracted') || content.includes('author:') || content.includes('createdBy:');
|
|
349
|
+
all.push({
|
|
350
|
+
...meta, file: f, dir: dir.replace(/\\/g, '/'),
|
|
351
|
+
source: scope === 'claude-code' ? 'claude-code' : scope === 'plugin' ? 'plugin' : scope === 'project' ? 'project:' + projectName : 'minions',
|
|
352
|
+
scope,
|
|
353
|
+
autoGenerated: isAutoGenerated,
|
|
354
|
+
});
|
|
355
|
+
} catch {}
|
|
356
|
+
}
|
|
357
|
+
return all;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function getSkillIndex(config) {
|
|
361
|
+
try {
|
|
362
|
+
const skillFiles = collectSkillFiles(config);
|
|
363
|
+
if (skillFiles.length === 0) return '';
|
|
364
|
+
|
|
365
|
+
let index = '## Available Minions Skills\n\n';
|
|
366
|
+
index += 'These are reusable workflows discovered by agents. Follow them when the trigger matches your task.\n\n';
|
|
367
|
+
|
|
368
|
+
for (const { file: f, dir, scope, projectName } of skillFiles) {
|
|
369
|
+
const content = safeRead(path.join(dir, f));
|
|
370
|
+
const meta = parseSkillFrontmatter(content, f);
|
|
371
|
+
index += `### ${meta.name}`;
|
|
372
|
+
if (scope === 'project') index += ` (${projectName})`;
|
|
373
|
+
index += '\n';
|
|
374
|
+
if (meta.description) index += `${meta.description}\n`;
|
|
375
|
+
if (meta.trigger) index += `**When:** ${meta.trigger}\n`;
|
|
376
|
+
if (meta.project !== 'any') index += `**Project:** ${meta.project}\n`;
|
|
377
|
+
index += `**File:** \`${dir}/${f}\`\n`;
|
|
378
|
+
index += `Read the full skill file before following the steps.\n\n`;
|
|
379
|
+
}
|
|
380
|
+
return index;
|
|
381
|
+
} catch { return ''; }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Knowledge Base ──────────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
let _kbCache = null;
|
|
387
|
+
let _kbCacheTs = 0;
|
|
388
|
+
const KB_CACHE_TTL = 30000; // 30s — KB changes infrequently
|
|
389
|
+
|
|
390
|
+
function getKnowledgeBaseEntries() {
|
|
391
|
+
const now = Date.now();
|
|
392
|
+
if (_kbCache && (now - _kbCacheTs) < KB_CACHE_TTL) return _kbCache;
|
|
393
|
+
|
|
394
|
+
const entries = [];
|
|
395
|
+
for (const cat of KB_CATEGORIES) {
|
|
396
|
+
const catDir = path.join(KNOWLEDGE_DIR, cat);
|
|
397
|
+
const files = safeReadDir(catDir).filter(f => f.endsWith('.md'));
|
|
398
|
+
for (const f of files) {
|
|
399
|
+
const content = safeRead(path.join(catDir, f)) || '';
|
|
400
|
+
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
401
|
+
const title = titleMatch ? titleMatch[1].trim() : f.replace(/\.md$/, '');
|
|
402
|
+
const agentMatch = f.match(/^\d{4}-\d{2}-\d{2}-(\w+)-/);
|
|
403
|
+
const dateMatch = f.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
404
|
+
entries.push({
|
|
405
|
+
cat, file: f, title,
|
|
406
|
+
agent: agentMatch ? agentMatch[1] : '',
|
|
407
|
+
date: dateMatch ? dateMatch[1] : '',
|
|
408
|
+
preview: content.slice(0, 200),
|
|
409
|
+
size: content.length,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
_kbCache = entries;
|
|
414
|
+
_kbCacheTs = now;
|
|
415
|
+
return entries;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function getKnowledgeBaseIndex() {
|
|
419
|
+
try {
|
|
420
|
+
const entries = getKnowledgeBaseEntries();
|
|
421
|
+
if (entries.length === 0) return '';
|
|
422
|
+
let index = '## Knowledge Base Reference\n\n';
|
|
423
|
+
index += 'Deep-reference docs from past work. Read the file if you need detail.\n\n';
|
|
424
|
+
for (const e of entries) {
|
|
425
|
+
index += `- \`knowledge/${e.cat}/${e.file}\` \u2014 ${e.title}\n`;
|
|
426
|
+
}
|
|
427
|
+
return index + '\n';
|
|
428
|
+
} catch { return ''; }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Work Items ──────────────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
function getWorkItems(config) {
|
|
434
|
+
config = config || getConfig();
|
|
435
|
+
const projects = getProjects(config);
|
|
436
|
+
const allItems = [];
|
|
437
|
+
|
|
438
|
+
// Central work items
|
|
439
|
+
const centralData = safeRead(path.join(MINIONS_DIR, 'work-items.json'));
|
|
440
|
+
if (centralData) {
|
|
441
|
+
try {
|
|
442
|
+
for (const item of JSON.parse(centralData)) {
|
|
443
|
+
item._source = 'central';
|
|
444
|
+
allItems.push(item);
|
|
445
|
+
}
|
|
446
|
+
} catch {}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Per-project work items
|
|
450
|
+
for (const project of projects) {
|
|
451
|
+
const data = safeRead(projectWorkItemsPath(project));
|
|
452
|
+
if (data) {
|
|
453
|
+
try {
|
|
454
|
+
for (const item of JSON.parse(data)) {
|
|
455
|
+
item._source = project.name || 'project';
|
|
456
|
+
allItems.push(item);
|
|
457
|
+
}
|
|
458
|
+
} catch {}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Cross-reference with PRs
|
|
463
|
+
const allPrs = getPullRequests(config);
|
|
464
|
+
const dispatch = getDispatch();
|
|
465
|
+
for (const item of allItems) {
|
|
466
|
+
if (item._pr && !item._prUrl) {
|
|
467
|
+
const prId = String(item._pr).replace('PR-', '');
|
|
468
|
+
const pr = allPrs.find(p => String(p.id).includes(prId));
|
|
469
|
+
if (pr) item._prUrl = pr.url;
|
|
470
|
+
}
|
|
471
|
+
if (!item._pr) {
|
|
472
|
+
const prLinks = shared.getPrLinks();
|
|
473
|
+
const linkedPrId = Object.entries(prLinks).find(([, v]) => v === item.id)?.[0];
|
|
474
|
+
if (linkedPrId) {
|
|
475
|
+
item._pr = linkedPrId;
|
|
476
|
+
const linkedPr = allPrs.find(p => p.id === linkedPrId);
|
|
477
|
+
if (linkedPr) item._prUrl = linkedPr.url;
|
|
478
|
+
} else {
|
|
479
|
+
// no further fallback — pr-links.json and wi._pr are the sources of truth
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const statusOrder = {
|
|
485
|
+
pending: 0,
|
|
486
|
+
queued: 0,
|
|
487
|
+
dispatched: 1,
|
|
488
|
+
'in-pr': 3, // backward compat — treated as done
|
|
489
|
+
done: 3,
|
|
490
|
+
implemented: 3,
|
|
491
|
+
failed: 4,
|
|
492
|
+
paused: 5,
|
|
493
|
+
};
|
|
494
|
+
allItems.sort((a, b) => {
|
|
495
|
+
const sa = statusOrder[a.status] ?? 1;
|
|
496
|
+
const sb = statusOrder[b.status] ?? 1;
|
|
497
|
+
if (sa !== sb) return sa - sb;
|
|
498
|
+
return (b.created || '').localeCompare(a.created || '');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
return allItems;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ── PRD Progress ────────────────────────────────────────────────────────────
|
|
505
|
+
|
|
506
|
+
function getPrdInfo(config) {
|
|
507
|
+
config = config || getConfig();
|
|
508
|
+
const projects = getProjects(config);
|
|
509
|
+
let allPrdItems = [];
|
|
510
|
+
let latestStat = null;
|
|
511
|
+
|
|
512
|
+
// Scan active PRDs and archived PRDs (completed PRDs still need to show progress)
|
|
513
|
+
const planDirs = [
|
|
514
|
+
{ dir: PRD_DIR, archived: false },
|
|
515
|
+
{ dir: path.join(PRD_DIR, 'archive'), archived: true },
|
|
516
|
+
];
|
|
517
|
+
for (const { dir, archived } of planDirs) {
|
|
518
|
+
try {
|
|
519
|
+
const planFiles = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
520
|
+
for (const pf of planFiles) {
|
|
521
|
+
try {
|
|
522
|
+
const plan = JSON.parse(fs.readFileSync(path.join(dir, pf), 'utf8'));
|
|
523
|
+
if (!plan.missing_features) continue;
|
|
524
|
+
const stat = fs.statSync(path.join(dir, pf));
|
|
525
|
+
if (!latestStat || stat.mtimeMs > latestStat.mtimeMs) latestStat = stat;
|
|
526
|
+
// Staleness: compare source plan mtime to recorded sourcePlanModifiedAt
|
|
527
|
+
let planStale = false;
|
|
528
|
+
if (!archived && plan.source_plan) {
|
|
529
|
+
try {
|
|
530
|
+
const sourceMtime = Math.floor(fs.statSync(path.join(PLANS_DIR, plan.source_plan)).mtimeMs);
|
|
531
|
+
const recorded = plan.sourcePlanModifiedAt ? new Date(plan.sourcePlanModifiedAt).getTime() : null;
|
|
532
|
+
if (recorded && sourceMtime > recorded) planStale = true;
|
|
533
|
+
} catch {}
|
|
534
|
+
}
|
|
535
|
+
for (const f of plan.missing_features) {
|
|
536
|
+
allPrdItems.push({
|
|
537
|
+
...f, _source: pf, _planStatus: plan.status || 'active',
|
|
538
|
+
_planSummary: plan.plan_summary || pf, _planProject: plan.project || '',
|
|
539
|
+
_archived: archived, _sourcePlan: plan.source_plan || '',
|
|
540
|
+
_planStale: planStale || plan.planStale || false, _lastSyncedFromPlan: plan.lastSyncedFromPlan || null,
|
|
541
|
+
_prdUpdatedAt: new Date(stat.mtimeMs).toISOString(),
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
} catch {}
|
|
545
|
+
}
|
|
546
|
+
} catch {}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (allPrdItems.length === 0) return { progress: null, status: null };
|
|
550
|
+
|
|
551
|
+
const items = allPrdItems;
|
|
552
|
+
const total = items.length;
|
|
553
|
+
|
|
554
|
+
// Build work item lookup — work item ID = PRD item ID
|
|
555
|
+
const wiById = {};
|
|
556
|
+
for (const project of projects) {
|
|
557
|
+
try {
|
|
558
|
+
const workItems = safeJson(projectWorkItemsPath(project)) || [];
|
|
559
|
+
for (const wi of workItems) { if (wi.sourcePlan) wiById[wi.id] = wi; }
|
|
560
|
+
} catch {}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// PR-to-PRD linking — primary source is pr-links.json (single-writer, never clobbered by polling)
|
|
564
|
+
const allPrs = getPullRequests(config);
|
|
565
|
+
const prById = {};
|
|
566
|
+
for (const pr of allPrs) prById[pr.id] = pr;
|
|
567
|
+
|
|
568
|
+
const prdToPr = {};
|
|
569
|
+
const prLinks = shared.getPrLinks(); // { "PR-xxxx": "P-xxxx" }
|
|
570
|
+
for (const [prId, itemId] of Object.entries(prLinks)) {
|
|
571
|
+
const pr = prById[prId];
|
|
572
|
+
const project = projects.find(p => p.name === pr?._project) || projects[0];
|
|
573
|
+
const url = pr?.url || (project?.prUrlBase ? project.prUrlBase + prId.replace('PR-', '') : '');
|
|
574
|
+
if (!prdToPr[itemId]) prdToPr[itemId] = [];
|
|
575
|
+
prdToPr[itemId].push({ id: prId, url, title: pr?.title || '', status: pr?.status || 'active', _project: pr?._project || '' });
|
|
576
|
+
}
|
|
577
|
+
// Fallback: work item _pr field for anything still missing
|
|
578
|
+
for (const wi of Object.values(wiById)) {
|
|
579
|
+
if (!wi._pr || prdToPr[wi.id]?.length) continue;
|
|
580
|
+
const pr = prById[wi._pr];
|
|
581
|
+
const project = projects.find(p => p.name === wi.project || p.name === wi._source);
|
|
582
|
+
const url = pr?.url || (project?.prUrlBase ? project.prUrlBase + wi._pr.replace('PR-', '') : '');
|
|
583
|
+
prdToPr[wi.id] = [{ id: wi._pr, url, title: pr?.title || '', status: pr?.status || 'active', _project: project?.name || '' }];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// PRD JSON status is the source of truth — kept in sync with work item by syncPrdItemStatus.
|
|
587
|
+
// Map from PRD JSON values to display values (dispatched → in-progress etc.)
|
|
588
|
+
// Augment each item with execution metadata from the work item.
|
|
589
|
+
const statusDisplay = { dispatched: 'in-progress', pending: 'missing' };
|
|
590
|
+
for (const item of items) {
|
|
591
|
+
const wi = wiById[item.id];
|
|
592
|
+
item.status = statusDisplay[item.status] || item.status || 'missing';
|
|
593
|
+
// Attach execution metadata for display (agent, PR link, fail reason)
|
|
594
|
+
if (wi) {
|
|
595
|
+
if (wi.dispatched_to) item._agent = wi.dispatched_to;
|
|
596
|
+
if (wi.failReason) item._failReason = wi.failReason;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const byStatus = {};
|
|
601
|
+
items.forEach(item => { const s = item.status || 'missing'; byStatus[s] = byStatus[s] || []; byStatus[s].push(item); });
|
|
602
|
+
const complete = (byStatus['done'] || []).length + (byStatus['in-pr'] || []).length; // in-pr counted as done for backward compat
|
|
603
|
+
const inProgress = (byStatus['in-progress'] || []).length;
|
|
604
|
+
const paused = (byStatus['paused'] || []).length;
|
|
605
|
+
const missing = (byStatus['missing'] || []).length;
|
|
606
|
+
const donePercent = total > 0 ? Math.round((complete / total) * 100) : 0;
|
|
607
|
+
|
|
608
|
+
// Plan timings
|
|
609
|
+
const planTimings = {};
|
|
610
|
+
for (const project of projects) {
|
|
611
|
+
try {
|
|
612
|
+
const workItems = safeJson(projectWorkItemsPath(project)) || [];
|
|
613
|
+
for (const wi of workItems) {
|
|
614
|
+
if (!wi.sourcePlan) continue;
|
|
615
|
+
if (!planTimings[wi.sourcePlan]) planTimings[wi.sourcePlan] = { firstDispatched: null, lastCompleted: null, allDone: true };
|
|
616
|
+
const t = planTimings[wi.sourcePlan];
|
|
617
|
+
if (wi.dispatched_at) { const d = new Date(wi.dispatched_at).getTime(); if (!t.firstDispatched || d < t.firstDispatched) t.firstDispatched = d; }
|
|
618
|
+
if (wi.completedAt) { const c = new Date(wi.completedAt).getTime(); if (!t.lastCompleted || c > t.lastCompleted) t.lastCompleted = c; }
|
|
619
|
+
if (wi.status !== 'done' && wi.status !== 'in-pr') t.allDone = false; // in-pr treated as done for backward compat
|
|
620
|
+
}
|
|
621
|
+
} catch {}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const progress = {
|
|
625
|
+
total, complete, inProgress, paused, missing, donePercent, planTimings,
|
|
626
|
+
items: items.map(i => ({
|
|
627
|
+
id: i.id, name: i.name || i.title, priority: i.priority,
|
|
628
|
+
complexity: i.estimated_complexity || i.size, status: i.status || 'missing',
|
|
629
|
+
description: (i.description || '').slice(0, 200), projects: i.projects || [],
|
|
630
|
+
prs: prdToPr[i.id] || [], depends_on: i.depends_on || [],
|
|
631
|
+
project: i.project || '', source: i._source || '', planSummary: i._planSummary || '', planProject: i._planProject || '', planStatus: i._planStatus || 'active', _archived: i._archived || false, sourcePlan: i._sourcePlan || '',
|
|
632
|
+
planStale: i._planStale || false, lastSyncedFromPlan: i._lastSyncedFromPlan || null, prdUpdatedAt: i._prdUpdatedAt || null,
|
|
633
|
+
})),
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const status = {
|
|
637
|
+
exists: true, age: latestStat ? timeSince(latestStat.mtimeMs) : 'unknown',
|
|
638
|
+
existing: 0, missing: items.filter(i => i.status === 'missing').length, questions: 0, summary: '',
|
|
639
|
+
missingList: items.filter(i => i.status === 'missing').map(f => ({ id: f.id, name: f.name || f.title, priority: f.priority, complexity: f.estimated_complexity || f.size })),
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
return { progress, status };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ── Exports ─────────────────────────────────────────────────────────────────
|
|
646
|
+
|
|
647
|
+
module.exports = {
|
|
648
|
+
// Paths (for modules that need direct access)
|
|
649
|
+
MINIONS_DIR, AGENTS_DIR, ENGINE_DIR, INBOX_DIR, PLANS_DIR, PRD_DIR, SKILLS_DIR, KNOWLEDGE_DIR, ARCHIVE_DIR,
|
|
650
|
+
CONFIG_PATH, CONTROL_PATH, DISPATCH_PATH, LOG_PATH, NOTES_PATH,
|
|
651
|
+
|
|
652
|
+
// Helpers
|
|
653
|
+
timeSince,
|
|
654
|
+
|
|
655
|
+
// Core state
|
|
656
|
+
getConfig, getControl, getDispatch, getDispatchQueue,
|
|
657
|
+
getNotes, getNotesWithMeta, getEngineLog, getMetrics,
|
|
658
|
+
|
|
659
|
+
// Inbox
|
|
660
|
+
getInboxFiles, getInbox,
|
|
661
|
+
|
|
662
|
+
// Agents
|
|
663
|
+
getAgentStatus, getAgentCharter, getAgents, getAgentDetail,
|
|
664
|
+
|
|
665
|
+
// Pull requests
|
|
666
|
+
getPrs, getPullRequests,
|
|
667
|
+
|
|
668
|
+
// Skills
|
|
669
|
+
collectSkillFiles, getSkills, getSkillIndex,
|
|
670
|
+
|
|
671
|
+
// Knowledge base
|
|
672
|
+
getKnowledgeBaseEntries, getKnowledgeBaseIndex,
|
|
673
|
+
|
|
674
|
+
// Work items & PRD
|
|
675
|
+
getWorkItems, getPrdInfo,
|
|
676
|
+
};
|
|
677
|
+
|