@yemi33/minions 0.1.1777 → 0.1.1779
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 +7 -0
- package/dashboard/js/command-center.js +24 -6
- package/dashboard/js/modal-qa.js +41 -4
- package/dashboard.js +306 -123
- package/engine/copilot-models.json +1 -1
- package/engine/shared.js +22 -0
- package/package.json +1 -1
- package/prompts/cc-system.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -103,15 +103,15 @@ function _ccActiveTab() {
|
|
|
103
103
|
|
|
104
104
|
// Build a plain-text transcript from a tab's stored messages — sent on every
|
|
105
105
|
// initial request so the server can carry it over if the session has to reset
|
|
106
|
-
// (runtime switch, system-prompt change)
|
|
107
|
-
//
|
|
106
|
+
// (runtime switch, system-prompt change) or if the previous turn has local
|
|
107
|
+
// action results the runtime session never saw.
|
|
108
108
|
var CC_TRANSCRIPT_MAX_TURNS = 20;
|
|
109
109
|
function _ccBuildTranscript(tab) {
|
|
110
110
|
if (!tab || !Array.isArray(tab.messages) || tab.messages.length === 0) return [];
|
|
111
111
|
var out = [];
|
|
112
112
|
for (var i = 0; i < tab.messages.length; i++) {
|
|
113
113
|
var m = tab.messages[i];
|
|
114
|
-
if (!m || (m.role !== 'user' && m.role !== 'assistant')) continue;
|
|
114
|
+
if (!m || (m.role !== 'user' && m.role !== 'assistant' && m.role !== 'action' && m.role !== 'system')) continue;
|
|
115
115
|
var html = typeof m.html === 'string' ? m.html : '';
|
|
116
116
|
var tmp = document.createElement('div');
|
|
117
117
|
tmp.innerHTML = html;
|
|
@@ -141,6 +141,23 @@ function _ccMergeStreamText(prev, incoming) {
|
|
|
141
141
|
return current + '\n\n' + next;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
var CC_DISPATCH_ACTION_ALIASES = ['fix', 'implement', 'explore', 'review', 'test'];
|
|
145
|
+
function _ccNormalizeDispatchAction(action) {
|
|
146
|
+
if (!action || typeof action !== 'object' || typeof action.type !== 'string') return action;
|
|
147
|
+
var type = action.type.trim().toLowerCase();
|
|
148
|
+
if (type === 'dispatch') {
|
|
149
|
+
if (action.type === 'dispatch') return action;
|
|
150
|
+
var dispatchAction = Object.assign({}, action);
|
|
151
|
+
dispatchAction.type = 'dispatch';
|
|
152
|
+
return dispatchAction;
|
|
153
|
+
}
|
|
154
|
+
if (CC_DISPATCH_ACTION_ALIASES.indexOf(type) < 0) return action;
|
|
155
|
+
var normalized = Object.assign({}, action);
|
|
156
|
+
normalized.type = 'dispatch';
|
|
157
|
+
if (!normalized.workType) normalized.workType = type;
|
|
158
|
+
return normalized;
|
|
159
|
+
}
|
|
160
|
+
|
|
144
161
|
async function _ccDashboardHealth() {
|
|
145
162
|
var controller = new AbortController();
|
|
146
163
|
var timer = setTimeout(function() { controller.abort(); }, 3000);
|
|
@@ -745,7 +762,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
745
762
|
if (!isReconnect && res.status === 429 && (!activeTab._429retries || activeTab._429retries < 3)) {
|
|
746
763
|
activeTab._429retries = (activeTab._429retries || 0) + 1;
|
|
747
764
|
await new Promise(function(r) { setTimeout(r, 1500); });
|
|
748
|
-
return await _ccConsumeStream({ message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null }, false);
|
|
765
|
+
return await _ccConsumeStream({ message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null, transcript: _ccBuildTranscript(activeTab) }, false);
|
|
749
766
|
}
|
|
750
767
|
activeTab._429retries = 0;
|
|
751
768
|
var errText = await res.text();
|
|
@@ -994,6 +1011,7 @@ function _tagServerExecuted(actions, actionResults) {
|
|
|
994
1011
|
}
|
|
995
1012
|
|
|
996
1013
|
async function ccExecuteAction(action, targetTabId) {
|
|
1014
|
+
action = _ccNormalizeDispatchAction(action);
|
|
997
1015
|
var status = document.createElement('div');
|
|
998
1016
|
status.style.cssText = 'padding:4px 10px;border-radius:4px;font-size:10px;align-self:flex-start;border:1px dashed var(--border);color:var(--muted)';
|
|
999
1017
|
|
|
@@ -1010,7 +1028,7 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1010
1028
|
status.style.color = action._serverDuplicate ? 'var(--orange)' : 'var(--green)';
|
|
1011
1029
|
}
|
|
1012
1030
|
ccAddMessage('action', status.outerHTML, false, targetTabId);
|
|
1013
|
-
if (['dispatch','
|
|
1031
|
+
if (['dispatch','create-meeting'].includes(action.type)) wakeEngine();
|
|
1014
1032
|
refresh();
|
|
1015
1033
|
return;
|
|
1016
1034
|
}
|
|
@@ -1023,7 +1041,7 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1023
1041
|
case 'explore':
|
|
1024
1042
|
case 'review':
|
|
1025
1043
|
case 'test': {
|
|
1026
|
-
var workType = action.workType ||
|
|
1044
|
+
var workType = action.workType || 'implement';
|
|
1027
1045
|
// Forward both singular (`agent`) and plural (`agents`) hint shapes —
|
|
1028
1046
|
// the LLM emits either depending on phrasing ("assign to lambert" vs
|
|
1029
1047
|
// "dispatch to dallas, ralph"). The server-side handler promotes a
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -28,6 +28,7 @@ let _qaAbortController = null;
|
|
|
28
28
|
let _qaQueue = []; // queued messages while processing
|
|
29
29
|
const QA_QUEUE_CAP = 10; // max queued messages
|
|
30
30
|
const QA_STREAM_STALL_MS = 6 * 60 * 1000; // allow the full doc-chat timeout before treating heartbeat-only streams as stalled
|
|
31
|
+
const QA_TRANSCRIPT_MAX_TURNS = 20;
|
|
31
32
|
let _qaSessionKey = ''; // key for current conversation (title or filePath)
|
|
32
33
|
|
|
33
34
|
const QA_STICKY_BOTTOM_PX = 80;
|
|
@@ -97,6 +98,40 @@ function _qaCloneQueue(queue) {
|
|
|
97
98
|
return Array.isArray(queue) ? queue.map(item => ({ ...item })) : [];
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
function _qaBuildTranscript(history, currentMessage) {
|
|
102
|
+
if (!Array.isArray(history) || history.length === 0) return [];
|
|
103
|
+
const current = typeof currentMessage === 'string' ? currentMessage.trim() : '';
|
|
104
|
+
const out = [];
|
|
105
|
+
for (let i = 0; i < history.length; i++) {
|
|
106
|
+
const m = history[i];
|
|
107
|
+
if (!m || (m.role !== 'user' && m.role !== 'assistant' && m.role !== 'action' && m.role !== 'system')) continue;
|
|
108
|
+
const text = typeof m.text === 'string' ? m.text.trim() : '';
|
|
109
|
+
if (!text) continue;
|
|
110
|
+
if (current && m.role === 'user' && text === current && i === history.length - 1) continue;
|
|
111
|
+
out.push({ role: m.role, text });
|
|
112
|
+
}
|
|
113
|
+
return out.slice(-QA_TRANSCRIPT_MAX_TURNS);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _qaSummarizeActionContext(actions, actionResults) {
|
|
117
|
+
if (!Array.isArray(actions) || actions.length === 0) return '';
|
|
118
|
+
const lines = [];
|
|
119
|
+
for (let i = 0; i < actions.length; i++) {
|
|
120
|
+
const action = actions[i] || {};
|
|
121
|
+
const result = Array.isArray(actionResults) ? actionResults[i] : null;
|
|
122
|
+
const type = action.type || 'action';
|
|
123
|
+
const subject = result?.id || action.id || action.title || action.file || action.target || action.endpoint || '';
|
|
124
|
+
if (result?.error) {
|
|
125
|
+
lines.push(`${type}${subject ? ' ' + subject : ''} failed: ${result.error}`);
|
|
126
|
+
} else if (result?.ok || action._serverExecuted) {
|
|
127
|
+
lines.push(`${type}${subject ? ' ' + subject : ''} completed${result?.duplicate ? ' (duplicate)' : ''}${result?.warning ? ': ' + result.warning : ''}`);
|
|
128
|
+
} else {
|
|
129
|
+
lines.push(`${type}${subject ? ' ' + subject : ''} emitted`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
100
135
|
function _qaGetRuntime(key) {
|
|
101
136
|
if (!key) return null;
|
|
102
137
|
let runtime = _qaRuntime.get(key);
|
|
@@ -609,6 +644,7 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
609
644
|
filePath: capturedFilePath || null,
|
|
610
645
|
model: window._lastStatus?.autoMode?.ccModel || undefined,
|
|
611
646
|
contentHash: capturedDocContext.content ? (function(s) { const m = Math.floor(s.length / 2); return s.length + ':' + s.charCodeAt(0) + ':' + s.charCodeAt(m) + ':' + s.charCodeAt(s.length - 1); })(capturedDocContext.content) : undefined,
|
|
647
|
+
transcript: _qaBuildTranscript(runtime.history, message),
|
|
612
648
|
}),
|
|
613
649
|
});
|
|
614
650
|
let sessionDocContext = { ...capturedDocContext };
|
|
@@ -692,20 +728,21 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
692
728
|
if (rawErrorHtml) _qaInsertBeforeQueued(tmp, rawErrorHtml);
|
|
693
729
|
});
|
|
694
730
|
|
|
695
|
-
runtime.history.push({ role: 'user', text: message });
|
|
696
|
-
runtime.history.push({ role: 'assistant', text: finalText || '' });
|
|
697
|
-
if (_qaIsActiveSession(sessionKey)) _qaHistory = runtime.history.slice();
|
|
698
|
-
|
|
699
731
|
_qaNotifySidebar(capturedFilePath);
|
|
732
|
+
runtime.history.push({ role: 'user', text: message });
|
|
733
|
+
runtime.history.push({ role: 'assistant', text: bodyText || finalText || '' });
|
|
700
734
|
if (evt.actions && evt.actions.length > 0) {
|
|
701
735
|
if (evt.actionResults && typeof _tagServerExecuted === 'function') _tagServerExecuted(evt.actions, evt.actionResults);
|
|
702
736
|
for (const action of evt.actions) await ccExecuteAction(action);
|
|
737
|
+
const actionContext = _qaSummarizeActionContext(evt.actions, evt.actionResults);
|
|
738
|
+
if (actionContext) runtime.history.push({ role: 'action', text: actionContext });
|
|
703
739
|
} else if (evt.actionParseError) {
|
|
704
740
|
const warning = '<div class="modal-qa-a" style="color:var(--red)">Actions block emitted but JSON could not be parsed — no actions were executed. Resend or rephrase. (' + escHtml(String(evt.actionParseError).slice(0, 200)) + ')</div>';
|
|
705
741
|
updatedThreadHtml = _qaMutateThreadHtml(sessionKey, tmp => {
|
|
706
742
|
_qaInsertBeforeQueued(tmp, warning);
|
|
707
743
|
});
|
|
708
744
|
}
|
|
745
|
+
if (_qaIsActiveSession(sessionKey)) _qaHistory = runtime.history.slice();
|
|
709
746
|
|
|
710
747
|
if (evt.edited && evt.content) {
|
|
711
748
|
const display = evt.content.replace(/^---[\s\S]*?---\n*/m, '');
|
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,
|
|
@@ -1279,10 +1366,36 @@ function _readCcTabSessions({ prune = true } = {}) {
|
|
|
1279
1366
|
|
|
1280
1367
|
const CC_CARRYOVER_MAX_TURNS = 20;
|
|
1281
1368
|
const CC_CARRYOVER_PER_MSG_CHARS = 2000;
|
|
1369
|
+
const CC_TRANSCRIPT_DIALOG_ROLES = new Set(['user', 'assistant']);
|
|
1370
|
+
const CC_TRANSCRIPT_CONTEXT_ROLES = new Set(['user', 'assistant', 'action', 'system']);
|
|
1282
1371
|
|
|
1283
|
-
function
|
|
1372
|
+
function _normalizeTranscriptRole(role) {
|
|
1373
|
+
const value = String(role || '').toLowerCase();
|
|
1374
|
+
return CC_TRANSCRIPT_CONTEXT_ROLES.has(value) ? value : null;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function _transcriptHasCarryoverContext(transcript, { outOfBandOnly = false, currentMessage } = {}) {
|
|
1378
|
+
if (!Array.isArray(transcript)) return false;
|
|
1379
|
+
const current = typeof currentMessage === 'string' ? currentMessage.trim() : '';
|
|
1380
|
+
return transcript.some(m => {
|
|
1381
|
+
const role = _normalizeTranscriptRole(m?.role);
|
|
1382
|
+
if (!role || typeof m.text !== 'string' || !m.text.trim()) return false;
|
|
1383
|
+
if (outOfBandOnly && CC_TRANSCRIPT_DIALOG_ROLES.has(role)) return false;
|
|
1384
|
+
return !(current && role === 'user' && m.text.trim() === current);
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function _buildTranscriptCarryover(transcript, { previousRuntime, currentMessage, outOfBandOnly = false } = {}) {
|
|
1284
1389
|
if (!Array.isArray(transcript) || transcript.length === 0) return '';
|
|
1285
|
-
let filtered = transcript
|
|
1390
|
+
let filtered = transcript
|
|
1391
|
+
.map(m => {
|
|
1392
|
+
const role = _normalizeTranscriptRole(m?.role);
|
|
1393
|
+
return role && typeof m?.text === 'string' && m.text.trim()
|
|
1394
|
+
? { role, text: m.text }
|
|
1395
|
+
: null;
|
|
1396
|
+
})
|
|
1397
|
+
.filter(Boolean);
|
|
1398
|
+
if (outOfBandOnly) filtered = filtered.filter(m => !CC_TRANSCRIPT_DIALOG_ROLES.has(m.role));
|
|
1286
1399
|
const current = typeof currentMessage === 'string' ? currentMessage.trim() : '';
|
|
1287
1400
|
if (current && filtered.length > 0) {
|
|
1288
1401
|
const last = filtered[filtered.length - 1];
|
|
@@ -1291,11 +1404,19 @@ function _buildTranscriptCarryover(transcript, { previousRuntime, currentMessage
|
|
|
1291
1404
|
if (filtered.length === 0) return '';
|
|
1292
1405
|
const recent = filtered.slice(-CC_CARRYOVER_MAX_TURNS);
|
|
1293
1406
|
const truncated = filtered.length > recent.length;
|
|
1294
|
-
const header =
|
|
1295
|
-
? `--- Previous
|
|
1296
|
-
:
|
|
1407
|
+
const header = outOfBandOnly
|
|
1408
|
+
? `--- Previous out-of-band UI/server events (carried over) ---`
|
|
1409
|
+
: previousRuntime
|
|
1410
|
+
? `--- Previous conversation (carried over from ${previousRuntime} session) ---`
|
|
1411
|
+
: `--- Previous conversation (carried over) ---`;
|
|
1297
1412
|
const lines = recent.map(m => {
|
|
1298
|
-
const who = m.role === 'user'
|
|
1413
|
+
const who = m.role === 'user'
|
|
1414
|
+
? 'User'
|
|
1415
|
+
: m.role === 'assistant'
|
|
1416
|
+
? 'Assistant'
|
|
1417
|
+
: m.role === 'action'
|
|
1418
|
+
? 'Action result'
|
|
1419
|
+
: 'System note';
|
|
1299
1420
|
let text = m.text.trim();
|
|
1300
1421
|
if (text.length > CC_CARRYOVER_PER_MSG_CHARS) text = text.slice(0, CC_CARRYOVER_PER_MSG_CHARS) + '… [truncated]';
|
|
1301
1422
|
return `${who}: ${text}`;
|
|
@@ -1664,6 +1785,19 @@ function _extractActionsJson(segment) {
|
|
|
1664
1785
|
return null;
|
|
1665
1786
|
}
|
|
1666
1787
|
|
|
1788
|
+
const CC_DISPATCH_ACTION_ALIASES = new Set(['fix', 'implement', 'explore', 'review', 'test']);
|
|
1789
|
+
|
|
1790
|
+
function normalizeCCAction(action) {
|
|
1791
|
+
if (!action || typeof action !== 'object') return action;
|
|
1792
|
+
if (typeof action.type !== 'string') return action;
|
|
1793
|
+
const type = action.type.trim().toLowerCase();
|
|
1794
|
+
if (type === 'dispatch') {
|
|
1795
|
+
return action.type === 'dispatch' ? action : { ...action, type: 'dispatch' };
|
|
1796
|
+
}
|
|
1797
|
+
if (!CC_DISPATCH_ACTION_ALIASES.has(type)) return action;
|
|
1798
|
+
return { ...action, type: 'dispatch', workType: action.workType || type };
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1667
1801
|
function parseCCActions(text) {
|
|
1668
1802
|
let actions = [];
|
|
1669
1803
|
let displayText = stripCCActionsForDisplay(text);
|
|
@@ -1704,6 +1838,7 @@ function parseCCActions(text) {
|
|
|
1704
1838
|
parseError = null; // legacy fallback recovered actions
|
|
1705
1839
|
}
|
|
1706
1840
|
}
|
|
1841
|
+
actions = actions.map(normalizeCCAction);
|
|
1707
1842
|
const result = { text: displayText, actions };
|
|
1708
1843
|
if (parseError && actions.length === 0) {
|
|
1709
1844
|
result._actionParseError = parseError;
|
|
@@ -2099,7 +2234,8 @@ function _ccValidateAction(action) {
|
|
|
2099
2234
|
|
|
2100
2235
|
async function executeCCActions(actions) {
|
|
2101
2236
|
const results = [];
|
|
2102
|
-
for (const
|
|
2237
|
+
for (const rawAction of actions) {
|
|
2238
|
+
const action = normalizeCCAction(rawAction);
|
|
2103
2239
|
const validationError = _ccValidateAction(action);
|
|
2104
2240
|
if (validationError) {
|
|
2105
2241
|
results.push({ type: action?.type || 'unknown', error: validationError });
|
|
@@ -2108,7 +2244,7 @@ async function executeCCActions(actions) {
|
|
|
2108
2244
|
try {
|
|
2109
2245
|
switch (action.type) {
|
|
2110
2246
|
case 'dispatch': case 'fix': case 'implement': case 'explore': case 'review': case 'test': {
|
|
2111
|
-
const workType = routing.normalizeWorkType(action.workType ||
|
|
2247
|
+
const workType = routing.normalizeWorkType(action.workType || WORK_TYPE.IMPLEMENT, WORK_TYPE.IMPLEMENT);
|
|
2112
2248
|
const id = 'W-' + shared.uid();
|
|
2113
2249
|
const project = action.project || '';
|
|
2114
2250
|
const prRef = getWorkItemPrRef(action);
|
|
@@ -2574,12 +2710,14 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
2574
2710
|
const existing = resolveSession(store, sessionKey);
|
|
2575
2711
|
let sessionId = existing ? existing.sessionId : null;
|
|
2576
2712
|
const resumeNeedsCarryover = !!sessionId && _ccRuntimeNeedsResumeCarryover(shared.resolveCcCli(CONFIG.engine));
|
|
2713
|
+
const resumeHasOutOfBandCarryover = !!sessionId && _transcriptHasCarryoverContext(transcript, { outOfBandOnly: true, currentMessage: message });
|
|
2714
|
+
const freshNeedsCarryover = _transcriptHasCarryoverContext(transcript, { currentMessage: message });
|
|
2577
2715
|
|
|
2578
|
-
function buildPrompt({ includePreamble = true, includeCarryover = false } = {}) {
|
|
2716
|
+
function buildPrompt({ includePreamble = true, includeCarryover = false, outOfBandOnly = false } = {}) {
|
|
2579
2717
|
const parts = (!skipStatePreamble && includePreamble) ? [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`] : [];
|
|
2580
2718
|
if (extraContext) parts.push(extraContext);
|
|
2581
2719
|
if (includeCarryover) {
|
|
2582
|
-
const carryover = _buildTranscriptCarryover(transcript, { currentMessage: message });
|
|
2720
|
+
const carryover = _buildTranscriptCarryover(transcript, { currentMessage: message, outOfBandOnly });
|
|
2583
2721
|
if (carryover) parts.push(carryover);
|
|
2584
2722
|
}
|
|
2585
2723
|
parts.push(message);
|
|
@@ -2590,7 +2728,11 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
2590
2728
|
|
|
2591
2729
|
// Attempt 1: resume existing session — skip preamble (session already has context)
|
|
2592
2730
|
if (sessionId && maxTurns > 1) {
|
|
2593
|
-
const p1 = llm.callLLM(buildPrompt({
|
|
2731
|
+
const p1 = llm.callLLM(buildPrompt({
|
|
2732
|
+
includePreamble: false,
|
|
2733
|
+
includeCarryover: resumeNeedsCarryover || resumeHasOutOfBandCarryover,
|
|
2734
|
+
outOfBandOnly: !resumeNeedsCarryover,
|
|
2735
|
+
}), '', {
|
|
2594
2736
|
timeout, label, model, maxTurns, allowedTools, sessionId, effort: ccEffort, direct: true,
|
|
2595
2737
|
engineConfig: CONFIG.engine,
|
|
2596
2738
|
});
|
|
@@ -2627,7 +2769,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
2627
2769
|
}
|
|
2628
2770
|
|
|
2629
2771
|
// Attempt 2: fresh session (include preamble for full context)
|
|
2630
|
-
const freshPrompt = buildPrompt({ includeCarryover:
|
|
2772
|
+
const freshPrompt = buildPrompt({ includeCarryover: freshNeedsCarryover });
|
|
2631
2773
|
const p2 = llm.callLLM(freshPrompt, systemPrompt, {
|
|
2632
2774
|
timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
|
|
2633
2775
|
engineConfig: CONFIG.engine,
|
|
@@ -2675,12 +2817,14 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
|
|
|
2675
2817
|
const existing = resolveSession(store, sessionKey);
|
|
2676
2818
|
let sessionId = existing ? existing.sessionId : null;
|
|
2677
2819
|
const resumeNeedsCarryover = !!sessionId && _ccRuntimeNeedsResumeCarryover(shared.resolveCcCli(CONFIG.engine));
|
|
2820
|
+
const resumeHasOutOfBandCarryover = !!sessionId && _transcriptHasCarryoverContext(transcript, { outOfBandOnly: true, currentMessage: message });
|
|
2821
|
+
const freshNeedsCarryover = _transcriptHasCarryoverContext(transcript, { currentMessage: message });
|
|
2678
2822
|
|
|
2679
|
-
function buildPrompt({ includePreamble = true, includeCarryover = false } = {}) {
|
|
2823
|
+
function buildPrompt({ includePreamble = true, includeCarryover = false, outOfBandOnly = false } = {}) {
|
|
2680
2824
|
const parts = (!skipStatePreamble && includePreamble) ? [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`] : [];
|
|
2681
2825
|
if (extraContext) parts.push(extraContext);
|
|
2682
2826
|
if (includeCarryover) {
|
|
2683
|
-
const carryover = _buildTranscriptCarryover(transcript, { currentMessage: message });
|
|
2827
|
+
const carryover = _buildTranscriptCarryover(transcript, { currentMessage: message, outOfBandOnly });
|
|
2684
2828
|
if (carryover) parts.push(carryover);
|
|
2685
2829
|
}
|
|
2686
2830
|
parts.push(message);
|
|
@@ -2690,7 +2834,11 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
|
|
|
2690
2834
|
let result;
|
|
2691
2835
|
|
|
2692
2836
|
if (sessionId && maxTurns > 1) {
|
|
2693
|
-
const p1 = llm.callLLMStreaming(buildPrompt({
|
|
2837
|
+
const p1 = llm.callLLMStreaming(buildPrompt({
|
|
2838
|
+
includePreamble: false,
|
|
2839
|
+
includeCarryover: resumeNeedsCarryover || resumeHasOutOfBandCarryover,
|
|
2840
|
+
outOfBandOnly: !resumeNeedsCarryover,
|
|
2841
|
+
}), '', {
|
|
2694
2842
|
timeout, label, model, maxTurns, allowedTools, sessionId, effort: ccEffort, direct: true,
|
|
2695
2843
|
engineConfig: CONFIG.engine,
|
|
2696
2844
|
onChunk,
|
|
@@ -2727,7 +2875,7 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
|
|
|
2727
2875
|
}
|
|
2728
2876
|
|
|
2729
2877
|
if (onRetry) onRetry(2);
|
|
2730
|
-
const freshPrompt = buildPrompt({ includeCarryover:
|
|
2878
|
+
const freshPrompt = buildPrompt({ includeCarryover: freshNeedsCarryover });
|
|
2731
2879
|
const p2 = llm.callLLMStreaming(freshPrompt, systemPrompt, {
|
|
2732
2880
|
timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
|
|
2733
2881
|
engineConfig: CONFIG.engine,
|
|
@@ -3099,7 +3247,7 @@ function _makeDocChatStreamStripper(onChunk) {
|
|
|
3099
3247
|
}
|
|
3100
3248
|
|
|
3101
3249
|
// Doc-specific wrapper — adds document context, parses ---DOCUMENT---
|
|
3102
|
-
async function ccDocCall({ message, document, title, filePath, selection, canEdit, isJson, model, freshSession, onAbortReady }) {
|
|
3250
|
+
async function ccDocCall({ message, document, title, filePath, selection, canEdit, isJson, model, freshSession, transcript, onAbortReady }) {
|
|
3103
3251
|
const sessionKey = filePath || title;
|
|
3104
3252
|
const docSlice = String(document || '');
|
|
3105
3253
|
|
|
@@ -3126,6 +3274,7 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
3126
3274
|
allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
|
|
3127
3275
|
skipStatePreamble: true,
|
|
3128
3276
|
systemPrompt: DOC_CHAT_SYSTEM_PROMPT,
|
|
3277
|
+
transcript,
|
|
3129
3278
|
...(model ? { model } : {}),
|
|
3130
3279
|
onAbortReady,
|
|
3131
3280
|
});
|
|
@@ -3163,7 +3312,7 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
3163
3312
|
return _parseDocChatResultText(result.text);
|
|
3164
3313
|
}
|
|
3165
3314
|
|
|
3166
|
-
async function ccDocCallStreaming({ message, document, title, filePath, selection, canEdit, isJson, model, freshSession, onAbortReady, onChunk, onToolUse, onRetry }) {
|
|
3315
|
+
async function ccDocCallStreaming({ message, document, title, filePath, selection, canEdit, isJson, model, freshSession, transcript, onAbortReady, onChunk, onToolUse, onRetry }) {
|
|
3167
3316
|
const sessionKey = filePath || title;
|
|
3168
3317
|
const docSlice = String(document || '');
|
|
3169
3318
|
const streamStripper = _makeDocChatStreamStripper(onChunk);
|
|
@@ -3186,6 +3335,7 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
|
|
|
3186
3335
|
allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
|
|
3187
3336
|
skipStatePreamble: true,
|
|
3188
3337
|
systemPrompt: DOC_CHAT_SYSTEM_PROMPT,
|
|
3338
|
+
transcript,
|
|
3189
3339
|
...(model ? { model } : {}),
|
|
3190
3340
|
onAbortReady,
|
|
3191
3341
|
onChunk: streamStripper,
|
|
@@ -5346,6 +5496,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5346
5496
|
filePath: body.filePath, selection: body.selection, canEdit, isJson,
|
|
5347
5497
|
model: body.model || undefined,
|
|
5348
5498
|
freshSession: !!body.freshSession,
|
|
5499
|
+
transcript: body.transcript,
|
|
5349
5500
|
onAbortReady: (abort) => { _docAbort = abort; },
|
|
5350
5501
|
});
|
|
5351
5502
|
const actionResults = await executeDocChatActions(actions);
|
|
@@ -5436,6 +5587,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5436
5587
|
filePath: body.filePath, selection: body.selection, canEdit, isJson,
|
|
5437
5588
|
model: body.model || undefined,
|
|
5438
5589
|
freshSession: !!body.freshSession,
|
|
5590
|
+
transcript: body.transcript,
|
|
5439
5591
|
onAbortReady: (abort) => { _docAbort = abort; },
|
|
5440
5592
|
onChunk: (text) => { writeDocEvent({ type: 'chunk', text }); },
|
|
5441
5593
|
onToolUse: (name, input) => { writeDocEvent({ type: 'tool', name, input: _lightToolInput(input) }); },
|
|
@@ -5734,13 +5886,15 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5734
5886
|
});
|
|
5735
5887
|
}
|
|
5736
5888
|
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5889
|
+
// Check if already linked under the config lock so concurrent dashboard
|
|
5890
|
+
// adds cannot both pass the preflight check and then clobber config.json.
|
|
5891
|
+
let alreadyLinked = false;
|
|
5892
|
+
mutateDashboardConfig(config => {
|
|
5893
|
+
if (!Array.isArray(config.projects)) config.projects = [];
|
|
5894
|
+
alreadyLinked = config.projects.some(p => path.resolve(p.localPath) === target);
|
|
5895
|
+
return config;
|
|
5896
|
+
});
|
|
5897
|
+
if (alreadyLinked) {
|
|
5744
5898
|
return jsonReply(res, 400, { error: 'Project already linked at ' + target });
|
|
5745
5899
|
}
|
|
5746
5900
|
|
|
@@ -5779,8 +5933,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5779
5933
|
// .minions state without leaving repo-local state files behind.
|
|
5780
5934
|
shared.ensureProjectStateFiles(project, { migrateLegacy: true, removeLegacy: true });
|
|
5781
5935
|
|
|
5782
|
-
|
|
5783
|
-
|
|
5936
|
+
let duplicate = false;
|
|
5937
|
+
mutateDashboardConfig(config => {
|
|
5938
|
+
if (!Array.isArray(config.projects)) config.projects = [];
|
|
5939
|
+
if (config.projects.some(p => path.resolve(p.localPath) === target)) {
|
|
5940
|
+
duplicate = true;
|
|
5941
|
+
return config;
|
|
5942
|
+
}
|
|
5943
|
+
config.projects.push(project);
|
|
5944
|
+
return config;
|
|
5945
|
+
});
|
|
5946
|
+
if (duplicate) return jsonReply(res, 400, { error: 'Project already linked at ' + target });
|
|
5784
5947
|
reloadConfig(); // Update in-memory project list immediately
|
|
5785
5948
|
invalidateStatusCache();
|
|
5786
5949
|
|
|
@@ -6181,11 +6344,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6181
6344
|
const wasResume = !!tabSessionId;
|
|
6182
6345
|
const sessionId = tabSessionId || null;
|
|
6183
6346
|
const resumeNeedsCarryover = wasResume && _ccRuntimeNeedsResumeCarryover(currentRuntime);
|
|
6347
|
+
const resumeHasOutOfBandCarryover = wasResume && _transcriptHasCarryoverContext(body.transcript, { outOfBandOnly: true, currentMessage: body.message });
|
|
6184
6348
|
const preamble = wasResume ? '' : buildCCStatePreamble();
|
|
6185
|
-
const
|
|
6349
|
+
const includeFullCarryover = sessionReset || resumeNeedsCarryover;
|
|
6350
|
+
const carryover = (includeFullCarryover || resumeHasOutOfBandCarryover)
|
|
6186
6351
|
? _buildTranscriptCarryover(body.transcript, {
|
|
6187
6352
|
previousRuntime: sessionReset ? previousRuntime : null,
|
|
6188
6353
|
currentMessage: body.message,
|
|
6354
|
+
outOfBandOnly: !includeFullCarryover,
|
|
6189
6355
|
})
|
|
6190
6356
|
: '';
|
|
6191
6357
|
const prompt = _joinCcPromptParts(preamble, carryover, body.message);
|
|
@@ -6358,24 +6524,25 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6358
6524
|
if (!id) id = 'schedule';
|
|
6359
6525
|
}
|
|
6360
6526
|
|
|
6527
|
+
let sched;
|
|
6528
|
+
mutateDashboardConfig(config => {
|
|
6529
|
+
if (!Array.isArray(config.schedules)) config.schedules = [];
|
|
6530
|
+
// If auto-generated ID collides, append a short numeric suffix
|
|
6531
|
+
let scheduleId = id;
|
|
6532
|
+
if (config.schedules.some(s => s.id === scheduleId)) {
|
|
6533
|
+
let suffix = 2;
|
|
6534
|
+
while (config.schedules.some(s => s.id === `${scheduleId}-${suffix}`)) suffix++;
|
|
6535
|
+
scheduleId = `${scheduleId}-${suffix}`;
|
|
6536
|
+
}
|
|
6537
|
+
sched = { id: scheduleId, cron, title, type: type || 'implement', enabled: enabled !== false };
|
|
6538
|
+
if (project) sched.project = project;
|
|
6539
|
+
if (agent) sched.agent = agent;
|
|
6540
|
+
if (description) sched.description = description;
|
|
6541
|
+
if (priority) sched.priority = priority;
|
|
6542
|
+
config.schedules.push(sched);
|
|
6543
|
+
return config;
|
|
6544
|
+
});
|
|
6361
6545
|
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
6546
|
invalidateStatusCache();
|
|
6380
6547
|
return jsonReply(res, 200, { ok: true, schedule: sched });
|
|
6381
6548
|
}
|
|
@@ -6385,21 +6552,28 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6385
6552
|
const { id, cron, title, type, project, agent, description, priority, enabled } = body;
|
|
6386
6553
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
6387
6554
|
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6555
|
+
let missingSchedules = false;
|
|
6556
|
+
let sched = null;
|
|
6557
|
+
mutateDashboardConfig(config => {
|
|
6558
|
+
if (!Array.isArray(config.schedules)) {
|
|
6559
|
+
missingSchedules = true;
|
|
6560
|
+
return config;
|
|
6561
|
+
}
|
|
6562
|
+
sched = config.schedules.find(s => s.id === id);
|
|
6563
|
+
if (!sched) return config;
|
|
6564
|
+
if (cron !== undefined) sched.cron = cron;
|
|
6565
|
+
if (title !== undefined) sched.title = title;
|
|
6566
|
+
if (type !== undefined) sched.type = type;
|
|
6567
|
+
if (project !== undefined) sched.project = project || null;
|
|
6568
|
+
if (agent !== undefined) sched.agent = agent || null;
|
|
6569
|
+
if (description !== undefined) sched.description = description;
|
|
6570
|
+
if (priority !== undefined) sched.priority = priority;
|
|
6571
|
+
if (enabled !== undefined) sched.enabled = enabled;
|
|
6572
|
+
return config;
|
|
6573
|
+
});
|
|
6574
|
+
if (missingSchedules) return jsonReply(res, 404, { error: 'No schedules configured' });
|
|
6391
6575
|
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);
|
|
6576
|
+
reloadConfig();
|
|
6403
6577
|
invalidateStatusCache();
|
|
6404
6578
|
return jsonReply(res, 200, { ok: true, schedule: sched });
|
|
6405
6579
|
}
|
|
@@ -6409,13 +6583,22 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6409
6583
|
const { id } = body;
|
|
6410
6584
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
6411
6585
|
|
|
6586
|
+
let missingSchedules = false;
|
|
6587
|
+
let deleted = false;
|
|
6588
|
+
mutateDashboardConfig(config => {
|
|
6589
|
+
if (!Array.isArray(config.schedules)) {
|
|
6590
|
+
missingSchedules = true;
|
|
6591
|
+
return config;
|
|
6592
|
+
}
|
|
6593
|
+
const idx = config.schedules.findIndex(s => s.id === id);
|
|
6594
|
+
if (idx < 0) return config;
|
|
6595
|
+
config.schedules.splice(idx, 1);
|
|
6596
|
+
deleted = true;
|
|
6597
|
+
return config;
|
|
6598
|
+
});
|
|
6599
|
+
if (missingSchedules) return jsonReply(res, 404, { error: 'No schedules configured' });
|
|
6600
|
+
if (!deleted) return jsonReply(res, 404, { error: 'Schedule not found' });
|
|
6412
6601
|
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
6602
|
invalidateStatusCache();
|
|
6420
6603
|
return jsonReply(res, 200, { ok: true });
|
|
6421
6604
|
}
|
|
@@ -6588,8 +6771,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6588
6771
|
async function handleSettingsUpdate(req, res) {
|
|
6589
6772
|
try {
|
|
6590
6773
|
const body = await readBody(req);
|
|
6591
|
-
const
|
|
6592
|
-
const config = safeJson(configPath) || {};
|
|
6774
|
+
const config = safeJson(CONFIG_PATH) || {};
|
|
6593
6775
|
if (!config.engine) config.engine = {};
|
|
6594
6776
|
if (!config.agents) config.agents = {};
|
|
6595
6777
|
shared.pruneDefaultClaudeConfig(config);
|
|
@@ -6597,6 +6779,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6597
6779
|
const _clamped = [];
|
|
6598
6780
|
const _engineModelDiscovery = require('./engine/model-discovery');
|
|
6599
6781
|
const _engineRuntimes = require('./engine/runtimes');
|
|
6782
|
+
const _configPatch = { engine: { set: {}, delete: new Set() } };
|
|
6783
|
+
function _setEngineConfig(key, value) {
|
|
6784
|
+
config.engine[key] = value;
|
|
6785
|
+
_configPatch.engine.set[key] = value;
|
|
6786
|
+
_configPatch.engine.delete.delete(key);
|
|
6787
|
+
}
|
|
6788
|
+
function _deleteEngineConfig(key) {
|
|
6789
|
+
delete config.engine[key];
|
|
6790
|
+
delete _configPatch.engine.set[key];
|
|
6791
|
+
_configPatch.engine.delete.add(key);
|
|
6792
|
+
}
|
|
6600
6793
|
function _resolveModelForRuntime(modelStr, runtimeName) {
|
|
6601
6794
|
try {
|
|
6602
6795
|
const adapter = _engineRuntimes.resolveRuntime(runtimeName);
|
|
@@ -6630,13 +6823,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6630
6823
|
val = Math.max(min, val);
|
|
6631
6824
|
if (max !== undefined) val = Math.min(max, val);
|
|
6632
6825
|
if (val !== raw) _clamped.push(`${key}: ${raw} → ${val} (range: ${min}–${max || '∞'})`);
|
|
6633
|
-
|
|
6826
|
+
_setEngineConfig(key, val);
|
|
6634
6827
|
}
|
|
6635
6828
|
}
|
|
6636
|
-
|
|
6637
|
-
|
|
6829
|
+
_deleteEngineConfig('adoPollStatusEvery');
|
|
6830
|
+
_deleteEngineConfig('adoPollCommentsEvery');
|
|
6638
6831
|
// String fields
|
|
6639
|
-
if (e.worktreeRoot !== undefined)
|
|
6832
|
+
if (e.worktreeRoot !== undefined) _setEngineConfig('worktreeRoot', String(e.worktreeRoot || D.worktreeRoot));
|
|
6640
6833
|
|
|
6641
6834
|
// ── Runtime fleet (P-7a5c1f8e) ─────────────────────────────────────
|
|
6642
6835
|
// Empty string clears the override — the dashboard's "Default (CLI
|
|
@@ -6653,13 +6846,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6653
6846
|
return _registeredCliNames.length === 0 || _registeredCliNames.includes(String(name));
|
|
6654
6847
|
};
|
|
6655
6848
|
if (e.defaultCli !== undefined) {
|
|
6656
|
-
if (_isClear(e.defaultCli))
|
|
6657
|
-
else if (_validCli(e.defaultCli))
|
|
6849
|
+
if (_isClear(e.defaultCli)) _deleteEngineConfig('defaultCli');
|
|
6850
|
+
else if (_validCli(e.defaultCli)) _setEngineConfig('defaultCli', String(e.defaultCli));
|
|
6658
6851
|
else _clamped.push(`defaultCli: "${e.defaultCli}" not registered (kept previous value)`);
|
|
6659
6852
|
}
|
|
6660
6853
|
if (e.ccCli !== undefined) {
|
|
6661
|
-
if (_isClear(e.ccCli))
|
|
6662
|
-
else if (_validCli(e.ccCli))
|
|
6854
|
+
if (_isClear(e.ccCli)) _deleteEngineConfig('ccCli');
|
|
6855
|
+
else if (_validCli(e.ccCli)) _setEngineConfig('ccCli', String(e.ccCli));
|
|
6663
6856
|
else _clamped.push(`ccCli: "${e.ccCli}" not registered (kept previous value)`);
|
|
6664
6857
|
}
|
|
6665
6858
|
// Validate fleet-level model assignments against the resolved runtime.
|
|
@@ -6695,47 +6888,47 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6695
6888
|
return null;
|
|
6696
6889
|
}
|
|
6697
6890
|
if (e.defaultModel !== undefined) {
|
|
6698
|
-
if (_isClear(e.defaultModel))
|
|
6891
|
+
if (_isClear(e.defaultModel)) _deleteEngineConfig('defaultModel');
|
|
6699
6892
|
else {
|
|
6700
6893
|
const candidate = String(e.defaultModel);
|
|
6701
6894
|
const resolvedCli = config.engine.defaultCli || 'claude';
|
|
6702
6895
|
const rejection = await _validateFleetModel(candidate, resolvedCli);
|
|
6703
6896
|
if (rejection) _clamped.push(`engine.defaultModel: "${candidate}" ${rejection} — kept previous value`);
|
|
6704
|
-
else
|
|
6897
|
+
else _setEngineConfig('defaultModel', candidate);
|
|
6705
6898
|
}
|
|
6706
6899
|
}
|
|
6707
6900
|
if (e.ccModel !== undefined) {
|
|
6708
|
-
if (_isClear(e.ccModel))
|
|
6901
|
+
if (_isClear(e.ccModel)) _deleteEngineConfig('ccModel');
|
|
6709
6902
|
else {
|
|
6710
6903
|
const candidate = String(e.ccModel);
|
|
6711
6904
|
const resolvedCli = config.engine.ccCli || config.engine.defaultCli || 'claude';
|
|
6712
6905
|
const rejection = await _validateFleetModel(candidate, resolvedCli);
|
|
6713
6906
|
if (rejection) _clamped.push(`engine.ccModel: "${candidate}" ${rejection} — kept previous value`);
|
|
6714
|
-
else
|
|
6907
|
+
else _setEngineConfig('ccModel', candidate);
|
|
6715
6908
|
}
|
|
6716
6909
|
}
|
|
6717
6910
|
if (e.claudeFallbackModel !== undefined) {
|
|
6718
|
-
if (_isClear(e.claudeFallbackModel))
|
|
6719
|
-
else
|
|
6911
|
+
if (_isClear(e.claudeFallbackModel)) _deleteEngineConfig('claudeFallbackModel');
|
|
6912
|
+
else _setEngineConfig('claudeFallbackModel', String(e.claudeFallbackModel));
|
|
6720
6913
|
}
|
|
6721
6914
|
if (e.copilotStreamMode !== undefined) {
|
|
6722
6915
|
const valid = ['on', 'off'];
|
|
6723
|
-
if (_isClear(e.copilotStreamMode))
|
|
6724
|
-
else if (valid.includes(e.copilotStreamMode))
|
|
6916
|
+
if (_isClear(e.copilotStreamMode)) _deleteEngineConfig('copilotStreamMode');
|
|
6917
|
+
else if (valid.includes(e.copilotStreamMode)) _setEngineConfig('copilotStreamMode', e.copilotStreamMode);
|
|
6725
6918
|
else _clamped.push(`copilotStreamMode: "${e.copilotStreamMode}" not in [on, off] (kept previous value)`);
|
|
6726
6919
|
}
|
|
6727
6920
|
// maxBudgetUsd uses ?? semantics — 0 is a valid cap (read-only / dry-run agents).
|
|
6728
6921
|
if (e.maxBudgetUsd !== undefined) {
|
|
6729
|
-
if (_isClear(e.maxBudgetUsd))
|
|
6922
|
+
if (_isClear(e.maxBudgetUsd)) _deleteEngineConfig('maxBudgetUsd');
|
|
6730
6923
|
else {
|
|
6731
6924
|
const n = Number(e.maxBudgetUsd);
|
|
6732
|
-
if (Number.isFinite(n) && n >= 0)
|
|
6925
|
+
if (Number.isFinite(n) && n >= 0) _setEngineConfig('maxBudgetUsd', n);
|
|
6733
6926
|
else _clamped.push(`maxBudgetUsd: "${e.maxBudgetUsd}" must be ≥ 0 (kept previous value)`);
|
|
6734
6927
|
}
|
|
6735
6928
|
}
|
|
6736
6929
|
if (e.ccEffort !== undefined) {
|
|
6737
6930
|
const valid = [null, 'low', 'medium', 'high'];
|
|
6738
|
-
|
|
6931
|
+
_setEngineConfig('ccEffort', valid.includes(e.ccEffort) ? e.ccEffort : null);
|
|
6739
6932
|
}
|
|
6740
6933
|
// Per-type max turns
|
|
6741
6934
|
if (e.maxTurnsByType !== undefined && typeof e.maxTurnsByType === 'object') {
|
|
@@ -6744,18 +6937,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6744
6937
|
const n = Number(val);
|
|
6745
6938
|
if (n && n >= 5 && n <= 500) mbt[type] = n;
|
|
6746
6939
|
}
|
|
6747
|
-
|
|
6940
|
+
_setEngineConfig('maxTurnsByType', mbt);
|
|
6748
6941
|
}
|
|
6749
6942
|
// Boolean fields
|
|
6750
6943
|
const booleanFields = Object.keys(shared.ENGINE_DEFAULTS).filter(k => typeof shared.ENGINE_DEFAULTS[k] === 'boolean');
|
|
6751
6944
|
for (const key of booleanFields) {
|
|
6752
|
-
if (e[key] !== undefined)
|
|
6945
|
+
if (e[key] !== undefined) _setEngineConfig(key, !!e[key]);
|
|
6753
6946
|
}
|
|
6754
6947
|
// Eval loop settings
|
|
6755
|
-
if (e.evalMaxIterations !== undefined)
|
|
6756
|
-
if (e.evalMaxCost !== undefined)
|
|
6948
|
+
if (e.evalMaxIterations !== undefined) _setEngineConfig('evalMaxIterations', Math.max(1, Math.min(10, Number(e.evalMaxIterations) || D.evalMaxIterations)));
|
|
6949
|
+
if (e.evalMaxCost !== undefined) _setEngineConfig('evalMaxCost', e.evalMaxCost === null || e.evalMaxCost === '' ? null : Math.max(0, Number(e.evalMaxCost) || 0));
|
|
6757
6950
|
if (e.ignoredCommentAuthors !== undefined) {
|
|
6758
|
-
|
|
6951
|
+
_setEngineConfig('ignoredCommentAuthors', String(e.ignoredCommentAuthors || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean));
|
|
6759
6952
|
}
|
|
6760
6953
|
}
|
|
6761
6954
|
|
|
@@ -6892,7 +7085,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6892
7085
|
}
|
|
6893
7086
|
|
|
6894
7087
|
shared.pruneDefaultClaudeConfig(config);
|
|
6895
|
-
|
|
7088
|
+
mutateDashboardConfig(current => mergeSettingsConfigUpdate(current, config, body, _configPatch));
|
|
6896
7089
|
// Refresh in-memory CONFIG so subsequent reads see the update
|
|
6897
7090
|
reloadConfig();
|
|
6898
7091
|
invalidateStatusCache();
|
|
@@ -6915,11 +7108,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6915
7108
|
|
|
6916
7109
|
async function handleSettingsReset(req, res) {
|
|
6917
7110
|
try {
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
7111
|
+
mutateDashboardConfig(config => {
|
|
7112
|
+
config.engine = { ...shared.ENGINE_DEFAULTS };
|
|
7113
|
+
delete config.claude;
|
|
7114
|
+
config.agents = { ...shared.DEFAULT_AGENTS };
|
|
7115
|
+
return config;
|
|
7116
|
+
});
|
|
6923
7117
|
reloadConfig();
|
|
6924
7118
|
invalidateStatusCache();
|
|
6925
7119
|
return jsonReply(res, 200, { ok: true });
|
|
@@ -7189,18 +7383,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7189
7383
|
|
|
7190
7384
|
// Pinned notes
|
|
7191
7385
|
{ method: 'GET', path: '/api/pinned', desc: 'Get pinned notes', handler: async (req, res) => {
|
|
7192
|
-
const content = safeRead(
|
|
7386
|
+
const content = safeRead(PINNED_PATH);
|
|
7193
7387
|
return jsonReply(res, 200, { content, entries: parsePinnedEntries(content) });
|
|
7194
7388
|
}},
|
|
7195
7389
|
{ method: 'POST', path: '/api/pinned', desc: 'Add a pinned note', params: 'title, content, level?', handler: async (req, res) => {
|
|
7196
7390
|
const body = await readBody(req);
|
|
7197
7391
|
const { title, content, level } = body;
|
|
7198
7392
|
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);
|
|
7393
|
+
addPinnedEntryLocked({ title, content, level });
|
|
7204
7394
|
// pinned.md is in slow-state cache — opt-in invalidation so the new entry is visible immediately (closes #1295)
|
|
7205
7395
|
invalidateStatusCache({ includeSlow: true });
|
|
7206
7396
|
return jsonReply(res, 200, { ok: true });
|
|
@@ -7209,12 +7399,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7209
7399
|
const body = await readBody(req);
|
|
7210
7400
|
const { title } = body;
|
|
7211
7401
|
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);
|
|
7402
|
+
if (!removePinnedEntryLocked(title)) return jsonReply(res, 404, { error: 'No pinned notes' });
|
|
7218
7403
|
// pinned.md is in slow-state cache — opt-in invalidation so the unpin is visible immediately
|
|
7219
7404
|
invalidateStatusCache({ includeSlow: true });
|
|
7220
7405
|
return jsonReply(res, 200, { ok: true });
|
|
@@ -7222,24 +7407,20 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7222
7407
|
|
|
7223
7408
|
// KB pin state (server-side so CC can pin items)
|
|
7224
7409
|
{ method: 'GET', path: '/api/kb-pins', desc: 'Get pinned KB item keys', handler: async (req, res) => {
|
|
7225
|
-
const pins = shared.safeJson(
|
|
7410
|
+
const pins = shared.safeJson(KB_PINS_PATH) || [];
|
|
7226
7411
|
return jsonReply(res, 200, { pins });
|
|
7227
7412
|
}},
|
|
7228
7413
|
{ method: 'POST', path: '/api/kb-pins', desc: 'Set pinned KB item keys', params: 'pins[]', handler: async (req, res) => {
|
|
7229
7414
|
const body = await readBody(req);
|
|
7230
7415
|
if (!Array.isArray(body.pins)) return jsonReply(res, 400, { error: 'pins array required' });
|
|
7231
|
-
|
|
7416
|
+
setKbPinsLocked(body.pins);
|
|
7232
7417
|
return jsonReply(res, 200, { ok: true });
|
|
7233
7418
|
}},
|
|
7234
7419
|
{ method: 'POST', path: '/api/kb-pins/toggle', desc: 'Toggle a single KB pin', params: 'key', handler: async (req, res) => {
|
|
7235
7420
|
const body = await readBody(req);
|
|
7236
7421
|
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 });
|
|
7422
|
+
const pinned = toggleKbPinLocked(body.key);
|
|
7423
|
+
return jsonReply(res, 200, { ok: true, pinned });
|
|
7243
7424
|
}},
|
|
7244
7425
|
|
|
7245
7426
|
// Notes
|
|
@@ -7479,8 +7660,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7479
7660
|
{ method: 'GET', path: /^\/api\/knowledge\/([^/]+)\/([^?]+)/, template: '/api/knowledge/:category/:file', desc: 'Read a specific knowledge base entry', handler: handleKnowledgeRead },
|
|
7480
7661
|
|
|
7481
7662
|
// Doc chat
|
|
7482
|
-
{ method: 'POST', path: '/api/doc-chat', desc: 'Minions-aware doc Q&A + editing via CC session', params: 'message, document, title?, filePath?, selection?, contentHash?', handler: handleDocChat },
|
|
7483
|
-
{ method: 'POST', path: '/api/doc-chat/stream', desc: 'Streaming doc chat — SSE with text chunks and tool progress', params: 'message, document, title?, filePath?, selection?, contentHash?', handler: handleDocChatStream },
|
|
7663
|
+
{ method: 'POST', path: '/api/doc-chat', desc: 'Minions-aware doc Q&A + editing via CC session', params: 'message, document, title?, filePath?, selection?, contentHash?, transcript?', handler: handleDocChat },
|
|
7664
|
+
{ method: 'POST', path: '/api/doc-chat/stream', desc: 'Streaming doc chat — SSE with text chunks and tool progress', params: 'message, document, title?, filePath?, selection?, contentHash?, transcript?', handler: handleDocChatStream },
|
|
7484
7665
|
|
|
7485
7666
|
// Inbox
|
|
7486
7667
|
{ method: 'POST', path: '/api/inbox/persist', desc: 'Promote an inbox item to team notes', params: 'name', handler: handleInboxPersist },
|
|
@@ -7504,8 +7685,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7504
7685
|
// Command Center
|
|
7505
7686
|
{ method: 'POST', path: '/api/command-center/new-session', desc: 'Clear active CC session', handler: handleCommandCenterNewSession },
|
|
7506
7687
|
{ method: 'POST', path: '/api/command-center/abort', desc: 'Abort an in-flight CC request for a tab', params: 'tabId?', handler: handleCommandCenterAbort },
|
|
7507
|
-
{ method: 'POST', path: '/api/command-center', desc: 'Conversational command center with full minions context', params: 'message, sessionId?', handler: handleCommandCenter },
|
|
7508
|
-
{ method: 'POST', path: '/api/command-center/stream', desc: 'Streaming CC — SSE with text chunks as they arrive', params: 'message, tabId?', handler: handleCommandCenterStream },
|
|
7688
|
+
{ method: 'POST', path: '/api/command-center', desc: 'Conversational command center with full minions context', params: 'message, tabId?, sessionId?, transcript?', handler: handleCommandCenter },
|
|
7689
|
+
{ method: 'POST', path: '/api/command-center/stream', desc: 'Streaming CC — SSE with text chunks as they arrive', params: 'message, tabId?, sessionId?, transcript?', handler: handleCommandCenterStream },
|
|
7509
7690
|
{ method: 'GET', path: '/api/cc-sessions', desc: 'List CC session metadata for all tabs', handler: handleCCSessionsList },
|
|
7510
7691
|
{ method: 'DELETE', path: /^\/api\/cc-sessions\/([\w-]+)$/, template: '/api/cc-sessions/:id', desc: 'Delete a CC session by tab ID', handler: handleCCSessionDelete },
|
|
7511
7692
|
|
|
@@ -7909,6 +8090,7 @@ module.exports = {
|
|
|
7909
8090
|
buildCCStatePreamble,
|
|
7910
8091
|
_routesAsMeta,
|
|
7911
8092
|
_buildTranscriptCarryover,
|
|
8093
|
+
_transcriptHasCarryoverContext,
|
|
7912
8094
|
_ccRuntimeNeedsResumeCarryover,
|
|
7913
8095
|
_joinCcPromptParts,
|
|
7914
8096
|
_captureApiRoutesMeta,
|
|
@@ -7916,6 +8098,7 @@ module.exports = {
|
|
|
7916
8098
|
_formatCcCliCommandsIndex,
|
|
7917
8099
|
_resetPreambleCache,
|
|
7918
8100
|
_installCrashHandlers,
|
|
8101
|
+
_mergeSettingsConfigUpdate: mergeSettingsConfigUpdate,
|
|
7919
8102
|
};
|
|
7920
8103
|
|
|
7921
8104
|
// 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.1779",
|
|
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
|
@@ -85,13 +85,13 @@ I'll dispatch dallas to fix that bug.
|
|
|
85
85
|
|
|
86
86
|
**Required fields per action type — server rejects with an error if missing:**
|
|
87
87
|
|
|
88
|
-
- `dispatch
|
|
88
|
+
- `dispatch`: `title` is REQUIRED. `description` recommended. `project` REQUIRED when multiple projects are configured (server returns the list of known names if you guess wrong). For agent hints emit either `agents: ["dallas"]` (array, preferred) or `agent: "dallas"` (string — auto-promoted server-side). Unknown agent names error. Always emit `"type":"dispatch"` for dispatch-like work and preserve the semantic intent in `workType` (`fix`, `implement`, `explore`, `review`, or `test`) instead of using those words as action types.
|
|
89
89
|
- `build-and-test`: `pr` REQUIRED (number, ID, or URL).
|
|
90
90
|
- `note`: `title` and `content` (or `description`) REQUIRED.
|
|
91
91
|
- `knowledge`: `title`, `content`, and `category` REQUIRED. Valid categories: architecture, conventions, project-notes, build-reports, reviews.
|
|
92
92
|
|
|
93
93
|
Core action types:
|
|
94
|
-
- **dispatch**: title (REQUIRED), workType, priority (low/medium/high), agents[] or agent (optional — both shapes accepted), project (REQUIRED when multi-project unless `pr` resolves to a tracked PR), description, pr (optional PR number/id/url for work that targets an existing PR), scope (`"fan-out"` only when the user explicitly asks to fan out to all agents)
|
|
94
|
+
- **dispatch**: title (REQUIRED), workType, priority (low/medium/high), agents[] or agent (optional — both shapes accepted), project (REQUIRED when multi-project unless `pr` resolves to a tracked PR), description, pr (optional PR number/id/url for work that targets an existing PR), scope (`"fan-out"` only when the user explicitly asks to fan out to all agents). Do not emit `type:"fix"` or `type:"implement"`; emit `type:"dispatch"` with `workType:"fix"` or `workType:"implement"`.
|
|
95
95
|
workTypes: `explore` (research/report only, NO PR), `ask` (answer/report, NO PR), `implement` (new code, PR REQUIRED), `fix` (standalone bug fix creates a PR; include `pr` when fixing review comments/build failures on an existing PR), `review` (code review, NO PR), `test` (tests, PR if new), `verify` (merge/build/maintenance, NO PR)
|
|
96
96
|
If the user wants a design/architecture artifact committed through a PR, dispatch `implement` or `docs` rather than `explore`.
|
|
97
97
|
When the user names a specific agent ("assign this to lambert"), put exactly that one name in `agents` (e.g. `"agents": ["lambert"]`). A single-agent assignment is hard-pinned by the server — it will queue for that agent only and skip the routing table. If the user explicitly asks for fan-out/all agents, set `scope: "fan-out"`.
|