@yemi33/minions 0.1.1777 → 0.1.1778
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 +5 -0
- package/dashboard.js +214 -100
- package/engine/copilot-models.json +1 -1
- package/engine/shared.js +22 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dashboard.js
CHANGED
|
@@ -34,7 +34,7 @@ const projectDiscovery = require('./engine/project-discovery');
|
|
|
34
34
|
const features = require('./engine/features');
|
|
35
35
|
const os = require('os');
|
|
36
36
|
|
|
37
|
-
const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeJsonNoRestore, safeUnlink, mutateJsonFileLocked, mutateControl, mutateCooldowns, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS, WORK_TYPE, reopenWorkItem } = shared;
|
|
37
|
+
const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeJsonNoRestore, safeUnlink, mutateJsonFileLocked, mutateTextFileLocked, mutateControl, mutateCooldowns, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS, WORK_TYPE, reopenWorkItem } = shared;
|
|
38
38
|
const { getAgents, getAgentDetail, getPrdInfo, getWorkItems, getDispatchQueue,
|
|
39
39
|
getSkills, getInbox, getNotesWithMeta, getPullRequests,
|
|
40
40
|
getEngineLog, getMetrics, getKnowledgeBaseEntries, timeSince,
|
|
@@ -61,6 +61,10 @@ const { getAgents, getAgentDetail, getPrdInfo, getWorkItems, getDispatchQueue,
|
|
|
61
61
|
const PORT = parseInt(process.env.PORT || process.argv[2]) || 7331;
|
|
62
62
|
let CONFIG = queries.getConfig();
|
|
63
63
|
let PROJECTS = _getProjects(CONFIG);
|
|
64
|
+
const CONFIG_PATH = path.join(MINIONS_DIR, 'config.json');
|
|
65
|
+
const PINNED_PATH = path.join(MINIONS_DIR, 'pinned.md');
|
|
66
|
+
const PINNED_DEFAULT_CONTENT = '# Pinned Context\n\nCritical notes visible to all agents.';
|
|
67
|
+
const KB_PINS_PATH = shared.PINNED_ITEMS_PATH;
|
|
64
68
|
const DASHBOARD_BROWSER_PRESENCE_PATH = path.join(ENGINE_DIR, 'dashboard-browser.json');
|
|
65
69
|
const DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS = 45000;
|
|
66
70
|
|
|
@@ -93,6 +97,89 @@ function reloadConfig() {
|
|
|
93
97
|
}
|
|
94
98
|
ensureConfiguredProjectStateFiles();
|
|
95
99
|
|
|
100
|
+
function mutateDashboardConfig(mutator) {
|
|
101
|
+
return mutateJsonFileLocked(CONFIG_PATH, (config) => {
|
|
102
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) config = {};
|
|
103
|
+
const next = mutator(config);
|
|
104
|
+
return next === undefined ? config : next;
|
|
105
|
+
}, { defaultValue: { projects: [], agents: {}, engine: {} }, skipWriteIfUnchanged: true });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
|
|
109
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) current = {};
|
|
110
|
+
if (body.engine) {
|
|
111
|
+
if (!current.engine || typeof current.engine !== 'object' || Array.isArray(current.engine)) current.engine = {};
|
|
112
|
+
const enginePatch = patch.engine || {};
|
|
113
|
+
for (const key of enginePatch.delete || []) delete current.engine[key];
|
|
114
|
+
for (const [key, value] of Object.entries(enginePatch.set || {})) current.engine[key] = value;
|
|
115
|
+
}
|
|
116
|
+
if (body.claude) {
|
|
117
|
+
if (candidate.claude) current.claude = candidate.claude;
|
|
118
|
+
else delete current.claude;
|
|
119
|
+
}
|
|
120
|
+
if (body.agents) {
|
|
121
|
+
if (!current.agents || typeof current.agents !== 'object' || Array.isArray(current.agents)) current.agents = {};
|
|
122
|
+
for (const id of Object.keys(body.agents)) {
|
|
123
|
+
if (candidate.agents && candidate.agents[id]) current.agents[id] = candidate.agents[id];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (body.teams) {
|
|
127
|
+
if (candidate.teams) current.teams = candidate.teams;
|
|
128
|
+
else delete current.teams;
|
|
129
|
+
}
|
|
130
|
+
if (body.projects && Array.isArray(body.projects)) {
|
|
131
|
+
if (!Array.isArray(current.projects)) current.projects = [];
|
|
132
|
+
for (const update of body.projects) {
|
|
133
|
+
const candidateProject = (candidate.projects || []).find(p => p.name === update.name);
|
|
134
|
+
const currentProject = current.projects.find(p => p.name === update.name);
|
|
135
|
+
if (!candidateProject || !currentProject) continue;
|
|
136
|
+
currentProject.workSources = candidateProject.workSources;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
shared.pruneDefaultClaudeConfig(current);
|
|
140
|
+
return current;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function addPinnedEntryLocked({ title, content, level }, now = new Date()) {
|
|
144
|
+
const levelTag = level === 'critical' ? '🔴 ' : level === 'warning' ? '🟡 ' : '';
|
|
145
|
+
const entry = '\n\n### ' + levelTag + title + '\n\n' + content + '\n\n*Pinned by human on ' + now.toISOString().slice(0, 10) + '*';
|
|
146
|
+
return mutateTextFileLocked(PINNED_PATH, existing => (existing || PINNED_DEFAULT_CONTENT) + entry, { defaultValue: PINNED_DEFAULT_CONTENT });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function removePinnedEntryLocked(title) {
|
|
150
|
+
let missing = false;
|
|
151
|
+
mutateTextFileLocked(PINNED_PATH, content => {
|
|
152
|
+
if (!content) {
|
|
153
|
+
missing = true;
|
|
154
|
+
return content;
|
|
155
|
+
}
|
|
156
|
+
const regex = new RegExp('\\n\\n###\\s*(?:🔴\\s*|🟡\\s*)?' + title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\n[\\s\\S]*?(?=\\n\\n###|$)', 'i');
|
|
157
|
+
return content.replace(regex, '');
|
|
158
|
+
}, { defaultValue: '', skipWriteIfUnchanged: true });
|
|
159
|
+
return !missing;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function setKbPinsLocked(pins) {
|
|
163
|
+
return mutateJsonFileLocked(KB_PINS_PATH, () => pins, { defaultValue: [], skipWriteIfUnchanged: true });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function toggleKbPinLocked(key) {
|
|
167
|
+
let pinned = false;
|
|
168
|
+
mutateJsonFileLocked(KB_PINS_PATH, pins => {
|
|
169
|
+
if (!Array.isArray(pins)) pins = [];
|
|
170
|
+
const idx = pins.indexOf(key);
|
|
171
|
+
if (idx >= 0) {
|
|
172
|
+
pins.splice(idx, 1);
|
|
173
|
+
pinned = false;
|
|
174
|
+
} else {
|
|
175
|
+
pins.unshift(key);
|
|
176
|
+
pinned = true;
|
|
177
|
+
}
|
|
178
|
+
return pins;
|
|
179
|
+
}, { defaultValue: [], skipWriteIfUnchanged: true });
|
|
180
|
+
return pinned;
|
|
181
|
+
}
|
|
182
|
+
|
|
96
183
|
function getWorkItemIdFromPrLinkContext(context, workItemId) {
|
|
97
184
|
if (typeof workItemId === 'string' && workItemId.trim()) return workItemId.trim();
|
|
98
185
|
if (!context) return null;
|
|
@@ -995,7 +1082,7 @@ function getStatus() {
|
|
|
995
1082
|
});
|
|
996
1083
|
})(),
|
|
997
1084
|
pipelines: (() => { try { const pl = require('./engine/pipeline'); return pl.getPipelines().map(p => ({ ...p, runs: (pl.getPipelineRuns()[p.id] || []).slice(-5) })); } catch { return []; } })(),
|
|
998
|
-
pinned: (() => { try { return parsePinnedEntries(safeRead(
|
|
1085
|
+
pinned: (() => { try { return parsePinnedEntries(safeRead(PINNED_PATH)); } catch { return []; } })(),
|
|
999
1086
|
projects: PROJECTS.map(p => ({ name: p.name, path: p.localPath, description: p.description || '' })),
|
|
1000
1087
|
autoMode: {
|
|
1001
1088
|
approvePlans: !!CONFIG.engine?.autoApprovePlans,
|
|
@@ -5734,13 +5821,15 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5734
5821
|
});
|
|
5735
5822
|
}
|
|
5736
5823
|
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5824
|
+
// Check if already linked under the config lock so concurrent dashboard
|
|
5825
|
+
// adds cannot both pass the preflight check and then clobber config.json.
|
|
5826
|
+
let alreadyLinked = false;
|
|
5827
|
+
mutateDashboardConfig(config => {
|
|
5828
|
+
if (!Array.isArray(config.projects)) config.projects = [];
|
|
5829
|
+
alreadyLinked = config.projects.some(p => path.resolve(p.localPath) === target);
|
|
5830
|
+
return config;
|
|
5831
|
+
});
|
|
5832
|
+
if (alreadyLinked) {
|
|
5744
5833
|
return jsonReply(res, 400, { error: 'Project already linked at ' + target });
|
|
5745
5834
|
}
|
|
5746
5835
|
|
|
@@ -5779,8 +5868,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5779
5868
|
// .minions state without leaving repo-local state files behind.
|
|
5780
5869
|
shared.ensureProjectStateFiles(project, { migrateLegacy: true, removeLegacy: true });
|
|
5781
5870
|
|
|
5782
|
-
|
|
5783
|
-
|
|
5871
|
+
let duplicate = false;
|
|
5872
|
+
mutateDashboardConfig(config => {
|
|
5873
|
+
if (!Array.isArray(config.projects)) config.projects = [];
|
|
5874
|
+
if (config.projects.some(p => path.resolve(p.localPath) === target)) {
|
|
5875
|
+
duplicate = true;
|
|
5876
|
+
return config;
|
|
5877
|
+
}
|
|
5878
|
+
config.projects.push(project);
|
|
5879
|
+
return config;
|
|
5880
|
+
});
|
|
5881
|
+
if (duplicate) return jsonReply(res, 400, { error: 'Project already linked at ' + target });
|
|
5784
5882
|
reloadConfig(); // Update in-memory project list immediately
|
|
5785
5883
|
invalidateStatusCache();
|
|
5786
5884
|
|
|
@@ -6358,24 +6456,25 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6358
6456
|
if (!id) id = 'schedule';
|
|
6359
6457
|
}
|
|
6360
6458
|
|
|
6459
|
+
let sched;
|
|
6460
|
+
mutateDashboardConfig(config => {
|
|
6461
|
+
if (!Array.isArray(config.schedules)) config.schedules = [];
|
|
6462
|
+
// If auto-generated ID collides, append a short numeric suffix
|
|
6463
|
+
let scheduleId = id;
|
|
6464
|
+
if (config.schedules.some(s => s.id === scheduleId)) {
|
|
6465
|
+
let suffix = 2;
|
|
6466
|
+
while (config.schedules.some(s => s.id === `${scheduleId}-${suffix}`)) suffix++;
|
|
6467
|
+
scheduleId = `${scheduleId}-${suffix}`;
|
|
6468
|
+
}
|
|
6469
|
+
sched = { id: scheduleId, cron, title, type: type || 'implement', enabled: enabled !== false };
|
|
6470
|
+
if (project) sched.project = project;
|
|
6471
|
+
if (agent) sched.agent = agent;
|
|
6472
|
+
if (description) sched.description = description;
|
|
6473
|
+
if (priority) sched.priority = priority;
|
|
6474
|
+
config.schedules.push(sched);
|
|
6475
|
+
return config;
|
|
6476
|
+
});
|
|
6361
6477
|
reloadConfig();
|
|
6362
|
-
if (!CONFIG.schedules) CONFIG.schedules = [];
|
|
6363
|
-
|
|
6364
|
-
// If auto-generated ID collides, append a short numeric suffix
|
|
6365
|
-
if (CONFIG.schedules.some(s => s.id === id)) {
|
|
6366
|
-
let suffix = 2;
|
|
6367
|
-
while (CONFIG.schedules.some(s => s.id === `${id}-${suffix}`)) suffix++;
|
|
6368
|
-
id = `${id}-${suffix}`;
|
|
6369
|
-
}
|
|
6370
|
-
|
|
6371
|
-
const sched = { id, cron, title, type: type || 'implement', enabled: enabled !== false };
|
|
6372
|
-
if (project) sched.project = project;
|
|
6373
|
-
if (agent) sched.agent = agent;
|
|
6374
|
-
if (description) sched.description = description;
|
|
6375
|
-
if (priority) sched.priority = priority;
|
|
6376
|
-
|
|
6377
|
-
CONFIG.schedules.push(sched);
|
|
6378
|
-
safeWrite(path.join(MINIONS_DIR, 'config.json'), CONFIG);
|
|
6379
6478
|
invalidateStatusCache();
|
|
6380
6479
|
return jsonReply(res, 200, { ok: true, schedule: sched });
|
|
6381
6480
|
}
|
|
@@ -6385,21 +6484,28 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6385
6484
|
const { id, cron, title, type, project, agent, description, priority, enabled } = body;
|
|
6386
6485
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
6387
6486
|
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6487
|
+
let missingSchedules = false;
|
|
6488
|
+
let sched = null;
|
|
6489
|
+
mutateDashboardConfig(config => {
|
|
6490
|
+
if (!Array.isArray(config.schedules)) {
|
|
6491
|
+
missingSchedules = true;
|
|
6492
|
+
return config;
|
|
6493
|
+
}
|
|
6494
|
+
sched = config.schedules.find(s => s.id === id);
|
|
6495
|
+
if (!sched) return config;
|
|
6496
|
+
if (cron !== undefined) sched.cron = cron;
|
|
6497
|
+
if (title !== undefined) sched.title = title;
|
|
6498
|
+
if (type !== undefined) sched.type = type;
|
|
6499
|
+
if (project !== undefined) sched.project = project || null;
|
|
6500
|
+
if (agent !== undefined) sched.agent = agent || null;
|
|
6501
|
+
if (description !== undefined) sched.description = description;
|
|
6502
|
+
if (priority !== undefined) sched.priority = priority;
|
|
6503
|
+
if (enabled !== undefined) sched.enabled = enabled;
|
|
6504
|
+
return config;
|
|
6505
|
+
});
|
|
6506
|
+
if (missingSchedules) return jsonReply(res, 404, { error: 'No schedules configured' });
|
|
6391
6507
|
if (!sched) return jsonReply(res, 404, { error: 'Schedule not found' });
|
|
6392
|
-
|
|
6393
|
-
if (cron !== undefined) sched.cron = cron;
|
|
6394
|
-
if (title !== undefined) sched.title = title;
|
|
6395
|
-
if (type !== undefined) sched.type = type;
|
|
6396
|
-
if (project !== undefined) sched.project = project || null;
|
|
6397
|
-
if (agent !== undefined) sched.agent = agent || null;
|
|
6398
|
-
if (description !== undefined) sched.description = description;
|
|
6399
|
-
if (priority !== undefined) sched.priority = priority;
|
|
6400
|
-
if (enabled !== undefined) sched.enabled = enabled;
|
|
6401
|
-
|
|
6402
|
-
safeWrite(path.join(MINIONS_DIR, 'config.json'), CONFIG);
|
|
6508
|
+
reloadConfig();
|
|
6403
6509
|
invalidateStatusCache();
|
|
6404
6510
|
return jsonReply(res, 200, { ok: true, schedule: sched });
|
|
6405
6511
|
}
|
|
@@ -6409,13 +6515,22 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6409
6515
|
const { id } = body;
|
|
6410
6516
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
6411
6517
|
|
|
6518
|
+
let missingSchedules = false;
|
|
6519
|
+
let deleted = false;
|
|
6520
|
+
mutateDashboardConfig(config => {
|
|
6521
|
+
if (!Array.isArray(config.schedules)) {
|
|
6522
|
+
missingSchedules = true;
|
|
6523
|
+
return config;
|
|
6524
|
+
}
|
|
6525
|
+
const idx = config.schedules.findIndex(s => s.id === id);
|
|
6526
|
+
if (idx < 0) return config;
|
|
6527
|
+
config.schedules.splice(idx, 1);
|
|
6528
|
+
deleted = true;
|
|
6529
|
+
return config;
|
|
6530
|
+
});
|
|
6531
|
+
if (missingSchedules) return jsonReply(res, 404, { error: 'No schedules configured' });
|
|
6532
|
+
if (!deleted) return jsonReply(res, 404, { error: 'Schedule not found' });
|
|
6412
6533
|
reloadConfig();
|
|
6413
|
-
if (!CONFIG.schedules) return jsonReply(res, 404, { error: 'No schedules configured' });
|
|
6414
|
-
const idx = CONFIG.schedules.findIndex(s => s.id === id);
|
|
6415
|
-
if (idx < 0) return jsonReply(res, 404, { error: 'Schedule not found' });
|
|
6416
|
-
|
|
6417
|
-
CONFIG.schedules.splice(idx, 1);
|
|
6418
|
-
safeWrite(path.join(MINIONS_DIR, 'config.json'), CONFIG);
|
|
6419
6534
|
invalidateStatusCache();
|
|
6420
6535
|
return jsonReply(res, 200, { ok: true });
|
|
6421
6536
|
}
|
|
@@ -6588,8 +6703,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6588
6703
|
async function handleSettingsUpdate(req, res) {
|
|
6589
6704
|
try {
|
|
6590
6705
|
const body = await readBody(req);
|
|
6591
|
-
const
|
|
6592
|
-
const config = safeJson(configPath) || {};
|
|
6706
|
+
const config = safeJson(CONFIG_PATH) || {};
|
|
6593
6707
|
if (!config.engine) config.engine = {};
|
|
6594
6708
|
if (!config.agents) config.agents = {};
|
|
6595
6709
|
shared.pruneDefaultClaudeConfig(config);
|
|
@@ -6597,6 +6711,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6597
6711
|
const _clamped = [];
|
|
6598
6712
|
const _engineModelDiscovery = require('./engine/model-discovery');
|
|
6599
6713
|
const _engineRuntimes = require('./engine/runtimes');
|
|
6714
|
+
const _configPatch = { engine: { set: {}, delete: new Set() } };
|
|
6715
|
+
function _setEngineConfig(key, value) {
|
|
6716
|
+
config.engine[key] = value;
|
|
6717
|
+
_configPatch.engine.set[key] = value;
|
|
6718
|
+
_configPatch.engine.delete.delete(key);
|
|
6719
|
+
}
|
|
6720
|
+
function _deleteEngineConfig(key) {
|
|
6721
|
+
delete config.engine[key];
|
|
6722
|
+
delete _configPatch.engine.set[key];
|
|
6723
|
+
_configPatch.engine.delete.add(key);
|
|
6724
|
+
}
|
|
6600
6725
|
function _resolveModelForRuntime(modelStr, runtimeName) {
|
|
6601
6726
|
try {
|
|
6602
6727
|
const adapter = _engineRuntimes.resolveRuntime(runtimeName);
|
|
@@ -6630,13 +6755,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6630
6755
|
val = Math.max(min, val);
|
|
6631
6756
|
if (max !== undefined) val = Math.min(max, val);
|
|
6632
6757
|
if (val !== raw) _clamped.push(`${key}: ${raw} → ${val} (range: ${min}–${max || '∞'})`);
|
|
6633
|
-
|
|
6758
|
+
_setEngineConfig(key, val);
|
|
6634
6759
|
}
|
|
6635
6760
|
}
|
|
6636
|
-
|
|
6637
|
-
|
|
6761
|
+
_deleteEngineConfig('adoPollStatusEvery');
|
|
6762
|
+
_deleteEngineConfig('adoPollCommentsEvery');
|
|
6638
6763
|
// String fields
|
|
6639
|
-
if (e.worktreeRoot !== undefined)
|
|
6764
|
+
if (e.worktreeRoot !== undefined) _setEngineConfig('worktreeRoot', String(e.worktreeRoot || D.worktreeRoot));
|
|
6640
6765
|
|
|
6641
6766
|
// ── Runtime fleet (P-7a5c1f8e) ─────────────────────────────────────
|
|
6642
6767
|
// Empty string clears the override — the dashboard's "Default (CLI
|
|
@@ -6653,13 +6778,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6653
6778
|
return _registeredCliNames.length === 0 || _registeredCliNames.includes(String(name));
|
|
6654
6779
|
};
|
|
6655
6780
|
if (e.defaultCli !== undefined) {
|
|
6656
|
-
if (_isClear(e.defaultCli))
|
|
6657
|
-
else if (_validCli(e.defaultCli))
|
|
6781
|
+
if (_isClear(e.defaultCli)) _deleteEngineConfig('defaultCli');
|
|
6782
|
+
else if (_validCli(e.defaultCli)) _setEngineConfig('defaultCli', String(e.defaultCli));
|
|
6658
6783
|
else _clamped.push(`defaultCli: "${e.defaultCli}" not registered (kept previous value)`);
|
|
6659
6784
|
}
|
|
6660
6785
|
if (e.ccCli !== undefined) {
|
|
6661
|
-
if (_isClear(e.ccCli))
|
|
6662
|
-
else if (_validCli(e.ccCli))
|
|
6786
|
+
if (_isClear(e.ccCli)) _deleteEngineConfig('ccCli');
|
|
6787
|
+
else if (_validCli(e.ccCli)) _setEngineConfig('ccCli', String(e.ccCli));
|
|
6663
6788
|
else _clamped.push(`ccCli: "${e.ccCli}" not registered (kept previous value)`);
|
|
6664
6789
|
}
|
|
6665
6790
|
// Validate fleet-level model assignments against the resolved runtime.
|
|
@@ -6695,47 +6820,47 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6695
6820
|
return null;
|
|
6696
6821
|
}
|
|
6697
6822
|
if (e.defaultModel !== undefined) {
|
|
6698
|
-
if (_isClear(e.defaultModel))
|
|
6823
|
+
if (_isClear(e.defaultModel)) _deleteEngineConfig('defaultModel');
|
|
6699
6824
|
else {
|
|
6700
6825
|
const candidate = String(e.defaultModel);
|
|
6701
6826
|
const resolvedCli = config.engine.defaultCli || 'claude';
|
|
6702
6827
|
const rejection = await _validateFleetModel(candidate, resolvedCli);
|
|
6703
6828
|
if (rejection) _clamped.push(`engine.defaultModel: "${candidate}" ${rejection} — kept previous value`);
|
|
6704
|
-
else
|
|
6829
|
+
else _setEngineConfig('defaultModel', candidate);
|
|
6705
6830
|
}
|
|
6706
6831
|
}
|
|
6707
6832
|
if (e.ccModel !== undefined) {
|
|
6708
|
-
if (_isClear(e.ccModel))
|
|
6833
|
+
if (_isClear(e.ccModel)) _deleteEngineConfig('ccModel');
|
|
6709
6834
|
else {
|
|
6710
6835
|
const candidate = String(e.ccModel);
|
|
6711
6836
|
const resolvedCli = config.engine.ccCli || config.engine.defaultCli || 'claude';
|
|
6712
6837
|
const rejection = await _validateFleetModel(candidate, resolvedCli);
|
|
6713
6838
|
if (rejection) _clamped.push(`engine.ccModel: "${candidate}" ${rejection} — kept previous value`);
|
|
6714
|
-
else
|
|
6839
|
+
else _setEngineConfig('ccModel', candidate);
|
|
6715
6840
|
}
|
|
6716
6841
|
}
|
|
6717
6842
|
if (e.claudeFallbackModel !== undefined) {
|
|
6718
|
-
if (_isClear(e.claudeFallbackModel))
|
|
6719
|
-
else
|
|
6843
|
+
if (_isClear(e.claudeFallbackModel)) _deleteEngineConfig('claudeFallbackModel');
|
|
6844
|
+
else _setEngineConfig('claudeFallbackModel', String(e.claudeFallbackModel));
|
|
6720
6845
|
}
|
|
6721
6846
|
if (e.copilotStreamMode !== undefined) {
|
|
6722
6847
|
const valid = ['on', 'off'];
|
|
6723
|
-
if (_isClear(e.copilotStreamMode))
|
|
6724
|
-
else if (valid.includes(e.copilotStreamMode))
|
|
6848
|
+
if (_isClear(e.copilotStreamMode)) _deleteEngineConfig('copilotStreamMode');
|
|
6849
|
+
else if (valid.includes(e.copilotStreamMode)) _setEngineConfig('copilotStreamMode', e.copilotStreamMode);
|
|
6725
6850
|
else _clamped.push(`copilotStreamMode: "${e.copilotStreamMode}" not in [on, off] (kept previous value)`);
|
|
6726
6851
|
}
|
|
6727
6852
|
// maxBudgetUsd uses ?? semantics — 0 is a valid cap (read-only / dry-run agents).
|
|
6728
6853
|
if (e.maxBudgetUsd !== undefined) {
|
|
6729
|
-
if (_isClear(e.maxBudgetUsd))
|
|
6854
|
+
if (_isClear(e.maxBudgetUsd)) _deleteEngineConfig('maxBudgetUsd');
|
|
6730
6855
|
else {
|
|
6731
6856
|
const n = Number(e.maxBudgetUsd);
|
|
6732
|
-
if (Number.isFinite(n) && n >= 0)
|
|
6857
|
+
if (Number.isFinite(n) && n >= 0) _setEngineConfig('maxBudgetUsd', n);
|
|
6733
6858
|
else _clamped.push(`maxBudgetUsd: "${e.maxBudgetUsd}" must be ≥ 0 (kept previous value)`);
|
|
6734
6859
|
}
|
|
6735
6860
|
}
|
|
6736
6861
|
if (e.ccEffort !== undefined) {
|
|
6737
6862
|
const valid = [null, 'low', 'medium', 'high'];
|
|
6738
|
-
|
|
6863
|
+
_setEngineConfig('ccEffort', valid.includes(e.ccEffort) ? e.ccEffort : null);
|
|
6739
6864
|
}
|
|
6740
6865
|
// Per-type max turns
|
|
6741
6866
|
if (e.maxTurnsByType !== undefined && typeof e.maxTurnsByType === 'object') {
|
|
@@ -6744,18 +6869,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6744
6869
|
const n = Number(val);
|
|
6745
6870
|
if (n && n >= 5 && n <= 500) mbt[type] = n;
|
|
6746
6871
|
}
|
|
6747
|
-
|
|
6872
|
+
_setEngineConfig('maxTurnsByType', mbt);
|
|
6748
6873
|
}
|
|
6749
6874
|
// Boolean fields
|
|
6750
6875
|
const booleanFields = Object.keys(shared.ENGINE_DEFAULTS).filter(k => typeof shared.ENGINE_DEFAULTS[k] === 'boolean');
|
|
6751
6876
|
for (const key of booleanFields) {
|
|
6752
|
-
if (e[key] !== undefined)
|
|
6877
|
+
if (e[key] !== undefined) _setEngineConfig(key, !!e[key]);
|
|
6753
6878
|
}
|
|
6754
6879
|
// Eval loop settings
|
|
6755
|
-
if (e.evalMaxIterations !== undefined)
|
|
6756
|
-
if (e.evalMaxCost !== undefined)
|
|
6880
|
+
if (e.evalMaxIterations !== undefined) _setEngineConfig('evalMaxIterations', Math.max(1, Math.min(10, Number(e.evalMaxIterations) || D.evalMaxIterations)));
|
|
6881
|
+
if (e.evalMaxCost !== undefined) _setEngineConfig('evalMaxCost', e.evalMaxCost === null || e.evalMaxCost === '' ? null : Math.max(0, Number(e.evalMaxCost) || 0));
|
|
6757
6882
|
if (e.ignoredCommentAuthors !== undefined) {
|
|
6758
|
-
|
|
6883
|
+
_setEngineConfig('ignoredCommentAuthors', String(e.ignoredCommentAuthors || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean));
|
|
6759
6884
|
}
|
|
6760
6885
|
}
|
|
6761
6886
|
|
|
@@ -6892,7 +7017,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6892
7017
|
}
|
|
6893
7018
|
|
|
6894
7019
|
shared.pruneDefaultClaudeConfig(config);
|
|
6895
|
-
|
|
7020
|
+
mutateDashboardConfig(current => mergeSettingsConfigUpdate(current, config, body, _configPatch));
|
|
6896
7021
|
// Refresh in-memory CONFIG so subsequent reads see the update
|
|
6897
7022
|
reloadConfig();
|
|
6898
7023
|
invalidateStatusCache();
|
|
@@ -6915,11 +7040,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6915
7040
|
|
|
6916
7041
|
async function handleSettingsReset(req, res) {
|
|
6917
7042
|
try {
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
7043
|
+
mutateDashboardConfig(config => {
|
|
7044
|
+
config.engine = { ...shared.ENGINE_DEFAULTS };
|
|
7045
|
+
delete config.claude;
|
|
7046
|
+
config.agents = { ...shared.DEFAULT_AGENTS };
|
|
7047
|
+
return config;
|
|
7048
|
+
});
|
|
6923
7049
|
reloadConfig();
|
|
6924
7050
|
invalidateStatusCache();
|
|
6925
7051
|
return jsonReply(res, 200, { ok: true });
|
|
@@ -7189,18 +7315,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7189
7315
|
|
|
7190
7316
|
// Pinned notes
|
|
7191
7317
|
{ method: 'GET', path: '/api/pinned', desc: 'Get pinned notes', handler: async (req, res) => {
|
|
7192
|
-
const content = safeRead(
|
|
7318
|
+
const content = safeRead(PINNED_PATH);
|
|
7193
7319
|
return jsonReply(res, 200, { content, entries: parsePinnedEntries(content) });
|
|
7194
7320
|
}},
|
|
7195
7321
|
{ method: 'POST', path: '/api/pinned', desc: 'Add a pinned note', params: 'title, content, level?', handler: async (req, res) => {
|
|
7196
7322
|
const body = await readBody(req);
|
|
7197
7323
|
const { title, content, level } = body;
|
|
7198
7324
|
if (!title || !content) return jsonReply(res, 400, { error: 'title and content required' });
|
|
7199
|
-
|
|
7200
|
-
const existing = safeRead(pinnedPath);
|
|
7201
|
-
const levelTag = level === 'critical' ? '🔴 ' : level === 'warning' ? '🟡 ' : '';
|
|
7202
|
-
const entry = '\n\n### ' + levelTag + title + '\n\n' + content + '\n\n*Pinned by human on ' + new Date().toISOString().slice(0, 10) + '*';
|
|
7203
|
-
safeWrite(pinnedPath, (existing || '# Pinned Context\n\nCritical notes visible to all agents.') + entry);
|
|
7325
|
+
addPinnedEntryLocked({ title, content, level });
|
|
7204
7326
|
// pinned.md is in slow-state cache — opt-in invalidation so the new entry is visible immediately (closes #1295)
|
|
7205
7327
|
invalidateStatusCache({ includeSlow: true });
|
|
7206
7328
|
return jsonReply(res, 200, { ok: true });
|
|
@@ -7209,12 +7331,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7209
7331
|
const body = await readBody(req);
|
|
7210
7332
|
const { title } = body;
|
|
7211
7333
|
if (!title) return jsonReply(res, 400, { error: 'title required' });
|
|
7212
|
-
|
|
7213
|
-
let content = safeRead(pinnedPath);
|
|
7214
|
-
if (!content) return jsonReply(res, 404, { error: 'No pinned notes' });
|
|
7215
|
-
const regex = new RegExp('\\n\\n###\\s*(?:🔴\\s*|🟡\\s*)?' + title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\n[\\s\\S]*?(?=\\n\\n###|$)', 'i');
|
|
7216
|
-
content = content.replace(regex, '');
|
|
7217
|
-
safeWrite(pinnedPath, content);
|
|
7334
|
+
if (!removePinnedEntryLocked(title)) return jsonReply(res, 404, { error: 'No pinned notes' });
|
|
7218
7335
|
// pinned.md is in slow-state cache — opt-in invalidation so the unpin is visible immediately
|
|
7219
7336
|
invalidateStatusCache({ includeSlow: true });
|
|
7220
7337
|
return jsonReply(res, 200, { ok: true });
|
|
@@ -7222,24 +7339,20 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7222
7339
|
|
|
7223
7340
|
// KB pin state (server-side so CC can pin items)
|
|
7224
7341
|
{ method: 'GET', path: '/api/kb-pins', desc: 'Get pinned KB item keys', handler: async (req, res) => {
|
|
7225
|
-
const pins = shared.safeJson(
|
|
7342
|
+
const pins = shared.safeJson(KB_PINS_PATH) || [];
|
|
7226
7343
|
return jsonReply(res, 200, { pins });
|
|
7227
7344
|
}},
|
|
7228
7345
|
{ method: 'POST', path: '/api/kb-pins', desc: 'Set pinned KB item keys', params: 'pins[]', handler: async (req, res) => {
|
|
7229
7346
|
const body = await readBody(req);
|
|
7230
7347
|
if (!Array.isArray(body.pins)) return jsonReply(res, 400, { error: 'pins array required' });
|
|
7231
|
-
|
|
7348
|
+
setKbPinsLocked(body.pins);
|
|
7232
7349
|
return jsonReply(res, 200, { ok: true });
|
|
7233
7350
|
}},
|
|
7234
7351
|
{ method: 'POST', path: '/api/kb-pins/toggle', desc: 'Toggle a single KB pin', params: 'key', handler: async (req, res) => {
|
|
7235
7352
|
const body = await readBody(req);
|
|
7236
7353
|
if (!body.key) return jsonReply(res, 400, { error: 'key required' });
|
|
7237
|
-
const
|
|
7238
|
-
|
|
7239
|
-
const idx = pins.indexOf(body.key);
|
|
7240
|
-
if (idx >= 0) pins.splice(idx, 1); else pins.unshift(body.key);
|
|
7241
|
-
safeWrite(pinsPath, pins);
|
|
7242
|
-
return jsonReply(res, 200, { ok: true, pinned: idx < 0 });
|
|
7354
|
+
const pinned = toggleKbPinLocked(body.key);
|
|
7355
|
+
return jsonReply(res, 200, { ok: true, pinned });
|
|
7243
7356
|
}},
|
|
7244
7357
|
|
|
7245
7358
|
// Notes
|
|
@@ -7916,6 +8029,7 @@ module.exports = {
|
|
|
7916
8029
|
_formatCcCliCommandsIndex,
|
|
7917
8030
|
_resetPreambleCache,
|
|
7918
8031
|
_installCrashHandlers,
|
|
8032
|
+
_mergeSettingsConfigUpdate: mergeSettingsConfigUpdate,
|
|
7919
8033
|
};
|
|
7920
8034
|
|
|
7921
8035
|
// Start the HTTP server only when run directly (node dashboard.js).
|
package/engine/shared.js
CHANGED
|
@@ -421,6 +421,27 @@ function safeWrite(p, data) {
|
|
|
421
421
|
}
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
function mutateTextFileLocked(filePath, mutateFn, {
|
|
425
|
+
defaultValue = '',
|
|
426
|
+
lockRetries,
|
|
427
|
+
lockRetryBackoffMs,
|
|
428
|
+
skipWriteIfUnchanged = false
|
|
429
|
+
} = {}) {
|
|
430
|
+
const lockPath = `${filePath}.lock`;
|
|
431
|
+
const retries = lockRetries ?? ENGINE_DEFAULTS.lockRetries;
|
|
432
|
+
const retryBackoffMs = lockRetryBackoffMs ?? ENGINE_DEFAULTS.lockRetryBackoffMs;
|
|
433
|
+
return withFileLock(lockPath, () => {
|
|
434
|
+
const fileExists = fs.existsSync(filePath);
|
|
435
|
+
const before = fileExists ? safeRead(filePath) : String(defaultValue || '');
|
|
436
|
+
const next = mutateFn(before);
|
|
437
|
+
const finalText = next === undefined ? before : String(next);
|
|
438
|
+
if (!skipWriteIfUnchanged || finalText !== before) {
|
|
439
|
+
safeWrite(filePath, finalText);
|
|
440
|
+
}
|
|
441
|
+
return finalText;
|
|
442
|
+
}, { retries, retryBackoffMs });
|
|
443
|
+
}
|
|
444
|
+
|
|
424
445
|
function safeUnlink(p) {
|
|
425
446
|
try { fs.unlinkSync(p); } catch { /* cleanup */ }
|
|
426
447
|
}
|
|
@@ -3146,6 +3167,7 @@ module.exports = {
|
|
|
3146
3167
|
safeReadDir,
|
|
3147
3168
|
safeJson, safeJsonObj, safeJsonArr, safeJsonNoRestore,
|
|
3148
3169
|
safeWrite,
|
|
3170
|
+
mutateTextFileLocked,
|
|
3149
3171
|
safeUnlink,
|
|
3150
3172
|
resolveMinionsHome,
|
|
3151
3173
|
saveMinionsRootPointer,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1778",
|
|
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"
|